From a2b75b15325fe5af8f6774f4ac21d573be7c28b8 Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Mon, 1 Jan 2024 08:14:02 +0000 Subject: [PATCH] UNFINISHED: first overlay activity: Examine User prompt. This doesn't actually examine users yet. When you press Return at the prompt, it just beeps, because transferring the data to where you actually wanted it is not yet implemented. But it's enough to test the editing itself. --- src/editor.rs | 283 +++++++++++++++++++++++++++++++++++++++++++++++++- src/menu.rs | 7 +- src/tui.rs | 3 + 3 files changed, 290 insertions(+), 3 deletions(-) diff --git a/src/editor.rs b/src/editor.rs index bc85f2c..43c11f8 100644 --- a/src/editor.rs +++ b/src/editor.rs @@ -1,6 +1,11 @@ use unicode_width::UnicodeWidthChar; -use super::tui::{OurKey, OurKey::*}; +use super::client::Client; +use super::coloured_string::ColouredString; +use super::tui::{ + ActivityState, CursorPosition, LogicalAction, + OurKey, OurKey::*, +}; struct EditorCore { text: String, @@ -22,6 +27,20 @@ impl EditorCore { } } + fn char_width_and_bytes(&self, pos: usize) -> Option<(usize, usize)> { + match self.text[pos..].chars().next() { + None => None, + Some(c) => { + let width = UnicodeWidthChar::width(c).unwrap_or(0); + let mut end = pos + 1; + while !self.is_char_boundary(end) { + end += 1; + } + Some((width, end - pos)) + }, + } + } + fn is_word_boundary(&self, pos: usize) -> bool { if !self.is_char_boundary(pos) { false @@ -154,6 +173,7 @@ impl EditorCore { Ctrl('T') => { self.forward_word(); }, Ctrl('Y') => { self.paste(); }, Pr(c) => { self.insert(&c.to_string()); }, + Space => { self.insert(" "); }, _ => (), } } @@ -264,6 +284,8 @@ fn test_insert() { pub struct SingleLineEditor { core: EditorCore, + width: usize, + first_visible: usize, } impl SingleLineEditor { @@ -274,6 +296,57 @@ impl SingleLineEditor { point: 0, paste_buffer: "".to_owned(), }, + width: 0, + first_visible: 0, + } + } + + fn update_first_visible(&mut self) { + if self.first_visible > self.core.point { + self.first_visible = self.core.point; + } else { + let mut avail_width = self.width.saturating_sub(1); + let mut counted_initial_trunc_marker = false; + if self.first_visible > 0 { + counted_initial_trunc_marker = true; + avail_width = avail_width.saturating_sub(1); + } + let mut head = self.first_visible; + let mut tail = self.first_visible; + let mut currwidth = 0; + while head < self.core.point || currwidth > avail_width { + if currwidth <= avail_width { + match self.core.char_width_and_bytes(head) { + None => break, + Some((w, b)) => { + head += b; + currwidth += w; + }, + } + } else { + match self.core.char_width_and_bytes(tail) { + None => panic!("tail should always be behind head"), + Some((w, b)) => { + tail += b; + currwidth -= w; + if !counted_initial_trunc_marker { + counted_initial_trunc_marker = true; + avail_width = avail_width.saturating_sub(1); + } + }, + } + } + } + self.first_visible = tail; + } + + // Special case: if the < indicator hides a single character + // at the start of the buffer which is the same width as it, + // we can just reveal it, which is strictly better. + if let Some((w, b)) = self.core.char_width_and_bytes(0) { + if w == 1 && b == self.first_visible { + self.first_visible = 0; + } } } @@ -289,6 +362,214 @@ impl SingleLineEditor { Return => { return true; }, _ => { self.core.handle_keypress(key); } } + self.update_first_visible(); return false; } + + pub fn draw(&self, width: usize) -> (ColouredString, Option) { + let mut s = ColouredString::plain(""); + if self.first_visible > 0 { + s.push_str(&ColouredString::uniform("<", '>').slice()); + } + let mut pos = self.first_visible; + let mut cursor = None; + loop { + let width_so_far = s.width(); + if pos == self.core.point { + cursor = Some(width_so_far); + } + match self.core.char_width_and_bytes(pos) { + None => break, + Some((w, b)) => { + if width_so_far + w > width || + (width_so_far + w == width && + pos + b < self.core.text.len()) + { + s.push_str(&ColouredString::uniform(">", '>').slice()); + break; + } else { + s.push_str(&ColouredString::plain( + &self.core.text[pos..pos+b]).slice()); + pos += b; + } + } + } + } + (s, cursor) + } + + pub fn resize(&mut self, width: usize) { + self.width = width; + self.update_first_visible(); + } +} + +#[test] +fn test_single_line_extra_ops() { + let mut sle = SingleLineEditor { + core: EditorCore { + text: "hélło".to_owned(), + point: 3, + paste_buffer: "".to_owned(), + }, + width: 0, + first_visible: 0, + }; + + sle.cut_to_end(); + assert_eq!(&sle.core.text, "hé"); + assert_eq!(sle.core.point, 3); + assert_eq!(sle.core.paste_buffer, "lło"); + + sle.core.beginning_of_buffer(); + assert_eq!(sle.core.point, 0); + + sle.core.paste(); + assert_eq!(&sle.core.text, "lłohé"); + assert_eq!(sle.core.point, 4); + + sle.core.end_of_buffer(); + assert_eq!(sle.core.point, 7); +} + +#[test] +fn test_single_line_visibility() { + let mut sle = SingleLineEditor { + core: EditorCore { + text: "".to_owned(), + point: 0, + paste_buffer: "".to_owned(), + }, + width: 5, + first_visible: 0, + }; + + assert_eq!(sle.draw(sle.width), + (ColouredString::plain(""), Some(0))); + + // Typing 'a' doesn't move first_visible away from the buffer start + sle.core.insert("a"); + assert_eq!(sle.core.point, 1); + sle.update_first_visible(); + assert_eq!(sle.first_visible, 0); + assert_eq!(sle.draw(sle.width), + (ColouredString::plain("a"), Some(1))); + + // Typing three more characters leaves the cursor in the last of + // the 5 positions, so we're still good: we can print "abcd" + // followed by an empty space containing the cursor. + sle.core.insert("bcd"); + assert_eq!(sle.core.point, 4); + sle.update_first_visible(); + assert_eq!(sle.first_visible, 0); + assert_eq!(sle.draw(sle.width), + (ColouredString::plain("abcd"), Some(4))); + + // One more character and we overflow. Now we must print " "), Some(4))); + + // And another two characters move that on in turn: " "), Some(4))); + + // Now start moving backwards. Three backwards movements leave the + // cursor on the e, but nothing has changed. + sle.core.backward(); + sle.core.backward(); + sle.core.backward(); + assert_eq!(sle.core.point, 4); + sle.update_first_visible(); + assert_eq!(sle.first_visible, 4); + assert_eq!(sle.draw(sle.width), + (ColouredString::general(" "), Some(1))); + + // Move backwards one more, so that we must scroll to get the d in view. + sle.core.backward(); + assert_eq!(sle.core.point, 3); + sle.update_first_visible(); + assert_eq!(sle.first_visible, 3); + assert_eq!(sle.draw(sle.width), + (ColouredString::general(" "), Some(1))); + + // And on the _next_ backwards scroll, the end of the string also + // becomes hidden. + sle.core.backward(); + assert_eq!(sle.core.point, 2); + sle.update_first_visible(); + assert_eq!(sle.first_visible, 2); + assert_eq!(sle.draw(sle.width), + (ColouredString::general("", "> >"), Some(1))); + + // The one after that would naively leave us at "" with the + // cursor on the b. But we can do better! In this case, the < + // marker hides just one single-width character, so we can reveal + // it and put first_visible right back to 0. + sle.core.backward(); + assert_eq!(sle.core.point, 1); + sle.update_first_visible(); + assert_eq!(sle.first_visible, 0); + assert_eq!(sle.draw(sle.width), + (ColouredString::general("abcd>", " >"), Some(1))); +} + +struct BottomLineEditorOverlay { + prompt: ColouredString, + promptwidth: usize, + ed: SingleLineEditor, +} + +impl BottomLineEditorOverlay { + fn new(prompt: ColouredString) -> Self { + let promptwidth = prompt.width(); + BottomLineEditorOverlay { + prompt: prompt, + promptwidth, + ed: SingleLineEditor::new(), + } + } +} + +impl ActivityState for BottomLineEditorOverlay { + fn resize(&mut self, w: usize, _h: usize) { + self.ed.resize(w.saturating_sub(self.promptwidth)); + } + + fn draw(&self, w: usize, _h: usize) -> + (Vec, CursorPosition) + { + let (buffer, cursorpos) = self.ed.draw( + w.saturating_sub(self.promptwidth)); + + let cursorpos = match cursorpos { + Some(x) => CursorPosition::At(x + self.promptwidth, 0), + None => CursorPosition::None, + }; + + (vec! { &self.prompt + buffer }, cursorpos) + } + + fn handle_keypress(&mut self, key: OurKey, _client: &mut Client) -> + LogicalAction + { + if self.ed.handle_keypress(key) { + LogicalAction::Beep // FIXME: do something! + } else { + LogicalAction::Nothing + } + } +} + +pub fn get_user_to_examine() -> Box { + Box::new(BottomLineEditorOverlay::new( + ColouredString::plain("Examine User: "))) } diff --git a/src/menu.rs b/src/menu.rs index 92774be..afbf1e9 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -2,7 +2,9 @@ use std::collections::HashMap; use std::cmp::max; use itertools::Itertools; -use super::activity_stack::{NonUtilityActivity, UtilityActivity}; +use super::activity_stack::{ + NonUtilityActivity, UtilityActivity, OverlayActivity +}; use super::client::Client; use super::coloured_string::ColouredString; use super::text::*; @@ -221,7 +223,8 @@ pub fn utils_menu() -> Box { menu.add_action(Pr('R'), "Read Mentions", LogicalAction::Goto( UtilityActivity::ReadMentions.into())); menu.add_blank_line(); - menu.add_action(Pr('E'), "Examine User", LogicalAction::NYI); + menu.add_action(Pr('E'), "Examine User", LogicalAction::Goto( + OverlayActivity::GetUserToExamine.into())); menu.add_action(Pr('Y'), "Examine Yourself", LogicalAction::NYI); menu.add_blank_line(); menu.add_action(Pr('L'), "Logs menu", LogicalAction::Goto( diff --git a/src/tui.rs b/src/tui.rs index 704cbf5..1157a0e 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -21,6 +21,7 @@ use super::coloured_string::{ColouredString, ColouredStringSlice}; use super::config::ConfigLocation; use super::menu::*; use super::file::*; +use super::editor::*; use super::auth::AuthError; fn ratatui_style_from_colour(colour: char) -> Style { @@ -438,6 +439,8 @@ fn new_activity_state(activity: Activity, client: &mut Client) -> mentions(client), Activity::Util(UtilityActivity::EgoLog) => ego_log(client), + Activity::Overlay(OverlayActivity::GetUserToExamine) => + Ok(get_user_to_examine()), _ => panic!("FIXME"), }; -- 2.30.2