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.
lazy_fetch
Manos Pitsidianakis 2021-01-07 20:26:17 +02:00
parent 5eb4342af8
commit 6d63429ad3
Signed by: Manos Pitsidianakis
GPG Key ID: 73627C2F690DF710
11 changed files with 242 additions and 48 deletions

View File

@ -66,6 +66,22 @@ pub enum PageMovement {
End, 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. /// 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 /// 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. /// fields (eg self.dirty = false) and act upon that in their `draw` implementation.

View File

@ -414,6 +414,27 @@ impl ContactList {
let top_idx = page_no * rows; 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 /* If cursor position has changed, remove the highlight from the previous position and
* apply it in the new one. */ * apply it in the new one. */
if self.cursor_pos != self.new_cursor_pos && prev_page_no == page_no { 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.mode = ViewMode::View(manager.id());
self.view = Some(manager); self.view = Some(manager);
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
ScrollUpdate::End(self.id),
)));
return true; return true;
} }
@ -639,6 +665,11 @@ impl Component for ContactList {
self.mode = ViewMode::View(manager.id()); self.mode = ViewMode::View(manager.id());
self.view = Some(manager); self.view = Some(manager);
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
ScrollUpdate::End(self.id),
)));
return true; return true;
} }

View File

@ -925,6 +925,9 @@ impl Component for Composer {
} }
fn process_event(&mut self, mut event: &mut UIEvent, context: &mut Context) -> bool { 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); let shortcuts = self.get_shortcuts(context);
match (&mut self.mode, &mut event) { match (&mut self.mode, &mut event) {
(ViewMode::Edit, _) => { (ViewMode::Edit, _) => {

View File

@ -703,6 +703,8 @@ impl Component for Listing {
fallback = *cur; fallback = *cur;
} }
if self.component.coordinates() == (*account_hash, *mailbox_hash) { if self.component.coordinates() == (*account_hash, *mailbox_hash) {
self.component
.process_event(&mut UIEvent::VisibilityChange(false), context);
self.component.set_coordinates(( self.component.set_coordinates((
self.accounts[self.cursor_pos.0].hash, self.accounts[self.cursor_pos.0].hash,
self.accounts[self.cursor_pos.0].entries[fallback].3, 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; let account_hash = self.accounts[self.cursor_pos.0].hash;
self.cursor_pos.1 = MenuEntryCursor::Mailbox(*idx); self.cursor_pos.1 = MenuEntryCursor::Mailbox(*idx);
self.status = None; self.status = None;
self.component
.process_event(&mut UIEvent::VisibilityChange(false), context);
self.component self.component
.set_coordinates((account_hash, *mailbox_hash)); .set_coordinates((account_hash, *mailbox_hash));
self.menu_content.empty(); self.menu_content.empty();
@ -1148,6 +1152,11 @@ impl Component for Listing {
match *event { match *event {
UIEvent::Input(Key::Right) => { UIEvent::Input(Key::Right) => {
self.focus = ListingFocus::Mailbox; self.focus = ListingFocus::Mailbox;
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
ScrollUpdate::End(self.id),
)));
self.ratio = 90; self.ratio = 90;
self.set_dirty(true); self.set_dirty(true);
return true; return true;
@ -1161,6 +1170,11 @@ impl Component for Listing {
self.set_dirty(true); self.set_dirty(true);
self.focus = ListingFocus::Mailbox; self.focus = ListingFocus::Mailbox;
self.ratio = 90; self.ratio = 90;
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
ScrollUpdate::End(self.id),
)));
return true; return true;
} }
UIEvent::Input(ref k) UIEvent::Input(ref k)
@ -1171,6 +1185,11 @@ impl Component for Listing {
self.focus = ListingFocus::Mailbox; self.focus = ListingFocus::Mailbox;
self.ratio = 90; self.ratio = 90;
self.set_dirty(true); self.set_dirty(true);
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
ScrollUpdate::End(self.id),
)));
context context
.replies .replies
.push_back(UIEvent::StatusEvent(StatusEvent::UpdateStatus( .push_back(UIEvent::StatusEvent(StatusEvent::UpdateStatus(
@ -1650,6 +1669,20 @@ impl Listing {
), ),
); );
if self.show_menu_scrollbar == ShowMenuScrollbar::True && total_height > rows { 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( ScrollBar::default().set_show_arrows(true).draw(
grid, grid,
( (
@ -1664,6 +1697,12 @@ impl Listing {
/* length */ /* length */
total_height, 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); context.dirty_areas.push_back(area);
@ -1964,6 +2003,8 @@ impl Listing {
if let Some((_, _, _, mailbox_hash)) = if let Some((_, _, _, mailbox_hash)) =
self.accounts[self.cursor_pos.0].entries.get(idx) self.accounts[self.cursor_pos.0].entries.get(idx)
{ {
self.component
.process_event(&mut UIEvent::VisibilityChange(false), context);
self.component self.component
.set_coordinates((account_hash, *mailbox_hash)); .set_coordinates((account_hash, *mailbox_hash));
/* Check if per-mailbox configuration overrides general configuration */ /* Check if per-mailbox configuration overrides general configuration */

View File

@ -1627,6 +1627,8 @@ impl Component for CompactListing {
) => ) =>
{ {
self.unfocused = false; self.unfocused = false;
self.view
.process_event(&mut UIEvent::VisibilityChange(false), context);
self.dirty = true; self.dirty = true;
/* If self.row_updates is not empty and we exit a thread, the row_update events /* 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. * will be performed but the list will not be drawn. So force a draw in any case.

View File

@ -1491,6 +1491,8 @@ impl Component for ConversationsListing {
) => ) =>
{ {
self.unfocused = false; self.unfocused = false;
self.view
.process_event(&mut UIEvent::VisibilityChange(false), context);
self.dirty = true; self.dirty = true;
/* If self.row_updates is not empty and we exit a thread, the row_update events /* 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. * will be performed but the list will not be drawn. So force a draw in any case.

View File

@ -1146,6 +1146,8 @@ impl Component for PlainListing {
&& shortcut!(k == shortcuts[PlainListing::DESCRIPTION]["exit_thread"]) => && shortcut!(k == shortcuts[PlainListing::DESCRIPTION]["exit_thread"]) =>
{ {
self.unfocused = false; self.unfocused = false;
self.view
.process_event(&mut UIEvent::VisibilityChange(false), context);
self.dirty = true; self.dirty = true;
/* If self.row_updates is not empty and we exit a thread, the row_update events /* 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. * will be performed but the list will not be drawn. So force a draw in any case.

View File

@ -1224,6 +1224,9 @@ impl Component for ThreadListing {
} }
UIEvent::Input(Key::Char('i')) if self.unfocused => { UIEvent::Input(Key::Char('i')) if self.unfocused => {
self.unfocused = false; self.unfocused = false;
if let Some(ref mut s) = self.view {
s.process_event(&mut UIEvent::VisibilityChange(false), context);
}
self.dirty = true; self.dirty = true;
self.view = None; self.view = None;
return true; return true;

View File

@ -64,6 +64,7 @@ pub struct StatusBar {
progress_spinner: ProgressSpinner, progress_spinner: ProgressSpinner,
in_progress_jobs: HashSet<JobId>, in_progress_jobs: HashSet<JobId>,
done_jobs: HashSet<JobId>, done_jobs: HashSet<JobId>,
scroll_contexts: IndexMap<ComponentId, ScrollContext>,
auto_complete: AutoComplete, auto_complete: AutoComplete,
cmd_history: Vec<String>, cmd_history: Vec<String>,
@ -104,6 +105,7 @@ impl StatusBar {
progress_spinner, progress_spinner,
in_progress_jobs: HashSet::default(), in_progress_jobs: HashSet::default(),
done_jobs: HashSet::default(), done_jobs: HashSet::default(),
scroll_contexts: IndexMap::default(),
cmd_history: crate::command::history::old_cmd_history(), cmd_history: crate::command::history::old_cmd_history(),
} }
} }
@ -140,6 +142,33 @@ impl StatusBar {
grid[(x, y)].set_attrs(attribute.attrs | Attr::BOLD); 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); let (mut x, y) = bottom_right!(area);
if self.progress_spinner.is_active() { if self.progress_spinner.is_active() {
@ -706,6 +735,21 @@ impl Component for StatusBar {
self.progress_spinner.set_dirty(true); self.progress_spinner.set_dirty(true);
self.in_progress_jobs.insert(*job_id); 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(_) => { UIEvent::Timer(_) => {
if self.progress_spinner.process_event(event, context) { if self.progress_spinner.process_event(event, context) {
return true; return true;
@ -975,6 +1019,21 @@ impl Component for Tabbed {
), ),
); );
if height.wrapping_div(rows + 1) > 0 || width.wrapping_div(cols + 1) > 0 { 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( ScrollBar::default().set_show_arrows(true).draw(
grid, grid,
( (
@ -989,6 +1048,12 @@ impl Component for Tabbed {
/* length */ /* length */
height, height,
); );
} else {
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
ScrollUpdate::End(self.id),
)));
} }
self.dirty = false; self.dirty = false;
return; return;
@ -1192,6 +1257,21 @@ impl Component for Tabbed {
), ),
); );
if height.wrapping_div(rows + 1) > 0 || width.wrapping_div(cols + 1) > 0 { 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( ScrollBar::default().set_show_arrows(true).draw(
grid, grid,
( (
@ -1206,6 +1286,12 @@ impl Component for Tabbed {
/* length */ /* length */
height, height,
); );
} else {
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
ScrollUpdate::End(self.id),
)));
} }
} }
self.dirty = false; self.dirty = false;
@ -1250,6 +1336,11 @@ impl Component for Tabbed {
if self.show_shortcuts { if self.show_shortcuts {
/* children below the shortcut overlay must be redrawn */ /* children below the shortcut overlay must be redrawn */
self.set_dirty(true); self.set_dirty(true);
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
ScrollUpdate::End(self.id),
)));
} }
self.show_shortcuts = !self.show_shortcuts; self.show_shortcuts = !self.show_shortcuts;
self.dirty = true; self.dirty = true;
@ -1281,6 +1372,8 @@ impl Component for Tabbed {
return true; return true;
} }
if let Some(c_idx) = self.children.iter().position(|x| x.id() == *id) { 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.children.remove(c_idx);
self.cursor_pos = 0; self.cursor_pos = 0;
self.set_dirty(true); self.set_dirty(true);

View File

@ -533,28 +533,30 @@ impl Component for Pager {
} }
if (rows < height) || self.search.is_some() { if (rows < height) || self.search.is_some() {
const RESULTS_STR: &str = "Results for "; 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 shown_lines = self.cursor.1 + rows;
let total_lines = height; let total_lines = height;
let scrolling = if rows < height { if rows < height {
format!( context
"{shown_percentage}% {line_desc}{shown_lines}/{total_lines}{has_more_lines}", .replies
line_desc = if grid.ascii_drawing { "lines:" } else { "" }, .push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
shown_percentage = shown_percentage, ScrollUpdate::Update {
shown_lines = shown_lines, id: self.id,
total_lines = total_lines, context: ScrollContext {
has_more_lines = if self.line_breaker.is_finished() { shown_lines,
"" total_lines,
} else { has_more_lines: !self.line_breaker.is_finished(),
"(+)" },
} },
) )));
} else { } 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 { if let Some(ref search) = self.search {
format!( let status_message = format!(
"{results_str}{search_pattern}: {current_pos}/{total_results}{has_more_lines}", "{results_str}{search_pattern}: {current_pos}/{total_results}{has_more_lines}",
results_str = RESULTS_STR, results_str = RESULTS_STR,
search_pattern = &search.pattern, search_pattern = &search.pattern,
@ -569,39 +571,29 @@ impl Component for Pager {
} else { } else {
"(+)" "(+)"
} }
) );
} else { let mut attribute = crate::conf::value(context, "status.bar");
String::new() if !context.settings.terminal.use_color() {
}; attribute.attrs |= Attr::REVERSE;
let status_message = format!( }
"{search_results}{divider}{scrolling}", let (_, y) = write_string_to_grid(
search_results = search_results, &status_message,
divider = if self.search.is_some() { " " } else { "" }, grid,
scrolling = scrolling, attribute.fg,
); attribute.bg,
let mut attribute = crate::conf::value(context, "status.bar"); attribute.attrs,
if !context.settings.terminal.use_color() { (
attribute.attrs |= Attr::REVERSE; set_y(upper_left!(area), get_y(bottom_right!(area))),
} bottom_right!(area),
let (_, y) = write_string_to_grid( ),
&status_message, None,
grid, );
attribute.fg, /* set search pattern to italics */
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 start_x = get_x(upper_left!(area)) + RESULTS_STR.len(); 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) { for c in grid.row_iter(start_x..(start_x + search.pattern.grapheme_width()), y) {
grid[c].set_attrs(attribute.attrs | Attr::ITALICS); grid[c].set_attrs(attribute.attrs | Attr::ITALICS);
} }
} };
} }
context.dirty_areas.push_back(area); context.dirty_areas.push_back(area);
} }
@ -746,6 +738,13 @@ impl Component for Pager {
self.initialised = false; self.initialised = false;
self.dirty = true; self.dirty = true;
} }
UIEvent::VisibilityChange(false) => {
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
ScrollUpdate::End(self.id),
)));
}
_ => {} _ => {}
} }
false false

View File

@ -38,7 +38,7 @@ pub use self::helpers::*;
use super::command::Action; use super::command::Action;
use super::jobs::{JobExecutor, JobId}; use super::jobs::{JobExecutor, JobId};
use super::terminal::*; use super::terminal::*;
use crate::components::{Component, ComponentId}; use crate::components::{Component, ComponentId, ScrollUpdate};
use std::sync::Arc; use std::sync::Arc;
use melib::backends::{AccountHash, BackendEvent, MailboxHash}; use melib::backends::{AccountHash, BackendEvent, MailboxHash};
@ -57,6 +57,7 @@ pub enum StatusEvent {
JobFinished(JobId), JobFinished(JobId),
JobCanceled(JobId), JobCanceled(JobId),
SetMouse(bool), SetMouse(bool),
ScrollUpdate(ScrollUpdate),
} }
/// `ThreadEvent` encapsulates all of the possible values we need to transfer between our threads /// `ThreadEvent` encapsulates all of the possible values we need to transfer between our threads
@ -149,6 +150,7 @@ pub enum UIEvent {
ConfigReload { ConfigReload {
old_settings: crate::conf::Settings, old_settings: crate::conf::Settings,
}, },
VisibilityChange(bool),
} }
pub struct CallbackFn(pub Box<dyn FnOnce(&mut crate::Context) -> () + Send + 'static>); pub struct CallbackFn(pub Box<dyn FnOnce(&mut crate::Context) -> () + Send + 'static>);