From 0e3a0c4b7049139994a65c6fe914dd3587c6713e Mon Sep 17 00:00:00 2001 From: Manos Pitsidianakis Date: Mon, 23 Oct 2023 13:56:13 +0300 Subject: [PATCH] Add safe UI widget area drawing API Make Screen generic over its display kind: Screen and Screen. The latter is for "cached" renderings we want to keep and copy to the actual screen when the Component::draw() method is called. Only Screen can write to stdout and it needs an stdout handle. Add a generation integer field to Screen, that changes each time it is resized. This way, we can track if "stale" areas are used and panic on runtime (in debug mode). Introduce a new type, Area, that keeps metadata about a subsection of a Screen, and the generation it came from. New areas can only be created from a Screen and by operating on an Area to create subsections of it. This way, it's impossible to make an area refer to (x, y) cells outside the screen generation of its provenance. If stabilised this API should eliminate all out of bounds accesses in CellBuffers. Signed-off-by: Manos Pitsidianakis --- meli/src/command/argcheck.rs | 8 +- meli/src/contacts/editor.rs | 6 +- meli/src/contacts/list.rs | 965 ++++++++---------- meli/src/contacts/mod.rs | 2 +- meli/src/jobs_view.rs | 68 +- meli/src/lib.rs | 8 +- meli/src/mail/compose.rs | 434 ++++---- meli/src/mail/compose/edit_attachments.rs | 8 +- meli/src/mail/compose/gpg.rs | 4 +- meli/src/mail/listing.rs | 465 ++++----- meli/src/mail/listing/compact.rs | 456 +++++---- meli/src/mail/listing/conversations.rs | 42 +- meli/src/mail/listing/offline.rs | 19 +- meli/src/mail/listing/plain.rs | 589 ++++++----- meli/src/mail/listing/thread.rs | 22 +- meli/src/mail/status.rs | 175 ++-- meli/src/mail/view/envelope.rs | 239 ++--- meli/src/mail/view/thread.rs | 547 +++++----- meli/src/mailbox_management.rs | 10 +- meli/src/state.rs | 454 ++++----- meli/src/svg.rs | 4 +- meli/src/terminal.rs | 2 +- meli/src/terminal/cells.rs | 907 +++++++++-------- meli/src/terminal/embed.rs | 6 +- meli/src/terminal/embed/grid.rs | 116 +-- meli/src/terminal/position.rs | 201 ---- meli/src/terminal/screen.rs | 1132 +++++++++++++++++++-- meli/src/utilities.rs | 967 +++++++++--------- meli/src/utilities/dialogs.rs | 451 ++------ meli/src/utilities/layouts.rs | 14 +- meli/src/utilities/pager.rs | 297 +++--- meli/src/utilities/tables.rs | 96 +- meli/src/utilities/text.rs | 7 +- meli/src/utilities/widgets.rs | 221 ++-- tools/src/embed.rs | 22 +- 35 files changed, 4700 insertions(+), 4264 deletions(-) diff --git a/meli/src/command/argcheck.rs b/meli/src/command/argcheck.rs index a935818f..4ea8d2a6 100644 --- a/meli/src/command/argcheck.rs +++ b/meli/src/command/argcheck.rs @@ -47,9 +47,9 @@ impl ArgCheck { } ); }; - let is_empty = dbg!(input.trim().is_empty()); - if is_empty && dbg!(MIN) > 0 { - return dbg!(Err(CommandError::WrongNumberOfArguments { + let is_empty = input.trim().is_empty(); + if is_empty && MIN > 0 { + return Err(CommandError::WrongNumberOfArguments { too_many: false, takes: (MIN, MAX.into()), given: 0, @@ -60,7 +60,7 @@ impl ArgCheck { MIN ) .into(), - })); + }); } *self = Self::BeforeArgument { so_far: 0, diff --git a/meli/src/contacts/editor.rs b/meli/src/contacts/editor.rs index 41edfe5b..6549e615 100644 --- a/meli/src/contacts/editor.rs +++ b/meli/src/contacts/editor.rs @@ -147,8 +147,8 @@ impl Component for ContactManager { self.initialized = true; } - let upper_left = upper_left!(area); - let bottom_right = bottom_right!(area); + let upper_left = area.upper_left(); + let bottom_right = area.bottom_right(); if self.dirty { let (width, _height) = self.content.size(); @@ -157,7 +157,7 @@ impl Component for ContactManager { (upper_left, set_y(bottom_right, get_y(upper_left) + 1)), self.theme_default, ); - grid.copy_area_with_break(&self.content, area, ((0, 0), (width - 1, 0))); + grid.copy_area(&self.content, area, ((0, 0), (width - 1, 0))); self.dirty = false; } diff --git a/meli/src/contacts/list.rs b/meli/src/contacts/list.rs index 285742aa..81f6e396 100644 --- a/meli/src/contacts/list.rs +++ b/meli/src/contacts/list.rs @@ -21,12 +21,12 @@ use std::cmp; -use melib::{backends::AccountHash, log, text_processing::TextProcessing, Card, CardId, Draft}; +use melib::{backends::AccountHash, text_processing::TextProcessing, Card, CardId, Draft}; use crate::{ - conf, contacts::editor::ContactManager, shortcut, terminal::*, Action::Tab, Component, - ComponentId, Composer, Context, DataColumns, PageMovement, ScrollContext, ScrollUpdate, - ShortcutMaps, Shortcuts, StatusEvent, TabAction, ThemeAttribute, UIEvent, UIMode, + conf, /* contacts::editor::ContactManager, */ shortcut, terminal::*, Action::Tab, + Component, ComponentId, Composer, Context, DataColumns, PageMovement, ScrollContext, + ScrollUpdate, ShortcutMaps, Shortcuts, StatusEvent, TabAction, ThemeAttribute, UIEvent, UIMode, }; #[derive(Debug, PartialEq, Eq)] @@ -66,7 +66,7 @@ pub struct ContactList { menu_visibility: bool, movement: Option, cmd_buf: String, - view: Option, + //view: Option, ratio: usize, // right/(container width) * 100 id: ComponentId, } @@ -104,7 +104,7 @@ impl ContactList { dirty: true, movement: None, cmd_buf: String::with_capacity(8), - view: None, + //view: None, ratio: 90, sidebar_divider: context.settings.listing.sidebar_divider, sidebar_divider_theme: conf::value(context, "mail.sidebar_divider"), @@ -121,6 +121,7 @@ impl ContactList { } fn initialize(&mut self, context: &mut Context) { + self.data_columns.clear(); let account = &context.accounts[self.account_pos]; let book = &account.address_book; self.length = book.len(); @@ -134,25 +135,34 @@ impl ContactList { for c in book.values() { /* name */ - min_width.0 = cmp::max(min_width.0, c.name().split_graphemes().len()); + let name = c.name().split_graphemes().len(); + if name > 0 { + min_width.0 = cmp::max(min_width.0, name + 1); + } /* email */ - min_width.1 = cmp::max(min_width.1, c.email().split_graphemes().len()); + let email = c.email().split_graphemes().len(); + if email > 0 { + min_width.1 = cmp::max(min_width.1, email + 1); + } /* url */ - min_width.2 = cmp::max(min_width.2, c.url().split_graphemes().len()); + let url = c.url().split_graphemes().len(); + if url > 0 { + min_width.2 = cmp::max(min_width.2, url + 1); + } } /* name column */ - self.data_columns.columns[0] = - CellBuffer::new_with_context(min_width.0, self.length, None, context); + _ = self.data_columns.columns[0].resize_with_context(min_width.0, self.length, context); /* email column */ - self.data_columns.columns[1] = - CellBuffer::new_with_context(min_width.1, self.length, None, context); + _ = self.data_columns.columns[1].resize_with_context(min_width.1, self.length, context); /* url column */ - self.data_columns.columns[2] = - CellBuffer::new_with_context(min_width.2, self.length, None, context); + _ = self.data_columns.columns[2].resize_with_context(min_width.2, self.length, context); /* source column */ - self.data_columns.columns[3] = - CellBuffer::new_with_context("external".len(), self.length, None, context); + _ = self.data_columns.columns[3].resize_with_context( + "external".len(), + self.length, + context, + ); let account = &context.accounts[self.account_pos]; let book = &account.address_book; @@ -161,59 +171,73 @@ impl ContactList { for (idx, c) in book_values.iter().enumerate() { self.id_positions.push(*c.id()); - self.data_columns.columns[0].write_string( - c.name(), - self.theme_default.fg, - self.theme_default.bg, - self.theme_default.attrs, - ((0, idx), (min_width.0, idx)), - None, - ); + { + let area = self.data_columns.columns[0].area().nth_row(idx); + self.data_columns.columns[0].grid_mut().write_string( + c.name(), + self.theme_default.fg, + self.theme_default.bg, + self.theme_default.attrs, + area, + None, + ) + }; - self.data_columns.columns[1].write_string( - c.email(), - self.theme_default.fg, - self.theme_default.bg, - self.theme_default.attrs, - ((0, idx), (min_width.1, idx)), - None, - ); + { + let area = self.data_columns.columns[1].area().nth_row(idx); + self.data_columns.columns[1].grid_mut().write_string( + c.email(), + self.theme_default.fg, + self.theme_default.bg, + self.theme_default.attrs, + area, + None, + ) + }; - self.data_columns.columns[2].write_string( - c.url(), - self.theme_default.fg, - self.theme_default.bg, - self.theme_default.attrs, - ((0, idx), (min_width.2, idx)), - None, - ); + { + let area = self.data_columns.columns[2].area().nth_row(idx); + self.data_columns.columns[2].grid_mut().write_string( + c.url(), + self.theme_default.fg, + self.theme_default.bg, + self.theme_default.attrs, + area, + None, + ) + }; - self.data_columns.columns[3].write_string( - if c.external_resource() { - "external" - } else { - "local" - }, - self.theme_default.fg, - self.theme_default.bg, - self.theme_default.attrs, - ((0, idx), (min_width.3, idx)), - None, - ); + { + let area = self.data_columns.columns[3].area().nth_row(idx); + self.data_columns.columns[3].grid_mut().write_string( + if c.external_resource() { + "external" + } else { + "local" + }, + self.theme_default.fg, + self.theme_default.bg, + self.theme_default.attrs, + area, + None, + ) + }; } if self.length == 0 { let message = "Address book is empty.".to_string(); - self.data_columns.columns[0] = - CellBuffer::new_with_context(message.len(), self.length, None, context); - self.data_columns.columns[0].write_string( - &message, - self.theme_default.fg, - self.theme_default.bg, - self.theme_default.attrs, - ((0, 0), (message.len() - 1, 0)), - None, - ); + if self.data_columns.columns[0].resize_with_context(message.len(), self.length, context) + { + let area = self.data_columns.columns[0].area(); + self.data_columns.columns[0].grid_mut().write_string( + &message, + self.theme_default.fg, + self.theme_default.bg, + self.theme_default.attrs, + area, + None, + ); + } } } @@ -236,17 +260,14 @@ impl ContactList { return; } grid.clear_area(area, self.theme_default); - let upper_left = upper_left!(area); - let bottom_right = bottom_right!(area); self.dirty = false; - let mut y = get_y(upper_left); - for a in &self.accounts { - self.print_account(grid, (set_y(upper_left, y), bottom_right), a, context); - y += 1; + for (y, a) in self.accounts.iter().enumerate() { + self.print_account(grid, area.nth_row(y), a, context); } context.dirty_areas.push_back(area); } + /* * Print a single account in the menu area. */ @@ -257,11 +278,7 @@ impl ContactList { a: &AccountMenuEntry, context: &mut Context, ) { - if !is_valid_area!(area) { - log::debug!("BUG: invalid area in print_account"); - } - - let width = width!(area); + let width = area.width(); let must_highlight_account: bool = self.account_pos == a.index; let account_attrs = if must_highlight_account { let mut v = crate::conf::value(context, "mail.sidebar_highlighted"); @@ -273,104 +290,53 @@ impl ContactList { crate::conf::value(context, "mail.sidebar_account_name") }; + grid.change_theme(area, account_attrs); let s = format!(" [{}]", context.accounts[a.index].address_book.len()); + /* Print account name */ + grid.write_string( + &a.name, + account_attrs.fg, + account_attrs.bg, + account_attrs.attrs, + area, + None, + ); + grid.write_string( + &s, + account_attrs.fg, + account_attrs.bg, + account_attrs.attrs, + area.skip_cols(area.width().saturating_sub(s.len())), + None, + ); if a.name.grapheme_len() + s.len() > width + 1 { - /* Print account name */ - let (x, y) = grid.write_string( - &a.name, - account_attrs.fg, - account_attrs.bg, - account_attrs.attrs, - area, - None, - ); - grid.write_string( - &s, - account_attrs.fg, - account_attrs.bg, - account_attrs.attrs, - ( - pos_dec( - (get_x(bottom_right!(area)), get_y(upper_left!(area))), - (s.len() - 1, 0), - ), - bottom_right!(area), - ), - None, - ); grid.write_string( "…", account_attrs.fg, account_attrs.bg, account_attrs.attrs, - ( - pos_dec( - (get_x(bottom_right!(area)), get_y(upper_left!(area))), - (s.len() - 1, 0), - ), - bottom_right!(area), - ), + area.skip_cols(area.width().saturating_sub(s.len() + 1)), None, ); - - for x in x..=get_x(bottom_right!(area)) { - grid[(x, y)] - .set_fg(account_attrs.fg) - .set_bg(account_attrs.bg) - .set_attrs(account_attrs.attrs); - } - } else { - /* Print account name */ - - let (x, y) = grid.write_string( - &a.name, - account_attrs.fg, - account_attrs.bg, - account_attrs.attrs, - area, - None, - ); - grid.write_string( - &s, - account_attrs.fg, - account_attrs.bg, - account_attrs.attrs, - ( - pos_dec( - (get_x(bottom_right!(area)), get_y(upper_left!(area))), - (s.len() - 1, 0), - ), - bottom_right!(area), - ), - None, - ); - for x in x..=get_x(bottom_right!(area)) { - grid[(x, y)] - .set_fg(account_attrs.fg) - .set_bg(account_attrs.bg) - .set_attrs(account_attrs.attrs); - } } } fn draw_list(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { - /* reserve top row for column headers */ - let upper_left = pos_inc(upper_left!(area), (0, 1)); - let bottom_right = bottom_right!(area); - if self.length == 0 { + /* reserve top row for column headers */ + let area = area.skip_rows(1); grid.clear_area(area, self.theme_default); grid.copy_area( - &self.data_columns.columns[0], + self.data_columns.columns[0].grid(), area, - ((0, 0), pos_dec(self.data_columns.columns[0].size(), (1, 1))), + self.data_columns.columns[0].area(), ); context.dirty_areas.push_back(area); return; } - let rows = get_y(bottom_right) - get_y(upper_left) + 1; + let rows = area.height(); if let Some(mvm) = self.movement.take() { match mvm { @@ -440,12 +406,9 @@ impl ContactList { self.cursor_pos = self.new_cursor_pos; for idx in &[old_cursor_pos, self.new_cursor_pos] { if *idx >= self.length { - continue; //bounds check + continue; } - let new_area = ( - set_y(upper_left, get_y(upper_left) + (*idx % rows)), - set_y(bottom_right, get_y(upper_left) + (*idx % rows)), - ); + let new_area = area.nth_row(1 + *idx % rows); self.highlight_line(grid, new_area, *idx); context.dirty_areas.push_back(new_area); } @@ -458,33 +421,25 @@ impl ContactList { self.cursor_pos = self.new_cursor_pos; } - let width = width!(area); - self.data_columns.widths = Default::default(); - self.data_columns.widths[0] = self.data_columns.columns[0].size().0; /* name */ - self.data_columns.widths[1] = self.data_columns.columns[1].size().0; /* email */ - self.data_columns.widths[2] = self.data_columns.columns[2].size().0; /* url */ - self.data_columns.widths[3] = self.data_columns.columns[3].size().0; /* source */ - - let min_col_width = std::cmp::min( - 15, - std::cmp::min(self.data_columns.widths[0], self.data_columns.widths[1]), - ); - 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] + self.data_columns.widths[1] + 4); - self.data_columns.widths[2] = remainder / 6; - } - grid.clear_area(area, self.theme_default); /* Page_no has changed, so draw new page */ + grid.clear_area(area, self.theme_default); + _ = self + .data_columns + .recalc_widths((area.width(), area.height().saturating_sub(1)), top_idx); + /* copy table columns */ + self.data_columns.draw( + grid, + top_idx, + self.cursor_pos, + grid.bounds_iter(area.skip_rows(1)), + ); let header_attrs = crate::conf::value(context, "widgets.list.header"); - let mut x = get_x(upper_left); + let mut x = 0; for i in 0..self.data_columns.columns.len() { if self.data_columns.widths[i] == 0 { continue; } - let (column_width, column_height) = self.data_columns.columns[i].size(); grid.write_string( match i { 0 => "NAME", @@ -496,62 +451,28 @@ impl ContactList { header_attrs.fg, header_attrs.bg, header_attrs.attrs, - ( - set_x(upper_left!(area), x), - ( - std::cmp::min(get_x(bottom_right), x + (self.data_columns.widths[i])), - get_y(upper_left!(area)), - ), - ), + area.skip_cols(x) + .take_cols(x + (self.data_columns.widths[i])), None, ); - grid.copy_area( - &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), - column_height.saturating_sub(1), - ), - ), - ); x += self.data_columns.widths[i] + 2; // + SEPARATOR - if x > get_x(bottom_right) { + if x > area.width() { break; } } - grid.change_theme( - ( - upper_left!(area), - set_y(bottom_right, get_y(upper_left!(area))), - ), - header_attrs, - ); + grid.change_theme(area.nth_row(0), header_attrs); if top_idx + rows > self.length { grid.clear_area( - ( - pos_inc(upper_left, (0, self.length - top_idx + 2)), - bottom_right, - ), + area.skip_rows(top_idx + rows - self.length.saturating_sub(1)), self.theme_default, ); } self.highlight_line( grid, - ( - set_y(upper_left, get_y(upper_left) + (self.cursor_pos % rows)), - set_y(bottom_right, get_y(upper_left) + (self.cursor_pos % rows)), - ), + area.nth_row(1 + self.cursor_pos % rows), self.cursor_pos, ); context.dirty_areas.push_back(area); @@ -560,10 +481,10 @@ impl ContactList { impl Component for ContactList { fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { - if let Some(mgr) = self.view.as_mut() { - mgr.draw(grid, area, context); - return; - } + //if let Some(mgr) = self.view.as_mut() { + // mgr.draw(grid, area, context); + // return; + //} if !self.dirty { return; @@ -572,27 +493,26 @@ impl Component for ContactList { self.initialize(context); } - let upper_left = upper_left!(area); - let bottom_right = bottom_right!(area); - let total_cols = get_x(bottom_right) - get_x(upper_left); + let total_cols = area.width(); let right_component_width = if self.menu_visibility { (self.ratio * total_cols) / 100 } else { total_cols }; - let mid = get_x(bottom_right) - right_component_width; - if self.dirty && mid != get_x(upper_left) { - for i in get_y(upper_left)..=get_y(bottom_right) { - grid[(mid, i)] - .set_ch(self.sidebar_divider) - .set_fg(self.sidebar_divider_theme.fg) - .set_bg(self.sidebar_divider_theme.bg) - .set_attrs(self.sidebar_divider_theme.attrs); + let mid = area.width().saturating_sub(right_component_width); + if self.dirty && mid != 0 { + let divider_area = area.nth_col(mid); + for row in grid.bounds_iter(divider_area) { + for c in row { + grid[c] + .set_ch(self.sidebar_divider) + .set_fg(self.sidebar_divider_theme.fg) + .set_bg(self.sidebar_divider_theme.bg) + .set_attrs(self.sidebar_divider_theme.attrs); + } } - context - .dirty_areas - .push_back(((mid, get_y(upper_left)), (mid, get_y(bottom_right)))); + context.dirty_areas.push_back(divider_area); } if right_component_width == total_cols { @@ -600,18 +520,19 @@ impl Component for ContactList { } else if right_component_width == 0 { self.draw_menu(grid, area, context); } else { - self.draw_menu( - grid, - (upper_left, (mid.saturating_sub(1), get_y(bottom_right))), - context, - ); - self.draw_list(grid, (set_x(upper_left, mid + 1), bottom_right), context); + self.draw_menu(grid, area.take_cols(mid), context); + self.draw_list(grid, area.skip_cols(mid + 1), context); } self.dirty = false; } fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool { match event { + UIEvent::VisibilityChange(true) => { + self.initialized = false; + self.set_dirty(true); + return true; + } UIEvent::ConfigReload { old_settings: _ } => { self.theme_default = crate::conf::value(context, "theme_default"); self.initialized = false; @@ -625,7 +546,7 @@ impl Component for ContactList { } UIEvent::ComponentUnrealize(ref kill_id) if self.mode == ViewMode::View(*kill_id) => { self.mode = ViewMode::List; - self.view.take(); + //self.view.take(); self.set_dirty(true); return true; } @@ -638,313 +559,314 @@ impl Component for ContactList { _ => {} } - if let Some(ref mut v) = self.view { - if v.process_event(event, context) { - return true; - } - } + //if let Some(ref mut v) = self.view { + // if v.process_event(event, context) { + // return true; + // } + //} let shortcuts = self.shortcuts(context); - if self.view.is_none() { - match *event { - UIEvent::Input(ref key) - if shortcut!(key == shortcuts[Shortcuts::CONTACT_LIST]["create_contact"]) => - { - let mut manager = ContactManager::new(context); - manager.set_parent_id(self.id); - manager.account_pos = self.account_pos; + //if self.view.is_none() { + match *event { + UIEvent::Input(ref key) + if shortcut!(key == shortcuts[Shortcuts::CONTACT_LIST]["create_contact"]) => + { + /* + let mut manager = ContactManager::new(context); + manager.set_parent_id(self.id); + manager.account_pos = self.account_pos; - self.mode = ViewMode::View(manager.id()); - self.view = Some(manager); - context - .replies - .push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate( - ScrollUpdate::End(self.id), - ))); + self.mode = ViewMode::View(manager.id()); + self.view = Some(manager); + context + .replies + .push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate( + ScrollUpdate::End(self.id), + ))); + */ + return true; + } + + UIEvent::Input(ref key) + if shortcut!(key == shortcuts[Shortcuts::CONTACT_LIST]["edit_contact"]) => + { + if self.length == 0 { return true; } + /* + let account = &mut context.accounts[self.account_pos]; + let book = &mut account.address_book; + let card = book[&self.id_positions[self.cursor_pos]].clone(); + let mut manager = ContactManager::new(context); + manager.set_parent_id(self.id); + manager.card = card; + manager.account_pos = self.account_pos; - UIEvent::Input(ref key) - if shortcut!(key == shortcuts[Shortcuts::CONTACT_LIST]["edit_contact"]) => - { - if self.length == 0 { - return true; - } - let account = &mut context.accounts[self.account_pos]; - let book = &mut account.address_book; - let card = book[&self.id_positions[self.cursor_pos]].clone(); - let mut manager = ContactManager::new(context); - manager.set_parent_id(self.id); - manager.card = card; - manager.account_pos = self.account_pos; - - self.mode = ViewMode::View(manager.id()); - self.view = Some(manager); - context - .replies - .push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate( - ScrollUpdate::End(self.id), - ))); + self.mode = ViewMode::View(manager.id()); + self.view = Some(manager); + context + .replies + .push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate( + ScrollUpdate::End(self.id), + ))); + */ + return true; + } + UIEvent::Input(ref key) + if shortcut!(key == shortcuts[Shortcuts::CONTACT_LIST]["mail_contact"]) => + { + if self.length == 0 { return true; } - UIEvent::Input(ref key) - if shortcut!(key == shortcuts[Shortcuts::CONTACT_LIST]["mail_contact"]) => - { - if self.length == 0 { - return true; - } - let account = &context.accounts[self.account_pos]; - let account_hash = account.hash(); - let book = &account.address_book; - let card = &book[&self.id_positions[self.cursor_pos]]; - let mut draft: Draft = Draft::default(); - *draft.headers_mut().get_mut("To").unwrap() = - format!("{} <{}>", &card.name(), &card.email()); - let mut composer = Composer::with_account(account_hash, context); - composer.set_draft(draft, context); - context - .replies - .push_back(UIEvent::Action(Tab(TabAction::New(Some(Box::new( - composer, - )))))); + let account = &context.accounts[self.account_pos]; + let account_hash = account.hash(); + let book = &account.address_book; + let card = &book[&self.id_positions[self.cursor_pos]]; + let mut draft: Draft = Draft::default(); + *draft.headers_mut().get_mut("To").unwrap() = + format!("{} <{}>", &card.name(), &card.email()); + let mut composer = Composer::with_account(account_hash, context); + composer.set_draft(draft, context); + context + .replies + .push_back(UIEvent::Action(Tab(TabAction::New(Some(Box::new( + composer, + )))))); + return true; + } + UIEvent::Input(ref key) + if shortcut!(key == shortcuts[Shortcuts::CONTACT_LIST]["delete_contact"]) => + { + if self.length == 0 { return true; } - UIEvent::Input(ref key) - if shortcut!(key == shortcuts[Shortcuts::CONTACT_LIST]["delete_contact"]) => - { - if self.length == 0 { - return true; - } - // [ref:TODO]: add a confirmation dialog? - context.accounts[self.account_pos] - .address_book - .remove_card(self.id_positions[self.cursor_pos]); - self.initialized = false; - self.set_dirty(true); + // [ref:TODO]: add a confirmation dialog? + context.accounts[self.account_pos] + .address_book + .remove_card(self.id_positions[self.cursor_pos]); + self.initialized = false; + self.set_dirty(true); + context + .replies + .push_back(UIEvent::StatusEvent(StatusEvent::BufClear)); + + return true; + } + UIEvent::Input(ref key) + if shortcut!(key == shortcuts[Shortcuts::CONTACT_LIST]["next_account"]) => + { + let amount = if self.cmd_buf.is_empty() { + 1 + } else if let Ok(amount) = self.cmd_buf.parse::() { + self.cmd_buf.clear(); context .replies .push_back(UIEvent::StatusEvent(StatusEvent::BufClear)); - - return true; - } - UIEvent::Input(ref key) - if shortcut!(key == shortcuts[Shortcuts::CONTACT_LIST]["next_account"]) => - { - let amount = if self.cmd_buf.is_empty() { - 1 - } else if let Ok(amount) = self.cmd_buf.parse::() { - self.cmd_buf.clear(); - context - .replies - .push_back(UIEvent::StatusEvent(StatusEvent::BufClear)); - amount - } else { - self.cmd_buf.clear(); - context - .replies - .push_back(UIEvent::StatusEvent(StatusEvent::BufClear)); - return true; - }; - if self.account_pos + amount < self.accounts.len() { - self.account_pos += amount; - self.set_dirty(true); - self.initialized = false; - self.cursor_pos = 0; - self.new_cursor_pos = 0; - self.length = 0; - context - .replies - .push_back(UIEvent::StatusEvent(StatusEvent::UpdateStatus( - self.status(context), - ))); - } - - return true; - } - UIEvent::Input(ref key) - if shortcut!(key == shortcuts[Shortcuts::CONTACT_LIST]["prev_account"]) => - { - let amount = if self.cmd_buf.is_empty() { - 1 - } else if let Ok(amount) = self.cmd_buf.parse::() { - self.cmd_buf.clear(); - context - .replies - .push_back(UIEvent::StatusEvent(StatusEvent::BufClear)); - amount - } else { - self.cmd_buf.clear(); - context - .replies - .push_back(UIEvent::StatusEvent(StatusEvent::BufClear)); - return true; - }; - if self.accounts.is_empty() { - return true; - } - if self.account_pos >= amount { - self.account_pos -= amount; - self.set_dirty(true); - self.cursor_pos = 0; - self.new_cursor_pos = 0; - self.length = 0; - self.initialized = false; - context - .replies - .push_back(UIEvent::StatusEvent(StatusEvent::UpdateStatus( - self.status(context), - ))); - } - return true; - } - UIEvent::Input(ref k) - if shortcut!( - k == shortcuts[Shortcuts::CONTACT_LIST]["toggle_menu_visibility"] - ) => - { - self.menu_visibility = !self.menu_visibility; - self.set_dirty(true); - } - UIEvent::Input(Key::Esc) | UIEvent::Input(Key::Alt('')) - if !self.cmd_buf.is_empty() => - { + amount + } else { self.cmd_buf.clear(); context .replies .push_back(UIEvent::StatusEvent(StatusEvent::BufClear)); return true; - } - UIEvent::Input(Key::Char(c)) if c.is_ascii_digit() => { - self.cmd_buf.push(c); + }; + if self.account_pos + amount < self.accounts.len() { + self.account_pos += amount; + self.set_dirty(true); + self.initialized = false; + self.cursor_pos = 0; + self.new_cursor_pos = 0; + self.length = 0; context .replies - .push_back(UIEvent::StatusEvent(StatusEvent::BufSet( - self.cmd_buf.clone(), + .push_back(UIEvent::StatusEvent(StatusEvent::UpdateStatus( + self.status(context), ))); - return true; } - UIEvent::Input(ref key) - if shortcut!(key == shortcuts[Shortcuts::CONTACT_LIST]["scroll_up"]) => - { - let amount = if self.cmd_buf.is_empty() { - 1 - } else if let Ok(amount) = self.cmd_buf.parse::() { - self.cmd_buf.clear(); - context - .replies - .push_back(UIEvent::StatusEvent(StatusEvent::BufClear)); - amount - } else { - self.cmd_buf.clear(); - context - .replies - .push_back(UIEvent::StatusEvent(StatusEvent::BufClear)); - return true; - }; - self.movement = Some(PageMovement::Up(amount)); - self.set_dirty(true); - return true; - } - UIEvent::Input(ref key) - if shortcut!(key == shortcuts[Shortcuts::CONTACT_LIST]["scroll_down"]) => - { - if self.cursor_pos >= self.length.saturating_sub(1) { - return true; - } - let amount = if self.cmd_buf.is_empty() { - 1 - } else if let Ok(amount) = self.cmd_buf.parse::() { - self.cmd_buf.clear(); - context - .replies - .push_back(UIEvent::StatusEvent(StatusEvent::BufClear)); - amount - } else { - self.cmd_buf.clear(); - context - .replies - .push_back(UIEvent::StatusEvent(StatusEvent::BufClear)); - return true; - }; - self.set_dirty(true); - self.movement = Some(PageMovement::Down(amount)); - return true; - } - UIEvent::Input(ref key) - if shortcut!(key == shortcuts[Shortcuts::GENERAL]["prev_page"]) => - { - let mult = if self.cmd_buf.is_empty() { - 1 - } else if let Ok(mult) = self.cmd_buf.parse::() { - self.cmd_buf.clear(); - context - .replies - .push_back(UIEvent::StatusEvent(StatusEvent::BufClear)); - mult - } else { - self.cmd_buf.clear(); - context - .replies - .push_back(UIEvent::StatusEvent(StatusEvent::BufClear)); - return true; - }; - self.set_dirty(true); - self.movement = Some(PageMovement::PageUp(mult)); - return true; - } - UIEvent::Input(ref key) - if shortcut!(key == shortcuts[Shortcuts::GENERAL]["next_page"]) => - { - let mult = if self.cmd_buf.is_empty() { - 1 - } else if let Ok(mult) = self.cmd_buf.parse::() { - self.cmd_buf.clear(); - context - .replies - .push_back(UIEvent::StatusEvent(StatusEvent::BufClear)); - mult - } else { - self.cmd_buf.clear(); - context - .replies - .push_back(UIEvent::StatusEvent(StatusEvent::BufClear)); - return true; - }; - self.set_dirty(true); - self.movement = Some(PageMovement::PageDown(mult)); - return true; - } - UIEvent::Input(ref key) - if shortcut!(key == shortcuts[Shortcuts::GENERAL]["home_page"]) => - { - self.set_dirty(true); - self.movement = Some(PageMovement::Home); - return true; - } - UIEvent::Input(ref key) - if shortcut!(key == shortcuts[Shortcuts::GENERAL]["end_page"]) => - { - self.set_dirty(true); - self.movement = Some(PageMovement::End); - return true; - } - _ => {} + + return true; } + UIEvent::Input(ref key) + if shortcut!(key == shortcuts[Shortcuts::CONTACT_LIST]["prev_account"]) => + { + let amount = if self.cmd_buf.is_empty() { + 1 + } else if let Ok(amount) = self.cmd_buf.parse::() { + self.cmd_buf.clear(); + context + .replies + .push_back(UIEvent::StatusEvent(StatusEvent::BufClear)); + amount + } else { + self.cmd_buf.clear(); + context + .replies + .push_back(UIEvent::StatusEvent(StatusEvent::BufClear)); + return true; + }; + if self.accounts.is_empty() { + return true; + } + if self.account_pos >= amount { + self.account_pos -= amount; + self.set_dirty(true); + self.cursor_pos = 0; + self.new_cursor_pos = 0; + self.length = 0; + self.initialized = false; + context + .replies + .push_back(UIEvent::StatusEvent(StatusEvent::UpdateStatus( + self.status(context), + ))); + } + return true; + } + UIEvent::Input(ref k) + if shortcut!(k == shortcuts[Shortcuts::CONTACT_LIST]["toggle_menu_visibility"]) => + { + self.menu_visibility = !self.menu_visibility; + self.set_dirty(true); + } + UIEvent::Input(Key::Esc) | UIEvent::Input(Key::Alt('')) if !self.cmd_buf.is_empty() => { + self.cmd_buf.clear(); + context + .replies + .push_back(UIEvent::StatusEvent(StatusEvent::BufClear)); + return true; + } + UIEvent::Input(Key::Char(c)) if c.is_ascii_digit() => { + self.cmd_buf.push(c); + context + .replies + .push_back(UIEvent::StatusEvent(StatusEvent::BufSet( + self.cmd_buf.clone(), + ))); + return true; + } + UIEvent::Input(ref key) + if shortcut!(key == shortcuts[Shortcuts::CONTACT_LIST]["scroll_up"]) => + { + let amount = if self.cmd_buf.is_empty() { + 1 + } else if let Ok(amount) = self.cmd_buf.parse::() { + self.cmd_buf.clear(); + context + .replies + .push_back(UIEvent::StatusEvent(StatusEvent::BufClear)); + amount + } else { + self.cmd_buf.clear(); + context + .replies + .push_back(UIEvent::StatusEvent(StatusEvent::BufClear)); + return true; + }; + self.movement = Some(PageMovement::Up(amount)); + self.set_dirty(true); + return true; + } + UIEvent::Input(ref key) + if shortcut!(key == shortcuts[Shortcuts::CONTACT_LIST]["scroll_down"]) => + { + if self.cursor_pos >= self.length.saturating_sub(1) { + return true; + } + let amount = if self.cmd_buf.is_empty() { + 1 + } else if let Ok(amount) = self.cmd_buf.parse::() { + self.cmd_buf.clear(); + context + .replies + .push_back(UIEvent::StatusEvent(StatusEvent::BufClear)); + amount + } else { + self.cmd_buf.clear(); + context + .replies + .push_back(UIEvent::StatusEvent(StatusEvent::BufClear)); + return true; + }; + self.set_dirty(true); + self.movement = Some(PageMovement::Down(amount)); + return true; + } + UIEvent::Input(ref key) + if shortcut!(key == shortcuts[Shortcuts::GENERAL]["prev_page"]) => + { + let mult = if self.cmd_buf.is_empty() { + 1 + } else if let Ok(mult) = self.cmd_buf.parse::() { + self.cmd_buf.clear(); + context + .replies + .push_back(UIEvent::StatusEvent(StatusEvent::BufClear)); + mult + } else { + self.cmd_buf.clear(); + context + .replies + .push_back(UIEvent::StatusEvent(StatusEvent::BufClear)); + return true; + }; + self.set_dirty(true); + self.movement = Some(PageMovement::PageUp(mult)); + return true; + } + UIEvent::Input(ref key) + if shortcut!(key == shortcuts[Shortcuts::GENERAL]["next_page"]) => + { + let mult = if self.cmd_buf.is_empty() { + 1 + } else if let Ok(mult) = self.cmd_buf.parse::() { + self.cmd_buf.clear(); + context + .replies + .push_back(UIEvent::StatusEvent(StatusEvent::BufClear)); + mult + } else { + self.cmd_buf.clear(); + context + .replies + .push_back(UIEvent::StatusEvent(StatusEvent::BufClear)); + return true; + }; + self.set_dirty(true); + self.movement = Some(PageMovement::PageDown(mult)); + return true; + } + UIEvent::Input(ref key) + if shortcut!(key == shortcuts[Shortcuts::GENERAL]["home_page"]) => + { + self.set_dirty(true); + self.movement = Some(PageMovement::Home); + return true; + } + UIEvent::Input(ref key) + if shortcut!(key == shortcuts[Shortcuts::GENERAL]["end_page"]) => + { + self.set_dirty(true); + self.movement = Some(PageMovement::End); + return true; + } + _ => {} } + //} false } fn is_dirty(&self) -> bool { - self.dirty || self.view.as_ref().map(|v| v.is_dirty()).unwrap_or(false) + self.dirty //|| self.view.as_ref().map(|v| + //|| v.is_dirty()).unwrap_or(false) } fn set_dirty(&mut self, value: bool) { - if let Some(p) = self.view.as_mut() { - p.set_dirty(value); - }; + //if let Some(p) = self.view.as_mut() { + // p.set_dirty(value); + //}; self.dirty = value; } @@ -955,11 +877,11 @@ impl Component for ContactList { .push_back(UIEvent::Action(Tab(TabAction::Kill(uuid)))); } fn shortcuts(&self, context: &Context) -> ShortcutMaps { - let mut map = self - .view - .as_ref() - .map(|p| p.shortcuts(context)) - .unwrap_or_default(); + let mut map = ShortcutMaps::default(); //self + //.view + //.as_ref() + //.map(|p| p.shortcuts(context)) + //.unwrap_or_default(); map.insert( Shortcuts::CONTACT_LIST, @@ -977,11 +899,10 @@ impl Component for ContactList { self.id } - fn can_quit_cleanly(&mut self, context: &Context) -> bool { - self.view - .as_mut() - .map(|p| p.can_quit_cleanly(context)) - .unwrap_or(true) + fn can_quit_cleanly(&mut self, _context: &Context) -> bool { + true + //self.view .as_mut() .map(|p| p.can_quit_cleanly(context)) + // .unwrap_or(true) } fn status(&self, context: &Context) -> String { diff --git a/meli/src/contacts/mod.rs b/meli/src/contacts/mod.rs index c063b1c5..79e33e84 100644 --- a/meli/src/contacts/mod.rs +++ b/meli/src/contacts/mod.rs @@ -21,4 +21,4 @@ pub mod list; -pub mod editor; +//pub mod editor; diff --git a/meli/src/jobs_view.rs b/meli/src/jobs_view.rs index b52767fc..50aaabc5 100644 --- a/meli/src/jobs_view.rs +++ b/meli/src/jobs_view.rs @@ -329,7 +329,7 @@ impl JobManager { if idx >= self.length { continue; //bounds check } - let new_area = nth_row_area(area, idx % rows); + let new_area = area.nth_row(idx % rows); self.data_columns .draw(grid, idx, self.cursor_pos, grid.bounds_iter(new_area)); let row_attr = if highlight { @@ -351,18 +351,14 @@ impl JobManager { /* Page_no has changed, so draw new page */ _ = self .data_columns - .recalc_widths((width!(area), height!(area)), top_idx); + .recalc_widths((area.width(), area.height()), top_idx); grid.clear_area(area, self.theme_default); /* copy table columns */ self.data_columns .draw(grid, top_idx, self.cursor_pos, grid.bounds_iter(area)); /* highlight cursor */ - - grid.change_theme( - nth_row_area(area, self.cursor_pos % rows), - self.highlight_theme, - ); + grid.change_theme(area.nth_row(self.cursor_pos % rows), self.highlight_theme); /* clear gap if available height is more than count of entries */ if top_idx + rows > self.length { @@ -386,45 +382,43 @@ impl Component for JobManager { if !self.initialized { self.initialize(context); } - { - // Draw column headers. - let area = nth_row_area(area, 0); - grid.clear_area(area, self.theme_default); - let mut x_offset = 0; - let (upper_left, bottom_right) = area; - for (i, (h, w)) in Self::HEADERS.iter().zip(self.min_width).enumerate() { + let area = area.nth_row(0); + // Draw column headers. + grid.clear_area(area, self.theme_default); + let mut x_offset = 0; + let (upper_left, bottom_right) = area; + for (i, (h, w)) in Self::HEADERS.iter().zip(self.min_width).enumerate() { + grid.write_string( + h, + self.theme_default.fg, + self.theme_default.bg, + self.theme_default.attrs | Attr::BOLD, + (pos_inc(upper_left, (x_offset, 0)), bottom_right), + None, + ); + if self.sort_col as usize == i { + use SortOrder::*; + let arrow = match (grid.ascii_drawing, self.sort_order) { + (true, Asc) => DataColumns::<5>::ARROW_UP_ASCII, + (true, Desc) => DataColumns::<5>::ARROW_DOWN_ASCII, + (false, Asc) => DataColumns::<5>::ARROW_UP, + (false, Desc) => DataColumns::<5>::ARROW_DOWN, + }; grid.write_string( - h, + arrow, self.theme_default.fg, self.theme_default.bg, - self.theme_default.attrs | Attr::BOLD, - (pos_inc(upper_left, (x_offset, 0)), bottom_right), + self.theme_default.attrs, + (pos_inc(upper_left, (x_offset + h.len(), 0)), bottom_right), None, ); - if self.sort_col as usize == i { - use SortOrder::*; - let arrow = match (grid.ascii_drawing, self.sort_order) { - (true, Asc) => DataColumns::<5>::ARROW_UP_ASCII, - (true, Desc) => DataColumns::<5>::ARROW_DOWN_ASCII, - (false, Asc) => DataColumns::<5>::ARROW_UP, - (false, Desc) => DataColumns::<5>::ARROW_DOWN, - }; - grid.write_string( - arrow, - self.theme_default.fg, - self.theme_default.bg, - self.theme_default.attrs, - (pos_inc(upper_left, (x_offset + h.len(), 0)), bottom_right), - None, - ); - } - x_offset += w + 2; } - context.dirty_areas.push_back(area); + x_offset += w + 2; } + context.dirty_areas.push_back(area); // Draw entry rows. - if let Some(area) = skip_rows(area, 1) { + if let Some(area) = area.skip_rows(1) { self.draw_list(grid, area, context); } self.dirty = false; diff --git a/meli/src/lib.rs b/meli/src/lib.rs index 5fb3cd1d..6c816669 100644 --- a/meli/src/lib.rs +++ b/meli/src/lib.rs @@ -85,11 +85,11 @@ pub use crate::mail::*; pub mod notifications; -pub mod mailbox_management; -pub use mailbox_management::*; +//pub mod mailbox_management; +//pub use mailbox_management::*; -pub mod jobs_view; -pub use jobs_view::*; +//pub mod jobs_view; +//pub use jobs_view::*; #[cfg(feature = "svgscreenshot")] pub mod svg; diff --git a/meli/src/mail/compose.rs b/meli/src/mail/compose.rs index a481a830..6d0cc5c9 100644 --- a/meli/src/mail/compose.rs +++ b/meli/src/mail/compose.rs @@ -42,8 +42,8 @@ use crate::{accounts::JobRequest, jobs::JoinHandle, terminal::embed::EmbedTermin #[cfg(feature = "gpgme")] pub mod gpg; -pub mod edit_attachments; -use edit_attachments::*; +//pub mod edit_attachments; +//use edit_attachments::*; pub mod hooks; @@ -87,6 +87,11 @@ impl std::ops::DerefMut for EmbedStatus { } } +#[derive(Debug)] +struct Embedded { + status: EmbedStatus, +} + #[derive(Debug)] pub struct Composer { reply_context: Option<(MailboxHash, EnvelopeHash)>, @@ -100,8 +105,8 @@ pub struct Composer { mode: ViewMode, - embed_area: Area, - embed: Option, + embedded: Option, + embed_dimensions: (usize, usize), #[cfg(feature = "gpgme")] gpg_state: gpg::GpgComposeState, dirty: bool, @@ -114,9 +119,9 @@ pub struct Composer { #[derive(Debug)] enum ViewMode { Discard(ComponentId, UIDialog), - EditAttachments { - widget: EditAttachments, - }, + //EditAttachments { + // widget: EditAttachments, + //}, Edit, Embed, SelectRecipients(UIDialog
), @@ -131,9 +136,9 @@ impl ViewMode { matches!(self, ViewMode::Edit) } - fn is_edit_attachments(&self) -> bool { - matches!(self, ViewMode::EditAttachments { .. }) - } + //fn is_edit_attachments(&self) -> bool { + // matches!(self, ViewMode::EditAttachments { .. }) + //} } impl std::fmt::Display for Composer { @@ -180,8 +185,8 @@ impl Composer { gpg_state: gpg::GpgComposeState::default(), dirty: true, has_changes: false, - embed_area: ((0, 0), (0, 0)), - embed: None, + embedded: None, + embed_dimensions: (80, 20), initialized: false, id: ComponentId::default(), } @@ -707,7 +712,7 @@ To: {} theme_default.bg }, theme_default.attrs, - (pos_inc(upper_left!(area), (0, 1)), bottom_right!(area)), + area.skip_rows(1), None, ); } else { @@ -720,7 +725,7 @@ To: {} theme_default.bg }, theme_default.attrs, - (pos_inc(upper_left!(area), (0, 1)), bottom_right!(area)), + area.skip_rows(1), None, ); } @@ -755,7 +760,7 @@ To: {} theme_default.bg }, theme_default.attrs, - (pos_inc(upper_left!(area), (0, 2)), bottom_right!(area)), + area.skip_rows(2), None, ); } else { @@ -768,7 +773,7 @@ To: {} theme_default.bg }, theme_default.attrs, - (pos_inc(upper_left!(area), (0, 2)), bottom_right!(area)), + area.skip_rows(2), None, ); } @@ -782,7 +787,7 @@ To: {} theme_default.bg }, theme_default.attrs, - (pos_inc(upper_left!(area), (0, 3)), bottom_right!(area)), + area.skip_rows(3), None, ); } else { @@ -795,40 +800,33 @@ To: {} theme_default.bg }, theme_default.attrs, - (pos_inc(upper_left!(area), (0, 3)), bottom_right!(area)), + area.skip_rows(3), None, ); for (i, a) in self.draft.attachments().iter().enumerate() { - if let Some(name) = a.content_type().name() { - grid.write_string( - &format!( + grid.write_string( + &if let Some(name) = a.content_type().name() { + format!( "[{}] \"{}\", {} {}", i, name, a.content_type(), melib::BytesDisplay(a.raw.len()) - ), - theme_default.fg, - theme_default.bg, - theme_default.attrs, - (pos_inc(upper_left!(area), (0, 4 + i)), bottom_right!(area)), - None, - ); - } else { - grid.write_string( - &format!( + ) + } else { + format!( "[{}] {} {}", i, a.content_type(), melib::BytesDisplay(a.raw.len()) - ), - theme_default.fg, - theme_default.bg, - theme_default.attrs, - (pos_inc(upper_left!(area), (0, 4 + i)), bottom_right!(area)), - None, - ); - } + ) + }, + theme_default.fg, + theme_default.bg, + theme_default.attrs, + area.skip_rows(4 + i), + None, + ); } } } @@ -859,12 +857,7 @@ To: {} impl Component for Composer { fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { - let upper_left = upper_left!(area); - let bottom_right = bottom_right!(area); - - let upper_left = set_y(upper_left, get_y(upper_left) + 1); - - if height!(area) < 4 { + if area.height() < 4 { return; } @@ -917,34 +910,25 @@ impl Component for Composer { }; */ - let header_area = ( - set_x(upper_left, mid + 1), - ( - get_x(bottom_right).saturating_sub(mid), - get_y(upper_left) + header_height, - ), - ); + let header_area = area + .take_rows(header_height) + .skip_cols(mid + 1) + .skip_cols_from_end(mid); let attachments_no = self.draft.attachments().len(); - let attachment_area = ( - ( - mid + 1, - get_y(bottom_right).saturating_sub(4 + attachments_no), - ), - pos_dec(bottom_right, (mid, 0)), - ); + let attachment_area = area + .skip_rows(header_height) + .skip_rows( + area.height() + .saturating_sub(header_area.height() + 4 + attachments_no), + ) + .skip_cols(mid + 1); - let body_area = ( - ( - get_x(upper_left!(header_area)), - get_y(bottom_right!(header_area)) + 1, - ), - ( - get_x(bottom_right!(header_area)), - get_y(upper_left!(attachment_area)).saturating_sub(1), - ), - ); + let body_area = area + .skip_rows(header_height) + .skip_rows_from_end(attachment_area.height()); - let (x, y) = grid.write_string( + grid.clear_area(area.nth_row(0), crate::conf::value(context, "highlight")); + grid.write_string( if self.reply_context.is_some() { "COMPOSING REPLY" } else { @@ -953,18 +937,15 @@ impl Component for Composer { crate::conf::value(context, "highlight").fg, crate::conf::value(context, "highlight").bg, crate::conf::value(context, "highlight").attrs, - ( - pos_dec(upper_left!(header_area), (0, 1)), - bottom_right!(header_area), - ), + area.nth_row(0), None, ); - grid.clear_area(((x, y), (set_y(bottom_right, y))), theme_default); + /* grid.change_theme( ( - set_x(pos_dec(upper_left!(header_area), (0, 1)), x), - set_y(bottom_right!(header_area), y), + set_x(pos_dec(header_area.upper_left(), (0, 1)), x), + set_y(header_area.bottom_right(), y), ), crate::conf::value(context, "highlight"), ); @@ -987,22 +968,20 @@ impl Component for Composer { ), theme_default, ); + */ /* Regardless of view mode, do the following */ self.form.draw(grid, header_area, context); - if let Some(ref mut embed_pty) = self.embed { - let embed_area = (upper_left!(header_area), bottom_right!(body_area)); + if let Some(ref mut embedded) = self.embedded { + let embed_pty = &mut embedded.status; + let embed_area = area; match embed_pty { EmbedStatus::Running(_, _) => { let mut guard = embed_pty.lock().unwrap(); grid.clear_area(embed_area, theme_default); - grid.copy_area( - guard.grid.buffer(), - embed_area, - ((0, 0), pos_dec(guard.grid.terminal_size, (1, 1))), - ); - guard.set_terminal_size((width!(embed_area), height!(embed_area))); + grid.copy_area(guard.grid.buffer(), embed_area, guard.grid.area()); + guard.set_terminal_size((embed_area.width(), embed_area.height())); context.dirty_areas.push_back(area); self.dirty = false; return; @@ -1010,11 +989,7 @@ impl Component for Composer { EmbedStatus::Stopped(_, _) => { let guard = embed_pty.lock().unwrap(); - grid.copy_area( - guard.grid.buffer(), - embed_area, - ((0, 0), pos_dec(guard.grid.terminal_size, (1, 1))), - ); + grid.copy_area(guard.grid.buffer(), embed_area, guard.grid.buffer().area()); grid.change_colors(embed_area, Color::Byte(8), theme_default.bg); let our_map: ShortcutMap = account_settings!(context[self.account_hash].shortcuts.composing) @@ -1033,19 +1008,7 @@ impl Component for Composer { stopped_message.len(), std::cmp::max(stopped_message_2.len(), STOPPED_MESSAGE_3.len()), ); - let inner_area = create_box( - grid, - ( - pos_inc(upper_left!(body_area), (1, 0)), - pos_inc( - upper_left!(body_area), - ( - std::cmp::min(max_len + 5, width!(body_area)), - std::cmp::min(5, height!(body_area)), - ), - ), - ), - ); + let inner_area = create_box(grid, area.center_inside((max_len + 5, 5))); grid.clear_area(inner_area, theme_default); for (i, l) in [ stopped_message.as_str(), @@ -1060,11 +1023,8 @@ impl Component for Composer { theme_default.fg, theme_default.bg, theme_default.attrs, - ( - pos_inc((0, i), upper_left!(inner_area)), - bottom_right!(inner_area), - ), - Some(get_x(upper_left!(inner_area))), + inner_area.skip_rows(i), + None, //Some(get_x(inner_area.upper_left())), ); } context.dirty_areas.push_back(area); @@ -1073,10 +1033,10 @@ impl Component for Composer { } } } else { - self.embed_area = (upper_left!(header_area), bottom_right!(body_area)); + self.embed_dimensions = (area.width(), area.height()); } - if self.pager.size().0 > width!(body_area) { + if self.pager.size().0 > body_area.width() { self.pager.set_initialised(false); } // Force clean pager area, because if body height is less than body_area it will @@ -1087,23 +1047,26 @@ impl Component for Composer { match self.cursor { Cursor::Headers => { + /* grid.change_theme( ( - pos_dec(upper_left!(body_area), (1, 0)), + pos_dec(body_area.upper_left(), (1, 0)), pos_dec( - set_y(upper_left!(body_area), get_y(bottom_right!(body_area))), + set_y(body_area.upper_left(), get_y(body_area.bottom_right())), (1, 0), ), ), theme_default, ); + */ } Cursor::Body => { + /* grid.change_theme( ( - pos_dec(upper_left!(body_area), (1, 0)), + pos_dec(body_area.upper_left(), (1, 0)), pos_dec( - set_y(upper_left!(body_area), get_y(bottom_right!(body_area))), + set_y(body_area.upper_left(), get_y(body_area.bottom_right())), (1, 0), ), ), @@ -1117,32 +1080,26 @@ impl Component for Composer { }, }, ); + */ } Cursor::Sign | Cursor::Encrypt | Cursor::Attachments => {} } + //if !self.mode.is_edit_attachments() { + self.draw_attachments(grid, attachment_area, context); + //} match self.mode { ViewMode::Edit | ViewMode::Embed => {} - ViewMode::EditAttachments { ref mut widget } => { - let inner_area = create_box( - grid, - (upper_left!(body_area), bottom_right!(attachment_area)), - ); - (EditAttachmentsRefMut { - inner: widget, - draft: &mut self.draft, - }) - .draw( - grid, - ( - pos_inc(upper_left!(inner_area), (1, 1)), - bottom_right!(inner_area), - ), - context, - ); - } + //ViewMode::EditAttachments { ref mut widget } => { + // let inner_area = create_box(grid, area); + // (EditAttachmentsRefMut { + // inner: widget, + // draft: &mut self.draft, + // }) + // .draw(grid, inner_area, context); + //} ViewMode::Send(ref mut s) => { - s.draw(grid, area, context); + s.draw(grid, body_area, context); } #[cfg(feature = "gpgme")] ViewMode::SelectEncryptKey( @@ -1152,25 +1109,22 @@ impl Component for Composer { keys: _, }, ) => { - widget.draw(grid, area, context); + widget.draw(grid, body_area, context); } #[cfg(feature = "gpgme")] ViewMode::SelectEncryptKey(_, _) => {} ViewMode::SelectRecipients(ref mut s) => { - s.draw(grid, area, context); + s.draw(grid, body_area, context); } ViewMode::Discard(_, ref mut s) => { /* Let user choose whether to quit with/without saving or cancel */ - s.draw(grid, area, context); + s.draw(grid, body_area, context); } ViewMode::WaitingForSendResult(ref mut s, _) => { /* Let user choose whether to wait for success or cancel */ - s.draw(grid, area, context); + s.draw(grid, body_area, context); } } - if !self.mode.is_edit_attachments() { - self.draw_attachments(grid, attachment_area, context); - } self.dirty = false; context.dirty_areas.push_back(area); } @@ -1239,23 +1193,22 @@ impl Component for Composer { return true; } } - (ViewMode::EditAttachments { ref mut widget }, _) => { - if (EditAttachmentsRefMut { - inner: widget, - draft: &mut self.draft, - }) - .process_event(event, context) - { - if matches!( - widget.buttons.result(), - Some(FormButtonActions::Cancel | FormButtonActions::Accept) - ) { - self.mode = ViewMode::Edit; - } - self.set_dirty(true); - return true; - } - } + //(ViewMode::EditAttachments { ref mut widget }, _) => { + // if (EditAttachmentsRefMut { + // inner: widget, + // draft: &mut self.draft, + // }) + // .process_event(event, context) + // { + // if matches!( + // widget.buttons.result(), + // Some(FormButtonActions::Cancel | FormButtonActions::Accept) + // ) { self.mode = ViewMode::Edit; + // } + // self.set_dirty(true); + // return true; + // } + //} (ViewMode::Send(ref selector), UIEvent::FinishedUIDialog(id, result)) if selector.id() == *id => { @@ -1598,10 +1551,23 @@ impl Component for Composer { return true; } UIEvent::EmbedInput((Key::Ctrl('z'), _)) => { - self.embed.as_ref().unwrap().lock().unwrap().stop(); - match self.embed.take() { - Some(EmbedStatus::Running(e, f)) | Some(EmbedStatus::Stopped(e, f)) => { - self.embed = Some(EmbedStatus::Stopped(e, f)); + self.embedded + .as_ref() + .unwrap() + .status + .lock() + .unwrap() + .stop(); + match self.embedded.take() { + Some(Embedded { + status: EmbedStatus::Running(e, f), + }) + | Some(Embedded { + status: EmbedStatus::Stopped(e, f), + }) => { + self.embedded = Some(Embedded { + status: EmbedStatus::Stopped(e, f), + }); } _ => {} } @@ -1611,13 +1577,13 @@ impl Component for Composer { self.set_dirty(true); } UIEvent::EmbedInput((ref k, ref b)) => { - if let Some(ref mut embed) = self.embed { - let mut embed_guard = embed.lock().unwrap(); + if let Some(ref mut embed) = self.embedded { + let mut embed_guard = embed.status.lock().unwrap(); if embed_guard.write_all(b).is_err() { match embed_guard.is_active() { Ok(WaitStatus::Exited(_, exit_code)) => { drop(embed_guard); - let embed = self.embed.take(); + let embedded = self.embedded.take(); if exit_code != 0 { context.replies.push_back(UIEvent::Notification( None, @@ -1629,7 +1595,10 @@ impl Component for Composer { melib::error::ErrorKind::External, )), )); - } else if let Some(EmbedStatus::Running(_, file)) = embed { + } else if let Some(Embedded { + status: EmbedStatus::Running(_, file), + }) = embedded + { self.update_from_file(file, context); } self.initialized = false; @@ -1643,10 +1612,16 @@ impl Component for Composer { Ok(WaitStatus::PtraceEvent(_, _, _)) | Ok(WaitStatus::PtraceSyscall(_)) => { drop(embed_guard); - match self.embed.take() { - Some(EmbedStatus::Running(e, f)) - | Some(EmbedStatus::Stopped(e, f)) => { - self.embed = Some(EmbedStatus::Stopped(e, f)); + match self.embedded.take() { + Some(Embedded { + status: EmbedStatus::Running(e, f), + }) + | Some(Embedded { + status: EmbedStatus::Stopped(e, f), + }) => { + self.embedded = Some(Embedded { + status: EmbedStatus::Stopped(e, f), + }); } _ => {} } @@ -1659,10 +1634,16 @@ impl Component for Composer { } Ok(WaitStatus::Stopped(_, _)) => { drop(embed_guard); - match self.embed.take() { - Some(EmbedStatus::Running(e, f)) - | Some(EmbedStatus::Stopped(e, f)) => { - self.embed = Some(EmbedStatus::Stopped(e, f)); + match self.embedded.take() { + Some(Embedded { + status: EmbedStatus::Running(e, f), + }) + | Some(Embedded { + status: EmbedStatus::Stopped(e, f), + }) => { + self.embedded = Some(Embedded { + status: EmbedStatus::Stopped(e, f), + }); } _ => {} } @@ -1691,7 +1672,7 @@ impl Component for Composer { )), )); self.initialized = false; - self.embed = None; + self.embedded = None; self.mode = ViewMode::Edit; context .replies @@ -1707,7 +1688,7 @@ impl Component for Composer { )); drop(embed_guard); self.initialized = false; - self.embed = None; + self.embedded = None; self.mode = ViewMode::Edit; context .replies @@ -1804,21 +1785,34 @@ impl Component for Composer { && self.cursor == Cursor::Attachments && shortcut!(key == shortcuts[Shortcuts::COMPOSING]["edit"]) => { - self.mode = ViewMode::EditAttachments { - widget: EditAttachments::new(Some(self.account_hash)), - }; + //self.mode = ViewMode::EditAttachments { + // widget: EditAttachments::new(Some(self.account_hash)), + //}; self.set_dirty(true); return true; } UIEvent::Input(ref key) - if self.embed.is_some() + if self.embedded.is_some() && shortcut!(key == shortcuts[Shortcuts::COMPOSING]["edit"]) => { - self.embed.as_ref().unwrap().lock().unwrap().wake_up(); - match self.embed.take() { - Some(EmbedStatus::Running(e, f)) | Some(EmbedStatus::Stopped(e, f)) => { - self.embed = Some(EmbedStatus::Running(e, f)); + self.embedded + .as_ref() + .unwrap() + .status + .lock() + .unwrap() + .wake_up(); + match self.embedded.take() { + Some(Embedded { + status: EmbedStatus::Running(e, f), + }) + | Some(Embedded { + status: EmbedStatus::Stopped(e, f), + }) => { + self.embedded = Some(Embedded { + status: EmbedStatus::Running(e, f), + }); } _ => {} } @@ -1830,11 +1824,16 @@ impl Component for Composer { return true; } UIEvent::Input(Key::Ctrl('c')) - if self.embed.is_some() && self.embed.as_ref().unwrap().is_stopped() => + if self.embedded.is_some() + && self.embedded.as_ref().unwrap().status.is_stopped() => { - match self.embed.take() { - Some(EmbedStatus::Running(embed, file)) - | Some(EmbedStatus::Stopped(embed, file)) => { + match self.embedded.take() { + Some(Embedded { + status: EmbedStatus::Running(embed, file), + }) + | Some(Embedded { + status: EmbedStatus::Stopped(embed, file), + }) => { let guard = embed.lock().unwrap(); guard.wake_up(); guard.terminate(); @@ -1908,18 +1907,26 @@ impl Component for Composer { if *account_settings!(context[self.account_hash].composing.embed) { match crate::terminal::embed::create_pty( - width!(self.embed_area), - height!(self.embed_area), + self.embed_dimensions.0, + self.embed_dimensions.1, [editor, f.path().display().to_string()].join(" "), ) { Ok(embed) => { - self.embed = Some(EmbedStatus::Running(embed, f)); + self.embedded = Some(Embedded { + status: EmbedStatus::Running(embed, f), + }); self.set_dirty(true); context .replies .push_back(UIEvent::ChangeMode(UIMode::Embed)); context.replies.push_back(UIEvent::Fork(ForkType::Embed( - self.embed.as_ref().unwrap().lock().unwrap().child_pid, + self.embedded + .as_ref() + .unwrap() + .status + .lock() + .unwrap() + .child_pid, ))); self.mode = ViewMode::Embed; } @@ -2199,13 +2206,13 @@ impl Component for Composer { fn is_dirty(&self) -> bool { match self.mode { ViewMode::Embed => true, - ViewMode::EditAttachments { ref widget } => { - widget.dirty - || widget.buttons.is_dirty() - || self.dirty - || self.pager.is_dirty() - || self.form.is_dirty() - } + //ViewMode::EditAttachments { ref widget } => { + // widget.dirty + // || widget.buttons.is_dirty() + // || self.dirty + // || self.pager.is_dirty() + // || self.form.is_dirty() + //} ViewMode::Edit => self.dirty || self.pager.is_dirty() || self.form.is_dirty(), ViewMode::Discard(_, ref widget) => { widget.is_dirty() || self.pager.is_dirty() || self.form.is_dirty() @@ -2230,13 +2237,32 @@ impl Component for Composer { self.dirty = value; self.pager.set_dirty(value); self.form.set_dirty(value); - if let ViewMode::EditAttachments { ref mut widget } = self.mode { - (EditAttachmentsRefMut { - inner: widget, - draft: &mut self.draft, - }) - .set_dirty(value); + match self.mode { + ViewMode::Discard(_, ref mut widget) => { + widget.set_dirty(value); + } + ViewMode::SelectRecipients(ref mut widget) => { + widget.set_dirty(value); + } + #[cfg(feature = "gpgme")] + ViewMode::SelectEncryptKey(_, ref mut widget) => { + widget.set_dirty(value); + } + ViewMode::Send(ref mut widget) => { + widget.set_dirty(value); + } + ViewMode::WaitingForSendResult(ref mut widget, _) => { + widget.set_dirty(value); + } + ViewMode::Edit | ViewMode::Embed => {} } + //if let ViewMode::EditAttachments { ref mut widget } = self.mode { + // (EditAttachmentsRefMut { + // inner: widget, + // draft: &mut self.draft, + // }) + // .set_dirty(value); + //} } fn kill(&mut self, uuid: ComponentId, context: &mut Context) { diff --git a/meli/src/mail/compose/edit_attachments.rs b/meli/src/mail/compose/edit_attachments.rs index 53b41d08..378695cb 100644 --- a/meli/src/mail/compose/edit_attachments.rs +++ b/meli/src/mail/compose/edit_attachments.rs @@ -164,7 +164,7 @@ impl Component for EditAttachmentsRefMut<'_, '_> { theme_default.fg, bg, theme_default.attrs, - (pos_inc(upper_left!(area), (0, 1 + i)), bottom_right!(area)), + (pos_inc(area.upper_left(), (0, 1 + i)), area.bottom_right()), None, ); } else { @@ -178,7 +178,7 @@ impl Component for EditAttachmentsRefMut<'_, '_> { theme_default.fg, bg, theme_default.attrs, - (pos_inc(upper_left!(area), (0, 1 + i)), bottom_right!(area)), + (pos_inc(area.upper_left(), (0, 1 + i)), area.bottom_right()), None, ); } @@ -187,8 +187,8 @@ impl Component for EditAttachmentsRefMut<'_, '_> { self.inner.buttons.draw( grid, ( - pos_inc(upper_left!(area), (0, 1 + self.draft.attachments().len())), - bottom_right!(area), + pos_inc(area.upper_left(), (0, 1 + self.draft.attachments().len())), + area.bottom_right(), ), context, ); diff --git a/meli/src/mail/compose/gpg.rs b/meli/src/mail/compose/gpg.rs index 589f9919..17127e57 100644 --- a/meli/src/mail/compose/gpg.rs +++ b/meli/src/mail/compose/gpg.rs @@ -86,7 +86,7 @@ impl Component for KeySelection { KeySelection::LoadingKeys { ref mut progress_spinner, .. - } => progress_spinner.draw(grid, center_area(area, (2, 2)), context), + } => progress_spinner.draw(grid, area.center_inside((2, 2)), context), KeySelection::Error { ref err, .. } => { let theme_default = crate::conf::value(context, "theme_default"); grid.write_string( @@ -94,7 +94,7 @@ impl Component for KeySelection { theme_default.fg, theme_default.bg, theme_default.attrs, - center_area(area, (15, 2)), + area.center_inside((15, 2)), Some(0), ); } diff --git a/meli/src/mail/listing.rs b/meli/src/mail/listing.rs index f8f7e336..9b6af4be 100644 --- a/meli/src/mail/listing.rs +++ b/meli/src/mail/listing.rs @@ -31,8 +31,7 @@ use std::{ use futures::future::try_join_all; use melib::{ - backends::EnvelopeHashBatch, mbox::MboxMetadata, utils::datetime, Address, FlagOp, - UnixTimestamp, + backends::EnvelopeHashBatch, mbox::MboxMetadata, utils::datetime, FlagOp, UnixTimestamp, }; use smallvec::SmallVec; @@ -40,7 +39,6 @@ use super::*; use crate::{ accounts::{JobRequest, MailboxStatus}, components::ExtendShortcutsMaps, - types::segment_tree::SegmentTree, }; // [ref:TODO]: emoji_text_presentation_selector should be printed along with the chars @@ -232,14 +230,14 @@ impl RowsState { } } -mod conversations; -pub use self::conversations::*; +//mod conversations; +//pub use self::conversations::*; -mod compact; -pub use self::compact::*; +//mod compact; +//pub use self::compact::*; -mod thread; -pub use self::thread::*; +//mod thread; +//pub use self::thread::*; mod plain; pub use self::plain::*; @@ -284,7 +282,7 @@ pub struct ColorCache { pub odd_highlighted_selected: ThemeAttribute, pub tag_default: ThemeAttribute, - /* Conversations */ + // Conversations pub subject: ThemeAttribute, pub from: ThemeAttribute, pub date: ThemeAttribute, @@ -885,11 +883,11 @@ pub trait ListingTrait: Component { #[derive(Debug)] pub enum ListingComponent { - Plain(Box), - Threaded(Box), - Compact(Box), - Conversations(Box), + //Compact(Box), + //Conversations(Box), Offline(Box), + Plain(Box), + //Threaded(Box), } use crate::ListingComponent::*; @@ -898,11 +896,11 @@ impl std::ops::Deref for ListingComponent { fn deref(&self) -> &Self::Target { match &self { - Compact(ref l) => l.as_ref(), - Plain(ref l) => l.as_ref(), - Threaded(ref l) => l.as_ref(), - Conversations(ref l) => l.as_ref(), + //Compact(ref l) => l.as_ref(), + //Conversations(ref l) => l.as_ref(), Offline(ref l) => l.as_ref(), + Plain(ref l) => l.as_ref(), + //Threaded(ref l) => l.as_ref(), } } } @@ -910,11 +908,11 @@ impl std::ops::Deref for ListingComponent { impl std::ops::DerefMut for ListingComponent { fn deref_mut(&mut self) -> &mut (dyn MailListingTrait + 'static) { match self { - Compact(l) => l.as_mut(), - Plain(l) => l.as_mut(), - Threaded(l) => l.as_mut(), - Conversations(l) => l.as_mut(), + //Compact(l) => l.as_mut(), + //Conversations(l) => l.as_mut(), Offline(l) => l.as_mut(), + Plain(l) => l.as_mut(), + //Threaded(l) => l.as_mut(), } } } @@ -922,11 +920,11 @@ impl std::ops::DerefMut for ListingComponent { impl ListingComponent { fn id(&self) -> ComponentId { match self { - Compact(l) => l.as_component().id(), - Plain(l) => l.as_component().id(), - Threaded(l) => l.as_component().id(), - Conversations(l) => l.as_component().id(), + //Compact(l) => l.as_component().id(), + //Conversations(l) => l.as_component().id(), Offline(l) => l.as_component().id(), + Plain(l) => l.as_component().id(), + //Threaded(l) => l.as_component().id(), } } } @@ -976,7 +974,7 @@ pub struct Listing { dirty: bool, cursor_pos: CursorPos, menu_cursor_pos: CursorPos, - menu_content: CellBuffer, + menu: Screen, menu_scrollbar_show_timer: crate::jobs::Timer, show_menu_scrollbar: ShowMenuScrollbar, startup_checks_rate: RateLimit, @@ -999,11 +997,11 @@ pub struct Listing { impl std::fmt::Display for Listing { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self.component { - Compact(ref l) => write!(f, "{}", l), - Plain(ref l) => write!(f, "{}", l), - Threaded(ref l) => write!(f, "{}", l), - Conversations(ref l) => write!(f, "{}", l), + //Compact(ref l) => write!(f, "{}", l), + //Conversations(ref l) => write!(f, "{}", l), Offline(ref l) => write!(f, "{}", l), + Plain(ref l) => write!(f, "{}", l), + //Threaded(ref l) => write!(f, "{}", l), } } } @@ -1013,12 +1011,7 @@ impl Component for Listing { if !self.is_dirty() { return; } - if !is_valid_area!(area) { - return; - } - let upper_left = upper_left!(area); - let bottom_right = bottom_right!(area); - let total_cols = get_x(bottom_right) - get_x(upper_left); + let total_cols = area.width(); let right_component_width = if self.is_menu_visible() { if self.focus == ListingFocus::Menu { @@ -1043,18 +1036,19 @@ impl Component for Listing { } else { total_cols }; - let mid = get_x(bottom_right) - right_component_width; - if self.dirty && mid != get_x(upper_left) { - for i in get_y(upper_left)..=get_y(bottom_right) { - grid[(mid, i)] - .set_ch(self.sidebar_divider) - .set_fg(self.sidebar_divider_theme.fg) - .set_bg(self.sidebar_divider_theme.bg) - .set_attrs(self.sidebar_divider_theme.attrs); + let mid = area.width().saturating_sub(right_component_width); + if self.dirty && mid != 0 { + let divider_area = area.nth_col(mid); + for row in grid.bounds_iter(divider_area) { + for c in row { + grid[c] + .set_ch(self.sidebar_divider) + .set_fg(self.sidebar_divider_theme.fg) + .set_bg(self.sidebar_divider_theme.bg) + .set_attrs(self.sidebar_divider_theme.attrs); + } } - context - .dirty_areas - .push_back(((mid, get_y(upper_left)), (mid, get_y(bottom_right)))); + context.dirty_areas.push_back(divider_area); } let account_hash = self.accounts[self.cursor_pos.account].hash; @@ -1065,6 +1059,8 @@ impl Component for Listing { self.component.unrealize(context); self.component = Offline(OfflineListing::new((account_hash, MailboxHash::default()))); + self.component + .process_event(&mut UIEvent::VisibilityChange(true), context); self.component.realize(self.id().into(), context); } @@ -1080,23 +1076,21 @@ impl Component for Listing { } else if right_component_width == 0 { self.draw_menu(grid, area, context); } else { - self.draw_menu( - grid, - (upper_left, (mid.saturating_sub(1), get_y(bottom_right))), - context, - ); + self.draw_menu(grid, area.take_cols(mid), context); if context.is_online(account_hash).is_err() && !matches!(self.component, ListingComponent::Offline(_)) { self.component.unrealize(context); self.component = Offline(OfflineListing::new((account_hash, MailboxHash::default()))); + self.component + .process_event(&mut UIEvent::VisibilityChange(true), context); self.component.realize(self.id().into(), context); } if let Some(s) = self.status.as_mut() { - s.draw(grid, (set_x(upper_left, mid + 1), bottom_right), context); + s.draw(grid, area.skip_cols(mid + 1), context); } else { - let area = (set_x(upper_left, mid + 1), bottom_right); + let area = area.skip_cols(mid + 1); self.component.draw(grid, area, context); if self.component.unfocused() { self.view @@ -1115,14 +1109,14 @@ impl Component for Listing { self.sidebar_divider = *account_settings!(context[account_hash].listing.sidebar_divider); self.sidebar_divider_theme = conf::value(context, "mail.sidebar_divider"); - self.menu_content = CellBuffer::new_with_context(0, 0, None, context); + self.menu.grid_mut().empty(); self.set_dirty(true); } UIEvent::Timer(n) if *n == self.menu_scrollbar_show_timer.id() => { if self.show_menu_scrollbar == ShowMenuScrollbar::True { self.show_menu_scrollbar = ShowMenuScrollbar::False; self.set_dirty(true); - self.menu_content.empty(); + self.menu.grid_mut().empty(); } return true; } @@ -1181,7 +1175,7 @@ impl Component for Listing { }, }) .collect::<_>(); - self.menu_content.empty(); + self.menu.grid_mut().empty(); context .replies .push_back(UIEvent::StatusEvent(StatusEvent::UpdateStatus(match msg { @@ -1196,7 +1190,7 @@ impl Component for Listing { .accounts .get_index_of(account_hash) .expect("Invalid account_hash in UIEventMailbox{Delete,Create}"); - self.menu_content.empty(); + self.menu.grid_mut().empty(); let previous_collapsed_mailboxes: BTreeSet = self.accounts [account_index] .entries @@ -1245,6 +1239,8 @@ impl Component for Listing { self.accounts[self.cursor_pos.account].entries[fallback].mailbox_hash, )); self.component.refresh_mailbox(context, true); + self.component + .process_event(&mut UIEvent::VisibilityChange(true), context); } context .replies @@ -1271,7 +1267,9 @@ impl Component for Listing { .process_event(&mut UIEvent::VisibilityChange(false), context); self.component .set_coordinates((account_hash, *mailbox_hash)); - self.menu_content.empty(); + self.component + .process_event(&mut UIEvent::VisibilityChange(true), context); + self.menu.grid_mut().empty(); self.set_dirty(true); } return true; @@ -1292,9 +1290,10 @@ impl Component for Listing { // Need to clear gap between sidebar and listing component, if any. self.dirty = true; } + self.set_dirty(matches!(new_value, Focus::EntryFullscreen)); } Some(ListingMessage::UpdateView) => { - log::trace!("UpdateView"); + self.view.set_dirty(true); } Some(ListingMessage::OpenEntryUnderCursor { env_hash, @@ -1844,7 +1843,7 @@ impl Component for Listing { { target.collapsed = !(target.collapsed); self.dirty = true; - self.menu_content.empty(); + self.menu.grid_mut().empty(); context .replies .push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate( @@ -1932,7 +1931,7 @@ impl Component for Listing { } else if shortcut!(k == shortcuts[Shortcuts::LISTING]["scroll_down"]) { while amount > 0 { match self.menu_cursor_pos { - /* If current account has mailboxes, go to first mailbox */ + // If current account has mailboxes, go to first mailbox CursorPos { ref account, ref mut menu, @@ -1941,7 +1940,7 @@ impl Component for Listing { { *menu = MenuEntryCursor::Mailbox(0); } - /* If current account has no mailboxes, go to next account */ + // If current account has no mailboxes, go to next account CursorPos { ref mut account, ref mut menu, @@ -1951,8 +1950,8 @@ impl Component for Listing { *account += 1; *menu = MenuEntryCursor::Status; } - /* If current account has no mailboxes and there is no next - * account, return true */ + // If current account has no mailboxes and there is no next account, + // return true CursorPos { menu: MenuEntryCursor::Status, .. @@ -1985,7 +1984,7 @@ impl Component for Listing { self.menu_scrollbar_show_timer.rearm(); self.show_menu_scrollbar = ShowMenuScrollbar::True; } - self.menu_content.empty(); + self.menu.grid_mut().empty(); self.set_dirty(true); return true; } @@ -2046,7 +2045,7 @@ impl Component for Listing { self.menu_scrollbar_show_timer.rearm(); self.show_menu_scrollbar = ShowMenuScrollbar::True; } - self.menu_content.empty(); + self.menu.grid_mut().empty(); return true; } UIEvent::Input(ref k) @@ -2107,7 +2106,7 @@ impl Component for Listing { self.menu_scrollbar_show_timer.rearm(); self.show_menu_scrollbar = ShowMenuScrollbar::True; } - self.menu_content.empty(); + self.menu.grid_mut().empty(); self.set_dirty(true); return true; @@ -2133,7 +2132,7 @@ impl Component for Listing { self.menu_scrollbar_show_timer.rearm(); self.show_menu_scrollbar = ShowMenuScrollbar::True; } - self.menu_content.empty(); + self.menu.grid_mut().empty(); self.set_dirty(true); return true; } @@ -2175,7 +2174,7 @@ impl Component for Listing { self.menu_scrollbar_show_timer.rearm(); self.show_menu_scrollbar = ShowMenuScrollbar::True; } - self.menu_content.empty(); + self.menu.grid_mut().empty(); self.set_dirty(true); return true; } @@ -2192,18 +2191,18 @@ impl Component for Listing { return true; } UIEvent::Action(Action::Tab(ManageMailboxes)) => { - let account_pos = self.cursor_pos.account; - let mgr = MailboxManager::new(context, account_pos); - context - .replies - .push_back(UIEvent::Action(Tab(New(Some(Box::new(mgr)))))); + //let account_pos = self.cursor_pos.account; + //let mgr = MailboxManager::new(context, account_pos); + //context + // .replies + // .push_back(UIEvent::Action(Tab(New(Some(Box::new(mgr)))))); return true; } UIEvent::Action(Action::Tab(ManageJobs)) => { - let mgr = JobManager::new(context); - context - .replies - .push_back(UIEvent::Action(Tab(New(Some(Box::new(mgr)))))); + //let mgr = JobManager::new(context); + //context + // .replies + // .push_back(UIEvent::Action(Tab(New(Some(Box::new(mgr)))))); return true; } UIEvent::Action(Action::Compose(ComposeAction::Mailto(ref mailto))) => { @@ -2221,8 +2220,8 @@ impl Component for Listing { | UIEvent::EnvelopeRename(_, _) | UIEvent::EnvelopeRemove(_, _) => { self.dirty = true; - /* clear menu to force redraw */ - self.menu_content.empty(); + // clear menu to force redraw + self.menu.grid_mut().empty(); context .replies .push_back(UIEvent::StatusEvent(StatusEvent::UpdateStatus( @@ -2355,11 +2354,11 @@ impl Component for Listing { ret.insert( self.component.id(), match &self.component { - Compact(l) => l.as_component(), - Plain(l) => l.as_component(), - Threaded(l) => l.as_component(), - Conversations(l) => l.as_component(), + //Compact(l) => l.as_component(), + //Conversations(l) => l.as_component(), Offline(l) => l.as_component(), + Plain(l) => l.as_component(), + //Threaded(l) => l.as_component(), }, ); @@ -2371,11 +2370,11 @@ impl Component for Listing { ret.insert( self.component.id(), match &mut self.component { - Compact(l) => l.as_component_mut(), - Plain(l) => l.as_component_mut(), - Threaded(l) => l.as_component_mut(), - Conversations(l) => l.as_component_mut(), + //Compact(l) => l.as_component_mut(), + //Conversations(l) => l.as_component_mut(), Offline(l) => l.as_component_mut(), + Plain(l) => l.as_component_mut(), + //Threaded(l) => l.as_component_mut(), }, ); @@ -2430,7 +2429,7 @@ impl Listing { account: 0, menu: MenuEntryCursor::Mailbox(0), }, - menu_content: CellBuffer::new_with_context(0, 0, None, context), + menu: Screen::::new(), menu_scrollbar_show_timer: context.main_loop_handler.job_executor.clone().create_timer( std::time::Duration::from_secs(0), std::time::Duration::from_millis(1200), @@ -2469,27 +2468,23 @@ impl Listing { .iter() .map(|entry| entry.entries.len() + 1) .sum::(); - let min_width: usize = 2 * width!(area); - let (width, height) = self.menu_content.size(); + let min_width: usize = 2 * area.width(); + let (width, height) = self.menu.grid().size(); let cursor = match self.focus { ListingFocus::Mailbox => self.cursor_pos, ListingFocus::Menu => self.menu_cursor_pos, }; if min_width > width || height < total_height || self.dirty { - let _ = self.menu_content.resize(min_width * 2, total_height, None); - let bottom_right = pos_dec(self.menu_content.size(), (1, 1)); + let _ = self.menu.resize(min_width * 2, total_height); let mut y = 0; for a in 0..self.accounts.len() { - if y > get_y(bottom_right) { - break; - } - y += self.print_account(((0, y), bottom_right), a, context); + let menu_area = self.menu.area().skip_rows(y); + y += self.print_account(menu_area, a, context); y += 3; } } - let rows = height!(area); - let (width, height) = self.menu_content.size(); + let rows = area.height(); const SCROLLING_CONTEXT: usize = 3; let y_offset = (cursor.account) + self @@ -2510,15 +2505,12 @@ impl Listing { }; grid.copy_area( - &self.menu_content, + self.menu.grid(), area, - ( - ( - 0, - std::cmp::min((height - 1).saturating_sub(rows), skip_offset), - ), - (width - 1, std::cmp::min(skip_offset + rows, height - 1)), - ), + self.menu + .area() + .skip_rows(skip_offset.min((self.menu.area().height() - 1).saturating_sub(rows))) + .take_rows((skip_offset + rows).min(self.menu.area().height() - 1)), ); if self.show_menu_scrollbar == ShowMenuScrollbar::True && total_height > rows { if self.focus == ListingFocus::Menu { @@ -2537,16 +2529,13 @@ impl Listing { } ScrollBar::default().set_show_arrows(true).draw( grid, - ( - pos_inc(upper_left!(area), (width!(area).saturating_sub(1), 0)), - bottom_right!(area), - ), + area.nth_col(area.width().saturating_sub(1)), context, - /* position */ + // position skip_offset, - /* visible_rows */ + // visible_rows rows, - /* length */ + // length total_height, ); } else if total_height < rows { @@ -2560,12 +2549,9 @@ impl Listing { context.dirty_areas.push_back(area); } - /* - * Print a single account in the menu area. - */ - fn print_account(&mut self, area: Area, aidx: usize, context: &mut Context) -> usize { - debug_assert!(is_valid_area!(area)); - + /// Print a single account in the menu area. + fn print_account(&mut self, mut area: Area, aidx: usize, context: &mut Context) -> usize { + let account_y = self.menu.area().height() - area.height(); #[derive(Copy, Debug, Clone)] struct Line { collapsed: bool, @@ -2584,9 +2570,6 @@ impl Listing { .map(|(&hash, entry)| (hash, entry.ref_mailbox.clone())) .collect(); - let upper_left = upper_left!(area); - let bottom_right = bottom_right!(area); - let cursor = match self.focus { ListingFocus::Mailbox => self.cursor_pos, ListingFocus::Menu => self.menu_cursor_pos, @@ -2652,8 +2635,8 @@ impl Listing { crate::conf::value(context, "mail.sidebar_account_name") }; - /* Print account name first */ - self.menu_content.write_string( + // Print account name first + self.menu.grid_mut().write_string( &self.accounts[aidx].name, account_attrs.fg, account_attrs.bg, @@ -2661,18 +2644,20 @@ impl Listing { area, None, ); + area = self.menu.area().skip_rows(account_y); if lines.is_empty() { - self.menu_content.write_string( + self.menu.grid_mut().write_string( "offline", crate::conf::value(context, "error_message").fg, account_attrs.bg, account_attrs.attrs, - (pos_inc(upper_left, (0, 1)), bottom_right), + area.skip_rows(1), None, ); return 0; } + area = self.menu.area().skip_rows(account_y); let lines_len = lines.len(); let mut idx = 0; @@ -2683,7 +2668,7 @@ impl Listing { // are not visible. let mut skip: Option = None; let mut skipped_counter: usize = 0; - 'grid_loop: for y in get_y(upper_left) + 1..get_y(bottom_right) { + 'grid_loop: for y in 0..area.height() { if idx == lines_len { break; } @@ -2751,14 +2736,13 @@ impl Listing { ) }; - /* Calculate how many columns the mailbox index tags should occupy with right - * alignment, eg. - * 1 - * 2 - * ... - * 9 - * 10 - */ + // Calculate how many columns the mailbox index tags should occupy with right + // alignment, eg. + // 1 + // 2 + // ... + // 9 + // 10 let total_mailbox_no_digits = { let mut len = lines_len; let mut ctr = 1; @@ -2804,7 +2788,7 @@ impl Listing { .map(|s| s.as_str()) .unwrap_or(" "); - let (x, _) = self.menu_content.write_string( + let (x, _) = self.menu.grid_mut().write_string( &if *account_settings!( context[self.accounts[aidx].hash] .listing @@ -2822,9 +2806,10 @@ impl Listing { index_att.fg, index_att.bg, index_att.attrs, - (set_y(upper_left, y), bottom_right), + area.nth_row(y + 1), None, ); + area = self.menu.area().skip_rows(account_y); { branches.clear(); branches.push_str(no_sibling_str); @@ -2846,24 +2831,37 @@ impl Listing { } } } - let (x, _) = self.menu_content.write_string( - &branches, - att.fg, - att.bg, - att.attrs, - ((x, y), bottom_right), - None, - ); - let (x, _) = self.menu_content.write_string( - context.accounts[self.accounts[aidx].index].mailbox_entries[&l.mailbox_idx].name(), - att.fg, - att.bg, - att.attrs, - ((x, y), bottom_right), - None, - ); + let x = self + .menu + .grid_mut() + .write_string( + &branches, + att.fg, + att.bg, + att.attrs, + area.nth_row(y + 1).skip_cols(x), + None, + ) + .0 + + x; + area = self.menu.area().skip_rows(account_y); + let x = self + .menu + .grid_mut() + .write_string( + context.accounts[self.accounts[aidx].index].mailbox_entries[&l.mailbox_idx] + .name(), + att.fg, + att.bg, + att.attrs, + area.nth_row(y + 1).skip_cols(x), + None, + ) + .0 + + x; + area = self.menu.area().skip_rows(account_y); - /* Unread message count */ + // Unread message count let count_string = match (l.count, l.collapsed_count) { (None, None) => " ...".to_string(), (Some(0), None) => String::new(), @@ -2875,28 +2873,28 @@ impl Listing { (None, Some(coll)) => format!(" ({}) v", coll), }; - let (x, _) = self.menu_content.write_string( - &count_string, - unread_count_att.fg, - unread_count_att.bg, - unread_count_att.attrs - | if l.count.unwrap_or(0) > 0 { - Attr::BOLD - } else { - Attr::DEFAULT - }, - ( - ( - /* Hide part of mailbox name if need be to fit the message count */ - std::cmp::min(x, get_x(bottom_right).saturating_sub(count_string.len())), - y, - ), - bottom_right, - ), - None, - ); - for c in self.menu_content.row_iter(x..(get_x(bottom_right) + 1), y) { - self.menu_content[c] + let x = self + .menu + .grid_mut() + .write_string( + &count_string, + unread_count_att.fg, + unread_count_att.bg, + unread_count_att.attrs + | if l.count.unwrap_or(0) > 0 { + Attr::BOLD + } else { + Attr::DEFAULT + }, + area.nth_row(y + 1) + .skip_cols(x.min(area.width().saturating_sub(count_string.len()))), + None, + ) + .0 + + x.min(area.width().saturating_sub(count_string.len())); + area = self.menu.area().skip_rows(account_y); + for c in self.menu.grid_mut().row_iter(area, x..area.width(), y + 1) { + self.menu.grid_mut()[c] .set_fg(att.fg) .set_bg(att.bg) .set_attrs(att.attrs); @@ -2958,7 +2956,6 @@ impl Listing { self.component .set_coordinates((account_hash, *mailbox_hash)); self.component.refresh_mailbox(context, true); - /* Check if per-mailbox configuration overrides general configuration */ let index_style = mailbox_settings!(context[account_hash][mailbox_hash].listing.index_style); @@ -2969,6 +2966,8 @@ impl Listing { Offline(OfflineListing::new((account_hash, MailboxHash::default()))); self.component.realize(self.id().into(), context); } + self.component + .process_event(&mut UIEvent::VisibilityChange(true), context); self.status = None; context .replies @@ -2983,8 +2982,8 @@ impl Listing { self.sidebar_divider = *account_settings!(context[account_hash].listing.sidebar_divider); self.set_dirty(true); self.menu_cursor_pos = self.cursor_pos; - /* clear menu to force redraw */ - self.menu_content.empty(); + // clear menu to force redraw + self.menu.grid_mut().empty(); if *account_settings!(context[account_hash].listing.show_menu_scrollbar) { self.show_menu_scrollbar = ShowMenuScrollbar::True; self.menu_scrollbar_show_timer.rearm(); @@ -2995,7 +2994,7 @@ impl Listing { fn open_status(&mut self, account_idx: usize, context: &mut Context) { self.status = Some(AccountStatus::new(account_idx, self.theme_default)); - self.menu_content.empty(); + self.menu.grid_mut().empty(); context .replies .push_back(UIEvent::StatusEvent(StatusEvent::UpdateStatus( @@ -3007,51 +3006,61 @@ impl Listing { !matches!(self.component.focus(), Focus::EntryFullscreen) && self.menu_visibility } - fn set_style(&mut self, new_style: IndexStyle, context: &mut Context) { - let old = match new_style { - IndexStyle::Plain => { - if matches!(self.component, Plain(_)) { - return; - } - let coordinates = self.component.coordinates(); - std::mem::replace( - &mut self.component, - Plain(PlainListing::new(self.id, coordinates)), - ) - } - IndexStyle::Threaded => { - if matches!(self.component, Threaded(_)) { - return; - } - let coordinates = self.component.coordinates(); - std::mem::replace( - &mut self.component, - Threaded(ThreadListing::new(self.id, coordinates, context)), - ) - } - IndexStyle::Compact => { - if matches!(self.component, Compact(_)) { - return; - } - let coordinates = self.component.coordinates(); - std::mem::replace( - &mut self.component, - Compact(CompactListing::new(self.id, coordinates)), - ) - } - IndexStyle::Conversations => { - if matches!(self.component, Conversations(_)) { - return; - } - let coordinates = self.component.coordinates(); - std::mem::replace( - &mut self.component, - Conversations(ConversationsListing::new(self.id, coordinates)), - ) - } - }; - old.unrealize(context); - self.component.realize(self.id.into(), context); + fn set_style(&mut self, _new_style: IndexStyle, context: &mut Context) { + if matches!(self.component, Plain(_)) { + return; + } + let coordinates = self.component.coordinates(); + self.component = Plain(PlainListing::new(self.id, coordinates)); + self.component + .process_event(&mut UIEvent::VisibilityChange(true), context); + //let old = match new_style { + // IndexStyle::Plain => { + // if matches!(self.component, Plain(_)) { + // return; + // } + // let coordinates = self.component.coordinates(); + // std::mem::replace( + // &mut self.component, + // Plain(PlainListing::new(self.id, coordinates)), + // ) + // } + // IndexStyle::Threaded => { + // return; + // //if matches!(self.component, Threaded(_)) { + // // return; + // //} + // //let coordinates = self.component.coordinates(); + // //std::mem::replace( + // // &mut self.component, + // // Threaded(ThreadListing::new(self.id, coordinates, + // context)), //) + // } + // IndexStyle::Compact => { + // return; + // //if matches!(self.component, Compact(_)) { + // // return; + // //} + // //let coordinates = self.component.coordinates(); + // //std::mem::replace( + // // &mut self.component, + // // Compact(CompactListing::new(self.id, coordinates)), + // //) + // } + // IndexStyle::Conversations => { + // return; + // //if matches!(self.component, Conversations(_)) { + // // return; + // //} + // //let coordinates = self.component.coordinates(); + // //std::mem::replace( + // // &mut self.component, + // // Conversations(ConversationsListing::new(self.id, + // coordinates)), //) + // } + //}; + //old.unrealize(context); + //self.component.realize(self.id.into(), context); } } diff --git a/meli/src/mail/listing/compact.rs b/meli/src/mail/listing/compact.rs index 1d9fb6b5..8da65604 100644 --- a/meli/src/mail/listing/compact.rs +++ b/meli/src/mail/listing/compact.rs @@ -286,17 +286,19 @@ impl MailListingTrait for CompactListing { Err(_) => { let message: String = context.accounts[&self.cursor_pos.0][&self.cursor_pos.1].status(); - self.data_columns.columns[0] = - CellBuffer::new_with_context(message.len(), 1, None, context); + self.data_columns.columns[0].resize_with_context(message.len(), 1, context); self.length = 0; - self.data_columns.columns[0].write_string( - message.as_str(), - self.color_cache.theme_default.fg, - self.color_cache.theme_default.bg, - self.color_cache.theme_default.attrs, - ((0, 0), (message.len() - 1, 0)), - None, - ); + { + let area = self.data_columns.columns[0].area(); + self.data_columns.columns[0].grid_mut().write_string( + message.as_str(), + self.color_cache.theme_default.fg, + self.color_cache.theme_default.bg, + self.color_cache.theme_default.attrs, + area, + None, + ) + }; return; } } @@ -550,21 +552,17 @@ impl MailListingTrait for CompactListing { .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); + self.data_columns.columns[0].resize_with_context(min_width.0, self.rows.len(), context); self.data_columns.segment_tree[0] = row_widths.0.into(); /* date column */ - self.data_columns.columns[1] = - CellBuffer::new_with_context(min_width.1, self.rows.len(), None, context); + self.data_columns.columns[1].resize_with_context(min_width.1, self.rows.len(), context); self.data_columns.segment_tree[1] = row_widths.1.into(); /* from column */ - self.data_columns.columns[2] = - CellBuffer::new_with_context(min_width.2, self.rows.len(), None, context); + self.data_columns.columns[2].resize_with_context(min_width.2, self.rows.len(), context); self.data_columns.segment_tree[2] = row_widths.2.into(); /* subject column */ - self.data_columns.columns[3] = - CellBuffer::new_with_context(min_width.3, self.rows.len(), None, context); + self.data_columns.columns[3].resize_with_context(min_width.3, self.rows.len(), context); self.data_columns.segment_tree[3] = row_widths.3.into(); self.rows_drawn = SegmentTree::from( @@ -580,16 +578,18 @@ impl MailListingTrait for CompactListing { ); if self.length == 0 && self.filter_term.is_empty() { let message: String = account[&self.cursor_pos.1].status(); - self.data_columns.columns[0] = - CellBuffer::new_with_context(message.len(), self.length + 1, None, context); - self.data_columns.columns[0].write_string( - &message, - self.color_cache.theme_default.fg, - self.color_cache.theme_default.bg, - self.color_cache.theme_default.attrs, - ((0, 0), (message.len() - 1, 0)), - None, - ); + self.data_columns.columns[0].resize_with_context(message.len(), 1, context); + { + let area = self.data_columns.columns[0].area(); + self.data_columns.columns[0].grid_mut().write_string( + &message, + self.color_cache.theme_default.fg, + self.color_cache.theme_default.bg, + self.color_cache.theme_default.attrs, + area, + None, + ) + }; } } } @@ -663,17 +663,12 @@ impl ListingTrait for CompactListing { self.cursor_pos.2 == idx, self.rows.is_thread_selected(thread_hash) ); - let (upper_left, bottom_right) = area; - let x = get_x(upper_left) - + self.data_columns.widths[0] + let x = self.data_columns.widths[0] + self.data_columns.widths[1] + self.data_columns.widths[2] + 3 * 2; - for c in grid.row_iter( - get_x(upper_left)..(get_x(bottom_right) + 1), - get_y(upper_left), - ) { + for c in grid.row_iter(area, 0..area.width(), 0) { grid[c] .set_fg(row_attr.fg) .set_bg(row_attr.bg) @@ -681,20 +676,11 @@ impl ListingTrait for CompactListing { } grid.copy_area( - &self.data_columns.columns[3], - (set_x(upper_left, x), bottom_right), - ( - (0, idx), - pos_dec( - ( - self.data_columns.widths[3], - self.data_columns.columns[3].size().1, - ), - (1, 1), - ), - ), + self.data_columns.columns[3].grid(), + area.skip_cols(x), + self.data_columns.columns[3].area().nth_row(idx), ); - for c in grid.row_iter(x..(get_x(bottom_right) + 1), get_y(upper_left)) { + for c in grid.row_iter(area, x..area.width(), 0) { grid[c].set_bg(row_attr.bg).set_attrs(row_attr.attrs); } } @@ -705,20 +691,20 @@ impl ListingTrait for CompactListing { { self.refresh_mailbox(context, false); } - let upper_left = upper_left!(area); - let bottom_right = bottom_right!(area); + let upper_left = area.upper_left(); + let bottom_right = area.bottom_right(); if self.length == 0 { grid.clear_area(area, self.color_cache.theme_default); grid.copy_area( - &self.data_columns.columns[0], + self.data_columns.columns[0].grid(), area, - ((0, 0), pos_dec(self.data_columns.columns[0].size(), (1, 1))), + self.data_columns.columns[0].area(), ); context.dirty_areas.push_back(area); return; } - let rows = get_y(bottom_right) - get_y(upper_left) + 1; + let rows = area.height(); if rows == 0 { return; } @@ -773,7 +759,7 @@ impl ListingTrait for CompactListing { if idx >= self.length { continue; //bounds check } - let new_area = nth_row_area(area, idx % rows); + let new_area = area.nth_row(idx % rows); self.data_columns .draw(grid, idx, self.cursor_pos.2, grid.bounds_iter(new_area)); if highlight { @@ -798,15 +784,15 @@ impl ListingTrait for CompactListing { /* Page_no has changed, so draw new page */ _ = self .data_columns - .recalc_widths((width!(area), height!(area)), top_idx); + .recalc_widths((area.width(), area.height()), top_idx); grid.clear_area(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)) { + for i in top_idx..(top_idx + area.height()) { if let Some(row_attr) = self.rows.row_attr_cache.get(&i) { - grid.change_theme(nth_row_area(area, i % rows), *row_attr); + grid.change_theme(area.nth_row(i % rows), *row_attr); } } @@ -818,12 +804,12 @@ impl ListingTrait for CompactListing { true, false ); - grid.change_theme(nth_row_area(area, self.cursor_pos.2 % rows), row_attr); + grid.change_theme(area.nth_row(self.cursor_pos.2 % rows), row_attr); /* clear gap if available height is more than count of entries */ if top_idx + rows > self.length { grid.clear_area( - (pos_inc(upper_left, (0, rows - 1)), bottom_right), + area.skip_rows(top_idx + rows - self.length), self.color_cache.theme_default, ); } @@ -870,7 +856,7 @@ impl ListingTrait for CompactListing { self.new_cursor_pos.2 = std::cmp::min(self.filtered_selection.len() - 1, self.cursor_pos.2); } else { - self.data_columns.columns[0] = CellBuffer::new_with_context(0, 0, None, context); + self.data_columns.columns[0].resize_with_context(0, 0, context); } self.redraw_threads_list( context, @@ -1228,97 +1214,155 @@ impl CompactListing { drop(envelope); let columns = &mut self.data_columns.columns; let min_width = ( - columns[0].size().0, - columns[1].size().0, - columns[2].size().0, - columns[3].size().0, + columns[0].area().width(), + columns[1].area().width(), + columns[2].area().width(), + columns[3].area().width(), ); - let (x, _) = columns[0].write_string( - &idx.to_string(), - row_attr.fg, - row_attr.bg, - row_attr.attrs, - ((0, idx), (min_width.0, idx)), - None, - ); - for c in columns[0].row_iter(x..min_width.0, idx) { - columns[0][c].set_bg(row_attr.bg).set_ch(' '); + let (x, _) = { + let area = columns[0].area().nth_row(idx); + columns[0].grid_mut().write_string( + &idx.to_string(), + row_attr.fg, + row_attr.bg, + row_attr.attrs, + area, + None, + ) + }; + for c in { + let area = columns[0].area(); + columns[0].grid_mut().row_iter(area, x..min_width.0, idx) + } { + columns[0].grid_mut()[c] + .set_bg(row_attr.bg) + .set_attrs(row_attr.attrs) + .set_ch(' '); } - let (x, _) = columns[1].write_string( - &strings.date, - row_attr.fg, - row_attr.bg, - row_attr.attrs, - ((0, idx), (min_width.1.saturating_sub(1), idx)), - None, - ); - for c in columns[1].row_iter(x..min_width.1, idx) { - columns[1][c].set_bg(row_attr.bg).set_ch(' '); + let (x, _) = { + let area = columns[1].area().nth_row(idx); + columns[1].grid_mut().write_string( + &strings.date, + row_attr.fg, + row_attr.bg, + row_attr.attrs, + area, + None, + ) + }; + for c in { + let area = columns[1].area(); + columns[1].grid_mut().row_iter(area, x..min_width.1, idx) + } { + columns[1].grid_mut()[c] + .set_bg(row_attr.bg) + .set_attrs(row_attr.attrs) + .set_ch(' '); } - let (x, _) = columns[2].write_string( - &strings.from, - row_attr.fg, - row_attr.bg, - row_attr.attrs, - ((0, idx), (min_width.2, idx)), - None, - ); - for c in columns[2].row_iter(x..min_width.2, idx) { - columns[2][c].set_bg(row_attr.bg).set_ch(' '); + let (x, _) = { + let area = columns[2].area().nth_row(idx); + columns[2].grid_mut().write_string( + &strings.from, + row_attr.fg, + row_attr.bg, + row_attr.attrs, + area, + None, + ) + }; + for c in { + let area = columns[2].area(); + columns[2].grid_mut().row_iter(area, x..min_width.2, idx) + } { + columns[2].grid_mut()[c] + .set_bg(row_attr.bg) + .set_attrs(row_attr.attrs) + .set_ch(' '); } - let (x, _) = columns[3].write_string( - &strings.flag, - row_attr.fg, - row_attr.bg, - row_attr.attrs, - ((0, idx), (min_width.3, idx)), - None, - ); - let (x, _) = columns[3].write_string( - &strings.subject, - row_attr.fg, - row_attr.bg, - row_attr.attrs, - ((x, idx), (min_width.3, idx)), - None, - ); - if let Some(c) = columns[3].get_mut(x, idx) { - c.set_bg(row_attr.bg).set_ch(' '); + let (x, _) = { + let area = columns[3].area().nth_row(idx); + columns[3].grid_mut().write_string( + &strings.flag, + row_attr.fg, + row_attr.bg, + row_attr.attrs, + area, + None, + ) + }; + let (x, _) = { + let area = columns[3].area().nth_row(idx).skip_cols(x); + columns[3].grid_mut().write_string( + &strings.subject, + row_attr.fg, + row_attr.bg, + row_attr.attrs, + area, + None, + ) + }; + if let Some(c) = columns[3].grid_mut().get_mut(x, idx) { + c.set_bg(row_attr.bg).set_attrs(row_attr.attrs).set_ch(' '); } let x = { let mut x = x + 1; for (t, &color) in strings.tags.split_whitespace().zip(strings.tags.1.iter()) { let color = color.unwrap_or(self.color_cache.tag_default.bg); - let (_x, _) = columns[3].write_string( - t, - self.color_cache.tag_default.fg, - color, - self.color_cache.tag_default.attrs, - ((x + 1, idx), (min_width.3, idx)), - None, - ); - for c in columns[3].row_iter(x..(x + 1), idx) { - columns[3][c].set_bg(color); + let _x = { + let area = columns[3].area().nth_row(idx).skip_cols(x + 1); + columns[3] + .grid_mut() + .write_string( + t, + self.color_cache.tag_default.fg, + color, + self.color_cache.tag_default.attrs, + area, + None, + ) + .0 + + x + + 1 + }; + for c in { + let area = columns[3].area(); + columns[3].grid_mut().row_iter(area, x..(x + 1), idx) + } { + columns[3].grid_mut()[c].set_bg(color); } - for c in columns[3].row_iter(_x..(_x + 1), idx) { - columns[3][c].set_bg(color).set_keep_bg(true); + for c in { + let area = columns[3].area(); + columns[3].grid_mut().row_iter(area, _x..(_x + 1), idx) + } { + columns[3].grid_mut()[c].set_bg(color).set_keep_bg(true); } - for c in columns[3].row_iter((x + 1)..(_x + 1), idx) { - columns[3][c] + for c in { + let area = columns[3].area(); + columns[3].grid_mut().row_iter(area, (x + 1)..(_x + 1), idx) + } { + columns[3].grid_mut()[c] .set_keep_fg(true) .set_keep_bg(true) .set_keep_attrs(true); } - for c in columns[3].row_iter(x..(x + 1), idx) { - columns[3][c].set_keep_bg(true); + for c in { + let area = columns[3].area(); + columns[3].grid_mut().row_iter(area, x..(x + 1), idx) + } { + columns[3].grid_mut()[c].set_keep_bg(true); } - x = _x + 1; - columns[3][(x, idx)].set_bg(row_attr.bg).set_ch(' '); + x = _x + 2; } x }; - for c in columns[3].row_iter(x..min_width.3, idx) { - columns[3][c].set_ch(' ').set_bg(row_attr.bg); + for c in { + let area = columns[3].area(); + columns[3].grid_mut().row_iter(area, x..min_width.3, idx) + } { + columns[3].grid_mut()[c] + .set_ch(' ') + .set_bg(row_attr.bg) + .set_attrs(row_attr.attrs); } *self.rows.entries.get_mut(idx).unwrap() = ((thread_hash, env_hash), strings); self.rows_drawn.update(idx, 1); @@ -1336,10 +1380,10 @@ impl CompactListing { self.rows_drawn.update(i, 0); } let min_width = ( - self.data_columns.columns[0].size().0, - self.data_columns.columns[1].size().0, - self.data_columns.columns[2].size().0, - self.data_columns.columns[3].size().0, + self.data_columns.columns[0].area().width(), + self.data_columns.columns[1].area().width(), + self.data_columns.columns[2].area().width(), + self.data_columns.columns[3].area().width(), ); for (idx, ((_thread_hash, root_env_hash), strings)) in self @@ -1362,76 +1406,103 @@ impl CompactListing { panic!(); } let row_attr = self.rows.row_attr_cache[&idx]; - let (x, _) = self.data_columns.columns[0].write_string( - &idx.to_string(), - row_attr.fg, - row_attr.bg, - row_attr.attrs, - ((0, idx), (min_width.0, idx)), - None, - ); + let (x, _) = { + let area = self.data_columns.columns[0].area().nth_row(idx); + self.data_columns.columns[0].grid_mut().write_string( + &idx.to_string(), + row_attr.fg, + row_attr.bg, + row_attr.attrs, + area, + None, + ) + }; for x in x..min_width.0 { - self.data_columns.columns[0][(x, idx)] + self.data_columns.columns[0].grid_mut()[(x, idx)] .set_bg(row_attr.bg) .set_attrs(row_attr.attrs); } - let (x, _) = self.data_columns.columns[1].write_string( - &strings.date, - row_attr.fg, - row_attr.bg, - row_attr.attrs, - ((0, idx), (min_width.1, idx)), - None, - ); + let (x, _) = { + let area = self.data_columns.columns[1].area().nth_row(idx); + self.data_columns.columns[1].grid_mut().write_string( + &strings.date, + row_attr.fg, + row_attr.bg, + row_attr.attrs, + area, + None, + ) + }; for x in x..min_width.1 { - self.data_columns.columns[1][(x, idx)] + self.data_columns.columns[1].grid_mut()[(x, idx)] .set_bg(row_attr.bg) .set_attrs(row_attr.attrs); } - let (x, _) = self.data_columns.columns[2].write_string( - &strings.from, - row_attr.fg, - row_attr.bg, - row_attr.attrs, - ((0, idx), (min_width.2, idx)), - None, - ); + let (x, _) = { + let area = self.data_columns.columns[2].area().nth_row(idx); + self.data_columns.columns[2].grid_mut().write_string( + &strings.from, + row_attr.fg, + row_attr.bg, + row_attr.attrs, + area, + None, + ) + }; #[cfg(feature = "regexp")] { for text_formatter in crate::conf::text_format_regexps(context, "listing.from") { - let t = self.data_columns.columns[2].insert_tag(text_formatter.tag); + let t = self.data_columns.columns[2] + .grid_mut() + .insert_tag(text_formatter.tag); for (start, end) in text_formatter.regexp.find_iter(strings.from.as_str()) { - self.data_columns.columns[2].set_tag(t, (start, idx), (end, idx)); + self.data_columns.columns[2].grid_mut().set_tag( + t, + (start, idx), + (end, idx), + ); } } } for x in x..min_width.2 { - self.data_columns.columns[2][(x, idx)] + self.data_columns.columns[2].grid_mut()[(x, idx)] .set_bg(row_attr.bg) .set_attrs(row_attr.attrs); } - let (x, _) = self.data_columns.columns[3].write_string( - &strings.flag, - row_attr.fg, - row_attr.bg, - row_attr.attrs, - ((0, idx), (min_width.3, idx)), - None, - ); - let (x, _) = self.data_columns.columns[3].write_string( + let (x, _) = { + let area = self.data_columns.columns[3].area().nth_row(idx); + self.data_columns.columns[3].grid_mut().write_string( + &strings.flag, + row_attr.fg, + row_attr.bg, + row_attr.attrs, + area, + None, + ) + }; + let (x, _) = self.data_columns.columns[3].grid_mut().write_string( &strings.subject, row_attr.fg, row_attr.bg, row_attr.attrs, - ((x, idx), (min_width.3, idx)), + self.data_columns.columns[3] + .area() + .nth_row(idx) + .skip_cols(x), None, ); #[cfg(feature = "regexp")] { for text_formatter in crate::conf::text_format_regexps(context, "listing.subject") { - let t = self.data_columns.columns[3].insert_tag(text_formatter.tag); + let t = self.data_columns.columns[3] + .grid_mut() + .insert_tag(text_formatter.tag); for (start, end) in text_formatter.regexp.find_iter(strings.subject.as_str()) { - self.data_columns.columns[3].set_tag(t, (start, idx), (end, idx)); + self.data_columns.columns[3].grid_mut().set_tag( + t, + (start, idx), + (end, idx), + ); } } } @@ -1439,33 +1510,36 @@ impl CompactListing { let mut x = x + 1; for (t, &color) in strings.tags.split_whitespace().zip(strings.tags.1.iter()) { let color = color.unwrap_or(self.color_cache.tag_default.bg); - let (_x, _) = self.data_columns.columns[3].write_string( + let (_x, _) = self.data_columns.columns[3].grid_mut().write_string( t, self.color_cache.tag_default.fg, color, self.color_cache.tag_default.attrs, - ((x + 1, idx), (min_width.3, idx)), + self.data_columns.columns[3] + .area() + .nth_row(idx) + .skip_cols(x + 1), None, ); - self.data_columns.columns[3][(x, idx)].set_bg(color); + self.data_columns.columns[3].grid_mut()[(x, idx)].set_bg(color); if _x < min_width.3 { - self.data_columns.columns[3][(_x, idx)] + self.data_columns.columns[3].grid_mut()[(_x, idx)] .set_bg(color) .set_keep_bg(true); } for x in (x + 1).._x { - self.data_columns.columns[3][(x, idx)] + self.data_columns.columns[3].grid_mut()[(x, idx)] .set_keep_fg(true) .set_keep_bg(true) .set_keep_attrs(true); } - self.data_columns.columns[3][(x, idx)].set_keep_bg(true); + self.data_columns.columns[3].grid_mut()[(x, idx)].set_keep_bg(true); x = _x + 1; } x }; for x in x..min_width.3 { - self.data_columns.columns[3][(x, idx)] + self.data_columns.columns[3].grid_mut()[(x, idx)] .set_ch(' ') .set_bg(row_attr.bg) .set_attrs(row_attr.attrs); @@ -1533,7 +1607,6 @@ impl Component for CompactListing { if !self.unfocused() { let mut area = area; if !self.filter_term.is_empty() { - let (upper_left, bottom_right) = area; let (x, y) = grid.write_string( &format!( "{} results for `{}` (Press ESC to exit)", @@ -1544,7 +1617,7 @@ impl Component for CompactListing { self.color_cache.theme_default.bg, self.color_cache.theme_default.attrs, area, - Some(get_x(upper_left)), + None, ); let default_cell = { let mut ret = Cell::with_char(' '); @@ -1553,19 +1626,16 @@ impl Component for CompactListing { .set_attrs(self.color_cache.theme_default.attrs); ret }; - for row in grid.bounds_iter(((x, y), set_y(bottom_right, y))) { + for row in grid.bounds_iter(area.nth_row(y).skip_cols(x)) { for c in row { grid[c] = default_cell; } } - context - .dirty_areas - .push_back((upper_left, set_y(bottom_right, y + 1))); + context.dirty_areas.push_back(area); - area = (set_y(upper_left, y + 1), bottom_right); + area = area.skip_rows(y + 1); } - let (upper_left, bottom_right) = area; - let rows = get_y(bottom_right) - get_y(upper_left) + 1; + let rows = area.height(); if let Some(modifier) = self.modifier_command.take() { if let Some(mvm) = self.movement.as_ref() { diff --git a/meli/src/mail/listing/conversations.rs b/meli/src/mail/listing/conversations.rs index 1c8b7f3b..7f560002 100644 --- a/meli/src/mail/listing/conversations.rs +++ b/meli/src/mail/listing/conversations.rs @@ -460,8 +460,8 @@ impl ListingTrait for ConversationsListing { { self.refresh_mailbox(context, false); } - let upper_left = upper_left!(area); - let bottom_right = bottom_right!(area); + let upper_left = area.upper_left(); + let bottom_right = area.bottom_right(); if let Err(message) = self.error.as_ref() { grid.clear_area(area, self.color_cache.theme_default); @@ -528,10 +528,7 @@ impl ListingTrait for ConversationsListing { if *idx >= self.length { continue; //bounds check } - let new_area = ( - set_y(upper_left, get_y(upper_left) + 3 * (*idx % rows)), - set_y(bottom_right, get_y(upper_left) + 3 * (*idx % rows) + 2), - ); + let new_area = area.skip_rows(3 * (*idx % rows)).take_rows(2); self.highlight_line(grid, new_area, *idx, context); context.dirty_areas.push_back(new_area); } @@ -552,13 +549,7 @@ impl ListingTrait for ConversationsListing { self.highlight_line( grid, - ( - pos_inc(upper_left, (0, 3 * (self.cursor_pos.2 % rows))), - set_y( - bottom_right, - get_y(upper_left) + 3 * (self.cursor_pos.2 % rows) + 2, - ), - ), + area.skip_rows(3 * (self.cursor_pos.2 % rows)).take_rows(2), self.cursor_pos.2, context, ); @@ -888,12 +879,11 @@ impl ConversationsListing { let account = &context.accounts[&self.cursor_pos.0]; let threads = account.collection.get_threads(self.cursor_pos.1); grid.clear_area(area, self.color_cache.theme_default); - let (mut upper_left, bottom_right) = area; for (idx, ((thread_hash, root_env_hash), strings)) in self.rows.entries.iter().enumerate().skip(top_idx) { if !context.accounts[&self.cursor_pos.0].contains_key(*root_env_hash) { - panic!(); + continue; } let thread = threads.thread_ref(*thread_hash); @@ -909,11 +899,11 @@ impl ConversationsListing { row_attr.fg, row_attr.bg, row_attr.attrs, - (upper_left, bottom_right), + area.skip_rows(idx), None, ); - for x in x..(x + 3) { - grid[set_x(upper_left, x)].set_bg(row_attr.bg); + for c in grid.row_iter(area, x..(x + 3), idx) { + grid[c].set_bg(row_attr.bg); } let subject_attr = row_attr!( subject, @@ -928,10 +918,10 @@ impl ConversationsListing { subject_attr.fg, subject_attr.bg, subject_attr.attrs, - (set_x(upper_left, x), bottom_right), + area.skip(idx, x), None, ); - let mut subject_overflowed = subject_overflowed > get_y(upper_left); + let mut subject_overflowed = subject_overflowed > 0; for (t, &color) in strings.tags.split_whitespace().zip(strings.tags.1.iter()) { if subject_overflowed { break; @@ -942,10 +932,10 @@ impl ConversationsListing { self.color_cache.tag_default.fg, color, self.color_cache.tag_default.attrs, - (set_x(upper_left, x + 1), bottom_right), + area.skip(idx, x + 1), None, ); - if _y > get_y(upper_left) { + if _y > 0 { subject_overflowed = true; break; } @@ -1054,7 +1044,7 @@ impl Component for ConversationsListing { area, Some(get_x(upper_left)), ); - for c in grid.row_iter(x..(get_x(bottom_right) + 1), y) { + for c in grid.row_iter(area, x..(get_x(bottom_right) + 1), y) { grid[c] = Cell::default(); } @@ -1278,12 +1268,12 @@ impl Component for ConversationsListing { } let entry_area = ( - set_x(upper_left, get_x(upper_left) + width!(area) / 3 + 2), + set_x(upper_left, get_x(upper_left) + area.width() / 3 + 2), bottom_right, ); let gap_area = ( - pos_dec(upper_left!(entry_area), (1, 0)), - bottom_right!(entry_area), + pos_dec(entry_area.upper_left(), (1, 0)), + entry_area.bottom_right(), ); grid.clear_area(gap_area, self.color_cache.theme_default); context.dirty_areas.push_back(gap_area); diff --git a/meli/src/mail/listing/offline.rs b/meli/src/mail/listing/offline.rs index 2494cfaf..d8c1b06c 100644 --- a/meli/src/mail/listing/offline.rs +++ b/meli/src/mail/listing/offline.rs @@ -150,36 +150,38 @@ impl Component for OfflineListing { None, ); - grid.write_string( + let (_, mut y_offset) = grid.write_string( &err.to_string(), error_message.fg, error_message.bg, error_message.attrs, - (set_x(upper_left!(area), x + 1), bottom_right!(area)), - Some(get_x(upper_left!(area))), + area.skip_cols(x + 1), + Some(0), ); + y_offset += 1; if let Some(msg) = self.messages.last() { grid.write_string( msg, text_unfocused.fg, text_unfocused.bg, Attr::BOLD, - (pos_inc((0, 1), upper_left!(area)), bottom_right!(area)), + area.skip_rows(y_offset), None, ); } + y_offset += 1; for (i, msg) in self.messages.iter().rev().skip(1).enumerate() { grid.write_string( msg, text_unfocused.fg, text_unfocused.bg, text_unfocused.attrs, - (pos_inc((0, 2 + i), upper_left!(area)), bottom_right!(area)), + area.skip_rows(y_offset + i), None, ); } } else { - let (_, mut y) = grid.write_string( + grid.write_string( "loading...", conf::value(context, "highlight").fg, conf::value(context, "highlight").bg, @@ -192,16 +194,15 @@ impl Component for OfflineListing { .iter() .collect(); jobs.sort_by_key(|(j, _)| *j); - for (job_id, j) in jobs { + for (i, (job_id, j)) in jobs.into_iter().enumerate() { grid.write_string( &format!("{}: {:?}", job_id, j), text_unfocused.fg, text_unfocused.bg, text_unfocused.attrs, - (set_y(upper_left!(area), y + 1), bottom_right!(area)), + area.skip_rows(i + 1), None, ); - y += 1; } context diff --git a/meli/src/mail/listing/plain.rs b/meli/src/mail/listing/plain.rs index 1a0cb84b..f52634e7 100644 --- a/meli/src/mail/listing/plain.rs +++ b/meli/src/mail/listing/plain.rs @@ -26,22 +26,6 @@ use melib::{Address, SortField, SortOrder, ThreadNode}; use super::{EntryStrings, *}; use crate::{components::PageMovement, jobs::JoinHandle}; -macro_rules! address_list { - (($name:expr) as comma_sep_list) => {{ - let mut ret: String = - $name - .into_iter() - .fold(String::new(), |mut s: String, n: &Address| { - s.extend(n.to_string().chars()); - s.push_str(", "); - s - }); - ret.pop(); - ret.pop(); - ret - }}; -} - macro_rules! row_attr { ($color_cache:expr, $even: expr, $unseen:expr, $highlighted:expr, $selected:expr $(,)*) => {{ let color_cache = &$color_cache; @@ -210,6 +194,7 @@ impl MailListingTrait for PlainListing { /// account mailbox the user has chosen. fn refresh_mailbox(&mut self, context: &mut Context, force: bool) { self.set_dirty(true); + self.force_draw = true; let old_cursor_pos = self.cursor_pos; if !(self.cursor_pos.0 == self.new_cursor_pos.0 && self.cursor_pos.1 == self.new_cursor_pos.1) @@ -227,19 +212,20 @@ impl MailListingTrait for PlainListing { match context.accounts[&self.cursor_pos.0].load(self.cursor_pos.1) { Ok(()) => {} Err(_) => { + self.length = 0; let message: String = context.accounts[&self.cursor_pos.0][&self.cursor_pos.1].status(); - self.data_columns.columns[0] = - CellBuffer::new_with_context(message.len(), 1, None, context); - self.length = 0; - self.data_columns.columns[0].write_string( - message.as_str(), - self.color_cache.theme_default.fg, - self.color_cache.theme_default.bg, - self.color_cache.theme_default.attrs, - ((0, 0), (message.len().saturating_sub(1), 0)), - None, - ); + if self.data_columns.columns[0].resize_with_context(message.len(), 1, context) { + let area = self.data_columns.columns[0].area(); + self.data_columns.columns[0].grid_mut().write_string( + message.as_str(), + self.color_cache.theme_default.fg, + self.color_cache.theme_default.bg, + self.color_cache.theme_default.attrs, + area, + None, + ); + } return; } } @@ -338,6 +324,7 @@ impl ListingTrait for PlainListing { self.filtered_order.clear(); self.filter_term.clear(); self.rows.row_updates.clear(); + self.data_columns.clear(); } fn next_entry(&mut self, context: &mut Context) { @@ -390,17 +377,12 @@ impl ListingTrait for PlainListing { self.rows.selection[&i] ); - let (upper_left, bottom_right) = area; - let x = get_x(upper_left) - + self.data_columns.widths[0] + let x = self.data_columns.widths[0] + self.data_columns.widths[1] + self.data_columns.widths[2] + 3 * 2; - for c in grid.row_iter( - get_x(upper_left)..(get_x(bottom_right) + 1), - get_y(upper_left), - ) { + for c in grid.row_iter(area, 0..area.width(), 0) { grid[c] .set_fg(row_attr.fg) .set_bg(row_attr.bg) @@ -408,14 +390,11 @@ impl ListingTrait for PlainListing { } grid.copy_area( - &self.data_columns.columns[3], - (set_x(upper_left, x), bottom_right), - ( - (0, idx), - pos_dec(self.data_columns.columns[3].size(), (1, 1)), - ), + self.data_columns.columns[3].grid(), + area.skip_cols(x), + self.data_columns.columns[3].area().nth_row(idx), ); - for c in grid.row_iter(x..get_x(bottom_right), get_y(upper_left)) { + for c in grid.row_iter(area, x..area.width(), 0) { grid[c].set_bg(row_attr.bg).set_attrs(row_attr.attrs); } } @@ -426,20 +405,18 @@ impl ListingTrait for PlainListing { { self.refresh_mailbox(context, false); } - let upper_left = upper_left!(area); - let bottom_right = bottom_right!(area); if self.length == 0 { grid.clear_area(area, self.color_cache.theme_default); grid.copy_area( - &self.data_columns.columns[0], + self.data_columns.columns[0].grid(), area, - ((0, 0), pos_dec(self.data_columns.columns[0].size(), (1, 1))), + self.data_columns.columns[0].area(), ); context.dirty_areas.push_back(area); return; } - let rows = get_y(bottom_right) - get_y(upper_left) + 1; + let rows = area.height(); if rows == 0 { return; } @@ -478,6 +455,10 @@ impl ListingTrait for PlainListing { } } + if self.force_draw { + grid.clear_area(area, self.color_cache.theme_default); + } + let prev_page_no = (self.cursor_pos.2).wrapping_div(rows); let page_no = (self.new_cursor_pos.2).wrapping_div(rows); @@ -492,7 +473,7 @@ impl ListingTrait for PlainListing { if idx >= self.length { continue; //bounds check } - let new_area = nth_row_area(area, idx % rows); + let new_area = area.nth_row(idx % rows); self.data_columns .draw(grid, idx, self.cursor_pos.2, grid.bounds_iter(new_area)); if highlight { @@ -514,18 +495,18 @@ impl ListingTrait for PlainListing { self.cursor_pos.2 = self.new_cursor_pos.2; } + if !self.force_draw { + grid.clear_area(area, self.color_cache.theme_default); + } /* Page_no has changed, so draw new page */ - _ = self - .data_columns - .recalc_widths((width!(area), height!(area)), top_idx); - grid.clear_area(area, self.color_cache.theme_default); + _ = self.data_columns.recalc_widths(area.size(), top_idx); /* 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)) { + for i in top_idx..(top_idx + area.height()) { if let Some(row_attr) = self.rows.row_attr_cache.get(&i) { - grid.change_theme(nth_row_area(area, i % rows), *row_attr); + grid.change_theme(area.nth_row(i % rows), *row_attr); } } @@ -537,18 +518,17 @@ impl ListingTrait for PlainListing { true, false ); - grid.change_theme(nth_row_area(area, self.cursor_pos.2 % rows), row_attr); + grid.change_theme(area.nth_row(self.cursor_pos.2 % rows), row_attr); /* clear gap if available height is more than count of entries */ if top_idx + rows > self.length { - grid.clear_area( - ( - pos_inc(upper_left, (0, self.length - top_idx)), - bottom_right, - ), + grid.change_theme( + area.skip_rows(self.length - top_idx), self.color_cache.theme_default, ); } + + self.force_draw = false; context.dirty_areas.push_back(area); } @@ -589,7 +569,7 @@ impl ListingTrait for PlainListing { self.new_cursor_pos.2 = std::cmp::min(self.filtered_selection.len() - 1, self.cursor_pos.2); } else { - self.data_columns.columns[0] = CellBuffer::new_with_context(0, 0, None, context); + _ = self.data_columns.columns[0].resize_with_context(0, 0, context); } self.redraw_list( context, @@ -867,7 +847,7 @@ impl PlainListing { 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[2].set_grow(15, Some(35)); self.data_columns.elasticities[3].set_rigid(); self.data_columns .cursor_config @@ -881,17 +861,13 @@ impl PlainListing { .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); + _ = self.data_columns.columns[0].resize_with_context(min_width.0, self.rows.len(), context); /* date column */ - self.data_columns.columns[1] = - CellBuffer::new_with_context(min_width.1, self.rows.len(), None, context); + _ = self.data_columns.columns[1].resize_with_context(min_width.1, self.rows.len(), context); /* from column */ - self.data_columns.columns[2] = - CellBuffer::new_with_context(min_width.2, self.rows.len(), None, context); + _ = self.data_columns.columns[2].resize_with_context(min_width.2, self.rows.len(), context); /* subject column */ - self.data_columns.columns[3] = - CellBuffer::new_with_context(min_width.3, self.rows.len(), None, context); + _ = self.data_columns.columns[3].resize_with_context(min_width.3, self.rows.len(), context); let iter = if self.filter_term.is_empty() { Box::new(self.local_collection.iter().cloned()) @@ -912,104 +888,157 @@ impl PlainListing { //); //log::debug!("{:#?}", context.accounts); - panic!(); + continue; } let row_attr = self.rows.row_attr_cache[&idx]; - let (x, _) = columns[0].write_string( - &idx.to_string(), - row_attr.fg, - row_attr.bg, - row_attr.attrs, - ((0, idx), (min_width.0, idx)), - None, - ); - for c in columns[0].row_iter(x..min_width.0, idx) { - columns[0][c].set_bg(row_attr.bg).set_attrs(row_attr.attrs); - } - let (x, _) = columns[1].write_string( - &strings.date, - row_attr.fg, - row_attr.bg, - row_attr.attrs, - ((0, idx), (min_width.1, idx)), - None, - ); - for c in columns[1].row_iter(x..min_width.1, idx) { - columns[1][c].set_bg(row_attr.bg).set_attrs(row_attr.attrs); - } - let (x, _) = columns[2].write_string( - &strings.from, - row_attr.fg, - row_attr.bg, - row_attr.attrs, - ((0, idx), (min_width.2, idx)), - None, - ); - for c in columns[2].row_iter(x..min_width.2, idx) { - columns[2][c].set_bg(row_attr.bg).set_attrs(row_attr.attrs); - } - let (x, _) = columns[3].write_string( - &strings.flag, - row_attr.fg, - row_attr.bg, - row_attr.attrs, - ((0, idx), (min_width.3, idx)), - None, - ); - let (x, _) = columns[3].write_string( - &strings.subject, - row_attr.fg, - row_attr.bg, - row_attr.attrs, - ((x, idx), (min_width.3, idx)), - None, - ); - let x = { - let mut x = x + 1; - for (t, &color) in strings.tags.split_whitespace().zip(strings.tags.1.iter()) { - let color = color.unwrap_or(self.color_cache.tag_default.bg); - let (_x, _) = columns[3].write_string( - t, - self.color_cache.tag_default.fg, - color, - self.color_cache.tag_default.attrs, - ((x + 1, idx), (min_width.3, idx)), - None, - ); - for c in columns[3].row_iter(x..(x + 1), idx) { - columns[3][c].set_bg(color); - } - for c in columns[3].row_iter(_x..(_x + 1), idx) { - columns[3][c].set_bg(color).set_keep_bg(true); - } - for c in columns[3].row_iter((x + 1)..(_x + 1), idx) { - columns[3][c].set_keep_fg(true).set_keep_bg(true); - } - for c in columns[3].row_iter(x..(x + 1), idx) { - columns[3][c].set_keep_bg(true); - } - x = _x + 1; - } - x + let (x, _) = { + let area = columns[0].area().nth_row(idx); + columns[0].grid_mut().write_string( + &idx.to_string(), + row_attr.fg, + row_attr.bg, + row_attr.attrs, + area, + None, + ) }; - 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); + for c in { + let area = columns[0].area(); + columns[0].grid_mut().row_iter(area, x..min_width.0, idx) + } { + columns[0].grid_mut()[c] + .set_bg(row_attr.bg) + .set_attrs(row_attr.attrs); + } + let (x, _) = { + let area = columns[1].area().nth_row(idx); + columns[1].grid_mut().write_string( + &strings.date, + row_attr.fg, + row_attr.bg, + row_attr.attrs, + area, + None, + ) + }; + for c in { + let area = columns[1].area(); + columns[1].grid_mut().row_iter(area, x..min_width.1, idx) + } { + columns[1].grid_mut()[c] + .set_bg(row_attr.bg) + .set_attrs(row_attr.attrs); + } + let (x, _) = { + let area = columns[2].area().nth_row(idx); + columns[2].grid_mut().write_string( + &strings.from, + row_attr.fg, + row_attr.bg, + row_attr.attrs, + area, + None, + ) + }; + for c in { + let area = columns[2].area(); + columns[2].grid_mut().row_iter(area, x..min_width.2, idx) + } { + columns[2].grid_mut()[c] + .set_bg(row_attr.bg) + .set_attrs(row_attr.attrs) + .set_ch(' '); + } + let (x, _) = { + let area = columns[3].area().nth_row(idx); + columns[3].grid_mut().write_string( + &strings.flag, + row_attr.fg, + row_attr.bg, + row_attr.attrs, + area, + None, + ) + }; + let x = { + let area = columns[3].area().nth_row(idx).skip_cols(x); + columns[3] + .grid_mut() + .write_string( + &strings.subject, + row_attr.fg, + row_attr.bg, + row_attr.attrs, + area, + None, + ) + .0 + + x + }; + let mut x = x + 1; + for (t, &color) in strings.tags.split_whitespace().zip(strings.tags.1.iter()) { + let color = color.unwrap_or(self.color_cache.tag_default.bg); + let _x = { + let area = columns[3].area().nth_row(idx).skip_cols(x + 1); + columns[3] + .grid_mut() + .write_string( + t, + self.color_cache.tag_default.fg, + color, + self.color_cache.tag_default.attrs, + area, + None, + ) + .0 + + x + + 1 + }; + for c in { + let area = columns[3].area(); + columns[3].grid_mut().row_iter(area, x..(x + 1), idx) + } { + columns[3].grid_mut()[c].set_bg(color); + } + for c in { + let area = columns[3].area(); + columns[3].grid_mut().row_iter(area, _x..(_x + 1), idx) + } { + columns[3].grid_mut()[c].set_bg(color).set_keep_bg(true); + } + for c in { + let area = columns[3].area(); + columns[3].grid_mut().row_iter(area, (x + 1)..(_x + 1), idx) + } { + columns[3].grid_mut()[c] + .set_keep_fg(true) + .set_keep_bg(true) + .set_keep_attrs(true); + } + for c in { + let area = columns[3].area(); + columns[3].grid_mut().row_iter(area, x..(x + 1), idx) + } { + columns[3].grid_mut()[c].set_keep_bg(true); + } + x = _x + 2; } } if self.length == 0 && self.filter_term.is_empty() { let message: String = account[&self.cursor_pos.1].status(); - self.data_columns.columns[0] = - CellBuffer::new_with_context(message.len(), self.length + 1, None, context); - self.data_columns.columns[0].write_string( - &message, - self.color_cache.theme_default.fg, - self.color_cache.theme_default.bg, - self.color_cache.theme_default.attrs, - ((0, 0), (message.len() - 1, 0)), - None, - ); + if self.data_columns.columns[0].resize_with_context(message.len(), 1, context) { + let area = self.data_columns.columns[0].area(); + self.data_columns.columns[0].grid_mut().write_string( + message.as_str(), + self.color_cache.theme_default.fg, + self.color_cache.theme_default.bg, + self.color_cache.theme_default.attrs, + area, + None, + ); + } } } @@ -1044,97 +1073,152 @@ impl PlainListing { let strings = self.make_entry_string(&envelope, context); drop(envelope); let columns = &mut self.data_columns.columns; - let min_width = ( - columns[0].size().0, - columns[1].size().0, - columns[2].size().0, - columns[3].size().0, - ); + { + let area = columns[0].area().nth_row(idx); + columns[0].grid_mut().clear_area(area, row_attr) + }; + { + let area = columns[1].area().nth_row(idx); + columns[1].grid_mut().clear_area(area, row_attr); + } + { + let area = columns[2].area().nth_row(idx); + columns[2].grid_mut().clear_area(area, row_attr); + } + { + let area = columns[3].area().nth_row(idx); + columns[3].grid_mut().clear_area(area, row_attr); + } - columns[0].clear_area(((0, idx), (min_width.0, idx)), row_attr); - columns[1].clear_area(((0, idx), (min_width.1, idx)), row_attr); - columns[2].clear_area(((0, idx), (min_width.2, idx)), row_attr); - columns[3].clear_area(((0, idx), (min_width.3, idx)), row_attr); - - let (x, _) = columns[0].write_string( - &idx.to_string(), - row_attr.fg, - row_attr.bg, - row_attr.attrs, - ((0, idx), (min_width.0, idx)), - None, - ); - for c in columns[0].row_iter(x..min_width.0, idx) { - columns[0][c].set_bg(row_attr.bg).set_attrs(row_attr.attrs); + let (x, _) = { + let area = columns[0].area().nth_row(idx); + columns[0].grid_mut().write_string( + &idx.to_string(), + row_attr.fg, + row_attr.bg, + row_attr.attrs, + area, + None, + ) + }; + for c in { + let area = columns[0].area(); + columns[0].grid_mut().row_iter(area, x..area.width(), idx) + } { + columns[0].grid_mut()[c] + .set_bg(row_attr.bg) + .set_attrs(row_attr.attrs); } - let (x, _) = columns[1].write_string( - &strings.date, - row_attr.fg, - row_attr.bg, - row_attr.attrs, - ((0, idx), (min_width.1, idx)), - None, - ); - for c in columns[1].row_iter(x..min_width.1, idx) { - columns[1][c].set_bg(row_attr.bg).set_attrs(row_attr.attrs); + let (x, _) = { + let area = columns[1].area().nth_row(idx); + columns[1].grid_mut().write_string( + &strings.date, + row_attr.fg, + row_attr.bg, + row_attr.attrs, + area, + None, + ) + }; + for c in { + let area = columns[1].area(); + columns[1].grid_mut().row_iter(area, x..area.width(), idx) + } { + columns[1].grid_mut()[c] + .set_bg(row_attr.bg) + .set_attrs(row_attr.attrs); } - let (x, _) = columns[2].write_string( - &strings.from, - row_attr.fg, - row_attr.bg, - row_attr.attrs, - ((0, idx), (min_width.2, idx)), - None, - ); - for c in columns[2].row_iter(x..min_width.2, idx) { - columns[2][c].set_bg(row_attr.bg).set_attrs(row_attr.attrs); + let (x, _) = { + let area = columns[2].area().nth_row(idx); + columns[2].grid_mut().write_string( + &strings.from, + row_attr.fg, + row_attr.bg, + row_attr.attrs, + area, + None, + ) + }; + for c in { + let area = columns[2].area(); + columns[2].grid_mut().row_iter(area, x..area.width(), idx) + } { + columns[2].grid_mut()[c] + .set_bg(row_attr.bg) + .set_attrs(row_attr.attrs); } - let (x, _) = columns[3].write_string( - &strings.flag, - row_attr.fg, - row_attr.bg, - row_attr.attrs, - ((0, idx), (min_width.3, idx)), - None, - ); - let (x, _) = columns[3].write_string( - &strings.subject, - row_attr.fg, - row_attr.bg, - row_attr.attrs, - ((x, idx), (min_width.3, idx)), - None, - ); + let (x, _) = { + let area = columns[3].area().nth_row(idx); + columns[3].grid_mut().write_string( + &strings.flag, + row_attr.fg, + row_attr.bg, + row_attr.attrs, + area, + None, + ) + }; + let (x, _) = { + let area = columns[3].area().nth_row(idx).skip_cols(x); + columns[3].grid_mut().write_string( + &strings.subject, + row_attr.fg, + row_attr.bg, + row_attr.attrs, + area, + None, + ) + }; let x = { let mut x = x + 1; for (t, &color) in strings.tags.split_whitespace().zip(strings.tags.1.iter()) { let color = color.unwrap_or(self.color_cache.tag_default.bg); - let (_x, _) = columns[3].write_string( - t, - self.color_cache.tag_default.fg, - color, - self.color_cache.tag_default.attrs, - ((x + 1, idx), (min_width.3, idx)), - None, - ); - for c in columns[3].row_iter(x..(x + 1), idx) { - columns[3][c].set_bg(color); + let (_x, _) = { + let area = columns[3].area().nth_row(idx).skip_cols(x + 1); + columns[3].grid_mut().write_string( + t, + self.color_cache.tag_default.fg, + color, + self.color_cache.tag_default.attrs, + area, + None, + ) + }; + for c in { + let area = columns[3].area(); + columns[3].grid_mut().row_iter(area, x..(x + 1), idx) + } { + columns[3].grid_mut()[c].set_bg(color); } - for c in columns[3].row_iter(_x..(_x + 1), idx) { - columns[3][c].set_bg(color).set_keep_bg(true); + for c in { + let area = columns[3].area(); + columns[3].grid_mut().row_iter(area, _x..(_x + 1), idx) + } { + columns[3].grid_mut()[c].set_bg(color).set_keep_bg(true); } - for c in columns[3].row_iter((x + 1)..(_x + 1), idx) { - columns[3][c].set_keep_fg(true).set_keep_bg(true); + for c in { + let area = columns[3].area(); + columns[3].grid_mut().row_iter(area, (x + 1)..(_x + 1), idx) + } { + columns[3].grid_mut()[c].set_keep_fg(true).set_keep_bg(true); } - for c in columns[3].row_iter(x..(x + 1), idx) { - columns[3][c].set_keep_bg(true); + for c in { + let area = columns[3].area(); + columns[3].grid_mut().row_iter(area, x..(x + 1), idx) + } { + columns[3].grid_mut()[c].set_keep_bg(true); } - x = _x + 1; + x = _x + 2; } x }; - 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); + for c in { + let area = columns[3].area(); + columns[3].grid().row_iter(area, x..area.width(), idx) + } { + columns[3].grid_mut()[c] + .set_bg(row_attr.bg) + .set_attrs(row_attr.attrs); } *self.rows.entries.get_mut(idx).unwrap() = ((thread_hash, env_hash), strings); } @@ -1154,7 +1238,6 @@ impl Component for PlainListing { if matches!(self.focus, Focus::None) { let mut area = area; if !self.filter_term.is_empty() { - let (upper_left, bottom_right) = area; let (x, y) = grid.write_string( &format!( "{} results for `{}` (Press ESC to exit)", @@ -1165,22 +1248,16 @@ impl Component for PlainListing { self.color_cache.theme_default.bg, self.color_cache.theme_default.attrs, area, - Some(get_x(upper_left)), + Some(0), ); - grid.clear_area( - ((x, y), set_y(bottom_right, y)), - self.color_cache.theme_default, - ); - context - .dirty_areas - .push_back((upper_left, set_y(bottom_right, y + 1))); + grid.clear_area(area.skip(x, y).nth_row(y), self.color_cache.theme_default); + context.dirty_areas.push_back(area); - area = (set_y(upper_left, y + 1), bottom_right); + area = area.skip_rows(y + 1); } - let (upper_left, bottom_right) = area; - let rows = get_y(bottom_right) - get_y(upper_left) + 1; + let rows = area.height(); if let Some(modifier) = self.modifier_command.take() { if let Some(mvm) = self.movement.as_ref() { @@ -1379,13 +1456,13 @@ impl Component for PlainListing { self.draw_list(grid, area, context); } } else { + self.view_area = area.into(); if self.length == 0 && self.dirty { grid.clear_area(area, self.color_cache.theme_default); context.dirty_areas.push_back(area); + self.dirty = false; return; } - - self.view_area = area.into(); } self.dirty = false; } @@ -1394,6 +1471,11 @@ impl Component for PlainListing { let shortcuts = self.shortcuts(context); match (&event, self.focus) { + (UIEvent::VisibilityChange(true), _) => { + self.force_draw = true; + self.set_dirty(true); + return true; + } (UIEvent::Input(ref k), Focus::Entry) if shortcut!(k == shortcuts[Shortcuts::LISTING]["focus_right"]) => { @@ -1493,7 +1575,7 @@ impl Component for PlainListing { UIEvent::MailboxUpdate((ref idxa, ref idxf)) if (*idxa, *idxf) == (self.new_cursor_pos.0, self.cursor_pos.1) => { - self.refresh_mailbox(context, false); + self.refresh_mailbox(context, true); self.set_dirty(true); } UIEvent::StartupCheck(ref f) if *f == self.cursor_pos.1 => { @@ -1617,10 +1699,11 @@ impl Component for PlainListing { } fn is_dirty(&self) -> bool { - match self.focus { - Focus::None => self.dirty, - Focus::Entry | Focus::EntryFullscreen => false, - } + self.force_draw + || match self.focus { + Focus::None => self.dirty, + Focus::Entry | Focus::EntryFullscreen => false, + } } fn set_dirty(&mut self, value: bool) { diff --git a/meli/src/mail/listing/thread.rs b/meli/src/mail/listing/thread.rs index 35df8800..0337d8ab 100644 --- a/meli/src/mail/listing/thread.rs +++ b/meli/src/mail/listing/thread.rs @@ -489,8 +489,8 @@ impl ListingTrait for ThreadListing { { self.refresh_mailbox(context, false); } - let upper_left = upper_left!(area); - let bottom_right = bottom_right!(area); + let upper_left = area.upper_left(); + let bottom_right = area.bottom_right(); if self.length == 0 { grid.clear_area(area, self.color_cache.theme_default); context.dirty_areas.push_back(area); @@ -551,7 +551,7 @@ impl ListingTrait for ThreadListing { if idx >= self.length { continue; //bounds check } - let new_area = nth_row_area(area, idx % rows); + let new_area = area.nth_row(idx % rows); self.data_columns .draw(grid, idx, self.cursor_pos.2, grid.bounds_iter(new_area)); if let Some(env_hash) = self.get_env_under_cursor(idx) { @@ -589,7 +589,7 @@ impl ListingTrait for ThreadListing { _ = self .data_columns - .recalc_widths((width!(area), height!(area)), top_idx); + .recalc_widths((area.width(), area.height()), top_idx); grid.clear_area(area, self.color_cache.theme_default); /* copy table columns */ self.data_columns @@ -598,7 +598,7 @@ impl ListingTrait for ThreadListing { self.draw_relative_numbers(grid, area, top_idx); } /* apply each row colors separately */ - for idx in top_idx..(top_idx + height!(area)) { + for idx in top_idx..(top_idx + area.height()) { if let Some(env_hash) = self.get_env_under_cursor(idx) { let row_attr = row_attr!( self.color_cache, @@ -607,7 +607,7 @@ impl ListingTrait for ThreadListing { self.cursor_pos.2 == idx, self.rows.selection[&env_hash] ); - grid.change_theme(nth_row_area(area, idx % rows), row_attr); + grid.change_theme(area.nth_row(idx % rows), row_attr); } } @@ -620,7 +620,7 @@ impl ListingTrait for ThreadListing { true, // because self.cursor_pos.2 == idx, self.rows.selection[&env_hash] ); - grid.change_theme(nth_row_area(area, self.cursor_pos.2 % rows), row_attr); + grid.change_theme(area.nth_row(self.cursor_pos.2 % rows), row_attr); } /* clear gap if available height is more than count of entries */ @@ -1110,8 +1110,8 @@ impl ThreadListing { fn draw_relative_numbers(&mut self, grid: &mut CellBuffer, area: Area, top_idx: usize) { let width = self.data_columns.columns[0].size().0; - let upper_left = upper_left!(area); - for i in 0..height!(area) { + let upper_left = area.upper_left(); + for i in 0..area.height() { let row_attr = if let Some(env_hash) = self.get_env_under_cursor(top_idx + i) { row_attr!( self.color_cache, @@ -1366,8 +1366,8 @@ impl Component for ThreadListing { self.draw_list(grid, area, context); } else { self.cursor_pos = self.new_cursor_pos; - let upper_left = upper_left!(area); - let bottom_right = bottom_right!(area); + let upper_left = area.upper_left(); + let bottom_right = area.bottom_right(); if self.length == 0 && self.dirty { grid.clear_area(area, self.color_cache.theme_default); context.dirty_areas.push_back(area); diff --git a/meli/src/mail/status.rs b/meli/src/mail/status.rs index 6450a8f0..754eb8d5 100644 --- a/meli/src/mail/status.rs +++ b/meli/src/mail/status.rs @@ -22,12 +22,13 @@ use melib::{MailBackendExtensionStatus, SpecialUsageMailbox}; use super::*; +use crate::accounts::JobRequest; #[derive(Debug)] pub struct AccountStatus { cursor: (usize, usize), account_pos: usize, - content: CellBuffer, + content: Screen, dirty: bool, theme_default: ThemeAttribute, id: ComponentId, @@ -48,8 +49,10 @@ impl AccountStatus { .set_attrs(theme_default.attrs); ret }; - let mut content = CellBuffer::new(120, 5, default_cell); - content.set_growable(true); + let mut content = Screen::::new(); + content.grid_mut().default_cell = default_cell; + content.grid_mut().set_growable(true); + _ = content.resize(80, 20); AccountStatus { cursor: (0, 0), @@ -60,54 +63,51 @@ impl AccountStatus { id: ComponentId::default(), } } -} -impl Component for AccountStatus { - fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { - if !self.dirty { + fn update_content(&mut self, (width, height): (usize, usize), context: &Context) { + if !self.content.resize_with_context(width, height, context) { return; } - self.dirty = false; - let (mut width, _) = self.content.size(); let a = &context.accounts[self.account_pos]; - let (_x, _y) = self.content.write_string( + let area = self.content.area().skip_cols(1); + let (_x, _y) = self.content.grid_mut().write_string( "Account ", self.theme_default.fg, self.theme_default.bg, self.theme_default.attrs | Attr::UNDERLINE, - ((1, 0), (width - 1, 0)), + area, None, ); - let (_x, _y) = self.content.write_string( + let area = self.content.area().skip(_x + 1, _y); + let (_x, _y) = self.content.grid_mut().write_string( a.name(), self.theme_default.fg, self.theme_default.bg, Attr::BOLD | Attr::UNDERLINE, - ((_x, _y), (width - 1, _y)), + area, None, ); - width = self.content.size().0; let mut line = 2; - self.content.write_string( + let area = self.content.area().skip(1, line); + self.content.grid_mut().write_string( "In-progress jobs:", self.theme_default.fg, self.theme_default.bg, Attr::BOLD, - ((1, line), (width - 1, line)), + area, None, ); line += 2; for (job_id, req) in a.active_jobs.iter() { - width = self.content.size().0; - use crate::accounts::JobRequest; - let (x, y) = self.content.write_string( + let area = self.content.area().skip(1, line); + let (x, y) = self.content.grid_mut().write_string( &format!("{} {}", req, job_id), self.theme_default.fg, self.theme_default.bg, self.theme_default.attrs, - ((1, line), (width - 1, line)), + area, None, ); if let JobRequest::DeleteMailbox { mailbox_hash, .. } @@ -116,12 +116,13 @@ impl Component for AccountStatus { | JobRequest::Refresh { mailbox_hash, .. } | JobRequest::Fetch { mailbox_hash, .. } = req { - self.content.write_string( + let area = self.content.area().skip(x + 1, y + line); + self.content.grid_mut().write_string( a.mailbox_entries[mailbox_hash].name(), self.theme_default.fg, self.theme_default.bg, self.theme_default.attrs, - ((x + 1, y), (width - 1, y)), + area, None, ); } @@ -130,18 +131,18 @@ impl Component for AccountStatus { } line += 2; - width = self.content.size().0; - let (_x, _y) = self.content.write_string( + let area = self.content.area().skip(1, line); + let (_x, _y) = self.content.grid_mut().write_string( "Tag support: ", self.theme_default.fg, self.theme_default.bg, Attr::BOLD, - ((1, line), (width - 1, line)), + area, None, ); - width = self.content.size().0; - self.content.write_string( + let area = self.content.area().skip(_x + 1, _y + line); + self.content.grid_mut().write_string( if a.backend_capabilities.supports_tags { "yes" } else { @@ -150,21 +151,21 @@ impl Component for AccountStatus { self.theme_default.fg, self.theme_default.bg, self.theme_default.attrs, - ((_x, _y), (width - 1, line)), + area, None, ); - width = self.content.size().0; line += 1; - let (_x, _y) = self.content.write_string( + let area = self.content.area().skip(1, line); + let (_x, _y) = self.content.grid_mut().write_string( "Search backend: ", self.theme_default.fg, self.theme_default.bg, Attr::BOLD, - ((1, line), (width - 1, line)), + area, None, ); - width = self.content.size().0; - self.content.write_string( + let area = self.content.area().skip(_x + 1, _y + line); + self.content.grid_mut().write_string( &match ( a.settings.conf.search_backend(), a.backend_capabilities.supports_search, @@ -187,18 +188,18 @@ impl Component for AccountStatus { self.theme_default.fg, self.theme_default.bg, self.theme_default.attrs, - ((_x, _y), (width - 1, _y)), + area, None, ); - width = self.content.size().0; line += 1; - self.content.write_string( + let area = self.content.area().skip(1, line); + self.content.grid_mut().write_string( "Special Mailboxes:", self.theme_default.fg, self.theme_default.bg, Attr::BOLD, - ((1, line), (width - 1, line)), + area, None, ); for f in a @@ -207,38 +208,38 @@ impl Component for AccountStatus { .map(|entry| &entry.ref_mailbox) .filter(|f| f.special_usage() != SpecialUsageMailbox::Normal) { - width = self.content.size().0; line += 1; - self.content.write_string( + let area = self.content.area().skip(1, line); + self.content.grid_mut().write_string( &format!("{}: {}", f.path(), f.special_usage()), self.theme_default.fg, self.theme_default.bg, self.theme_default.attrs, - ((1, line), (width - 1, line)), + area, None, ); } line += 2; - width = self.content.size().0; - self.content.write_string( + let area = self.content.area().skip(1, line); + self.content.grid_mut().write_string( "Subscribed mailboxes:", self.theme_default.fg, self.theme_default.bg, Attr::BOLD, - ((1, line), (width - 1, line)), + area, None, ); line += 2; for mailbox_node in a.list_mailboxes() { - width = self.content.size().0; let f: &Mailbox = &a[&mailbox_node.hash].ref_mailbox; if f.is_subscribed() { - self.content.write_string( + let area = self.content.area().skip(1, line); + self.content.grid_mut().write_string( f.path(), self.theme_default.fg, self.theme_default.bg, self.theme_default.attrs, - ((1, line), (width - 1, line)), + area, None, ); line += 1; @@ -246,14 +247,14 @@ impl Component for AccountStatus { } line += 1; - width = self.content.size().0; if let Some(ref extensions) = a.backend_capabilities.extensions { - self.content.write_string( + let area = self.content.area().skip(1, line); + self.content.grid_mut().write_string( "Server Extensions:", self.theme_default.fg, self.theme_default.bg, Attr::BOLD, - ((1, line), (width - 1, line)), + area, None, ); let max_name_width = std::cmp::max( @@ -264,28 +265,27 @@ impl Component for AccountStatus { .max() .unwrap_or(0), ); - width = self.content.size().0; - self.content.write_string( + let area = self.content.area().skip(max_name_width + 6, line); + self.content.grid_mut().write_string( "meli support:", self.theme_default.fg, self.theme_default.bg, self.theme_default.attrs, - ((max_name_width + 6, line), (width - 1, line)), + area, None, ); line += 1; for (name, status) in extensions.iter() { - width = self.content.size().0; - self.content.write_string( + let area = self.content.area().skip(1, line); + self.content.grid_mut().write_string( name.trim_at_boundary(30), self.theme_default.fg, self.theme_default.bg, self.theme_default.attrs, - ((1, line), (width - 1, line)), + area, None, ); - width = self.content.size().0; let (x, y) = { let (status, color) = match status { MailBackendExtensionStatus::Unsupported { comment: _ } => { @@ -298,12 +298,13 @@ impl Component for AccountStatus { ("enabled", Color::Green) } }; - self.content.write_string( + let area = self.content.area().skip(max_name_width + 6, line); + self.content.grid_mut().write_string( status, color, self.theme_default.bg, self.theme_default.attrs, - ((max_name_width + 6, line), (width - 1, line)), + area, None, ) }; @@ -312,28 +313,44 @@ impl Component for AccountStatus { | MailBackendExtensionStatus::Supported { comment } | MailBackendExtensionStatus::Enabled { comment } => { if let Some(s) = comment { - let (x, y) = self.content.write_string( + let area = self + .content + .area() + .skip(max_name_width + 6, line) + .skip(x, y); + let (_x, _y) = self.content.grid_mut().write_string( " (", self.theme_default.fg, self.theme_default.bg, self.theme_default.attrs, - ((x, y), (width - 1, y)), + area, None, ); - let (x, y) = self.content.write_string( + let area = self + .content + .area() + .skip(max_name_width + 6, line) + .skip(x, y) + .skip(_x, _y); + let (__x, __y) = self.content.grid_mut().write_string( s, self.theme_default.fg, self.theme_default.bg, self.theme_default.attrs, - ((x, y), (width - 1, y)), + area, None, ); - self.content.write_string( + let area = self + .content + .area() + .skip(max_name_width + 6, line) + .skip(x + _x + __x, y + _y + __y); + self.content.grid_mut().write_string( ")", self.theme_default.fg, self.theme_default.bg, self.theme_default.attrs, - ((x, y), (width - 1, y)), + area, None, ); } @@ -342,11 +359,21 @@ impl Component for AccountStatus { line += 1; } } + } +} + +impl Component for AccountStatus { + fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { + if !self.dirty { + return; + } + self.dirty = false; + self.update_content(area.size(), context); /* self.content may have been resized with write_string() calls above * since it has growable set */ - let (width, height) = self.content.size(); - let (cols, rows) = (width!(area), height!(area)); + let (width, height) = self.content.area().size(); + let (cols, rows) = area.size(); self.cursor = ( std::cmp::min(width.saturating_sub(cols), self.cursor.0), std::cmp::min(height.saturating_sub(rows), self.cursor.1), @@ -354,18 +381,12 @@ impl Component for AccountStatus { grid.clear_area(area, self.theme_default); grid.copy_area( - &self.content, + self.content.grid(), area, - ( - ( - std::cmp::min((width - 1).saturating_sub(cols), self.cursor.0), - std::cmp::min((height - 1).saturating_sub(rows), self.cursor.1), - ), - ( - std::cmp::min(self.cursor.0 + cols, width - 1), - std::cmp::min(self.cursor.1 + rows, height - 1), - ), - ), + self.content + .area() + .skip(self.cursor.0, self.cursor.1) + .take(cols, rows), ); context.dirty_areas.push_back(area); } diff --git a/meli/src/mail/view/envelope.rs b/meli/src/mail/view/envelope.rs index 36ea8e81..9af9c489 100644 --- a/meli/src/mail/view/envelope.rs +++ b/meli/src/mail/view/envelope.rs @@ -711,74 +711,61 @@ impl EnvelopeView { impl Component for EnvelopeView { fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { - let upper_left = upper_left!(area); - let bottom_right = bottom_right!(area); - self.view_settings.theme_default = crate::conf::value(context, "theme_default"); - let headers = crate::conf::value(context, "mail.view.headers"); - let headers_names = crate::conf::value(context, "mail.view.headers_names"); - let headers_area = crate::conf::value(context, "mail.view.headers_area"); + let hdr_theme = crate::conf::value(context, "mail.view.headers"); + let hdr_name_theme = crate::conf::value(context, "mail.view.headers_names"); + let hdr_area_theme = crate::conf::value(context, "mail.view.headers_area"); let y: usize = { if self.mode.is_source() { grid.clear_area(area, self.view_settings.theme_default); context.dirty_areas.push_back(area); - get_y(upper_left) + 0 } else { let envelope = &self.mail; let height_p = self.pager.size().1; - let height = height!(area) + let height = area + .height() .saturating_sub(self.headers_no) .saturating_sub(1); self.headers_no = 0; let mut skip_header_ctr = self.headers_cursor; let sticky = self.view_settings.sticky_headers || height_p < height; - let (_, mut y) = upper_left; + let mut y = 0; macro_rules! print_header { ($(($header:path, $string:expr)),*$(,)?) => { $({ if sticky || skip_header_ctr == 0 { - if y <= get_y(bottom_right) { + if y <= area.height() { + grid.clear_area( + area.skip_rows(y), + hdr_area_theme, + ); let (_x, _y) = grid.write_string( &format!("{}:", $header), - headers_names.fg, - headers_names.bg, - headers_names.attrs, - (set_y(upper_left, y), bottom_right), - Some(get_x(upper_left)), + hdr_name_theme.fg, + hdr_name_theme.bg, + hdr_name_theme.attrs, + area.skip_rows(y), + Some(0), ); - if let Some(cell) = grid.get_mut(_x, _y) { - cell.set_ch(' ') - .set_fg(headers_area.fg) - .set_bg(headers_area.bg) - .set_attrs(headers_area.attrs); - } - - let (_x, _y) = + let (__x, __y) = grid.write_string( &$string, - headers.fg, - headers.bg, - headers.attrs, - ((_x + 1, _y), bottom_right), - Some(get_x(upper_left)), + hdr_theme.fg, + hdr_theme.bg, + hdr_theme.attrs, + area.skip(_x+1, y+ _y), + Some(0), ); - - grid.clear_area( - ( - (std::cmp::min(_x, get_x(bottom_right)), _y), - (get_x(bottom_right), _y), - ), - headers_area, - ); - y = _y + 1; + y += _y +__y + 1; } } else { - skip_header_ctr -= 1; + skip_header_ctr = skip_header_ctr.saturating_sub(1); } self.headers_no += 1; })+ @@ -871,34 +858,32 @@ impl Component for EnvelopeView { ref unsubscribe, }) = list_management::ListActions::detect(envelope) { - let mut x = get_x(upper_left); + let mut x = 0; if let Some(id) = id { if sticky || skip_header_ctr == 0 { - grid.clear_area( - (set_y(upper_left, y), set_y(bottom_right, y)), - headers_area, - ); + grid.clear_area(area.nth_row(y), hdr_area_theme); let (_x, _) = grid.write_string( "List-ID: ", - headers_names.fg, - headers_names.bg, - headers_names.attrs, - (set_y(upper_left, y), bottom_right), - None, - ); - let (_x, _y) = grid.write_string( - id, - headers.fg, - headers.bg, - headers.attrs, - ((_x, y), bottom_right), + hdr_name_theme.fg, + hdr_name_theme.bg, + hdr_name_theme.attrs, + area.nth_row(y), None, ); x = _x; - if _y != y { - x = get_x(upper_left); + let (_x, _y) = grid.write_string( + id, + hdr_theme.fg, + hdr_theme.bg, + hdr_theme.attrs, + area.nth_row(y).skip_cols(_x), + None, + ); + x += _x; + if _y != 0 { + x = 0; } - y = _y; + y += _y; } self.headers_no += 1; } @@ -906,71 +891,72 @@ impl Component for EnvelopeView { if archive.is_some() || post.is_some() || unsubscribe.is_some() { let (_x, _y) = grid.write_string( " Available actions: [ ", - headers_names.fg, - headers_names.bg, - headers_names.attrs, - ((x, y), bottom_right), - Some(get_x(upper_left)), + hdr_name_theme.fg, + hdr_name_theme.bg, + hdr_name_theme.attrs, + area.skip(x, y), + Some(0), ); - x = _x; - y = _y; + x += _x; + y += _y; } if archive.is_some() { let (_x, _y) = grid.write_string( "list-archive, ", - headers.fg, - headers.bg, - headers.attrs, - ((x, y), bottom_right), - Some(get_x(upper_left)), + hdr_theme.fg, + hdr_theme.bg, + hdr_theme.attrs, + area.skip(x, y), + Some(0), ); - x = _x; - y = _y; + x += _x; + y += _y; } if post.is_some() { let (_x, _y) = grid.write_string( "list-post, ", - headers.fg, - headers.bg, - headers.attrs, - ((x, y), bottom_right), - Some(get_x(upper_left)), + hdr_theme.fg, + hdr_theme.bg, + hdr_theme.attrs, + area.skip(x, y), + Some(0), ); - x = _x; - y = _y; + x += _x; + y += _y; } if unsubscribe.is_some() { let (_x, _y) = grid.write_string( "list-unsubscribe, ", - headers.fg, - headers.bg, - headers.attrs, - ((x, y), bottom_right), - Some(get_x(upper_left)), + hdr_theme.fg, + hdr_theme.bg, + hdr_theme.attrs, + area.skip(x, y), + Some(0), ); - x = _x; - y = _y; + x += _x; + y += _y; } if archive.is_some() || post.is_some() || unsubscribe.is_some() { if x >= 2 { - if let Some(cell) = grid.get_mut(x - 2, y) { - cell.set_ch(' '); + for c in grid.row_iter(area, (x - 2)..(x - 1), y) { + grid[c].set_ch(' '); } } if x > 0 { - if let Some(cell) = grid.get_mut(x - 1, y) { - cell.set_ch(']') - .set_fg(headers_names.fg) - .set_bg(headers_names.bg) - .set_attrs(headers_names.attrs); + for c in grid.row_iter(area, (x - 1)..x, y) { + grid[c] + .set_ch(']') + .set_fg(hdr_name_theme.fg) + .set_bg(hdr_name_theme.bg) + .set_attrs(hdr_name_theme.attrs); } } } - for x in x..=get_x(bottom_right) { - grid[(x, y)] + for c in grid.row_iter(area, (x + 1)..area.width(), y) { + grid[c] .set_ch(' ') - .set_fg(headers_area.fg) - .set_bg(headers_area.bg); + .set_fg(hdr_area_theme.fg) + .set_bg(hdr_area_theme.bg); } y += 1; } @@ -978,24 +964,21 @@ impl Component for EnvelopeView { self.force_draw_headers = false; - grid.clear_area((set_y(upper_left, y), set_y(bottom_right, y)), headers_area); - context.dirty_areas.push_back(( - upper_left, - set_y(bottom_right, std::cmp::min(y + 3, get_y(bottom_right))), - )); + grid.clear_area(area.nth_row(y), hdr_area_theme); + context.dirty_areas.push_back(area.take_rows(y + 3)); if !self.view_settings.sticky_headers { let height_p = self.pager.size().1; - let height = height!(area).saturating_sub(y).saturating_sub(1); + let height = area.height().saturating_sub(y).saturating_sub(1); if self.pager.cursor_pos() >= self.headers_no { - get_y(upper_left) + 0 } else if (height_p > height && self.headers_cursor < self.headers_no + 1) || self.headers_cursor == 0 || height_p < height { y + 1 } else { - get_y(upper_left) + 0 } } else { y + 1 @@ -1214,12 +1197,11 @@ impl Component for EnvelopeView { if !s.is_dirty() { s.set_dirty(true); } - s.draw(grid, (set_y(upper_left, y), bottom_right), context); + s.draw(grid, area.skip_rows(y), context); } } _ => { - self.pager - .draw(grid, (set_y(upper_left, y), bottom_right), context); + self.pager.draw(grid, area.skip_rows(y), context); } } if let ForceCharset::Dialog(ref mut s) = self.force_charset { @@ -1228,21 +1210,17 @@ impl Component for EnvelopeView { // Draw number command buffer at the bottom right corner: - let l = nth_row_area(area, height!(area)); + let l = area.nth_row(area.height()); if self.cmd_buf.is_empty() { - grid.clear_area( - (pos_inc(l.0, (width!(area).saturating_sub(8), 0)), l.1), - self.view_settings.theme_default, - ); + grid.clear_area(l.skip_cols_from_end(8), self.view_settings.theme_default); } else { let s = self.cmd_buf.to_string(); - grid.write_string( &s, self.view_settings.theme_default.fg, self.view_settings.theme_default.bg, self.view_settings.theme_default.attrs, - (pos_inc(l.0, (width!(area).saturating_sub(s.len()), 0)), l.1), + l.skip_cols_from_end(8), None, ); } @@ -1284,8 +1262,35 @@ impl Component for EnvelopeView { if sub.process_event(event, context) { return true; } - } else if self.pager.process_event(event, context) { - return true; + } else { + if !self.view_settings.sticky_headers { + let shortcuts = self.pager.shortcuts(context); + match event { + UIEvent::Input(ref key) + if shortcut!(key == shortcuts[Shortcuts::PAGER]["scroll_up"]) + && self.pager.cursor_pos() == 0 + && self.headers_cursor > 0 => + { + self.headers_cursor -= 1; + self.set_dirty(true); + return true; + } + UIEvent::Input(ref key) + if shortcut!(key == shortcuts[Shortcuts::PAGER]["scroll_down"]) + && self.headers_cursor < self.headers_no => + { + self.headers_cursor += 1; + self.set_dirty(true); + return true; + } + + _ => {} + } + } + + if self.pager.process_event(event, context) { + return true; + } } let shortcuts = &self.shortcuts(context); diff --git a/meli/src/mail/view/thread.rs b/meli/src/mail/view/thread.rs index 94ab6646..7a8d4717 100644 --- a/meli/src/mail/view/thread.rs +++ b/meli/src/mail/view/thread.rs @@ -65,7 +65,7 @@ pub struct ThreadView { horizontal: Option, movement: Option, dirty: bool, - content: CellBuffer, + content: Screen, id: ComponentId, } @@ -353,65 +353,99 @@ impl ThreadView { e.heading = string; width = cmp::max(width, e.index.0 * 4 + e.heading.grapheme_width() + 2); } + if !self.content.resize_with_context(width, height, context) { + return; + } let theme_default = crate::conf::value(context, "theme_default"); let highlight_theme = crate::conf::value(context, "highlight"); - let mut content = CellBuffer::new_with_context(width, height, None, context); if self.reversed { for (y, e) in self.entries.iter().rev().enumerate() { /* Box character drawing stuff */ - if y > 0 && content.get_mut(e.index.0 * 4, 2 * y - 1).is_some() { + if y > 0 + && self + .content + .grid_mut() + .get_mut(e.index.0 * 4, 2 * y - 1) + .is_some() + { let index = (e.index.0 * 4, 2 * y - 1); - if content[index].ch() == ' ' { + if self.content.grid()[index].ch() == ' ' { let mut ctr = 1; - while content.get(e.index.0 * 4 + ctr, 2 * y - 1).is_some() { - if content[(e.index.0 * 4 + ctr, 2 * y - 1)].ch() != ' ' { + while self + .content + .grid() + .get(e.index.0 * 4 + ctr, 2 * y - 1) + .is_some() + { + if self.content.grid()[(e.index.0 * 4 + ctr, 2 * y - 1)].ch() != ' ' { break; } set_and_join_box( - &mut content, + self.content.grid_mut(), (e.index.0 * 4 + ctr, 2 * y - 1), BoxBoundary::Horizontal, ); ctr += 1; } - set_and_join_box(&mut content, index, BoxBoundary::Horizontal); + set_and_join_box(self.content.grid_mut(), index, BoxBoundary::Horizontal); } } - content.write_string( - &e.heading, - if e.seen { - theme_default.fg - } else { - highlight_theme.fg - }, - if e.seen { - theme_default.bg - } else { - highlight_theme.bg - }, - theme_default.attrs, - ( - (e.index.0 * 4 + 1, 2 * y), - (e.index.0 * 4 + e.heading.grapheme_width() + 1, height - 1), - ), - None, - ); + { + let area = self + .content + .area() + .skip(e.index.0 * 4 + 1, 2 * y) + .take(e.heading.grapheme_width() + 1, height - 1); + self.content.grid_mut().write_string( + &e.heading, + if e.seen { + theme_default.fg + } else { + highlight_theme.fg + }, + if e.seen { + theme_default.bg + } else { + highlight_theme.bg + }, + theme_default.attrs, + area, + None, + ); + } if let Some(len) = highlight_reply_subjects[y] { let index = e.index.0 * 4 + 1 + e.heading.grapheme_width() - len; - let area = ((index, 2 * y), (width - 2, 2 * y)); - content.change_theme(area, highlight_theme); + //let area = ((index, 2 * y), (width - 2, 2 * y)); + let area = self.content.area().skip(index, 2 * y).take_rows(1); + self.content.grid_mut().change_theme(area, highlight_theme); } - set_and_join_box(&mut content, (e.index.0 * 4, 2 * y), BoxBoundary::Vertical); set_and_join_box( - &mut content, + self.content.grid_mut(), + (e.index.0 * 4, 2 * y), + BoxBoundary::Vertical, + ); + set_and_join_box( + self.content.grid_mut(), (e.index.0 * 4, 2 * y + 1), BoxBoundary::Vertical, ); for i in ((e.index.0 * 4) + 1)..width - 1 { - set_and_join_box(&mut content, (i, 2 * y + 1), BoxBoundary::Horizontal); + set_and_join_box( + self.content.grid_mut(), + (i, 2 * y + 1), + BoxBoundary::Horizontal, + ); } - set_and_join_box(&mut content, (width - 1, 2 * y), BoxBoundary::Vertical); - set_and_join_box(&mut content, (width - 1, 2 * y + 1), BoxBoundary::Vertical); + set_and_join_box( + self.content.grid_mut(), + (width - 1, 2 * y), + BoxBoundary::Vertical, + ); + set_and_join_box( + self.content.grid_mut(), + (width - 1, 2 * y + 1), + BoxBoundary::Vertical, + ); } } else { for (y, e) in self.entries.iter().enumerate() { @@ -420,71 +454,112 @@ impl ThreadView { for i in 0..e.index.0 { let att = self.indentation_colors[(i).wrapping_rem(self.indentation_colors.len())]; - content.change_theme(((x, 2 * y), (x + 3, 2 * y + 1)), att); + let area = self.content.area().skip(x, 2 * y).take(3, 1); + self.content.grid_mut().change_theme(area, att); x += 4; } - if y > 0 && content.get_mut(e.index.0 * 4, 2 * y - 1).is_some() { + if y > 0 + && self + .content + .grid_mut() + .get_mut(e.index.0 * 4, 2 * y - 1) + .is_some() + { let index = (e.index.0 * 4, 2 * y - 1); - if content[index].ch() == ' ' { + if self.content.grid()[index].ch() == ' ' { let mut ctr = 1; - content[(e.index.0 * 4, 2 * y - 1)].set_bg(theme_default.bg); - while content.get(e.index.0 * 4 + ctr, 2 * y - 1).is_some() { - content[(e.index.0 * 4 + ctr, 2 * y - 1)].set_bg(theme_default.bg); - if content[(e.index.0 * 4 + ctr, 2 * y - 1)].ch() != ' ' { + self.content.grid_mut()[(e.index.0 * 4, 2 * y - 1)] + .set_bg(theme_default.bg); + while self + .content + .grid() + .get(e.index.0 * 4 + ctr, 2 * y - 1) + .is_some() + { + self.content.grid_mut()[(e.index.0 * 4 + ctr, 2 * y - 1)] + .set_bg(theme_default.bg); + if self.content.grid()[(e.index.0 * 4 + ctr, 2 * y - 1)].ch() != ' ' { break; } set_and_join_box( - &mut content, + self.content.grid_mut(), (e.index.0 * 4 + ctr, 2 * y - 1), BoxBoundary::Horizontal, ); ctr += 1; } - set_and_join_box(&mut content, index, BoxBoundary::Horizontal); + set_and_join_box(self.content.grid_mut(), index, BoxBoundary::Horizontal); } } - content.write_string( - &e.heading, - if e.seen { - theme_default.fg - } else { - highlight_theme.fg - }, - if e.seen { - theme_default.bg - } else { - highlight_theme.bg - }, - theme_default.attrs, - ( - (e.index.0 * 4 + 1, 2 * y), - (e.index.0 * 4 + e.heading.grapheme_width() + 1, height - 1), - ), - None, - ); + { + let area = self + .content + .area() + .skip(e.index.0 * 4 + 1, 2 * y) + .take(e.heading.grapheme_width(), height - 1); + self.content.grid_mut().write_string( + &e.heading, + if e.seen { + theme_default.fg + } else { + highlight_theme.fg + }, + if e.seen { + theme_default.bg + } else { + highlight_theme.bg + }, + theme_default.attrs, + area, + None, + ); + } if highlight_reply_subjects[y].is_some() { let index = e.index.0 * 4 + 1; - let area = ((index, 2 * y), (width - 2, 2 * y)); - content.change_theme(area, highlight_theme); + let area = self + .content + .area() + .skip(index, 2 * y) + .take(width - 2, 2 * y); + self.content.grid_mut().change_theme(area, highlight_theme); } - set_and_join_box(&mut content, (e.index.0 * 4, 2 * y), BoxBoundary::Vertical); set_and_join_box( - &mut content, + self.content.grid_mut(), + (e.index.0 * 4, 2 * y), + BoxBoundary::Vertical, + ); + set_and_join_box( + self.content.grid_mut(), (e.index.0 * 4, 2 * y + 1), BoxBoundary::Vertical, ); for i in ((e.index.0 * 4) + 1)..width - 1 { - set_and_join_box(&mut content, (i, 2 * y + 1), BoxBoundary::Horizontal); + set_and_join_box( + self.content.grid_mut(), + (i, 2 * y + 1), + BoxBoundary::Horizontal, + ); } - set_and_join_box(&mut content, (width - 1, 2 * y), BoxBoundary::Vertical); - set_and_join_box(&mut content, (width - 1, 2 * y + 1), BoxBoundary::Vertical); + set_and_join_box( + self.content.grid_mut(), + (width - 1, 2 * y), + BoxBoundary::Vertical, + ); + set_and_join_box( + self.content.grid_mut(), + (width - 1, 2 * y + 1), + BoxBoundary::Vertical, + ); } for y in 0..height - 1 { - set_and_join_box(&mut content, (width - 1, y), BoxBoundary::Vertical); + set_and_join_box( + self.content.grid_mut(), + (width - 1, y), + BoxBoundary::Vertical, + ); } } - self.content = content; self.visible_entries = vec![(0..self.entries.len()).collect()]; } @@ -517,17 +592,20 @@ impl ThreadView { return; } - grid.copy_area(&self.content, dest_area, src_area); + grid.copy_area(self.content.grid(), dest_area, src_area); } fn draw_list(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { - let (upper_left, bottom_right) = area; - let (width, height) = self.content.size(); + if self.entries.is_empty() { + context.dirty_areas.push_back(area); + return; + } + let height = self.content.area().height(); if height == 0 { context.dirty_areas.push_back(area); return; } - let rows = (get_y(bottom_right) - get_y(upper_left)).wrapping_div(2); + let rows = area.height(); if rows == 0 { context.dirty_areas.push_back(area); return; @@ -576,12 +654,9 @@ impl ThreadView { let get_entry_area = |idx: usize, entries: &[ThreadEntry]| { let entries = &entries; let visual_indentation = entries[idx].index.0 * 4; - ( - (visual_indentation, 2 * idx), - ( - visual_indentation + entries[idx].heading.grapheme_width() + 1, - 2 * idx, - ), + self.content.area().skip(visual_indentation, 2 * idx).take( + visual_indentation + entries[idx].heading.grapheme_width() + 1, + 2 * idx, ) }; @@ -599,15 +674,9 @@ impl ThreadView { let idx = *v; grid.copy_area( - &self.content, - ( - pos_inc(upper_left, (0, 2 * visible_entry_counter)), // dest_area - bottom_right, - ), - ( - (0, 2 * idx), //src_area - (width - 1, 2 * idx + 1), - ), + self.content.grid(), + area.skip_rows(2 * visible_entry_counter), + self.content.area().nth_row(2 * idx), ); } /* If cursor position has changed, remove the highlight from the previous @@ -617,36 +686,20 @@ impl ThreadView { self.cursor_pos = visibles.len().saturating_sub(1); } let idx = *visibles[self.cursor_pos]; - let src_area = { get_entry_area(idx, &self.entries) }; + let src_area = get_entry_area(idx, &self.entries); let visual_indentation = self.entries[idx].indentation * 4; - let dest_area = ( - pos_inc( - upper_left, - (visual_indentation, 2 * (self.cursor_pos - top_idx)), - ), - ( - cmp::min( - get_x(bottom_right), - get_x(upper_left) - + visual_indentation - + self.entries[idx].heading.grapheme_width() - + 1, - ), - cmp::min( - get_y(bottom_right), - get_y(upper_left) + 2 * (self.cursor_pos - top_idx), - ), - ), - ); + let dest_area = area + .skip(visual_indentation, 2 * (self.cursor_pos - top_idx)) + .take( + visual_indentation + self.entries[idx].heading.grapheme_width() + 1, + 2 * (self.cursor_pos - top_idx), + ); self.highlight_line(grid, dest_area, src_area, idx, context); if rows < visibles.len() { ScrollBar::default().set_show_arrows(true).draw( grid, - ( - pos_inc(upper_left!(area), (width!(area).saturating_sub(1), 0)), - bottom_right, - ), + area.nth_col(area.width().saturating_sub(1)), context, 2 * self.cursor_pos, rows, @@ -655,14 +708,10 @@ impl ThreadView { } if 2 * top_idx + rows > 2 * visibles.len() + 1 { grid.clear_area( - ( - pos_inc(upper_left, (0, 2 * (visibles.len() - top_idx) + 1)), - bottom_right, - ), + area.skip_rows(2 * (visibles.len() - top_idx) + 1), crate::conf::value(context, "theme_default"), ); } - context.dirty_areas.push_back(area); } else { let old_cursor_pos = self.cursor_pos; self.cursor_pos = self.new_cursor_pos; @@ -672,67 +721,36 @@ impl ThreadView { self.visible_entries.iter().flat_map(|v| v.iter()).collect(); for &idx in &[old_cursor_pos, self.cursor_pos] { let entry_idx = *visibles[idx]; - let src_area = { get_entry_area(entry_idx, &self.entries) }; + let src_area = get_entry_area(entry_idx, &self.entries); let visual_indentation = self.entries[entry_idx].indentation * 4; - let dest_area = ( - pos_inc( - upper_left, - (visual_indentation, 2 * (visibles[..idx].len() - top_idx)), - ), - ( - cmp::min( - get_x(bottom_right), - get_x(upper_left) - + visual_indentation - + self.entries[entry_idx].heading.grapheme_width() - + 1, - ), - cmp::min( - get_y(bottom_right), - get_y(upper_left) + 2 * (visibles[..idx].len() - top_idx), - ), - ), - ); + let dest_area = area + .skip(visual_indentation, 2 * (visibles[..idx].len() - top_idx)) + .take( + visual_indentation + self.entries[entry_idx].heading.grapheme_width() + 1, + 2 * (visibles[..idx].len() - top_idx), + ); self.highlight_line(grid, dest_area, src_area, entry_idx, context); if rows < visibles.len() { ScrollBar::default().set_show_arrows(true).draw( grid, - ( - pos_inc(upper_left!(area), (width!(area).saturating_sub(1), 0)), - bottom_right, - ), + area.nth_col(area.width().saturating_sub(1)), context, 2 * self.cursor_pos, rows, 2 * visibles.len() + 1, ); - context.dirty_areas.push_back(( - upper_left!(area), - set_x( - bottom_right, - cmp::min(get_x(bottom_right!(area)), get_x(upper_left!(area)) + 1), - ), - )); } - - let (upper_left, bottom_right) = dest_area; - context.dirty_areas.push_back(( - upper_left, - ( - get_x(bottom_right), - cmp::min(get_y(bottom_right), get_y(upper_left) + 1), - ), - )); } } + context.dirty_areas.push_back(area); } fn draw_vert(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { - let upper_left = upper_left!(area); - - let bottom_right = bottom_right!(area); - let mid = get_x(upper_left) + self.content.size().0; + if self.entries.is_empty() { + return; + } + let mid = area.width() / 2; //get_x(upper_left) + self.content.size().0; let theme_default = crate::conf::value(context, "theme_default"); /* First draw the thread subject on the first row */ @@ -751,41 +769,26 @@ impl ThreadView { }); let envelope: EnvelopeRef = account.collection.get_env(i); - let (x, y) = grid.write_string( + let (_, y) = grid.write_string( &envelope.subject(), crate::conf::value(context, "highlight").fg, theme_default.bg, theme_default.attrs, area, - Some(get_x(upper_left)), + Some(0), ); - for x in x..=get_x(bottom_right) { - grid[(x, y)] - .set_ch(' ') - .set_fg(theme_default.fg) - .set_bg(theme_default.bg); - } - context.dirty_areas.push_back(( - upper_left, - set_y(bottom_right, std::cmp::min(get_y(bottom_right), y + 1)), - )); - if y + 1 < get_y(bottom_right) && mid < get_x(bottom_right) { - context.dirty_areas.push_back(( - (mid, y + 1), - set_x(bottom_right, std::cmp::min(get_x(bottom_right), mid)), - )); - } + context.dirty_areas.push_back(area); - grid.clear_area(((mid, y + 1), set_x(bottom_right, mid)), theme_default); + grid.clear_area(area.skip(mid, y + 1), theme_default); y + 2 } else { - get_y(upper_left) + 2 + 2 }; - let (width, height) = self.content.size(); + let (width, height) = self.content.area().size(); if height == 0 || width == 0 { return; } - for x in get_x(upper_left)..=get_x(bottom_right) { + for x in get_x(area.upper_left())..=get_x(area.bottom_right()) { set_and_join_box(grid, (x, y - 1), BoxBoundary::Horizontal); grid[(x, y - 1)] .set_fg(theme_default.fg) @@ -796,25 +799,18 @@ impl ThreadView { ThreadViewFocus::None => { self.draw_list( grid, - ( - set_y(upper_left, std::cmp::min(get_y(bottom_right), y)), - set_x(bottom_right, std::cmp::min(get_x(bottom_right), mid - 1)), - ), + area.skip_rows(y).take_cols(mid.saturating_sub(1)), context, ); - let upper_left = (mid + 1, get_y(upper_left)); self.entries[self.new_expanded_pos].mailview.draw( grid, - (upper_left, bottom_right), + area.skip_cols(mid + 1), context, ); } ThreadViewFocus::Thread => { - grid.clear_area( - ((mid + 1, get_y(upper_left) + y - 1), bottom_right), - theme_default, - ); - self.draw_list(grid, (set_y(upper_left, y), bottom_right), context); + grid.clear_area(area.skip(mid + 1, y - 1), theme_default); + self.draw_list(grid, area.skip_rows(y), context); } ThreadViewFocus::MailView => { self.entries[self.new_expanded_pos] @@ -825,9 +821,12 @@ impl ThreadView { } fn draw_horz(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { - let upper_left = upper_left!(area); - let bottom_right = bottom_right!(area); - let total_rows = height!(area); + if self.entries.is_empty() { + return; + } + let upper_left = area.upper_left(); + let bottom_right = area.bottom_right(); + let total_rows = area.height(); let pager_ratio = *mailbox_settings!( context[self.coordinates.0][&self.coordinates.1] @@ -877,10 +876,7 @@ impl ThreadView { .set_fg(theme_default.fg) .set_bg(theme_default.bg); } - context.dirty_areas.push_back(( - upper_left, - set_y(bottom_right, std::cmp::min(get_y(bottom_right), y + 2)), - )); + context.dirty_areas.push_back(area); y + 2 }; @@ -891,27 +887,17 @@ impl ThreadView { .set_bg(theme_default.bg); } - let (width, height) = self.content.size(); + let (width, height) = self.content.area().size(); if height == 0 || height == self.cursor_pos || width == 0 { return; } - grid.clear_area( - (set_y(upper_left, y), set_y(bottom_right, mid + 1)), - theme_default, - ); - let (width, height) = self.content.size(); + grid.clear_area(area.skip_rows(y).take_rows(mid + 1), theme_default); match self.focus { ThreadViewFocus::None => { - let area = ( - set_y(upper_left, y), - set_y(bottom_right, std::cmp::min(get_y(bottom_right), mid)), - ); - let upper_left = upper_left!(area); - let bottom_right = bottom_right!(area); - - let rows = (get_y(bottom_right).saturating_sub(get_y(upper_left) + 1)) / 2; + let area = area.skip_rows(y).take_rows(mid); + let rows = area.height() / 2; if rows == 0 { return; } @@ -919,20 +905,22 @@ impl ThreadView { let top_idx = page_no * rows; grid.copy_area( - &self.content, + self.content.grid(), area, - ((0, 2 * top_idx), (width - 1, height - 1)), + self.content.area().skip_rows(2 * top_idx), ); context.dirty_areas.push_back(area); } ThreadViewFocus::Thread => { - let area = ( - set_y(upper_left, std::cmp::min(y, get_y(bottom_right))), - bottom_right, - ); - let upper_left = upper_left!(area); - - let rows = (get_y(bottom_right).saturating_sub(get_y(upper_left) + 1)) / 2; + let area = { + let val = area.skip_rows(y); + if val.height() < 20 { + area + } else { + val + } + }; + let rows = area.height() / 2; if rows == 0 { return; } @@ -940,9 +928,9 @@ impl ThreadView { let top_idx = page_no * rows; grid.copy_area( - &self.content, + self.content.grid(), area, - ((0, 2 * top_idx), (width - 1, height - 1)), + self.content.area().skip_rows(2 * top_idx), ); context.dirty_areas.push_back(area); } @@ -951,41 +939,35 @@ impl ThreadView { match self.focus { ThreadViewFocus::None => { - let area = ( - set_y(upper_left, std::cmp::min(get_y(bottom_right), mid)), - set_y(bottom_right, std::cmp::min(get_y(bottom_right), mid)), - ); - context.dirty_areas.push_back(area); - for x in get_x(upper_left)..=get_x(bottom_right) { - set_and_join_box(grid, (x, mid), BoxBoundary::Horizontal); - grid[(x, mid)] - .set_fg(theme_default.fg) - .set_bg(theme_default.bg); + { + let area = { + let val = area.skip_rows(mid); + if val.height() < 20 { + area + } else { + val + } + }; + context.dirty_areas.push_back(area); + for x in get_x(area.upper_left())..=get_x(area.bottom_right()) { + set_and_join_box(grid, (x, mid), BoxBoundary::Horizontal); + grid[(x, mid)] + .set_fg(theme_default.fg) + .set_bg(theme_default.bg); + } } - let area = ( - set_y(upper_left, std::cmp::min(get_y(bottom_right), y)), - set_y(bottom_right, std::cmp::min(get_y(bottom_right), mid - 1)), - ); - self.draw_list(grid, area, context); - self.entries[self.new_expanded_pos].mailview.draw( - grid, - ( - set_y(upper_left, std::cmp::min(get_y(bottom_right), mid + 1)), - bottom_right, - ), - context, - ); + { + let area = area.skip_rows(y).take_rows(mid - 1); + self.draw_list(grid, area, context); + } + let area = area.take_rows(mid); + self.entries[self.new_expanded_pos] + .mailview + .draw(grid, area, context); } ThreadViewFocus::Thread => { self.dirty = true; - self.draw_list( - grid, - ( - set_y(upper_left, std::cmp::min(get_y(bottom_right), y)), - bottom_right, - ), - context, - ); + self.draw_list(grid, area.skip_rows(y), context); } ThreadViewFocus::MailView => { self.entries[self.new_expanded_pos] @@ -996,6 +978,9 @@ impl ThreadView { } fn recalc_visible_entries(&mut self) { + if self.entries.is_empty() { + return; + } if self .entries .iter_mut() @@ -1071,7 +1056,7 @@ impl std::fmt::Display for ThreadView { impl Component for ThreadView { fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { - let total_cols = width!(area); + let total_cols = area.width(); if self.entries.is_empty() { self.dirty = false; return; @@ -1091,7 +1076,7 @@ impl Component for ThreadView { .draw(grid, area, context); } else if self .horizontal - .unwrap_or(total_cols >= self.content.size().0 + 74) + .unwrap_or(total_cols >= self.content.area().width() + 74) { self.draw_horz(grid, area, context); } else { @@ -1101,7 +1086,7 @@ impl Component for ThreadView { } fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool { - if let UIEvent::Action(Listing(OpenInNewTab)) = event { + if let (UIEvent::Action(Listing(OpenInNewTab)), false) = (&event, self.entries.is_empty()) { /* Handle this before self.mailview does */ let mut new_tab = Self::new( self.coordinates, @@ -1121,9 +1106,10 @@ impl Component for ThreadView { if matches!( self.focus, ThreadViewFocus::None | ThreadViewFocus::MailView - ) && self.entries[self.new_expanded_pos] - .mailview - .process_event(event, context) + ) && !self.entries.is_empty() + && self.entries[self.new_expanded_pos] + .mailview + .process_event(event, context) { return true; } @@ -1220,15 +1206,20 @@ impl Component for ThreadView { UIEvent::Input(ref key) if shortcut!(key == shortcuts[Shortcuts::THREAD_VIEW]["reverse_thread_order"]) => { - self.reversed = !self.reversed; - let expanded_hash = self.entries[self.expanded_pos].msg_hash; - self.initiate(Some(expanded_hash), false, context); - self.set_dirty(true); + if !self.entries.is_empty() { + self.reversed = !self.reversed; + let expanded_hash = self.entries[self.expanded_pos].msg_hash; + self.initiate(Some(expanded_hash), false, context); + self.set_dirty(true); + } true } UIEvent::Input(ref key) if shortcut!(key == shortcuts[Shortcuts::THREAD_VIEW]["collapse_subtree"]) => { + if self.entries.is_empty() { + return true; + } let current_pos = self.current_pos(); self.entries[current_pos].hidden = !self.entries[current_pos].hidden; self.entries[current_pos].dirty = true; @@ -1317,15 +1308,21 @@ impl Component for ThreadView { fn set_dirty(&mut self, value: bool) { self.dirty = value; - self.entries[self.new_expanded_pos] - .mailview - .set_dirty(value); + if !self.entries.is_empty() { + self.entries[self.new_expanded_pos] + .mailview + .set_dirty(value); + } } fn shortcuts(&self, context: &Context) -> ShortcutMaps { - let mut map = self.entries[self.new_expanded_pos] - .mailview - .shortcuts(context); + let mut map = if !self.entries.is_empty() { + self.entries[self.new_expanded_pos] + .mailview + .shortcuts(context) + } else { + ShortcutMaps::default() + }; map.insert( Shortcuts::GENERAL, diff --git a/meli/src/mailbox_management.rs b/meli/src/mailbox_management.rs index 5b30fbcb..93fbaf40 100644 --- a/meli/src/mailbox_management.rs +++ b/meli/src/mailbox_management.rs @@ -273,7 +273,7 @@ impl MailboxManager { if idx >= self.length { continue; //bounds check } - let new_area = nth_row_area(area, idx % rows); + let new_area = area.nth_row(idx % rows); self.data_columns .draw(grid, idx, self.cursor_pos, grid.bounds_iter(new_area)); let row_attr = if highlight { @@ -295,18 +295,14 @@ impl MailboxManager { /* Page_no has changed, so draw new page */ _ = self .data_columns - .recalc_widths((width!(area), height!(area)), top_idx); + .recalc_widths((area.width(), area.height()), top_idx); grid.clear_area(area, self.theme_default); /* copy table columns */ self.data_columns .draw(grid, top_idx, self.cursor_pos, grid.bounds_iter(area)); /* highlight cursor */ - - grid.change_theme( - nth_row_area(area, self.cursor_pos % rows), - self.highlight_theme, - ); + grid.change_theme(area.nth_row(self.cursor_pos % rows), self.highlight_theme); /* clear gap if available height is more than count of entries */ if top_idx + rows > self.length { diff --git a/meli/src/state.rs b/meli/src/state.rs index 33143e30..bd541424 100644 --- a/meli/src/state.rs +++ b/meli/src/state.rs @@ -55,7 +55,7 @@ use smallvec::SmallVec; use super::*; use crate::{ jobs::JobExecutor, - terminal::{get_events, Screen}, + terminal::{get_events, Screen, Tty}, }; struct InputHandler { @@ -276,7 +276,7 @@ impl Context { /// `State` is responsible for managing the terminal and interfacing with /// `melib` pub struct State { - screen: Box, + screen: Box>, draw_rate_limit: RateLimit, child: Option, pub mode: UIMode, @@ -292,7 +292,7 @@ pub struct State { display_messages_dirty: bool, display_messages_initialised: bool, display_messages_pos: usize, - display_messages_area: Area, + //display_messages_area: Area, } #[derive(Debug)] @@ -413,20 +413,17 @@ impl State { let working = Arc::new(()); let control = Arc::downgrade(&working); + let mut screen = Box::new(Screen::::new().with_cols_and_rows(cols, rows)); + screen + .tty_mut() + .set_mouse(settings.terminal.use_mouse.is_true()) + .set_draw_fn(if settings.terminal.use_color() { + Screen::draw_horizontal_segment + } else { + Screen::draw_horizontal_segment_no_color + }); let mut s = State { - screen: Box::new(Screen { - cols, - rows, - grid: CellBuffer::new(cols, rows, Cell::with_char(' ')), - overlay_grid: CellBuffer::new(cols, rows, Cell::with_char(' ')), - mouse: settings.terminal.use_mouse.is_true(), - stdout: None, - draw_horizontal_segment_fn: if settings.terminal.use_color() { - Screen::draw_horizontal_segment - } else { - Screen::draw_horizontal_segment_no_color - }, - }), + screen, child: None, mode: UIMode::Normal, components: IndexMap::default(), @@ -440,7 +437,6 @@ impl State { display_messages_active: false, display_messages_dirty: false, display_messages_initialised: false, - display_messages_area: ((0, 0), (0, 0)), context: Box::new(Context { accounts, settings, @@ -467,8 +463,8 @@ impl State { }), }; if s.context.settings.terminal.ascii_drawing { - s.screen.grid.set_ascii_drawing(true); - s.screen.overlay_grid.set_ascii_drawing(true); + s.screen.grid_mut().set_ascii_drawing(true); + s.screen.overlay_grid_mut().set_ascii_drawing(true); } s.screen.switch_to_alternate_screen(&s.context); @@ -541,7 +537,6 @@ impl State { self.rcv_event(UIEvent::Resize); self.display_messages_dirty = true; self.display_messages_initialised = false; - self.display_messages_area = ((0, 0), (0, 0)); // Invalidate dirty areas. self.context.dirty_areas.clear(); @@ -569,259 +564,225 @@ impl State { self.display_messages_dirty = true; self.display_messages_initialised = false; self.display_messages_expiration_start = None; - areas.push(( - (0, 0), - ( - self.screen.cols.saturating_sub(1), - self.screen.rows.saturating_sub(1), - ), - )); + areas.push(self.screen.area()); } } /* Sort by x_start, ie upper_left corner's x coordinate */ - areas.sort_by(|a, b| (a.0).0.partial_cmp(&(b.0).0).unwrap()); + areas.sort_by(|a, b| a.upper_left().0.partial_cmp(&b.upper_left().0).unwrap()); if self.display_messages_active { - /* Check if any dirty area intersects with the area occupied by floating - * notification box */ - let (displ_top, displ_bot) = self.display_messages_area; - for &((top_x, top_y), (bottom_x, bottom_y)) in &areas { - self.display_messages_dirty |= !(bottom_y < displ_top.1 - || displ_bot.1 < top_y - || bottom_x < displ_top.0 - || displ_bot.0 < top_x); - } + /* Check if any dirty area intersects with the area occupied by + * floating notification box */ + //let (displ_top, displ_bot) = self.display_messages_area; + //for &((top_x, top_y), (bottom_x, bottom_y)) in &areas { + // self.display_messages_dirty |= !(bottom_y < displ_top.1 + // || displ_bot.1 < top_y + // || bottom_x < displ_top.0 + // || displ_bot.0 < top_x); + //} } /* draw each dirty area */ - let rows = self.screen.rows; + let rows = self.screen.area().height(); for y in 0..rows { let mut segment = None; - for ((x_start, y_start), (x_end, y_end)) in &areas { - if y < *y_start || y > *y_end { + for ((x_start, y_start), (x_end, y_end)) in + areas.iter().map(|a| (a.upper_left(), a.bottom_right())) + { + if y < y_start || y > y_end { continue; } if let Some((x_start, x_end)) = segment.take() { - (self.screen.draw_horizontal_segment_fn)( - &mut self.screen.grid, - self.screen.stdout.as_mut().unwrap(), - x_start, - x_end, - y, - ); + self.screen.draw(x_start, x_end, y); } match segment { ref mut s @ None => { - *s = Some((*x_start, *x_end)); + *s = Some((x_start, x_end)); } - ref mut s @ Some(_) if s.unwrap().1 < *x_start => { - (self.screen.draw_horizontal_segment_fn)( - &mut self.screen.grid, - self.screen.stdout.as_mut().unwrap(), - s.unwrap().0, - s.unwrap().1, - y, - ); - *s = Some((*x_start, *x_end)); + ref mut s @ Some(_) if s.unwrap().1 < x_start => { + self.screen.draw(s.unwrap().0, s.unwrap().1, y); + *s = Some((x_start, x_end)); } - ref mut s @ Some(_) if s.unwrap().1 < *x_end => { - (self.screen.draw_horizontal_segment_fn)( - &mut self.screen.grid, - self.screen.stdout.as_mut().unwrap(), - s.unwrap().0, - s.unwrap().1, - y, - ); - *s = Some((s.unwrap().1, *x_end)); + ref mut s @ Some(_) if s.unwrap().1 < x_end => { + self.screen.draw(s.unwrap().0, s.unwrap().1, y); + *s = Some((s.unwrap().1, x_end)); } Some((_, ref mut x)) => { - *x = *x_end; + *x = x_end; } } } if let Some((x_start, x_end)) = segment { - (self.screen.draw_horizontal_segment_fn)( - &mut self.screen.grid, - self.screen.stdout.as_mut().unwrap(), - x_start, - x_end, - y, - ); + self.screen.draw(x_start, x_end, y); } } if self.display_messages_dirty && self.display_messages_active { - if let Some(DisplayMessage { - ref timestamp, - ref msg, - .. - }) = self.display_messages.get(self.display_messages_pos) - { - if !self.display_messages_initialised { - { - /* Clear area previously occupied by floating notification box */ - let displ_area = self.display_messages_area; - for y in get_y(upper_left!(displ_area))..=get_y(bottom_right!(displ_area)) { - (self.screen.draw_horizontal_segment_fn)( - &mut self.screen.grid, - self.screen.stdout.as_mut().unwrap(), - get_x(upper_left!(displ_area)), - get_x(bottom_right!(displ_area)), - y, - ); - } - } - let noto_colors = crate::conf::value(&self.context, "status.notification"); - use crate::melib::text_processing::{Reflow, TextProcessing}; + //if let Some(DisplayMessage { + // ref timestamp, + // ref msg, + // .. + //}) = self.display_messages.get(self.display_messages_pos) + //{ + // if !self.display_messages_initialised { + // { + // /* Clear area previously occupied by floating + // * notification box */ + // //let displ_area = self.display_messages_area; + // //for y in get_y(displ_area.upper_left()).. + // // =get_y(displ_area.bottom_right()) { + // // (self.screen.tty().draw_fn())( + // // self.screen.grid_mut(), + // // self.screen.tty_mut().stdout_mut(), + // // get_x(displ_area.upper_left()), + // // get_x(displ_area.bottom_right()), + // // y, + // // ); + // //} + // } + // let noto_colors = crate::conf::value(&self.context, + // "status.notification"); use + // crate::melib::text_processing::{Reflow, TextProcessing}; - let msg_lines = msg.split_lines_reflow(Reflow::All, Some(self.screen.cols / 3)); - let width = msg_lines - .iter() - .map(|line| line.grapheme_len() + 4) - .max() - .unwrap_or(0); + // let msg_lines = + // msg.split_lines_reflow(Reflow::All, + // Some(self.screen.area().width() / 3)); let width = + // msg_lines .iter() + // .map(|line| line.grapheme_len() + 4) + // .max() + // .unwrap_or(0); - let displ_area = place_in_area( - ( - (0, 0), - ( - self.screen.cols.saturating_sub(1), - self.screen.rows.saturating_sub(1), - ), - ), - (width, std::cmp::min(self.screen.rows, msg_lines.len() + 4)), - false, - false, - ); - let box_displ_area = create_box(&mut self.screen.overlay_grid, displ_area); - for row in self.screen.overlay_grid.bounds_iter(box_displ_area) { - for c in row { - self.screen.overlay_grid[c] - .set_ch(' ') - .set_fg(noto_colors.fg) - .set_bg(noto_colors.bg) - .set_attrs(noto_colors.attrs); - } - } - let ((x, mut y), box_displ_area_bottom_right) = box_displ_area; - for line in msg_lines - .into_iter() - .chain(Some(String::new())) - .chain(Some(datetime::timestamp_to_string(*timestamp, None, false))) - { - self.screen.overlay_grid.write_string( - &line, - noto_colors.fg, - noto_colors.bg, - noto_colors.attrs, - ((x, y), box_displ_area_bottom_right), - Some(x), - ); - y += 1; - } + // let displ_area = self.screen.area().place_inside( + // ( + // width, + // std::cmp::min(self.screen.area().height(), msg_lines.len() + + // 4), ), + // false, + // false, + // ); + // /* + // let box_displ_area = create_box(&mut self.screen.overlay_grid, + // displ_area); for row in + // self.screen.overlay_grid.bounds_iter(box_displ_area) { + // for c in row { + // self.screen.overlay_grid[c] + // .set_ch(' ') + // .set_fg(noto_colors.fg) + // .set_bg(noto_colors.bg) + // .set_attrs(noto_colors.attrs); + // } + // } + // let ((x, mut y), box_displ_area_bottom_right) = box_displ_area; + // for line in msg_lines + // .into_iter() + // .chain(Some(String::new())) + // .chain(Some(datetime::timestamp_to_string(*timestamp, None, + // false))) { + // self.screen.overlay_grid.write_string( + // &line, + // noto_colors.fg, + // noto_colors.bg, + // noto_colors.attrs, + // ((x, y), box_displ_area_bottom_right), + // Some(x), + // ); + // y += 1; + // } - if self.display_messages.len() > 1 { - self.screen.overlay_grid.write_string( - &if self.display_messages_pos == 0 { - format!( - "Next: {}", - self.context.settings.shortcuts.general.info_message_next - ) - } else if self.display_messages_pos + 1 == self.display_messages.len() { - format!( - "Prev: {}", - self.context - .settings - .shortcuts - .general - .info_message_previous - ) - } else { - format!( - "Prev: {} Next: {}", - self.context - .settings - .shortcuts - .general - .info_message_previous, - self.context.settings.shortcuts.general.info_message_next - ) - }, - noto_colors.fg, - noto_colors.bg, - noto_colors.attrs, - ((x, y), box_displ_area_bottom_right), - Some(x), - ); - } - self.display_messages_area = displ_area; - } - for y in get_y(upper_left!(self.display_messages_area)) - ..=get_y(bottom_right!(self.display_messages_area)) - { - (self.screen.draw_horizontal_segment_fn)( - &mut self.screen.overlay_grid, - self.screen.stdout.as_mut().unwrap(), - get_x(upper_left!(self.display_messages_area)), - get_x(bottom_right!(self.display_messages_area)), - y, - ); - } - } + // if self.display_messages.len() > 1 { + // self.screen.overlay_grid.write_string( + // &if self.display_messages_pos == 0 { + // format!( + // "Next: {}", + // + // self.context.settings.shortcuts.general.info_message_next + // ) + // } else if self.display_messages_pos + 1 == + // self.display_messages.len() { format!( + // "Prev: {}", + // self.context + // .settings + // .shortcuts + // .general + // .info_message_previous + // ) + // } else { + // format!( + // "Prev: {} Next: {}", + // self.context + // .settings + // .shortcuts + // .general + // .info_message_previous, + // + // self.context.settings.shortcuts.general.info_message_next + // ) + // }, + // noto_colors.fg, + // noto_colors.bg, + // noto_colors.attrs, + // ((x, y), box_displ_area_bottom_right), + // Some(x), + // ); + // } + // self.display_messages_area = displ_area; + // */ + // } + // //for y in get_y(self.display_messages_area.upper_left()) + // // ..=get_y(self.display_messages_area.bottom_right()) + // //{ + // // (self.screen.tty().draw_fn())( + // // &mut self.screen.overlay_grid, + // // self.screen.tty_mut().stdout_mut(), + // // get_x(self.display_messages_area.upper_left()), + // // get_x(self.display_messages_area.bottom_right()), + // // y, + // // ); + // //} + //} self.display_messages_dirty = false; } else if self.display_messages_dirty { /* Clear area previously occupied by floating notification box */ - let displ_area = self.display_messages_area; - for y in get_y(upper_left!(displ_area))..=get_y(bottom_right!(displ_area)) { - (self.screen.draw_horizontal_segment_fn)( - &mut self.screen.grid, - self.screen.stdout.as_mut().unwrap(), - get_x(upper_left!(displ_area)), - get_x(bottom_right!(displ_area)), - y, - ); - } + //let displ_area = self.display_messages_area; + //for y in get_y(displ_area.upper_left())..=get_y(displ_area.bottom_right()) { + // (self.screen.tty().draw_fn())( + // self.screen.grid_mut(), + // self.screen.tty_mut().stdout_mut(), + // get_x(displ_area.upper_left()), + // get_x(displ_area.bottom_right()), + // y, + // ); + //} self.display_messages_dirty = false; } + if !self.overlay.is_empty() { - let area = center_area( - ( - (0, 0), - ( - self.screen.cols.saturating_sub(1), - self.screen.rows.saturating_sub(1), - ), - ), - ( - if self.screen.cols / 3 > 30 { - self.screen.cols / 3 - } else { - self.screen.cols - }, - if self.screen.rows / 5 > 10 { - self.screen.rows / 5 - } else { - self.screen.rows - }, - ), - ); - self.screen - .overlay_grid - .copy_area(&self.screen.grid, area, area); - self.overlay.get_index_mut(0).unwrap().1.draw( - &mut self.screen.overlay_grid, - area, - &mut self.context, - ); - for y in get_y(upper_left!(area))..=get_y(bottom_right!(area)) { - (self.screen.draw_horizontal_segment_fn)( - &mut self.screen.overlay_grid, - self.screen.stdout.as_mut().unwrap(), - get_x(upper_left!(area)), - get_x(bottom_right!(area)), - y, + let area: Area = self.screen.area(); + let overlay_area = area.center_inside(( + if self.screen.cols() / 3 > 30 { + self.screen.cols() / 3 + } else { + self.screen.cols() + }, + if self.screen.rows() / 5 > 10 { + self.screen.rows() / 5 + } else { + self.screen.rows() + }, + )); + { + let (grid, overlay_grid) = self.screen.grid_and_overlay_grid_mut(); + overlay_grid.copy_area(grid, area, area); + self.overlay.get_index_mut(0).unwrap().1.draw( + overlay_grid, + overlay_area, + &mut self.context, ); } + for row in self.screen.overlay_grid().bounds_iter(overlay_area) { + self.screen + .draw_overlay(row.cols().start, row.cols().end, row.row_index()); + } } self.flush(); } @@ -829,26 +790,17 @@ impl State { /// Draw the entire screen from scratch. pub fn render(&mut self) { self.screen.update_size(); - let cols = self.screen.cols; - let rows = self.screen.rows; - self.context - .dirty_areas - .push_back(((0, 0), (cols - 1, rows - 1))); + self.context.dirty_areas.push_back(self.screen.area()); self.redraw(); } pub fn draw_component(&mut self, idx: usize) { let component = &mut self.components[idx]; - let upper_left = (0, 0); - let bottom_right = (self.screen.cols - 1, self.screen.rows - 1); if component.is_dirty() { - component.draw( - &mut self.screen.grid, - (upper_left, bottom_right), - &mut self.context, - ); + let area = self.screen.area(); + component.draw(self.screen.grid_mut(), area, &mut self.context); } } @@ -1024,11 +976,9 @@ impl State { ))); } ToggleMouse => { - self.screen.mouse = !self.screen.mouse; - self.screen.set_mouse(self.screen.mouse); - self.rcv_event(UIEvent::StatusEvent(StatusEvent::SetMouse( - self.screen.mouse, - ))); + let new_val = !self.screen.tty().mouse(); + self.screen.tty_mut().set_mouse(new_val); + self.rcv_event(UIEvent::StatusEvent(StatusEvent::SetMouse(new_val))); } Quit => { self.context @@ -1061,7 +1011,7 @@ impl State { Ok(action) => { if action.needs_confirmation() { let new = Box::new(UIConfirmationDialog::new( - "You sure?", + "Are you sure?", vec![(true, "yes".to_string()), (false, "no".to_string())], true, Some(Box::new(move |id: ComponentId, result: bool| { diff --git a/meli/src/svg.rs b/meli/src/svg.rs index 9bcea757..e7df5d8d 100644 --- a/meli/src/svg.rs +++ b/meli/src/svg.rs @@ -51,7 +51,7 @@ impl Default for SVGScreenshotFilter { } impl Component for SVGScreenshotFilter { - fn draw(&mut self, _grid: &mut CellBuffer, _area: Area, context: &mut Context) { + fn draw(&mut self, _grid: &mut CellBuffer, area: Area, context: &mut Context) { if !self.save_screenshot { return; } @@ -89,7 +89,7 @@ impl Component for SVGScreenshotFilter { /* keep a map with used colors and write a stylesheet when we're done */ let mut classes: BTreeMap<(u8, u8, u8), usize> = BTreeMap::new(); - for (row_idx, row) in grid.bounds_iter(((0, 0), (width, height))).enumerate() { + for (row_idx, row) in grid.bounds_iter(area).enumerate() { text.clear(); escaped_text.clear(); /* Each row is a group element, consisting of text elements */ diff --git a/meli/src/terminal.rs b/meli/src/terminal.rs index 7ab57dab..7203318d 100644 --- a/meli/src/terminal.rs +++ b/meli/src/terminal.rs @@ -39,7 +39,7 @@ pub mod text_editing; use std::io::{BufRead, Write}; pub use braille::BraillePixelIter; -pub use screen::{Screen, StateStdout}; +pub use screen::{Area, Screen, ScreenGeneration, StateStdout, Tty, Virtual}; pub use self::{cells::*, keys::*, position::*, text_editing::*}; diff --git a/meli/src/terminal/cells.rs b/meli/src/terminal/cells.rs index 113c48e4..13562556 100644 --- a/meli/src/terminal/cells.rs +++ b/meli/src/terminal/cells.rs @@ -35,7 +35,7 @@ use melib::{ use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; use smallvec::SmallVec; -use super::{position::*, Color}; +use super::{position::*, Area, Color, ScreenGeneration}; use crate::{state::Context, ThemeAttribute}; /// In a scroll region up and down cursor movements shift the region vertically. @@ -73,6 +73,7 @@ pub struct CellBuffer { growable: bool, tag_table: HashMap, tag_associations: SmallVec<[(u64, (usize, usize)); 128]>, + pub(super) area: Area, } impl std::fmt::Debug for CellBuffer { @@ -87,25 +88,39 @@ impl std::fmt::Debug for CellBuffer { .field("growable", &self.growable) .field("tag_table", &self.tag_table) .field("tag_associations", &self.tag_associations) + .field("area", &self.area) + .field("generation", &self.area.generation()) .finish() } } impl CellBuffer { - pub const MAX_SIZE: usize = 300_000; - pub fn area(&self) -> Area { - ( - (0, 0), - (self.cols.saturating_sub(1), self.rows.saturating_sub(1)), - ) + pub const MAX_SIZE: usize = 1_000_000; + + pub fn nil(area: Area) -> Self { + Self { + cols: 0, + rows: 0, + buf: vec![], + default_cell: Cell::new_default(), + growable: false, + ascii_drawing: false, + use_color: false, + tag_table: Default::default(), + tag_associations: SmallVec::new(), + area, + } } + pub fn set_cols(&mut self, new_cols: usize) { self.cols = new_cols; } /// Constructs a new `CellBuffer` with the given number of columns and rows, /// using the given `cell` as a blank. - pub fn new(cols: usize, rows: usize, default_cell: Cell) -> Self { + pub fn new(default_cell: Cell, area: Area) -> Self { + let cols = area.width(); + let rows = area.height(); Self { cols, rows, @@ -116,15 +131,11 @@ impl CellBuffer { use_color: true, tag_table: Default::default(), tag_associations: SmallVec::new(), + area, } } - pub fn new_with_context( - cols: usize, - rows: usize, - default_cell: Option, - context: &Context, - ) -> Self { + pub fn new_with_context(default_cell: Option, area: Area, context: &Context) -> Self { let default_cell = default_cell.unwrap_or_else(|| { let mut ret = Cell::default(); let theme_default = crate::conf::value(context, "theme_default"); @@ -134,15 +145,9 @@ impl CellBuffer { ret }); Self { - cols, - rows, - buf: vec![default_cell; cols * rows], - default_cell, - growable: false, ascii_drawing: context.settings.terminal.ascii_drawing, use_color: context.settings.terminal.use_color(), - tag_table: Default::default(), - tag_associations: SmallVec::new(), + ..Self::new(default_cell, area) } } @@ -161,21 +166,45 @@ impl CellBuffer { /// Resizes `CellBuffer` to the given number of rows and columns, using the /// given `Cell` as a blank. #[must_use] - pub fn resize(&mut self, newcols: usize, newrows: usize, blank: Option) -> bool { + pub(super) fn resize_with_context( + &mut self, + newcols: usize, + newrows: usize, + context: &Context, + ) -> bool { + self.default_cell = { + let mut ret = Cell::default(); + let theme_default = crate::conf::value(context, "theme_default"); + ret.set_fg(theme_default.fg) + .set_bg(theme_default.bg) + .set_attrs(theme_default.attrs); + ret + }; + self.ascii_drawing = context.settings.terminal.ascii_drawing; + self.use_color = context.settings.terminal.use_color(); + + let newlen = newcols * newrows; + if (self.cols, self.rows) == (newcols, newrows) || newlen >= Self::MAX_SIZE { + return newlen < Self::MAX_SIZE; + } + + self.buf = vec![self.default_cell; newlen]; + self.cols = newcols; + self.rows = newrows; + true + } + + /// Resizes `CellBuffer` to the given number of rows and columns, using the + /// given `Cell` as a blank. + #[must_use] + pub(super) fn resize(&mut self, newcols: usize, newrows: usize, blank: Option) -> bool { let newlen = newcols * newrows; if (self.cols, self.rows) == (newcols, newrows) || newlen >= Self::MAX_SIZE { return newlen < Self::MAX_SIZE; } let blank = blank.unwrap_or(self.default_cell); - let mut newbuf: Vec = Vec::with_capacity(newlen); - for y in 0..newrows { - for x in 0..newcols { - let cell = self.get(x, y).unwrap_or(&blank); - newbuf.push(*cell); - } - } - self.buf = newbuf; + self.buf = vec![blank; newlen]; self.cols = newcols; self.rows = newrows; true @@ -359,33 +388,44 @@ impl CellBuffer { /// See `BoundsIterator` documentation. pub fn bounds_iter(&self, area: Area) -> BoundsIterator { + debug_assert_eq!(self.generation(), area.generation()); + 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)), + width: area.width(), + height: area.height(), + rows: std::cmp::min(self.rows.saturating_sub(1), get_y(area.upper_left())) + ..(std::cmp::min(self.rows, get_y(area.bottom_right()) + 1)), cols: ( - std::cmp::min(self.cols.saturating_sub(1), get_x(upper_left!(area))), - std::cmp::min(self.cols, get_x(bottom_right!(area)) + 1), + std::cmp::min(self.cols.saturating_sub(1), get_x(area.upper_left())), + std::cmp::min(self.cols, get_x(area.bottom_right()) + 1), ), + area, } } /// See `RowIterator` documentation. - pub fn row_iter(&self, bounds: std::ops::Range, row: usize) -> RowIterator { + pub fn row_iter( + &self, + area: Area, + bounds: std::ops::Range, + relative_row: usize, + ) -> RowIterator { + debug_assert_eq!(self.generation(), area.generation()); + if self.generation() != area.generation() { + return RowIterator::empty(self.generation()); + } + let row = area.offset().1 + relative_row; + if row < self.rows { - RowIterator { - row, - col: std::cmp::min(self.cols.saturating_sub(1), bounds.start) - ..(std::cmp::min(self.cols, bounds.end)), - _width: bounds.len(), - } + let col = std::cmp::min(self.cols.saturating_sub(1), area.offset().0 + bounds.start) + ..(std::cmp::min(self.cols, area.offset().0 + bounds.end)); + let area = area + .nth_row(relative_row) + .skip_cols(bounds.start) + .take_cols(bounds.len()); + RowIterator { row, col, area } } else { - RowIterator { - row, - col: 0..0, - _width: 0, - } + RowIterator::empty(self.generation()) } } @@ -435,207 +475,98 @@ impl CellBuffer { } } - /// Write an `&str` to a `CellBuffer` in a specified `Area` with the passed - /// colors. - pub fn write_string( - &mut self, - s: &str, - fg_color: Color, - bg_color: Color, - attrs: Attr, - area: Area, - // The left-most x coordinate. - line_break: Option, - ) -> Pos { - macro_rules! inspect_bounds { - ($grid:ident, $area:ident, $x: ident, $y: ident, $line_break:ident) => { - let bounds = $grid.size(); - let (upper_left, bottom_right) = $area; - if $x > (get_x(bottom_right)) || $x >= get_x(bounds) { - if $grid.growable { - if !$grid.resize(std::cmp::max($x + 1, $grid.cols), $grid.rows, None) { - break; - }; - } else { - $x = get_x(upper_left); - $y += 1; - if let Some(_x) = $line_break { - $x = _x; - } else { - break; - } - } - } - if $y > (get_y(bottom_right)) || $y >= get_y(bounds) { - if $grid.growable { - if !$grid.resize($grid.cols, std::cmp::max($y + 1, $grid.rows), None) { - break; - }; - } else { - return ($x, $y - 1); - } - } - }; - } - - let mut bounds = self.size(); - let upper_left = upper_left!(area); - let bottom_right = bottom_right!(area); - let (mut x, mut y) = upper_left; - if y == get_y(bounds) || x == get_x(bounds) { - if self.growable { - if !self.resize( - std::cmp::max(self.cols, x + 2), - std::cmp::max(self.rows, y + 2), - None, - ) { - return (x, y); - } - bounds = self.size(); - } else { - return (x, y); - } - } - - if y > (get_y(bottom_right)) - || x > get_x(bottom_right) - || y > get_y(bounds) - || x > get_x(bounds) - { - if self.growable { - if !self.resize( - std::cmp::max(self.cols, x + 2), - std::cmp::max(self.rows, y + 2), - None, - ) { - return (x, y); - } - } else { - log::debug!(" Invalid area with string {} and area {:?}", s, area); - return (x, y); - } - } - for c in s.chars() { - inspect_bounds!(self, area, x, y, line_break); - if c == '\r' { - continue; - } - if c == '\n' { - y += 1; - if let Some(_x) = line_break { - x = _x; - inspect_bounds!(self, area, x, y, line_break); - continue; - } else { - break; - } - } - if c == '\t' { - self[(x, y)].set_ch(' '); - x += 1; - inspect_bounds!(self, area, x, y, line_break); - self[(x, y)].set_ch(' '); - } else { - self[(x, y)].set_ch(c); - } - self[(x, y)] - .set_fg(fg_color) - .set_bg(bg_color) - .set_attrs(attrs); - - match wcwidth(u32::from(c)) { - Some(0) | None => { - /* Skip drawing zero width characters */ - self[(x, y)].empty = true; - } - Some(2) => { - /* Grapheme takes more than one column, so the next cell will be - * drawn over. Set it as empty to skip drawing it. */ - x += 1; - inspect_bounds!(self, area, x, y, line_break); - self[(x, y)] = Cell::default(); - self[(x, y)] - .set_fg(fg_color) - .set_bg(bg_color) - .set_attrs(attrs) - .set_empty(true); - } - _ => {} - } - x += 1; - } - (x, y) + #[inline(always)] + pub fn generation(&self) -> ScreenGeneration { + self.area.generation() } - pub fn copy_area_with_break(&mut self, grid_src: &Self, dest: Area, src: Area) -> Pos { - if !is_valid_area!(dest) || !is_valid_area!(src) { - log::debug!( - "BUG: Invalid areas in copy_area:\n src: {:?}\n dest: {:?}", - src, - dest - ); - return upper_left!(dest); - } - - if grid_src.is_empty() || self.is_empty() { - return upper_left!(dest); - } - - let mut ret = bottom_right!(dest); - let mut src_x = get_x(upper_left!(src)); - let mut src_y = get_y(upper_left!(src)); - - 'y_: for y in get_y(upper_left!(dest))..=get_y(bottom_right!(dest)) { - 'x_: for x in get_x(upper_left!(dest))..=get_x(bottom_right!(dest)) { - if grid_src[(src_x, src_y)].ch() == '\n' { - src_y += 1; - src_x = 0; - if src_y >= get_y(bottom_right!(src)) { - ret.1 = y; - break 'y_; - } - continue 'y_; - } - - self[(x, y)] = grid_src[(src_x, src_y)]; - src_x += 1; - if src_x >= get_x(bottom_right!(src)) { - src_y += 1; - src_x = 0; - if src_y >= get_y(bottom_right!(src)) { - //clear_area(self, ((get_x(upper_left!(dest)), y), bottom_right!(dest))); - ret.1 = y; - break 'y_; - } - break 'x_; - } + /// Completely clear an `Area` with an empty char and the terminal's default + /// colors. + pub fn clear_area(&mut self, area: Area, attributes: ThemeAttribute) { + for row in self.bounds_iter(area) { + for c in row { + self[c] = Cell::default(); + self[c] + .set_fg(attributes.fg) + .set_bg(attributes.bg) + .set_attrs(attributes.attrs); + } + } + } + + /// Change foreground and background colors in an `Area` + pub fn change_colors(&mut self, area: Area, fg_color: Color, bg_color: Color) { + if cfg!(feature = "debug-tracing") { + let bounds = self.size(); + let upper_left = area.upper_left(); + let bottom_right = area.bottom_right(); + let (x, y) = upper_left; + if y > (get_y(bottom_right)) + || x > get_x(bottom_right) + || y >= get_y(bounds) + || x >= get_x(bounds) + { + log::debug!("BUG: Invalid area in change_colors:\n area: {:?}", area); + return; + } + } + for row in self.bounds_iter(area) { + for c in row { + self[c].set_fg(fg_color).set_bg(bg_color); + } + } + } + + /// Change [`ThemeAttribute`] in an `Area` + pub fn change_theme(&mut self, area: Area, theme: ThemeAttribute) { + if cfg!(feature = "debug-tracing") { + let bounds = self.size(); + let upper_left = area.upper_left(); + let bottom_right = area.bottom_right(); + let (x, y) = upper_left; + if y > (get_y(bottom_right)) + || x > get_x(bottom_right) + || y >= get_y(bounds) + || x >= get_x(bounds) + { + log::debug!("BUG: Invalid area in change_theme:\n area: {:?}", area); + return; + } + } + for row in self.bounds_iter(area) { + for c in row { + self[c] + .set_fg(theme.fg) + .set_bg(theme.bg) + .set_attrs(theme.attrs); } } - ret } /// Copy a source `Area` to a destination. pub fn copy_area(&mut self, grid_src: &Self, dest: Area, src: Area) -> Pos { - if !is_valid_area!(dest) || !is_valid_area!(src) { + debug_assert_eq!(self.generation(), dest.generation()); + debug_assert_eq!(grid_src.generation(), src.generation()); + if self.generation() != dest.generation() || grid_src.generation() != src.generation() { log::debug!( "BUG: Invalid areas in copy_area:\n src: {:?}\n dest: {:?}", src, dest ); - return upper_left!(dest); + return dest.upper_left(); } if grid_src.is_empty() || self.is_empty() { - return upper_left!(dest); + return dest.upper_left(); } - let mut ret = bottom_right!(dest); - let mut src_x = get_x(upper_left!(src)); - let mut src_y = get_y(upper_left!(src)); + let mut ret = dest.bottom_right(); + let mut src_x = get_x(src.upper_left()); + let mut src_y = get_y(src.upper_left()); let (cols, rows) = grid_src.size(); if src_x >= cols || src_y >= rows { log::debug!("BUG: src area outside of grid_src in copy_area",); - return upper_left!(dest); + return dest.upper_left(); } let tag_associations = grid_src.tag_associations(); @@ -645,8 +576,8 @@ impl CellBuffer { .unwrap_or_else(|i| i); let mut stack: std::collections::BTreeSet<&FormatTag> = std::collections::BTreeSet::default(); - for y in get_y(upper_left!(dest))..=get_y(bottom_right!(dest)) { - 'for_x: for x in get_x(upper_left!(dest))..=get_x(bottom_right!(dest)) { + for y in get_y(dest.upper_left())..=get_y(dest.bottom_right()) { + 'for_x: for x in get_x(dest.upper_left())..=get_x(dest.bottom_right()) { let idx = grid_src.pos_to_index(src_x, src_y).unwrap(); while tag_offset < tag_associations.len() && tag_associations[tag_offset].0 <= idx { if tag_associations[tag_offset].2 { @@ -669,17 +600,15 @@ impl CellBuffer { self[(x, y)].set_keep_attrs(true); } } - if src_x >= get_x(bottom_right!(src)) { + if src_x >= get_x(src.bottom_right()) { break 'for_x; } src_x += 1; } - src_x = get_x(upper_left!(src)); + src_x = get_x(src.upper_left()); src_y += 1; - if src_y > get_y(bottom_right!(src)) { - for row in - self.bounds_iter(((get_x(upper_left!(dest)), y + 1), bottom_right!(dest))) - { + if src_y > get_y(src.bottom_right()) { + for row in self.bounds_iter(dest.skip_rows(y + 1 - get_y(dest.upper_left()))) { for c in row { self[c].set_ch(' '); } @@ -691,78 +620,135 @@ impl CellBuffer { ret } - /// Change foreground and background colors in an `Area` - pub fn change_colors(&mut self, area: Area, fg_color: Color, bg_color: Color) { - if cfg!(feature = "debug-tracing") { - let bounds = self.size(); - let upper_left = upper_left!(area); - let bottom_right = bottom_right!(area); - let (x, y) = upper_left; - if y > (get_y(bottom_right)) - || x > get_x(bottom_right) - || y >= get_y(bounds) - || x >= get_x(bounds) - { - log::debug!("BUG: Invalid area in change_colors:\n area: {:?}", area); - return; - } - if !is_valid_area!(area) { - log::debug!("BUG: Invalid area in change_colors:\n area: {:?}", area); - return; - } - } - for row in self.bounds_iter(area) { - for c in row { - self[c].set_fg(fg_color).set_bg(bg_color); - } - } - } - - /// Change [`ThemeAttribute`] in an `Area` - pub fn change_theme(&mut self, area: Area, theme: ThemeAttribute) { - if cfg!(feature = "debug-tracing") { - let bounds = self.size(); - let upper_left = upper_left!(area); - let bottom_right = bottom_right!(area); - let (x, y) = upper_left; - if y > (get_y(bottom_right)) - || x > get_x(bottom_right) - || y >= get_y(bounds) - || x >= get_x(bounds) - { - log::debug!("BUG: Invalid area in change_theme:\n area: {:?}", area); - return; - } - if !is_valid_area!(area) { - log::debug!("BUG: Invalid area in change_theme:\n area: {:?}", area); - return; - } - } - for row in self.bounds_iter(area) { - for c in row { - self[c] - .set_fg(theme.fg) - .set_bg(theme.bg) - .set_attrs(theme.attrs); - } - } - } - - /// Completely clear an `Area` with an empty char and the terminal's default + /// Write an `&str` to a `CellBuffer` in a specified `Area` with the passed /// colors. - pub fn clear_area(&mut self, area: Area, attributes: ThemeAttribute) { - if !is_valid_area!(area) { - return; + pub fn write_string( + &mut self, + s: &str, + fg_color: Color, + bg_color: Color, + attrs: Attr, + area: Area, + // The left-most x coordinate. + line_break: Option, + ) -> Pos { + debug_assert_eq!(area.generation(), self.generation()); + if area.generation() != self.generation() { + // [ref:TODO] log error + return (0, 0); } - for row in self.bounds_iter(area) { - for c in row { - self[c] = Cell::default(); - self[c] - .set_fg(attributes.fg) - .set_bg(attributes.bg) - .set_attrs(attributes.attrs); + let mut bounds = self.size(); + let upper_left = area.upper_left(); + let bottom_right = area.bottom_right(); + let (mut x, mut y) = upper_left; + if y == get_y(bounds) || x == get_x(bounds) { + if self.growable { + if !self.resize( + std::cmp::max(self.cols, x + 2), + std::cmp::max(self.rows, y + 2), + None, + ) { + return (x - upper_left.0, y - upper_left.1); + } + bounds = self.size(); + } else { + return (x - upper_left.0, y - upper_left.1); } } + + if y > (get_y(bottom_right)) + || x > get_x(bottom_right) + || y > get_y(bounds) + || x > get_x(bounds) + { + if self.growable { + if !self.resize( + std::cmp::max(self.cols, x + 2), + std::cmp::max(self.rows, y + 2), + None, + ) { + return (x - upper_left.0, y - upper_left.1); + } + } else { + log::debug!(" Invalid area with string {} and area {:?}", s, area); + return (x - upper_left.0, y - upper_left.1); + } + } + for c in s.chars() { + if c == '\r' { + continue; + } + if c == '\n' { + y += 1; + if let Some(_x) = line_break { + x = _x + get_x(upper_left); + continue; + } else { + break; + } + } + if y > get_y(bottom_right) + || x > get_x(bottom_right) + || y > get_y(bounds) + || x > get_x(bounds) + { + if let Some(_x) = line_break { + if !(y > get_y(bottom_right) || y > get_y(bounds)) { + x = _x + get_x(upper_left); + y += 1; + continue; + } + } + break; + } + if c == '\t' { + self[(x, y)].set_ch(' '); + x += 1; + if let Some(c) = self.get_mut(x, y) { + c.set_ch(' '); + } else if let Some(_x) = line_break { + if !(y > get_y(bottom_right) || y > get_y(bounds)) { + x = _x + get_x(upper_left); + y += 1; + continue; + } else { + break; + } + } + } else { + self[(x, y)].set_ch(c); + } + self[(x, y)] + .set_fg(fg_color) + .set_bg(bg_color) + .set_attrs(attrs); + + match wcwidth(u32::from(c)) { + Some(0) | None => { + /* Skip drawing zero width characters */ + self[(x, y)].empty = true; + } + Some(2) => { + /* Grapheme takes more than one column, so the next cell will be + * drawn over. Set it as empty to skip drawing it. */ + x += 1; + self[(x, y)] = Cell::default(); + self[(x, y)] + .set_fg(fg_color) + .set_bg(bg_color) + .set_attrs(attrs) + .set_empty(true); + } + _ => {} + } + x += 1; + } + (x - upper_left.0, y - upper_left.1) + } + + #[inline] + pub const fn area(&self) -> Area { + self.area } } @@ -796,14 +782,6 @@ impl IndexMut for CellBuffer { } } -impl Default for CellBuffer { - /// Constructs a new `CellBuffer` with a size of `(0, 0)`, using the default - /// `Cell` as a blank. - fn default() -> Self { - Self::new(0, 0, Cell::default()) - } -} - impl std::fmt::Display for CellBuffer { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { '_y: for y in 0..self.rows { @@ -851,7 +829,7 @@ impl Cell { /// assert_eq!(cell.bg(), Color::Green); /// assert_eq!(cell.attrs(), Attr::DEFAULT); /// ``` - pub fn new(ch: char, fg: Color, bg: Color, attrs: Attr) -> Self { + pub const fn new(ch: char, fg: Color, bg: Color, attrs: Attr) -> Self { Self { ch, fg, @@ -864,6 +842,10 @@ impl Cell { } } + pub const fn new_default() -> Self { + Self::new(' ', Color::Default, Color::Default, Attr::DEFAULT) + } + /// Creates a new `Cell` with the given `char` and default style. /// /// # Examples @@ -1279,127 +1261,137 @@ impl Attr { } } -/// Use `RowIterator` to iterate the cells of a row without the need to do any +/// Use [`RowIterator`] to iterate the cells of a row without the need to do any /// bounds checking; the iterator will simply return `None` when it reaches the -/// end of the row. `RowIterator` can be created via the `CellBuffer::row_iter` -/// method and can be returned by `BoundsIterator` which iterates each row. -/// ```no_run -/// # let mut grid = meli::CellBuffer::new(1, 1, meli::Cell::default()); -/// # let x = 0; -/// for c in grid.row_iter(x..(x + 11), 0) { -/// grid[c].set_ch('w'); +/// end of the row. [`RowIterator`] can be created via the +/// [`CellBuffer::row_iter`] method and can be returned by [`BoundsIterator`] +/// which iterates each row. +/// +/// ```rust,no_run +/// # use meli::terminal::{Screen, Virtual, Area}; +/// # let mut screen = Screen::::new(); +/// # assert!(screen.resize(120, 20)); +/// # let area = screen.area(); +/// for c in screen.grid().row_iter(area, 0..area.width(), 2) { +/// screen.grid_mut()[c].set_ch('g'); /// } /// ``` #[derive(Debug)] pub struct RowIterator { row: usize, - _width: usize, col: std::ops::Range, + area: Area, } -/// `BoundsIterator` iterates each row returning a `RowIterator`. -/// ```no_run -/// # let mut grid = meli::CellBuffer::new(1, 1, meli::Cell::default()); -/// # let area = ((0, 0), (1, 1)); -/// /* Visit each `Cell` in `area`. */ -/// for row in grid.bounds_iter(area) { +/// [`BoundsIterator`] iterates each row returning a [`RowIterator`]. +/// +/// ```rust,no_run +/// # use meli::terminal::{Screen, Virtual, Area}; +/// # let mut screen = Screen::::new(); +/// # assert!(screen.resize(120, 20)); +/// # let area = screen.area(); +/// for row in screen.grid().bounds_iter(area) { /// for c in row { -/// grid[c].set_ch('w'); +/// screen.grid_mut()[c].set_ch('g'); /// } /// } /// ``` -#[derive(Clone, Debug)] +#[derive(Clone)] pub struct BoundsIterator { rows: std::ops::Range, - pub width: usize, - pub height: usize, cols: (usize, usize), + width: usize, + height: usize, + area: Area, +} + +impl std::fmt::Debug for BoundsIterator { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + f.debug_struct(stringify!(BoundsIterator)) + .field("rows", &self.rows) + .field("cols", &self.cols) + .field("width", &self.width) + .field("height", &self.height) + .field("is_empty", &self.is_empty()) + .field("bounds_area", &self.area) + .finish() + } } impl BoundsIterator { - const EMPTY: Self = BoundsIterator { - rows: 0..0, - width: 0, - height: 0, - cols: (0, 0), - }; - + #[inline] 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)), - ), - ) + self.area } + #[inline] + pub fn width(&self) -> usize { + self.width + } + + #[inline] + pub fn height(&self) -> usize { + self.height + } + + #[inline] pub fn is_empty(&self) -> bool { - self.width == 0 || self.height == 0 || self.rows.len() == 0 + self.area.is_empty() || self.width == 0 || self.height == 0 || self.rows.len() == 0 } - pub fn add_x(&mut self, x: usize) -> Self { + pub fn add_x(&mut self, x: usize) { if x == 0 { - return Self::EMPTY; + return; } - 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 + self.width = self.width.saturating_sub(x); + self.cols.0 += x; + self.cols.0 = self.cols.0.min(self.cols.1); + self.area = self.area.skip_cols(x); } } 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 { - None - } + let row = self.rows.next()?; + let area = self.area.nth_row(0); + self.area = self.area.skip_rows(1); + self.height = self.area.height(); + Some(RowIterator { + row, + col: self.cols.0..self.cols.1, + area, + }) } } impl Iterator for RowIterator { type Item = (usize, usize); fn next(&mut self) -> Option { - if let Some(next_col) = self.col.next() { - Some((next_col, self.row)) - } else { - None - } + let x = self.col.next()?; + self.area = self.area.skip_cols(1); + Some((x, self.row)) } } impl RowIterator { - pub fn forward_col(mut self, new_val: usize) -> Self { - if self.col.start > new_val { - self - } else if self.col.end <= new_val { - self.col.start = self.col.end; - self - } else { - self.col.start = new_val; - self - } + #[inline] + pub const fn area(&self) -> Area { + self.area } - pub fn area(&self) -> Area { - ((self.col.start, self.row), (self.col.end, self.row)) + #[inline] + pub const fn row_index(&self) -> usize { + self.row + } + + pub const fn empty(generation: ScreenGeneration) -> Self { + Self { + row: 0, + col: 0..0, + area: Area::new_empty(generation), + } } } @@ -1689,11 +1681,9 @@ pub mod boundaries { /// Puts boundaries in `area`. /// Returns the inner area of the created box. pub fn create_box(grid: &mut CellBuffer, area: Area) -> Area { - if !is_valid_area!(area) { - return ((0, 0), (0, 0)); - } - let upper_left = upper_left!(area); - let bottom_right = bottom_right!(area); + debug_assert_eq!(grid.generation(), area.generation()); + let upper_left = area.upper_left(); + let bottom_right = area.bottom_right(); if !grid.ascii_drawing { for x in get_x(upper_left)..get_x(bottom_right) { @@ -1719,28 +1709,7 @@ pub mod boundaries { set_and_join_box(grid, bottom_right, BoxBoundary::Vertical); } - ( - ( - std::cmp::min( - get_x(upper_left) + 2, - std::cmp::min(get_x(upper_left) + 1, get_x(bottom_right)), - ), - std::cmp::min( - get_y(upper_left) + 2, - std::cmp::min(get_y(upper_left) + 1, get_y(bottom_right)), - ), - ), - ( - std::cmp::max( - get_x(bottom_right).saturating_sub(2), - std::cmp::max(get_x(bottom_right).saturating_sub(1), get_x(upper_left)), - ), - std::cmp::max( - get_y(bottom_right).saturating_sub(2), - std::cmp::max(get_y(bottom_right).saturating_sub(1), get_y(upper_left)), - ), - ), - ) + area.skip(1, 1).skip_rows_from_end(1).skip_cols_from_end(1) } } @@ -1833,34 +1802,96 @@ pub enum WidgetWidth { #[cfg(test)] mod tests { - use melib::text_processing::{Reflow, TextProcessing, _ALICE_CHAPTER_1}; + use crate::terminal::{Screen, Virtual}; - use super::*; + //use melib::text_processing::{Reflow, TextProcessing, _ALICE_CHAPTER_1}; #[test] fn test_cellbuffer_search() { - let lines: Vec = _ALICE_CHAPTER_1.split_lines_reflow(Reflow::All, Some(78)); - let mut buf = CellBuffer::new( - lines.iter().map(String::len).max().unwrap(), - lines.len(), - Cell::with_char(' '), - ); - let width = buf.size().0; - for (i, l) in lines.iter().enumerate() { - buf.write_string( - l, - Color::Default, - Color::Default, - Attr::DEFAULT, - ((0, i), (width.saturating_sub(1), i)), - None, - ); - } - for ind in buf.kmp_search("Alice") { - for c in &buf.cellvec()[ind..std::cmp::min(buf.cellvec().len(), ind + 25)] { - print!("{}", c.ch()); + //let lines: Vec = + // _ALICE_CHAPTER_1.split_lines_reflow(Reflow::All, Some(78)); + // let mut buf = CellBuffer::new( + // lines.iter().map(String::len).max().unwrap(), + // lines.len(), + // Cell::with_char(' '), + //); + //let width = buf.size().0; + //for (i, l) in lines.iter().enumerate() { + // buf.write_string( + // l, + // Color::Default, + // Color::Default, + // Attr::DEFAULT, + // ((0, i), (width.saturating_sub(1), i)), + // None, + // ); + //} + //for ind in buf.kmp_search("Alice") { + // for c in &buf.cellvec()[ind..std::cmp::min(buf.cellvec().len(), + // ind + 25)] { print!("{}", c.ch()); + // } + // println!(); + //} + } + + #[test] + fn test_bounds_iter() { + let mut screen = Screen::::new(); + assert!(screen.resize(120, 20)); + let area = screen.area(); + assert_eq!(area.width(), 120); + assert_eq!(area.height(), 20); + + let mut full_bounds = screen.grid().bounds_iter(area); + assert_eq!(full_bounds.area(), area); + assert_eq!(full_bounds.width(), area.width()); + assert_eq!(full_bounds.height(), area.height()); + assert!(!full_bounds.is_empty()); + + full_bounds.add_x(0); + assert_eq!(full_bounds.area(), area); + + full_bounds.add_x(1); + assert_eq!(full_bounds.area().width(), area.width() - 1); + full_bounds.add_x(area.width()); + assert_eq!(full_bounds.width(), 0); + assert_eq!(full_bounds.area().width(), 0); + + let full_bounds = screen.grid().bounds_iter(area); + let row_iters = full_bounds.into_iter().collect::>(); + assert_eq!(row_iters.len(), area.height()); + + for mid in 0..row_iters.len() { + assert_eq!(mid, row_iters[mid].row_index()); + let (left, right) = row_iters.as_slice().split_at(mid); + let mid = &right[0]; + assert!(area.contains(mid.area())); + for l in left { + assert!(area.contains(l.area())); + assert!(!mid.area().contains(l.area())); } - println!(); + for r in &right[1..] { + assert!(area.contains(r.area())); + assert!(!mid.area().contains(r.area())); + } + } + + let inner_area = area.place_inside((60, 10), true, true); + let bounds = screen.grid().bounds_iter(inner_area); + let row_iters = bounds.into_iter().collect::>(); + assert_eq!(row_iters.len(), inner_area.height()); + + for mut row in row_iters { + let row_index = row.row_index(); + assert_eq!(row.area().width(), 61); + assert_eq!(row.next(), Some((2, row_index))); + assert_eq!(row.area().width(), 60); + assert_eq!( + &row.collect::>(), + &(3..63) + .zip(std::iter::repeat(row_index)) + .collect::>() + ); } } } diff --git a/meli/src/terminal/embed.rs b/meli/src/terminal/embed.rs index 60d28933..593486c5 100644 --- a/meli/src/terminal/embed.rs +++ b/meli/src/terminal/embed.rs @@ -42,8 +42,6 @@ use nix::{ }; use smallvec::SmallVec; -use crate::terminal::position::*; - mod grid; #[cfg(not(target_os = "macos"))] @@ -54,7 +52,7 @@ use std::{ sync::{Arc, Mutex}, }; -pub use grid::{EmbedGrid, EmbedTerminal}; +pub use grid::{EmbedGrid, EmbedTerminal, ScreenBuffer}; // ioctl request code to "Make the given terminal the controlling terminal of the calling // process" use libc::TIOCSCTTY; @@ -193,7 +191,7 @@ fn forward_pty_translate_escape_codes(pty_fd: std::fs::File, grid: Arc>, pub state: State, /// (width, height) - pub terminal_size: (usize, usize), + terminal_size: (usize, usize), initialized: bool, fg_color: Color, bg_color: Color, @@ -69,7 +71,7 @@ pub struct EmbedGrid { wrap_next: bool, /// Store state in case a multi-byte character is encountered codepoints: CodepointBuf, - pub normal_screen: CellBuffer, + pub normal_screen: Box>, screen_buffer: ScreenBuffer, } @@ -177,7 +179,7 @@ impl EmbedTerminal { } } -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] enum CodepointBuf { None, TwoCodepoints(u8), @@ -187,8 +189,7 @@ enum CodepointBuf { impl EmbedGrid { pub fn new() -> Self { - let mut normal_screen = CellBuffer::default(); - normal_screen.set_growable(true); + let normal_screen = Box::new(Screen::::new()); EmbedGrid { cursor: (0, 0), scroll_region: ScrollRegion { @@ -199,7 +200,7 @@ impl EmbedGrid { }, terminal_size: (0, 0), initialized: false, - alternate_screen: CellBuffer::default(), + alternate_screen: Box::new(Screen::::new()), state: State::Normal, fg_color: Color::Default, bg_color: Color::Default, @@ -220,15 +221,15 @@ impl EmbedGrid { pub fn buffer(&self) -> &CellBuffer { match self.screen_buffer { - ScreenBuffer::Normal => &self.normal_screen, - ScreenBuffer::Alternate => &self.alternate_screen, + ScreenBuffer::Normal => self.normal_screen.grid(), + ScreenBuffer::Alternate => self.alternate_screen.grid(), } } pub fn buffer_mut(&mut self) -> &mut CellBuffer { match self.screen_buffer { - ScreenBuffer::Normal => &mut self.normal_screen, - ScreenBuffer::Alternate => &mut self.alternate_screen, + ScreenBuffer::Normal => self.normal_screen.grid_mut(), + ScreenBuffer::Alternate => self.alternate_screen.grid_mut(), } } @@ -236,30 +237,32 @@ impl EmbedGrid { if new_val == self.terminal_size && self.initialized { return; } + if !self.alternate_screen.resize(new_val.0, new_val.1) { + return; + } + _ = self.normal_screen.resize(new_val.0, new_val.1); self.initialized = true; self.scroll_region.top = 0; self.scroll_region.bottom = new_val.1.saturating_sub(1); self.terminal_size = new_val; - if !self.alternate_screen.resize(new_val.0, new_val.1, None) { - panic!( - "Terminal size too big: ({} cols, {} rows)", - new_val.0, new_val.1 - ); - } - self.alternate_screen.clear(Some(Cell::default())); - if !self.normal_screen.resize(new_val.0, new_val.1, None) { - panic!( - "Terminal size too big: ({} cols, {} rows)", - new_val.0, new_val.1 - ); - } - self.normal_screen.clear(Some(Cell::default())); self.cursor = (0, 0); self.wrap_next = false; } + pub const fn terminal_size(&self) -> (usize, usize) { + self.terminal_size + } + + pub const fn area(&self) -> Area { + match self.screen_buffer { + ScreenBuffer::Normal => self.normal_screen.area(), + _ => self.alternate_screen.area(), + } + } + pub fn process_byte(&mut self, stdin: &mut std::fs::File, byte: u8) { + let area = self.area(); let EmbedGrid { ref mut cursor, ref mut scroll_region, @@ -282,12 +285,12 @@ impl EmbedGrid { ref mut normal_screen, initialized: _, } = self; - let mut grid = normal_screen; + let mut grid = normal_screen.grid_mut(); let is_alternate = match *screen_buffer { ScreenBuffer::Normal => false, _ => { - grid = alternate_screen; + grid = alternate_screen.grid_mut(); true } }; @@ -661,19 +664,10 @@ impl EmbedGrid { /* Erase Below (default). */ grid.clear_area( - ( - ( - 0, - std::cmp::min( - cursor.1 + 1 + scroll_region.top, - terminal_size.1.saturating_sub(1), - ), - ), - ( - terminal_size.0.saturating_sub(1), - terminal_size.1.saturating_sub(1), - ), - ), + area.skip_rows(std::cmp::min( + cursor.1 + 1 + scroll_region.top, + terminal_size.1.saturating_sub(1), + )), Default::default(), ); //log::trace!("{}", EscCode::from((&(*state), byte))); @@ -759,7 +753,7 @@ impl EmbedGrid { //log::trace!("{}", EscCode::from((&(*state), byte))); grid.clear_area( - ((0, 0), pos_dec(*terminal_size, (1, 1))), + area.take_cols(terminal_size.0).take_rows(terminal_size.1), Default::default(), ); *state = State::Normal; @@ -769,19 +763,10 @@ impl EmbedGrid { /* Erase Below (default). */ grid.clear_area( - ( - ( - 0, - std::cmp::min( - cursor.1 + 1 + scroll_region.top, - terminal_size.1.saturating_sub(1), - ), - ), - ( - terminal_size.0.saturating_sub(1), - terminal_size.1.saturating_sub(1), - ), - ), + area.skip_rows(std::cmp::min( + cursor.1 + 1 + scroll_region.top, + terminal_size.1.saturating_sub(1), + )), Default::default(), ); //log::trace!("{}", EscCode::from((&(*state), byte))); @@ -792,13 +777,7 @@ impl EmbedGrid { /* Erase Above */ grid.clear_area( - ( - (0, 0), - ( - terminal_size.0.saturating_sub(1), - cursor.1.saturating_sub(1) + scroll_region.top, - ), - ), + area.take_rows(cursor.1.saturating_sub(1) + scroll_region.top), Default::default(), ); //log::trace!("{}", EscCode::from((&(*state), byte))); @@ -808,10 +787,7 @@ impl EmbedGrid { /* Erase in Display (ED), VT100. */ /* Erase All */ - grid.clear_area( - ((0, 0), pos_dec(*terminal_size, (1, 1))), - Default::default(), - ); + grid.clear_area(area, Default::default()); //log::trace!("{}", EscCode::from((&(*state), byte))); *state = State::Normal; } diff --git a/meli/src/terminal/position.rs b/meli/src/terminal/position.rs index 5ad8221f..270c6d92 100644 --- a/meli/src/terminal/position.rs +++ b/meli/src/terminal/position.rs @@ -52,132 +52,6 @@ pub fn pos_dec(p: Pos, dec: (usize, usize)) -> Pos { (p.0.saturating_sub(dec.0), p.1.saturating_sub(dec.1)) } -/// An `Area` consists of two points: the upper left and bottom right corners. -/// -/// Example: -/// ```no_run -/// let new_area = ((0, 0), (1, 1)); -/// ``` -pub type Area = (Pos, Pos); - -#[inline(always)] -pub fn skip_rows(area: Area, n: usize) -> Option { - let (upper_left, bottom_right) = area; - if upper_left.1 + n <= bottom_right.1 { - return Some((pos_inc(upper_left, (0, n)), bottom_right)); - } - None -} - -/// Get an area's height -/// -/// Example: -/// ```no_run -/// use meli::height; -/// -/// let new_area = ((0, 0), (1, 1)); -/// assert_eq!(height!(new_area), 1); -/// ``` -#[macro_export] -macro_rules! height { - ($a:expr) => { - ($crate::get_y($crate::bottom_right!($a))) - .saturating_sub($crate::get_y($crate::upper_left!($a))) - + 1 - }; -} - -/// Get an area's width -/// -/// Example: -/// ```no_run -/// use meli::width; -/// -/// let new_area = ((0, 0), (1, 1)); -/// assert_eq!(width!(new_area), 1); -/// ``` -#[macro_export] -macro_rules! width { - ($a:expr) => { - ($crate::get_x($crate::bottom_right!($a))) - .saturating_sub($crate::get_x($crate::upper_left!($a))) - + 1 - }; -} - -/// Get the upper left Position of an area -/// -/// Example: -/// ```no_run -/// use meli::upper_left; -/// -/// let new_area = ((0, 0), (1, 1)); -/// assert_eq!(upper_left!(new_area), (0, 0)); -/// ``` -#[macro_export] -macro_rules! upper_left { - ($a:expr) => { - $a.0 - }; -} - -/// Get the bottom right Position of an area -/// -/// Example: -/// ```no_run -/// use meli::bottom_right; -/// -/// let new_area = ((0, 0), (1, 1)); -/// assert_eq!(bottom_right!(new_area), (1, 1)); -/// ``` -#[macro_export] -macro_rules! bottom_right { - ($a:expr) => { - $a.1 - }; -} - -/// Check if area is valid. -/// -/// Example: -/// ```no_run -/// use meli::is_valid_area; -/// -/// let valid_area = ((0, 0), (1, 1)); -/// assert!(is_valid_area!(valid_area)); -/// -/// let invalid_area = ((2, 2), (1, 1)); -/// assert!(!is_valid_area!(invalid_area)); -/// ``` -#[macro_export] -macro_rules! is_valid_area { - ($a:expr) => {{ - let upper_left = $crate::upper_left!($a); - let bottom_right = $crate::bottom_right!($a); - !($crate::get_y(upper_left) > $crate::get_y(bottom_right) - || $crate::get_x(upper_left) > $crate::get_x(bottom_right)) - }}; -} - -/// Place box given by `(width, height)` in center of `area` -pub fn center_area(area: Area, (width, height): (usize, usize)) -> Area { - let mid_x = { std::cmp::max(width!(area) / 2, width / 2) - width / 2 }; - let mid_y = { std::cmp::max(height!(area) / 2, height / 2) - height / 2 }; - - let (upper_x, upper_y) = upper_left!(area); - let (max_x, max_y) = bottom_right!(area); - ( - ( - std::cmp::min(max_x, upper_x + mid_x), - std::cmp::min(max_y, upper_y + mid_y), - ), - ( - std::cmp::min(max_x, upper_x + mid_x + width), - std::cmp::min(max_y, upper_y + mid_y + height), - ), - ) -} - #[derive(Debug, Copy, Clone, PartialEq, Eq, Default)] pub enum Alignment { /// Stretch to fill all space if possible, center if no meaningful way to @@ -191,78 +65,3 @@ pub enum Alignment { #[default] Center, } - -/// Place given area of dimensions `(width, height)` inside `area` according to -/// given alignment -pub fn align_area( - area: Area, - (width, height): (usize, usize), - vertical_alignment: Alignment, - horizontal_alignment: Alignment, -) -> Area { - let (top_x, width) = match horizontal_alignment { - Alignment::Center => ( - { std::cmp::max(width!(area) / 2, width / 2) - width / 2 }, - width, - ), - Alignment::Start => (0, width), - Alignment::End => (width!(area).saturating_sub(width), width!(area)), - Alignment::Fill => (0, width!(area)), - }; - let (top_y, height) = match vertical_alignment { - Alignment::Center => ( - { std::cmp::max(height!(area) / 2, height / 2) - height / 2 }, - height, - ), - Alignment::Start => (0, height), - Alignment::End => (height!(area).saturating_sub(height), height!(area)), - Alignment::Fill => (0, height!(area)), - }; - - let (upper_x, upper_y) = upper_left!(area); - let (max_x, max_y) = bottom_right!(area); - ( - ( - std::cmp::min(max_x, upper_x + top_x), - std::cmp::min(max_y, upper_y + top_y), - ), - ( - std::cmp::min(max_x, upper_x + top_x + width), - std::cmp::min(max_y, upper_y + top_y + height), - ), - ) -} - -/// Place box given by `(width, height)` in corner of `area` -pub fn place_in_area(area: Area, (width, height): (usize, usize), upper: bool, left: bool) -> Area { - let (upper_x, upper_y) = upper_left!(area); - let (max_x, max_y) = bottom_right!(area); - let x = if upper { - upper_x + 2 - } else { - max_x.saturating_sub(2).saturating_sub(width) - }; - - let y = if left { - upper_y + 2 - } else { - max_y.saturating_sub(2).saturating_sub(height) - }; - - ( - (std::cmp::min(x, max_x), std::cmp::min(y, max_y)), - ( - std::cmp::min(x + width, max_x), - std::cmp::min(y + height, max_y), - ), - ) -} - -#[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)) -} diff --git a/meli/src/terminal/screen.rs b/meli/src/terminal/screen.rs index 9edc9fe2..ad4cea2d 100644 --- a/meli/src/terminal/screen.rs +++ b/meli/src/terminal/screen.rs @@ -22,112 +22,245 @@ //! Terminal grid cells, keys, colors, etc. use std::io::{BufWriter, Write}; -use melib::log; +use melib::{log, uuid}; use termion::{clear, cursor, raw::IntoRawMode, screen::AlternateScreen}; use crate::{ - cells::CellBuffer, terminal::{ - BracketModeEnd, BracketModeStart, Color, DisableMouse, DisableSGRMouse, EnableMouse, - EnableSGRMouse, RestoreWindowTitleIconFromStack, SaveWindowTitleIconToStack, + cells::CellBuffer, position::*, BracketModeEnd, BracketModeStart, Cell, Color, + DisableMouse, DisableSGRMouse, EnableMouse, EnableSGRMouse, + RestoreWindowTitleIconFromStack, SaveWindowTitleIconToStack, }, - Attr, + Attr, Context, }; -pub type StateStdout = - termion::screen::AlternateScreen>>; +pub type StateStdout = termion::screen::AlternateScreen< + termion::raw::RawTerminal>>, +>; type DrawHorizontalSegmentFn = fn(&mut CellBuffer, &mut StateStdout, usize, usize, usize) -> (); -pub struct Screen { - pub cols: usize, - pub rows: usize, - pub grid: CellBuffer, - pub overlay_grid: CellBuffer, - pub stdout: Option, - pub mouse: bool, - pub draw_horizontal_segment_fn: DrawHorizontalSegmentFn, +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug, Hash)] +#[repr(transparent)] +pub struct ScreenGeneration((u64, u64)); + +impl ScreenGeneration { + pub const NIL: ScreenGeneration = Self((0, 0)); + + #[inline] + pub fn next(self) -> Self { + Self(uuid::Uuid::new_v4().as_u64_pair()) + } } -impl Screen { - /// Switch back to the terminal's main screen (The command line the user - /// sees before opening the application) - pub fn switch_to_main_screen(&mut self) { - let mouse = self.mouse; - write!( - self.stdout.as_mut().unwrap(), - "{}{}{}{}{disable_sgr_mouse}{disable_mouse}", - termion::screen::ToMainScreen, - cursor::Show, - RestoreWindowTitleIconFromStack, - BracketModeEnd, - disable_sgr_mouse = if mouse { DisableSGRMouse.as_ref() } else { "" }, - disable_mouse = if mouse { DisableMouse.as_ref() } else { "" }, - ) - .unwrap(); - self.flush(); - self.stdout = None; +impl Default for ScreenGeneration { + fn default() -> Self { + Self::NIL + } +} + +#[derive(Clone, Copy, Debug)] +pub struct Virtual; + +pub struct Tty { + stdout: Option, + mouse: bool, + draw_horizontal_segment_fn: DrawHorizontalSegmentFn, +} + +impl Tty { + #[inline] + pub fn stdout_mut(&mut self) -> Option<&mut StateStdout> { + self.stdout.as_mut() } - pub fn switch_to_alternate_screen(&mut self, context: &crate::Context) { - let s = std::io::stdout(); - let s = BufWriter::with_capacity(240 * 80, s); - - let mut stdout = AlternateScreen::from(s.into_raw_mode().unwrap()); - - write!( - &mut stdout, - "{save_title_to_stack}{}{}{}{window_title}{}{}{enable_mouse}{enable_sgr_mouse}", - termion::screen::ToAlternateScreen, - cursor::Hide, - clear::All, - cursor::Goto(1, 1), - BracketModeStart, - save_title_to_stack = SaveWindowTitleIconToStack, - window_title = if let Some(ref title) = context.settings.terminal.window_title { - format!("\x1b]2;{}\x07", title) - } else { - String::new() - }, - enable_mouse = if self.mouse { EnableMouse.as_ref() } else { "" }, - enable_sgr_mouse = if self.mouse { - EnableSGRMouse.as_ref() - } else { - "" - }, - ) - .unwrap(); - - self.stdout = Some(stdout); - self.flush(); + #[inline] + pub fn draw_fn(&self) -> DrawHorizontalSegmentFn { + self.draw_horizontal_segment_fn } - pub fn flush(&mut self) { - if let Some(s) = self.stdout.as_mut() { - s.flush().unwrap(); + #[inline] + pub fn mouse(&self) -> bool { + self.mouse + } + + #[inline] + pub fn set_mouse(&mut self, mouse: bool) -> &mut Self { + self.mouse = mouse; + self + } + + #[inline] + pub fn set_draw_fn( + &mut self, + draw_horizontal_segment_fn: DrawHorizontalSegmentFn, + ) -> &mut Self { + self.draw_horizontal_segment_fn = draw_horizontal_segment_fn; + self + } +} + +mod private { + pub trait Sealed {} +} +impl private::Sealed for Virtual {} +impl private::Sealed for Tty {} + +pub struct Screen { + cols: usize, + rows: usize, + grid: CellBuffer, + overlay_grid: CellBuffer, + display: Display, + generation: ScreenGeneration, +} + +impl std::fmt::Debug for Screen { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + f.debug_struct(stringify!(Screen)) + .field("cols", &self.cols) + .field("rows", &self.rows) + .field("grid", &self.grid) + .field("overlay_grid", &self.overlay_grid) + .field("generation", &self.generation) + .finish() + } +} + +impl Screen { + #[inline] + pub fn init(display: D) -> Self { + let area = Area { + offset: (0, 0), + upper_left: (0, 0), + bottom_right: (0, 0), + empty: true, + canvas_cols: 0, + canvas_rows: 0, + generation: ScreenGeneration::NIL, + }; + Self { + cols: 0, + rows: 0, + grid: CellBuffer::nil(area), + overlay_grid: CellBuffer::nil(area), + display, + generation: ScreenGeneration::NIL, } } - pub fn set_mouse(&mut self, value: bool) { - if let Some(stdout) = self.stdout.as_mut() { - write!( - stdout, - "{mouse}{sgr_mouse}", - mouse = if value { - AsRef::::as_ref(&EnableMouse) - } else { - AsRef::::as_ref(&DisableMouse) - }, - sgr_mouse = if value { - AsRef::::as_ref(&EnableSGRMouse) - } else { - AsRef::::as_ref(&DisableSGRMouse) - }, - ) - .unwrap(); + #[inline] + pub fn with_cols_and_rows(mut self, cols: usize, rows: usize) -> Self { + self.generation = self.generation.next(); + self.cols = cols; + self.rows = rows; + Self { + cols, + rows, + grid: CellBuffer::new(Cell::with_char(' '), self.area()), + overlay_grid: CellBuffer::new(Cell::with_char(' '), self.area()), + ..self } - self.flush(); } + + pub const fn area(&self) -> Area { + let upper_left = (0, 0); + let bottom_right = (self.cols.saturating_sub(1), self.rows.saturating_sub(1)); + Area { + offset: upper_left, + upper_left, + bottom_right, + empty: matches!((self.cols, self.rows), (0, 0)), + canvas_cols: self.cols, + canvas_rows: self.rows, + generation: self.generation, + } + } + + #[inline] + pub fn grid(&self) -> &CellBuffer { + &self.grid + } + + #[inline] + pub fn grid_mut(&mut self) -> &mut CellBuffer { + &mut self.grid + } + + #[inline] + pub fn overlay_grid(&self) -> &CellBuffer { + &self.overlay_grid + } + + #[inline] + pub fn overlay_grid_mut(&mut self) -> &mut CellBuffer { + &mut self.overlay_grid + } + + #[inline] + pub fn grid_and_overlay_grid_mut(&mut self) -> (&mut CellBuffer, &mut CellBuffer) { + (&mut self.grid, &mut self.overlay_grid) + } + + #[inline(always)] + pub const fn generation(&self) -> ScreenGeneration { + self.generation + } + + #[inline] + pub const fn cols(&self) -> usize { + self.cols + } + + #[inline] + pub const fn rows(&self) -> usize { + self.rows + } +} + +impl Clone for Screen { + fn clone(&self) -> Self { + Self { + grid: self.grid.clone(), + overlay_grid: self.overlay_grid.clone(), + ..*self + } + } +} + +impl Screen { + #[inline] + pub fn new() -> Self { + Self::init(Tty { + stdout: None, + mouse: false, + draw_horizontal_segment_fn: Screen::draw_horizontal_segment, + }) + } + + #[inline] + pub fn with_tty(self, display: Tty) -> Self { + Self { display, ..self } + } + + #[inline] + pub fn tty(&self) -> &Tty { + &self.display + } + + #[inline] + pub fn tty_mut(&mut self) -> &mut Tty { + &mut self.display + } + + #[inline] + pub fn draw(&mut self, x_start: usize, x_end: usize, y: usize) { + let Some(stdout) = self.display.stdout.as_mut() else { + return; + }; + (self.display.draw_horizontal_segment_fn)(&mut self.grid, stdout, x_start, x_end, y); + } + /// On `SIGWNICH` the `State` redraws itself according to the new /// terminal size. pub fn update_size(&mut self) { @@ -145,15 +278,80 @@ impl Screen { termrows ); } - self.cols = termcols.unwrap_or(72) as usize; - self.rows = termrows.unwrap_or(120) as usize; - if !self.grid.resize(self.cols, self.rows, None) { - panic!( - "Terminal size too big: ({} cols, {} rows)", - self.cols, self.rows - ); + let cols = termcols.unwrap_or(72) as usize; + let rows = termrows.unwrap_or(120) as usize; + if self.grid.resize(cols, rows, None) && self.overlay_grid.resize(cols, rows, None) { + self.generation = self.generation.next(); + self.cols = cols; + self.rows = rows; + self.grid.area = self.area(); + self.overlay_grid.area = self.area(); + } else { + log::warn!("Terminal size too big: ({} cols, {} rows)", cols, rows); + } + } + + /// Switch back to the terminal's main screen (The command line the user + /// sees before opening the application) + pub fn switch_to_main_screen(&mut self) { + let Some(stdout) = self.display.stdout.as_mut() else { + return; + }; + let mouse = self.display.mouse; + write!( + stdout, + "{}{}{}{}{disable_sgr_mouse}{disable_mouse}", + termion::screen::ToMainScreen, + cursor::Show, + RestoreWindowTitleIconFromStack, + BracketModeEnd, + disable_sgr_mouse = if mouse { DisableSGRMouse.as_ref() } else { "" }, + disable_mouse = if mouse { DisableMouse.as_ref() } else { "" }, + ) + .unwrap(); + self.flush(); + self.display.stdout = None; + } + + pub fn switch_to_alternate_screen(&mut self, context: &crate::Context) { + let mut stdout = BufWriter::with_capacity(240 * 80, Box::new(std::io::stdout()) as _); + + write!( + &mut stdout, + "{save_title_to_stack}{}{}{}{window_title}{}{}{enable_mouse}{enable_sgr_mouse}", + termion::screen::ToAlternateScreen, + cursor::Hide, + clear::All, + cursor::Goto(1, 1), + BracketModeStart, + save_title_to_stack = SaveWindowTitleIconToStack, + window_title = if let Some(ref title) = context.settings.terminal.window_title { + format!("\x1b]2;{}\x07", title) + } else { + String::new() + }, + enable_mouse = if self.display.mouse { + EnableMouse.as_ref() + } else { + "" + }, + enable_sgr_mouse = if self.display.mouse { + EnableSGRMouse.as_ref() + } else { + "" + }, + ) + .unwrap(); + + self.display.stdout = Some(AlternateScreen::from(stdout.into_raw_mode().unwrap())); + self.flush(); + } + + #[inline] + pub fn flush(&mut self) { + if let Some(stdout) = self.display.stdout.as_mut() { + stdout.flush().unwrap(); } - let _ = self.overlay_grid.resize(self.cols, self.rows, None); } /// Draw only a specific `area` on the screen. @@ -221,3 +419,751 @@ impl Screen { } } } + +impl Default for Screen { + fn default() -> Self { + Self::new() + } +} + +impl Screen { + #[inline] + pub fn new() -> Self { + Self::init(Virtual) + } + + #[must_use] + pub fn resize(&mut self, cols: usize, rows: usize) -> bool { + if self.grid.resize(cols, rows, None) && self.overlay_grid.resize(cols, rows, None) { + self.generation = self.generation.next(); + self.cols = cols; + self.rows = rows; + self.grid.area = self.area(); + self.overlay_grid.area = self.area(); + return true; + } + + false + } + + #[must_use] + pub fn resize_with_context(&mut self, cols: usize, rows: usize, context: &Context) -> bool { + if self.grid.resize_with_context(cols, rows, context) + && self.overlay_grid.resize_with_context(cols, rows, context) + { + self.generation = self.generation.next(); + self.cols = cols; + self.rows = rows; + self.grid.area = self.area(); + self.overlay_grid.area = self.area(); + return true; + } + + false + } +} + +/// An `Area` consists of two points: the upper left and bottom right corners. +#[derive(Clone, Copy, Eq, PartialEq, Hash)] +pub struct Area { + offset: Pos, + upper_left: Pos, + bottom_right: Pos, + empty: bool, + canvas_cols: usize, + canvas_rows: usize, + generation: ScreenGeneration, +} + +impl std::fmt::Debug for Area { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + f.debug_struct(stringify!(Area)) + .field("width", &self.width()) + .field("height", &self.height()) + .field("offset", &self.offset) + .field("upper_left", &self.upper_left) + .field("bottom_right", &self.bottom_right) + .field("empty flag", &self.empty) + .field("is_empty", &self.is_empty()) + .field("canvas_cols", &self.canvas_cols) + .field("canvas_rows", &self.canvas_rows) + .field("generation", &self.generation) + .finish() + } +} + +impl From<&Screen> for Area { + fn from(sc: &Screen) -> Self { + sc.area() + } +} + +impl Area { + #[inline] + pub fn height(&self) -> usize { + if self.is_empty() { + return 0; + } + get_y(self.bottom_right).saturating_sub(get_y(self.upper_left)) + 1 + } + + #[inline] + pub fn width(&self) -> usize { + if self.is_empty() { + return 0; + } + get_x(self.bottom_right).saturating_sub(get_x(self.upper_left)) + 1 + } + + #[inline] + pub fn size(&self) -> (usize, usize) { + (self.width(), self.height()) + } + + /// Get `n`th row of `area` or its last one. + #[inline] + pub fn nth_row(&self, n: usize) -> Self { + let Self { + offset, + upper_left, + bottom_right, + empty, + canvas_cols, + canvas_rows, + generation, + } = *self; + let (_, max_y) = bottom_right; + let n = std::cmp::min(n, self.height()); + if self.is_empty() || max_y < (get_y(upper_left) + n) { + return self.into_empty(); + } + let y = std::cmp::min(max_y, get_y(upper_left) + n); + Self { + offset: pos_inc(offset, (0, n)), + upper_left: set_y(upper_left, y), + bottom_right: set_y(bottom_right, y), + empty, + canvas_cols, + canvas_rows, + generation, + } + } + + /// Get `n`th col of `area` or its last one. + #[inline] + pub fn nth_col(&self, n: usize) -> Self { + let Self { + offset, + upper_left, + bottom_right, + empty, + canvas_cols, + canvas_rows, + generation, + } = *self; + let (max_x, _) = bottom_right; + let n = std::cmp::min(n, self.width()); + if self.is_empty() || max_x < (get_x(upper_left) + n) { + return self.into_empty(); + } + let x = std::cmp::min(max_x, get_x(upper_left) + n); + Self { + offset: pos_inc(offset, (x, 0)), + upper_left: set_x(upper_left, x), + bottom_right: set_x(bottom_right, x), + empty, + canvas_cols, + canvas_rows, + generation, + } + } + + /// Place box given by `(width, height)` in corner of `area` + pub fn place_inside(&self, (width, height): (usize, usize), upper: bool, left: bool) -> Self { + if self.is_empty() || width < 3 || height < 3 { + return *self; + } + let (upper_x, upper_y) = self.upper_left; + let (max_x, max_y) = self.bottom_right; + let x = if upper { + upper_x + 2 + } else { + max_x.saturating_sub(2).saturating_sub(width) + }; + + let y = if left { + upper_y + 2 + } else { + max_y.saturating_sub(2).saturating_sub(height) + }; + let upper_left = (std::cmp::min(x, max_x), std::cmp::min(y, max_y)); + let bottom_right = ( + std::cmp::min(x + width, max_x), + std::cmp::min(y + height, max_y), + ); + + Self { + offset: pos_inc( + self.offset, + ( + (get_x(upper_left) - get_x(self.upper_left)), + (get_y(upper_left) - get_y(self.upper_left)), + ), + ), + upper_left, + bottom_right, + empty: self.empty, + canvas_cols: self.canvas_cols, + canvas_rows: self.canvas_rows, + generation: self.generation, + } + } + + /// Place given area of dimensions `(width, height)` inside `area` according + /// to given alignment + pub fn align_inside( + &self, + (width, height): (usize, usize), + horizontal_alignment: Alignment, + vertical_alignment: Alignment, + ) -> Self { + if self.is_empty() || width == 0 || height == 0 { + return *self; + } + let (top_x, width) = match horizontal_alignment { + Alignment::Center => ( + { std::cmp::max(self.width() / 2, width / 2) - width / 2 }, + width, + ), + Alignment::Start => (0, self.width().min(width)), + Alignment::End => (self.width().saturating_sub(width), self.width().min(width)), + Alignment::Fill => (0, self.width()), + }; + let (top_y, height) = match vertical_alignment { + Alignment::Center => ( + { std::cmp::max(self.height() / 2, height / 2) - height / 2 }, + self.height().min(height), + ), + Alignment::Start => (0, self.height().min(height)), + Alignment::End => (self.height().saturating_sub(height), self.height()), + Alignment::Fill => (0, self.height()), + }; + + self.skip(top_x, top_y).take(width, height) + } + + /// Place box given by `dimensions` in center of `area` + #[inline] + pub fn center_inside(&self, dimensions: (usize, usize)) -> Self { + self.align_inside(dimensions, Alignment::Center, Alignment::Center) + } + + #[inline] + pub fn contains(&self, other: Self) -> bool { + debug_assert_eq!(self.generation, other.generation); + if self.is_empty() { + return false; + } else if other.is_empty() { + return true; + } + get_y(other.bottom_right) <= get_y(self.bottom_right) + && get_x(other.upper_left) >= get_x(self.upper_left) + && get_y(other.upper_left) >= get_y(self.upper_left) + && get_x(other.bottom_right) <= get_x(self.bottom_right) + } + + /// Skip `n` rows and return the remaining area. + /// Return value will be an empty area if `n` is more than the height. + /// + /// # Examples + /// + /// ```rust + /// # use meli::terminal::{Screen, Virtual, Area}; + /// # let mut screen = Screen::::new(); + /// # assert!(screen.resize(120, 20)); + /// # let area = screen.area(); + /// assert_eq!(area.width(), 120); + /// assert_eq!(area.height(), 20); + /// // Skip first two rows: + /// let body = area.skip_rows(2); + /// assert_eq!(body.height(), 18); + /// ``` + #[inline] + pub fn skip_rows(&self, n: usize) -> Self { + let n = std::cmp::min(n, self.height()); + if self.is_empty() || self.upper_left.1 + n > self.bottom_right.1 { + return self.into_empty(); + } + + Self { + offset: pos_inc(self.offset, (0, n)), + upper_left: pos_inc(self.upper_left, (0, n)), + ..*self + } + } + + /// Skip the last `n` rows and return the remaining area. + /// Return value will be an empty area if `n` is more than the height. + /// + /// # Examples + /// + /// ```rust + /// # use meli::terminal::{Screen, Virtual, Area}; + /// # let mut screen = Screen::::new(); + /// # assert!(screen.resize(120, 20)); + /// # let area = screen.area(); + /// assert_eq!(area.width(), 120); + /// assert_eq!(area.height(), 20); + /// // Take only first two rows (equivalent to area.take_rows(2)) + /// let header = area.skip_rows_from_end(18); + /// assert_eq!(header.height(), 2); + /// assert_eq!(header, area.take_rows(2)); + /// ``` + #[inline] + pub fn skip_rows_from_end(&self, n: usize) -> Self { + let n = std::cmp::min(n, self.height()); + if self.is_empty() || self.bottom_right.1 < n { + return self.into_empty(); + } + + Self { + bottom_right: (self.bottom_right.0, self.bottom_right.1 - n), + ..*self + } + } + + /// Skip the first `n` rows and return the remaining area. + /// Return value will be an empty area if `n` is more than the width. + /// + /// # Examples + /// + /// ```rust + /// # use meli::terminal::{Screen, Virtual, Area}; + /// # let mut screen = Screen::::new(); + /// # assert!(screen.resize(120, 20)); + /// # let area = screen.area(); + /// assert_eq!(area.width(), 120); + /// assert_eq!(area.height(), 20); + /// // Skip first two columns + /// let indent = area.skip_cols(2); + /// assert_eq!(indent.width(), 118); + /// ``` + #[inline] + pub fn skip_cols(&self, n: usize) -> Self { + let n = std::cmp::min(n, self.width()); + if self.is_empty() || self.bottom_right.0 < self.upper_left.0 + n { + return self.into_empty(); + } + + Self { + offset: pos_inc(self.offset, (n, 0)), + upper_left: pos_inc(self.upper_left, (n, 0)), + ..*self + } + } + + /// Skip the last `n` rows and return the remaining area. + /// Return value will be an empty area if `n` is more than the width. + /// + /// # Examples + /// + /// ```rust + /// # use meli::terminal::{Screen, Virtual, Area}; + /// # let mut screen = Screen::::new(); + /// # assert!(screen.resize(120, 20)); + /// # let area = screen.area(); + /// assert_eq!(area.width(), 120); + /// assert_eq!(area.height(), 20); + /// // Skip last two columns + /// let indent = area.skip_cols_from_end(2); + /// assert_eq!(indent.width(), 118); + /// assert_eq!(indent, area.take_cols(118)); + /// ``` + #[inline] + pub fn skip_cols_from_end(&self, n: usize) -> Self { + let n = std::cmp::min(n, self.width()); + if self.is_empty() || self.bottom_right.0 < n { + return self.into_empty(); + } + Self { + bottom_right: (self.bottom_right.0 - n, self.bottom_right.1), + ..*self + } + } + + /// Shortcut for using `Area::skip_cols` and `Area::skip_rows` together. + #[inline] + pub fn skip(&self, n_cols: usize, n_rows: usize) -> Self { + self.skip_cols(n_cols).skip_rows(n_rows) + } + + /// Take the first `n` rows and return the remaining area. + /// Return value will be an empty area if `n` is more than the height. + /// + /// # Examples + /// + /// ```rust + /// # use meli::terminal::{Screen, Virtual, Area}; + /// # let mut screen = Screen::::new(); + /// # assert!(screen.resize(120, 20)); + /// # let area = screen.area(); + /// assert_eq!(area.width(), 120); + /// assert_eq!(area.height(), 20); + /// // Take only first two rows + /// let header = area.take_rows(2); + /// assert_eq!(header.height(), 2); + /// ``` + #[inline] + pub fn take_rows(&self, n: usize) -> Self { + let n = std::cmp::min(n, self.height()); + if self.is_empty() || self.bottom_right.1 < (self.height() - n) { + return self.into_empty(); + } + + Self { + bottom_right: ( + self.bottom_right.0, + self.bottom_right.1 - (self.height() - n), + ), + ..*self + } + } + + /// Take the first `n` columns and return the remaining area. + /// Return value will be an empty area if `n` is more than the width. + /// + /// # Examples + /// + /// ```rust + /// # use meli::terminal::{Screen, Virtual, Area}; + /// # let mut screen = Screen::::new(); + /// # assert!(screen.resize(120, 20)); + /// # let area = screen.area(); + /// assert_eq!(area.width(), 120); + /// assert_eq!(area.height(), 20); + /// // Take only first two columns + /// let header = area.take_cols(2); + /// assert_eq!(header.width(), 2); + /// ``` + #[inline] + pub fn take_cols(&self, n: usize) -> Self { + let n = std::cmp::min(n, self.width()); + if self.is_empty() || self.bottom_right.0 < (self.width() - n) { + return self.into_empty(); + } + + Self { + bottom_right: ( + self.bottom_right.0 - (self.width() - n), + self.bottom_right.1, + ), + ..*self + } + } + + /// Shortcut for using `Area::take_cols` and `Area::take_rows` together. + #[inline] + pub fn take(&self, n_cols: usize, n_rows: usize) -> Self { + self.take_cols(n_cols).take_rows(n_rows) + } + + #[inline] + pub const fn upper_left(&self) -> Pos { + self.upper_left + } + + #[inline] + pub const fn bottom_right(&self) -> Pos { + self.bottom_right + } + + #[inline] + pub const fn offset(&self) -> Pos { + self.offset + } + + #[inline] + pub const fn generation(&self) -> ScreenGeneration { + self.generation + } + + #[inline] + pub const fn new_empty(generation: ScreenGeneration) -> Self { + Self { + offset: (0, 0), + upper_left: (0, 0), + bottom_right: (0, 0), + canvas_rows: 0, + canvas_cols: 0, + empty: true, + generation, + } + } + + #[inline] + pub const fn into_empty(self) -> Self { + Self { + offset: (0, 0), + upper_left: (0, 0), + bottom_right: (0, 0), + empty: true, + ..self + } + } + + #[inline] + pub const fn is_empty(&self) -> bool { + self.empty + || (self.upper_left.0 > self.bottom_right.0 || self.upper_left.1 > self.bottom_right.1) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_skip_rows() { + let mut screen = Screen::::new(); + assert!(screen.resize(120, 20)); + let area = screen.area(); + assert_eq!(area.width(), 120); + assert_eq!(area.height(), 20); + + for i in 1..=area.height() { + assert_eq!(area.skip_rows(i).height(), area.height() - i); + assert!(area.contains(area.skip_rows(i))); + if i < area.height() { + assert!(!area.take_rows(i).contains(area.skip_rows(i))); + } else { + assert!(area.take_rows(i).contains(area.skip_rows(i))); + } + } + + assert!(area.skip_rows(area.height()).is_empty()); + assert_eq!(area.skip_rows(0), area); + } + + #[test] + fn test_skip_rows_from_end() { + let mut screen = Screen::::new(); + assert!(screen.resize(120, 20)); + let area = screen.area(); + assert_eq!(area.width(), 120); + assert_eq!(area.height(), 20); + + for i in 1..=area.height() { + assert_eq!(area.skip_rows_from_end(i).height(), area.height() - i); + assert!(area.contains(area.skip_rows_from_end(i))); + } + + assert!(area.skip_rows_from_end(area.height()).is_empty()); + assert_eq!(area.skip_rows_from_end(0), area); + } + + #[test] + fn test_skip_cols() { + let mut screen = Screen::::new(); + assert!(screen.resize(120, 20)); + let area = screen.area(); + assert_eq!(area.width(), 120); + assert_eq!(area.height(), 20); + + for i in 1..=area.width() { + assert_eq!(area.skip_cols(i).width(), area.width() - i); + assert!(area.contains(area.skip_cols(i))); + if i < area.width() { + assert!(!area.take_cols(i).contains(area.skip_cols(i))); + } else { + assert!(area.take_cols(i).contains(area.skip_cols(i))); + } + } + + assert!(area.skip_cols(area.width()).is_empty()); + assert_eq!(area.skip_cols(0), area); + } + + #[test] + fn test_skip_cols_from_end() { + let mut screen = Screen::::new(); + assert!(screen.resize(120, 20)); + let area = screen.area(); + assert_eq!(area.width(), 120); + assert_eq!(area.height(), 20); + + for i in 1..=area.width() { + assert_eq!(area.skip_cols_from_end(i).width(), area.width() - i); + assert!(area.contains(area.skip_cols_from_end(i))); + } + + assert!(area.skip_cols_from_end(area.width()).is_empty()); + assert_eq!(area.skip_cols_from_end(0), area); + } + + #[test] + fn test_take_rows() { + let mut screen = Screen::::new(); + assert!(screen.resize(120, 20)); + let area = screen.area(); + assert_eq!(area.width(), 120); + assert_eq!(area.height(), 20); + + for i in 1..=area.height() { + assert_eq!(area.take_rows(i).height(), i); + assert!(area.contains(area.take_rows(i))); + } + + assert!(area.take_rows(0).is_empty()); + assert_eq!(area.take_rows(area.height()), area); + } + + #[test] + fn test_take_cols() { + let mut screen = Screen::::new(); + assert!(screen.resize(120, 20)); + let area = screen.area(); + assert_eq!(area.width(), 120); + assert_eq!(area.height(), 20); + + for i in 1..=area.width() { + assert_eq!(area.take_cols(i).width(), i); + assert!(area.contains(area.take_cols(i))); + } + + assert!(area.take_cols(0).is_empty()); + assert_eq!(area.take_cols(area.width()), area); + } + + #[test] + fn test_nth_area() { + let mut screen = Screen::::new(); + assert!(screen.resize(120, 20)); + let area = screen.area(); + assert_eq!(area.width(), 120); + assert_eq!(area.height(), 20); + + for i in 0..area.width() { + assert_eq!(area.nth_col(i).width(), 1); + assert!(area.contains(area.nth_col(i))); + if i + 1 == area.width() { + assert!(area.nth_col(i).contains(area.nth_col(i + 1))); + } else { + assert!(!area.nth_col(i).contains(area.nth_col(i + 1))); + } + } + + for i in 0..area.height() { + assert_eq!(area.nth_row(i).height(), 1); + assert!(area.contains(area.nth_row(i))); + if i + 1 == area.height() { + assert!(area.nth_row(i).contains(area.nth_row(i + 1))); + } else { + assert!(!area.nth_row(i).contains(area.nth_row(i + 1))); + } + } + } + + #[test] + fn test_place_inside_area() { + let mut screen = Screen::::new(); + assert!(screen.resize(120, 20)); + let area = screen.area(); + assert_eq!(area.width(), 120); + assert_eq!(area.height(), 20); + + for width in 0..area.width() { + for height in 0..area.height() { + for upper in [true, false] { + for left in [true, false] { + let inner = area.place_inside((width, height), upper, left); + assert!(area.contains(inner)); + if (3..area.height() - 2).contains(&height) + && (3..area.width() - 2).contains(&width) + { + assert_ne!(area, inner); + } + } + } + } + } + } + + #[test] + fn test_align_inside_area() { + use Alignment::{Center, End, Fill, Start}; + + const ALIGNMENTS: [Alignment; 4] = [Fill, Start, End, Center]; + + let mut screen = Screen::::new(); + assert!(screen.resize(120, 20)); + let area = screen.area(); + assert_eq!(area.width(), 120); + assert_eq!(area.height(), 20); + + // Ask for all subsets that area has: + for width in 1..=area.width() { + for height in 1..=area.height() { + for horz in ALIGNMENTS { + for vert in ALIGNMENTS { + let inner = area.align_inside((width, height), horz, vert); + assert!(area.contains(inner)); + assert!(!inner.is_empty()); + match (horz, vert) { + (Fill, Fill) => { + assert_eq!(area, inner); + } + (Fill, _) => { + assert_eq!(inner.width(), area.width()); + assert_eq!(inner.height(), height); + } + (_, Fill) => { + assert_eq!(inner.height(), area.height()); + assert_eq!(inner.width(), width); + } + _ => { + assert_eq!((width, height), inner.size()); + if (width, height) != area.size() { + assert_ne!(area, inner); + } + } + } + } + } + } + } + + // Ask for more width/height than area has: + for width in 1..=(2 * area.width()) { + for height in 1..=(2 * area.height()) { + for horz in ALIGNMENTS { + for vert in ALIGNMENTS { + let inner = area.align_inside((width, height), horz, vert); + assert!(area.contains(inner)); + assert!(!inner.is_empty()); + match (horz, vert) { + (Fill, Fill) => { + assert_eq!(area, inner); + } + (Fill, _) => { + assert_eq!(inner.width(), area.width()); + assert!( + height >= inner.height() && area.height() >= inner.height() + ); + } + (_, Fill) => { + assert_eq!(inner.height(), area.height()); + assert!(width >= inner.width() && area.width() >= inner.width()); + } + _ => { + assert!( + height >= inner.height() && area.height() >= inner.height() + ); + assert!(width >= inner.width() && area.width() >= inner.width()); + } + } + } + } + } + } + } +} diff --git a/meli/src/utilities.rs b/meli/src/utilities.rs index a54bed6d..dded7ea5 100644 --- a/meli/src/utilities.rs +++ b/meli/src/utilities.rs @@ -36,8 +36,9 @@ pub use self::text::*; mod widgets; pub use self::widgets::*; -mod layouts; -pub use self::layouts::*; +// TODO: remove +//mod layouts; +//pub use self::layouts::*; mod dialogs; pub use self::dialogs::*; @@ -70,7 +71,7 @@ pub struct StatusBar { display_buffer: String, mode: UIMode, mouse: bool, - height: usize, + status_bar_height: usize, dirty: bool, id: ComponentId, progress_spinner: ProgressSpinner, @@ -115,7 +116,7 @@ impl StatusBar { dirty: true, mode: UIMode::Normal, mouse: context.settings.terminal.use_mouse.is_true(), - height: 1, + status_bar_height: 1, id: ComponentId::default(), auto_complete: AutoComplete::new(Vec::new()), progress_spinner, @@ -139,7 +140,7 @@ impl StatusBar { area, None, ); - for c in grid.row_iter(x..(get_x(bottom_right!(area)) + 1), y) { + for c in grid.row_iter(area, x..(get_x(area.bottom_right()) + 1), y) { grid[c] .set_ch(' ') .set_fg(attribute.fg) @@ -147,11 +148,11 @@ impl StatusBar { .set_attrs(attribute.attrs); } let offset = self.status.find('|').unwrap_or(self.status.len()); - if y < get_y(bottom_right!(area)) + 1 { - for x in get_x(upper_left!(area)) + if y < get_y(area.bottom_right()) + 1 { + for x in get_x(area.upper_left()) ..std::cmp::min( - get_x(upper_left!(area)) + offset, - get_x(bottom_right!(area)), + get_x(area.upper_left()) + offset, + get_x(area.bottom_right()), ) { grid[(x, y)].set_attrs(attribute.attrs | Attr::BOLD); @@ -179,22 +180,20 @@ impl StatusBar { attribute.fg, attribute.bg, attribute.attrs, - ((x + 1, y), bottom_right!(area)), + area.skip_cols(x + 1).skip_rows(y), None, ); } - let (mut x, y) = bottom_right!(area); + let (mut x, y) = area.bottom_right(); if self.progress_spinner.is_active() { x = x.saturating_sub(1 + self.progress_spinner.width); } if self.progress_spinner.is_dirty() { self.progress_spinner.draw( grid, - ( - pos_dec(bottom_right!(area), (self.progress_spinner.width, 0)), - bottom_right!(area), - ), + area.skip_rows(area.height().saturating_sub(1)) + .skip_cols(area.width().saturating_sub(self.progress_spinner.width)), context, ); } @@ -246,7 +245,7 @@ impl StatusBar { ); grid.change_theme(area, command_bar); if let Some(ref mut cell) = grid.get_mut( - pos_inc(upper_left!(area), (self.ex_buffer.cursor(), 0)).0, + pos_inc(area.upper_left(), (self.ex_buffer.cursor(), 0)).0, y, ) { cell.set_attrs(command_bar.attrs | Attr::UNDERLINE); @@ -257,31 +256,21 @@ impl StatusBar { impl Component for StatusBar { fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { - if !is_valid_area!(area) { + let total_rows = area.height(); + if total_rows <= self.status_bar_height { return; } - let upper_left = upper_left!(area); - let bottom_right = bottom_right!(area); - - let total_rows = get_y(bottom_right) - get_y(upper_left); - if total_rows <= self.height { - return; - } - let height = self.height; self.container.draw( grid, - ( - upper_left, - (get_x(bottom_right), get_y(bottom_right) - height), - ), + area.take_rows(total_rows.saturating_sub(self.status_bar_height)), context, ); self.dirty = false; self.draw_status_bar( grid, - (set_y(upper_left, get_y(bottom_right)), bottom_right), + area.skip_rows(total_rows.saturating_sub(self.status_bar_height)), context, ); @@ -291,14 +280,8 @@ impl Component for StatusBar { match self.mode { UIMode::Normal => {} UIMode::Command => { - self.draw_command_bar( - grid, - ( - set_y(upper_left, get_y(bottom_right) - height + 1), - set_y(bottom_right, get_y(bottom_right) - height + 1), - ), - context, - ); + let area = area.skip_rows(total_rows.saturating_sub(self.status_bar_height + 2)); + self.draw_command_bar(grid, area, context); /* don't autocomplete for less than 3 characters */ if self.ex_buffer.as_str().split_graphemes().len() <= 2 { return; @@ -372,40 +355,39 @@ impl Component for StatusBar { self.container.set_dirty(true); } + /* let hist_height = std::cmp::min(15, self.auto_complete.suggestions().len()); - let hist_area = if height < self.auto_complete.suggestions().len() { - let hist_area = ( + let hist_area = if status_bar_height < self.auto_complete.suggestions().len() { + let hist_area = ( + ( + get_x(upper_left), + std::cmp::min( + get_y(bottom_right) - status_bar_height - hist_height + 1, + get_y(pos_dec(bottom_right, (0, status_bar_height))), + ), + ), + pos_dec(bottom_right, (0, status_bar_height)), + ); + ScrollBar::default().set_show_arrows(false).draw( + grid, + hist_area, + context, + self.auto_complete.cursor(), + hist_height, + self.auto_complete.suggestions().len(), + ); + grid.change_theme(hist_area, crate::conf::value(context, "status.history")); + context.dirty_areas.push_back(hist_area); + hist_area + } else { ( get_x(upper_left), std::cmp::min( - get_y(bottom_right) - height - hist_height + 1, - get_y(pos_dec(bottom_right, (0, height))), + get_y(bottom_right) - status_bar_height - hist_height + 1, + get_y(pos_dec(bottom_right, (0, status_bar_height))), ), ), - pos_dec(bottom_right, (0, height)), - ); - ScrollBar::default().set_show_arrows(false).draw( - grid, - hist_area, - context, - self.auto_complete.cursor(), - hist_height, - self.auto_complete.suggestions().len(), - ); - - grid.change_theme(hist_area, crate::conf::value(context, "status.history")); - context.dirty_areas.push_back(hist_area); - hist_area - } else { - ( - ( - get_x(upper_left), - std::cmp::min( - get_y(bottom_right) - height - hist_height + 1, - get_y(pos_dec(bottom_right, (0, height))), - ), - ), - pos_dec(bottom_right, (0, height)), + pos_dec(bottom_right, (0, status_bar_height)), ) }; let offset = if hist_height @@ -454,35 +436,96 @@ impl Component for StatusBar { if y_offset + offset == self.auto_complete.cursor() { grid.change_theme( ( - set_y( - upper_left!(hist_area), - get_y(bottom_right!(hist_area)) - hist_height + y_offset + 1, - ), - set_y( - bottom_right!(hist_area), - get_y(bottom_right!(hist_area)) - hist_height + y_offset + 1, + get_x(upper_left), + std::cmp::min( + get_y(bottom_right) - status_bar_height - hist_height + 1, + get_y(pos_dec(bottom_right, (0, status_bar_height))), ), ), - history_hints, - ); - grid.write_string( - &s.as_str()[self.ex_buffer.as_str().len()..], + pos_dec(bottom_right, (0, status_bar_height)), + ) + }; + let offset = if hist_height + > (self.auto_complete.suggestions().len() - self.auto_complete.cursor()) + { + self.auto_complete.suggestions().len() - hist_height + } else { + self.auto_complete.cursor() + }; + grid.clear_area(hist_area, crate::conf::value(context, "theme_default")); + let history_hints = crate::conf::value(context, "status.history.hints"); + if hist_height > 0 { + grid.change_theme(hist_area, history_hints); + } + for (y_offset, s) in self + .auto_complete + .suggestions() + .iter() + .skip(offset) + .take(hist_height) + .enumerate() + { + let (x, y) = grid.write_string( + s.as_str(), + grid, history_hints.fg, history_hints.bg, history_hints.attrs, ( - ( - get_x(upper_left) - + self.ex_buffer.as_str().split_graphemes().len(), - get_y(bottom_right) - height + 1, + set_y( + hist_area.upper_left(), + get_y(hist_area.bottom_right()) - hist_height + y_offset + 1, ), - set_y(bottom_right, get_y(bottom_right) - height + 1), + hist_area.bottom_right(), ), + Some(get_x(hist_area.upper_left())), + ); + grid.write_string( + &s.description, + grid, + history_hints.fg, + history_hints.bg, + history_hints.attrs, + ((x + 2, y), hist_area.bottom_right()), None, ); + if y_offset + offset == self.auto_complete.cursor() { + grid.change_theme( + ( + set_y( + hist_area.upper_left(), + get_y(hist_area.bottom_right()) - hist_height + + y_offset + + 1, + ), + set_y( + hist_area.bottom_right(), + get_y(hist_area.bottom_right()) - hist_height + + y_offset + + 1, + ), + ), + history_hints, + ); + grid.write_string( + &s.as_str()[self.ex_buffer.as_str().len()..], + history_hints.fg, + history_hints.bg, + history_hints.attrs, + ( + ( + get_x(upper_left) + + self.ex_buffer.as_str().split_graphemes().len(), + get_y(bottom_right) - status_bar_height + 1, + ), + set_y(bottom_right, get_y(bottom_right) - status_bar_height + 1), + ), + None, + ); + } } - } - context.dirty_areas.push_back(hist_area); + context.dirty_areas.push_back(hist_area); + */ } _ => {} } @@ -539,7 +582,7 @@ impl Component for StatusBar { self.mode = *m; match m { UIMode::Normal => { - self.height = 1; + self.status_bar_height = 1; if !self.ex_buffer.is_empty() { context .replies @@ -556,10 +599,10 @@ impl Component for StatusBar { self.ex_buffer_cmd_history_pos.take(); } UIMode::Command => { - self.height = 2; + self.status_bar_height = 2; } _ => { - self.height = 1; + self.status_bar_height = 1; } }; } @@ -810,7 +853,6 @@ pub struct Tabbed { show_shortcuts: bool, help_screen_cursor: (usize, usize), - help_content: CellBuffer, help_curr_views: ShortcutMaps, help_search: Option, theme_default: ThemeAttribute, @@ -827,7 +869,6 @@ impl Tabbed { .get(0) .map(|c| c.shortcuts(context)) .unwrap_or_default(), - help_content: CellBuffer::default(), help_screen_cursor: (0, 0), help_search: None, theme_default: crate::conf::value(context, "theme_default"), @@ -843,8 +884,8 @@ impl Tabbed { } fn draw_tabs(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { - let upper_left = upper_left!(area); - let bottom_right = bottom_right!(area); + let upper_left = area.upper_left(); + let bottom_right = area.bottom_right(); let tab_bar_attribute = crate::conf::value(context, "tab.bar"); grid.clear_area(area, tab_bar_attribute); @@ -870,7 +911,7 @@ impl Tabbed { fg, bg, attrs, - (set_x(upper_left, x), bottom_right!(area)), + area.skip_cols(x - get_x(upper_left)), None, ); x = x_ + 1; @@ -906,7 +947,7 @@ impl Tabbed { .set_bg(tab_unfocused_attribute.fg) .set_attrs(tab_unfocused_attribute.attrs); } - for c in grid.row_iter(x..cols, get_y(upper_left)) { + for c in grid.row_iter(area, x..cols, get_y(upper_left)) { grid[c] .set_fg(tab_bar_attribute.fg) .set_bg(tab_bar_attribute.bg) @@ -943,17 +984,9 @@ impl std::fmt::Display for Tabbed { impl Component for Tabbed { fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { if self.dirty { - grid.clear_area( - ( - upper_left!(area), - set_x(upper_left!(area), get_x(bottom_right!(area))), - ), - crate::conf::value(context, "tab.bar"), - ); - context.dirty_areas.push_back(( - upper_left!(area), - set_x(upper_left!(area), get_x(bottom_right!(area))), - )); + let first_row = area.nth_row(0); + grid.clear_area(first_row, crate::conf::value(context, "tab.bar")); + context.dirty_areas.push_back(first_row); } /* If children are dirty but self isn't and the shortcuts panel is visible, @@ -964,385 +997,369 @@ impl Component for Tabbed { * overwrite the panel on the grid. the drawing order is determined * by the dirty_areas queue which is LIFO */ if self.children.len() > 1 { - self.draw_tabs( - grid, - ( - upper_left!(area), - set_x(upper_left!(area), get_x(bottom_right!(area))), - ), - context, - ); - let y = get_y(upper_left!(area)); - self.children[self.cursor_pos].draw( - grid, - (set_y(upper_left!(area), y + 1), bottom_right!(area)), - context, - ); + self.draw_tabs(grid, area.nth_row(0), context); + self.children[self.cursor_pos].draw(grid, area.skip_rows(1), context); } else { self.children[self.cursor_pos].draw(grid, area, context); } if (self.show_shortcuts && self.dirty) || must_redraw_shortcuts { - let mut children_maps = self.children[self.cursor_pos].shortcuts(context); - children_maps.extend_shortcuts(self.shortcuts(context)); - if children_maps.is_empty() { - return; - } - if let Some(i) = children_maps - .get_index_of(Shortcuts::GENERAL) - .filter(|i| i + 1 != children_maps.len()) - { - children_maps.move_index(i, children_maps.len().saturating_sub(1)); - } - if (children_maps == self.help_curr_views) && must_redraw_shortcuts { - let dialog_area = align_area( - area, - /* add box perimeter padding */ - pos_inc(self.help_content.size(), (2, 2)), - /* vertical */ - Alignment::Center, - /* horizontal */ - Alignment::Center, - ); - context.dirty_areas.push_back(dialog_area); - grid.clear_area(dialog_area, self.theme_default); - let inner_area = create_box(grid, dialog_area); - let (x, y) = grid.write_string( - "shortcuts", - self.theme_default.fg, - self.theme_default.bg, - self.theme_default.attrs | Attr::BOLD, - ( - pos_inc(upper_left!(dialog_area), (2, 0)), - bottom_right!(dialog_area), - ), - None, - ); - grid.write_string( - &format!( - "Press {} to close", - children_maps[Shortcuts::GENERAL]["toggle_help"] - ), - self.theme_default.fg, - self.theme_default.bg, - self.theme_default.attrs | Attr::ITALICS, - ((x + 2, y), bottom_right!(dialog_area)), - None, - ); - let (width, height) = self.help_content.size(); - let (cols, rows) = (width!(inner_area), height!(inner_area)); + /* + let mut children_maps = self.children[self.cursor_pos].shortcuts(context); + children_maps.extend_shortcuts(self.shortcuts(context)); + if children_maps.is_empty() { + return; + } + if let Some(i) = children_maps + .get_index_of(Shortcuts::GENERAL) + .filter(|i| i + 1 != children_maps.len()) + { + children_maps.move_index(i, children_maps.len().saturating_sub(1)); + } + if (children_maps == self.help_curr_views) && must_redraw_shortcuts { + let dialog_area = area.align_inside( + /* add box perimeter padding */ + pos_inc(self.help_content.size(), (2, 2)), + /* vertical */ + Alignment::Center, + /* horizontal */ + Alignment::Center, + ); + context.dirty_areas.push_back(dialog_area); + grid.clear_area(dialog_area, self.theme_default); + let inner_area = create_box(grid, dialog_area); + let (x, y) = grid.write_string( + "shortcuts", + self.theme_default.fg, + self.theme_default.bg, + self.theme_default.attrs | Attr::BOLD, + ( + pos_inc(dialog_area.upper_left(), (2, 0)), + dialog_area.bottom_right(), + ), + None, + ); + grid.write_string( + &format!( + "Press {} to close", + children_maps[Shortcuts::GENERAL]["toggle_help"] + ), + self.theme_default.fg, + self.theme_default.bg, + self.theme_default.attrs | Attr::ITALICS, + ((x + 2, y), dialog_area.bottom_right()), + None, + ); + let (width, height) = self.help_content.size(); + let (cols, rows) = (width!(inner_area), height!(inner_area)); - grid.copy_area( - &self.help_content, - inner_area, - ( - ( - std::cmp::min( - (width - 1).saturating_sub(cols), - self.help_screen_cursor.0, - ), - std::cmp::min( - (height - 1).saturating_sub(rows), - self.help_screen_cursor.1, - ), - ), - ( - std::cmp::min(self.help_screen_cursor.0 + cols, width - 1), - std::cmp::min(self.help_screen_cursor.1 + rows, height - 1), - ), - ), - ); - if height.wrapping_div(rows + 1) > 0 || width.wrapping_div(cols + 1) > 0 { - context - .replies - .push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate( - ScrollUpdate::Update { - id: self.id, - context: ScrollContext { - shown_lines: std::cmp::min( - (height).saturating_sub(rows + 1), - self.help_screen_cursor.1, - ) + rows, - total_lines: height, - has_more_lines: false, - }, - }, - ))); - ScrollBar::default().set_show_arrows(true).draw( - grid, - ( - pos_inc( - upper_left!(inner_area), - (width!(inner_area).saturating_sub(1), 0), - ), - bottom_right!(inner_area), - ), - context, - /* position */ - std::cmp::min((height).saturating_sub(rows + 1), self.help_screen_cursor.1), - /* visible_rows */ - rows, - /* length */ - height, - ); - } else { - context - .replies - .push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate( - ScrollUpdate::End(self.id), - ))); - } - self.dirty = false; - return; - } - let mut max_length = 6; - let mut max_width = - "Press XXXX to close, use COMMAND \"search\" to find shortcuts".len() + 3; + grid.copy_area( + &self.help_content, + inner_area, + ( + ( + std::cmp::min( + (width - 1).saturating_sub(cols), + self.help_screen_cursor.0, + ), + std::cmp::min( + (height - 1).saturating_sub(rows), + self.help_screen_cursor.1, + ), + ), + ( + std::cmp::min(self.help_screen_cursor.0 + cols, width - 1), + std::cmp::min(self.help_screen_cursor.1 + rows, height - 1), + ), + ), + ); + if height.wrapping_div(rows + 1) > 0 || width.wrapping_div(cols + 1) > 0 { + context + .replies + .push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate( + ScrollUpdate::Update { + id: self.id, + context: ScrollContext { + shown_lines: std::cmp::min( + (height).saturating_sub(rows + 1), + self.help_screen_cursor.1, + ) + rows, + total_lines: height, + has_more_lines: false, + }, + }, + ))); + ScrollBar::default().set_show_arrows(true).draw( + grid, + ( + pos_inc( + inner_area.upper_left(), + (width!(inner_area).saturating_sub(1), 0), + ), + inner_area.bottom_right(), + ), + context, + /* position */ + std::cmp::min((height).saturating_sub(rows + 1), self.help_screen_cursor.1), + /* visible_rows */ + rows, + /* length */ + height, + ); + } else { + context + .replies + .push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate( + ScrollUpdate::End(self.id), + ))); + } + self.dirty = false; + return; + } + let mut max_length = 6; + let mut max_width = + "Press XXXX to close, use COMMAND \"search\" to find shortcuts".len() + 3; - let mut max_first_column_width = 3; + let mut max_first_column_width = 3; - for (desc, shortcuts) in children_maps.iter() { - max_length += shortcuts.len() + 3; - max_width = std::cmp::max( - max_width, - std::cmp::max( - desc.len(), - shortcuts - .values() - .map(|v| v.to_string().len() + 5) - .max() - .unwrap_or(0), - ), - ); - max_first_column_width = std::cmp::max( - max_first_column_width, - shortcuts - .values() - .map(|v| v.to_string().len() + 5) - .max() - .unwrap_or(0), - ); - } - self.help_content = - CellBuffer::new_with_context(max_width, max_length + 2, None, context); - self.help_content.set_growable(true); - self.help_content.write_string( - "use COMMAND \"search\" to find shortcuts", - self.theme_default.fg, - self.theme_default.bg, - self.theme_default.attrs, - ((2, 1), (max_width.saturating_sub(2), max_length - 1)), - None, - ); - let mut idx = 2; - for (desc, shortcuts) in children_maps.iter() { - self.help_content.write_string( - desc, - self.theme_default.fg, - self.theme_default.bg, - self.theme_default.attrs, - ((2, 2 + idx), (max_width.saturating_sub(2), max_length - 1)), - None, - ); - idx += 2; - for (k, v) in shortcuts { - let (x, y) = self.help_content.write_string( - &format!( - "{: >width$}", - format!("{}", v), - width = max_first_column_width - ), - self.theme_default.fg, - self.theme_default.bg, - self.theme_default.attrs | Attr::BOLD, - ((2, 2 + idx), (max_width.saturating_sub(2), max_length - 1)), - None, - ); - self.help_content.write_string( - k, - self.theme_default.fg, - self.theme_default.bg, - self.theme_default.attrs, - ((x + 2, y), (max_width.saturating_sub(2), max_length - 1)), - None, - ); - idx += 1; - } - idx += 1; - } - self.help_curr_views = children_maps; - let dialog_area = align_area( - area, - /* add box perimeter padding */ - pos_inc(self.help_content.size(), (2, 2)), - /* vertical */ - Alignment::Center, - /* horizontal */ - Alignment::Center, - ); - context.dirty_areas.push_back(dialog_area); - grid.clear_area(dialog_area, self.theme_default); - let inner_area = create_box(grid, dialog_area); - let (x, y) = grid.write_string( - "shortcuts", - self.theme_default.fg, - self.theme_default.bg, - self.theme_default.attrs | Attr::BOLD, - ( - pos_inc(upper_left!(dialog_area), (2, 0)), - bottom_right!(dialog_area), - ), - None, - ); - grid.write_string( - &format!( - "Press {} to close", - self.help_curr_views[Shortcuts::GENERAL]["toggle_help"] - ), - self.theme_default.fg, - self.theme_default.bg, - self.theme_default.attrs | Attr::ITALICS, - ((x + 2, y), bottom_right!(dialog_area)), - None, - ); - let (width, height) = self.help_content.size(); - let (cols, rows) = (width!(inner_area), height!(inner_area)); - if let Some(ref mut search) = self.help_search { - use crate::melib::text_processing::search::KMP; - search.positions = self - .help_content - .kmp_search(&search.pattern) - .into_iter() - .map(|offset| (offset / width, offset % width)) - .collect::>(); - let results_attr = crate::conf::value(context, "pager.highlight_search"); - let results_current_attr = - crate::conf::value(context, "pager.highlight_search_current"); - search.cursor = - std::cmp::min(search.positions.len().saturating_sub(1), search.cursor); - for (i, (y, x)) in search.positions.iter().enumerate() { - for c in self - .help_content - .row_iter(*x..*x + search.pattern.grapheme_len(), *y) - { - if i == search.cursor { - self.help_content[c] - .set_fg(results_current_attr.fg) - .set_bg(results_current_attr.bg) - .set_attrs(results_current_attr.attrs); + for (desc, shortcuts) in children_maps.iter() { + max_length += shortcuts.len() + 3; + max_width = std::cmp::max( + max_width, + std::cmp::max( + desc.len(), + shortcuts + .values() + .map(|v| v.to_string().len() + 5) + .max() + .unwrap_or(0), + ), + ); + max_first_column_width = std::cmp::max( + max_first_column_width, + shortcuts + .values() + .map(|v| v.to_string().len() + 5) + .max() + .unwrap_or(0), + ); + } + //self.help_content_size = (max_width, max_length + 2); + self.help_content = + CellBuffer::new_with_context(max_width, max_length + 2, None, context); + self.help_content.set_growable(true); + self.help_content.write_string( + "use COMMAND \"search\" to find shortcuts", + self.theme_default.fg, + self.theme_default.bg, + self.theme_default.attrs, + ((2, 1), (max_width.saturating_sub(2), max_length - 1)), + None, + ); + let mut idx = 2; + for (desc, shortcuts) in children_maps.iter() { + self.help_content.write_string( + desc, + self.theme_default.fg, + self.theme_default.bg, + self.theme_default.attrs, + ((2, 2 + idx), (max_width.saturating_sub(2), max_length - 1)), + None, + ); + idx += 2; + for (k, v) in shortcuts { + let (x, y) = self.help_content.write_string( + &format!( + "{: >width$}", + format!("{}", v), + width = max_first_column_width + ), + self.theme_default.fg, + self.theme_default.bg, + self.theme_default.attrs | Attr::BOLD, + ((2, 2 + idx), (max_width.saturating_sub(2), max_length - 1)), + None, + ); + self.help_content.write_string( + k, + self.theme_default.fg, + self.theme_default.bg, + self.theme_default.attrs, + ((x + 2, y), (max_width.saturating_sub(2), max_length - 1)), + None, + ); + idx += 1; + } + idx += 1; + } + self.help_curr_views = children_maps; + let dialog_area = area.align_inside( + area, + /* add box perimeter padding */ + pos_inc(self.help_content.size(), (2, 2)), + /* vertical */ + Alignment::Center, + /* horizontal */ + Alignment::Center, + ); + context.dirty_areas.push_back(dialog_area); + grid.clear_area(dialog_area, self.theme_default); + let inner_area = create_box(grid, dialog_area); + let (x, y) = grid.write_string( + "shortcuts", + self.theme_default.fg, + self.theme_default.bg, + self.theme_default.attrs | Attr::BOLD, + ( + pos_inc(dialog_area.upper_left(), (2, 0)), + dialog_area.bottom_right(), + ), + None, + ); + grid.write_string( + &format!( + "Press {} to close", + self.help_curr_views[Shortcuts::GENERAL]["toggle_help"] + ), + self.theme_default.fg, + self.theme_default.bg, + self.theme_default.attrs | Attr::ITALICS, + ((x + 2, y), dialog_area.bottom_right()), + None, + ); + let (width, height) = self.help_content.size(); + let (cols, rows) = (width!(inner_area), height!(inner_area)); + if let Some(ref mut search) = self.help_search { + use crate::melib::text_processing::search::KMP; + search.positions = self + .help_content + .kmp_search(&search.pattern) + .into_iter() + .map(|offset| (offset / width, offset % width)) + .collect::>(); + let results_attr = crate::conf::value(context, "pager.highlight_search"); + let results_current_attr = + crate::conf::value(context, "pager.highlight_search_current"); + search.cursor = + std::cmp::min(search.positions.len().saturating_sub(1), search.cursor); + for (i, (y, x)) in search.positions.iter().enumerate() { + for c in self + .help_content + .row_iter(area, *x..*x + search.pattern.grapheme_len(), *y) + { + if i == search.cursor { + self.help_content[c] + .set_fg(results_current_attr.fg) + .set_bg(results_current_attr.bg) + .set_attrs(results_current_attr.attrs); + } else { + self.help_content[c] + .set_fg(results_attr.fg) + .set_bg(results_attr.bg) + .set_attrs(results_attr.attrs); + } + } + } + if !search.positions.is_empty() { + if let Some(mvm) = search.movement.take() { + match mvm { + PageMovement::Home => { + if self.help_screen_cursor.1 > search.positions[search.cursor].0 { + self.help_screen_cursor.1 = search.positions[search.cursor].0; + } + if self.help_screen_cursor.1 + rows + < search.positions[search.cursor].0 + { + self.help_screen_cursor.1 = search.positions[search.cursor].0; + } + } + PageMovement::Up(_) => { + if self.help_screen_cursor.1 > search.positions[search.cursor].0 { + self.help_screen_cursor.1 = search.positions[search.cursor].0; + } + } + PageMovement::Down(_) => { + if self.help_screen_cursor.1 + rows + < search.positions[search.cursor].0 + { + self.help_screen_cursor.1 = search.positions[search.cursor].0; + } + } + _ => {} + } + } + } + } + /* trim cursor if it's bigger than the help screen */ + self.help_screen_cursor = ( + std::cmp::min((width).saturating_sub(cols), self.help_screen_cursor.0), + std::cmp::min((height).saturating_sub(rows), self.help_screen_cursor.1), + ); + if cols == 0 || rows == 0 { + return; + } + + /* In this case we will be scrolling, so show the user how to do it */ + if height.wrapping_div(rows + 1) > 0 || width.wrapping_div(cols + 1) > 0 { + self.help_content.write_string( + "Use Up, Down, Left, Right to scroll.", + self.theme_default.fg, + self.theme_default.bg, + self.theme_default.attrs | Attr::ITALICS, + ((2, 2), (max_width.saturating_sub(2), max_length - 1)), + None, + ); + } + + grid.copy_area( + &self.help_content, + inner_area, + ( + ( + std::cmp::min((width - 1).saturating_sub(cols), self.help_screen_cursor.0), + std::cmp::min((height - 1).saturating_sub(rows), self.help_screen_cursor.1), + ), + ( + std::cmp::min(self.help_screen_cursor.0 + cols, width - 1), + std::cmp::min(self.help_screen_cursor.1 + rows, height - 1), + ), + ), + ); + if height.wrapping_div(rows + 1) > 0 || width.wrapping_div(cols + 1) > 0 { + context + .replies + .push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate( + ScrollUpdate::Update { + id: self.id, + context: ScrollContext { + shown_lines: std::cmp::min( + (height).saturating_sub(rows), + self.help_screen_cursor.1, + ) + rows, + total_lines: height, + has_more_lines: false, + }, + }, + ))); + ScrollBar::default().set_show_arrows(true).draw( + grid, + inner_area.skip_cols(inner_area.width().saturating_sub(1)), + context, + /* position */ + std::cmp::min((height).saturating_sub(rows), self.help_screen_cursor.1), + /* visible_rows */ + rows, + /* length */ + height, + ); } else { - self.help_content[c] - .set_fg(results_attr.fg) - .set_bg(results_attr.bg) - .set_attrs(results_attr.attrs); + context + .replies + .push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate( + ScrollUpdate::End(self.id), + ))); } - } - } - if !search.positions.is_empty() { - if let Some(mvm) = search.movement.take() { - match mvm { - PageMovement::Home => { - if self.help_screen_cursor.1 > search.positions[search.cursor].0 { - self.help_screen_cursor.1 = search.positions[search.cursor].0; - } - if self.help_screen_cursor.1 + rows - < search.positions[search.cursor].0 - { - self.help_screen_cursor.1 = search.positions[search.cursor].0; - } - } - PageMovement::Up(_) => { - if self.help_screen_cursor.1 > search.positions[search.cursor].0 { - self.help_screen_cursor.1 = search.positions[search.cursor].0; - } - } - PageMovement::Down(_) => { - if self.help_screen_cursor.1 + rows - < search.positions[search.cursor].0 - { - self.help_screen_cursor.1 = search.positions[search.cursor].0; - } - } - _ => {} - } - } - } - } - /* trim cursor if it's bigger than the help screen */ - self.help_screen_cursor = ( - std::cmp::min((width).saturating_sub(cols), self.help_screen_cursor.0), - std::cmp::min((height).saturating_sub(rows), self.help_screen_cursor.1), - ); - if cols == 0 || rows == 0 { - return; - } - - /* In this case we will be scrolling, so show the user how to do it */ - if height.wrapping_div(rows + 1) > 0 || width.wrapping_div(cols + 1) > 0 { - self.help_content.write_string( - "Use Up, Down, Left, Right to scroll.", - self.theme_default.fg, - self.theme_default.bg, - self.theme_default.attrs | Attr::ITALICS, - ((2, 2), (max_width.saturating_sub(2), max_length - 1)), - None, - ); - } - - grid.copy_area( - &self.help_content, - inner_area, - ( - ( - std::cmp::min((width - 1).saturating_sub(cols), self.help_screen_cursor.0), - std::cmp::min((height - 1).saturating_sub(rows), self.help_screen_cursor.1), - ), - ( - std::cmp::min(self.help_screen_cursor.0 + cols, width - 1), - std::cmp::min(self.help_screen_cursor.1 + rows, height - 1), - ), - ), - ); - if height.wrapping_div(rows + 1) > 0 || width.wrapping_div(cols + 1) > 0 { - context - .replies - .push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate( - ScrollUpdate::Update { - id: self.id, - context: ScrollContext { - shown_lines: std::cmp::min( - (height).saturating_sub(rows), - self.help_screen_cursor.1, - ) + rows, - total_lines: height, - has_more_lines: false, - }, - }, - ))); - ScrollBar::default().set_show_arrows(true).draw( - grid, - ( - pos_inc( - upper_left!(inner_area), - (width!(inner_area).saturating_sub(1), 0), - ), - bottom_right!(inner_area), - ), - context, - /* position */ - std::cmp::min((height).saturating_sub(rows), self.help_screen_cursor.1), - /* visible_rows */ - rows, - /* length */ - height, - ); - } else { - context - .replies - .push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate( - ScrollUpdate::End(self.id), - ))); - } + */ } self.dirty = false; } @@ -1600,6 +1617,7 @@ impl Component for Tabbed { } } +/* #[derive(Debug, Clone, PartialEq, Eq)] pub struct RawBuffer { pub buf: CellBuffer, @@ -1619,7 +1637,7 @@ impl Component for RawBuffer { fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { if self.dirty { let (width, height) = self.buf.size(); - let (cols, rows) = (width!(area), height!(area)); + let (cols, rows) = (area.width(), area.height()); self.cursor = ( std::cmp::min(width.saturating_sub(cols), self.cursor.0), std::cmp::min(height.saturating_sub(rows), self.cursor.1), @@ -1697,3 +1715,4 @@ impl RawBuffer { self.title.as_deref().unwrap_or("untitled") } } +*/ diff --git a/meli/src/utilities/dialogs.rs b/meli/src/utilities/dialogs.rs index 9a5c5004..750e8496 100644 --- a/meli/src/utilities/dialogs.rs +++ b/meli/src/utilities/dialogs.rs @@ -53,7 +53,6 @@ pub struct Selector< single_only: bool, entries: Vec<(T, bool)>, entry_titles: Vec, - pub content: CellBuffer, theme_default: ThemeAttribute, cursor: SelectorCursor, @@ -114,7 +113,6 @@ impl Component f return false; } - let (width, height) = self.content.size(); let shortcuts = self.shortcuts(context); let mut highlighted_attrs = crate::conf::value(context, "widgets.options.highlighted"); if !context.settings.terminal.use_color() { @@ -134,25 +132,6 @@ impl Component f /* User can select multiple entries, so Enter key toggles the entry under the * cursor */ self.entries[c].1 = !self.entries[c].1; - if self.entries[c].1 { - self.content.write_string( - "x", - highlighted_attrs.fg, - highlighted_attrs.bg, - highlighted_attrs.attrs, - ((1, c), (width - 1, c)), - None, - ); - } else { - self.content.write_string( - " ", - highlighted_attrs.fg, - highlighted_attrs.bg, - highlighted_attrs.attrs, - ((1, c), (width - 1, c)), - None, - ); - } self.dirty = true; return true; } @@ -192,20 +171,7 @@ impl Component f if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_down"]) => { if self.single_only { - for c in self.content.row_iter(0..(width - 1), 0) { - self.content[c] - .set_fg(highlighted_attrs.fg) - .set_bg(highlighted_attrs.bg) - .set_attrs(highlighted_attrs.attrs); - } self.entries[0].1 = true; - } else { - for c in self.content.row_iter(0..3, 0) { - self.content[c] - .set_fg(highlighted_attrs.fg) - .set_bg(highlighted_attrs.bg) - .set_attrs(highlighted_attrs.attrs); - } } self.cursor = SelectorCursor::Entry(0); self.dirty = true; @@ -216,34 +182,8 @@ impl Component f { if self.single_only { // Redraw selection - for c in self.content.row_iter(0..(width - 1), c) { - self.content[c] - .set_fg(self.theme_default.fg) - .set_bg(self.theme_default.bg) - .set_attrs(self.theme_default.attrs); - } - for c in self.content.row_iter(0..(width - 1), c - 1) { - self.content[c] - .set_fg(highlighted_attrs.fg) - .set_bg(highlighted_attrs.bg) - .set_attrs(highlighted_attrs.attrs); - } self.entries[c].1 = false; self.entries[c - 1].1 = true; - } else { - // Redraw cursor - for c in self.content.row_iter(0..3, c) { - self.content[c] - .set_fg(self.theme_default.fg) - .set_bg(self.theme_default.bg) - .set_attrs(self.theme_default.attrs); - } - for c in self.content.row_iter(0..3, c - 1) { - self.content[c] - .set_fg(highlighted_attrs.fg) - .set_bg(highlighted_attrs.bg) - .set_attrs(highlighted_attrs.attrs); - } } self.cursor = SelectorCursor::Entry(c - 1); self.dirty = true; @@ -253,23 +193,8 @@ impl Component f | (UIEvent::Input(ref key), SelectorCursor::Cancel) if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_up"]) => { - for c in self - .content - .row_iter(((width - OK_CANCEL.len()) / 2)..(width - 1), height - 1) - { - self.content[c] - .set_fg(self.theme_default.fg) - .set_bg(self.theme_default.bg) - .set_attrs(self.theme_default.attrs); - } let c = self.entries.len().saturating_sub(1); self.cursor = SelectorCursor::Entry(c); - for c in self.content.row_iter(0..3, c) { - self.content[c] - .set_fg(highlighted_attrs.fg) - .set_bg(highlighted_attrs.bg) - .set_attrs(highlighted_attrs.attrs); - } self.dirty = true; return true; } @@ -279,34 +204,8 @@ impl Component f { if self.single_only { // Redraw selection - for c in self.content.row_iter(0..(width - 1), c) { - self.content[c] - .set_fg(self.theme_default.fg) - .set_bg(self.theme_default.bg) - .set_attrs(self.theme_default.attrs); - } - for c in self.content.row_iter(0..(width - 1), c + 1) { - self.content[c] - .set_fg(highlighted_attrs.fg) - .set_bg(highlighted_attrs.bg) - .set_attrs(highlighted_attrs.attrs); - } self.entries[c].1 = false; self.entries[c + 1].1 = true; - } else { - // Redraw cursor - for c in self.content.row_iter(0..3, c) { - self.content[c] - .set_fg(self.theme_default.fg) - .set_bg(self.theme_default.bg) - .set_attrs(self.theme_default.attrs); - } - for c in self.content.row_iter(0..3, c + 1) { - self.content[c] - .set_fg(highlighted_attrs.fg) - .set_bg(highlighted_attrs.bg) - .set_attrs(highlighted_attrs.attrs); - } } self.cursor = SelectorCursor::Entry(c + 1); self.dirty = true; @@ -317,22 +216,6 @@ impl Component f && shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_down"]) => { self.cursor = SelectorCursor::Ok; - for c in self.content.row_iter(0..3, c) { - self.content[c] - .set_fg(self.theme_default.fg) - .set_bg(self.theme_default.bg) - .set_attrs(self.theme_default.attrs); - } - for c in self.content.row_iter( - ((width - OK_CANCEL.len()) / 2 + OK_OFFSET) - ..((width - OK_CANCEL.len()) / 2 + OK_OFFSET + OK_LENGTH), - height - 1, - ) { - self.content[c] - .set_fg(highlighted_attrs.fg) - .set_bg(highlighted_attrs.bg) - .set_attrs(highlighted_attrs.attrs); - } self.dirty = true; return true; } @@ -340,26 +223,6 @@ impl Component f if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_right"]) => { self.cursor = SelectorCursor::Cancel; - for c in self.content.row_iter( - ((width - OK_CANCEL.len()) / 2 + OK_OFFSET) - ..((width - OK_CANCEL.len()) / 2 + OK_OFFSET + OK_LENGTH), - height - 1, - ) { - self.content[c] - .set_fg(self.theme_default.fg) - .set_bg(self.theme_default.bg) - .set_attrs(self.theme_default.attrs); - } - for c in self.content.row_iter( - ((width - OK_CANCEL.len()) / 2 + CANCEL_OFFSET) - ..((width - OK_CANCEL.len()) / 2 + CANCEL_OFFSET + CANCEL_LENGTH), - height - 1, - ) { - self.content[c] - .set_fg(highlighted_attrs.fg) - .set_bg(highlighted_attrs.bg) - .set_attrs(highlighted_attrs.attrs); - } self.dirty = true; return true; } @@ -367,27 +230,6 @@ impl Component f if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_left"]) => { self.cursor = SelectorCursor::Ok; - for c in self.content.row_iter( - ((width - OK_CANCEL.len()) / 2 + OK_OFFSET) - ..((width - OK_CANCEL.len()) / 2 + OK_OFFSET + OK_LENGTH), - height - 1, - ) { - self.content[c] - .set_fg(highlighted_attrs.fg) - .set_bg(highlighted_attrs.bg) - .set_attrs(highlighted_attrs.attrs); - } - - self.content.change_theme( - ( - ((width - OK_CANCEL.len()) / 2 + CANCEL_OFFSET, height - 1), - ( - (width - OK_CANCEL.len()) / 2 + CANCEL_OFFSET + CANCEL_LENGTH, - height - 1, - ), - ), - self.theme_default, - ); self.dirty = true; return true; } @@ -439,7 +281,6 @@ impl Component for UIConfirmationDialog { return false; } - let (width, height) = self.content.size(); let shortcuts = self.shortcuts(context); let mut highlighted_attrs = crate::conf::value(context, "widgets.options.highlighted"); if !context.settings.terminal.use_color() { @@ -459,25 +300,6 @@ impl Component for UIConfirmationDialog { /* User can select multiple entries, so Enter key toggles the entry under the * cursor */ self.entries[c].1 = !self.entries[c].1; - if self.entries[c].1 { - self.content.write_string( - "x", - highlighted_attrs.fg, - highlighted_attrs.bg, - highlighted_attrs.attrs, - ((1, c), (width - 1, c)), - None, - ); - } else { - self.content.write_string( - " ", - highlighted_attrs.fg, - highlighted_attrs.bg, - highlighted_attrs.attrs, - ((1, c), (width - 1, c)), - None, - ); - } self.dirty = true; return true; } @@ -518,34 +340,8 @@ impl Component for UIConfirmationDialog { { if self.single_only { // Redraw selection - for c in self.content.row_iter(0..(width - 1), c) { - self.content[c] - .set_fg(self.theme_default.fg) - .set_bg(self.theme_default.bg) - .set_attrs(self.theme_default.attrs); - } - for c in self.content.row_iter(0..(width - 1), c - 1) { - self.content[c] - .set_fg(highlighted_attrs.fg) - .set_bg(highlighted_attrs.bg) - .set_attrs(highlighted_attrs.attrs); - } self.entries[c].1 = false; self.entries[c - 1].1 = true; - } else { - // Redraw cursor - for c in self.content.row_iter(0..3, c) { - self.content[c] - .set_fg(self.theme_default.fg) - .set_bg(self.theme_default.bg) - .set_attrs(self.theme_default.attrs); - } - for c in self.content.row_iter(0..3, c - 1) { - self.content[c] - .set_fg(highlighted_attrs.fg) - .set_bg(highlighted_attrs.bg) - .set_attrs(highlighted_attrs.attrs); - } } self.cursor = SelectorCursor::Entry(c - 1); self.dirty = true; @@ -555,23 +351,8 @@ impl Component for UIConfirmationDialog { | (UIEvent::Input(ref key), SelectorCursor::Cancel) if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_up"]) => { - for c in self - .content - .row_iter(((width - OK_CANCEL.len()) / 2)..(width - 1), height - 1) - { - self.content[c] - .set_fg(self.theme_default.fg) - .set_bg(self.theme_default.bg) - .set_attrs(self.theme_default.attrs); - } let c = self.entries.len().saturating_sub(1); self.cursor = SelectorCursor::Entry(c); - for c in self.content.row_iter(0..3, c) { - self.content[c] - .set_fg(highlighted_attrs.fg) - .set_bg(highlighted_attrs.bg) - .set_attrs(highlighted_attrs.attrs); - } self.dirty = true; return true; } @@ -579,20 +360,7 @@ impl Component for UIConfirmationDialog { if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_down"]) => { if self.single_only { - for c in self.content.row_iter(0..(width - 1), 0) { - self.content[c] - .set_fg(highlighted_attrs.fg) - .set_bg(highlighted_attrs.bg) - .set_attrs(highlighted_attrs.attrs); - } self.entries[0].1 = true; - } else { - for c in self.content.row_iter(0..3, 0) { - self.content[c] - .set_fg(highlighted_attrs.fg) - .set_bg(highlighted_attrs.bg) - .set_attrs(highlighted_attrs.attrs); - } } self.cursor = SelectorCursor::Entry(0); self.dirty = true; @@ -604,34 +372,8 @@ impl Component for UIConfirmationDialog { { if self.single_only { // Redraw selection - for c in self.content.row_iter(0..(width - 1), c) { - self.content[c] - .set_fg(self.theme_default.fg) - .set_bg(self.theme_default.bg) - .set_attrs(self.theme_default.attrs); - } - for c in self.content.row_iter(0..(width - 1), c + 1) { - self.content[c] - .set_fg(highlighted_attrs.fg) - .set_bg(highlighted_attrs.bg) - .set_attrs(highlighted_attrs.attrs); - } self.entries[c].1 = false; self.entries[c + 1].1 = true; - } else { - // Redraw cursor - for c in self.content.row_iter(0..3, c) { - self.content[c] - .set_fg(self.theme_default.fg) - .set_bg(self.theme_default.bg) - .set_attrs(self.theme_default.attrs); - } - for c in self.content.row_iter(0..3, c + 1) { - self.content[c] - .set_fg(highlighted_attrs.fg) - .set_bg(highlighted_attrs.bg) - .set_attrs(highlighted_attrs.attrs); - } } self.cursor = SelectorCursor::Entry(c + 1); self.dirty = true; @@ -642,22 +384,6 @@ impl Component for UIConfirmationDialog { && shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_down"]) => { self.cursor = SelectorCursor::Ok; - for c in self.content.row_iter(0..3, c) { - self.content[c] - .set_fg(self.theme_default.fg) - .set_bg(self.theme_default.bg) - .set_attrs(self.theme_default.attrs); - } - for c in self.content.row_iter( - ((width - OK_CANCEL.len()) / 2 + OK_OFFSET) - ..((width - OK_CANCEL.len()) / 2 + OK_OFFSET + OK_LENGTH), - height - 1, - ) { - self.content[c] - .set_fg(highlighted_attrs.fg) - .set_bg(highlighted_attrs.bg) - .set_attrs(highlighted_attrs.attrs); - } self.dirty = true; return true; } @@ -665,26 +391,6 @@ impl Component for UIConfirmationDialog { if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_right"]) => { self.cursor = SelectorCursor::Cancel; - for c in self.content.row_iter( - ((width - OK_CANCEL.len()) / 2 + OK_OFFSET) - ..((width - OK_CANCEL.len()) / 2 + OK_OFFSET + OK_LENGTH), - height - 1, - ) { - self.content[c] - .set_fg(self.theme_default.fg) - .set_bg(self.theme_default.bg) - .set_attrs(self.theme_default.attrs); - } - for c in self.content.row_iter( - ((width - OK_CANCEL.len()) / 2 + CANCEL_OFFSET) - ..((width - OK_CANCEL.len()) / 2 + CANCEL_OFFSET + CANCEL_LENGTH), - height - 1, - ) { - self.content[c] - .set_fg(highlighted_attrs.fg) - .set_bg(highlighted_attrs.bg) - .set_attrs(highlighted_attrs.attrs); - } self.dirty = true; return true; } @@ -692,27 +398,6 @@ impl Component for UIConfirmationDialog { if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_left"]) => { self.cursor = SelectorCursor::Ok; - for c in self.content.row_iter( - ((width - OK_CANCEL.len()) / 2 + OK_OFFSET) - ..((width - OK_CANCEL.len()) / 2 + OK_OFFSET + OK_LENGTH), - height - 1, - ) { - self.content[c] - .set_fg(highlighted_attrs.fg) - .set_bg(highlighted_attrs.bg) - .set_attrs(highlighted_attrs.attrs); - } - - self.content.change_theme( - ( - ((width - OK_CANCEL.len()) / 2 + CANCEL_OFFSET, height - 1), - ( - (width - OK_CANCEL.len()) / 2 + CANCEL_OFFSET + CANCEL_LENGTH, - height - 1, - ), - ), - self.theme_default, - ); self.dirty = true; return true; } @@ -761,7 +446,7 @@ impl Selector { + ) -> Self { let entry_titles = entries .iter_mut() .map(|(_id, ref mut title)| std::mem::take(title)) @@ -773,11 +458,10 @@ impl bool { @@ -871,18 +501,17 @@ impl)>, + filtered_content: Option<(String, Result)>, text_lines: Vec, line_breaker: LineBreakText, movement: Option, @@ -180,7 +181,7 @@ impl Pager { } pub fn filter(&mut self, cmd: &str) { - let _f = |bin: &str, text: &str| -> Result { + let _f = |bin: &str, text: &str| -> Result { use std::{ io::Write, process::{Command, Stdio}, @@ -200,16 +201,16 @@ impl Pager { .chain_err_summary(|| "Failed to wait on filter")? .stdout; let mut dev_null = std::fs::File::open("/dev/null")?; - let mut embedded = crate::terminal::embed::EmbedGrid::new(); + let mut embedded = EmbedGrid::new(); embedded.set_terminal_size((80, 20)); for b in out { embedded.process_byte(&mut dev_null, b); } - Ok(std::mem::take(embedded.buffer_mut())) + Ok(embedded) }; let buf = _f(cmd, &self.text); - if let Some((width, height)) = buf.as_ref().ok().map(CellBuffer::size) { + if let Some((width, height)) = buf.as_ref().ok().map(EmbedGrid::terminal_size) { self.width = width; self.height = height; } @@ -225,7 +226,7 @@ impl Pager { } pub fn initialise(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { - let mut width = width!(area); + let mut width = area.width(); if width < self.minimum_width { width = self.minimum_width; } @@ -253,7 +254,7 @@ impl Pager { ); } if let Some(pos) = search.positions.get(search.cursor) { - if self.cursor.1 > pos.0 || self.cursor.1 + height!(area) < pos.0 { + if self.cursor.1 > pos.0 || self.cursor.1 + area.height() < pos.0 { self.cursor.1 = pos.0.saturating_sub(3); } } @@ -262,7 +263,7 @@ impl Pager { grid, area, context, - self.cursor.1 + Self::PAGES_AHEAD_TO_RENDER_NO * height!(area), + self.cursor.1 + Self::PAGES_AHEAD_TO_RENDER_NO * area.height(), ); } self.draw_page(grid, area, context); @@ -284,10 +285,10 @@ impl Pager { if up_to == 0 { self.text_lines.extend(self.line_breaker.by_ref()); } else { - if old_lines_no >= up_to + height!(area) { + if old_lines_no >= up_to + area.height() { return; } - let new_lines_no = (up_to + height!(area)) - old_lines_no; + let new_lines_no = (up_to + area.height()) - old_lines_no; self.text_lines .extend(self.line_breaker.by_ref().take(new_lines_no)); }; @@ -310,21 +311,14 @@ impl Pager { match filtered_content { Ok(ref content) => { grid.copy_area( - content, + content.buffer(), area, - ( - ( - std::cmp::min( - self.cursor.0, - content.size().0.saturating_sub(width!(area)), - ), - std::cmp::min( - self.cursor.1, - content.size().1.saturating_sub(height!(area)), - ), - ), - pos_dec(content.size(), (1, 1)), - ), + content + .area() + .skip_cols(self.cursor.0) + .skip_rows(self.cursor.1) + .take_cols(content.terminal_size().0.saturating_sub(area.width())) + .take_rows(content.terminal_size().1.saturating_sub(area.height())), ); context .replies @@ -346,86 +340,95 @@ impl Pager { } } - let (mut upper_left, bottom_right) = area; - for l in self - .text_lines - .iter() - .skip(self.cursor.1) - .take(height!(area)) { - grid.write_string( - l, - self.colors.fg, - self.colors.bg, - Attr::DEFAULT, - (upper_left, bottom_right), - None, - ); - if l.starts_with('⤷') { - grid[upper_left] - .set_fg(crate::conf::value(context, "highlight").fg) - .set_attrs(crate::conf::value(context, "highlight").attrs); + let mut area2 = area; + for l in self + .text_lines + .iter() + .skip(self.cursor.1) + .take(area2.height()) + { + if area2.is_empty() { + break; + } + grid.write_string( + l, + self.colors.fg, + self.colors.bg, + Attr::DEFAULT, + area2, + None, + ); + if l.starts_with('⤷') { + grid[area2.upper_left()] + .set_fg(crate::conf::value(context, "highlight").fg) + .set_attrs(crate::conf::value(context, "highlight").attrs); + } + area2 = area2.skip_rows(1); + } + + if area2.height() <= 1 { + grid.clear_area(area2, crate::conf::value(context, "theme_default")); } - upper_left = pos_inc(upper_left, (0, 1)); } - if get_y(upper_left) <= get_y(bottom_right) { - grid.clear_area( - (upper_left, bottom_right), - crate::conf::value(context, "theme_default"), - ); - } - - let (upper_left, _bottom_right) = area; - #[cfg(feature = "regexp")] { - for text_formatter in crate::conf::text_format_regexps(context, "pager.envelope.body") { - let t = grid.insert_tag(text_formatter.tag); - for (i, l) in self - .text_lines - .iter() - .skip(self.cursor.1) - .enumerate() - .take(height!(area) + 1) + #[cfg(feature = "regexp")] + { + let area3 = area; + for text_formatter in + crate::conf::text_format_regexps(context, "pager.envelope.body") { - let i = i + get_y(upper_left); - for (start, end) in text_formatter.regexp.find_iter(l) { - let start = start + get_x(upper_left); - let end = end + get_x(upper_left); - grid.set_tag(t, (start, i), (end, i)); + let t = grid.insert_tag(text_formatter.tag); + for (i, l) in self + .text_lines + .iter() + .skip(self.cursor.1) + .enumerate() + .take(area3.height() + 1) + { + let i = i + area3.upper_left().1; + for (start, end) in text_formatter.regexp.find_iter(l) { + let start = start + area3.upper_left().0; + let end = end + area3.upper_left().0; + grid.set_tag(t, (start, i), (end, i)); + } } } } - } - let cursor_line = self.cursor.1; - if let Some(ref mut search) = self.search { - let results_attr = crate::conf::value(context, "pager.highlight_search"); - let results_current_attr = - crate::conf::value(context, "pager.highlight_search_current"); - search.cursor = std::cmp::min(search.positions.len().saturating_sub(1), search.cursor); - for (i, (y, x)) in search - .positions - .iter() - .enumerate() - .filter(|(_, (y, _))| *y >= cursor_line) - .take(height!(area) + 1) - { - let x = *x + get_x(upper_left); - let y = *y - cursor_line; - for c in grid.row_iter( - x..x + search.pattern.grapheme_width(), - y + get_y(upper_left), - ) { - if i == search.cursor { - grid[c] - .set_fg(results_current_attr.fg) - .set_bg(results_current_attr.bg) - .set_attrs(results_current_attr.attrs); + if let Some(ref mut search) = self.search { + // Last row will be reserved for the "Results for ..." line. + let area3 = area.skip_rows_from_end(1); + let cursor_line = self.cursor.1; + let results_attr = crate::conf::value(context, "pager.highlight_search"); + let results_current_attr = + crate::conf::value(context, "pager.highlight_search_current"); + search.cursor = + std::cmp::min(search.positions.len().saturating_sub(1), search.cursor); + for (i, (y, offset)) in search + .positions + .iter() + .enumerate() + .filter(|(_, &(y, _))| y >= cursor_line && y < cursor_line + area3.height()) + { + let attr = if i == search.cursor { + results_current_attr } else { + results_attr + }; + + let (y, x) = (*y, *offset); + let row_iter = grid.row_iter( + area3.nth_row(y - cursor_line), + x..x + search.pattern.grapheme_width(), + 0, + ); + debug_assert_eq!(row_iter.area().width(), search.pattern.grapheme_width()); + for c in row_iter { grid[c] - .set_fg(results_attr.fg) - .set_bg(results_attr.bg) - .set_attrs(results_attr.attrs); + .set_fg(attr.fg) + .set_bg(attr.bg) + .set_attrs(attr.attrs); } } } @@ -435,9 +438,6 @@ impl Pager { impl Component for Pager { fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { - if !is_valid_area!(area) { - return; - } if !self.is_dirty() { return; } @@ -448,14 +448,42 @@ impl Component for Pager { self.dirty = false; - let height = height!(area); + if self.height == 0 || self.width == 0 { + grid.clear_area(area, crate::conf::value(context, "theme_default")); + return; + } + + let (mut cols, mut rows) = (area.width(), area.height()); + let (has_more_lines, (width, height)) = if self.filtered_content.is_some() { + (false, (self.width, self.height)) + } else { + ( + !self.line_breaker.is_finished(), + (self.line_breaker.width().unwrap_or(cols), self.height), + ) + }; + if cols < 2 || rows < 2 { + return; + } + + if self.show_scrollbar && rows < height { + cols -= 1; + rows -= 1; + } else if self.search.is_some() { + rows -= 1; + } + + if self.show_scrollbar && cols < width { + rows -= 1; + } + if let Some(mvm) = self.movement.take() { match mvm { PageMovement::Up(amount) => { self.cursor.1 = self.cursor.1.saturating_sub(amount); } PageMovement::PageUp(multiplier) => { - self.cursor.1 = self.cursor.1.saturating_sub(height * multiplier); + self.cursor.1 = self.cursor.1.saturating_sub(rows * multiplier); } PageMovement::Down(amount) => { if self.cursor.1 + amount + 1 < self.height { @@ -467,22 +495,22 @@ impl Component for Pager { grid, area, context, - self.cursor.1 + Self::PAGES_AHEAD_TO_RENDER_NO * height, + self.cursor.1 + Self::PAGES_AHEAD_TO_RENDER_NO * rows, ); } PageMovement::PageDown(multiplier) => { - if self.cursor.1 + height * multiplier + 1 < self.height { - self.cursor.1 += height * multiplier; - } else if self.cursor.1 + height * multiplier > self.height { + if self.cursor.1 + rows * multiplier + 1 < self.height { + self.cursor.1 += rows * multiplier; + } else if self.cursor.1 + rows * multiplier > self.height { self.cursor.1 = self.height.saturating_sub(1); } else { - self.cursor.1 = (self.height / height) * height; + self.cursor.1 = (self.height / rows) * rows; } self.draw_lines_up_to( grid, area, context, - self.cursor.1 + Self::PAGES_AHEAD_TO_RENDER_NO * height, + self.cursor.1 + Self::PAGES_AHEAD_TO_RENDER_NO * rows, ); } PageMovement::Right(amount) => { @@ -505,9 +533,6 @@ impl Component for Pager { } } - if self.height == 0 || self.width == 0 { - return; - } if let Some(ref mut search) = self.search { if !search.positions.is_empty() { if let Some(mvm) = search.movement.take() { @@ -529,49 +554,19 @@ impl Component for Pager { } grid.clear_area(area, crate::conf::value(context, "theme_default")); - let (mut cols, mut rows) = (width!(area), height!(area)); - let (has_more_lines, (width, height)) = if self.filtered_content.is_some() { - (false, (self.width, self.height)) - } else { - ( - !self.line_breaker.is_finished(), - (self.line_breaker.width().unwrap_or(cols), self.height), - ) - }; self.cols_lt_width = cols + self.cursor.0 < width; self.rows_lt_height = rows + self.cursor.1 < height; - if cols < 2 || rows < 2 { - return; - } - - if self.show_scrollbar && rows < height { - cols -= 1; - rows -= 1; - } else if self.search.is_some() { - rows -= 1; - } - - if self.show_scrollbar && cols < width { - rows -= 1; - } self.cursor = ( std::cmp::min(width.saturating_sub(cols), self.cursor.0), std::cmp::min(height.saturating_sub(rows), self.cursor.1), ); - self.draw_page( - grid, - (upper_left!(area), pos_inc(upper_left!(area), (cols, rows))), - context, - ); + self.draw_page(grid, area.take_cols(cols).take_rows(rows), context); if self.show_scrollbar && rows < height { ScrollBar::default().set_show_arrows(true).draw( grid, - ( - set_x(upper_left!(area), get_x(bottom_right!(area))), - bottom_right!(area), - ), + area.nth_col(area.width()), context, /* position */ self.cursor.1, @@ -584,10 +579,7 @@ impl Component for Pager { if self.show_scrollbar && cols < width { ScrollBar::default().set_show_arrows(true).draw_horizontal( grid, - ( - set_y(upper_left!(area), get_y(bottom_right!(area))), - bottom_right!(area), - ), + area.nth_row(area.height()), context, self.cursor.0, cols, @@ -635,23 +627,26 @@ impl Component for Pager { if !context.settings.terminal.use_color() { attribute.attrs |= Attr::REVERSE; } - let (_, y) = grid.write_string( + grid.write_string( &status_message, attribute.fg, attribute.bg, attribute.attrs, - ( - set_y(upper_left!(area), get_y(bottom_right!(area))), - bottom_right!(area), - ), + area.nth_row(area.height().saturating_sub(1)), None, ); /* set search pattern to italics */ - let start_x = get_x(upper_left!(area)) + RESULTS_STR.len(); - for c in grid.row_iter(start_x..(start_x + search.pattern.grapheme_width()), y) { + let start_x = RESULTS_STR.len(); + let row_iter = grid.row_iter( + area.nth_row(area.height().saturating_sub(1)), + start_x..(start_x + search.pattern.grapheme_width()), + 0, + ); + debug_assert_eq!(row_iter.area().width(), search.pattern.grapheme_width()); + for c in row_iter { grid[c].set_attrs(attribute.attrs | Attr::ITALICS); } - }; + } } context.dirty_areas.push_back(area); } diff --git a/meli/src/utilities/tables.rs b/meli/src/utilities/tables.rs index ecb26a6a..8c3a2725 100644 --- a/meli/src/utilities/tables.rs +++ b/meli/src/utilities/tables.rs @@ -23,7 +23,10 @@ use std::mem::MaybeUninit; use super::*; -use crate::segment_tree::SegmentTree; +use crate::{ + segment_tree::SegmentTree, + terminal::{Screen, Virtual}, +}; #[derive(Debug, Default, Copy, Clone)] pub enum ColumnElasticity { @@ -122,7 +125,7 @@ impl TableCursorConfig { pub struct DataColumns { pub cursor_config: TableCursorConfig, pub theme_config: TableThemeConfig, - pub columns: Box<[CellBuffer; N]>, + pub columns: Box<[Screen; N]>, /// widths of columns calculated in first draw and after size changes pub widths: [usize; N], pub elasticities: [ColumnElasticity; N], @@ -144,10 +147,11 @@ impl Default for DataColumns { let ptr = &data as *const [MaybeUninit; N]; unsafe { (ptr as *const [T; N]).read() } } + Self { cursor_config: TableCursorConfig::default(), theme_config: TableThemeConfig::default(), - columns: Box::new(init_array(CellBuffer::default)), + columns: Box::new(init_array(|| Screen::init(Virtual))), widths: [0_usize; N], elasticities: [ColumnElasticity::default(); N], x_offset: 0, @@ -183,7 +187,7 @@ impl DataColumns { 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; + self.widths[i] = self.columns[i].cols(); } match self.elasticities[i] { ColumnElasticity::Rigid => {} @@ -191,11 +195,18 @@ impl DataColumns { min, max: Some(max), } => { - self.widths[i] = std::cmp::max(min, std::cmp::min(max, self.widths[i])); + if self.widths[i] < min { + self.widths[i] = min; + } + if self.widths[i] > max { + self.widths[i] = max; + } growees += 1; } ColumnElasticity::Grow { min, max: None } => { - self.widths[i] = std::cmp::max(min, self.widths[i]); + if self.widths[i] < min { + self.widths[i] = min; + } growees += 1; growees_max += 1; } @@ -240,7 +251,7 @@ impl DataColumns { 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)); + let height = total_area.height(); 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 { @@ -258,45 +269,42 @@ impl DataColumns { } let mut column_width = self.widths[col]; - if column_width > bounds.width { - column_width = bounds.width; + 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); - grid.copy_area( - &self.columns[col], - column_area.area(), - ( - (skip_cols.1, top_idx), - ( - column_width.saturating_sub(1), - self.columns[col].rows.saturating_sub(1), - ), - ), + self.columns[col].grid(), + bounds.area(), + self.columns[col] + .area() + .skip_rows(top_idx) + .skip_cols(skip_cols.1) + .take_cols(column_width), ); - let gap_area = column_area.add_x(column_width); - match self.theme_config.theme { - TableTheme::Single(row_attr) => { - grid.change_theme(gap_area.area(), row_attr); - } - TableTheme::EvenOdd { even, odd } => { - grid.change_theme(gap_area.area(), even); - let mut top_idx = top_idx; - for row in gap_area { - if top_idx % 2 != 0 { - grid.change_theme(row.area(), odd); - } - top_idx += 1; - } - } - }; - + bounds.add_x(column_width + 2); skip_cols.1 = 0; } + + match self.theme_config.theme { + TableTheme::Single(row_attr) => { + grid.change_theme(total_area, row_attr); + } + TableTheme::EvenOdd { even, odd } => { + grid.change_theme(total_area, even); + let mut top_idx = top_idx; + for row in 0..total_area.height() { + if top_idx % 2 != 0 { + grid.change_theme(total_area.nth_row(row), odd); + } + top_idx += 1; + } + } + } + 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 { @@ -305,13 +313,13 @@ impl DataColumns { TableTheme::EvenOdd { even: _, odd } => odd, }; - grid.change_theme( - ( - pos_inc(upper_left!(total_area), (0, offset)), - pos_inc(upper_left!(total_area), (width, offset)), - ), - row_attr, - ); + grid.change_theme(total_area.skip_rows(offset).take_rows(1), row_attr); + } + } + + pub fn clear(&mut self) { + for i in 0..N { + self.columns[i].grid_mut().clear(None); } } } diff --git a/meli/src/utilities/text.rs b/meli/src/utilities/text.rs index e13e7721..0c2bf61d 100644 --- a/meli/src/utilities/text.rs +++ b/meli/src/utilities/text.rs @@ -92,8 +92,7 @@ impl TextField { secondary_area: Area, context: &mut Context, ) { - let upper_left = upper_left!(area); - let width = width!(area); + let width = area.width(); let pos = if width < self.inner.grapheme_pos() { width } else { @@ -101,7 +100,7 @@ impl TextField { }; grid.change_colors( - (pos_inc(upper_left, (pos, 0)), pos_inc(upper_left, (pos, 0))), + area.skip_cols(pos).take_cols(1), crate::conf::value(context, "theme_default").fg, crate::conf::value(context, "highlight").bg, ); @@ -119,7 +118,7 @@ impl TextField { impl Component for TextField { fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { let theme_attr = crate::conf::value(context, "widgets.form.field"); - let width = width!(area); + let width = area.width(); let str = self.as_str(); /* Calculate which part of the str is visible * ########################################## diff --git a/meli/src/utilities/widgets.rs b/meli/src/utilities/widgets.rs index 88b8c70b..b35d868c 100644 --- a/meli/src/utilities/widgets.rs +++ b/meli/src/utilities/widgets.rs @@ -347,19 +347,10 @@ impl FormWidget impl Component for FormWidget { fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { - let upper_left = upper_left!(area); - let bottom_right = bottom_right!(area); - if self.is_dirty() { let theme_default = crate::conf::value(context, "theme_default"); - grid.clear_area( - ( - upper_left, - set_y(bottom_right, get_y(upper_left) + self.layout.len()), - ), - theme_default, - ); + grid.clear_area(area.take_rows(self.layout.len()), theme_default); let label_attrs = crate::conf::value(context, "widgets.form.label"); for (i, k) in self.layout.iter().enumerate().rev() { @@ -370,19 +361,13 @@ impl Component for label_attrs.fg, label_attrs.bg, label_attrs.attrs, - ( - pos_inc(upper_left, (1, i)), - set_y(bottom_right, i + get_y(upper_left)), - ), + area.nth_row(i).skip_cols(1), None, ); /* draw field */ v.draw( grid, - ( - pos_inc(upper_left, (self.field_name_max_length + 3, i)), - set_y(bottom_right, i + get_y(upper_left)), - ), + area.nth_row(i).skip_cols(self.field_name_max_length + 3), context, ); @@ -394,10 +379,9 @@ impl Component for if !context.settings.terminal.use_color() { field_attrs.attrs |= Attr::REVERSE; } - for row in grid.bounds_iter(( - pos_inc(upper_left, (0, i)), - (get_x(bottom_right).saturating_sub(1), i + get_y(upper_left)), - )) { + for row in grid + .bounds_iter(area.nth_row(i).take_cols(area.width().saturating_sub(1))) + { for c in row { grid[c] .set_fg(field_attrs.fg) @@ -409,14 +393,9 @@ impl Component for if self.focus == FormFocus::TextInput { v.draw_cursor( grid, - ( - pos_inc(upper_left, (self.field_name_max_length + 3, i)), - (get_x(bottom_right), i + get_y(upper_left)), - ), - ( - pos_inc(upper_left, (self.field_name_max_length + 3, i + 1)), - bottom_right, - ), + area.nth_row(i).skip_cols(self.field_name_max_length + 3), + area.nth_row(i + 1) + .skip_cols(self.field_name_max_length + 3), context, ); } @@ -425,28 +404,13 @@ impl Component for let length = self.layout.len(); - grid.clear_area( - ( - pos_inc(upper_left, (0, length)), - set_y(bottom_right, length + 2 + get_y(upper_left)), - ), - theme_default, - ); + grid.clear_area(area.skip_rows(length).take_rows(length + 2), theme_default); if !self.hide_buttons { - self.buttons.draw( - grid, - ( - pos_inc(upper_left, (1, length + 3)), - set_y(bottom_right, length + 3 + get_y(upper_left)), - ), - context, - ); + self.buttons + .draw(grid, area.nth_row(length + 3).skip_cols(1), context); } - if length + 4 < height!(area) { - grid.clear_area( - (pos_inc(upper_left, (0, length + 4)), bottom_right), - theme_default, - ); + if length + 4 < area.height() { + grid.clear_area(area.skip_rows(length + 3), theme_default); } self.set_dirty(false); context.dirty_areas.push_back(area); @@ -646,7 +610,6 @@ where if self.dirty { let theme_default = crate::conf::value(context, "theme_default"); grid.clear_area(area, theme_default); - let upper_left = upper_left!(area); let mut len = 0; for (i, k) in self.layout.iter().enumerate() { @@ -660,10 +623,7 @@ where theme_default.bg }, Attr::BOLD, - ( - pos_inc(upper_left, (len, 0)), - pos_inc(upper_left, (cur_len + len, 0)), - ), + area.skip_cols(len).take_cols(cur_len + len), None, ); len += cur_len + 3; @@ -755,7 +715,6 @@ impl From<(String, String)> for AutoCompleteEntry { #[derive(Debug, PartialEq, Eq, Clone)] pub struct AutoComplete { entries: Vec, - content: CellBuffer, cursor: usize, dirty: bool, @@ -775,8 +734,7 @@ impl Component for AutoComplete { }; self.dirty = false; - let (upper_left, bottom_right) = area; - let rows = get_y(bottom_right) - get_y(upper_left); + let rows = area.height(); if rows == 0 { return; } @@ -784,43 +742,62 @@ impl Component for AutoComplete { let top_idx = page_no * rows; let x_offset = if rows < self.entries.len() { 1 } else { 0 }; - let (width, height) = self.content.size(); grid.clear_area(area, crate::conf::value(context, "theme_default")); + let width = self + .entries + .iter() + .map(|a| a.entry.grapheme_len() + a.description.grapheme_len() + 2) + .max() + .unwrap_or(0) + + 1; + // [ref:hardcoded_color_value] + let theme_attr = ThemeAttribute { + fg: Color::Byte(23), + bg: Color::Byte(7), + attrs: Attr::DEFAULT, + }; + grid.change_theme(area, theme_attr); + for (i, e) in self.entries.iter().skip(top_idx).enumerate() { + let (x, _) = grid.write_string( + &e.entry, + Color::Byte(23), + Color::Byte(7), + Attr::DEFAULT, + area.nth_row(i).take_cols(width), + None, + ); + grid.write_string( + &e.description, + Color::Byte(23), + Color::Byte(7), + Attr::ITALICS, + area.nth_row(i).skip_cols(x + 2).take_cols(width), + None, + ); + grid.write_string( + "▒", + Color::Byte(23), + Color::Byte(7), + Attr::DEFAULT, + area.nth_row(i).skip_cols(width - 1), + None, + ); + } - grid.copy_area( - &self.content, - (upper_left, pos_dec(bottom_right, (x_offset, 0))), - ( - (0, top_idx), - (width.saturating_sub(1), height.saturating_sub(1)), - ), - ); /* Highlight cursor */ if self.cursor > 0 { let highlight = crate::conf::value(context, "highlight"); grid.change_theme( - ( - pos_inc(upper_left, (0, (self.cursor - 1) % rows)), - ( - std::cmp::min( - get_x(upper_left) + width.saturating_sub(1), - get_x(bottom_right), - ) - .saturating_sub(x_offset), - get_y(pos_inc(upper_left, (0, (self.cursor - 1) % rows))), - ), - ), + area.nth_row((self.cursor - 1) % rows) + .skip_cols(width.saturating_sub(1 + x_offset)), highlight, ); } if rows < self.entries.len() { ScrollBar { show_arrows: false }.draw( grid, - ( - set_y(pos_dec(bottom_right, (x_offset, 0)), get_y(upper_left)), - pos_dec(bottom_right, (x_offset, 0)), - ), + area.take_rows(x_offset), context, self.cursor.saturating_sub(1), rows, @@ -829,12 +806,15 @@ impl Component for AutoComplete { } context.dirty_areas.push_back(area); } + fn process_event(&mut self, _event: &mut UIEvent, _context: &mut Context) -> bool { false } + fn is_dirty(&self) -> bool { self.dirty } + fn set_dirty(&mut self, value: bool) { self.dirty = value; } @@ -848,7 +828,6 @@ impl AutoComplete { pub fn new(entries: Vec) -> Box { let mut ret = AutoComplete { entries: Vec::new(), - content: CellBuffer::default(), cursor: 0, dirty: true, id: ComponentId::default(), @@ -862,45 +841,6 @@ impl AutoComplete { return false; } - // [ref:hardcoded_color_value] - let mut content = CellBuffer::new( - entries - .iter() - .map(|a| a.entry.grapheme_len() + a.description.grapheme_len() + 2) - .max() - .unwrap_or(0) - + 1, - entries.len(), - Cell::with_style(Color::Byte(23), Color::Byte(7), Attr::DEFAULT), - ); - let width = content.cols(); - for (i, e) in entries.iter().enumerate() { - let (x, _) = content.write_string( - &e.entry, - Color::Byte(23), - Color::Byte(7), - Attr::DEFAULT, - ((0, i), (width - 1, i)), - None, - ); - content.write_string( - &e.description, - Color::Byte(23), - Color::Byte(7), - Attr::ITALICS, - ((x + 2, i), (width - 1, i)), - None, - ); - content.write_string( - "▒", - Color::Byte(23), - Color::Byte(7), - Attr::DEFAULT, - ((width - 1, i), (width - 1, i)), - None, - ); - } - self.content = content; self.entries = entries; self.cursor = 0; true @@ -933,7 +873,6 @@ impl AutoComplete { let ret = self.entries.remove(self.cursor - 1); self.entries.clear(); self.cursor = 0; - self.content.empty(); Some(ret.entry) } @@ -965,7 +904,7 @@ impl ScrollBar { if length == 0 { return; } - let height = height!(area); + let height = area.height(); if height < 3 { return; } @@ -977,21 +916,21 @@ impl ScrollBar { let ratio: f64 = (height as f64) / (length as f64); let scrollbar_height = std::cmp::max((ratio * (visible_rows as f64)) as usize, 1); let scrollbar_offset = (ratio * (pos as f64)) as usize; - let (mut upper_left, bottom_right) = area; + let mut area2 = area; if self.show_arrows { - grid[upper_left] + grid[area2.upper_left()] .set_ch(if ascii_drawing { '^' } else { '▀' }) .set_fg(crate::conf::value(context, "widgets.options.highlighted").bg); - upper_left = pos_inc(upper_left, (0, 1)); + area2 = area2.skip_rows(1); } - upper_left = pos_inc(upper_left, (0, scrollbar_offset)); + area2 = area2.skip_rows(scrollbar_offset); for _ in 0..scrollbar_height { - if get_y(upper_left) >= get_y(bottom_right) { + if area2.is_empty() { break; } - grid[upper_left] + grid[area2.upper_left()] .set_ch(if ascii_drawing { '#' } else { '█' }) .set_fg(crate::conf::value(context, "widgets.options.highlighted").bg) .set_attrs(if !context.settings.terminal.use_color() { @@ -999,10 +938,10 @@ impl ScrollBar { } else { theme_default.attrs }); - upper_left = pos_inc(upper_left, (0, 1)); + area2 = area2.skip_rows(1); } if self.show_arrows { - grid[bottom_right] + grid[area2.bottom_right()] .set_ch(if ascii_drawing { 'v' } else { '▄' }) .set_fg(crate::conf::value(context, "widgets.options.highlighted").bg) .set_bg(crate::conf::value(context, "theme_default").bg); @@ -1021,7 +960,7 @@ impl ScrollBar { if length == 0 { return; } - let width = width!(area); + let width = area.width(); if width < 3 { return; } @@ -1033,21 +972,21 @@ impl ScrollBar { let ratio: f64 = (width as f64) / (length as f64); let scrollbar_width = std::cmp::min((ratio * (visible_cols as f64)) as usize, 1); let scrollbar_offset = (ratio * (pos as f64)) as usize; - let (mut upper_left, bottom_right) = area; + let mut area2 = area; if self.show_arrows { - grid[upper_left] + grid[area2.upper_left()] .set_ch(if ascii_drawing { '<' } else { '▐' }) .set_fg(crate::conf::value(context, "widgets.options.highlighted").bg); - upper_left = pos_inc(upper_left, (1, 0)); + area2 = area2.skip_cols(1); } - upper_left = pos_inc(upper_left, (scrollbar_offset, 0)); + area2 = area2.skip_cols(scrollbar_offset); for _ in 0..scrollbar_width { - if get_x(upper_left) >= get_x(bottom_right) { + if area2.is_empty() { break; } - grid[upper_left] + grid[area2.upper_left()] .set_ch(if ascii_drawing { '#' } else { '█' }) .set_fg(crate::conf::value(context, "widgets.options.highlighted").bg) .set_attrs(if !context.settings.terminal.use_color() { @@ -1055,10 +994,10 @@ impl ScrollBar { } else { theme_default.attrs }); - upper_left = pos_inc(upper_left, (1, 0)); + area2 = area2.skip_cols(1); } if self.show_arrows { - grid[bottom_right] + grid[area2.bottom_right()] .set_ch(if ascii_drawing { '>' } else { '▌' }) .set_fg(crate::conf::value(context, "widgets.options.highlighted").bg) .set_bg(crate::conf::value(context, "theme_default").bg); diff --git a/tools/src/embed.rs b/tools/src/embed.rs index ce5964f2..0385da00 100644 --- a/tools/src/embed.rs +++ b/tools/src/embed.rs @@ -127,7 +127,7 @@ impl Component for EmbedContainer { embed_area, ((0, 0), pos_dec(guard.grid.terminal_size, (1, 1))), ); - guard.set_terminal_size((width!(embed_area), height!(embed_area))); + guard.set_terminal_size((embed_area.width(), embed_area.height())); context.dirty_areas.push_back(area); self.dirty = false; return; @@ -160,12 +160,12 @@ impl Component for EmbedContainer { let inner_area = create_box( grid, ( - pos_inc(upper_left!(area), (1, 0)), + pos_inc(area.upper_left(), (1, 0)), pos_inc( - upper_left!(area), + area.upper_left(), ( - std::cmp::min(max_len + 5, width!(area)), - std::cmp::min(5, height!(area)), + std::cmp::min(max_len + 5, area.width()), + std::cmp::min(5, area.height()), ), ), ), @@ -185,10 +185,10 @@ impl Component for EmbedContainer { theme_default.bg, theme_default.attrs, ( - pos_inc((0, i), upper_left!(inner_area)), - bottom_right!(inner_area), + pos_inc((0, i), inner_area.upper_left()), + inner_area.bottom_right(), ), - Some(get_x(upper_left!(inner_area))), + Some(get_x(inner_area.upper_left())), ); } } @@ -196,10 +196,10 @@ impl Component for EmbedContainer { } else { let theme_default = crate::conf::value(context, "theme_default"); grid.clear_area(area, theme_default); - self.embed_area = (upper_left!(area), bottom_right!(area)); + self.embed_area = (area.upper_left(), area.bottom_right()); match create_pty( - width!(self.embed_area), - height!(self.embed_area), + self.embed_area.width(), + self.embed_area.height(), self.command.clone(), ) { Ok(embed) => {