summaryrefslogtreecommitdiffstats
path: root/ui/src/components/mail/listing/mod.rs
diff options
context:
space:
mode:
authorManos Pitsidianakis <el13635@mail.ntua.gr>2018-09-07 09:44:37 +0300
committerManos Pitsidianakis <el13635@mail.ntua.gr>2019-06-10 19:40:33 +0300
commitbcef22b3f30b87176c9b3c2d336366510a930f2b (patch)
tree452a78e9c4b4624d0e5e85ce35f286d383765893 /ui/src/components/mail/listing/mod.rs
parent6003bdd28c2b87425a6a23092130e7bf4dbc6586 (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.rs655
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(),
+ }
}
}