chiark / gitweb /
Add highlighting methods to the TextFragment trait.
authorSimon Tatham <anakin@pobox.com>
Thu, 4 Jan 2024 07:30:41 +0000 (07:30 +0000)
committerSimon Tatham <anakin@pobox.com>
Thu, 4 Jan 2024 08:14:03 +0000 (08:14 +0000)
I've chosen to have three rather than two classes of thing you can
highlight, by separately indicating whether a text fragment contains
highlightable 'statuses' or 'whole statuses'. The distinction is that
a 'whole status' means you can see all the text, whereas a 'status'
just means you can somehow identify which post it is. I'll use that to
arrange that replying, favouriting and boosting only apply to whole
statuses (too easy to make a mistake otherwise), whereas any 'get
more info' commands apply to any old status.

(In particular, this way, you can see a one-line summary of a status,
press [I] to look at the whole thing, and _then_ fave/boost/reply.)

src/text.rs

index 527003d0656de49f5394ac8765484e799c2b7dc3..74fca60fc6f2bea976bec8c738689eeb66d4c7c7 100644 (file)
@@ -11,26 +11,142 @@ use super::types::*;
 use super::tui::OurKey;
 use super::coloured_string::{ColouredString, ColouredStringSlice};
 
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+pub enum HighlightType { User, Status, WholeStatus }
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+pub struct Highlight(pub HighlightType, pub usize);
+
+trait ConsumableHighlight {
+    fn consume(&mut self, htype: HighlightType, n: usize) -> Option<usize>;
+}
+
+impl ConsumableHighlight for Option<Highlight> {
+    fn consume(&mut self, htype: HighlightType, n: usize) -> Option<usize> {
+        let (answer, new_self) = match *self {
+            Some(hl) => {
+                if hl.0 != htype {
+                    (None, Some(hl))
+                } else if hl.1 < n {
+                    (Some(hl.1), None)
+                } else {
+                    (None, Some(Highlight(hl.0, hl.1 - n)))
+                }
+            }
+            None => (None, None),
+        };
+        *self = new_self;
+        answer
+    }
+}
+
 pub trait TextFragment {
-    // FIXME: we will also need ...
-    // Some indication of how many usernames are in here and how many statuses
-    // Some means of passing a flag to render() that says which username or
-    // status is to be highlighted
-    fn render(&self, width: usize) -> Vec<ColouredString>;
+    fn render(&self, width: usize) -> Vec<ColouredString> {
+        self.render_highlighted(width, None)
+    }
+
+    fn can_highlight(_htype: HighlightType) -> bool where Self : Sized {
+        false
+    }
+
+    fn count_highlightables(&self, _htype: HighlightType) -> usize { 0 }
+    fn highlighted_id(&self, _highlight: Option<Highlight>) -> Option<String> {
+        None
+    }
+    fn render_highlighted(&self, width: usize, _highlight: Option<Highlight>)
+                          -> Vec<ColouredString>;
+
+    fn render_highlighted_update(
+        &self, width: usize, highlight: &mut Option<Highlight>)
+        -> 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))
+                } else {
+                    (Some(Highlight(htype, index - count)),
+                     self.render_highlighted(width, None))
+                }
+            }
+            None => {
+                (None, self.render_highlighted(width, None))
+            }
+        };
+        *highlight = new_highlight;
+        text
+    }
+
+    fn highlighted_id_update(
+        &self, highlight: &mut Option<Highlight>) -> Option<String>
+    {
+        let (answer, new_highlight) = match *highlight {
+            Some(Highlight(htype, index)) => {
+                let count = self.count_highlightables(htype);
+                if index < count {
+                    (self.highlighted_id(Some(Highlight(htype, index))),
+                     None)
+                } else {
+                    (None, Some(Highlight(htype, index - count)))
+                }
+            }
+            None => (None, None),
+        };
+        *highlight = new_highlight;
+        answer
+    }
 }
 
 impl<T: TextFragment> TextFragment for Option<T> {
-    fn render(&self, width: usize) -> Vec<ColouredString> {
+    fn can_highlight(htype: HighlightType) -> bool where Self : Sized {
+        T::can_highlight(htype)
+    }
+
+    fn count_highlightables(&self, htype: HighlightType) -> usize {
         match self {
-            Some(ref inner) => inner.render(width),
+            Some(ref inner) => inner.count_highlightables(htype),
+            None => 0,
+        }
+    }
+    fn render_highlighted(&self, width: usize, highlight: Option<Highlight>)
+                          -> Vec<ColouredString> {
+        match self {
+            Some(ref inner) => inner.render_highlighted(width, highlight),
             None => Vec::new(),
         }
     }
+    fn highlighted_id(&self, highlight: Option<Highlight>) -> Option<String> {
+        match self {
+            Some(ref inner) => inner.highlighted_id(highlight),
+            None => None,
+        }
+    }
 }
 
 impl<T: TextFragment> TextFragment for Vec<T> {
-    fn render(&self, width: usize) -> Vec<ColouredString> {
-        itertools::concat(self.iter().map(|x| x.render(width)))
+    fn can_highlight(htype: HighlightType) -> bool where Self : Sized {
+        T::can_highlight(htype)
+    }
+
+    fn count_highlightables(&self, htype: HighlightType) -> usize {
+        self.iter().map(|x| x.count_highlightables(htype)).sum()
+    }
+    fn render_highlighted(&self, width: usize, highlight: Option<Highlight>)
+                          -> Vec<ColouredString> {
+        let mut highlight = highlight;
+        itertools::concat(self.iter().map(
+            |x| x.render_highlighted_update(width, &mut highlight)))
+    }
+    fn highlighted_id(&self, highlight: Option<Highlight>) -> Option<String> {
+        let mut highlight = highlight;
+        for item in self {
+            match item.highlighted_id_update(&mut highlight) {
+                result @ Some(..) => return result,
+                _ => (),
+            }
+        }
+        None
     }
 }
 
@@ -49,7 +165,9 @@ impl BlankLine {
 }
 
 impl TextFragment for BlankLine {
-    fn render(&self, _width: usize) -> Vec<ColouredString> {
+    fn render_highlighted(&self, _width: usize, _highlight: Option<Highlight>)
+                          -> Vec<ColouredString>
+    {
         Self::render_static()
     }
 }
@@ -86,7 +204,9 @@ fn format_date(date: DateTime<Utc>) -> String {
 }
 
 impl TextFragment for SeparatorLine {
-    fn render(&self, width: usize) -> Vec<ColouredString> {
+    fn render_highlighted(&self, width: usize, _highlight: Option<Highlight>)
+                          -> Vec<ColouredString>
+    {
         let mut suffix = ColouredString::plain("");
         let display_pre = ColouredString::uniform("[", 'S');
         let display_post = ColouredString::uniform("]--", 'S');
@@ -141,7 +261,9 @@ impl EditorHeaderSeparator {
 }
 
 impl TextFragment for EditorHeaderSeparator {
-    fn render(&self, width: usize) -> Vec<ColouredString> {
+    fn render_highlighted(&self, width: usize, _highlight: Option<Highlight>)
+                          -> Vec<ColouredString>
+    {
         vec! {
             ColouredString::uniform(
                 &((&"-".repeat(width - min(2, width))).to_owned() + "|"),
@@ -189,7 +311,9 @@ impl UsernameHeader {
 }
 
 impl TextFragment for UsernameHeader {
-    fn render(&self, _width: usize) -> Vec<ColouredString> {
+    fn render_highlighted(&self, _width: usize, highlight: Option<Highlight>)
+                          -> Vec<ColouredString>
+    {
         let header = ColouredString::plain(&format!("{}: ", self.header));
         let body = ColouredString::uniform(
             &format!("{} ({})", self.nameline, self.account), self.colour);
@@ -386,7 +510,9 @@ fn test_para_build() {
 }
 
 impl TextFragment for Paragraph {
-    fn render(&self, width: usize) -> Vec<ColouredString> {
+    fn render_highlighted(&self, width: usize, _highlight: Option<Highlight>)
+                          -> Vec<ColouredString>
+    {
         let mut lines = Vec::new();
         let mut curr_width = 0;
         let mut curr_pos;
@@ -490,7 +616,9 @@ impl FileHeader {
 }
 
 impl TextFragment for FileHeader {
-    fn render(&self, width: usize) -> Vec<ColouredString> {
+    fn render_highlighted(&self, width: usize, _highlight: Option<Highlight>)
+                          -> Vec<ColouredString>
+    {
         let elephants = width >= 16;
         let twidth = if elephants { width - 9 } else { width - 1 };
         let title = self.text.truncate(twidth);
@@ -601,7 +729,9 @@ impl Html {
 }
 
 impl TextFragment for Html {
-    fn render(&self, width: usize) -> Vec<ColouredString> {
+    fn render_highlighted(&self, width: usize, _highlight: Option<Highlight>)
+                          -> Vec<ColouredString>
+    {
         match self {
             Html::Rt(ref rt) => html::render(rt, width - min(width, 1)),
             Html::Bad(e) => vec! {
@@ -694,7 +824,9 @@ impl ExtendableIndicator {
 }
 
 impl TextFragment for ExtendableIndicator {
-    fn render(&self, width: usize) -> Vec<ColouredString> {
+    fn render_highlighted(&self, width: usize, _highlight: Option<Highlight>)
+                          -> Vec<ColouredString>
+    {
         // FIXME: decide how to make this message change when it's primed
         let message = if self.primed {
             ColouredString::general(
@@ -757,7 +889,9 @@ impl InReplyToLine {
 }
 
 impl TextFragment for InReplyToLine {
-    fn render(&self, width: usize) -> Vec<ColouredString> {
+    fn render_highlighted(&self, width: usize, _highlight: Option<Highlight>)
+                          -> Vec<ColouredString>
+    {
         let rendered_para = self.para.render(width - min(width, 3));
         let mut it = rendered_para.iter();
         // "Re:" guarantees the first line must exist at least
@@ -845,7 +979,9 @@ impl NotificationLog {
 }
 
 impl TextFragment for NotificationLog {
-    fn render(&self, width: usize) -> Vec<ColouredString> {
+    fn render_highlighted(&self, width: usize, highlight: Option<Highlight>)
+                          -> Vec<ColouredString>
+    {
         let mut full_para = Paragraph::new().set_indent(0, 2);
         let datestr = format_date(self.timestamp);
         full_para.push_text(&ColouredString::uniform(&datestr, ' '), false);
@@ -918,7 +1054,9 @@ impl UserListEntry {
 }
 
 impl TextFragment for UserListEntry {
-    fn render(&self, width: usize) -> Vec<ColouredString> {
+    fn render_highlighted(&self, width: usize, _highlight: Option<Highlight>)
+                          -> Vec<ColouredString>
+    {
         let mut para = Paragraph::new().set_indent(0, 2);
         // FIXME: highlight account_desc if active
         para.push_text(
@@ -966,7 +1104,9 @@ impl Media {
 }
 
 impl TextFragment for Media {
-    fn render(&self, width: usize) -> Vec<ColouredString> {
+    fn render_highlighted(&self, width: usize, _highlight: Option<Highlight>)
+                          -> Vec<ColouredString>
+    {
         let mut lines = vec! { ColouredString::uniform(&self.url, 'M') };
         for para in &self.description {
             lines.extend_from_slice(&para.render(width));
@@ -1155,7 +1295,9 @@ fn test_filestatus_build() {
 }
 
 impl TextFragment for FileStatusLineFinal {
-    fn render(&self, width: usize) -> Vec<ColouredString> {
+    fn render_highlighted(&self, width: usize, _highlight: Option<Highlight>)
+                          -> Vec<ColouredString>
+    {
         let mut line = ColouredString::plain("");
         let space = ColouredString::plain(" ").repeat(FileStatusLine::SPACING);
         let push = |line: &mut ColouredString, s: ColouredStringSlice<'_>| {
@@ -1344,7 +1486,9 @@ impl MenuKeypressLine {
 }
 
 impl TextFragment for MenuKeypressLine {
-    fn render(&self, width: usize) -> Vec<ColouredString> {
+    fn render_highlighted(&self, width: usize, _highlight: Option<Highlight>)
+                          -> Vec<ColouredString>
+    {
         let ourwidth = self.lmaxwid + self.rmaxwid + 3; // " = " in the middle
         let space = width - min(width, ourwidth + 1);
         let leftpad = min(space * 3 / 4, width - min(width, self.lmaxwid + 2));
@@ -1473,7 +1617,9 @@ fn push_fragment(lines: &mut Vec<ColouredString>, frag: Vec<ColouredString>) {
 }
 
 impl TextFragment for StatusDisplay {
-    fn render(&self, width: usize) -> Vec<ColouredString> {
+    fn render_highlighted(&self, width: usize, highlight: Option<Highlight>)
+                          -> Vec<ColouredString>
+    {
         let mut lines = Vec::new();
 
         push_fragment(&mut lines, self.sep.render(width));
@@ -1640,7 +1786,9 @@ impl DetailedStatusDisplay {
 }
 
 impl TextFragment for DetailedStatusDisplay {
-    fn render(&self, width: usize) -> Vec<ColouredString> {
+    fn render_highlighted(&self, width: usize, highlight: Option<Highlight>)
+                          -> Vec<ColouredString>
+    {
         let mut lines = Vec::new();
 
         push_fragment(&mut lines, self.sd.render(width));
@@ -1890,7 +2038,9 @@ impl ExamineUserDisplay {
 }
 
 impl TextFragment for ExamineUserDisplay {
-    fn render(&self, width: usize) -> Vec<ColouredString> {
+    fn render_highlighted(&self, width: usize, _highlight: Option<Highlight>)
+                          -> Vec<ColouredString>
+    {
         let mut lines = Vec::new();
 
         push_fragment(&mut lines, self.name.render(width));