From 104352e5950598f4a659bd593d587910af8adc12 Mon Sep 17 00:00:00 2001 From: Manos Pitsidianakis Date: Thu, 24 Nov 2022 16:43:53 +0200 Subject: [PATCH] Add table UI widget --- src/components/contacts/contact_list.rs | 2 +- src/components/mail/listing.rs | 13 +- src/components/mail/listing/compact.rs | 352 +++++-------------- src/components/mail/listing/conversations.rs | 15 +- src/components/mail/listing/plain.rs | 231 +++++------- src/components/mail/listing/thread.rs | 310 +++++----------- src/components/mail/view/thread.rs | 10 +- src/components/utilities.rs | 13 +- src/components/utilities/pager.rs | 2 +- src/components/utilities/tables.rs | 318 +++++++++++++++++ src/terminal/cells.rs | 89 ++++- src/terminal/position.rs | 11 + 12 files changed, 705 insertions(+), 661 deletions(-) create mode 100644 src/components/utilities/tables.rs diff --git a/src/components/contacts/contact_list.rs b/src/components/contacts/contact_list.rs index 52684cd9..8fc89e2b 100644 --- a/src/components/contacts/contact_list.rs +++ b/src/components/contacts/contact_list.rs @@ -46,7 +46,7 @@ pub struct ContactList { new_cursor_pos: usize, account_pos: usize, length: usize, - data_columns: DataColumns, + data_columns: DataColumns<4>, initialized: bool, theme_default: ThemeAttribute, highlight_theme: ThemeAttribute, diff --git a/src/components/mail/listing.rs b/src/components/mail/listing.rs index acba0dc7..6e75a44b 100644 --- a/src/components/mail/listing.rs +++ b/src/components/mail/listing.rs @@ -58,6 +58,7 @@ pub struct RowsState { pub entries: Vec<(T, EntryStrings)>, pub all_threads: HashSet, pub all_envelopes: HashSet, + pub row_attr_cache: HashMap, } impl RowsState { @@ -72,6 +73,7 @@ impl RowsState { self.entries.clear(); self.all_threads.clear(); self.all_envelopes.clear(); + self.row_attr_cache.clear(); } #[inline(always)] @@ -232,13 +234,6 @@ impl Default for Modifier { } } -#[derive(Debug, Default, Clone)] -pub struct DataColumns { - pub columns: Box<[CellBuffer; 12]>, - pub widths: [usize; 12], // widths of columns calculated in first draw and after size changes - pub segment_tree: Box<[SegmentTree; 12]>, -} - #[derive(Debug, Default)] /// Save theme colors to avoid looking them up again and again from settings struct ColorCache { @@ -255,8 +250,6 @@ struct ColorCache { odd_unseen: ThemeAttribute, odd_highlighted: ThemeAttribute, odd_selected: ThemeAttribute, - attachment_flag: ThemeAttribute, - thread_snooze_flag: ThemeAttribute, tag_default: ThemeAttribute, /* Conversations */ @@ -2078,7 +2071,7 @@ impl Listing { ScrollBar::default().set_show_arrows(true).draw( grid, ( - pos_inc(upper_left!(area), (width!(area), 0)), + pos_inc(upper_left!(area), (width!(area).saturating_sub(1), 0)), bottom_right!(area), ), context, diff --git a/src/components/mail/listing/compact.rs b/src/components/mail/listing/compact.rs index 131ec097..b09f4986 100644 --- a/src/components/mail/listing/compact.rs +++ b/src/components/mail/listing/compact.rs @@ -168,7 +168,7 @@ pub struct CompactListing { sortcmd: bool, subsort: (SortField, SortOrder), /// Cache current view. - data_columns: DataColumns, + data_columns: DataColumns<4>, rows_drawn: SegmentTree, rows: RowsState<(ThreadHash, EnvelopeHash)>, @@ -222,7 +222,7 @@ impl MailListingTrait for CompactListing { } else { 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())) + .and_then(|thread| self.rows.thread_to_env.get(&thread).cloned()) { cursor_iter = Some(env_hashes.into_iter()); } else { @@ -261,8 +261,6 @@ impl MailListingTrait for CompactListing { odd_highlighted: crate::conf::value(context, "mail.listing.compact.odd_highlighted"), even: crate::conf::value(context, "mail.listing.compact.even"), odd: crate::conf::value(context, "mail.listing.compact.odd"), - attachment_flag: crate::conf::value(context, "mail.listing.attachment_flag"), - thread_snooze_flag: crate::conf::value(context, "mail.listing.thread_snooze_flag"), tag_default: crate::conf::value(context, "mail.listing.tag_default"), theme_default: crate::conf::value(context, "theme_default"), ..self.color_cache @@ -394,6 +392,14 @@ impl MailListingTrait for CompactListing { continue; } } + let row_attr = row_attr!( + self.color_cache, + self.length % 2 == 0, + threads.thread_ref(thread).unseen() > 0, + false, + false + ); + self.rows.row_attr_cache.insert(self.length, row_attr); let entry_strings = self.make_entry_string(&root_envelope, context, &threads, thread); row_widths @@ -451,6 +457,21 @@ impl MailListingTrait for CompactListing { min_width.0 = self.length.saturating_sub(1).to_string().len(); + self.data_columns.elasticities[0].set_rigid(); + self.data_columns.elasticities[1].set_rigid(); + self.data_columns.elasticities[2].set_grow(5, Some(35)); + self.data_columns.elasticities[3].set_rigid(); + self.data_columns + .cursor_config + .set_handle(true) + .set_even_odd_theme( + self.color_cache.even_highlighted, + self.color_cache.odd_highlighted, + ); + self.data_columns + .theme_config + .set_even_odd_theme(self.color_cache.even, self.color_cache.odd); + /* index column */ self.data_columns.columns[0] = CellBuffer::new_with_context(min_width.0, self.rows.len(), None, context); @@ -629,26 +650,27 @@ impl ListingTrait for CompactListing { let page_no = (self.new_cursor_pos.2).wrapping_div(rows); let top_idx = page_no * rows; - self.draw_rows( - context, - top_idx, - cmp::min(self.length.saturating_sub(1), top_idx + rows - 1), - ); + let end_idx = cmp::min(self.length.saturating_sub(1), top_idx + rows - 1); + self.draw_rows(context, top_idx, end_idx); /* 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 { + for &(idx, highlight) in &[(old_cursor_pos.2, false), (self.new_cursor_pos.2, true)] { + 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); + let new_area = nth_row_area(area, idx % rows); + self.data_columns + .draw(grid, idx, self.cursor_pos.2, grid.bounds_iter(new_area)); + if highlight { + let row_attr = row_attr!(self.color_cache, idx % 2 == 0, false, true, false); + change_colors(grid, new_area, row_attr.fg, row_attr.bg); + } else if let Some(row_attr) = self.rows.row_attr_cache.get(&idx) { + change_colors(grid, new_area, row_attr.fg, row_attr.bg); + } context.dirty_areas.push_back(new_area); } if !self.force_draw { @@ -662,115 +684,37 @@ impl ListingTrait for CompactListing { self.cursor_pos.2 = self.new_cursor_pos.2; } - let width = width!(area); - self.data_columns.widths = Default::default(); - self.data_columns.widths[0] = self.data_columns.columns[0].size().0; - self.data_columns.widths[1] = self.data_columns.columns[1].size().0; /* date*/ - self.data_columns.widths[2] = self.data_columns.columns[2].size().0; /* from */ - self.data_columns.widths[3] = self.data_columns.columns[3].size().0; /* subject */ - - let min_col_width = std::cmp::min( - 15, - std::cmp::min(self.data_columns.widths[3], self.data_columns.widths[2]), - ); - if self.data_columns.widths[0] + self.data_columns.widths[1] + 2 * min_col_width + 4 > width - { - let remainder = width - .saturating_sub(self.data_columns.widths[0]) - .saturating_sub(self.data_columns.widths[1]) - .saturating_sub(2 * 2); - self.data_columns.widths[2] = remainder / 6; - } else { - let remainder = width - .saturating_sub(self.data_columns.widths[0]) - .saturating_sub(self.data_columns.widths[1]) - .saturating_sub(3 * 2); - if min_col_width + self.data_columns.widths[3] > remainder { - self.data_columns.widths[2] = min_col_width; - } - } - for i in 0..3 { - /* Set column widths to their maximum value width in the range - * [top_idx, top_idx + rows]. By using a segment tree the query is O(logn), which is - * great! - */ - self.data_columns.widths[i] = - self.data_columns.segment_tree[i].get_max(top_idx, top_idx + rows - 1) as usize; - } - if self.data_columns.widths.iter().sum::() > width { - let diff = self.data_columns.widths.iter().sum::() - width; - if self.data_columns.widths[2] > 2 * diff { - self.data_columns.widths[2] -= diff; - } else { - self.data_columns.widths[2] = std::cmp::max( - 15, - self.data_columns.widths[2].saturating_sub((2 * diff) / 3), - ); - } - } - clear_area(grid, area, self.color_cache.theme_default); /* Page_no has changed, so draw new page */ - let mut x = get_x(upper_left); - let mut flag_x = 0; - for i in 0..4 { - let column_width = self.data_columns.widths[i]; - if i == 3 { - flag_x = x; - } - if column_width == 0 { - continue; - } - copy_area( - grid, - &self.data_columns.columns[i], - (set_x(upper_left, x), bottom_right), - ( - (0, top_idx), - (column_width.saturating_sub(1), self.length - 1), - ), - ); - x += column_width + 2; // + SEPARATOR - if x > get_x(bottom_right) { - break; - } - } - - 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) { - 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); - } + _ = self + .data_columns + .recalc_widths((width!(area), height!(area)), top_idx); + clear_area(grid, area, self.color_cache.theme_default); + /* copy table columns */ + self.data_columns + .draw(grid, top_idx, self.cursor_pos.2, grid.bounds_iter(area)); + /* apply each row colors separately */ + for i in top_idx..(top_idx + height!(area)) { + if let Some(row_attr) = self.rows.row_attr_cache.get(&i) { + change_colors(grid, nth_row_area(area, i % rows), row_attr.fg, row_attr.bg); } } - self.highlight_line( + /* highlight cursor */ + let row_attr = row_attr!( + self.color_cache, + self.cursor_pos.2 % 2 == 0, + false, + true, + false + ); + change_colors( 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, + nth_row_area(area, self.cursor_pos.2 % rows), + row_attr.fg, + row_attr.bg, ); + /* clear gap if available height is more than count of entries */ if top_idx + rows > self.length { clear_area( grid, @@ -1050,43 +994,6 @@ impl CompactListing { fn update_line(&mut self, context: &Context, env_hash: EnvelopeHash) { let account = &context.accounts[&self.cursor_pos.0]; - let selected_flag_len = mailbox_settings!( - context[self.cursor_pos.0][&self.cursor_pos.1] - .listing - .selected_flag - ) - .as_ref() - .map(|s| s.as_str()) - .unwrap_or(super::DEFAULT_SELECTED_FLAG) - .grapheme_width(); - let thread_snoozed_flag_len = mailbox_settings!( - context[self.cursor_pos.0][&self.cursor_pos.1] - .listing - .thread_snoozed_flag - ) - .as_ref() - .map(|s| s.as_str()) - .unwrap_or(super::DEFAULT_SNOOZED_FLAG) - .grapheme_width(); - let unseen_flag_len = mailbox_settings!( - context[self.cursor_pos.0][&self.cursor_pos.1] - .listing - .unseen_flag - ) - .as_ref() - .map(|s| s.as_str()) - .unwrap_or(super::DEFAULT_UNSEEN_FLAG) - .grapheme_width(); - let attachment_flag_len = mailbox_settings!( - context[self.cursor_pos.0][&self.cursor_pos.1] - .listing - .attachment_flag - ) - .as_ref() - .map(|s| s.as_str()) - .unwrap_or(super::DEFAULT_ATTACHMENT_FLAG) - .grapheme_width(); - if !account.contains_key(env_hash) { /* The envelope has been renamed or removed, so wait for the appropriate event to * arrive */ @@ -1102,8 +1009,9 @@ impl CompactListing { idx % 2 == 0, thread.unseen() > 0, false, - false, + self.rows.is_thread_selected(thread_hash) ); + self.rows.row_attr_cache.insert(idx, row_attr); let strings = self.make_entry_string(&envelope, context, &threads, thread_hash); drop(envelope); let columns = &mut self.data_columns.columns; @@ -1204,25 +1112,6 @@ impl CompactListing { 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); - } - 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.entries.get_mut(idx).unwrap() = ((thread_hash, env_hash), strings); self.rows_drawn.update(idx, 1); } @@ -1246,48 +1135,8 @@ impl CompactListing { self.data_columns.columns[2].size().0, self.data_columns.columns[3].size().0, ); - let account = &context.accounts[&self.cursor_pos.0]; - - let threads = account.collection.get_threads(self.cursor_pos.1); - let selected_flag_len = mailbox_settings!( - context[self.cursor_pos.0][&self.cursor_pos.1] - .listing - .selected_flag - ) - .as_ref() - .map(|s| s.as_str()) - .unwrap_or(super::DEFAULT_SELECTED_FLAG) - .grapheme_width(); - let thread_snoozed_flag_len = mailbox_settings!( - context[self.cursor_pos.0][&self.cursor_pos.1] - .listing - .thread_snoozed_flag - ) - .as_ref() - .map(|s| s.as_str()) - .unwrap_or(super::DEFAULT_SNOOZED_FLAG) - .grapheme_width(); - let unseen_flag_len = mailbox_settings!( - context[self.cursor_pos.0][&self.cursor_pos.1] - .listing - .unseen_flag - ) - .as_ref() - .map(|s| s.as_str()) - .unwrap_or(super::DEFAULT_UNSEEN_FLAG) - .grapheme_width(); - let attachment_flag_len = mailbox_settings!( - context[self.cursor_pos.0][&self.cursor_pos.1] - .listing - .attachment_flag - ) - .as_ref() - .map(|s| s.as_str()) - .unwrap_or(super::DEFAULT_ATTACHMENT_FLAG) - .grapheme_width(); - - for (idx, ((thread_hash, root_env_hash), strings)) in self + for (idx, ((_thread_hash, root_env_hash), strings)) in self .rows .entries .iter() @@ -1306,14 +1155,7 @@ impl CompactListing { panic!(); } - let thread = threads.thread_ref(*thread_hash); - let row_attr = row_attr!( - self.color_cache, - idx % 2 == 0, - thread.unseen() > 0, - self.cursor_pos.2 == idx, - self.rows.selection[root_env_hash] - ); + let row_attr = self.rows.row_attr_cache[&idx]; let (x, _) = write_string_to_grid( &idx.to_string(), &mut self.data_columns.columns[0], @@ -1428,33 +1270,6 @@ impl CompactListing { .set_bg(row_attr.bg) .set_attrs(row_attr.attrs); } - /* Set fg color for flags */ - let mut x = 0; - if self - .rows - .selection - .get(root_env_hash) - .cloned() - .unwrap_or(false) - { - x += selected_flag_len; - } - if thread.snoozed() { - for x in x..(x + thread_snoozed_flag_len) { - self.data_columns.columns[3][(x, idx)] - .set_fg(self.color_cache.thread_snooze_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) { - self.data_columns.columns[3][(x, idx)] - .set_fg(self.color_cache.attachment_flag.fg); - } - } } } @@ -1726,19 +1541,23 @@ impl Component for CompactListing { } 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.rows.env_order[&row]; + while let Some(env_hash) = self.rows.row_updates.pop() { + self.update_line(context, env_hash); + let row: usize = self.rows.env_order[&env_hash]; let page_no = (self.new_cursor_pos.2).wrapping_div(rows); let top_idx = page_no * rows; if row >= top_idx && row < top_idx + rows { - let area = ( - set_y(upper_left, get_y(upper_left) + (row % rows)), - set_y(bottom_right, get_y(upper_left) + (row % rows)), + let new_area = nth_row_area(area, row % rows); + self.data_columns.draw( + grid, + row, + self.cursor_pos.2, + grid.bounds_iter(new_area), ); - self.highlight_line(grid, area, row, context); - context.dirty_areas.push_back(area); + let row_attr = self.rows.row_attr_cache[&row]; + change_colors(grid, new_area, row_attr.fg, row_attr.bg); + context.dirty_areas.push_back(new_area); } } if self.force_draw { @@ -1842,11 +1661,11 @@ impl Component for CompactListing { { if self.modifier_active && self.modifier_command.is_none() { self.modifier_command = Some(Modifier::default()); - } else { - 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); - } + } else 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; } @@ -1916,11 +1735,6 @@ impl Component for CompactListing { ), even: crate::conf::value(context, "mail.listing.compact.even"), odd: crate::conf::value(context, "mail.listing.compact.odd"), - attachment_flag: crate::conf::value(context, "mail.listing.attachment_flag"), - thread_snooze_flag: crate::conf::value( - context, - "mail.listing.thread_snooze_flag", - ), tag_default: crate::conf::value(context, "mail.listing.tag_default"), theme_default: crate::conf::value(context, "theme_default"), ..self.color_cache diff --git a/src/components/mail/listing/conversations.rs b/src/components/mail/listing/conversations.rs index 2b05a7e1..c2d396cc 100644 --- a/src/components/mail/listing/conversations.rs +++ b/src/components/mail/listing/conversations.rs @@ -151,7 +151,7 @@ impl MailListingTrait for ConversationsListing { } else { 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())) + .and_then(|thread| self.rows.thread_to_env.get(&thread).cloned()) { cursor_iter = Some(env_hashes.into_iter()); } else { @@ -187,8 +187,6 @@ impl MailListingTrait for ConversationsListing { selected: crate::conf::value(context, "mail.listing.conversations.selected"), unseen: crate::conf::value(context, "mail.listing.conversations.unseen"), highlighted: crate::conf::value(context, "mail.listing.conversations.highlighted"), - attachment_flag: crate::conf::value(context, "mail.listing.attachment_flag"), - thread_snooze_flag: crate::conf::value(context, "mail.listing.thread_snooze_flag"), tag_default: crate::conf::value(context, "mail.listing.tag_default"), ..self.color_cache }; @@ -1306,10 +1304,8 @@ impl Component for ConversationsListing { { if self.modifier_active && self.modifier_command.is_none() { self.modifier_command = Some(Modifier::default()); - } else { - if let Some(thread) = self.get_thread_under_cursor(self.cursor_pos.2) { - self.rows.update_selection_with_thread(thread, |e| *e = !*e); - } + } else 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; } @@ -1440,11 +1436,6 @@ impl Component for ConversationsListing { context, "mail.listing.conversations.highlighted", ), - attachment_flag: crate::conf::value(context, "mail.listing.attachment_flag"), - thread_snooze_flag: crate::conf::value( - context, - "mail.listing.thread_snooze_flag", - ), tag_default: crate::conf::value(context, "mail.listing.tag_default"), ..self.color_cache }; diff --git a/src/components/mail/listing/plain.rs b/src/components/mail/listing/plain.rs index c4296a4c..c54294c3 100644 --- a/src/components/mail/listing/plain.rs +++ b/src/components/mail/listing/plain.rs @@ -130,7 +130,7 @@ pub struct PlainListing { subsort: (SortField, SortOrder), rows: RowsState<(ThreadHash, EnvelopeHash)>, /// Cache current view. - data_columns: DataColumns, + data_columns: DataColumns<4>, #[allow(clippy::type_complexity)] search_job: Option<(String, JoinHandle>>)>, @@ -167,9 +167,9 @@ impl MailListingTrait for PlainListing { .values() .cloned() .any(std::convert::identity); - dbg!(is_selection_empty); if is_selection_empty { - return dbg!(self.get_env_under_cursor(self.cursor_pos.2)) + return self + .get_env_under_cursor(self.cursor_pos.2) .into_iter() .collect::<_>(); } @@ -205,8 +205,6 @@ impl MailListingTrait for PlainListing { odd_highlighted: crate::conf::value(context, "mail.listing.plain.odd_highlighted"), even_selected: crate::conf::value(context, "mail.listing.plain.even_selected"), odd_selected: crate::conf::value(context, "mail.listing.plain.odd_selected"), - attachment_flag: crate::conf::value(context, "mail.listing.attachment_flag"), - thread_snooze_flag: crate::conf::value(context, "mail.listing.thread_snooze_flag"), tag_default: crate::conf::value(context, "mail.listing.tag_default"), theme_default: crate::conf::value(context, "theme_default"), ..self.color_cache @@ -448,15 +446,19 @@ impl ListingTrait for PlainListing { 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 { + for &(idx, highlight) in &[(old_cursor_pos.2, false), (self.new_cursor_pos.2, true)] { + 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); + let new_area = nth_row_area(area, idx % rows); + self.data_columns + .draw(grid, idx, self.cursor_pos.2, grid.bounds_iter(new_area)); + if highlight { + let row_attr = row_attr!(self.color_cache, idx % 2 == 0, false, true, false); + change_colors(grid, new_area, row_attr.fg, row_attr.bg); + } else if let Some(row_attr) = self.rows.row_attr_cache.get(&idx) { + change_colors(grid, new_area, row_attr.fg, row_attr.bg); + } context.dirty_areas.push_back(new_area); } return; @@ -468,107 +470,37 @@ impl ListingTrait for PlainListing { self.cursor_pos.2 = self.new_cursor_pos.2; } - let width = width!(area); - self.data_columns.widths = Default::default(); - self.data_columns.widths[0] = self.data_columns.columns[0].size().0; - self.data_columns.widths[1] = self.data_columns.columns[1].size().0; /* date*/ - self.data_columns.widths[2] = self.data_columns.columns[2].size().0; /* from */ - self.data_columns.widths[3] = self.data_columns.columns[3].size().0; /* subject */ - - let min_col_width = std::cmp::min( - 15, - std::cmp::min(self.data_columns.widths[3], self.data_columns.widths[2]), - ); - if self.data_columns.widths[0] + self.data_columns.widths[1] + 2 * min_col_width + 4 > width - { - let remainder = width - .saturating_sub(self.data_columns.widths[0]) - .saturating_sub(self.data_columns.widths[1]) - .saturating_sub(2 * 2); - self.data_columns.widths[2] = remainder / 6; - } else { - let remainder = width - .saturating_sub(self.data_columns.widths[0]) - .saturating_sub(self.data_columns.widths[1]) - .saturating_sub(3 * 2); - if min_col_width + self.data_columns.widths[3] > remainder { - self.data_columns.widths[2] = min_col_width; - } - } - clear_area(grid, area, self.color_cache.theme_default); /* Page_no has changed, so draw new page */ - let mut x = get_x(upper_left); - let mut flag_x = 0; - for i in 0..4 { - let column_width = self.data_columns.widths[i]; - if i == 3 { - flag_x = x; - } - if column_width == 0 { - continue; - } - copy_area( - grid, - &self.data_columns.columns[i], - (set_x(upper_left, x), bottom_right), - ( - (0, top_idx), - (column_width.saturating_sub(1), self.length - 1), - ), - ); - x += column_width + 2; // + SEPARATOR - if x > get_x(bottom_right) { - break; - } - } - 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)]; - (c.fg(), c.bg()) - }; - change_colors( - grid, - ( - pos_inc(upper_left, (0, r)), - (flag_x.saturating_sub(1), get_y(upper_left) + r), - ), - fg_color, - bg_color, - ); - for c in grid.row_iter( - flag_x - ..std::cmp::min( - get_x(bottom_right), - flag_x + 2 + self.data_columns.widths[3], - ), - get_y(upper_left) + r, - ) { - grid[c].set_bg(bg_color); + _ = self + .data_columns + .recalc_widths((width!(area), height!(area)), top_idx); + clear_area(grid, area, self.color_cache.theme_default); + /* copy table columns */ + self.data_columns + .draw(grid, top_idx, self.cursor_pos.2, grid.bounds_iter(area)); + /* apply each row colors separately */ + for i in top_idx..(top_idx + height!(area)) { + if let Some(row_attr) = self.rows.row_attr_cache.get(&i) { + change_colors(grid, nth_row_area(area, i % rows), row_attr.fg, row_attr.bg); } - change_colors( - grid, - ( - ( - flag_x + 2 + self.data_columns.widths[3], - get_y(upper_left) + r, - ), - (get_x(bottom_right), get_y(upper_left) + r), - ), - fg_color, - bg_color, - ); } - self.highlight_line( + /* highlight cursor */ + let row_attr = row_attr!( + self.color_cache, + self.cursor_pos.2 % 2 == 0, + false, + true, + false + ); + change_colors( 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, + nth_row_area(area, self.cursor_pos.2 % rows), + row_attr.fg, + row_attr.bg, ); + /* clear gap if available height is more than count of entries */ if top_idx + rows > self.length { clear_area( grid, @@ -845,6 +777,14 @@ impl PlainListing { continue; } } + let row_attr = row_attr!( + self.color_cache, + self.length % 2 == 0, + !envelope.is_seen(), + false, + false + ); + self.rows.row_attr_cache.insert(self.length, row_attr); let entry_strings = self.make_entry_string(&envelope, context); min_width.1 = cmp::max(min_width.1, entry_strings.date.grapheme_width()); /* date */ @@ -868,6 +808,21 @@ impl PlainListing { min_width.0 = self.length.saturating_sub(1).to_string().len(); + self.data_columns.elasticities[0].set_rigid(); + self.data_columns.elasticities[1].set_rigid(); + self.data_columns.elasticities[2].set_grow(5, Some(35)); + self.data_columns.elasticities[3].set_rigid(); + self.data_columns + .cursor_config + .set_handle(true) + .set_even_odd_theme( + self.color_cache.even_highlighted, + self.color_cache.odd_highlighted, + ); + self.data_columns + .theme_config + .set_even_odd_theme(self.color_cache.even, self.color_cache.odd); + /* index column */ self.data_columns.columns[0] = CellBuffer::new_with_context(min_width.0, self.rows.len(), None, context); @@ -903,14 +858,7 @@ impl PlainListing { panic!(); } - let envelope: EnvelopeRef = context.accounts[&self.cursor_pos.0].collection.get_env(i); - let row_attr = row_attr!( - self.color_cache, - idx % 2 == 0, - !envelope.is_seen(), - false, - false - ); + let row_attr = self.rows.row_attr_cache[&idx]; let (x, _) = write_string_to_grid( &idx.to_string(), @@ -998,17 +946,6 @@ impl PlainListing { for c in columns[3].row_iter(x..min_width.3, idx) { columns[3][c].set_bg(row_attr.bg).set_attrs(row_attr.attrs); } - /* Set fg color for flags */ - let mut x = 0; - if self.rows.selection.get(&i).cloned().unwrap_or(false) { - x += 1; - } - if !envelope.is_seen() { - x += 1; - } - if envelope.has_attachments() { - columns[3][(x, idx)].set_fg(self.color_cache.attachment_flag.fg); - } } if self.length == 0 && self.filter_term.is_empty() { let message: String = account[&self.cursor_pos.1].status(); @@ -1091,19 +1028,34 @@ impl Component for PlainListing { if !self.rows.row_updates.is_empty() { let (upper_left, bottom_right) = area; - while let Some(row) = self.rows.row_updates.pop() { - let row: usize = self.rows.env_order[&row]; + while let Some(env_hash) = self.rows.row_updates.pop() { + let row: usize = self.rows.env_order[&env_hash]; let rows = get_y(bottom_right) - get_y(upper_left) + 1; let page_no = (self.new_cursor_pos.2).wrapping_div(rows); let top_idx = page_no * rows; if row >= top_idx && row <= top_idx + rows { - let area = ( - set_y(upper_left, get_y(upper_left) + (row % rows)), - set_y(bottom_right, get_y(upper_left) + (row % rows)), + let new_area = nth_row_area(area, row % rows); + self.data_columns.draw( + grid, + row, + self.cursor_pos.2, + grid.bounds_iter(new_area), ); - self.highlight_line(grid, area, row, context); - context.dirty_areas.push_back(area); + let envelope: EnvelopeRef = context.accounts[&self.cursor_pos.0] + .collection + .get_env(env_hash); + let row_attr = row_attr!( + self.color_cache, + row % 2 == 0, + !envelope.is_seen(), + false, + self.rows.selection[&env_hash] + ); + self.rows.row_attr_cache.insert(row, row_attr); + + change_colors(grid, new_area, row_attr.fg, row_attr.bg); + context.dirty_areas.push_back(new_area); } } if self.force_draw { @@ -1196,10 +1148,8 @@ impl Component for PlainListing { { 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); - } + } 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; } @@ -1243,11 +1193,6 @@ impl Component for PlainListing { ), even_selected: crate::conf::value(context, "mail.listing.plain.even_selected"), odd_selected: crate::conf::value(context, "mail.listing.plain.odd_selected"), - attachment_flag: crate::conf::value(context, "mail.listing.attachment_flag"), - thread_snooze_flag: crate::conf::value( - context, - "mail.listing.thread_snooze_flag", - ), tag_default: crate::conf::value(context, "mail.listing.tag_default"), theme_default: crate::conf::value(context, "theme_default"), ..self.color_cache @@ -1332,9 +1277,7 @@ impl Component for PlainListing { .cloned() .any(std::convert::identity) => { - for v in self.rows.selection.values_mut() { - *v = false; - } + self.rows.clear_selection(); self.dirty = true; return true; } diff --git a/src/components/mail/listing/thread.rs b/src/components/mail/listing/thread.rs index 6129730d..2194aece 100644 --- a/src/components/mail/listing/thread.rs +++ b/src/components/mail/listing/thread.rs @@ -121,9 +121,9 @@ pub struct ThreadListing { crate::jobs::JoinHandle>>, )>, - data_columns: DataColumns, + data_columns: DataColumns<5>, rows_drawn: SegmentTree, - rows: RowsState<(bool, bool, ThreadHash, EnvelopeHash)>, + rows: RowsState<(ThreadHash, EnvelopeHash)>, /// If we must redraw on next redraw event dirty: bool, /// If `self.view` is focused or not. @@ -189,8 +189,6 @@ impl MailListingTrait for ThreadListing { odd_highlighted: crate::conf::value(context, "mail.listing.plain.odd_highlighted"), even: crate::conf::value(context, "mail.listing.plain.even"), odd: crate::conf::value(context, "mail.listing.plain.odd"), - attachment_flag: crate::conf::value(context, "mail.listing.attachment_flag"), - thread_snooze_flag: crate::conf::value(context, "mail.listing.thread_snooze_flag"), tag_default: crate::conf::value(context, "mail.listing.tag_default"), theme_default: crate::conf::value(context, "theme_default"), ..self.color_cache @@ -358,15 +356,18 @@ impl MailListingTrait for ThreadListing { ); /* tags + subject */ self.rows.insert_thread( threads.envelope_to_thread[&env_hash], - ( - envelope.is_seen(), - envelope.has_attachments(), - threads.envelope_to_thread[&env_hash], - env_hash, - ), + (threads.envelope_to_thread[&env_hash], env_hash), smallvec::smallvec![env_hash], entry_strings, ); + let row_attr = row_attr!( + self.color_cache, + idx % 2 == 0, + !envelope.is_seen(), + false, + false, + ); + self.rows.row_attr_cache.insert(idx, row_attr); idx += 1; } else { continue; @@ -389,6 +390,23 @@ impl MailListingTrait for ThreadListing { } } min_width.0 = idx.saturating_sub(1).to_string().len(); + + self.data_columns.elasticities[0].set_rigid(); + self.data_columns.elasticities[1].set_rigid(); + self.data_columns.elasticities[2].set_grow(5, Some(35)); + self.data_columns.elasticities[3].set_rigid(); + self.data_columns.elasticities[4].set_rigid(); + self.data_columns + .cursor_config + .set_handle(true) + .set_even_odd_theme( + self.color_cache.even_highlighted, + self.color_cache.odd_highlighted, + ); + self.data_columns + .theme_config + .set_even_odd_theme(self.color_cache.even, self.color_cache.odd); + /* index column */ self.data_columns.columns[0] = CellBuffer::new_with_context(min_width.0, self.rows.len(), None, context); @@ -496,41 +514,24 @@ impl ListingTrait for ThreadListing { cmp::min(self.length.saturating_sub(1), top_idx + rows - 1), ); - /* - if !self.initialised { - self.initialised = false; - 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); - } - */ /* 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 { + for &(idx, highlight) in &[(old_cursor_pos.2, false), (self.new_cursor_pos.2, true)] { + 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); + let new_area = nth_row_area(area, idx % rows); + self.data_columns + .draw(grid, idx, self.cursor_pos.2, grid.bounds_iter(new_area)); + if highlight { + let row_attr = row_attr!(self.color_cache, idx % 2 == 0, false, true, false); + change_colors(grid, new_area, row_attr.fg, row_attr.bg); + } else if let Some(row_attr) = self.rows.row_attr_cache.get(&idx) { + change_colors(grid, new_area, row_attr.fg, row_attr.bg); + } context.dirty_areas.push_back(new_area); } return; @@ -544,148 +545,45 @@ impl ListingTrait for ThreadListing { self.cursor_pos.2 = self.new_cursor_pos.2; } - let width = width!(area); - self.data_columns.widths = Default::default(); - self.data_columns.widths[0] = self.data_columns.columns[0].size().0; - self.data_columns.widths[1] = self.data_columns.columns[1].size().0; /* date*/ - self.data_columns.widths[2] = self.data_columns.columns[2].size().0; /* from */ - self.data_columns.widths[3] = self.data_columns.columns[3].size().0; /* flags */ - self.data_columns.widths[4] = self.data_columns.columns[4].size().0; /* subject */ - - let min_col_width = std::cmp::min( - 15, - std::cmp::min(self.data_columns.widths[4], self.data_columns.widths[2]), - ); - if self.data_columns.widths[0] + self.data_columns.widths[1] + 3 * min_col_width + 8 > width - { - let remainder = width - .saturating_sub(self.data_columns.widths[0]) - .saturating_sub(self.data_columns.widths[1]) - .saturating_sub(4); - self.data_columns.widths[2] = remainder / 6; - self.data_columns.widths[4] = - ((2 * remainder) / 3).saturating_sub(self.data_columns.widths[3]); - } else { - let remainder = width - .saturating_sub(self.data_columns.widths[0]) - .saturating_sub(self.data_columns.widths[1]) - .saturating_sub(8); - if min_col_width + self.data_columns.widths[4] > remainder { - self.data_columns.widths[4] = - remainder.saturating_sub(min_col_width + self.data_columns.widths[3]); - self.data_columns.widths[2] = min_col_width; - } - } - for &i in &[2, 4] { - /* Set From and Subject column widths to their maximum value width in the range - * [top_idx, top_idx + rows]. By using a segment tree the query is O(logn), which is - * great! - */ - self.data_columns.widths[i] = - self.data_columns.segment_tree[i].get_max(top_idx, top_idx + rows) as usize; - } - if self.data_columns.widths.iter().sum::() > width { - let diff = self.data_columns.widths.iter().sum::() - width; - if self.data_columns.widths[2] > 2 * diff { - self.data_columns.widths[2] -= diff; - } else { - self.data_columns.widths[2] = std::cmp::max( - 15, - self.data_columns.widths[2].saturating_sub((2 * diff) / 3), - ); - self.data_columns.widths[4] = std::cmp::max( - 15, - self.data_columns.widths[4].saturating_sub(diff / 3 + diff % 3), - ); - } - } + _ = self + .data_columns + .recalc_widths((width!(area), height!(area)), top_idx); clear_area(grid, area, self.color_cache.theme_default); /* Page_no has changed, so draw new page */ - let mut x = get_x(upper_left); - let mut flag_x = 0; - for i in 0..self.data_columns.columns.len() { - let column_width = self.data_columns.columns[i].size().0; - if i == 3 { - flag_x = x; - } - if self.data_columns.widths[i] == 0 { - continue; - } - copy_area( - grid, - &self.data_columns.columns[i], - ( - set_x(upper_left, x), - set_x( - bottom_right, - std::cmp::min(get_x(bottom_right), x + (self.data_columns.widths[i])), - ), - ), - ( - (0, top_idx), - (column_width.saturating_sub(1), self.length - 1), - ), - ); - x += self.data_columns.widths[i] + 2; // + SEPARATOR - if x > get_x(bottom_right) { - break; - } - } + self.data_columns + .draw(grid, top_idx, self.cursor_pos.2, grid.bounds_iter(area)); - 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)]; - 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()) - } - }; - change_colors( - grid, - ( - pos_inc(upper_left, (0, r)), - (flag_x.saturating_sub(1), get_y(upper_left) + r), - ), - fg_color, - bg_color, - ); - for x in flag_x - ..std::cmp::min( - get_x(bottom_right), - flag_x + 2 + self.data_columns.widths[3], - ) - { - grid[(x, get_y(upper_left) + r)].set_bg(bg_color); + /* Page_no has changed, so draw new page */ + _ = self + .data_columns + .recalc_widths((width!(area), height!(area)), top_idx); + clear_area(grid, area, self.color_cache.theme_default); + /* copy table columns */ + self.data_columns + .draw(grid, top_idx, self.cursor_pos.2, grid.bounds_iter(area)); + /* apply each row colors separately */ + for i in top_idx..(top_idx + height!(area)) { + if let Some(row_attr) = self.rows.row_attr_cache.get(&i) { + change_colors(grid, nth_row_area(area, i % rows), row_attr.fg, row_attr.bg); } - change_colors( - grid, - ( - ( - flag_x + 2 + self.data_columns.widths[3], - get_y(upper_left) + r, - ), - (get_x(bottom_right), get_y(upper_left) + r), - ), - fg_color, - bg_color, - ); } - self.highlight_line( + /* highlight cursor */ + let row_attr = row_attr!( + self.color_cache, + self.cursor_pos.2 % 2 == 0, + false, + true, + false + ); + change_colors( 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, + nth_row_area(area, self.cursor_pos.2 % rows), + row_attr.fg, + row_attr.bg, ); + /* clear gap if available height is more than count of entries */ if top_idx + rows > self.length { clear_area( grid, @@ -697,24 +595,6 @@ impl ListingTrait for ThreadListing { ); } context.dirty_areas.push_back(area); - /* - 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 highlight_line(&mut self, grid: &mut CellBuffer, area: Area, idx: usize, context: &Context) { @@ -994,7 +874,7 @@ impl ThreadListing { self.data_columns.columns[4].size().0, ); - for (idx, ((is_seen, has_attachments, _thread_hash, env_hash), strings)) in self + for (idx, ((_thread_hash, env_hash), strings)) in self .rows .entries .iter() @@ -1013,13 +893,7 @@ impl ThreadListing { panic!(); } - let row_attr = row_attr!( - self.color_cache, - idx % 2 == 0, - !*is_seen, - self.cursor_pos.2 == idx, - self.rows.selection[&env_hash], - ); + let row_attr = self.rows.row_attr_cache[&idx]; let (x, _) = write_string_to_grid( &idx.to_string(), &mut self.data_columns.columns[0], @@ -1139,9 +1013,6 @@ impl ThreadListing { .set_bg(row_attr.bg) .set_attrs(row_attr.attrs); } - if *has_attachments { - self.data_columns.columns[3][(0, idx)].set_fg(self.color_cache.attachment_flag.fg); - } } } } @@ -1315,15 +1186,29 @@ impl Component for ThreadListing { 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]; + while let Some(env_hash) = self.rows.row_updates.pop() { + let row: usize = self.rows.env_order[&env_hash]; if row >= top_idx && row <= top_idx + rows { - let new_area = ( - set_y(upper_left, get_y(upper_left) + (row % rows)), - set_y(bottom_right, get_y(upper_left) + (row % rows)), + let new_area = nth_row_area(area, row % rows); + self.data_columns.draw( + grid, + row, + self.cursor_pos.2, + grid.bounds_iter(new_area), + ); + let envelope: EnvelopeRef = context.accounts[&self.cursor_pos.0] + .collection + .get_env(env_hash); + let row_attr = row_attr!( + self.color_cache, + row % 2 == 0, + !envelope.is_seen(), + false, + self.rows.selection[&env_hash] ); - self.highlight_line(grid, new_area, row, context); + self.rows.row_attr_cache.insert(row, row_attr); + change_colors(grid, new_area, row_attr.fg, row_attr.bg); context.dirty_areas.push_back(new_area); } } @@ -1486,11 +1371,6 @@ impl Component for ThreadListing { ), even: crate::conf::value(context, "mail.listing.plain.even"), odd: crate::conf::value(context, "mail.listing.plain.odd"), - attachment_flag: crate::conf::value(context, "mail.listing.attachment_flag"), - thread_snooze_flag: crate::conf::value( - context, - "mail.listing.thread_snooze_flag", - ), tag_default: crate::conf::value(context, "mail.listing.tag_default"), theme_default: crate::conf::value(context, "theme_default"), ..self.color_cache @@ -1559,7 +1439,7 @@ impl Component for ThreadListing { } 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.rows.entries[row].0).1 = *new_hash; } self.dirty = true; @@ -1623,10 +1503,8 @@ impl Component for ThreadListing { { 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); - } + } 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; } diff --git a/src/components/mail/view/thread.rs b/src/components/mail/view/thread.rs index 34001386..63642893 100644 --- a/src/components/mail/view/thread.rs +++ b/src/components/mail/view/thread.rs @@ -564,7 +564,10 @@ impl ThreadView { if rows < visibles.len() { ScrollBar::default().set_show_arrows(true).draw( grid, - (pos_inc(upper_left!(area), (width!(area), 0)), bottom_right), + ( + pos_inc(upper_left!(area), (width!(area).saturating_sub(1), 0)), + bottom_right, + ), context, 2 * self.cursor_pos, rows, @@ -618,7 +621,10 @@ impl ThreadView { if rows < visibles.len() { ScrollBar::default().set_show_arrows(true).draw( grid, - (pos_inc(upper_left!(area), (width!(area), 0)), bottom_right), + ( + pos_inc(upper_left!(area), (width!(area).saturating_sub(1), 0)), + bottom_right, + ), context, 2 * self.cursor_pos, rows, diff --git a/src/components/utilities.rs b/src/components/utilities.rs index d1ad816a..55508cf4 100644 --- a/src/components/utilities.rs +++ b/src/components/utilities.rs @@ -36,6 +36,9 @@ pub use self::layouts::*; mod dialogs; pub use self::dialogs::*; +mod tables; +pub use self::tables::*; + use crate::jobs::JobId; use std::collections::HashSet; @@ -1048,7 +1051,10 @@ impl Component for Tabbed { ScrollBar::default().set_show_arrows(true).draw( grid, ( - pos_inc(upper_left!(inner_area), (width!(inner_area), 0)), + pos_inc( + upper_left!(inner_area), + (width!(inner_area).saturating_sub(1), 0), + ), bottom_right!(inner_area), ), context, @@ -1300,7 +1306,10 @@ impl Component for Tabbed { ScrollBar::default().set_show_arrows(true).draw( grid, ( - pos_inc(upper_left!(inner_area), (width!(inner_area), 0)), + pos_inc( + upper_left!(inner_area), + (width!(inner_area).saturating_sub(1), 0), + ), bottom_right!(inner_area), ), context, diff --git a/src/components/utilities/pager.rs b/src/components/utilities/pager.rs index 9f24543e..546cd1a6 100644 --- a/src/components/utilities/pager.rs +++ b/src/components/utilities/pager.rs @@ -340,7 +340,7 @@ impl Pager { .text_lines .iter() .skip(self.cursor.1) - .take(height!(area) + 1) + .take(height!(area)) { write_string_to_grid( l, diff --git a/src/components/utilities/tables.rs b/src/components/utilities/tables.rs new file mode 100644 index 00000000..25ce5492 --- /dev/null +++ b/src/components/utilities/tables.rs @@ -0,0 +1,318 @@ +/* + * meli + * + * Copyright 2017-2022 Manos Pitsidianakis + * + * This file is part of meli. + * + * meli is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * meli is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with meli. If not, see . + */ + +/*! UI components to display tabular lists. */ +use super::*; +use crate::segment_tree::SegmentTree; +use std::mem::MaybeUninit; + +#[derive(Debug, Default, Copy, Clone)] +pub enum ColumnElasticity { + #[default] + Rigid, + Grow { + min: usize, + max: Option, + }, +} + +impl ColumnElasticity { + pub fn set_rigid(&mut self) { + *self = Self::Rigid; + } + pub fn set_grow(&mut self, min: usize, max: Option) { + *self = Self::Grow { min, max }; + } +} + +/*#[derive(Debug, Default, Clone)] +pub enum TableRowFormat { + #[default] + None, + Fill(FormatTag), + Range { + start: usize, + end: usize, + val: FormatTag, + }, +} + +impl TableRowFormat { + pub const SELECTED: u8 = 0; + pub const UNREAD: u8 = 1; +} +*/ + +#[derive(Debug, Default, Clone)] +pub struct TableThemeConfig { + pub theme: TableTheme, + //pub row_formats: HashMap>, +} + +impl TableThemeConfig { + pub fn set_single_theme(&mut self, value: ThemeAttribute) -> &mut Self { + self.theme = TableTheme::Single(value); + self + } + + pub fn set_even_odd_theme(&mut self, even: ThemeAttribute, odd: ThemeAttribute) -> &mut Self { + self.theme = TableTheme::EvenOdd { even, odd }; + self + } +} + +#[derive(Debug, Clone)] +pub enum TableTheme { + Single(ThemeAttribute), + EvenOdd { + even: ThemeAttribute, + odd: ThemeAttribute, + }, +} + +impl Default for TableTheme { + fn default() -> Self { + Self::Single(ThemeAttribute::default()) + } +} + +#[derive(Debug, Default, Clone)] +pub struct TableCursorConfig { + pub handle: bool, + pub theme: TableTheme, +} + +impl TableCursorConfig { + pub fn set_handle(&mut self, value: bool) -> &mut Self { + self.handle = value; + self + } + + pub fn set_single_theme(&mut self, value: ThemeAttribute) -> &mut Self { + self.theme = TableTheme::Single(value); + self + } + + pub fn set_even_odd_theme(&mut self, even: ThemeAttribute, odd: ThemeAttribute) -> &mut Self { + self.theme = TableTheme::EvenOdd { even, odd }; + self + } +} + +#[derive(Debug, Clone)] +pub struct DataColumns { + pub cursor_config: TableCursorConfig, + pub theme_config: TableThemeConfig, + pub columns: Box<[CellBuffer; N]>, + /// widths of columns calculated in first draw and after size changes + pub widths: [usize; N], + pub elasticities: [ColumnElasticity; N], + pub x_offset: usize, + pub width_accum: usize, + pub segment_tree: Box<[SegmentTree; N]>, +} + +// Workaround because Default derive doesn't work for const generic array lengths yet. +impl Default for DataColumns { + fn default() -> Self { + fn init_array(cl: impl Fn() -> T) -> [T; N] { + // https://doc.rust-lang.org/std/mem/union.MaybeUninit.html#initializing-an-array-element-by-element + let mut data: [MaybeUninit; N] = unsafe { MaybeUninit::uninit().assume_init() }; + for elem in &mut data[..] { + elem.write(cl()); + } + let ptr = &data as *const [MaybeUninit; N]; + std::mem::forget(data); + unsafe { (ptr as *const [T; N]).read() } + } + Self { + cursor_config: TableCursorConfig::default(), + theme_config: TableThemeConfig::default(), + columns: Box::new(init_array(CellBuffer::default)), + widths: [0_usize; N], + elasticities: [ColumnElasticity::default(); N], + x_offset: 0, + width_accum: 0, + segment_tree: Box::new(init_array(SegmentTree::default)), + } + } +} + +impl DataColumns { + pub fn recalc_widths( + &mut self, + (screen_width, screen_height): (usize, usize), + top_idx: usize, + ) -> usize { + let mut width_accum = 0; + let mut growees = 0; + let mut growees_max = 0; + let grow_minmax = None; + for i in 0..N { + if screen_height == 0 { + self.widths[i] = 0; + continue; + } + self.widths[i] = + self.segment_tree[i].get_max(top_idx, top_idx + screen_height - 1) as usize; + if self.widths[i] == 0 { + self.widths[i] = self.columns[i].cols; + } + match self.elasticities[i] { + ColumnElasticity::Rigid => {} + ColumnElasticity::Grow { + min, + max: Some(max), + } => { + self.widths[i] = std::cmp::max(min, std::cmp::min(max, self.widths[i])); + growees += 1; + } + ColumnElasticity::Grow { min, max: None } => { + self.widths[i] = std::cmp::max(min, self.widths[i]); + growees += 1; + growees_max += 1; + } + } + width_accum += self.widths[i]; + } + // add column gaps + width_accum += 2 * N.saturating_sub(1); + debug_assert!(growees >= growees_max); + debug_assert!(grow_minmax.is_none() || growees_max > 0); + if width_accum >= screen_width || screen_height == 0 || screen_width == 0 || growees == 0 { + self.width_accum = width_accum; + return width_accum; + } + let distribute = screen_width - width_accum; + let maxmins = growees_max * grow_minmax.unwrap_or(0); + let part = if maxmins != 0 && growees_max < growees { + distribute.saturating_sub(maxmins) / (growees - growees_max) + } else if maxmins != 0 { + distribute.saturating_sub(maxmins) / growees_max + grow_minmax.unwrap_or(0) + } else { + distribute / growees + }; + + for i in 0..N { + match self.elasticities[i] { + ColumnElasticity::Rigid => {} + ColumnElasticity::Grow { + min: _, + max: Some(_), + } => {} + ColumnElasticity::Grow { min: _, max: None } => { + self.widths[i] += part; + width_accum += part; + } + } + } + self.width_accum = width_accum; + width_accum + } + + pub fn draw( + &self, + grid: &mut CellBuffer, + top_idx: usize, + cursor_pos: usize, + mut bounds: BoundsIterator, + ) { + let mut _relative_x_offset = 0; + let mut skip_cols = (0, 0); + let mut start_col = 0; + let total_area = bounds.area(); + let (width, height) = (width!(total_area), height!(total_area)); + while _relative_x_offset < self.x_offset && start_col < N { + _relative_x_offset += self.widths[start_col] + 2; + if self.x_offset <= _relative_x_offset { + skip_cols.0 = start_col; + skip_cols.1 = _relative_x_offset - self.x_offset; + _relative_x_offset = self.x_offset; + break; + } + start_col += 1; + } + + for col in skip_cols.0..N { + if bounds.is_empty() { + break; + } + + let mut column_width = self.widths[col]; + if column_width > bounds.width { + column_width = bounds.width; + } else if column_width == 0 { + skip_cols.1 = 0; + continue; + } + + let mut column_area = bounds.add_x(column_width + 2); + copy_area( + grid, + &self.columns[col], + column_area.area(), + ( + (skip_cols.1, top_idx), + ( + column_width.saturating_sub(1), + self.columns[col].rows.saturating_sub(1), + ), + ), + ); + let gap_area = column_area.add_x(column_width); + match self.theme_config.theme { + TableTheme::Single(row_attr) => { + change_colors(grid, gap_area.area(), row_attr.fg, row_attr.bg); + } + TableTheme::EvenOdd { even, odd } => { + change_colors(grid, gap_area.area(), even.fg, even.bg); + let mut top_idx = top_idx; + for row in gap_area { + if top_idx % 2 != 0 { + change_colors(grid, row.area(), odd.fg, odd.bg); + } + top_idx += 1; + } + } + }; + + skip_cols.1 = 0; + } + if self.cursor_config.handle && (top_idx..(top_idx + height)).contains(&cursor_pos) { + let offset = cursor_pos - top_idx; + let row_attr = match self.cursor_config.theme { + TableTheme::Single(attr) => attr, + TableTheme::EvenOdd { even, odd: _ } if cursor_pos % 2 == 0 => even, + TableTheme::EvenOdd { even: _, odd } => odd, + }; + + change_colors( + grid, + ( + pos_inc(upper_left!(total_area), (0, offset)), + pos_inc(upper_left!(total_area), (width, offset)), + ), + row_attr.fg, + row_attr.bg, + ); + } + } +} diff --git a/src/terminal/cells.rs b/src/terminal/cells.rs index 914cbe56..20f239e8 100644 --- a/src/terminal/cells.rs +++ b/src/terminal/cells.rs @@ -26,6 +26,7 @@ use super::{position::*, Color}; use crate::state::Context; +use crate::ThemeAttribute; use melib::text_processing::wcwidth; use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; @@ -57,9 +58,9 @@ pub struct ScrollRegion { /// index, `Cellbuffer[y][x]`, corresponds to a column within a row and thus the x-axis. #[derive(Clone, PartialEq, Eq)] pub struct CellBuffer { - cols: usize, - rows: usize, - buf: Vec, + pub cols: usize, + pub rows: usize, + pub buf: Vec, pub default_cell: Cell, /// ASCII-only flag. pub ascii_drawing: bool, @@ -357,6 +358,8 @@ impl CellBuffer { /// See `BoundsIterator` documentation. pub fn bounds_iter(&self, area: Area) -> BoundsIterator { BoundsIterator { + width: width!(area), + height: height!(area), rows: std::cmp::min(self.rows.saturating_sub(1), get_y(upper_left!(area))) ..(std::cmp::min(self.rows, get_y(bottom_right!(area)) + 1)), cols: ( @@ -373,9 +376,14 @@ impl CellBuffer { row, col: std::cmp::min(self.cols.saturating_sub(1), bounds.start) ..(std::cmp::min(self.cols, bounds.end)), + _width: bounds.len(), } } else { - RowIterator { row, col: 0..0 } + RowIterator { + row, + col: 0..0, + _width: 0, + } } } @@ -1247,8 +1255,10 @@ pub fn clear_area(grid: &mut CellBuffer, area: Area, attributes: crate::conf::Th /// grid[c].set_ch('w'); /// } /// ``` +#[derive(Debug)] pub struct RowIterator { row: usize, + _width: usize, col: std::ops::Range, } @@ -1263,17 +1273,64 @@ pub struct RowIterator { /// } /// } /// ``` +#[derive(Clone, Debug)] pub struct BoundsIterator { rows: std::ops::Range, + pub width: usize, + pub height: usize, cols: (usize, usize), } +impl BoundsIterator { + const EMPTY: Self = BoundsIterator { + rows: 0..0, + width: 0, + height: 0, + cols: (0, 0), + }; + + pub fn area(&self) -> Area { + ( + (self.cols.0, self.rows.start), + ( + std::cmp::max(self.cols.0, self.cols.1.saturating_sub(1)), + std::cmp::max(self.rows.start, self.rows.end.saturating_sub(1)), + ), + ) + } + + pub fn is_empty(&self) -> bool { + self.width == 0 || self.height == 0 || self.rows.len() == 0 + } + + pub fn add_x(&mut self, x: usize) -> Self { + if x == 0 { + return Self::EMPTY; + } + + let ret = Self { + rows: self.rows.clone(), + width: self.width.saturating_sub(x), + height: self.height, + cols: self.cols, + }; + if self.cols.0 + x < self.cols.1 && self.width > x { + self.cols.0 += x; + self.width -= x; + return ret; + } + *self = Self::EMPTY; + ret + } +} + impl Iterator for BoundsIterator { type Item = RowIterator; fn next(&mut self) -> Option { if let Some(next_row) = self.rows.next() { Some(RowIterator { row: next_row, + _width: self.width, col: self.cols.0..self.cols.1, }) } else { @@ -1305,6 +1362,10 @@ impl RowIterator { self } } + + pub fn area(&self) -> Area { + ((self.col.start, self.row), (self.col.end, self.row)) + } } pub use boundaries::create_box; @@ -1739,6 +1800,26 @@ impl core::cmp::PartialOrd for FormatTag { } } +impl From for FormatTag { + fn from(val: ThemeAttribute) -> Self { + let ThemeAttribute { fg, bg, attrs, .. } = val; + Self { + fg: Some(fg), + bg: Some(bg), + attrs: Some(attrs), + priority: 0, + } + } +} + +impl FormatTag { + #[inline(always)] + pub fn set_priority(mut self, new_val: u8) -> Self { + self.priority = new_val; + self + } +} + #[derive(Debug, Copy, Hash, Clone, PartialEq, Eq)] pub enum WidgetWidth { Unset, diff --git a/src/terminal/position.rs b/src/terminal/position.rs index 9c93a0b0..8f62da86 100644 --- a/src/terminal/position.rs +++ b/src/terminal/position.rs @@ -76,6 +76,7 @@ macro_rules! height { ($a:expr) => { ($crate::get_y($crate::bottom_right!($a))) .saturating_sub($crate::get_y($crate::upper_left!($a))) + + 1 }; } @@ -93,6 +94,7 @@ macro_rules! width { ($a:expr) => { ($crate::get_x($crate::bottom_right!($a))) .saturating_sub($crate::get_x($crate::upper_left!($a))) + + 1 }; } @@ -252,3 +254,12 @@ pub fn place_in_area(area: Area, (width, height): (usize, usize), upper: bool, l ), ) } + +#[inline(always)] +/// Get `n`th row of `area` or its last one. +pub fn nth_row_area(area: Area, n: usize) -> Area { + let (upper_left, bottom_right) = area; + let (_, max_y) = bottom_right; + let y = std::cmp::min(max_y, get_y(upper_left) + n); + (set_y(upper_left, y), set_y(bottom_right, y)) +}