Add table UI widget

pull/168/head
Manos Pitsidianakis 2022-11-24 16:43:53 +02:00
parent bd22f986f0
commit 104352e595
12 changed files with 706 additions and 662 deletions

View File

@ -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,

View File

@ -58,6 +58,7 @@ pub struct RowsState<T> {
pub entries: Vec<(T, EntryStrings)>,
pub all_threads: HashSet<ThreadHash>,
pub all_envelopes: HashSet<EnvelopeHash>,
pub row_attr_cache: HashMap<usize, ThemeAttribute>,
}
impl<T> RowsState<T> {
@ -72,6 +73,7 @@ impl<T> RowsState<T> {
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,

View File

@ -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::<usize>() > width {
let diff = self.data_columns.widths.iter().sum::<usize>() - 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;
_ = 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);
}
}
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.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

View File

@ -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
};

View File

@ -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<Result<SmallVec<[EnvelopeHash; 512]>>>)>,
@ -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;
_ = 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);
}
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);
}
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;
}

View File

@ -121,9 +121,9 @@ pub struct ThreadListing {
crate::jobs::JoinHandle<Result<SmallVec<[EnvelopeHash; 512]>>>,
)>,
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::<usize>() > width {
let diff = self.data_columns.widths.iter().sum::<usize>() - 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));
/* 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);
}
}
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);
}
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),
);
self.highlight_line(grid, new_area, row, context);
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);
}
}
@ -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;
}

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
/*! 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<usize>,
},
}
impl ColumnElasticity {
pub fn set_rigid(&mut self) {
*self = Self::Rigid;
}
pub fn set_grow(&mut self, min: usize, max: Option<usize>) {
*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<usize, SmallVec<[(u8, TableRowFormat); 6]>>,
}
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<const N: usize> {
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<const N: usize> Default for DataColumns<N> {
fn default() -> Self {
fn init_array<T, const N: usize>(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<T>; N] = unsafe { MaybeUninit::uninit().assume_init() };
for elem in &mut data[..] {
elem.write(cl());
}
let ptr = &data as *const [MaybeUninit<T>; 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<const N: usize> DataColumns<N> {
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,
);
}
}
}

View File

@ -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<Cell>,
pub cols: usize,
pub rows: usize,
pub buf: Vec<Cell>,
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<usize>,
}
@ -1263,17 +1273,64 @@ pub struct RowIterator {
/// }
/// }
/// ```
#[derive(Clone, Debug)]
pub struct BoundsIterator {
rows: std::ops::Range<usize>,
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<Self::Item> {
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<ThemeAttribute> 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,

View File

@ -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))
}