From: Simon Tatham Date: Mon, 1 Jan 2024 07:21:08 +0000 (+0000) Subject: Introduce the concept of an 'overlay activity'. X-Git-Url: https://www.chiark.greenend.org.uk/ucgi/~ian/git?a=commitdiff_plain;h=cdcf457efac0d970122558eefc22f0617a484938;p=mastodonochrome.git Introduce the concept of an 'overlay activity'. This will be used for bottom-line editor prompts. Some of those are triggered within the context of an existing activity (for example, if you're reading a file and press the not-yet-implemented search key). Others can be imposed from outside, e.g. pressing Alt+E while reading a file. So there's a slightly fiddly mechanism to make the latter work, and not treat it as two totally independent ESC and E keystrokes (which would leave you looking at the Utilities Menu while entering the name of a user to examine, instead of at the screen you saw the username on). The overlay activity is drawn on top of the topmost non-overlay activity on the stack, at the bottom of the screen. --- diff --git a/src/activity_stack.rs b/src/activity_stack.rs index 7136ba4..fd22dcd 100644 --- a/src/activity_stack.rs +++ b/src/activity_stack.rs @@ -22,10 +22,16 @@ pub enum UtilityActivity { ListStatusBoosters(String), } +#[derive(PartialEq, Eq, Debug, Clone)] +pub enum OverlayActivity { + GetUserToExamine, +} + #[derive(PartialEq, Eq, Debug, Clone)] pub enum Activity { NonUtil(NonUtilityActivity), Util(UtilityActivity), + Overlay(OverlayActivity), } impl From for Activity { @@ -34,11 +40,16 @@ impl From for Activity { impl From for Activity { fn from(value: UtilityActivity) -> Self { Activity::Util(value) } } +impl From for Activity { + fn from(value: OverlayActivity) -> Self { Activity::Overlay(value) } +} #[derive(PartialEq, Eq, Debug)] pub struct ActivityStack { nonutil: Vec, // not counting MainMenu at the top util: Option, + initial_util: Option, + overlay: Option, } impl Activity { @@ -56,6 +67,8 @@ impl ActivityStack { ActivityStack { nonutil: Vec::new(), util: None, + initial_util: None, + overlay: None, } } @@ -68,11 +81,25 @@ impl ActivityStack { } } + pub fn new_event(&mut self) { + // Save the utility activity we were in at the start of an + // event. That way we can restore it when moving to an + // overlay, which means that for example Alt+E (desugaring as + // a very rapid ESC + E) can put the GetUserToExamine overlay + // on top of whatever you were previously looking at, rather + // than the less helpful utilities menu. + self.initial_util = self.util.clone(); + } + pub fn goto(&mut self, act: Activity) { match act { - Activity::Util(x) => self.util = Some(x), + Activity::Util(x) => { + self.overlay = None; + self.util = Some(x); + }, Activity::NonUtil(x) => { self.util = None; + self.overlay = None; match x { NonUtilityActivity::MainMenu => self.nonutil.clear(), y => { @@ -85,7 +112,11 @@ impl ActivityStack { self.nonutil.push(y); }, } - } + }, + Activity::Overlay(x) => { + self.util = self.initial_util.clone(); + self.overlay = Some(x); + }, } } @@ -98,6 +129,13 @@ impl ActivityStack { }, } } + + pub fn overlay(&self) -> Option { + match &self.overlay { + Some(x) => Some(Activity::Overlay(x.clone())), + _ => None, + } + } } #[test] @@ -107,6 +145,8 @@ fn test() { assert_eq!(stk, ActivityStack { nonutil: vec! {}, util: None, + initial_util: None, + overlay: None, }); stk.goto(NonUtilityActivity::HomeTimelineFile.into()); @@ -116,6 +156,8 @@ fn test() { NonUtilityActivity::HomeTimelineFile, }, util: None, + initial_util: None, + overlay: None, }); stk.goto(NonUtilityActivity::HomeTimelineFile.into()); @@ -125,6 +167,8 @@ fn test() { NonUtilityActivity::HomeTimelineFile, }, util: None, + initial_util: None, + overlay: None, }); stk.goto(NonUtilityActivity::MainMenu.into()); @@ -132,6 +176,8 @@ fn test() { assert_eq!(stk, ActivityStack { nonutil: vec! {}, util: None, + initial_util: None, + overlay: None, }); stk.goto(NonUtilityActivity::HomeTimelineFile.into()); @@ -141,6 +187,8 @@ fn test() { NonUtilityActivity::HomeTimelineFile, }, util: None, + initial_util: None, + overlay: None, }); stk.goto(UtilityActivity::UtilsMenu.into()); @@ -150,6 +198,8 @@ fn test() { NonUtilityActivity::HomeTimelineFile, }, util: Some(UtilityActivity::UtilsMenu), + initial_util: None, + overlay: None, }); stk.goto(UtilityActivity::ReadMentions.into()); @@ -159,6 +209,8 @@ fn test() { NonUtilityActivity::HomeTimelineFile, }, util: Some(UtilityActivity::ReadMentions), + initial_util: None, + overlay: None, }); stk.pop(); @@ -168,6 +220,8 @@ fn test() { NonUtilityActivity::HomeTimelineFile, }, util: None, + initial_util: None, + overlay: None, }); stk.goto(UtilityActivity::ReadMentions.into()); @@ -177,6 +231,8 @@ fn test() { NonUtilityActivity::HomeTimelineFile, }, util: Some(UtilityActivity::ReadMentions), + initial_util: None, + overlay: None, }); stk.goto(NonUtilityActivity::HomeTimelineFile.into()); @@ -186,6 +242,8 @@ fn test() { NonUtilityActivity::HomeTimelineFile, }, util: None, + initial_util: None, + overlay: None, }); stk.pop(); @@ -193,6 +251,8 @@ fn test() { assert_eq!(stk, ActivityStack { nonutil: vec! {}, util: None, + initial_util: None, + overlay: None, }); stk.pop(); @@ -200,5 +260,7 @@ fn test() { assert_eq!(stk, ActivityStack { nonutil: vec! {}, util: None, + initial_util: None, + overlay: None, }); } diff --git a/src/tui.rs b/src/tui.rs index 4b8ad00..704cbf5 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -10,6 +10,7 @@ use ratatui::{ prelude::{Buffer, CrosstermBackend, Rect, Terminal}, style::{Style, Color, Modifier}, }; +use std::cmp::min; use std::collections::HashSet; use std::io::{Stdout, Write, stdout}; use unicode_width::UnicodeWidthStr; @@ -319,6 +320,7 @@ impl Tui { match ev { Event::Key(key) => { if key.kind == KeyEventKind::Press { + state.new_event(); for ourkey in Self::translate_keypress(key) { match state.handle_keypress( ourkey, &mut self.client) { @@ -408,6 +410,7 @@ pub trait ActivityState { struct TuiLogicalState { activity_stack: ActivityStack, activity_state: Box, + overlay_activity_state: Option>, last_area: Option, } @@ -452,36 +455,71 @@ impl TuiLogicalState { TuiLogicalState { activity_stack: activity_stack, activity_state: activity_state, + overlay_activity_state: None, last_area: None, } } fn draw_frame(&mut self, area: Rect, buf: &mut Buffer) -> Option<(usize, usize)> { + let (w, h) = (area.width as usize, area.height as usize); + if self.last_area != Some(area) { self.last_area = Some(area); - self.activity_state.resize( - area.width as usize, area.height as usize); + self.activity_state.resize(w, h); + if let Some(ref mut state) = self.overlay_activity_state { + state.resize(w, h); + } } - let (lines, cursorpos) = self.activity_state.draw( - area.width as usize, area.height as usize); + let (lines, cursorpos) = self.activity_state.draw(w, h); + let mut cursorpos = cursorpos; buf.reset(); let mut last_x = 0; let mut last_y = 0; for (y, line) in lines.iter().enumerate() { - if y >= area.height as usize { + if y >= h { break; } ratatui_set_string(buf, 0, y, &line.slice()); last_y = y; last_x = line.width(); } - match cursorpos { + + let mut cursorcoords = match cursorpos { CursorPosition::None => None, CursorPosition::At(x, y) => Some((x, y)), CursorPosition::End => Some((last_x, last_y)), + }; + + if let Some(state) = &self.overlay_activity_state { + let (lines, overlay_cursorpos) = state.draw(w, h); + cursorpos = overlay_cursorpos; + let ytop = h - min(h, lines.len()); + for (line, y) in lines.iter().zip(ytop..) { + if y >= h { + break; + } + // Clear the whole previous line in case the new one is shorter + for x in 0..area.width { + buf.get_mut(x, y as u16).reset(); + } + ratatui_set_string(buf, 0, y, &line.slice()); + last_y = y; + last_x = line.width(); + } + cursorcoords = match cursorpos { + CursorPosition::None => None, + CursorPosition::At(x, y) => Some((x, y + ytop)), + CursorPosition::End => Some((last_x, last_y)), + }; } + + cursorcoords + } + + fn new_event(&mut self) { + self.activity_stack.new_event(); } fn handle_keypress(&mut self, key: OurKey, client: &mut Client) -> @@ -493,7 +531,11 @@ impl TuiLogicalState { OurKey::Escape => LogicalAction::Goto( UtilityActivity::UtilsMenu.into()), - _ => self.activity_state.handle_keypress(key, client), + _ => if let Some(ref mut state) = self.overlay_activity_state { + state.handle_keypress(key, client) + } else { + self.activity_state.handle_keypress(key, client) + } }; match logact { @@ -535,9 +577,16 @@ impl TuiLogicalState { fn changed_activity(&mut self, client: &mut Client) { self.activity_state = new_activity_state( self.activity_stack.top(), client); + self.overlay_activity_state = match self.activity_stack.overlay() { + Some(activity) => Some(new_activity_state(activity, client)), + None => None, + }; if let Some(area) = self.last_area { - self.activity_state.resize( - area.width as usize, area.height as usize); + let (w, h) = (area.width as usize, area.height as usize); + self.activity_state.resize(w, h); + if let Some(ref mut state) = self.overlay_activity_state { + state.resize(w, h); + } } } }