From 8e1cf0672463e9d4437ab74dadc2434e779e076f Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Thu, 28 Dec 2023 11:49:02 +0000 Subject: [PATCH] Begin actually displaying menu options! --- src/menu.rs | 122 ++++++++++++++++++++++++++++++++++++++++++++++++++-- src/text.rs | 10 +++-- src/tui.rs | 5 ++- 3 files changed, 129 insertions(+), 8 deletions(-) diff --git a/src/menu.rs b/src/menu.rs index 069ef9e..d5ce6dc 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -1,3 +1,6 @@ +use std::collections::HashMap; + +use super::activity_stack::{Activity, NonUtilityActivity}; use super::coloured_string::ColouredString; use super::text::*; use super::tui::{ @@ -5,15 +8,108 @@ use super::tui::{ OurKey, OurKey::* }; +enum MenuLine { + Blank, + Key(MenuKeypressLine), +} + +enum Action { + Goto(Activity), +} + +fn find_substring(haystack: &str, needle: &str) + -> Option<(usize, usize, usize)> { + if let Some(pos) = haystack.find(needle) { + let (pre, post) = haystack.split_at(pos); + let pre_nchars = pre.chars().count(); + let needle_nchars = needle.chars().count(); + let post_nchars = post.chars().count(); + Some((pre_nchars, needle_nchars, post_nchars - needle_nchars)) + } else { + None + } +} + +fn find_highlight_char(desc: &str, c: char) + -> Option<(usize, usize, usize)> { + let found = find_substring(desc, &c.to_uppercase().to_string()); + if found.is_some() { + found + } else { + find_substring(desc, &c.to_lowercase().to_string()) + } +} + struct Menu { title: FileHeader, + status: FileStatusLineFinal, + lines: Vec, + actions: HashMap, +} + +impl Menu { + fn new(title: ColouredString, is_main: bool) -> Self { + let status = FileStatusLine::new(); + let status = if is_main { + status.message("Select an option") + } else { + status.add(OurKey::Return, "Back", 10) + }; + + Menu{ + title: FileHeader::new(title), + status: status.finalise(), + lines: Vec::new(), + actions: HashMap::new(), + } + } + + fn add_action_coloured(&mut self, key: OurKey, desc: ColouredString, + action: Action) { + self.lines.push(MenuLine::Key(MenuKeypressLine::new(key, desc))); + self.actions.insert(key, action); + } + + fn add_action(&mut self, key: OurKey, desc: &str, action: Action) { + let highlight_char = match key { + Pr(c) => Some(c), + Ctrl(c) => Some(c), + _ => None, + }; + + let highlight = if let Some(c) = highlight_char { + find_highlight_char(desc, c) + } else { + None + }; + + let desc_coloured = if let Some((before, during, after)) = highlight { + ColouredString::general( + desc, + &("H".repeat(before) + + &"K".repeat(during) + + &"H".repeat(after))) + } else { + ColouredString::uniform(desc, 'H') + }; + + self.add_action_coloured(key, desc_coloured, action) + } + + fn add_blank_line(&mut self) { + self.lines.push(MenuLine::Blank); + } } pub fn main_menu() -> Box { - Box::new(Menu{ - title: FileHeader::new( - ColouredString::uniform("Mastodonochrome Main Menu", 'H')), - }) + let mut menu = Menu::new( + ColouredString::uniform("Mastodonochrome Main Menu", 'H'), true); + + menu.add_action(Pr('H'), "Home timeline", Action::Goto( + NonUtilityActivity::HomeTimelineFile.into())); + menu.add_blank_line(); + + Box::new(menu) } impl ActivityState for Menu { @@ -21,6 +117,24 @@ impl ActivityState for Menu { -> (Vec, CursorPosition) { let mut lines = Vec::new(); lines.extend_from_slice(&self.title.render(w)); + lines.extend_from_slice(&BlankLine::render_static()); + + // FIXME: once menus get too big, we'll need to keep a current + // starting position, and be able to scroll up and down the + // menu with [<] and [>] + for line in &self.lines { + lines.extend_from_slice(&match line { + MenuLine::Blank => BlankLine::render_static(), + MenuLine::Key(mk) => mk.render(w), + }); + } + + while lines.len() + 1 < h { + lines.extend_from_slice(&BlankLine::render_static()); + } + + lines.extend_from_slice(&self.status.render(w)); + (lines, CursorPosition::End) } diff --git a/src/text.rs b/src/text.rs index 8b3cff0..26cd153 100644 --- a/src/text.rs +++ b/src/text.rs @@ -23,16 +23,20 @@ impl BlankLine { pub fn new() -> Self { BlankLine{} } -} -impl TextFragment for BlankLine { - fn render(&self, _width: usize) -> Vec { + pub fn render_static() -> Vec { vec! { ColouredString::plain(""), } } } +impl TextFragment for BlankLine { + fn render(&self, _width: usize) -> Vec { + Self::render_static() + } +} + #[test] fn test_blank() { assert_eq!(BlankLine::new().render(40), vec! { diff --git a/src/tui.rs b/src/tui.rs index e191aee..03d769d 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -133,7 +133,7 @@ pub struct Tui { // FIXME: we'll need a Client here } -#[derive(Eq, PartialEq, Debug, Clone, Copy)] +#[derive(Eq, PartialEq, Debug, Clone, Copy, Hash)] pub enum OurKey { Pr(char), Ctrl(char), @@ -315,6 +315,9 @@ impl TuiLogicalState { let mut last_x = 0; let mut last_y = 0; for (y, line) in lines.iter().enumerate() { + if y >= area.height as usize { + break; + } ratatui_set_string(buf, 0, y, &line.slice()); last_y = y; last_x = line.width(); -- 2.30.2