chiark / gitweb /
Wrap poll_options into a general display-style trait.
authorSimon Tatham <anakin@pobox.com>
Mon, 8 Jan 2024 19:39:58 +0000 (19:39 +0000)
committerSimon Tatham <anakin@pobox.com>
Thu, 11 Jan 2024 08:05:28 +0000 (08:05 +0000)
TODO.md
src/file.rs
src/text.rs

diff --git a/TODO.md b/TODO.md
index da2b3e042826b875d2ee95c10e4787ec751a9389..92386e66c89f101dd9944fb86e0b793d0ca571e4 100644 (file)
--- a/TODO.md
+++ b/TODO.md
@@ -46,11 +46,6 @@ Currently we display sensitive-content tags, but then display the rest
 of the post anyway. It would be better to (at least have the option
 to) hide the post and then be able to show it on demand.
 
-This will probably involve some kind of state object in File which
-maps post IDs to data about them that informs display. Once we have
-that, it could also be used to make the handling of unsubmitted poll
-results less horrible.
-
 ### Scrolling to keep the selection in view
 
 Various keyboard commands let you select a user or a post from a file
index bfffdbabeea12b581f2d2ce2b87d706452b16408..1ddf913dede410302ce8ecc20de57aa261604ed3 100644 (file)
@@ -312,6 +312,23 @@ enum UIMode {
 #[derive(Debug, PartialEq, Eq, Clone, Copy)]
 pub enum SearchDirection { Up, Down }
 
+#[derive(Default)]
+struct FileDisplayStyles {
+    selected_poll_id: Option<String>,
+    selected_poll_options: HashSet<usize>,
+}
+impl DisplayStyleGetter for FileDisplayStyles {
+    fn poll_options(&self, id: &str) -> Option<HashSet<usize>> {
+        self.selected_poll_id.as_ref().and_then(
+            |s| if s == id {
+                Some(self.selected_poll_options.clone())
+            } else {
+                None
+            }
+        )
+    }
+}
+
 struct File<Type: FileType, Source: FileDataSource> {
     contents: FileContents<Type, Source>,
     rendered: HashMap<isize, Vec<ColouredString>>,
@@ -321,7 +338,7 @@ struct File<Type: FileType, Source: FileDataSource> {
     selection: Option<(isize, usize)>,
     selection_restrict_to_item: Option<isize>,
     select_aux: Option<bool>, // distinguishes fave from unfave, etc
-    poll_options_selected: HashSet<usize>,
+    display_styles: FileDisplayStyles,
     file_desc: Type,
     search_direction: Option<SearchDirection>,
     last_search: Option<Regex>,
@@ -394,7 +411,7 @@ impl<Type: FileType, Source: FileDataSource> File<Type, Source> {
             selection: None,
             selection_restrict_to_item: None,
             select_aux: None,
-            poll_options_selected: HashSet::new(),
+            display_styles: FileDisplayStyles::default(),
             file_desc,
             search_direction: None,
             last_search: None,
@@ -421,15 +438,8 @@ impl<Type: FileType, Source: FileDataSource> File<Type, Source> {
                 _ => None,
             };
 
-            let poll_options =
-                if self.selection_restrict_to_item == Some(index) {
-                    Some(&self.poll_options_selected)
-                } else {
-                    None
-                };
-
             for line in self.contents.get(index)
-                .render_highlighted(w, highlight, poll_options)
+                .render_highlighted(w, highlight, &self.display_styles)
             {
                 for frag in line.split(w) {
                     lines.push(frag.into());
@@ -718,6 +728,12 @@ impl<Type: FileType, Source: FileDataSource> File<Type, Source> {
             LogicalAction::Beep
         };
 
+        self.display_styles.selected_poll_id = match self.ui_mode {
+            UIMode::Select(HighlightType::PollOption, _) =>
+                self.selected_id(self.selection),
+            _ => None,
+        };
+
         self.select_aux = match self.ui_mode {
             UIMode::Select(_, SelectionPurpose::Favourite) => {
                 match self.selected_id(self.selection) {
@@ -791,16 +807,15 @@ impl<Type: FileType, Source: FileDataSource> File<Type, Source> {
             "we should only call this if we have a selection");
         self.selection_restrict_to_item = Some(item);
         if self.contents.get(item).is_multiple_choice_poll() {
-            if self.poll_options_selected.contains(&sub) {
-                self.poll_options_selected.remove(&sub);
+            if self.display_styles.selected_poll_options.contains(&sub) {
+                self.display_styles.selected_poll_options.remove(&sub);
             } else {
-                self.poll_options_selected.insert(sub);
+                self.display_styles.selected_poll_options.insert(sub);
             }
         } else {
-            self.poll_options_selected.clear();
-            self.poll_options_selected.insert(sub);
+            self.display_styles.selected_poll_options.clear();
+            self.display_styles.selected_poll_options.insert(sub);
         }
-        dbg!(&self.poll_options_selected);
         self.rerender_selected_item();
         self.ensure_enough_rendered();
         LogicalAction::Nothing
@@ -812,7 +827,8 @@ impl<Type: FileType, Source: FileDataSource> File<Type, Source> {
             self.selection = None;
             self.ensure_enough_rendered();
         }
-        self.poll_options_selected.clear();
+        self.display_styles.selected_poll_id = None;
+        self.display_styles.selected_poll_options.clear();
         self.ui_mode = UIMode::Normal;
     }
 
@@ -884,7 +900,7 @@ impl<Type: FileType, Source: FileDataSource> File<Type, Source> {
                     UtilityActivity::ThreadFile(id, alt).into()),
                 SelectionPurpose::Vote => {
                     match client.vote_in_poll(
-                        &id, self.poll_options_selected.iter().copied()) {
+                        &id, self.display_styles.selected_poll_options.iter().copied()) {
                         Ok(_) => {
                             self.contents.update_items(client);
                             LogicalAction::Nothing
@@ -1136,7 +1152,9 @@ impl<Type: FileType, Source: FileDataSource>
                         // the keypress for selecting something.
                         // Otherwise, prioritise the one for
                         // submitting your answer.
-                        if self.poll_options_selected.is_empty() {
+                        if self.display_styles.selected_poll_options
+                            .is_empty()
+                        {
                             fs.add(Space, verb, 98)
                                 .add(Ctrl('V'), "Submit Vote", 97)
                         } else {
index 0caf31ef64c6818adef8427de1a0330af30ce397..4fd9dad02fa6579ab3bffa3c376523d98f2bb5f9 100644 (file)
@@ -40,9 +40,19 @@ impl ConsumableHighlight for Option<Highlight> {
     }
 }
 
+// Trait that can be passed by the caller of render_highlighted(),
+// providing information to modify how things are displayed
+pub trait DisplayStyleGetter {
+    fn poll_options(&self, id: &str) -> Option<HashSet<usize>>;
+}
+struct DefaultDisplayStyle;
+impl DisplayStyleGetter for DefaultDisplayStyle {
+    fn poll_options(&self, _id: &str) -> Option<HashSet<usize>> { None }
+}
+
 pub trait TextFragment {
     fn render(&self, width: usize) -> Vec<ColouredString> {
-        self.render_highlighted(width, None, None)
+        self.render_highlighted(width, None, &DefaultDisplayStyle)
     }
 
     fn can_highlight(_htype: HighlightType) -> bool where Self : Sized {
@@ -54,29 +64,27 @@ pub trait TextFragment {
         None
     }
     fn render_highlighted(&self, width: usize, highlight: Option<Highlight>,
-                          poll_options: Option<&HashSet<usize>>)
+                          style: &dyn DisplayStyleGetter)
                           -> Vec<ColouredString>;
 
     fn is_multiple_choice_poll(&self) -> bool { false }
 
     fn render_highlighted_update(
         &self, width: usize, highlight: &mut Option<Highlight>,
-        poll_options: Option<&HashSet<usize>>)
-        -> Vec<ColouredString>
+        style: &dyn DisplayStyleGetter) -> Vec<ColouredString>
     {
         let (new_highlight, text) = match *highlight {
             Some(Highlight(htype, index)) => {
                 let count = self.count_highlightables(htype);
                 if index < count {
-                    (None, self.render_highlighted(
-                        width, *highlight, poll_options))
+                    (None, self.render_highlighted(width, *highlight, style))
                 } else {
                     (Some(Highlight(htype, index - count)),
-                     self.render_highlighted(width, None, poll_options))
+                     self.render_highlighted(width, None, style))
                 }
             }
             None => {
-                (None, self.render_highlighted(width, None, poll_options))
+                (None, self.render_highlighted(width, None, style))
             }
         };
         *highlight = new_highlight;
@@ -115,11 +123,11 @@ impl<T: TextFragment> TextFragment for Option<T> {
         }
     }
     fn render_highlighted(&self, width: usize, highlight: Option<Highlight>,
-                          poll_options: Option<&HashSet<usize>>)
+                          style: &dyn DisplayStyleGetter)
                           -> Vec<ColouredString> {
         match self {
             Some(ref inner) => inner.render_highlighted(
-                width, highlight, poll_options),
+                width, highlight, style),
             None => Vec::new(),
         }
     }
@@ -140,13 +148,11 @@ impl<T: TextFragment> TextFragment for Vec<T> {
         self.iter().map(|x| x.count_highlightables(htype)).sum()
     }
     fn render_highlighted(&self, width: usize, highlight: Option<Highlight>,
-                          _poll_options: Option<&HashSet<usize>>)
+                          style: &dyn DisplayStyleGetter)
                           -> Vec<ColouredString> {
         let mut highlight = highlight;
-        // Ignore poll_options, because that's only used when we're
-        // rendering a single item containing the poll in question
         itertools::concat(self.iter().map(
-            |x| x.render_highlighted_update(width, &mut highlight, None)))
+            |x| x.render_highlighted_update(width, &mut highlight, style)))
     }
     fn highlighted_id(&self, highlight: Option<Highlight>) -> Option<String> {
         let mut highlight = highlight;
@@ -177,7 +183,7 @@ impl BlankLine {
 
 impl TextFragment for BlankLine {
     fn render_highlighted(&self, _width: usize, _highlight: Option<Highlight>,
-                          _poll_options: Option<&HashSet<usize>>)
+                          _style: &dyn DisplayStyleGetter)
                           -> Vec<ColouredString>
     {
         Self::render_static()
@@ -217,7 +223,7 @@ fn format_date(date: DateTime<Utc>) -> String {
 
 impl TextFragment for SeparatorLine {
     fn render_highlighted(&self, width: usize, _highlight: Option<Highlight>,
-                          _poll_options: Option<&HashSet<usize>>)
+                          _style: &dyn DisplayStyleGetter)
                           -> Vec<ColouredString>
     {
         let mut suffix = ColouredString::plain("");
@@ -272,7 +278,7 @@ impl EditorHeaderSeparator {
 
 impl TextFragment for EditorHeaderSeparator {
     fn render_highlighted(&self, width: usize, _highlight: Option<Highlight>,
-                          _poll_options: Option<&HashSet<usize>>)
+                          _style: &dyn DisplayStyleGetter)
                           -> Vec<ColouredString>
     {
         vec! {
@@ -326,7 +332,7 @@ impl UsernameHeader {
 
 impl TextFragment for UsernameHeader {
     fn render_highlighted(&self, _width: usize, highlight: Option<Highlight>,
-                          _poll_options: Option<&HashSet<usize>>)
+                          _style: &dyn DisplayStyleGetter)
                           -> Vec<ColouredString>
     {
         let header = ColouredString::plain(&format!("{}: ", self.header));
@@ -556,7 +562,7 @@ fn test_para_build() {
 
 impl TextFragment for Paragraph {
     fn render_highlighted(&self, width: usize, _highlight: Option<Highlight>,
-                          _poll_options: Option<&HashSet<usize>>)
+                          _style: &dyn DisplayStyleGetter)
                           -> Vec<ColouredString>
     {
         let mut lines = Vec::new();
@@ -663,7 +669,7 @@ impl FileHeader {
 
 impl TextFragment for FileHeader {
     fn render_highlighted(&self, width: usize, _highlight: Option<Highlight>,
-                          _poll_options: Option<&HashSet<usize>>)
+                          _style: &dyn DisplayStyleGetter)
                           -> Vec<ColouredString>
     {
         let elephants = width >= 16;
@@ -784,7 +790,7 @@ impl Html {
 
 impl TextFragment for Html {
     fn render_highlighted(&self, width: usize, _highlight: Option<Highlight>,
-                          _poll_options: Option<&HashSet<usize>>)
+                          _style: &dyn DisplayStyleGetter)
                           -> Vec<ColouredString>
     {
         match self {
@@ -880,7 +886,7 @@ impl ExtendableIndicator {
 
 impl TextFragment for ExtendableIndicator {
     fn render_highlighted(&self, width: usize, _highlight: Option<Highlight>,
-                          _poll_options: Option<&HashSet<usize>>)
+                          _style: &dyn DisplayStyleGetter)
                           -> Vec<ColouredString>
     {
         let message = if self.primed {
@@ -953,7 +959,7 @@ impl InReplyToLine {
 
 impl TextFragment for InReplyToLine {
     fn render_highlighted(&self, width: usize, _highlight: Option<Highlight>,
-                          _poll_options: Option<&HashSet<usize>>)
+                          _style: &dyn DisplayStyleGetter)
                           -> Vec<ColouredString>
     {
         let rendered_para = self.para.render(width - min(width, 3));
@@ -1002,7 +1008,7 @@ impl VisibilityLine {
 
 impl TextFragment for VisibilityLine {
     fn render_highlighted(&self, width: usize, _highlight: Option<Highlight>,
-                          _poll_options: Option<&HashSet<usize>>)
+                          _style: &dyn DisplayStyleGetter)
                           -> Vec<ColouredString>
     {
         let line = match self.vis {
@@ -1082,7 +1088,7 @@ impl NotificationLog {
 
 impl TextFragment for NotificationLog {
     fn render_highlighted(&self, width: usize, highlight: Option<Highlight>,
-                          _poll_options: Option<&HashSet<usize>>)
+                          _style: &dyn DisplayStyleGetter)
                           -> Vec<ColouredString>
     {
         let mut full_para = Paragraph::new().set_indent(0, 2);
@@ -1195,7 +1201,7 @@ impl UserListEntry {
 
 impl TextFragment for UserListEntry {
     fn render_highlighted(&self, width: usize, highlight: Option<Highlight>,
-                          _poll_options: Option<&HashSet<usize>>)
+                          _style: &dyn DisplayStyleGetter)
                           -> Vec<ColouredString>
     {
         let mut para = Paragraph::new().set_indent(0, 2);
@@ -1268,7 +1274,7 @@ impl Media {
 
 impl TextFragment for Media {
     fn render_highlighted(&self, width: usize, _highlight: Option<Highlight>,
-                          _poll_options: Option<&HashSet<usize>>)
+                          _style: &dyn DisplayStyleGetter)
                           -> Vec<ColouredString>
     {
         let mut lines: Vec<_> = ColouredString::uniform(&self.url, 'M')
@@ -1461,7 +1467,7 @@ fn test_filestatus_build() {
 
 impl TextFragment for FileStatusLineFinal {
     fn render_highlighted(&self, width: usize, _highlight: Option<Highlight>,
-                          _poll_options: Option<&HashSet<usize>>)
+                          _style: &dyn DisplayStyleGetter)
                           -> Vec<ColouredString>
     {
         let mut line = ColouredString::plain("");
@@ -1649,7 +1655,7 @@ impl MenuKeypressLine {
 
 impl TextFragment for MenuKeypressLine {
     fn render_highlighted(&self, width: usize, _highlight: Option<Highlight>,
-                          _poll_options: Option<&HashSet<usize>>)
+                          _style: &dyn DisplayStyleGetter)
                           -> Vec<ColouredString>
     {
         let ourwidth = self.lmaxwid + self.rmaxwid + 3; // " = " in the middle
@@ -1723,7 +1729,7 @@ impl CentredInfoLine {
 
 impl TextFragment for CentredInfoLine {
     fn render_highlighted(&self, width: usize, _highlight: Option<Highlight>,
-                          _poll_options: Option<&HashSet<usize>>)
+                          _style: &dyn DisplayStyleGetter)
                           -> Vec<ColouredString>
     {
         let twidth = width.saturating_sub(1);
@@ -1886,7 +1892,7 @@ fn push_fragment_highlighted(lines: &mut Vec<ColouredString>,
 
 impl TextFragment for StatusDisplay {
     fn render_highlighted(&self, width: usize, highlight: Option<Highlight>,
-                          poll_options_selected: Option<&HashSet<usize>>)
+                          style: &dyn DisplayStyleGetter)
                           -> Vec<ColouredString>
     {
         let mut lines = Vec::new();
@@ -1903,9 +1909,9 @@ impl TextFragment for StatusDisplay {
 
         push_fragment(&mut lines, self.sep.render(width));
         push_fragment(&mut lines, self.from.render_highlighted_update(
-            width, &mut highlight, None));
+            width, &mut highlight, &DefaultDisplayStyle));
         push_fragment(&mut lines, self.via.render_highlighted_update(
-            width, &mut highlight, None));
+            width, &mut highlight, &DefaultDisplayStyle));
         push_fragment(&mut lines, self.vis.render(width));
         push_fragment(&mut lines, self.irt.render(width));
         push_fragment(&mut lines, self.blank.render(width));
@@ -1922,10 +1928,11 @@ impl TextFragment for StatusDisplay {
         }
         if let Some(poll) = &self.poll {
             push_fragment_opt_highlight(&mut lines, poll.title.render(width));
+            let poll_options_selected = style.poll_options(&poll.id);
             for (i, (voted, desc)) in poll.options.iter().enumerate() {
                 let highlighting_this_option =
                     highlight.consume(HighlightType::PollOption, 1) == Some(0);
-                let voted = poll_options_selected.map_or(
+                let voted = poll_options_selected.as_ref().map_or(
                     *voted, |opts| opts.contains(&i));
                 let option = Paragraph::new().set_indent(0, 2).add(
                     &ColouredString::uniform(
@@ -2157,14 +2164,14 @@ impl DetailedStatusDisplay {
 
 impl TextFragment for DetailedStatusDisplay {
     fn render_highlighted(&self, width: usize, highlight: Option<Highlight>,
-                          poll_options: Option<&HashSet<usize>>)
+                          style: &dyn DisplayStyleGetter)
                           -> Vec<ColouredString>
     {
         let mut lines = Vec::new();
         let mut highlight = highlight;
 
         push_fragment(&mut lines, self.sd.render_highlighted_update(
-            width, &mut highlight, poll_options));
+            width, &mut highlight, style));
         push_fragment(&mut lines, self.sep.render(width));
         push_fragment(&mut lines, self.blank.render(width));
 
@@ -2502,7 +2509,7 @@ impl ExamineUserDisplay {
 
 impl TextFragment for ExamineUserDisplay {
     fn render_highlighted(&self, width: usize, _highlight: Option<Highlight>,
-                          _poll_options: Option<&HashSet<usize>>)
+                          _style: &dyn DisplayStyleGetter)
                           -> Vec<ColouredString>
     {
         let mut lines = Vec::new();