From cc439b239ae27ae84fbcf50fbd82ec591c147c94 Mon Sep 17 00:00:00 2001 From: Manos Pitsidianakis Date: Mon, 7 Nov 2022 16:35:21 +0200 Subject: [PATCH] mail/listing.rs: add RowsState struct Keep state of rows in lists in this struct to reduce code duplication in list implementations --- src/components/mail/listing.rs | 194 +++++- src/components/mail/listing/compact.rs | 690 ++++++++++--------- src/components/mail/listing/conversations.rs | 418 ++++++----- src/components/mail/listing/offline.rs | 18 +- src/components/mail/listing/plain.rs | 274 +++----- src/components/mail/listing/thread.rs | 429 +++++++++--- 6 files changed, 1176 insertions(+), 847 deletions(-) diff --git a/src/components/mail/listing.rs b/src/components/mail/listing.rs index ae4f85f0..99fcc78e 100644 --- a/src/components/mail/listing.rs +++ b/src/components/mail/listing.rs @@ -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 { + pub selection: HashMap, + pub row_updates: SmallVec<[EnvelopeHash; 8]>, + pub thread_to_env: HashMap>, + pub env_to_thread: HashMap, + pub thread_order: HashMap, + pub env_order: HashMap, + #[allow(clippy::type_complexity)] + pub entries: Vec<(T, EntryStrings)>, + pub all_threads: HashSet, + pub all_envelopes: HashSet, +} + +impl RowsState { + #[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; - 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; + 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) {} - fn modifier_command(&self) -> Option { - None - } + fn set_modifier_active(&mut self, _new_val: bool); + fn set_modifier_command(&mut self, _new_val: Option); + fn modifier_command(&self) -> Option; 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()); } _ => {} }, diff --git a/src/components/mail/listing/compact.rs b/src/components/mail/listing/compact.rs index 2ae5ce0d..131ec097 100644 --- a/src/components/mail/listing/compact.rs +++ b/src/components/mail/listing/compact.rs @@ -167,13 +167,10 @@ pub struct CompactListing { sort: (SortField, SortOrder), sortcmd: bool, subsort: (SortField, SortOrder), - all_threads: HashSet, - order: HashMap, /// Cache current view. data_columns: DataColumns, rows_drawn: SegmentTree, - #[allow(clippy::type_complexity)] - rows: Vec<((usize, (ThreadHash, EnvelopeHash)), EntryStrings)>, + rows: RowsState<(ThreadHash, EnvelopeHash)>, #[allow(clippy::type_complexity)] search_job: Option<(String, JoinHandle>>)>, @@ -182,14 +179,12 @@ pub struct CompactListing { filter_term: String, filtered_selection: Vec, filtered_order: HashMap, - selection: HashMap, /// If we must redraw on next redraw event dirty: bool, force_draw: bool, /// If `self.view` exists or not. focus: Focus, view: Box, - row_updates: SmallVec<[ThreadHash; 8]>, color_cache: ColorCache, movement: Option, @@ -199,30 +194,46 @@ pub struct CompactListing { } impl MailListingTrait for CompactListing { - 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 { - &mut self.selection + fn selection(&mut self) -> &mut HashMap { + &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) } @@ -230,8 +241,7 @@ impl MailListingTrait for CompactListing { /// chosen. fn refresh_mailbox(&mut self, context: &mut Context, force: bool) { self.dirty = true; - self.all_threads.clear(); - self.selection.clear(); + self.rows.clear(); let old_cursor_pos = self.cursor_pos; if !(self.cursor_pos.0 == self.new_cursor_pos.0 && self.cursor_pos.1 == self.new_cursor_pos.1) @@ -305,9 +315,9 @@ impl MailListingTrait for CompactListing { if !force && old_cursor_pos == self.new_cursor_pos { self.view.update(context); } else if self.unfocused() { - let thread = self.get_thread_under_cursor(self.cursor_pos.2); - - self.view = Box::new(ThreadView::new(self.new_cursor_pos, thread, None, context)); + if let Some(thread) = self.get_thread_under_cursor(self.cursor_pos.2) { + self.view = Box::new(ThreadView::new(self.new_cursor_pos, thread, None, context)); + } } } @@ -319,13 +329,12 @@ impl MailListingTrait for CompactListing { let account = &context.accounts[&self.cursor_pos.0]; let threads = account.collection.get_threads(self.cursor_pos.1); - self.order.clear(); + self.rows.clear(); // Use account settings only if no sortcmd has been used if !self.sortcmd { self.sort = context.accounts[&self.cursor_pos.0].settings.account.order } self.length = 0; - let mut rows = Vec::with_capacity(1024); let mut min_width = (0, 0, 0, 0); #[allow(clippy::type_complexity)] let mut row_widths: ( @@ -426,11 +435,17 @@ impl MailListingTrait for CompactListing { + 1 + entry_strings.tags.grapheme_width(), ); /* subject */ - rows.push(((self.length, (thread, root_env_hash)), entry_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(), + entry_strings, + ); self.length += 1; } @@ -438,23 +453,22 @@ impl MailListingTrait for CompactListing { /* 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); self.data_columns.segment_tree[0] = row_widths.0.into(); /* 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); self.data_columns.segment_tree[1] = row_widths.1.into(); /* 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(); /* 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); self.data_columns.segment_tree[3] = row_widths.3.into(); - self.rows = rows; self.rows_drawn = SegmentTree::from( std::iter::repeat(1) .take(self.rows.len()) @@ -495,14 +509,15 @@ impl ListingTrait for CompactListing { 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 thread_hash = if let Some(h) = self.get_thread_under_cursor(idx) { + h + } else { return; - } - let thread_hash = self.get_thread_under_cursor(idx); + }; let account = &context.accounts[&self.cursor_pos.0]; let threads = account.collection.get_threads(self.cursor_pos.1); @@ -513,7 +528,7 @@ impl ListingTrait for CompactListing { idx % 2 == 0, thread.unseen() > 0, self.cursor_pos.2 == idx, - self.selection[&thread_hash] + self.rows.is_thread_selected(thread_hash) ); let (upper_left, bottom_right) = area; let x = get_x(upper_left) @@ -723,25 +738,26 @@ impl ListingTrait for CompactListing { let account = &context.accounts[&self.cursor_pos.0]; let threads = account.collection.get_threads(self.cursor_pos.1); for r in 0..cmp::min(self.length - top_idx, rows) { - let thread_hash = self.get_thread_under_cursor(r + top_idx); - let row_attr = row_attr!( - self.color_cache, - (r + top_idx) % 2 == 0, - threads.thread_ref(thread_hash).unseen() > 0, - self.cursor_pos.2 == (r + top_idx), - self.selection[&thread_hash] - ); - change_colors( - grid, - ( - pos_inc(upper_left, (0, r)), - (flag_x.saturating_sub(1), get_y(upper_left) + r), - ), - row_attr.fg, - row_attr.bg, - ); - for x in flag_x..get_x(bottom_right) { - grid[(x, get_y(upper_left) + r)].set_bg(row_attr.bg); + if let Some(thread_hash) = self.get_thread_under_cursor(r + top_idx) { + let row_attr = row_attr!( + self.color_cache, + (r + top_idx) % 2 == 0, + threads.thread_ref(thread_hash).unseen() > 0, + self.cursor_pos.2 == (r + top_idx), + self.rows.is_thread_selected(thread_hash) + ); + change_colors( + grid, + ( + pos_inc(upper_left, (0, r)), + (flag_x.saturating_sub(1), get_y(upper_left) + r), + ), + row_attr.fg, + row_attr.bg, + ); + for x in flag_x..get_x(bottom_right) { + grid[(x, get_y(upper_left) + r)].set_bg(row_attr.bg); + } } } @@ -771,13 +787,10 @@ impl ListingTrait for CompactListing { results: SmallVec<[EnvelopeHash; 512]>, context: &Context, ) { - 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(); let account = &context.accounts[&self.cursor_pos.0]; let threads = account.collection.get_threads(self.cursor_pos.1); @@ -793,7 +806,7 @@ impl ListingTrait for CompactListing { 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() - 1); @@ -844,7 +857,7 @@ impl ListingTrait for CompactListing { 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; @@ -882,19 +895,15 @@ impl CompactListing { sort: (Default::default(), Default::default()), sortcmd: false, subsort: (SortField::Date, SortOrder::Desc), - all_threads: HashSet::default(), - order: HashMap::default(), search_job: None, select_job: None, filter_term: String::new(), filtered_selection: Vec::new(), filtered_order: HashMap::default(), - selection: HashMap::default(), focus: Focus::None, - row_updates: SmallVec::new(), data_columns: DataColumns::default(), rows_drawn: SegmentTree::default(), - rows: vec![], + rows: RowsState::default(), dirty: true, force_draw: true, view: Box::new(ThreadView::default()), @@ -962,7 +971,7 @@ impl CompactListing { }, flag: FlagString(format!( "{selected}{snoozed}{unseen}{attachments}{whitespace}", - selected = if self.selection.get(&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 @@ -1010,7 +1019,7 @@ impl CompactListing { } else { "" }, - whitespace = if self.selection.get(&hash).cloned().unwrap_or(false) + whitespace = if self.rows.selection.get(&e.hash()).cloned().unwrap_or(false) || thread.unseen() > 0 || thread.snoozed() || thread.has_attachments() @@ -1025,27 +1034,21 @@ impl CompactListing { } } - fn get_thread_under_cursor(&self, cursor: usize) -> ThreadHash { + fn get_thread_under_cursor(&self, cursor: usize) -> Option { 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(|(h, _)| h) + .cloned() } 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 threads = account.collection.get_threads(self.cursor_pos.1); - let thread = threads.thread_ref(thread_hash); - let thread_node_hash = threads.thread_group_iter(thread_hash).next().unwrap().1; let selected_flag_len = mailbox_settings!( context[self.cursor_pos.0][&self.cursor_pos.1] @@ -1084,143 +1087,144 @@ impl CompactListing { .unwrap_or(super::DEFAULT_ATTACHMENT_FLAG) .grapheme_width(); - if let Some(env_hash) = threads.thread_nodes()[&thread_node_hash].message() { - if !account.contains_key(env_hash) { - /* The envelope has been renamed or removed, so wait for the appropriate event to - * arrive */ - return; - } - let idx = self.order[&thread_hash]; - let row_attr = row_attr!( - self.color_cache, - idx % 2 == 0, - thread.unseen() > 0, - false, - false, - ); - let envelope: EnvelopeRef = account.collection.get_env(env_hash); - let strings = self.make_entry_string(&envelope, context, &threads, thread_hash); - drop(envelope); - let columns = &mut self.data_columns.columns; - let min_width = ( - columns[0].size().0, - columns[1].size().0, - columns[2].size().0, - columns[3].size().0, - ); - let (x, _) = write_string_to_grid( - &idx.to_string(), - &mut columns[0], - row_attr.fg, - row_attr.bg, - row_attr.attrs, - ((0, idx), (min_width.0, idx)), - None, - ); - for c in columns[0].row_iter(x..min_width.0, idx) { - columns[0][c].set_bg(row_attr.bg).set_ch(' '); - } - let (x, _) = write_string_to_grid( - &strings.date, - &mut columns[1], - row_attr.fg, - row_attr.bg, - row_attr.attrs, - ((0, idx), (min_width.1.saturating_sub(1), idx)), - None, - ); - for c in columns[1].row_iter(x..min_width.1, idx) { - columns[1][c].set_bg(row_attr.bg).set_ch(' '); - } - let (x, _) = write_string_to_grid( - &strings.from, - &mut columns[2], - row_attr.fg, - row_attr.bg, - row_attr.attrs, - ((0, idx), (min_width.2, idx)), - None, - ); - for c in columns[2].row_iter(x..min_width.2, idx) { - columns[2][c].set_bg(row_attr.bg).set_ch(' '); - } - let (x, _) = write_string_to_grid( - &strings.flag, - &mut columns[3], - row_attr.fg, - row_attr.bg, - row_attr.attrs, - ((0, idx), (min_width.3, idx)), - None, - ); - let (x, _) = write_string_to_grid( - &strings.subject, - &mut columns[3], - row_attr.fg, - row_attr.bg, - row_attr.attrs, - ((x, idx), (min_width.3, idx)), - None, - ); - columns[3][(x, idx)].set_bg(row_attr.bg).set_ch(' '); - let x = { - let mut x = x + 1; - for (t, &color) in strings.tags.split_whitespace().zip(strings.tags.1.iter()) { - let color = color.unwrap_or(self.color_cache.tag_default.bg); - let (_x, _) = write_string_to_grid( - t, - &mut columns[3], - self.color_cache.tag_default.fg, - color, - self.color_cache.tag_default.attrs, - ((x + 1, idx), (min_width.3, idx)), - None, - ); - for c in columns[3].row_iter(x..(x + 1), idx) { - columns[3][c].set_bg(color); - } - for c in columns[3].row_iter(_x..(_x + 1), idx) { - columns[3][c].set_bg(color).set_keep_bg(true); - } - for c in columns[3].row_iter((x + 1)..(_x + 1), idx) { - columns[3][c] - .set_keep_fg(true) - .set_keep_bg(true) - .set_keep_attrs(true); - } - for c in columns[3].row_iter(x..(x + 1), idx) { - columns[3][c].set_keep_bg(true); - } - x = _x + 1; - columns[3][(x, idx)].set_bg(row_attr.bg).set_ch(' '); + if !account.contains_key(env_hash) { + /* The envelope has been renamed or removed, so wait for the appropriate event to + * arrive */ + return; + } + let envelope: EnvelopeRef = account.collection.get_env(env_hash); + let thread_hash = self.rows.env_to_thread[&env_hash]; + let threads = account.collection.get_threads(self.cursor_pos.1); + let thread = threads.thread_ref(thread_hash); + let idx = self.rows.thread_order[&thread_hash]; + let row_attr = row_attr!( + self.color_cache, + idx % 2 == 0, + thread.unseen() > 0, + false, + false, + ); + let strings = self.make_entry_string(&envelope, context, &threads, thread_hash); + drop(envelope); + let columns = &mut self.data_columns.columns; + let min_width = ( + columns[0].size().0, + columns[1].size().0, + columns[2].size().0, + columns[3].size().0, + ); + let (x, _) = write_string_to_grid( + &idx.to_string(), + &mut columns[0], + row_attr.fg, + row_attr.bg, + row_attr.attrs, + ((0, idx), (min_width.0, idx)), + None, + ); + for c in columns[0].row_iter(x..min_width.0, idx) { + columns[0][c].set_bg(row_attr.bg).set_ch(' '); + } + let (x, _) = write_string_to_grid( + &strings.date, + &mut columns[1], + row_attr.fg, + row_attr.bg, + row_attr.attrs, + ((0, idx), (min_width.1.saturating_sub(1), idx)), + None, + ); + for c in columns[1].row_iter(x..min_width.1, idx) { + columns[1][c].set_bg(row_attr.bg).set_ch(' '); + } + let (x, _) = write_string_to_grid( + &strings.from, + &mut columns[2], + row_attr.fg, + row_attr.bg, + row_attr.attrs, + ((0, idx), (min_width.2, idx)), + None, + ); + for c in columns[2].row_iter(x..min_width.2, idx) { + columns[2][c].set_bg(row_attr.bg).set_ch(' '); + } + let (x, _) = write_string_to_grid( + &strings.flag, + &mut columns[3], + row_attr.fg, + row_attr.bg, + row_attr.attrs, + ((0, idx), (min_width.3, idx)), + None, + ); + let (x, _) = write_string_to_grid( + &strings.subject, + &mut columns[3], + row_attr.fg, + row_attr.bg, + row_attr.attrs, + ((x, idx), (min_width.3, idx)), + None, + ); + columns[3][(x, idx)].set_bg(row_attr.bg).set_ch(' '); + let x = { + let mut x = x + 1; + for (t, &color) in strings.tags.split_whitespace().zip(strings.tags.1.iter()) { + let color = color.unwrap_or(self.color_cache.tag_default.bg); + let (_x, _) = write_string_to_grid( + t, + &mut columns[3], + self.color_cache.tag_default.fg, + color, + self.color_cache.tag_default.attrs, + ((x + 1, idx), (min_width.3, idx)), + None, + ); + for c in columns[3].row_iter(x..(x + 1), idx) { + columns[3][c].set_bg(color); } - x - }; - for c in columns[3].row_iter(x..min_width.3, idx) { - columns[3][c].set_ch(' ').set_bg(row_attr.bg); - } - /* Set fg color for flags */ - let mut x = 0; - if self.selection.get(&thread_hash).cloned().unwrap_or(false) { - x += selected_flag_len; - } - if thread.snoozed() { - for x in x..(x + thread_snoozed_flag_len) { - columns[3][(x, idx)].set_fg(self.color_cache.thread_snooze_flag.fg); + for c in columns[3].row_iter(_x..(_x + 1), idx) { + columns[3][c].set_bg(color).set_keep_bg(true); } - x += thread_snoozed_flag_len; + for c in columns[3].row_iter((x + 1)..(_x + 1), idx) { + columns[3][c] + .set_keep_fg(true) + .set_keep_bg(true) + .set_keep_attrs(true); + } + for c in columns[3].row_iter(x..(x + 1), idx) { + columns[3][c].set_keep_bg(true); + } + x = _x + 1; + columns[3][(x, idx)].set_bg(row_attr.bg).set_ch(' '); } - if thread.unseen() > 0 { - x += unseen_flag_len; + x + }; + for c in columns[3].row_iter(x..min_width.3, idx) { + columns[3][c].set_ch(' ').set_bg(row_attr.bg); + } + /* Set fg color for flags */ + let mut x = 0; + if self.rows.selection.get(&env_hash).cloned().unwrap_or(false) { + x += selected_flag_len; + } + if thread.snoozed() { + for x in x..(x + thread_snoozed_flag_len) { + columns[3][(x, idx)].set_fg(self.color_cache.thread_snooze_flag.fg); } - if thread.has_attachments() { - for x in x..(x + attachment_flag_len) { - columns[3][(x, idx)].set_fg(self.color_cache.attachment_flag.fg); - } + x += thread_snoozed_flag_len; + } + if thread.unseen() > 0 { + x += unseen_flag_len; + } + if thread.has_attachments() { + for x in x..(x + attachment_flag_len) { + columns[3][(x, idx)].set_fg(self.color_cache.attachment_flag.fg); } - *self.rows.get_mut(idx).unwrap() = ((idx, (thread_hash, env_hash)), strings); - self.rows_drawn.update(idx, 1); } + *self.rows.entries.get_mut(idx).unwrap() = ((thread_hash, env_hash), strings); + self.rows_drawn.update(idx, 1); } fn draw_rows(&mut self, context: &Context, start: usize, end: usize) { @@ -1283,10 +1287,14 @@ impl CompactListing { .unwrap_or(super::DEFAULT_ATTACHMENT_FLAG) .grapheme_width(); - for ((idx, (thread_hash, root_env_hash)), strings) in - self.rows.iter().skip(start).take(end - start + 1) + for (idx, ((thread_hash, root_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(*root_env_hash) { //debug!("key = {}", root_env_hash); //debug!( @@ -1304,7 +1312,7 @@ impl CompactListing { idx % 2 == 0, thread.unseen() > 0, self.cursor_pos.2 == idx, - self.selection[thread_hash] + self.rows.selection[root_env_hash] ); let (x, _) = write_string_to_grid( &idx.to_string(), @@ -1422,7 +1430,13 @@ impl CompactListing { } /* Set fg color for flags */ let mut x = 0; - if self.selection.get(thread_hash).cloned().unwrap_or(false) { + if self + .rows + .selection + .get(root_env_hash) + .cloned() + .unwrap_or(false) + { x += selected_flag_len; } if thread.snoozed() { @@ -1465,9 +1479,10 @@ impl CompactListing { } let thread = threads.find_group(threads.thread_nodes[&env_thread_node_hash].group); - if self.all_threads.contains(&thread) { - self.selection - .entry(thread) + if self.rows.all_threads.contains(&thread) { + self.rows + .selection + .entry(env_hash) .and_modify(|entry| *entry = true); } } @@ -1545,28 +1560,28 @@ impl Component for CompactListing { 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); + } } } } @@ -1574,49 +1589,48 @@ impl Component for CompactListing { 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); + } } } } @@ -1627,20 +1641,19 @@ impl Component for CompactListing { 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( @@ -1649,60 +1662,61 @@ impl Component for CompactListing { 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); + } } } } @@ -1711,10 +1725,10 @@ impl Component for CompactListing { self.force_draw = true; } - if !self.row_updates.is_empty() { - while let Some(row) = self.row_updates.pop() { + if !self.rows.row_updates.is_empty() { + 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.new_cursor_pos.2).wrapping_div(rows); let top_idx = page_no * rows; @@ -1784,9 +1798,11 @@ impl Component for CompactListing { && (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 = Box::new(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 = + Box::new(ThreadView::new(self.cursor_pos, thread, None, context)); + self.set_focus(Focus::Entry, context); + } return true; } UIEvent::Input(ref k) @@ -1827,9 +1843,10 @@ impl Component for CompactListing { 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_hash) = self.get_thread_under_cursor(self.cursor_pos.2) { + self.rows + .update_selection_with_thread(thread_hash, |e| *e = !*e); + } } return true; } @@ -1854,6 +1871,7 @@ impl Component for CompactListing { 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 @@ -1866,8 +1884,9 @@ impl Component for CompactListing { let is_snoozed = threads.thread_ref(thread).snoozed(); threads.thread_ref_mut(thread).set_snoozed(!is_snoozed); }); - self.row_updates.push(thread); + self.rows.row_updates.push(thread); self.refresh_mailbox(context, false); + */ return true; } @@ -1938,8 +1957,8 @@ impl Component for CompactListing { let thread: ThreadHash = threads.find_group(threads.thread_nodes()[&new_env_thread_node_hash].group); drop(threads); - if self.order.contains_key(&thread) { - self.row_updates.push(thread); + if self.rows.contains_thread(thread) { + self.rows.row_update_add_thread(thread); } self.dirty = true; @@ -1950,7 +1969,7 @@ impl Component for CompactListing { } } 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); } @@ -1968,8 +1987,8 @@ impl Component for CompactListing { let thread: ThreadHash = threads.find_group(threads.thread_nodes()[&new_env_thread_node_hash].group); drop(threads); - if self.order.contains_key(&thread) { - self.row_updates.push(thread); + if self.rows.contains_thread(thread) { + self.rows.row_update_add_thread(thread); } self.dirty = true; @@ -1987,9 +2006,14 @@ impl Component for CompactListing { } 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; diff --git a/src/components/mail/listing/conversations.rs b/src/components/mail/listing/conversations.rs index 3e3df96e..2b05a7e1 100644 --- a/src/components/mail/listing/conversations.rs +++ b/src/components/mail/listing/conversations.rs @@ -100,24 +100,20 @@ pub struct ConversationsListing { length: usize, sort: (SortField, SortOrder), subsort: (SortField, SortOrder), - all_threads: HashSet, - order: HashMap, - #[allow(clippy::type_complexity)] - rows: std::result::Result, String>, + rows: RowsState<(ThreadHash, EnvelopeHash)>, + error: std::result::Result<(), String>, #[allow(clippy::type_complexity)] search_job: Option<(String, JoinHandle>>)>, filter_term: String, filtered_selection: Vec, filtered_order: HashMap, - selection: HashMap, /// 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, @@ -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 { - &mut self.selection + fn selection(&mut self) -> &mut HashMap { + &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 { 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; } diff --git a/src/components/mail/listing/offline.rs b/src/components/mail/listing/offline.rs index bd3402c7..3fdcebe7 100644 --- a/src/components/mail/listing/offline.rs +++ b/src/components/mail/listing/offline.rs @@ -26,23 +26,23 @@ use std::borrow::Cow; #[derive(Debug)] pub struct OfflineListing { cursor_pos: (AccountHash, MailboxHash), - _row_updates: SmallVec<[ThreadHash; 8]>, - _selection: HashMap, + _row_updates: SmallVec<[EnvelopeHash; 8]>, + _selection: HashMap, messages: Vec>, 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 { + fn selection(&mut self) -> &mut HashMap { &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) {} + + fn modifier_command(&self) -> Option { + None + } + fn set_movement(&mut self, _: PageMovement) {} fn focus(&self) -> Focus { diff --git a/src/components/mail/listing/plain.rs b/src/components/mail/listing/plain.rs index c4073f11..c4296a4c 100644 --- a/src/components/mail/listing/plain.rs +++ b/src/components/mail/listing/plain.rs @@ -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, - order: HashMap, + rows: RowsState<(ThreadHash, EnvelopeHash)>, /// Cache current view. data_columns: DataColumns, @@ -138,9 +137,6 @@ pub struct PlainListing { filter_term: String, filtered_selection: Vec, filtered_order: HashMap, - selection: HashMap, - _selection: HashMap, - thread_node_hashes: HashMap, local_collection: Vec, /// 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>>, movement: Option, + modifier_active: bool, + modifier_command: Option, 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 { - &mut self._selection + fn selection(&mut self) -> &mut HashMap { + &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>; 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) { + self.modifier_command = new_val; + } + + fn modifier_command(&self) -> Option { + 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>) { 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 { 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; diff --git a/src/components/mail/listing/thread.rs b/src/components/mail/listing/thread.rs index 464fc352..6129730d 100644 --- a/src/components/mail/listing/thread.rs +++ b/src/components/mail/listing/thread.rs @@ -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, - order: HashMap, + 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>, + modifier_active: bool, + modifier_command: Option, movement: Option, 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 { - &mut self.selection + fn selection(&mut self) -> &mut HashMap { + &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 = 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) { + self.modifier_command = new_val; + } + + fn modifier_command(&self) -> Option { + 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 { + 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);