chiark / gitweb /
More reusable concept of an editable menu line.
authorSimon Tatham <anakin@pobox.com>
Thu, 11 Jan 2024 22:42:41 +0000 (22:42 +0000)
committerSimon Tatham <anakin@pobox.com>
Fri, 12 Jan 2024 12:52:04 +0000 (12:52 +0000)
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.

src/editor.rs
src/posting.rs
src/text.rs

index 395cade64a022db3455c8ecfd6e2a917e0d66b22..a8335b312ee4cfe718ed4d40e1f2f456b4175263 100644 (file)
@@ -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<String> {
+    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<Data: EditableMenuLineData> {
+    key: OurKey,
+    description: ColouredString,
+    data: Data,
+    menuline: MenuKeypressLine,
+    prompt: MenuKeypressLine,
+    editor: Option<SingleLineEditor>,
+    last_width: Option<usize>,
+}
+
+impl<Data: EditableMenuLineData> EditableMenuLine<Data> {
+    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<Data: EditableMenuLineData> MenuKeypressLineGeneral
+for EditableMenuLine<Data> {
+    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<dyn ActivityState> {
     Box::new(BottomLineEditorOverlay::new(
         ColouredString::plain("Examine User: "),
index 9582a99ac46d2100b7e6704789ff57b76f77250c..e4d892701c16f07f0d723a8baac2d1cae7cabc44 100644 (file)
@@ -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<SingleLineEditor>,
-    editor_prompt: Option<MenuKeypressLine>,
-    last_width: usize,
+    el_content_warning: EditableMenuLine<Option<String>>,
+    el_language: EditableMenuLine<String>,
 }
 
 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);
     }
 }
 
index d83c2fbb8877c913fef36e5e8e208be5c3c44875..029c449dbe2a35deda326d332b4533196b5abad8 100644 (file)
@@ -52,7 +52,7 @@ pub trait DisplayStyleGetter {
     fn poll_options(&self, id: &str) -> Option<HashSet<usize>>;
     fn unfolded(&self, id: &str) -> bool;
 }
-struct DefaultDisplayStyle;
+pub struct DefaultDisplayStyle;
 impl DisplayStyleGetter for DefaultDisplayStyle {
     fn poll_options(&self, _id: &str) -> Option<HashSet<usize>> { None }
     fn unfolded(&self, _id: &str) -> bool { true }