diff options
author | Manos Pitsidianakis <el13635@mail.ntua.gr> | 2018-09-07 09:44:37 +0300 |
---|---|---|
committer | Manos Pitsidianakis <el13635@mail.ntua.gr> | 2019-06-10 19:40:33 +0300 |
commit | bcef22b3f30b87176c9b3c2d336366510a930f2b (patch) | |
tree | 452a78e9c4b4624d0e5e85ce35f286d383765893 /ui/src/components/mail/listing/mod.rs | |
parent | 6003bdd28c2b87425a6a23092130e7bf4dbc6586 (diff) |
ui: add Listing component with 3 modes: compact plain and threaded
Diffstat (limited to 'ui/src/components/mail/listing/mod.rs')
-rw-r--r-- | ui/src/components/mail/listing/mod.rs | 655 |
1 files changed, 57 insertions, 598 deletions
diff --git a/ui/src/components/mail/listing/mod.rs b/ui/src/components/mail/listing/mod.rs index be1a2ef8..d600d33a 100644 --- a/ui/src/components/mail/listing/mod.rs +++ b/ui/src/components/mail/listing/mod.rs @@ -24,621 +24,75 @@ use super::*; mod compact; pub use self::compact::*; -const MAX_COLS: usize = 500; +mod thread; +pub use self::thread::*; -/// A list of all mail (`Envelope`s) in a `Mailbox`. On `\n` it opens the `Envelope` content in a -/// `MailView`. -#[derive(Debug)] -pub struct PlainListing { - /// (x, y, z): x is accounts, y is folders, z is index inside a folder. - cursor_pos: (usize, usize, usize), - new_cursor_pos: (usize, usize, usize), - length: usize, - local_collection: Vec<EnvelopeHash>, - sort: (SortField, SortOrder), - subsort: (SortField, SortOrder), - /// Cache current view. - content: CellBuffer, - /// If we must redraw on next redraw event - dirty: bool, - /// If `self.view` exists or not. - unfocused: bool, - view: Option<MailView>, -} +mod plain; +pub use self::plain::*; -impl Default for PlainListing { - fn default() -> Self { - Self::new() - } +#[derive(Debug)] +pub enum Listing { + Plain(PlainListing), + Threaded(ThreadListing), + Compact(CompactListing), } -impl fmt::Display for PlainListing { +impl fmt::Display for Listing { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "mail") - } -} - -impl PlainListing { - /// Helper function to format entry strings for PlainListing */ - /* TODO: Make this configurable */ - fn make_entry_string(e: &Envelope, idx: usize) -> String { - format!( - "{} {} {}", - idx, - &e.datetime().format("%Y-%m-%d %H:%M:%S").to_string(), - e.subject() - ) - } - - pub fn new() -> Self { - let content = CellBuffer::new(0, 0, Cell::with_char(' ')); - PlainListing { - cursor_pos: (0, 1, 0), - new_cursor_pos: (0, 0, 0), - length: 0, - local_collection: Vec::new(), - sort: (Default::default(), Default::default()), - subsort: (Default::default(), Default::default()), - content, - dirty: true, - unfocused: false, - view: None, - } - } - /// Fill the `self.content` `CellBuffer` with the contents of the account folder the user has - /// chosen. - fn refresh_mailbox(&mut self, context: &mut Context) { - self.dirty = true; - self.cursor_pos.2 = 0; - self.new_cursor_pos.2 = 0; - self.cursor_pos.1 = self.new_cursor_pos.1; - self.cursor_pos.0 = self.new_cursor_pos.0; - - let threaded = context.accounts[self.cursor_pos.0] - .runtime_settings - .conf() - .threaded(); - // Inform State that we changed the current folder view. - context.replies.push_back(UIEvent { - id: 0, - event_type: UIEventType::RefreshMailbox((self.cursor_pos.0, self.cursor_pos.1)), - }); - // Get mailbox as a reference. - // - loop { - // TODO: Show progress visually - if context.accounts[self.cursor_pos.0] - .status(self.cursor_pos.1) - .is_ok() - { - break; - } - } - let mailbox = &context.accounts[self.cursor_pos.0][self.cursor_pos.1] - .as_ref() - .unwrap(); - - self.length = mailbox.len(); - self.content = CellBuffer::new(MAX_COLS, self.length + 1, Cell::with_char(' ')); - if self.length == 0 { - write_string_to_grid( - &format!("Folder `{}` is empty.", mailbox.folder.name()), - &mut self.content, - Color::Default, - Color::Default, - ((0, 0), (MAX_COLS - 1, 0)), - true, - ); - return; - } - - // Populate `CellBuffer` with every entry. - let mut idx = 0; - for y in 0..=self.length { - if idx >= self.length { - /* No more entries left, so fill the rest of the area with empty space */ - clear_area(&mut self.content, ((0, y), (MAX_COLS - 1, self.length))); - break; - } - /* Write an entire line for each envelope entry. */ - self.local_collection = mailbox.collection.keys().map(|v| *v).collect(); - let sort = self.sort; - self.local_collection.sort_by(|a, b| match sort { - (SortField::Date, SortOrder::Desc) => { - let ma = &mailbox.collection[a]; - let mb = &mailbox.collection[b]; - mb.date().cmp(&ma.date()) - } - (SortField::Date, SortOrder::Asc) => { - let ma = &mailbox.collection[a]; - let mb = &mailbox.collection[b]; - ma.date().cmp(&mb.date()) - } - (SortField::Subject, SortOrder::Desc) => { - let ma = &mailbox.collection[a]; - let mb = &mailbox.collection[b]; - ma.subject().cmp(&mb.subject()) - } - (SortField::Subject, SortOrder::Asc) => { - let ma = &mailbox.collection[a]; - let mb = &mailbox.collection[b]; - mb.subject().cmp(&ma.subject()) - } - }); - let envelope: &Envelope = &mailbox.collection[&self.local_collection[idx]]; - - let fg_color = if !envelope.is_seen() { - Color::Byte(0) - } else { - Color::Default - }; - let bg_color = if !envelope.is_seen() { - Color::Byte(251) - } else if idx % 2 == 0 { - Color::Byte(236) - } else { - Color::Default - }; - let (x, y) = write_string_to_grid( - &PlainListing::make_entry_string(envelope, idx), - &mut self.content, - fg_color, - bg_color, - ((0, y), (MAX_COLS - 1, y)), - false, - ); - - for x in x..MAX_COLS { - self.content[(x, y)].set_ch(' '); - self.content[(x, y)].set_bg(bg_color); - } - - idx += 1; + match self { + Listing::Compact(l) => write!(f, "{}", l), + Listing::Plain(l) => write!(f, "{}", l), + Listing::Threaded(l) => write!(f, "{}", l), } } +} - fn highlight_line_self(&mut self, idx: usize, context: &Context) { - let threaded = context.accounts[self.cursor_pos.0] - .runtime_settings - .conf() - .threaded(); - let mailbox = &context.accounts[self.cursor_pos.0][self.cursor_pos.1] - .as_ref() - .unwrap(); - let envelope: &Envelope = &mailbox.collection[&self.local_collection[idx]]; - - let fg_color = if !envelope.is_seen() { - Color::Byte(0) - } else { - Color::Default - }; - let bg_color = if !envelope.is_seen() { - Color::Byte(251) - } else if idx % 2 == 0 { - Color::Byte(236) - } else { - Color::Default - }; - change_colors( - &mut self.content, - ((0, idx), (MAX_COLS - 1, idx)), - fg_color, - bg_color, - ); - } - - fn highlight_line(&self, grid: &mut CellBuffer, area: Area, idx: usize, context: &Context) { - let threaded = context.accounts[self.cursor_pos.0] - .runtime_settings - .conf() - .threaded(); - let mailbox = &context.accounts[self.cursor_pos.0][self.cursor_pos.1] - .as_ref() - .unwrap(); - let envelope: &Envelope = &mailbox.collection[&self.local_collection[idx]]; - - let fg_color = if !envelope.is_seen() { - Color::Byte(0) - } else { - Color::Default - }; - let bg_color = if self.cursor_pos.2 == idx { - Color::Byte(246) - } else if !envelope.is_seen() { - Color::Byte(251) - } else if idx % 2 == 0 { - Color::Byte(236) - } else { - Color::Default - }; - change_colors(grid, area, fg_color, bg_color); - } - - /// Draw the list of `Envelope`s. - fn draw_list(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { - if self.cursor_pos.1 != self.new_cursor_pos.1 { - self.refresh_mailbox(context); - } - let upper_left = upper_left!(area); - let bottom_right = bottom_right!(area); - if self.length == 0 { - clear_area(grid, area); - copy_area(grid, &self.content, area, ((0, 0), (MAX_COLS - 1, 0))); - context.dirty_areas.push_back(area); - return; - } - let rows = get_y(bottom_right) - get_y(upper_left) + 1; - let prev_page_no = (self.cursor_pos.2).wrapping_div(rows); - let page_no = (self.new_cursor_pos.2).wrapping_div(rows); - - let top_idx = page_no * rows; - - /* If cursor position has changed, remove the highlight from the previous position and - * apply it in the new one. */ - if self.cursor_pos.2 != self.new_cursor_pos.2 && prev_page_no == page_no { - let old_cursor_pos = self.cursor_pos; - self.cursor_pos = self.new_cursor_pos; - for idx in &[old_cursor_pos.2, self.new_cursor_pos.2] { - if *idx >= self.length { - continue; //bounds check - } - let new_area = ( - set_y(upper_left, get_y(upper_left) + (*idx % rows)), - set_y(bottom_right, get_y(upper_left) + (*idx % rows)), - ); - self.highlight_line(grid, new_area, *idx, context); - context.dirty_areas.push_back(new_area); - } - return; - } else if self.cursor_pos != self.new_cursor_pos { - self.cursor_pos = self.new_cursor_pos; - } - - /* Page_no has changed, so draw new page */ - copy_area( - grid, - &self.content, - area, - ((0, top_idx), (MAX_COLS - 1, self.length)), - ); - self.highlight_line( - grid, - ( - set_y(upper_left, get_y(upper_left) + (self.cursor_pos.2 % rows)), - set_y(bottom_right, get_y(upper_left) + (self.cursor_pos.2 % rows)), - ), - self.cursor_pos.2, - context, - ); - context.dirty_areas.push_back(area); - } - - fn make_thread_entry( - envelope: &Envelope, - idx: usize, - indent: usize, - node_idx: usize, - nodes: &Threads, - indentations: &[bool], - idx_width: usize, - //op: Box<BackendOp>, - ) -> String { - let has_sibling = nodes.has_sibling(node_idx); - let has_parent = nodes[node_idx].has_parent(); - let show_subject = nodes[node_idx].show_subject(); - - let mut s = format!( - "{}{}{} ", - idx, - " ".repeat(idx_width + 2 - (idx.to_string().chars().count())), - PlainListing::format_date(&envelope) - ); - for i in 0..indent { - if indentations.len() > i && indentations[i] { - s.push('│'); - } else { - s.push(' '); - } - if i > 0 { - s.push(' '); - } - } - if indent > 0 { - if has_sibling && has_parent { - s.push('├'); - } else if has_sibling { - s.push('┬'); - } else { - s.push('└'); - } - s.push('─'); - s.push('>'); - } - - if show_subject { - s.push_str(&format!("{:.85}", envelope.subject())); - } - /* TODO Very slow since we have to build all attachments - let attach_count = envelope.body(op).count_attachments(); - if attach_count > 1 { - s.push_str(&format!(" {}∞ ", attach_count - 1)); - } - */ - s - } - fn format_date(envelope: &Envelope) -> String { - let d = std::time::UNIX_EPOCH + std::time::Duration::from_secs(envelope.date()); - let now: std::time::Duration = std::time::SystemTime::now().duration_since(d).unwrap(); - match now.as_secs() { - n if n < 10 * 60 * 60 => format!("{} hours ago{}", n / (60 * 60), " ".repeat(8)), - n if n < 24 * 60 * 60 => format!("{} hours ago{}", n / (60 * 60), " ".repeat(7)), - n if n < 4 * 24 * 60 * 60 => { - format!("{} days ago{}", n / (24 * 60 * 60), " ".repeat(9)) - } - _ => envelope.datetime().format("%Y-%m-%d %H:%M:%S").to_string(), - } +impl Default for Listing { + fn default() -> Self { + Listing::Compact(Default::default()) } } -impl Component for PlainListing { +impl Component for Listing { fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { - if !self.unfocused { - if !self.is_dirty() { - return; - } - self.dirty = false; - /* Draw the entire list */ - self.draw_list(grid, area, context); - } else { - let upper_left = upper_left!(area); - let bottom_right = bottom_right!(area); - if self.length == 0 && self.dirty { - clear_area(grid, area); - context.dirty_areas.push_back(area); - } - - /* Render the mail body in a pager, basically copy what HSplit does */ - let total_rows = get_y(bottom_right) - get_y(upper_left); - let pager_ratio = context.runtime_settings.pager.pager_ratio; - let bottom_entity_rows = (pager_ratio * total_rows) / 100; - - if bottom_entity_rows > total_rows { - clear_area(grid, area); - context.dirty_areas.push_back(area); - return; - } - /* Mark message as read */ - let idx = self.cursor_pos.2; - let must_highlight = { - if self.length == 0 { - false - } else { - let threaded = context.accounts[self.cursor_pos.0] - .runtime_settings - .conf() - .threaded(); - let account = &mut context.accounts[self.cursor_pos.0]; - let (hash, is_seen) = { - let mailbox = &mut account[self.cursor_pos.1].as_mut().unwrap(); - let envelope: &mut Envelope = &mut mailbox - .collection - .entry(self.local_collection[idx]) - .or_default(); - (envelope.hash(), envelope.is_seen()) - }; - if !is_seen { - let op = { - let backend = &account.backend; - backend.operation(hash) - }; - let mailbox = &mut account[self.cursor_pos.1].as_mut().unwrap(); - let envelope: &mut Envelope = &mut mailbox - .collection - .entry(self.local_collection[idx]) - .or_default(); - envelope.set_seen(op).unwrap(); - true - } else { - false - } - } - }; - if must_highlight { - self.highlight_line_self(idx, context); - } - let mid = get_y(upper_left) + total_rows - bottom_entity_rows; - self.draw_list( - grid, - ( - upper_left, - (get_x(bottom_right), get_y(upper_left) + mid - 1), - ), - context, - ); - if self.length == 0 { - self.dirty = false; - return; - } - { - /* TODO: Move the box drawing business in separate functions */ - if get_x(upper_left) > 0 && grid[(get_x(upper_left) - 1, mid)].ch() == VERT_BOUNDARY - { - grid[(get_x(upper_left) - 1, mid)].set_ch(LIGHT_VERTICAL_AND_RIGHT); - } - - for i in get_x(upper_left)..=get_x(bottom_right) { - grid[(i, mid)].set_ch('─'); - } - context - .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() { - v.draw(grid, (set_y(upper_left, mid + 1), bottom_right), context); - } - return; - } - { - let threaded = context.accounts[self.cursor_pos.0] - .runtime_settings - .conf() - .threaded(); - let account = &context.accounts[self.cursor_pos.0]; - let mailbox = &account[self.cursor_pos.1].as_ref().unwrap(); - let mut coordinates = self.cursor_pos; - let coordinates = ( - coordinates.0, - coordinates.1, - self.local_collection[self.cursor_pos.2], - ); - self.view = Some(MailView::new(coordinates, None, None)); - } - self.view.as_mut().unwrap().draw( - grid, - (set_y(upper_left, mid + 1), bottom_right), - context, - ); - self.dirty = false; + match self { + Listing::Compact(l) => l.draw(grid, area, context), + Listing::Plain(l) => l.draw(grid, area, context), + Listing::Threaded(l) => l.draw(grid, area, context), } } fn process_event(&mut self, event: &UIEvent, context: &mut Context) -> bool { - if let Some(ref mut v) = self.view { - if v.process_event(event, context) { - return true; - } + if match self { + Listing::Plain(l) => l.process_event(event, context), + Listing::Compact(l) => l.process_event(event, context), + Listing::Threaded(l) => l.process_event(event, context), + } { + return true; } + match event.event_type { - UIEventType::Input(Key::Up) => { - if self.cursor_pos.2 > 0 { - self.new_cursor_pos.2 -= 1; - self.dirty = true; - } - return true; - } - UIEventType::Input(Key::Down) => { - if self.length > 0 && self.new_cursor_pos.2 < self.length - 1 { - self.new_cursor_pos.2 += 1; - self.dirty = true; - } - return true; - } - UIEventType::Input(Key::Char('\n')) if !self.unfocused => { - self.unfocused = true; - self.dirty = true; - return true; - } - UIEventType::Input(Key::Char('m')) if !self.unfocused => { - context.replies.push_back(UIEvent { - id: 0, - event_type: UIEventType::Action(Tab(NewDraft)), - }); - return true; - } - UIEventType::Input(Key::Char('i')) if self.unfocused => { - self.unfocused = false; - self.dirty = true; - self.view = None; - return true; - } - UIEventType::Input(Key::Char(k @ 'J')) | UIEventType::Input(Key::Char(k @ 'K')) => { - let folder_length = context.accounts[self.cursor_pos.0].len(); - let accounts_length = context.accounts.len(); - match k { - 'J' if folder_length > 0 => { - if self.new_cursor_pos.1 < folder_length - 1 { - self.new_cursor_pos.1 = self.cursor_pos.1 + 1; - self.dirty = true; - self.refresh_mailbox(context); - } else if accounts_length > 0 && self.new_cursor_pos.0 < accounts_length - 1 - { - self.new_cursor_pos.0 = self.cursor_pos.0 + 1; - self.new_cursor_pos.1 = 0; - self.dirty = true; - self.refresh_mailbox(context); - } - } - 'K' => { - if self.cursor_pos.1 > 0 { - self.new_cursor_pos.1 = self.cursor_pos.1 - 1; - self.dirty = true; - self.refresh_mailbox(context); - } else if self.cursor_pos.0 > 0 { - self.new_cursor_pos.0 = self.cursor_pos.0 - 1; - self.new_cursor_pos.1 = 0; - self.dirty = true; - self.refresh_mailbox(context); - } - } - _ => {} - } - return true; - } - UIEventType::Input(Key::Char(k @ 'h')) | UIEventType::Input(Key::Char(k @ 'l')) => { - let accounts_length = context.accounts.len(); - match k { - 'h' if accounts_length > 0 && self.new_cursor_pos.0 < accounts_length - 1 => { - self.new_cursor_pos.0 = self.cursor_pos.0 + 1; - self.new_cursor_pos.1 = 0; - self.dirty = true; - self.refresh_mailbox(context); - } - 'l' if self.cursor_pos.0 > 0 => { - self.new_cursor_pos.0 = self.cursor_pos.0 - 1; - self.new_cursor_pos.1 = 0; - self.dirty = true; - self.refresh_mailbox(context); - } - _ => {} - } - return true; - } - UIEventType::RefreshMailbox(_) => { - self.dirty = true; - self.view = None; - } - UIEventType::MailboxUpdate((ref idxa, ref idxf)) => { - if *idxa == self.new_cursor_pos.0 && *idxf == self.new_cursor_pos.1 { - self.dirty = true; - self.refresh_mailbox(context); - } - } - UIEventType::ChangeMode(UIMode::Normal) => { - self.dirty = true; - } - UIEventType::Resize => { - self.dirty = true; - } + UIEventType::Resize => self.set_dirty(), UIEventType::Action(ref action) => match action { - Action::Listing(ListingAction::ToggleThreaded) => { - context.accounts[self.cursor_pos.0] - .runtime_settings - .conf_mut() - .toggle_threaded(); - self.refresh_mailbox(context); - self.dirty = true; - return true; - } - Action::ViewMailbox(idx) => { - self.new_cursor_pos.1 = *idx; - self.dirty = true; - self.refresh_mailbox(context); + Action::Listing(ListingAction::SetPlain) => { + if let Listing::Plain(_) = self { + return true; + } + *self = Listing::Plain(PlainListing::default()); return true; } - Action::SubSort(field, order) => { - eprintln!("SubSort {:?} , {:?}", field, order); - self.subsort = (*field, *order); - self.dirty = true; - self.refresh_mailbox(context); + Action::Listing(ListingAction::SetThreaded) => { + if let Listing::Threaded(_) = self { + return true; + } + self.set_dirty(); + *self = Listing::Threaded(ThreadListing::default()); return true; } - Action::Sort(field, order) => { - eprintln!("Sort {:?} , {:?}", field, order); - self.sort = (*field, *order); - self.dirty = true; - self.refresh_mailbox(context); + Action::Listing(ListingAction::SetCompact) => { + if let Listing::Compact(_) = self { + return true; + } + *self = Listing::Compact(CompactListing::default()); return true; } _ => {} @@ -648,12 +102,17 @@ impl Component for PlainListing { false } fn is_dirty(&self) -> bool { - self.dirty || self.view.as_ref().map(|p| p.is_dirty()).unwrap_or(false) + match self { + Listing::Compact(l) => l.is_dirty(), + Listing::Plain(l) => l.is_dirty(), + Listing::Threaded(l) => l.is_dirty(), + } } fn set_dirty(&mut self) { - if let Some(p) = self.view.as_mut() { - p.set_dirty(); - }; - self.dirty = true; + match self { + Listing::Compact(l) => l.set_dirty(), + Listing::Plain(l) => l.set_dirty(), + Listing::Threaded(l) => l.set_dirty(), + } } } |