chiark / gitweb /
Introduce the concept of an 'overlay activity'.
authorSimon Tatham <anakin@pobox.com>
Mon, 1 Jan 2024 07:21:08 +0000 (07:21 +0000)
committerSimon Tatham <anakin@pobox.com>
Mon, 1 Jan 2024 08:25:17 +0000 (08:25 +0000)
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.

src/activity_stack.rs
src/tui.rs

index 7136ba416c9f0cbcda312268263bb553f4690c31..fd22dcdd13860c98dad132fc7d4d898b27a7a0d5 100644 (file)
@@ -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<NonUtilityActivity> for Activity {
@@ -34,11 +40,16 @@ impl From<NonUtilityActivity> for Activity {
 impl From<UtilityActivity> for Activity {
     fn from(value: UtilityActivity) -> Self { Activity::Util(value) }
 }
+impl From<OverlayActivity> for Activity {
+    fn from(value: OverlayActivity) -> Self { Activity::Overlay(value) }
+}
 
 #[derive(PartialEq, Eq, Debug)]
 pub struct ActivityStack {
     nonutil: Vec<NonUtilityActivity>, // not counting MainMenu at the top
     util: Option<UtilityActivity>,
+    initial_util: Option<UtilityActivity>,
+    overlay: Option<OverlayActivity>,
 }
 
 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<Activity> {
+        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,
     });
 }
index 4b8ad00dfb628ea9349f7d65b32d92c9d69e1835..704cbf5a13a2214df3910f3a8aa9b0c6d0277c61 100644 (file)
@@ -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<dyn ActivityState>,
+    overlay_activity_state: Option<Box<dyn ActivityState>>,
     last_area: Option<Rect>,
 }
 
@@ -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);
+            }
         }
     }
 }