mail/listing.rs: add RowsState struct

Keep state of rows in lists in this struct to reduce code duplication in
list implementations
pull/168/head
Manos Pitsidianakis 2022-11-07 16:35:21 +02:00
parent b776409d6c
commit cc439b239a
6 changed files with 1181 additions and 852 deletions

View File

@ -46,6 +46,156 @@ pub const DEFAULT_SELECTED_FLAG: &str = "☑️";
pub const DEFAULT_UNSEEN_FLAG: &str = "";
pub const DEFAULT_SNOOZED_FLAG: &str = "💤";
#[derive(Debug, Default)]
pub struct RowsState<T> {
pub selection: HashMap<EnvelopeHash, bool>,
pub row_updates: SmallVec<[EnvelopeHash; 8]>,
pub thread_to_env: HashMap<ThreadHash, SmallVec<[EnvelopeHash; 8]>>,
pub env_to_thread: HashMap<EnvelopeHash, ThreadHash>,
pub thread_order: HashMap<ThreadHash, usize>,
pub env_order: HashMap<EnvelopeHash, usize>,
#[allow(clippy::type_complexity)]
pub entries: Vec<(T, EntryStrings)>,
pub all_threads: HashSet<ThreadHash>,
pub all_envelopes: HashSet<EnvelopeHash>,
}
impl<T> RowsState<T> {
#[inline(always)]
pub fn clear(&mut self) {
self.selection.clear();
self.row_updates.clear();
self.thread_to_env.clear();
self.env_to_thread.clear();
self.thread_order.clear();
self.env_order.clear();
self.entries.clear();
self.all_threads.clear();
self.all_envelopes.clear();
}
#[inline(always)]
pub fn is_thread_selected(&self, thread: ThreadHash) -> bool {
debug_assert!(self.all_threads.contains(&thread));
debug_assert!(self.thread_order.contains_key(&thread));
debug_assert!(self.thread_to_env.contains_key(&thread));
self.thread_to_env
.get(&thread)
.iter()
.map(|v| v.iter())
.flatten()
.any(|env_hash| self.selection[env_hash])
}
#[inline(always)]
pub fn insert_thread(
&mut self,
thread: ThreadHash,
metadata: T,
env_hashes: SmallVec<[EnvelopeHash; 8]>,
entry_strings: EntryStrings,
) {
let index = self.entries.len();
self.thread_order.insert(thread, index);
self.all_threads.insert(thread);
for &env_hash in &env_hashes {
self.selection.insert(env_hash, false);
self.env_to_thread.insert(env_hash, thread);
self.env_order.insert(env_hash, index);
self.all_envelopes.insert(env_hash);
}
self.thread_to_env.insert(thread, env_hashes);
self.entries.push((metadata, entry_strings));
}
#[inline(always)]
pub fn row_update_add_thread(&mut self, thread: ThreadHash) {
let env_hashes = self.thread_to_env.entry(thread).or_default().clone();
for env_hash in env_hashes {
self.row_updates.push(env_hash);
}
}
#[inline(always)]
pub fn row_update_add_envelope(&mut self, env_hash: EnvelopeHash) {
self.row_updates.push(env_hash);
}
#[inline(always)]
pub fn contains_thread(&self, thread: ThreadHash) -> bool {
debug_assert_eq!(
self.all_threads.contains(&thread),
self.thread_order.contains_key(&thread)
);
debug_assert_eq!(
self.thread_order.contains_key(&thread),
self.thread_to_env.contains_key(&thread)
);
self.thread_order.contains_key(&thread)
}
#[inline(always)]
pub fn contains_env(&self, env_hash: EnvelopeHash) -> bool {
self.all_envelopes.contains(&env_hash)
}
#[inline(always)]
pub fn update_selection_with_thread(
&mut self,
thread: ThreadHash,
mut cl: impl FnMut(&mut bool),
) {
let env_hashes = self.thread_to_env.entry(thread).or_default().clone();
for env_hash in env_hashes {
self.selection.entry(env_hash).and_modify(&mut cl);
self.row_updates.push(env_hash);
}
}
#[inline(always)]
pub fn update_selection_with_env(
&mut self,
env_hash: EnvelopeHash,
mut cl: impl FnMut(&mut bool),
) {
self.selection.entry(env_hash).and_modify(&mut cl);
self.row_updates.push(env_hash);
}
#[inline(always)]
pub fn len(&self) -> usize {
self.entries.len()
}
#[inline(always)]
pub fn clear_selection(&mut self) {
for (k, v) in self.selection.iter_mut() {
if *v {
*v = false;
self.row_updates.push(*k);
}
}
}
pub fn rename_env(&mut self, old_hash: EnvelopeHash, new_hash: EnvelopeHash) {
self.row_updates.push(new_hash);
if let Some(row) = self.env_order.remove(&old_hash) {
self.env_order.insert(new_hash, row);
}
if let Some(thread) = self.env_to_thread.remove(&old_hash) {
self.thread_to_env
.entry(thread)
.or_default()
.retain(|h| *h != old_hash);
self.thread_to_env.entry(thread).or_default().push(new_hash);
}
let selection_status = self.selection.remove(&old_hash).unwrap_or(false);
self.selection.insert(new_hash, selection_status);
self.all_envelopes.remove(&old_hash);
self.all_envelopes.insert(old_hash);
}
}
mod conversations;
pub use self::conversations::*;
@ -116,12 +266,12 @@ struct ColorCache {
}
#[derive(Debug)]
pub(super) struct EntryStrings {
pub(super) date: DateString,
pub(super) subject: SubjectString,
pub(super) flag: FlagString,
pub(super) from: FromString,
pub(super) tags: TagString,
pub struct EntryStrings {
pub date: DateString,
pub subject: SubjectString,
pub flag: FlagString,
pub from: FromString,
pub tags: TagString,
}
#[macro_export]
@ -146,7 +296,7 @@ macro_rules! column_str {
(
struct $name:ident($($t:ty),+)) => {
#[derive(Debug)]
pub(super) struct $name($(pub $t),+);
pub struct $name($(pub $t),+);
impl Deref for $name {
type Target = String;
@ -190,14 +340,13 @@ pub trait MailListingTrait: ListingTrait {
fn perform_action(
&mut self,
context: &mut Context,
thread_hashes: SmallVec<[ThreadHash; 8]>,
envs_to_set: SmallVec<[EnvelopeHash; 8]>,
a: &ListingAction,
) {
let account_hash = self.coordinates().0;
let account = &mut context.accounts[&account_hash];
let mut envs_to_set: SmallVec<[EnvelopeHash; 8]> = SmallVec::new();
let mailbox_hash = self.coordinates().1;
{
/*{
let threads_lck = account.collection.get_threads(mailbox_hash);
for thread_hash in thread_hashes {
for (_, h) in threads_lck.thread_group_iter(thread_hash) {
@ -206,10 +355,12 @@ pub trait MailListingTrait: ListingTrait {
self.row_updates().push(thread_hash);
}
}
if envs_to_set.is_empty() {
*/
let env_hashes = if let Ok(batch) = EnvelopeHashBatch::try_from(envs_to_set.as_slice()) {
batch
} else {
return;
}
let env_hashes = EnvelopeHashBatch::try_from(envs_to_set.as_slice()).unwrap();
};
match a {
ListingAction::SetSeen => {
let job = account.backend.write().unwrap().set_flags(
@ -484,9 +635,9 @@ pub trait MailListingTrait: ListingTrait {
self.set_dirty(true);
}
fn row_updates(&mut self) -> &mut SmallVec<[ThreadHash; 8]>;
fn selection(&mut self) -> &mut HashMap<ThreadHash, bool>;
fn get_focused_items(&self, _context: &Context) -> SmallVec<[ThreadHash; 8]>;
fn row_updates(&mut self) -> &mut SmallVec<[EnvelopeHash; 8]>;
fn selection(&mut self) -> &mut HashMap<EnvelopeHash, bool>;
fn get_focused_items(&self, _context: &Context) -> SmallVec<[EnvelopeHash; 8]>;
fn redraw_threads_list(
&mut self,
context: &Context,
@ -517,11 +668,9 @@ pub trait ListingTrait: Component {
) {
}
fn unfocused(&self) -> bool;
fn set_modifier_active(&mut self, _new_val: bool) {}
fn set_modifier_command(&mut self, _new_val: Option<Modifier>) {}
fn modifier_command(&self) -> Option<Modifier> {
None
}
fn set_modifier_active(&mut self, _new_val: bool);
fn set_modifier_command(&mut self, _new_val: Option<Modifier>);
fn modifier_command(&self) -> Option<Modifier>;
fn set_movement(&mut self, mvm: PageMovement);
fn focus(&self) -> Focus;
fn set_focus(&mut self, new_value: Focus, context: &mut Context);
@ -1161,13 +1310,14 @@ impl Component for Listing {
| Action::Listing(a @ ListingAction::Tag(_)) => {
let focused = self.component.get_focused_items(context);
self.component.perform_action(context, focused, a);
let mut row_updates: SmallVec<[ThreadHash; 8]> = SmallVec::new();
let mut row_updates: SmallVec<[EnvelopeHash; 8]> = SmallVec::new();
for (k, v) in self.component.selection().iter_mut() {
if *v {
*v = false;
row_updates.push(*k);
}
}
self.component.row_updates().extend(row_updates.into_iter());
}
_ => {}
},

File diff suppressed because it is too large Load Diff

View File

@ -100,24 +100,20 @@ pub struct ConversationsListing {
length: usize,
sort: (SortField, SortOrder),
subsort: (SortField, SortOrder),
all_threads: HashSet<ThreadHash>,
order: HashMap<ThreadHash, usize>,
#[allow(clippy::type_complexity)]
rows: std::result::Result<Vec<((usize, (ThreadHash, EnvelopeHash)), EntryStrings)>, String>,
rows: RowsState<(ThreadHash, EnvelopeHash)>,
error: std::result::Result<(), String>,
#[allow(clippy::type_complexity)]
search_job: Option<(String, JoinHandle<Result<SmallVec<[EnvelopeHash; 512]>>>)>,
filter_term: String,
filtered_selection: Vec<ThreadHash>,
filtered_order: HashMap<ThreadHash, usize>,
selection: HashMap<ThreadHash, bool>,
/// If we must redraw on next redraw event
dirty: bool,
force_draw: bool,
/// If `self.view` exists or not.
focus: Focus,
view: ThreadView,
row_updates: SmallVec<[ThreadHash; 8]>,
color_cache: ColorCache,
movement: Option<PageMovement>,
@ -127,30 +123,46 @@ pub struct ConversationsListing {
}
impl MailListingTrait for ConversationsListing {
fn row_updates(&mut self) -> &mut SmallVec<[ThreadHash; 8]> {
&mut self.row_updates
fn row_updates(&mut self) -> &mut SmallVec<[EnvelopeHash; 8]> {
&mut self.rows.row_updates
}
fn selection(&mut self) -> &mut HashMap<ThreadHash, bool> {
&mut self.selection
fn selection(&mut self) -> &mut HashMap<EnvelopeHash, bool> {
&mut self.rows.selection
}
fn get_focused_items(&self, _context: &Context) -> SmallVec<[ThreadHash; 8]> {
let is_selection_empty = self.selection.values().cloned().any(std::convert::identity);
let i = [self.get_thread_under_cursor(self.cursor_pos.2)];
fn get_focused_items(&self, _context: &Context) -> SmallVec<[EnvelopeHash; 8]> {
let is_selection_empty = !self
.rows
.selection
.values()
.cloned()
.any(std::convert::identity);
let cursor_iter;
let sel_iter = if is_selection_empty {
let sel_iter = if !is_selection_empty {
cursor_iter = None;
Some(self.selection.iter().filter(|(_, v)| **v).map(|(k, _)| k))
Some(
self.rows
.selection
.iter()
.filter(|(_, v)| **v)
.map(|(k, _)| *k),
)
} else {
cursor_iter = Some(i.iter());
if let Some(env_hashes) = self
.get_thread_under_cursor(self.cursor_pos.2)
.and_then(|thread| self.rows.thread_to_env.get(&thread).map(|v| v.clone()))
{
cursor_iter = Some(env_hashes.into_iter());
} else {
cursor_iter = None;
}
None
};
let iter = sel_iter
.into_iter()
.flatten()
.chain(cursor_iter.into_iter().flatten())
.cloned();
.chain(cursor_iter.into_iter().flatten());
SmallVec::from_iter(iter)
}
@ -192,7 +204,7 @@ impl MailListingTrait for ConversationsListing {
Err(_) => {
let message: String =
context.accounts[&self.cursor_pos.0][&self.cursor_pos.1].status();
self.rows = Err(message);
self.error = Err(message);
return;
}
}
@ -200,7 +212,6 @@ impl MailListingTrait for ConversationsListing {
let threads = context.accounts[&self.cursor_pos.0]
.collection
.get_threads(self.cursor_pos.1);
self.all_threads.clear();
let mut roots = threads.roots();
threads.group_inner_sort_by(
&mut roots,
@ -217,9 +228,9 @@ impl MailListingTrait for ConversationsListing {
{
self.view.update(context);
} else if self.unfocused() {
let thread_group = self.get_thread_under_cursor(self.cursor_pos.2);
self.view = ThreadView::new(self.new_cursor_pos, thread_group, None, context);
if let Some(thread_group) = self.get_thread_under_cursor(self.cursor_pos.2) {
self.view = ThreadView::new(self.new_cursor_pos, thread_group, None, context);
}
}
}
@ -231,13 +242,10 @@ impl MailListingTrait for ConversationsListing {
let account = &context.accounts[&self.cursor_pos.0];
let threads = account.collection.get_threads(self.cursor_pos.1);
self.order.clear();
self.selection.clear();
self.rows.clear();
self.length = 0;
if self.rows.is_err() {
self.rows = Ok(vec![]);
} else {
self.rows.as_mut().unwrap().clear();
if self.error.is_err() {
self.error = Ok(());
}
let mut max_entry_columns = 0;
@ -343,20 +351,23 @@ impl MailListingTrait for ConversationsListing {
max_entry_columns,
strings.date.len() + 1 + strings.from.grapheme_width(),
);
self.rows
.as_mut()
.unwrap()
.push(((self.length, (thread, root_env_hash)), strings));
self.all_threads.insert(thread);
self.order.insert(thread, self.length);
self.selection.insert(thread, false);
self.rows.insert_thread(
thread,
(thread, root_env_hash),
threads
.thread_to_envelope
.get(&thread)
.cloned()
.unwrap_or_default()
.into(),
strings,
);
self.length += 1;
}
if self.length == 0 && self.filter_term.is_empty() {
let message: String = account[&self.cursor_pos.1].status();
self.rows = Err(message);
self.error = Err(message);
}
}
}
@ -373,7 +384,7 @@ impl ListingTrait for ConversationsListing {
self.filtered_selection.clear();
self.filtered_order.clear();
self.filter_term.clear();
self.row_updates.clear();
self.rows.clear();
}
fn highlight_line(&mut self, grid: &mut CellBuffer, area: Area, idx: usize, context: &Context) {
@ -391,7 +402,7 @@ impl ListingTrait for ConversationsListing {
}
let upper_left = upper_left!(area);
let bottom_right = bottom_right!(area);
if let Err(message) = self.rows.as_ref() {
if let Err(message) = self.error.as_ref() {
clear_area(grid, area, self.color_cache.theme_default);
write_string_to_grid(
message,
@ -503,16 +514,10 @@ impl ListingTrait for ConversationsListing {
return;
}
self.order.clear();
self.selection.clear();
self.length = 0;
self.filtered_selection.clear();
self.filtered_order.clear();
self.filter_term = filter_term;
self.row_updates.clear();
for v in self.selection.values_mut() {
*v = false;
}
let account = &context.accounts[&self.cursor_pos.0];
let threads = account.collection.get_threads(self.cursor_pos.1);
@ -528,7 +533,7 @@ impl ListingTrait for ConversationsListing {
if self.filtered_order.contains_key(&thread) {
continue;
}
if self.all_threads.contains(&thread) {
if self.rows.all_threads.contains(&thread) {
self.filtered_selection.push(thread);
self.filtered_order
.insert(thread, self.filtered_selection.len().saturating_sub(1));
@ -579,7 +584,7 @@ impl ListingTrait for ConversationsListing {
self.view
.process_event(&mut UIEvent::VisibilityChange(false), context);
self.dirty = true;
/* If self.row_updates is not empty and we exit a thread, the row_update events
/* If self.rows.row_updates is not empty and we exit a thread, the row_update events
* will be performed but the list will not be drawn. So force a draw in any case.
* */
self.force_draw = true;
@ -618,15 +623,12 @@ impl ConversationsListing {
length: 0,
sort: (Default::default(), Default::default()),
subsort: (SortField::Date, SortOrder::Desc),
order: HashMap::default(),
all_threads: HashSet::default(),
rows: RowsState::default(),
error: Ok(()),
search_job: None,
filter_term: String::new(),
filtered_selection: Vec::new(),
filtered_order: HashMap::default(),
selection: HashMap::default(),
row_updates: SmallVec::new(),
rows: Ok(Vec::with_capacity(1024)),
dirty: true,
force_draw: true,
focus: Focus::None,
@ -765,28 +767,23 @@ impl ConversationsListing {
}
}
fn get_thread_under_cursor(&self, cursor: usize) -> ThreadHash {
fn get_thread_under_cursor(&self, cursor: usize) -> Option<ThreadHash> {
if self.filter_term.is_empty() {
*self
.order
self.rows
.thread_order
.iter()
.find(|(_, &r)| r == cursor)
.unwrap_or_else(|| {
debug!("self.order empty ? cursor={} {:#?}", cursor, &self.order);
panic!();
})
.0
.map(|(k, _)| *k)
} else {
self.filtered_selection[cursor]
self.filtered_selection.get(cursor).cloned()
}
}
fn update_line(&mut self, context: &Context, thread_hash: ThreadHash) {
fn update_line(&mut self, context: &Context, env_hash: EnvelopeHash) {
let account = &context.accounts[&self.cursor_pos.0];
let thread_hash = self.rows.env_to_thread[&env_hash];
let threads = account.collection.get_threads(self.cursor_pos.1);
let thread_node_hash = threads.thread_group_iter(thread_hash).next().unwrap().1;
let idx: usize = self.order[&thread_hash];
let env_hash = threads.thread_nodes()[&thread_node_hash].message().unwrap();
let idx: usize = self.rows.thread_order[&thread_hash];
let mut other_subjects = IndexSet::new();
let mut from_address_list = Vec::new();
@ -829,22 +826,18 @@ impl ConversationsListing {
thread_hash,
);
drop(envelope);
if let Ok(rows) = self.rows.as_mut() {
if let Some(row) = rows.get_mut(idx) {
row.1 = strings;
}
if let Some(row) = self.rows.entries.get_mut(idx) {
row.1 = strings;
}
}
fn draw_rows(&self, grid: &mut CellBuffer, area: Area, context: &Context, top_idx: usize) {
let rows_ref = match self.rows.as_ref() {
Ok(rows) => rows,
Err(_) => return,
};
let account = &context.accounts[&self.cursor_pos.0];
let threads = account.collection.get_threads(self.cursor_pos.1);
let (mut upper_left, bottom_right) = area;
for ((idx, (thread_hash, root_env_hash)), strings) in rows_ref.iter().skip(top_idx) {
for (idx, ((thread_hash, root_env_hash), strings)) in
self.rows.entries.iter().enumerate().skip(top_idx)
{
if !context.accounts[&self.cursor_pos.0].contains_key(*root_env_hash) {
panic!();
}
@ -853,8 +846,8 @@ impl ConversationsListing {
let row_attr = row_attr!(
self.color_cache,
thread.unseen() > 0,
self.cursor_pos.2 == *idx,
self.selection[thread_hash]
self.cursor_pos.2 == idx,
self.rows.is_thread_selected(*thread_hash)
);
/* draw flags */
let (x, _) = write_string_to_grid(
@ -873,8 +866,8 @@ impl ConversationsListing {
subject,
self.color_cache,
thread.unseen() > 0,
self.cursor_pos.2 == *idx,
self.selection[thread_hash]
self.cursor_pos.2 == idx,
self.rows.is_thread_selected(*thread_hash)
);
/* draw subject */
let (mut x, _) = write_string_to_grid(
@ -919,8 +912,8 @@ impl ConversationsListing {
date,
self.color_cache,
thread.unseen() > 0,
self.cursor_pos.2 == *idx,
self.selection[thread_hash]
self.cursor_pos.2 == idx,
self.rows.is_thread_selected(*thread_hash)
);
upper_left.1 += 1;
if upper_left.1 >= bottom_right.1 {
@ -946,8 +939,8 @@ impl ConversationsListing {
from,
self.color_cache,
thread.unseen() > 0,
self.cursor_pos.2 == *idx,
self.selection[thread_hash]
self.cursor_pos.2 == idx,
self.rows.is_thread_selected(*thread_hash)
);
/* draw from */
let (x, _) = write_string_to_grid(
@ -1025,28 +1018,28 @@ impl Component for ConversationsListing {
for c in self.new_cursor_pos.2.saturating_sub(*amount)
..=self.new_cursor_pos.2
{
let thread = self.get_thread_under_cursor(c);
match modifier {
Modifier::SymmetricDifference => {
self.selection.entry(thread).and_modify(|e| *e = !*e);
}
Modifier::Union => {
self.selection.entry(thread).and_modify(|e| *e = true);
}
Modifier::Difference => {
self.selection.entry(thread).and_modify(|e| *e = false);
}
Modifier::Intersection => {}
if let Some(thread) = self.get_thread_under_cursor(c) {
self.rows.update_selection_with_thread(
thread,
match modifier {
Modifier::SymmetricDifference => {
|e: &mut bool| *e = !*e
}
Modifier::Union => |e: &mut bool| *e = true,
Modifier::Difference => |e: &mut bool| *e = false,
Modifier::Intersection => |_: &mut bool| {},
},
);
}
self.row_updates.push(thread);
}
if modifier == Modifier::Intersection {
for c in (0..self.new_cursor_pos.2.saturating_sub(*amount))
.chain((self.new_cursor_pos.2 + 2)..self.length)
{
let thread = self.get_thread_under_cursor(c);
self.selection.entry(thread).and_modify(|e| *e = false);
self.row_updates.push(thread);
if let Some(thread) = self.get_thread_under_cursor(c) {
self.rows
.update_selection_with_thread(thread, |e| *e = false);
}
}
}
}
@ -1054,49 +1047,48 @@ impl Component for ConversationsListing {
for c in self.new_cursor_pos.2.saturating_sub(rows * multiplier)
..=self.new_cursor_pos.2
{
let thread = self.get_thread_under_cursor(c);
match modifier {
Modifier::SymmetricDifference => {
self.selection.entry(thread).and_modify(|e| *e = !*e);
}
Modifier::Union => {
self.selection.entry(thread).and_modify(|e| *e = true);
}
Modifier::Difference => {
self.selection.entry(thread).and_modify(|e| *e = false);
}
Modifier::Intersection => {}
if let Some(thread) = self.get_thread_under_cursor(c) {
self.rows.update_selection_with_thread(
thread,
match modifier {
Modifier::SymmetricDifference => {
|e: &mut bool| *e = !*e
}
Modifier::Union => |e: &mut bool| *e = true,
Modifier::Difference => |e: &mut bool| *e = false,
Modifier::Intersection => |_: &mut bool| {},
},
);
}
self.row_updates.push(thread);
}
}
PageMovement::Down(amount) => {
for c in self.new_cursor_pos.2
..std::cmp::min(self.length, self.new_cursor_pos.2 + amount + 1)
{
let thread = self.get_thread_under_cursor(c);
match modifier {
Modifier::SymmetricDifference => {
self.selection.entry(thread).and_modify(|e| *e = !*e);
}
Modifier::Union => {
self.selection.entry(thread).and_modify(|e| *e = true);
}
Modifier::Difference => {
self.selection.entry(thread).and_modify(|e| *e = false);
}
Modifier::Intersection => {}
if let Some(thread) = self.get_thread_under_cursor(c) {
self.rows.update_selection_with_thread(
thread,
match modifier {
Modifier::SymmetricDifference => {
|e: &mut bool| *e = !*e
}
Modifier::Union => |e: &mut bool| *e = true,
Modifier::Difference => |e: &mut bool| *e = false,
Modifier::Intersection => |_: &mut bool| {},
},
);
}
self.row_updates.push(thread);
}
if modifier == Modifier::Intersection {
for c in (0..self.new_cursor_pos.2).chain(
(std::cmp::min(self.length, self.new_cursor_pos.2 + amount + 1)
+ 1)..self.length,
) {
let thread = self.get_thread_under_cursor(c);
self.selection.entry(thread).and_modify(|e| *e = false);
self.row_updates.push(thread);
if let Some(thread) = self.get_thread_under_cursor(c) {
self.rows
.update_selection_with_thread(thread, |e| *e = false);
}
}
}
}
@ -1107,20 +1099,19 @@ impl Component for ConversationsListing {
self.length,
)
{
let thread = self.get_thread_under_cursor(c);
match modifier {
Modifier::SymmetricDifference => {
self.selection.entry(thread).and_modify(|e| *e = !*e);
}
Modifier::Union => {
self.selection.entry(thread).and_modify(|e| *e = true);
}
Modifier::Difference => {
self.selection.entry(thread).and_modify(|e| *e = false);
}
Modifier::Intersection => {}
if let Some(thread) = self.get_thread_under_cursor(c) {
self.rows.update_selection_with_thread(
thread,
match modifier {
Modifier::SymmetricDifference => {
|e: &mut bool| *e = !*e
}
Modifier::Union => |e: &mut bool| *e = true,
Modifier::Difference => |e: &mut bool| *e = false,
Modifier::Intersection => |_: &mut bool| {},
},
);
}
self.row_updates.push(thread);
}
if modifier == Modifier::Intersection {
for c in (0..self.new_cursor_pos.2).chain(
@ -1129,72 +1120,73 @@ impl Component for ConversationsListing {
self.length,
) + 1)..self.length,
) {
let thread = self.get_thread_under_cursor(c);
self.selection.entry(thread).and_modify(|e| *e = false);
self.row_updates.push(thread);
if let Some(thread) = self.get_thread_under_cursor(c) {
self.rows
.update_selection_with_thread(thread, |e| *e = false);
}
}
}
}
PageMovement::Right(_) | PageMovement::Left(_) => {}
PageMovement::Home => {
for c in 0..=self.new_cursor_pos.2 {
let thread = self.get_thread_under_cursor(c);
match modifier {
Modifier::SymmetricDifference => {
self.selection.entry(thread).and_modify(|e| *e = !*e);
}
Modifier::Union => {
self.selection.entry(thread).and_modify(|e| *e = true);
}
Modifier::Difference => {
self.selection.entry(thread).and_modify(|e| *e = false);
}
Modifier::Intersection => {}
if let Some(thread) = self.get_thread_under_cursor(c) {
self.rows.update_selection_with_thread(
thread,
match modifier {
Modifier::SymmetricDifference => {
|e: &mut bool| *e = !*e
}
Modifier::Union => |e: &mut bool| *e = true,
Modifier::Difference => |e: &mut bool| *e = false,
Modifier::Intersection => |_: &mut bool| {},
},
);
}
self.row_updates.push(thread);
}
if modifier == Modifier::Intersection {
for c in (self.new_cursor_pos.2 + 1)..self.length {
let thread = self.get_thread_under_cursor(c);
self.selection.entry(thread).and_modify(|e| *e = false);
self.row_updates.push(thread);
if let Some(thread) = self.get_thread_under_cursor(c) {
self.rows
.update_selection_with_thread(thread, |e| *e = false);
}
}
}
}
PageMovement::End => {
for c in self.new_cursor_pos.2..self.length {
let thread = self.get_thread_under_cursor(c);
match modifier {
Modifier::SymmetricDifference => {
self.selection.entry(thread).and_modify(|e| *e = !*e);
}
Modifier::Union => {
self.selection.entry(thread).and_modify(|e| *e = true);
}
Modifier::Difference => {
self.selection.entry(thread).and_modify(|e| *e = false);
}
Modifier::Intersection => {}
if let Some(thread) = self.get_thread_under_cursor(c) {
self.rows.update_selection_with_thread(
thread,
match modifier {
Modifier::SymmetricDifference => {
|e: &mut bool| *e = !*e
}
Modifier::Union => |e: &mut bool| *e = true,
Modifier::Difference => |e: &mut bool| *e = false,
Modifier::Intersection => |_: &mut bool| {},
},
);
}
self.row_updates.push(thread);
}
if modifier == Modifier::Intersection {
for c in 0..self.new_cursor_pos.2 {
let thread = self.get_thread_under_cursor(c);
self.selection.entry(thread).and_modify(|e| *e = false);
self.row_updates.push(thread);
if let Some(thread) = self.get_thread_under_cursor(c) {
self.rows
.update_selection_with_thread(thread, |e| *e = false);
}
}
}
}
}
}
}
if !self.row_updates.is_empty() {
if !self.rows.row_updates.is_empty() {
/* certain rows need to be updated (eg an unseen message was just set seen)
* */
while let Some(row) = self.row_updates.pop() {
while let Some(row) = self.rows.row_updates.pop() {
self.update_line(context, row);
let row: usize = self.order[&row];
let row: usize = self.rows.env_order[&row];
let page_no = (self.cursor_pos.2).wrapping_div(rows);
@ -1271,9 +1263,10 @@ impl Component for ConversationsListing {
&& (shortcut!(k == shortcuts[Listing::DESCRIPTION]["open_entry"])
|| shortcut!(k == shortcuts[Listing::DESCRIPTION]["focus_right"])) =>
{
let thread = self.get_thread_under_cursor(self.cursor_pos.2);
self.view = ThreadView::new(self.cursor_pos, thread, None, context);
self.set_focus(Focus::Entry, context);
if let Some(thread) = self.get_thread_under_cursor(self.cursor_pos.2) {
self.view = ThreadView::new(self.cursor_pos, thread, None, context);
self.set_focus(Focus::Entry, context);
}
return true;
}
UIEvent::Input(ref k)
@ -1314,9 +1307,9 @@ impl Component for ConversationsListing {
if self.modifier_active && self.modifier_command.is_none() {
self.modifier_command = Some(Modifier::default());
} else {
let thread_hash = self.get_thread_under_cursor(self.cursor_pos.2);
self.selection.entry(thread_hash).and_modify(|e| *e = !*e);
self.row_updates.push(thread_hash);
if let Some(thread) = self.get_thread_under_cursor(self.cursor_pos.2) {
self.rows.update_selection_with_thread(thread, |e| *e = !*e);
}
}
return true;
}
@ -1333,8 +1326,8 @@ impl Component for ConversationsListing {
let thread: ThreadHash =
threads.find_group(threads.thread_nodes()[&env_thread_node_hash].group);
drop(threads);
if self.order.contains_key(&thread) {
self.row_updates.push(thread);
if self.rows.thread_order.contains_key(&thread) {
self.rows.rename_env(*old_hash, *new_hash);
}
self.dirty = true;
@ -1347,7 +1340,7 @@ impl Component for ConversationsListing {
}
}
UIEvent::EnvelopeRemove(ref _env_hash, ref thread_hash) => {
if self.order.contains_key(thread_hash) {
if self.rows.thread_order.contains_key(thread_hash) {
self.refresh_mailbox(context, false);
self.set_dirty(true);
}
@ -1365,8 +1358,8 @@ impl Component for ConversationsListing {
let thread: ThreadHash =
threads.find_group(threads.thread_nodes()[&env_thread_node_hash].group);
drop(threads);
if self.order.contains_key(&thread) {
self.row_updates.push(thread);
if self.rows.thread_order.contains_key(&thread) {
self.rows.row_updates.push(*env_hash);
}
self.dirty = true;
@ -1410,20 +1403,23 @@ impl Component for ConversationsListing {
return true;
}
Action::Listing(ToggleThreadSnooze) if !self.unfocused() => {
let thread = self.get_thread_under_cursor(self.cursor_pos.2);
let account = &mut context.accounts[&self.cursor_pos.0];
account
.collection
.threads
.write()
.unwrap()
.entry(self.cursor_pos.1)
.and_modify(|threads| {
let is_snoozed = threads.thread_ref(thread).snoozed();
threads.thread_ref_mut(thread).set_snoozed(!is_snoozed);
});
self.row_updates.push(thread);
self.refresh_mailbox(context, false);
/*
if let Some(thread) = self.get_thread_under_cursor(self.cursor_pos.2) {
let account = &mut context.accounts[&self.cursor_pos.0];
account
.collection
.threads
.write()
.unwrap()
.entry(self.cursor_pos.1)
.and_modify(|threads| {
let is_snoozed = threads.thread_ref(thread).snoozed();
threads.thread_ref_mut(thread).set_snoozed(!is_snoozed);
});
self.rows.row_updates.push(thread);
self.refresh_mailbox(context, false);
}
*/
return true;
}
_ => {}
@ -1504,14 +1500,14 @@ impl Component for ConversationsListing {
},
UIEvent::Input(Key::Esc)
if !self.unfocused()
&& self.selection.values().cloned().any(std::convert::identity) =>
&& self
.rows
.selection
.values()
.cloned()
.any(std::convert::identity) =>
{
for (k, v) in self.selection.iter_mut() {
if *v {
*v = false;
self.row_updates.push(*k);
}
}
self.rows.clear_selection();
self.dirty = true;
return true;
}

View File

@ -26,23 +26,23 @@ use std::borrow::Cow;
#[derive(Debug)]
pub struct OfflineListing {
cursor_pos: (AccountHash, MailboxHash),
_row_updates: SmallVec<[ThreadHash; 8]>,
_selection: HashMap<ThreadHash, bool>,
_row_updates: SmallVec<[EnvelopeHash; 8]>,
_selection: HashMap<EnvelopeHash, bool>,
messages: Vec<Cow<'static, str>>,
dirty: bool,
id: ComponentId,
}
impl MailListingTrait for OfflineListing {
fn row_updates(&mut self) -> &mut SmallVec<[ThreadHash; 8]> {
fn row_updates(&mut self) -> &mut SmallVec<[EnvelopeHash; 8]> {
&mut self._row_updates
}
fn selection(&mut self) -> &mut HashMap<ThreadHash, bool> {
fn selection(&mut self) -> &mut HashMap<EnvelopeHash, bool> {
&mut self._selection
}
fn get_focused_items(&self, _context: &Context) -> SmallVec<[ThreadHash; 8]> {
fn get_focused_items(&self, _context: &Context) -> SmallVec<[EnvelopeHash; 8]> {
SmallVec::new()
}
@ -85,6 +85,14 @@ impl ListingTrait for OfflineListing {
false
}
fn set_modifier_active(&mut self, _: bool) {}
fn set_modifier_command(&mut self, _: Option<Modifier>) {}
fn modifier_command(&self) -> Option<Modifier> {
None
}
fn set_movement(&mut self, _: PageMovement) {}
fn focus(&self) -> Focus {

View File

@ -22,7 +22,7 @@
use super::EntryStrings;
use super::*;
use crate::components::PageMovement;
use crate::jobs::{JobId, JoinHandle};
use crate::jobs::JoinHandle;
use std::cmp;
use std::iter::FromIterator;
@ -128,8 +128,7 @@ pub struct PlainListing {
length: usize,
sort: (SortField, SortOrder),
subsort: (SortField, SortOrder),
all_envelopes: HashSet<EnvelopeHash>,
order: HashMap<EnvelopeHash, usize>,
rows: RowsState<(ThreadHash, EnvelopeHash)>,
/// Cache current view.
data_columns: DataColumns,
@ -138,9 +137,6 @@ pub struct PlainListing {
filter_term: String,
filtered_selection: Vec<EnvelopeHash>,
filtered_order: HashMap<EnvelopeHash, usize>,
selection: HashMap<EnvelopeHash, bool>,
_selection: HashMap<ThreadHash, bool>,
thread_node_hashes: HashMap<EnvelopeHash, ThreadNodeHash>,
local_collection: Vec<EnvelopeHash>,
/// If we must redraw on next redraw event
dirty: bool,
@ -148,40 +144,42 @@ pub struct PlainListing {
/// If `self.view` exists or not.
focus: Focus,
view: MailView,
row_updates: SmallVec<[EnvelopeHash; 8]>,
_row_updates: SmallVec<[ThreadHash; 8]>,
color_cache: ColorCache,
active_jobs: HashMap<JobId, JoinHandle<Result<()>>>,
movement: Option<PageMovement>,
modifier_active: bool,
modifier_command: Option<Modifier>,
id: ComponentId,
}
impl MailListingTrait for PlainListing {
fn row_updates(&mut self) -> &mut SmallVec<[ThreadHash; 8]> {
&mut self._row_updates
fn row_updates(&mut self) -> &mut SmallVec<[EnvelopeHash; 8]> {
&mut self.rows.row_updates
}
fn selection(&mut self) -> &mut HashMap<ThreadHash, bool> {
&mut self._selection
fn selection(&mut self) -> &mut HashMap<EnvelopeHash, bool> {
&mut self.rows.selection
}
fn get_focused_items(&self, _context: &Context) -> SmallVec<[ThreadHash; 8]> {
SmallVec::new()
/*
let is_selection_empty = self.selection.values().cloned().any(std::convert::identity);
fn get_focused_items(&self, _context: &Context) -> SmallVec<[EnvelopeHash; 8]> {
let is_selection_empty: bool = !self
.rows
.selection
.values()
.cloned()
.any(std::convert::identity);
dbg!(is_selection_empty);
if is_selection_empty {
self.selection
.iter()
.filter(|(_, v)| **v)
.map(|(k, _)| self.thread_node_hashes[k])
.collect()
} else {
let mut ret = SmallVec::new();
ret.push(self.get_thread_under_cursor(self.cursor_pos.2, context));
ret
return dbg!(self.get_env_under_cursor(self.cursor_pos.2))
.into_iter()
.collect::<_>();
}
*/
SmallVec::from_iter(
self.rows
.selection
.iter()
.filter(|(_, &v)| v)
.map(|(k, _)| *k),
)
}
/// Fill the `self.data_columns` `CellBuffers` with the contents of the account mailbox the user has
@ -253,12 +251,6 @@ impl MailListingTrait for PlainListing {
.envelopes
.read()
.unwrap();
self.thread_node_hashes = context.accounts[&self.cursor_pos.0]
.collection
.get_mailbox(self.cursor_pos.1)
.iter()
.map(|h| (*h, env_lck[h].thread()))
.collect();
let sort = self.sort;
self.local_collection.sort_by(|a, b| match sort {
(SortField::Date, SortOrder::Desc) => {
@ -282,17 +274,13 @@ impl MailListingTrait for PlainListing {
mb.subject().cmp(&ma.subject())
}
});
for &env_hash in &self.local_collection {
self.all_envelopes.insert(env_hash);
}
let items = Box::new(self.local_collection.clone().into_iter())
as Box<dyn Iterator<Item = EnvelopeHash>>;
self.redraw_list(context, items);
drop(env_lck);
if self.length > 0 {
let env_hash = self.get_env_under_cursor(self.cursor_pos.2, context);
if let Some(env_hash) = self.get_env_under_cursor(self.cursor_pos.2) {
let temp = (self.new_cursor_pos.0, self.new_cursor_pos.1, env_hash);
if !force && old_cursor_pos == self.new_cursor_pos {
self.view.update(temp, context);
@ -340,14 +328,16 @@ impl ListingTrait for PlainListing {
self.filtered_selection.clear();
self.filtered_order.clear();
self.filter_term.clear();
self.row_updates.clear();
self.rows.row_updates.clear();
}
fn highlight_line(&mut self, grid: &mut CellBuffer, area: Area, idx: usize, context: &Context) {
if self.length == 0 {
let i = if let Some(i) = self.get_env_under_cursor(idx) {
i
} else {
// self.length == 0
return;
}
let i = self.get_env_under_cursor(idx, context);
};
let account = &context.accounts[&self.cursor_pos.0];
let envelope: EnvelopeRef = account.collection.get_env(i);
@ -357,7 +347,7 @@ impl ListingTrait for PlainListing {
idx % 2 == 0,
!envelope.is_seen(),
self.cursor_pos.2 == idx,
self.selection[&i]
self.rows.selection[&i]
);
let (upper_left, bottom_right) = area;
@ -602,14 +592,12 @@ impl ListingTrait for PlainListing {
return;
}
self.order.clear();
self.selection.clear();
self.length = 0;
self.filtered_selection.clear();
self.filtered_order.clear();
self.filter_term = filter_term;
self.row_updates.clear();
for v in self.selection.values_mut() {
self.rows.row_updates.clear();
for v in self.rows.selection.values_mut() {
*v = false;
}
@ -621,7 +609,7 @@ impl ListingTrait for PlainListing {
if self.filtered_order.contains_key(&env_hash) {
continue;
}
if self.all_envelopes.contains(&env_hash) {
if self.rows.contains_env(env_hash) {
self.filtered_selection.push(env_hash);
self.filtered_order
.insert(env_hash, self.filtered_selection.len() - 1);
@ -644,6 +632,18 @@ impl ListingTrait for PlainListing {
!matches!(self.focus, Focus::None)
}
fn set_modifier_active(&mut self, new_val: bool) {
self.modifier_active = new_val;
}
fn set_modifier_command(&mut self, new_val: Option<Modifier>) {
self.modifier_command = new_val;
}
fn modifier_command(&self) -> Option<Modifier> {
self.modifier_command
}
fn set_movement(&mut self, mvm: PageMovement) {
self.movement = Some(mvm);
self.set_dirty(true);
@ -655,18 +655,19 @@ impl ListingTrait for PlainListing {
self.view
.process_event(&mut UIEvent::VisibilityChange(false), context);
self.dirty = true;
/* If self.row_updates is not empty and we exit a thread, the row_update events
/* If self.rows.row_updates is not empty and we exit a thread, the row_update events
* will be performed but the list will not be drawn. So force a draw in any case.
* */
self.force_draw = true;
}
Focus::Entry => {
let env_hash = self.get_env_under_cursor(self.cursor_pos.2, context);
let temp = (self.cursor_pos.0, self.cursor_pos.1, env_hash);
self.view = MailView::new(temp, None, None, context);
self.force_draw = true;
self.dirty = true;
self.view.set_dirty(true);
if let Some(env_hash) = self.get_env_under_cursor(self.cursor_pos.2) {
let temp = (self.cursor_pos.0, self.cursor_pos.1, env_hash);
self.view = MailView::new(temp, None, None, context);
self.force_draw = true;
self.dirty = true;
self.view.set_dirty(true);
}
}
Focus::EntryFullscreen => {
self.dirty = true;
@ -696,32 +697,26 @@ impl PlainListing {
length: 0,
sort: (Default::default(), Default::default()),
subsort: (SortField::Date, SortOrder::Desc),
all_envelopes: HashSet::default(),
rows: RowsState::default(),
local_collection: Vec::new(),
thread_node_hashes: HashMap::default(),
order: HashMap::default(),
filter_term: String::new(),
search_job: None,
filtered_selection: Vec::new(),
filtered_order: HashMap::default(),
selection: HashMap::default(),
_selection: HashMap::default(),
row_updates: SmallVec::new(),
_row_updates: SmallVec::new(),
data_columns: DataColumns::default(),
dirty: true,
force_draw: true,
focus: Focus::None,
view: MailView::default(),
color_cache: ColorCache::default(),
active_jobs: HashMap::default(),
movement: None,
modifier_active: false,
modifier_command: None,
id: ComponentId::new_v4(),
})
}
fn make_entry_string(&self, e: EnvelopeRef, context: &Context) -> EntryStrings {
fn make_entry_string(&self, e: &Envelope, context: &Context) -> EntryStrings {
let mut tags = String::new();
let mut colors = SmallVec::new();
let account = &context.accounts[&self.cursor_pos.0];
@ -766,7 +761,7 @@ impl PlainListing {
subject: SubjectString(subject),
flag: FlagString(format!(
"{selected}{unseen}{attachments}{whitespace}",
selected = if self.selection.get(&e.hash()).cloned().unwrap_or(false) {
selected = if self.rows.selection.get(&e.hash()).cloned().unwrap_or(false) {
mailbox_settings!(
context[self.cursor_pos.0][&self.cursor_pos.1]
.listing
@ -802,7 +797,7 @@ impl PlainListing {
} else {
""
},
whitespace = if self.selection.get(&e.hash()).cloned().unwrap_or(false)
whitespace = if self.rows.selection.get(&e.hash()).cloned().unwrap_or(false)
|| !e.is_seen()
|| e.has_attachments()
{
@ -819,11 +814,10 @@ impl PlainListing {
fn redraw_list(&mut self, context: &Context, iter: Box<dyn Iterator<Item = EnvelopeHash>>) {
let account = &context.accounts[&self.cursor_pos.0];
let mailbox = &account[&self.cursor_pos.1];
let threads = account.collection.get_threads(self.cursor_pos.1);
self.order.clear();
self.selection.clear();
self.rows.clear();
self.length = 0;
let mut rows = Vec::with_capacity(1024);
let mut min_width = (0, 0, 0, 0, 0);
for i in iter {
@ -852,7 +846,7 @@ impl PlainListing {
}
}
let entry_strings = self.make_entry_string(envelope, context);
let entry_strings = self.make_entry_string(&envelope, context);
min_width.1 = cmp::max(min_width.1, entry_strings.date.grapheme_width()); /* date */
min_width.2 = cmp::max(min_width.2, entry_strings.from.grapheme_width()); /* from */
min_width.3 = cmp::max(
@ -862,10 +856,13 @@ impl PlainListing {
+ 1
+ entry_strings.tags.grapheme_width(),
); /* tags + subject */
rows.push(entry_strings);
self.rows.insert_thread(
threads.envelope_to_thread[&i],
(threads.envelope_to_thread[&i], i),
smallvec::smallvec![i],
entry_strings,
);
self.order.insert(i, self.length);
self.selection.insert(i, false);
self.length += 1;
}
@ -873,16 +870,16 @@ impl PlainListing {
/* index column */
self.data_columns.columns[0] =
CellBuffer::new_with_context(min_width.0, rows.len(), None, context);
CellBuffer::new_with_context(min_width.0, self.rows.len(), None, context);
/* date column */
self.data_columns.columns[1] =
CellBuffer::new_with_context(min_width.1, rows.len(), None, context);
CellBuffer::new_with_context(min_width.1, self.rows.len(), None, context);
/* from column */
self.data_columns.columns[2] =
CellBuffer::new_with_context(min_width.2, rows.len(), None, context);
CellBuffer::new_with_context(min_width.2, self.rows.len(), None, context);
/* subject column */
self.data_columns.columns[3] =
CellBuffer::new_with_context(min_width.3, rows.len(), None, context);
CellBuffer::new_with_context(min_width.3, self.rows.len(), None, context);
let iter = if self.filter_term.is_empty() {
Box::new(self.local_collection.iter().cloned())
@ -893,7 +890,7 @@ impl PlainListing {
};
let columns = &mut self.data_columns.columns;
for ((idx, i), strings) in iter.enumerate().zip(rows) {
for ((idx, i), (_, strings)) in iter.enumerate().zip(self.rows.entries.iter()) {
if !context.accounts[&self.cursor_pos.0].contains_key(i) {
//debug!("key = {}", i);
//debug!(
@ -1003,7 +1000,7 @@ impl PlainListing {
}
/* Set fg color for flags */
let mut x = 0;
if self.selection.get(&i).cloned().unwrap_or(false) {
if self.rows.selection.get(&i).cloned().unwrap_or(false) {
x += 1;
}
if !envelope.is_seen() {
@ -1029,11 +1026,11 @@ impl PlainListing {
}
}
fn get_env_under_cursor(&self, cursor: usize, _context: &Context) -> EnvelopeHash {
fn get_env_under_cursor(&self, cursor: usize) -> Option<EnvelopeHash> {
if self.filter_term.is_empty() {
self.local_collection[cursor]
self.local_collection.get(cursor).cloned()
} else {
self.filtered_selection[cursor]
self.filtered_selection.get(cursor).cloned()
}
}
@ -1051,42 +1048,6 @@ impl PlainListing {
_ => melib::datetime::timestamp_to_string(envelope.datetime(), None, false),
}
}
fn perform_action(&mut self, context: &mut Context, env_hash: EnvelopeHash, a: &ListingAction) {
let account = &mut context.accounts[&self.cursor_pos.0];
match {
match a {
ListingAction::SetSeen => account.backend.write().unwrap().set_flags(
env_hash.into(),
self.cursor_pos.1,
smallvec::smallvec![(Ok(Flag::SEEN), true)],
),
ListingAction::SetUnseen => account.backend.write().unwrap().set_flags(
env_hash.into(),
self.cursor_pos.1,
smallvec::smallvec![(Ok(Flag::SEEN), false)],
),
ListingAction::Delete => {
/* do nothing */
Err(MeliError::new("Delete is unimplemented"))
}
_ => unreachable!(),
}
} {
Err(e) => {
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::DisplayMessage(
e.to_string(),
)));
}
Ok(fut) => {
let handle = account.job_executor.spawn_specialized(fut);
self.active_jobs.insert(handle.job_id, handle);
}
}
self.row_updates.push(env_hash);
}
}
impl Component for PlainListing {
@ -1128,10 +1089,10 @@ impl Component for PlainListing {
area = (set_y(upper_left, y + 1), bottom_right);
}
if !self.row_updates.is_empty() {
if !self.rows.row_updates.is_empty() {
let (upper_left, bottom_right) = area;
while let Some(row) = self.row_updates.pop() {
let row: usize = self.order[&row];
while let Some(row) = self.rows.row_updates.pop() {
let row: usize = self.rows.env_order[&row];
let rows = get_y(bottom_right) - get_y(upper_left) + 1;
let page_no = (self.new_cursor_pos.2).wrapping_div(rows);
@ -1233,8 +1194,14 @@ impl Component for PlainListing {
if !self.unfocused()
&& shortcut!(key == shortcuts[Listing::DESCRIPTION]["select_entry"]) =>
{
let env_hash = self.get_env_under_cursor(self.cursor_pos.2, context);
self.selection.entry(env_hash).and_modify(|e| *e = !*e);
if self.modifier_active && self.modifier_command.is_none() {
self.modifier_command = Some(Modifier::default());
} else {
if let Some(env_hash) = self.get_env_under_cursor(self.cursor_pos.2) {
self.rows.update_selection_with_env(env_hash, |e| *e = !*e);
}
}
return true;
}
UIEvent::Action(ref action) => match action {
Action::SubSort(field, order) if !self.unfocused() => {
@ -1253,37 +1220,6 @@ impl Component for PlainListing {
self.sort = (*field, *order);
return true;
}
Action::Listing(a @ ListingAction::SetSeen)
| Action::Listing(a @ ListingAction::SetUnseen)
| Action::Listing(a @ ListingAction::Delete)
if !self.unfocused() =>
{
let is_selection_empty =
self.selection.values().cloned().any(std::convert::identity);
let i = [self.get_env_under_cursor(self.cursor_pos.2, context)];
let cursor_iter;
let sel_iter = if is_selection_empty {
cursor_iter = None;
Some(self.selection.iter().filter(|(_, v)| **v).map(|(k, _)| k))
} else {
cursor_iter = Some(i.iter());
None
};
let iter = sel_iter
.into_iter()
.flatten()
.chain(cursor_iter.into_iter().flatten())
.cloned();
let stack: SmallVec<[_; 8]> = SmallVec::from_iter(iter);
for i in stack {
self.perform_action(context, i, a);
}
self.dirty = true;
for v in self.selection.values_mut() {
*v = false;
}
return true;
}
_ => {}
},
@ -1347,16 +1283,11 @@ impl Component for PlainListing {
return false;
}
self.row_updates.push(*new_hash);
if let Some(row) = self.order.remove(old_hash) {
self.order.insert(*new_hash, row);
let selection_status = self.selection.remove(old_hash).unwrap();
self.selection.insert(*new_hash, selection_status);
for h in self.filtered_selection.iter_mut() {
if *h == *old_hash {
*h = *new_hash;
break;
}
self.rows.rename_env(*old_hash, *new_hash);
for h in self.filtered_selection.iter_mut() {
if *h == *old_hash {
*h = *new_hash;
break;
}
}
@ -1378,7 +1309,7 @@ impl Component for PlainListing {
return false;
}
self.row_updates.push(*env_hash);
self.rows.row_updates.push(*env_hash);
self.dirty = true;
if self.unfocused() {
@ -1394,9 +1325,14 @@ impl Component for PlainListing {
}
UIEvent::Input(Key::Esc)
if !self.unfocused()
&& self.selection.values().cloned().any(std::convert::identity) =>
&& self
.rows
.selection
.values()
.cloned()
.any(std::convert::identity) =>
{
for v in self.selection.values_mut() {
for v in self.rows.selection.values_mut() {
*v = false;
}
self.dirty = true;

View File

@ -24,6 +24,7 @@ use crate::components::PageMovement;
use std::cmp;
use std::convert::TryInto;
use std::fmt::Write;
use std::iter::FromIterator;
macro_rules! row_attr {
($color_cache:expr, $even: expr, $unseen:expr, $highlighted:expr, $selected:expr $(,)*) => {{
@ -122,31 +123,48 @@ pub struct ThreadListing {
data_columns: DataColumns,
rows_drawn: SegmentTree,
rows: Vec<((usize, bool, bool, EnvelopeHash), EntryStrings)>,
row_updates: SmallVec<[ThreadHash; 8]>,
selection: HashMap<ThreadHash, bool>,
order: HashMap<EnvelopeHash, usize>,
rows: RowsState<(bool, bool, ThreadHash, EnvelopeHash)>,
/// If we must redraw on next redraw event
dirty: bool,
/// If `self.view` is focused or not.
focus: Focus,
initialised: bool,
view: Option<Box<MailView>>,
modifier_active: bool,
modifier_command: Option<Modifier>,
movement: Option<PageMovement>,
id: ComponentId,
}
impl MailListingTrait for ThreadListing {
fn row_updates(&mut self) -> &mut SmallVec<[ThreadHash; 8]> {
&mut self.row_updates
fn row_updates(&mut self) -> &mut SmallVec<[EnvelopeHash; 8]> {
&mut self.rows.row_updates
}
fn selection(&mut self) -> &mut HashMap<ThreadHash, bool> {
&mut self.selection
fn selection(&mut self) -> &mut HashMap<EnvelopeHash, bool> {
&mut self.rows.selection
}
fn get_focused_items(&self, _context: &Context) -> SmallVec<[ThreadHash; 8]> {
SmallVec::new()
fn get_focused_items(&self, _context: &Context) -> SmallVec<[EnvelopeHash; 8]> {
let is_selection_empty: bool = !self
.rows
.selection
.values()
.cloned()
.any(std::convert::identity);
if is_selection_empty {
return self
.get_env_under_cursor(self.cursor_pos.2)
.into_iter()
.collect::<_>();
}
SmallVec::from_iter(
self.rows
.selection
.iter()
.filter(|(_, &v)| v)
.map(|(k, _)| *k),
)
}
/// Fill the `self.content` `CellBuffer` with the contents of the account mailbox the user has
@ -230,7 +248,7 @@ impl MailListingTrait for ThreadListing {
let account = &context.accounts[&self.cursor_pos.0];
let threads = account.collection.get_threads(self.cursor_pos.1);
self.length = 0;
self.order.clear();
self.rows.clear();
if threads.len() == 0 {
let message: String = account[&self.cursor_pos.1].status();
self.data_columns.columns[0] =
@ -246,7 +264,6 @@ impl MailListingTrait for ThreadListing {
);
return;
}
let mut rows = Vec::with_capacity(1024);
let mut min_width = (0, 0, 0, 0, 0);
#[allow(clippy::type_complexity)]
let mut row_widths: (
@ -270,15 +287,13 @@ impl MailListingTrait for ThreadListing {
let mut iter = threads.threads_group_iter(roots).peekable();
let thread_nodes: &HashMap<ThreadNodeHash, ThreadNode> = threads.thread_nodes();
/* This is just a desugared for loop so that we can use .peek() */
let mut idx = 0;
let mut idx: usize = 0;
let mut prev_group = ThreadHash::null();
while let Some((indentation, thread_node_hash, has_sibling)) = iter.next() {
let thread_node = &thread_nodes[&thread_node_hash];
if thread_node.has_message() {
let envelope: EnvelopeRef =
account.collection.get_env(thread_node.message().unwrap());
self.order.insert(envelope.hash(), idx);
if let Some(env_hash) = thread_node.message() {
let envelope: EnvelopeRef = account.collection.get_env(env_hash);
use melib::search::QueryTrait;
if let Some(filter_query) = mailbox_settings!(
context[self.cursor_pos.0][&self.cursor_pos.1]
@ -341,15 +356,17 @@ impl MailListingTrait for ThreadListing {
+ 1
+ entry_strings.tags.grapheme_width(),
); /* tags + subject */
rows.push((
self.rows.insert_thread(
threads.envelope_to_thread[&env_hash],
(
idx,
envelope.is_seen(),
envelope.has_attachments(),
envelope.hash(),
threads.envelope_to_thread[&env_hash],
env_hash,
),
smallvec::smallvec![env_hash],
entry_strings,
));
);
idx += 1;
} else {
continue;
@ -374,24 +391,23 @@ impl MailListingTrait for ThreadListing {
min_width.0 = idx.saturating_sub(1).to_string().len();
/* index column */
self.data_columns.columns[0] =
CellBuffer::new_with_context(min_width.0, rows.len(), None, context);
CellBuffer::new_with_context(min_width.0, self.rows.len(), None, context);
/* date column */
self.data_columns.columns[1] =
CellBuffer::new_with_context(min_width.1, rows.len(), None, context);
CellBuffer::new_with_context(min_width.1, self.rows.len(), None, context);
/* from column */
self.data_columns.columns[2] =
CellBuffer::new_with_context(min_width.2, rows.len(), None, context);
CellBuffer::new_with_context(min_width.2, self.rows.len(), None, context);
self.data_columns.segment_tree[2] = row_widths.2.into();
/* flags column */
self.data_columns.columns[3] =
CellBuffer::new_with_context(min_width.3, rows.len(), None, context);
CellBuffer::new_with_context(min_width.3, self.rows.len(), None, context);
/* subject column */
self.data_columns.columns[4] =
CellBuffer::new_with_context(min_width.4, rows.len(), None, context);
CellBuffer::new_with_context(min_width.4, self.rows.len(), None, context);
self.data_columns.segment_tree[4] = row_widths.4.into();
self.rows = rows;
self.rows_drawn = SegmentTree::from(
std::iter::repeat(1)
.take(self.rows.len())
@ -403,7 +419,7 @@ impl MailListingTrait for ThreadListing {
0,
std::cmp::min(80, self.rows.len().saturating_sub(1)),
);
self.length = self.order.len();
self.length = self.rows.len();
}
}
@ -416,8 +432,7 @@ impl ListingTrait for ThreadListing {
self.new_cursor_pos = (coordinates.0, coordinates.1, 0);
self.focus = Focus::None;
self.view = None;
self.order.clear();
self.row_updates.clear();
self.rows.clear();
self.initialised = false;
}
@ -620,15 +635,15 @@ impl ListingTrait for ThreadListing {
for r in 0..cmp::min(self.length - top_idx, rows) {
let (fg_color, bg_color) = {
let c = &self.data_columns.columns[0][(0, r + top_idx)];
/*
let thread_hash = self.get_thread_under_cursor(r + top_idx);
if self.selection[&thread_hash] {
(c.fg(), self.color_cache.selected.bg)
if let Some(env_hash) = self.get_env_under_cursor(r + top_idx) {
if self.rows.selection[&env_hash] {
(c.fg(), self.color_cache.selected.bg)
} else {
(c.fg(), c.bg())
}
} else {
(c.fg(), c.bg())
}
*/
(c.fg(), c.bg())
};
change_colors(
grid,
@ -703,11 +718,13 @@ impl ListingTrait for ThreadListing {
}
fn highlight_line(&mut self, grid: &mut CellBuffer, area: Area, idx: usize, context: &Context) {
if self.length == 0 {
let env_hash = if let Some(i) = self.get_env_under_cursor(idx) {
i
} else {
// self.length == 0
return;
}
};
let env_hash = self.get_env_under_cursor(idx, context);
let envelope: EnvelopeRef = context.accounts[&self.cursor_pos.0]
.collection
.get_env(env_hash);
@ -717,7 +734,7 @@ impl ListingTrait for ThreadListing {
idx % 2 == 0,
!envelope.is_seen(),
self.cursor_pos.2 == idx,
false,
self.rows.selection[&env_hash],
);
for row in grid.bounds_iter(area) {
for c in row {
@ -746,6 +763,18 @@ impl ListingTrait for ThreadListing {
!matches!(self.focus, Focus::None)
}
fn set_modifier_active(&mut self, new_val: bool) {
self.modifier_active = new_val;
}
fn set_modifier_command(&mut self, new_val: Option<Modifier>) {
self.modifier_command = new_val;
}
fn modifier_command(&self) -> Option<Modifier> {
self.modifier_command
}
fn set_movement(&mut self, mvm: PageMovement) {
self.movement = Some(mvm);
self.set_dirty(true);
@ -756,28 +785,26 @@ impl ListingTrait for ThreadListing {
Focus::None => {
self.view = None;
self.dirty = true;
/* If self.row_updates is not empty and we exit a thread, the row_update events
/* If self.rows.row_updates is not empty and we exit a thread, the row_update events
* will be performed but the list will not be drawn. So force a draw in any case.
* */
// self.force_draw = true;
}
Focus::Entry => {
// self.force_draw = true;
self.dirty = true;
let coordinates = (
self.cursor_pos.0,
self.cursor_pos.1,
self.get_env_under_cursor(self.cursor_pos.2, context),
);
if let Some(env_hash) = self.get_env_under_cursor(self.cursor_pos.2) {
// self.force_draw = true;
self.dirty = true;
let coordinates = (self.cursor_pos.0, self.cursor_pos.1, env_hash);
if let Some(ref mut v) = self.view {
v.update(coordinates, context);
} else {
self.view = Some(Box::new(MailView::new(coordinates, None, None, context)));
}
if let Some(ref mut v) = self.view {
v.update(coordinates, context);
} else {
self.view = Some(Box::new(MailView::new(coordinates, None, None, context)));
}
if let Some(ref mut s) = self.view {
s.set_dirty(true);
if let Some(ref mut s) = self.view {
s.set_dirty(true);
}
}
}
Focus::EntryFullscreen => {
@ -811,15 +838,14 @@ impl ThreadListing {
color_cache: ColorCache::default(),
data_columns: DataColumns::default(),
rows_drawn: SegmentTree::default(),
rows: vec![],
row_updates: SmallVec::new(),
selection: HashMap::default(),
order: HashMap::default(),
rows: RowsState::default(),
dirty: true,
focus: Focus::None,
view: None,
initialised: false,
movement: None,
modifier_active: false,
modifier_command: None,
id: ComponentId::new_v4(),
search_job: None,
})
@ -889,16 +915,13 @@ impl ThreadListing {
s
}
fn get_env_under_cursor(&self, cursor: usize, _context: &Context) -> EnvelopeHash {
*self
.order
fn get_env_under_cursor(&self, cursor: usize) -> Option<EnvelopeHash> {
self.rows
.env_order
.iter()
.find(|(_, &r)| r == cursor)
.unwrap_or_else(|| {
debug!("self.order empty ? cursor={} {:#?}", cursor, &self.order);
panic!();
})
.0
.map(|v| v.0)
.cloned()
}
fn make_entry_string(&self, e: &Envelope, context: &Context) -> EntryStrings {
@ -971,10 +994,14 @@ impl ThreadListing {
self.data_columns.columns[4].size().0,
);
for ((idx, is_seen, has_attachments, env_hash), strings) in
self.rows.iter().skip(start).take(end - start + 1)
for (idx, ((is_seen, has_attachments, _thread_hash, env_hash), strings)) in self
.rows
.entries
.iter()
.enumerate()
.skip(start)
.take(end - start + 1)
{
let idx = *idx;
if !context.accounts[&self.cursor_pos.0].contains_key(*env_hash) {
//debug!("key = {}", root_env_hash);
//debug!(
@ -986,7 +1013,13 @@ impl ThreadListing {
panic!();
}
let row_attr = row_attr!(self.color_cache, idx % 2 == 0, !*is_seen, false, false);
let row_attr = row_attr!(
self.color_cache,
idx % 2 == 0,
!*is_seen,
self.cursor_pos.2 == idx,
self.rows.selection[&env_hash],
);
let (x, _) = write_string_to_grid(
&idx.to_string(),
&mut self.data_columns.columns[0],
@ -1115,27 +1148,187 @@ impl ThreadListing {
impl Component for ThreadListing {
fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
/*
if !self.row_updates.is_empty() {
let (upper_left, bottom_right) = area;
while let Some(row) = self.row_updates.pop() {
let row: usize = self.order[&row];
let (upper_left, bottom_right) = area;
let rows = get_y(bottom_right) - get_y(upper_left) + 1;
let rows = get_y(bottom_right) - get_y(upper_left) + 1;
let page_no = (self.new_cursor_pos.2).wrapping_div(rows);
if let Some(modifier) = self.modifier_command.take() {
if let Some(mvm) = self.movement.as_ref() {
match mvm {
PageMovement::Up(amount) => {
for c in
self.new_cursor_pos.2.saturating_sub(*amount)..=self.new_cursor_pos.2
{
if let Some(env_hash) = self.get_env_under_cursor(c) {
self.rows.update_selection_with_env(
env_hash,
match modifier {
Modifier::SymmetricDifference => |e: &mut bool| *e = !*e,
Modifier::Union => |e: &mut bool| *e = true,
Modifier::Difference => |e: &mut bool| *e = false,
Modifier::Intersection => |_: &mut bool| {},
},
);
}
}
if modifier == Modifier::Intersection {
for c in (0..self.new_cursor_pos.2.saturating_sub(*amount))
.chain((self.new_cursor_pos.2 + 2)..self.length)
{
if let Some(env_hash) = self.get_env_under_cursor(c) {
self.rows
.update_selection_with_env(env_hash, |e| *e = false);
}
}
}
}
PageMovement::PageUp(multiplier) => {
for c in self.new_cursor_pos.2.saturating_sub(rows * multiplier)
..=self.new_cursor_pos.2
{
if let Some(env_hash) = self.get_env_under_cursor(c) {
self.rows.update_selection_with_env(
env_hash,
match modifier {
Modifier::SymmetricDifference => |e: &mut bool| *e = !*e,
Modifier::Union => |e: &mut bool| *e = true,
Modifier::Difference => |e: &mut bool| *e = false,
Modifier::Intersection => |_: &mut bool| {},
},
);
}
}
}
PageMovement::Down(amount) => {
for c in self.new_cursor_pos.2
..std::cmp::min(self.length, self.new_cursor_pos.2 + amount + 1)
{
if let Some(env_hash) = self.get_env_under_cursor(c) {
self.rows.update_selection_with_env(
env_hash,
match modifier {
Modifier::SymmetricDifference => |e: &mut bool| *e = !*e,
Modifier::Union => |e: &mut bool| *e = true,
Modifier::Difference => |e: &mut bool| *e = false,
Modifier::Intersection => |_: &mut bool| {},
},
);
}
}
if modifier == Modifier::Intersection {
for c in (0..self.new_cursor_pos.2).chain(
(std::cmp::min(self.length, self.new_cursor_pos.2 + amount + 1) + 1)
..self.length,
) {
if let Some(env_hash) = self.get_env_under_cursor(c) {
self.rows
.update_selection_with_env(env_hash, |e| *e = false);
}
}
}
}
PageMovement::PageDown(multiplier) => {
for c in self.new_cursor_pos.2
..std::cmp::min(
self.new_cursor_pos.2 + rows * multiplier + 1,
self.length,
)
{
if let Some(env_hash) = self.get_env_under_cursor(c) {
self.rows.update_selection_with_env(
env_hash,
match modifier {
Modifier::SymmetricDifference => |e: &mut bool| *e = !*e,
Modifier::Union => |e: &mut bool| *e = true,
Modifier::Difference => |e: &mut bool| *e = false,
Modifier::Intersection => |_: &mut bool| {},
},
);
}
}
if modifier == Modifier::Intersection {
for c in (0..self.new_cursor_pos.2).chain(
(std::cmp::min(
self.new_cursor_pos.2 + rows * multiplier + 1,
self.length,
) + 1)..self.length,
) {
if let Some(env_hash) = self.get_env_under_cursor(c) {
self.rows
.update_selection_with_env(env_hash, |e| *e = false);
}
}
}
}
PageMovement::Right(_) | PageMovement::Left(_) => {}
PageMovement::Home => {
for c in 0..=self.new_cursor_pos.2 {
if let Some(env_hash) = self.get_env_under_cursor(c) {
self.rows.update_selection_with_env(
env_hash,
match modifier {
Modifier::SymmetricDifference => |e: &mut bool| *e = !*e,
Modifier::Union => |e: &mut bool| *e = true,
Modifier::Difference => |e: &mut bool| *e = false,
Modifier::Intersection => |_: &mut bool| {},
},
);
}
}
if modifier == Modifier::Intersection {
for c in (self.new_cursor_pos.2 + 1)..self.length {
if let Some(env_hash) = self.get_env_under_cursor(c) {
self.rows
.update_selection_with_env(env_hash, |e| *e = false);
}
}
}
}
PageMovement::End => {
for c in self.new_cursor_pos.2..self.length {
if let Some(env_hash) = self.get_env_under_cursor(c) {
self.rows.update_selection_with_env(
env_hash,
match modifier {
Modifier::SymmetricDifference => |e: &mut bool| *e = !*e,
Modifier::Union => |e: &mut bool| *e = true,
Modifier::Difference => |e: &mut bool| *e = false,
Modifier::Intersection => |_: &mut bool| {},
},
);
}
}
if modifier == Modifier::Intersection {
for c in 0..self.new_cursor_pos.2 {
if let Some(env_hash) = self.get_env_under_cursor(c) {
self.rows
.update_selection_with_env(env_hash, |e| *e = false);
}
}
}
}
}
}
//self.force_draw = true;
}
if !self.rows.row_updates.is_empty() {
let page_no = (self.new_cursor_pos.2).wrapping_div(rows);
let top_idx = page_no * rows;
while let Some(row) = self.rows.row_updates.pop() {
let row: usize = self.rows.env_order[&row];
let top_idx = page_no * rows;
if row >= top_idx && row <= top_idx + rows {
let area = (
let new_area = (
set_y(upper_left, get_y(upper_left) + (row % rows)),
set_y(bottom_right, get_y(upper_left) + (row % rows)),
);
self.highlight_line(grid, area, row, context);
context.dirty_areas.push_back(area);
self.highlight_line(grid, new_area, row, context);
context.dirty_areas.push_back(new_area);
}
}
}
*/
if !self.is_dirty() {
return;
}
@ -1180,14 +1373,12 @@ impl Component for ThreadListing {
/* Mark message as read */
let must_highlight = {
if self.length == 0 {
false
} else {
if let Some(env_hash) = self.get_env_under_cursor(idx) {
let account = &context.accounts[&self.cursor_pos.0];
let envelope: EnvelopeRef = account
.collection
.get_env(self.get_env_under_cursor(idx, context));
let envelope: EnvelopeRef = account.collection.get_env(env_hash);
envelope.is_seen()
} else {
false
}
};
@ -1221,7 +1412,6 @@ impl Component for ThreadListing {
.dirty_areas
.push_back((set_y(upper_left, mid), set_y(bottom_right, mid)));
}
// TODO: Make headers view configurable
if !self.dirty {
if let Some(v) = self.view.as_mut() {
@ -1230,16 +1420,14 @@ impl Component for ThreadListing {
return;
}
let coordinates = (
self.cursor_pos.0,
self.cursor_pos.1,
self.get_env_under_cursor(self.cursor_pos.2, context),
);
if let Some(env_hash) = self.get_env_under_cursor(self.cursor_pos.2) {
let coordinates = (self.cursor_pos.0, self.cursor_pos.1, env_hash);
if let Some(ref mut v) = self.view {
v.update(coordinates, context);
} else {
self.view = Some(Box::new(MailView::new(coordinates, None, None, context)));
if let Some(ref mut v) = self.view {
v.update(coordinates, context);
} else {
self.view = Some(Box::new(MailView::new(coordinates, None, None, context)));
}
}
if let Some(v) = self.view.as_mut() {
@ -1369,10 +1557,9 @@ impl Component for ThreadListing {
if !account.collection.contains_key(new_hash) {
return false;
}
if let Some(row) = self.order.remove(old_hash) {
self.order.insert(*new_hash, row);
(self.rows[row].0).3 = *new_hash;
//self.row_updates.push(old_hash);
self.rows.rename_env(*old_hash, *new_hash);
if let Some(&row) = self.rows.env_order.get(new_hash) {
(self.rows.entries[row].0).3 = *new_hash;
}
self.dirty = true;
@ -1387,7 +1574,7 @@ impl Component for ThreadListing {
}
}
UIEvent::EnvelopeRemove(ref env_hash, _) => {
if self.order.contains_key(env_hash) {
if self.rows.contains_env(*env_hash) {
self.refresh_mailbox(context, false);
self.set_dirty(true);
}
@ -1397,8 +1584,8 @@ impl Component for ThreadListing {
if !account.collection.contains_key(env_hash) {
return false;
}
if self.order.contains_key(env_hash) {
//self.row_updates.push(*env_hash);
if self.rows.contains_env(*env_hash) {
self.rows.row_updates.push(*env_hash);
}
self.dirty = true;
@ -1415,6 +1602,34 @@ impl Component for ThreadListing {
UIEvent::Resize => {
self.dirty = true;
}
UIEvent::Input(Key::Esc)
if !self.unfocused()
&& self
.rows
.selection
.values()
.cloned()
.any(std::convert::identity) =>
{
for v in self.rows.selection.values_mut() {
*v = false;
}
self.dirty = true;
return true;
}
UIEvent::Input(ref key)
if !self.unfocused()
&& shortcut!(key == shortcuts[Listing::DESCRIPTION]["select_entry"]) =>
{
if self.modifier_active && self.modifier_command.is_none() {
self.modifier_command = Some(Modifier::default());
} else {
if let Some(env_hash) = self.get_env_under_cursor(self.cursor_pos.2) {
self.rows.update_selection_with_env(env_hash, |e| *e = !*e);
}
}
return true;
}
UIEvent::Action(ref action) => match action {
Action::SubSort(field, order) => {
debug!("SubSort {:?} , {:?}", field, order);