From: Simon Tatham Date: Thu, 11 Jan 2024 22:42:41 +0000 (+0000) Subject: More reusable concept of an editable menu line. X-Git-Url: http://www.chiark.greenend.org.uk/ucgi/~ian/git?a=commitdiff_plain;h=1e4445650545ae5784766a2858afa8ada0b70ab0;p=mastodonochrome.git More reusable concept of an editable menu line. This refactors a lot of the code from posting.rs into a type that does all the work of being switchable between a menu line and a single-line editor. No functional change (I hope), but it should make it easier to reuse in other similar options menus. --- diff --git a/src/editor.rs b/src/editor.rs index 395cade..a8335b3 100644 --- a/src/editor.rs +++ b/src/editor.rs @@ -666,6 +666,160 @@ impl ActivityState for BottomLineEditorOverlay { } } +pub trait EditableMenuLineData { + fn display(&self) -> ColouredString; + fn to_text(&self) -> String; + fn from_text(text: &str) -> Self; +} + +impl EditableMenuLineData for String { + fn display(&self) -> ColouredString { ColouredString::plain(self) } + fn to_text(&self) -> String { self.clone() } + fn from_text(text: &str) -> Self { text.to_owned() } +} + +impl EditableMenuLineData for Option { + fn display(&self) -> ColouredString { + match self { + None => ColouredString::uniform("none", '0'), + Some(ref text) => ColouredString::plain(text), + } + } + + fn to_text(&self) -> String { + match self { + None => "".to_owned(), + Some(ref text) => text.clone(), + } + } + + fn from_text(text: &str) -> Self { + match text { + "" => None, + text => Some(text.to_owned()), + } + } +} + +pub struct EditableMenuLine { + key: OurKey, + description: ColouredString, + data: Data, + menuline: MenuKeypressLine, + prompt: MenuKeypressLine, + editor: Option, + last_width: Option, +} + +impl EditableMenuLine { + pub fn new(key: OurKey, description: ColouredString, data: Data) -> Self + { + let menuline = Self::make_menuline(key, &description, &data); + let prompt = Self::make_prompt(key, &description); + + EditableMenuLine { + key, + description, + data, + menuline, + prompt, + editor: None, + last_width: None, + } + } + + fn make_menuline(key: OurKey, description: &ColouredString, data: &Data) + -> MenuKeypressLine + { + let desc = description + data.display(); + MenuKeypressLine::new(key, desc) + } + + fn make_prompt(key: OurKey, description: &ColouredString) + -> MenuKeypressLine + { + MenuKeypressLine::new(key, description.to_owned()) + } + + pub fn render(&self, width: usize, cursorpos: &mut CursorPosition, + cy: usize) -> ColouredString + { + if let Some(ref editor) = self.editor { + let (text, cx) = editor.draw(width); + if let Some(cx) = cx { + *cursorpos = CursorPosition::At(cx, cy); + } + text + } else { + self.menuline.render_oneline(width, None, &DefaultDisplayStyle) + } + } + + // Returns a LogicalAction just to make it more convenient to put + // in matches on keypresses + pub fn start_editing(&mut self) -> LogicalAction { + self.editor = Some(SingleLineEditor::new(self.data.to_text())); + self.refresh_editor_prompt(); + LogicalAction::Nothing + } + + pub fn resize(&mut self, w: usize) { + self.last_width = Some(w); + self.refresh_editor_prompt(); + } + + pub fn refresh_editor_prompt(&mut self) { + if let Some(ref mut editor) = self.editor { + if let Some(w) = self.last_width { + let prompt = self.prompt + .render_oneline(w, None, &DefaultDisplayStyle); + editor.resize(w); + editor.set_prompt(prompt); + } + } + } + + pub fn handle_keypress(&mut self, key: OurKey) -> bool { + let (consumed, done) = if let Some(ref mut editor) = self.editor { + if editor.handle_keypress(key) { + self.data = Data::from_text(editor.borrow_text()); + self.menuline = Self::make_menuline( + self.key, &self.description, &self.data); + (true, true) + } else { + (true, false) + } + } else { + (false, false) + }; + + if done { + self.editor = None; + } + + consumed + } + + pub fn get_data(&self) -> &Data { &self.data } + pub fn is_editing(&self) -> bool { self.editor.is_some() } +} + +impl MenuKeypressLineGeneral +for EditableMenuLine { + fn check_widths(&self, lmaxwid: &mut usize, rmaxwid: &mut usize) { + self.menuline.check_widths(lmaxwid, rmaxwid); + self.prompt.check_widths(lmaxwid, rmaxwid); + } + fn reset_widths(&mut self) { + self.menuline.reset_widths(); + self.prompt.reset_widths(); + } + fn ensure_widths(&mut self, lmaxwid: usize, rmaxwid: usize) { + self.menuline.ensure_widths(lmaxwid, rmaxwid); + self.prompt.ensure_widths(lmaxwid, rmaxwid); + } +} + pub fn get_user_to_examine() -> Box { Box::new(BottomLineEditorOverlay::new( ColouredString::plain("Examine User: "), diff --git a/src/posting.rs b/src/posting.rs index 9582a99..e4d8927 100644 --- a/src/posting.rs +++ b/src/posting.rs @@ -1,4 +1,3 @@ -use itertools::Itertools; use std::iter::once; use strum::IntoEnumIterator; use sys_locale::get_locale; @@ -10,7 +9,7 @@ use super::tui::{ }; use super::text::*; use super::types::Visibility; -use super::editor::SingleLineEditor; +use super::editor::EditableMenuLine; #[derive(Debug, PartialEq, Eq, Clone)] pub struct PostMetadata { @@ -85,14 +84,6 @@ impl Post { } } -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -enum PostMenuMode { - Normal, - EditCW, - EditLang, -} -use PostMenuMode::*; - struct PostMenu { post: Post, title: FileHeader, @@ -102,12 +93,8 @@ struct PostMenu { ml_cancel: MenuKeypressLine, ml_edit: MenuKeypressLine, ml_vis: MenuKeypressLine, - ml_content_warning: MenuKeypressLine, - ml_language: MenuKeypressLine, - mode: PostMenuMode, - editor: Option, - editor_prompt: Option, - last_width: usize, + el_content_warning: EditableMenuLine>, + el_language: EditableMenuLine, } impl PostMenu { @@ -130,9 +117,12 @@ impl PostMenu { let ml_edit = MenuKeypressLine::new( Pr('A'), ColouredString::plain("Re-edit post")); let ml_vis = Self::visibility_item(post.m.visibility); - let ml_content_warning = Self::content_warning_item( - post.m.content_warning.as_deref()); - let ml_language = Self::language_item(&post.m.language); + let el_content_warning = EditableMenuLine::new( + Pr('W'), ColouredString::plain("Content warning: "), + post.m.content_warning.clone()); + let el_language = EditableMenuLine::new( + Pr('L'), ColouredString::plain("Language: "), + post.m.language.clone()); let mut pm = PostMenu { post, @@ -143,12 +133,8 @@ impl PostMenu { ml_cancel, ml_edit, ml_vis, - ml_content_warning, - ml_language, - mode: Normal, - editor: None, - editor_prompt: None, - last_width: 0, + el_content_warning, + el_language, }; pm.fix_widths(); pm @@ -160,34 +146,25 @@ impl PostMenu { self.ml_post.check_widths(&mut lmaxwid, &mut rmaxwid); self.ml_cancel.check_widths(&mut lmaxwid, &mut rmaxwid); self.ml_edit.check_widths(&mut lmaxwid, &mut rmaxwid); - self.ml_content_warning.check_widths(&mut lmaxwid, &mut rmaxwid); - self.ml_language.check_widths(&mut lmaxwid, &mut rmaxwid); + self.el_content_warning.check_widths(&mut lmaxwid, &mut rmaxwid); + self.el_language.check_widths(&mut lmaxwid, &mut rmaxwid); for vis in Visibility::iter() { Self::visibility_item(vis).check_widths(&mut lmaxwid, &mut rmaxwid); } - if let Some(ref ml) = self.editor_prompt { - ml.check_widths(&mut lmaxwid, &mut rmaxwid); - } self.ml_post.reset_widths(); self.ml_cancel.reset_widths(); self.ml_edit.reset_widths(); self.ml_vis.reset_widths(); - self.ml_content_warning.reset_widths(); - self.ml_language.reset_widths(); - if let Some(ref mut ml) = self.editor_prompt { - ml.reset_widths(); - } + self.el_content_warning.reset_widths(); + self.el_language.reset_widths(); self.ml_post.ensure_widths(lmaxwid, rmaxwid); self.ml_cancel.ensure_widths(lmaxwid, rmaxwid); self.ml_edit.ensure_widths(lmaxwid, rmaxwid); self.ml_vis.ensure_widths(lmaxwid, rmaxwid); - self.ml_content_warning.ensure_widths(lmaxwid, rmaxwid); - self.ml_language.ensure_widths(lmaxwid, rmaxwid); - if let Some(ref mut ml) = self.editor_prompt { - ml.ensure_widths(lmaxwid, rmaxwid); - } + self.el_content_warning.ensure_widths(lmaxwid, rmaxwid); + self.el_language.ensure_widths(lmaxwid, rmaxwid); (lmaxwid, rmaxwid) } @@ -210,22 +187,6 @@ impl PostMenu { MenuKeypressLine::new(Pr('V'), text) } - fn content_warning_item(warning: Option<&str>) -> MenuKeypressLine { - let text = match warning { - None => ColouredString::general( - "Content warning: none", - " 0000"), - Some(text) => ColouredString::plain( - &format!("Content warning: {text}")), - }; - MenuKeypressLine::new(Pr('W'), text) - } - - fn language_item(lang: &str) -> MenuKeypressLine { - let text = ColouredString::plain(&format!("Language: {lang}")); - MenuKeypressLine::new(Pr('L'), text) - } - fn cycle_visibility(&mut self) -> LogicalAction { self.post.m.visibility = match self.post.m.visibility { Visibility::Public => Visibility::Unlisted, @@ -238,47 +199,11 @@ impl PostMenu { LogicalAction::Nothing } - fn setup_editor(&mut self, prompt: MenuKeypressLine, text: String, - mode: PostMenuMode) { - self.editor_prompt = Some(prompt); - self.fix_widths(); - - let mut ed = SingleLineEditor::new(text); - ed.resize(self.last_width); - self.editor = Some(ed); - - self.mode = mode; - self.set_editor_prompt(); - } - - fn set_editor_prompt(&mut self) { - if let Some(ref mut ed) = &mut self.editor { - ed.set_prompt( - self.editor_prompt.as_ref().expect("editor goes with prompt") - .render(self.last_width) - .iter() - .exactly_one() - .expect("MenuKeypressLine should return exactly 1 line") - .clone()); - ed.resize(self.last_width); - } - } + fn post(&mut self, client: &mut Client) -> LogicalAction { + self.post.m.content_warning = + self.el_content_warning.get_data().clone(); + self.post.m.language = self.el_language.get_data().clone(); - fn edit_content_warning(&mut self) -> LogicalAction { - let text = match &self.post.m.content_warning { - Some(s) => s.clone(), - None => "".to_owned(), - }; - self.setup_editor(Self::content_warning_item(Some("")), text, EditCW); - LogicalAction::Nothing - } - fn edit_language(&mut self) -> LogicalAction { - self.setup_editor(Self::language_item(""), - self.post.m.language.clone(), EditLang); - LogicalAction::Nothing - } - - fn post(&self, client: &mut Client) -> LogicalAction { match client.post_status(&self.post) { Ok(_) => LogicalAction::Pop, Err(_) => LogicalAction::Beep, // FIXME: report the error! @@ -298,35 +223,22 @@ impl ActivityState for PostMenu { lines.extend_from_slice(&self.ml_edit.render(w)); lines.extend_from_slice(&BlankLine::render_static()); lines.extend_from_slice(&self.ml_vis.render(w)); - - if self.mode == EditCW { - let (text, cx) = self.editor.as_ref().unwrap().draw(w); - if let Some(cx) = cx { - cursorpos = CursorPosition::At(cx, lines.len()); - } - lines.push(text); - } else { - lines.extend_from_slice(&self.ml_content_warning.render(w)); - } - - if self.mode == EditLang { - let (text, cx) = self.editor.as_ref().unwrap().draw(w); - if let Some(cx) = cx { - cursorpos = CursorPosition::At(cx, lines.len()); - } - lines.push(text); - } else { - lines.extend_from_slice(&self.ml_language.render(w)); - } + lines.push(self.el_content_warning.render( + w, &mut cursorpos, lines.len())); + lines.push(self.el_language.render( + w, &mut cursorpos, lines.len())); while lines.len() + 1 < h { lines.extend_from_slice(&BlankLine::render_static()); } - lines.extend_from_slice(&match self.mode { - Normal => self.normal_status.render(w), - _ => self.edit_status.render(w), - }); + if self.el_content_warning.is_editing() || + self.el_language.is_editing() + { + lines.extend_from_slice(&self.edit_status.render(w)); + } else { + lines.extend_from_slice(&self.normal_status.render(w)); + } (lines, cursorpos) } @@ -334,58 +246,29 @@ impl ActivityState for PostMenu { fn handle_keypress(&mut self, key: OurKey, client: &mut Client) -> LogicalAction { - match self.mode { - Normal => match key { - Space => self.post(client), - Pr('q') | Pr('Q') => LogicalAction::Pop, - Pr('a') | Pr('A') => LogicalAction::PostReEdit( - self.post.clone()), - Pr('v') | Pr('V') => self.cycle_visibility(), - Pr('w') | Pr('W') => self.edit_content_warning(), - Pr('l') | Pr('L') => self.edit_language(), - _ => LogicalAction::Nothing, - } - - EditCW => { - if self.editor.as_mut().unwrap().handle_keypress(key) { - self.post.m.content_warning = - match self.editor.as_ref().unwrap().borrow_text() { - "" => None, - s => Some(s.to_owned()), - }; - self.ml_content_warning = Self::content_warning_item( - self.post.m.content_warning.as_deref()); - - self.mode = Normal; - self.editor = None; - self.editor_prompt = None; - - self.fix_widths(); - } - LogicalAction::Nothing - } - - EditLang => { - if self.editor.as_mut().unwrap().handle_keypress(key) { - self.post.m.language = self.editor.as_ref().unwrap() - .borrow_text().to_owned(); - self.ml_language = Self::language_item( - &self.post.m.language); - - self.mode = Normal; - self.editor = None; - self.editor_prompt = None; - - self.fix_widths(); - } - LogicalAction::Nothing - } + // Let editable menu lines have first crack at the keypress + if self.el_content_warning.handle_keypress(key) || + self.el_language.handle_keypress(key) + { + self.fix_widths(); + return LogicalAction::Nothing; + } + + match key { + Space => self.post(client), + Pr('q') | Pr('Q') => LogicalAction::Pop, + Pr('a') | Pr('A') => LogicalAction::PostReEdit( + self.post.clone()), + Pr('v') | Pr('V') => self.cycle_visibility(), + Pr('w') | Pr('W') => self.el_content_warning.start_editing(), + Pr('l') | Pr('L') => self.el_language.start_editing(), + _ => LogicalAction::Nothing, } } fn resize(&mut self, w: usize, _h: usize) { - self.last_width = w; - self.set_editor_prompt(); + self.el_content_warning.resize(w); + self.el_language.resize(w); } } diff --git a/src/text.rs b/src/text.rs index d83c2fb..029c449 100644 --- a/src/text.rs +++ b/src/text.rs @@ -52,7 +52,7 @@ pub trait DisplayStyleGetter { fn poll_options(&self, id: &str) -> Option>; fn unfolded(&self, id: &str) -> bool; } -struct DefaultDisplayStyle; +pub struct DefaultDisplayStyle; impl DisplayStyleGetter for DefaultDisplayStyle { fn poll_options(&self, _id: &str) -> Option> { None } fn unfolded(&self, _id: &str) -> bool { true }