From 6d63429ad35a98b3bd1a83989832e20609f79b0a Mon Sep 17 00:00:00 2001 From: Manos Pitsidianakis Date: Thu, 7 Jan 2021 20:26:17 +0200 Subject: [PATCH] Add scrolling context to StatusBar - Whenever a scrolling context is entered/exited, send a ScrollUpdate event. - StatusBar maintains a stack of scrolling contexts and displays the last one, if it exists. Each context is associated with a ComponentId. - To handle dangling contexts after their Components aren't visible anymore, send a VisibilityChange event in situations where that scenario is possible. --- src/components.rs | 16 ++++ src/components/contacts/contact_list.rs | 31 +++++++ src/components/mail/compose.rs | 3 + src/components/mail/listing.rs | 41 +++++++++ src/components/mail/listing/compact.rs | 2 + src/components/mail/listing/conversations.rs | 2 + src/components/mail/listing/plain.rs | 2 + src/components/mail/listing/thread.rs | 3 + src/components/utilities.rs | 93 ++++++++++++++++++++ src/components/utilities/pager.rs | 93 ++++++++++---------- src/types.rs | 4 +- 11 files changed, 242 insertions(+), 48 deletions(-) diff --git a/src/components.rs b/src/components.rs index a22a57bf..6aa1a9ad 100644 --- a/src/components.rs +++ b/src/components.rs @@ -66,6 +66,22 @@ pub enum PageMovement { End, } +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct ScrollContext { + shown_lines: usize, + total_lines: usize, + has_more_lines: bool, +} + +#[derive(Debug, Clone, Copy)] +pub enum ScrollUpdate { + End(ComponentId), + Update { + id: ComponentId, + context: ScrollContext, + }, +} + /// Types implementing this Trait can draw on the terminal and receive events. /// If a type wants to skip drawing if it has not changed anything, it can hold some flag in its /// fields (eg self.dirty = false) and act upon that in their `draw` implementation. diff --git a/src/components/contacts/contact_list.rs b/src/components/contacts/contact_list.rs index e0b345e3..54e96211 100644 --- a/src/components/contacts/contact_list.rs +++ b/src/components/contacts/contact_list.rs @@ -414,6 +414,27 @@ impl ContactList { let top_idx = page_no * rows; + if self.length >= rows { + context + .replies + .push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate( + ScrollUpdate::Update { + id: self.id, + context: ScrollContext { + shown_lines: top_idx + rows, + total_lines: self.length, + has_more_lines: false, + }, + }, + ))); + } else { + context + .replies + .push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate( + ScrollUpdate::End(self.id), + ))); + } + /* If cursor position has changed, remove the highlight from the previous position and * apply it in the new one. */ if self.cursor_pos != self.new_cursor_pos && prev_page_no == page_no { @@ -621,6 +642,11 @@ impl Component for ContactList { self.mode = ViewMode::View(manager.id()); self.view = Some(manager); + context + .replies + .push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate( + ScrollUpdate::End(self.id), + ))); return true; } @@ -639,6 +665,11 @@ impl Component for ContactList { self.mode = ViewMode::View(manager.id()); self.view = Some(manager); + context + .replies + .push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate( + ScrollUpdate::End(self.id), + ))); return true; } diff --git a/src/components/mail/compose.rs b/src/components/mail/compose.rs index ffcd1f1a..1e3d5672 100644 --- a/src/components/mail/compose.rs +++ b/src/components/mail/compose.rs @@ -925,6 +925,9 @@ impl Component for Composer { } fn process_event(&mut self, mut event: &mut UIEvent, context: &mut Context) -> bool { + if let UIEvent::VisibilityChange(_) = event { + self.pager.process_event(event, context); + } let shortcuts = self.get_shortcuts(context); match (&mut self.mode, &mut event) { (ViewMode::Edit, _) => { diff --git a/src/components/mail/listing.rs b/src/components/mail/listing.rs index 067b1321..b31ef62c 100644 --- a/src/components/mail/listing.rs +++ b/src/components/mail/listing.rs @@ -703,6 +703,8 @@ impl Component for Listing { fallback = *cur; } if self.component.coordinates() == (*account_hash, *mailbox_hash) { + self.component + .process_event(&mut UIEvent::VisibilityChange(false), context); self.component.set_coordinates(( self.accounts[self.cursor_pos.0].hash, self.accounts[self.cursor_pos.0].entries[fallback].3, @@ -730,6 +732,8 @@ impl Component for Listing { let account_hash = self.accounts[self.cursor_pos.0].hash; self.cursor_pos.1 = MenuEntryCursor::Mailbox(*idx); self.status = None; + self.component + .process_event(&mut UIEvent::VisibilityChange(false), context); self.component .set_coordinates((account_hash, *mailbox_hash)); self.menu_content.empty(); @@ -1148,6 +1152,11 @@ impl Component for Listing { match *event { UIEvent::Input(Key::Right) => { self.focus = ListingFocus::Mailbox; + context + .replies + .push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate( + ScrollUpdate::End(self.id), + ))); self.ratio = 90; self.set_dirty(true); return true; @@ -1161,6 +1170,11 @@ impl Component for Listing { self.set_dirty(true); self.focus = ListingFocus::Mailbox; self.ratio = 90; + context + .replies + .push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate( + ScrollUpdate::End(self.id), + ))); return true; } UIEvent::Input(ref k) @@ -1171,6 +1185,11 @@ impl Component for Listing { self.focus = ListingFocus::Mailbox; self.ratio = 90; self.set_dirty(true); + context + .replies + .push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate( + ScrollUpdate::End(self.id), + ))); context .replies .push_back(UIEvent::StatusEvent(StatusEvent::UpdateStatus( @@ -1650,6 +1669,20 @@ impl Listing { ), ); if self.show_menu_scrollbar == ShowMenuScrollbar::True && total_height > rows { + if self.focus == ListingFocus::Menu { + context + .replies + .push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate( + ScrollUpdate::Update { + id: self.id, + context: ScrollContext { + shown_lines: skip_offset + rows, + total_lines: total_height, + has_more_lines: false, + }, + }, + ))); + } ScrollBar::default().set_show_arrows(true).draw( grid, ( @@ -1664,6 +1697,12 @@ impl Listing { /* length */ total_height, ); + } else if total_height < rows { + context + .replies + .push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate( + ScrollUpdate::End(self.id), + ))); } context.dirty_areas.push_back(area); @@ -1964,6 +2003,8 @@ impl Listing { if let Some((_, _, _, mailbox_hash)) = self.accounts[self.cursor_pos.0].entries.get(idx) { + self.component + .process_event(&mut UIEvent::VisibilityChange(false), context); self.component .set_coordinates((account_hash, *mailbox_hash)); /* Check if per-mailbox configuration overrides general configuration */ diff --git a/src/components/mail/listing/compact.rs b/src/components/mail/listing/compact.rs index 9df46905..c24b4405 100644 --- a/src/components/mail/listing/compact.rs +++ b/src/components/mail/listing/compact.rs @@ -1627,6 +1627,8 @@ impl Component for CompactListing { ) => { self.unfocused = false; + self.view + .process_event(&mut UIEvent::VisibilityChange(false), context); self.dirty = true; /* If self.row_updates is not empty and we exit a thread, the row_update events * will be performed but the list will not be drawn. So force a draw in any case. diff --git a/src/components/mail/listing/conversations.rs b/src/components/mail/listing/conversations.rs index 1c14a6b1..53e1b11c 100644 --- a/src/components/mail/listing/conversations.rs +++ b/src/components/mail/listing/conversations.rs @@ -1491,6 +1491,8 @@ impl Component for ConversationsListing { ) => { self.unfocused = false; + self.view + .process_event(&mut UIEvent::VisibilityChange(false), context); self.dirty = true; /* If self.row_updates is not empty and we exit a thread, the row_update events * will be performed but the list will not be drawn. So force a draw in any case. diff --git a/src/components/mail/listing/plain.rs b/src/components/mail/listing/plain.rs index 993cf1d7..dd044e56 100644 --- a/src/components/mail/listing/plain.rs +++ b/src/components/mail/listing/plain.rs @@ -1146,6 +1146,8 @@ impl Component for PlainListing { && shortcut!(k == shortcuts[PlainListing::DESCRIPTION]["exit_thread"]) => { self.unfocused = false; + self.view + .process_event(&mut UIEvent::VisibilityChange(false), context); self.dirty = true; /* If self.row_updates is not empty and we exit a thread, the row_update events * will be performed but the list will not be drawn. So force a draw in any case. diff --git a/src/components/mail/listing/thread.rs b/src/components/mail/listing/thread.rs index e21bfb6a..58566129 100644 --- a/src/components/mail/listing/thread.rs +++ b/src/components/mail/listing/thread.rs @@ -1224,6 +1224,9 @@ impl Component for ThreadListing { } UIEvent::Input(Key::Char('i')) if self.unfocused => { self.unfocused = false; + if let Some(ref mut s) = self.view { + s.process_event(&mut UIEvent::VisibilityChange(false), context); + } self.dirty = true; self.view = None; return true; diff --git a/src/components/utilities.rs b/src/components/utilities.rs index f1a233ed..9d2d91ca 100644 --- a/src/components/utilities.rs +++ b/src/components/utilities.rs @@ -64,6 +64,7 @@ pub struct StatusBar { progress_spinner: ProgressSpinner, in_progress_jobs: HashSet, done_jobs: HashSet, + scroll_contexts: IndexMap, auto_complete: AutoComplete, cmd_history: Vec, @@ -104,6 +105,7 @@ impl StatusBar { progress_spinner, in_progress_jobs: HashSet::default(), done_jobs: HashSet::default(), + scroll_contexts: IndexMap::default(), cmd_history: crate::command::history::old_cmd_history(), } } @@ -140,6 +142,33 @@ impl StatusBar { grid[(x, y)].set_attrs(attribute.attrs | Attr::BOLD); } } + if let Some(( + _, + ScrollContext { + shown_lines, + total_lines, + has_more_lines, + }, + )) = self.scroll_contexts.last() + { + let s = format!( + "| {shown_percentage}% {line_desc}{shown_lines}/{total_lines}{has_more_lines}", + line_desc = if grid.ascii_drawing { "lines:" } else { "☰ " }, + shown_percentage = (*shown_lines as f32 / (*total_lines as f32) * 100.0) as usize, + shown_lines = *shown_lines, + total_lines = *total_lines, + has_more_lines = if *has_more_lines { "(+)" } else { "" } + ); + write_string_to_grid( + &s, + grid, + attribute.fg, + attribute.bg, + attribute.attrs, + ((x + 1, y), bottom_right!(area)), + None, + ); + } let (mut x, y) = bottom_right!(area); if self.progress_spinner.is_active() { @@ -706,6 +735,21 @@ impl Component for StatusBar { self.progress_spinner.set_dirty(true); self.in_progress_jobs.insert(*job_id); } + UIEvent::StatusEvent(StatusEvent::ScrollUpdate(ScrollUpdate::End(component_id))) => { + if self.scroll_contexts.remove(component_id).is_some() { + self.dirty = true; + } + return true; + } + UIEvent::StatusEvent(StatusEvent::ScrollUpdate(ScrollUpdate::Update { + id, + context, + })) => { + if self.scroll_contexts.insert(*id, *context) != Some(*context) { + self.dirty = true; + } + return true; + } UIEvent::Timer(_) => { if self.progress_spinner.process_event(event, context) { return true; @@ -975,6 +1019,21 @@ impl Component for Tabbed { ), ); 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, ( @@ -989,6 +1048,12 @@ impl Component for Tabbed { /* length */ height, ); + } else { + context + .replies + .push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate( + ScrollUpdate::End(self.id), + ))); } self.dirty = false; return; @@ -1192,6 +1257,21 @@ impl Component for Tabbed { ), ); 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, ( @@ -1206,6 +1286,12 @@ impl Component for Tabbed { /* length */ height, ); + } else { + context + .replies + .push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate( + ScrollUpdate::End(self.id), + ))); } } self.dirty = false; @@ -1250,6 +1336,11 @@ impl Component for Tabbed { if self.show_shortcuts { /* children below the shortcut overlay must be redrawn */ self.set_dirty(true); + context + .replies + .push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate( + ScrollUpdate::End(self.id), + ))); } self.show_shortcuts = !self.show_shortcuts; self.dirty = true; @@ -1281,6 +1372,8 @@ impl Component for Tabbed { return true; } if let Some(c_idx) = self.children.iter().position(|x| x.id() == *id) { + self.children[c_idx] + .process_event(&mut UIEvent::VisibilityChange(false), context); self.children.remove(c_idx); self.cursor_pos = 0; self.set_dirty(true); diff --git a/src/components/utilities/pager.rs b/src/components/utilities/pager.rs index da46653e..fb3acd6a 100644 --- a/src/components/utilities/pager.rs +++ b/src/components/utilities/pager.rs @@ -533,28 +533,30 @@ impl Component for Pager { } if (rows < height) || self.search.is_some() { const RESULTS_STR: &str = "Results for "; - let shown_percentage = - ((self.cursor.1 + rows) as f32 / (height as f32) * 100.0) as usize; let shown_lines = self.cursor.1 + rows; let total_lines = height; - let scrolling = if rows < height { - format!( - "{shown_percentage}% {line_desc}{shown_lines}/{total_lines}{has_more_lines}", - line_desc = if grid.ascii_drawing { "lines:" } else { "☰ " }, - shown_percentage = shown_percentage, - shown_lines = shown_lines, - total_lines = total_lines, - has_more_lines = if self.line_breaker.is_finished() { - "" - } else { - "(+)" - } - ) + if rows < height { + context + .replies + .push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate( + ScrollUpdate::Update { + id: self.id, + context: ScrollContext { + shown_lines, + total_lines, + has_more_lines: !self.line_breaker.is_finished(), + }, + }, + ))); } else { - String::new() + context + .replies + .push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate( + ScrollUpdate::End(self.id), + ))); }; - let search_results = if let Some(ref search) = self.search { - format!( + if let Some(ref search) = self.search { + let status_message = format!( "{results_str}{search_pattern}: {current_pos}/{total_results}{has_more_lines}", results_str = RESULTS_STR, search_pattern = &search.pattern, @@ -569,39 +571,29 @@ impl Component for Pager { } else { "(+)" } - ) - } else { - String::new() - }; - let status_message = format!( - "{search_results}{divider}{scrolling}", - search_results = search_results, - divider = if self.search.is_some() { " " } else { "" }, - scrolling = scrolling, - ); - let mut attribute = crate::conf::value(context, "status.bar"); - if !context.settings.terminal.use_color() { - attribute.attrs |= Attr::REVERSE; - } - let (_, y) = write_string_to_grid( - &status_message, - grid, - attribute.fg, - attribute.bg, - attribute.attrs, - ( - set_y(upper_left!(area), get_y(bottom_right!(area))), - bottom_right!(area), - ), - None, - ); - /* set search pattern to italics */ - if let Some(ref search) = self.search { + ); + let mut attribute = crate::conf::value(context, "status.bar"); + if !context.settings.terminal.use_color() { + attribute.attrs |= Attr::REVERSE; + } + let (_, y) = write_string_to_grid( + &status_message, + grid, + attribute.fg, + attribute.bg, + attribute.attrs, + ( + set_y(upper_left!(area), get_y(bottom_right!(area))), + bottom_right!(area), + ), + 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) { grid[c].set_attrs(attribute.attrs | Attr::ITALICS); } - } + }; } context.dirty_areas.push_back(area); } @@ -746,6 +738,13 @@ impl Component for Pager { self.initialised = false; self.dirty = true; } + UIEvent::VisibilityChange(false) => { + context + .replies + .push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate( + ScrollUpdate::End(self.id), + ))); + } _ => {} } false diff --git a/src/types.rs b/src/types.rs index 47dc6a02..95c14c16 100644 --- a/src/types.rs +++ b/src/types.rs @@ -38,7 +38,7 @@ pub use self::helpers::*; use super::command::Action; use super::jobs::{JobExecutor, JobId}; use super::terminal::*; -use crate::components::{Component, ComponentId}; +use crate::components::{Component, ComponentId, ScrollUpdate}; use std::sync::Arc; use melib::backends::{AccountHash, BackendEvent, MailboxHash}; @@ -57,6 +57,7 @@ pub enum StatusEvent { JobFinished(JobId), JobCanceled(JobId), SetMouse(bool), + ScrollUpdate(ScrollUpdate), } /// `ThreadEvent` encapsulates all of the possible values we need to transfer between our threads @@ -149,6 +150,7 @@ pub enum UIEvent { ConfigReload { old_settings: crate::conf::Settings, }, + VisibilityChange(bool), } pub struct CallbackFn(pub Box () + Send + 'static>);