From: Simon Tatham Date: Thu, 4 Jan 2024 07:30:41 +0000 (+0000) Subject: Add highlighting methods to the TextFragment trait. X-Git-Url: https://www.chiark.greenend.org.uk/ucgi/~ian/git?a=commitdiff_plain;h=a3cfb23578e49a4a9270de2dc9ca325859912f4c;p=mastodonochrome.git Add highlighting methods to the TextFragment trait. 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.) --- diff --git a/src/text.rs b/src/text.rs index 527003d..74fca60 100644 --- a/src/text.rs +++ b/src/text.rs @@ -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; +} + +impl ConsumableHighlight for Option { + fn consume(&mut self, htype: HighlightType, n: usize) -> Option { + 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; + fn render(&self, width: usize) -> Vec { + 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) -> Option { + None + } + fn render_highlighted(&self, width: usize, _highlight: Option) + -> Vec; + + fn render_highlighted_update( + &self, width: usize, highlight: &mut Option) + -> Vec + { + 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) -> Option + { + 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 TextFragment for Option { - fn render(&self, width: usize) -> Vec { + 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) + -> Vec { + match self { + Some(ref inner) => inner.render_highlighted(width, highlight), None => Vec::new(), } } + fn highlighted_id(&self, highlight: Option) -> Option { + match self { + Some(ref inner) => inner.highlighted_id(highlight), + None => None, + } + } } impl TextFragment for Vec { - fn render(&self, width: usize) -> Vec { - 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) + -> Vec { + let mut highlight = highlight; + itertools::concat(self.iter().map( + |x| x.render_highlighted_update(width, &mut highlight))) + } + fn highlighted_id(&self, highlight: Option) -> Option { + 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 { + fn render_highlighted(&self, _width: usize, _highlight: Option) + -> Vec + { Self::render_static() } } @@ -86,7 +204,9 @@ fn format_date(date: DateTime) -> String { } impl TextFragment for SeparatorLine { - fn render(&self, width: usize) -> Vec { + fn render_highlighted(&self, width: usize, _highlight: Option) + -> Vec + { 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 { + fn render_highlighted(&self, width: usize, _highlight: Option) + -> Vec + { 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 { + fn render_highlighted(&self, _width: usize, highlight: Option) + -> Vec + { 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 { + fn render_highlighted(&self, width: usize, _highlight: Option) + -> Vec + { 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 { + fn render_highlighted(&self, width: usize, _highlight: Option) + -> Vec + { 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 { + fn render_highlighted(&self, width: usize, _highlight: Option) + -> Vec + { 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 { + fn render_highlighted(&self, width: usize, _highlight: Option) + -> Vec + { // 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 { + fn render_highlighted(&self, width: usize, _highlight: Option) + -> Vec + { 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 { + fn render_highlighted(&self, width: usize, highlight: Option) + -> Vec + { 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 { + fn render_highlighted(&self, width: usize, _highlight: Option) + -> Vec + { 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 { + fn render_highlighted(&self, width: usize, _highlight: Option) + -> Vec + { let mut lines = vec! { ColouredString::uniform(&self.url, 'M') }; for para in &self.description { lines.extend_from_slice(¶.render(width)); @@ -1155,7 +1295,9 @@ fn test_filestatus_build() { } impl TextFragment for FileStatusLineFinal { - fn render(&self, width: usize) -> Vec { + fn render_highlighted(&self, width: usize, _highlight: Option) + -> Vec + { 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 { + fn render_highlighted(&self, width: usize, _highlight: Option) + -> Vec + { 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, frag: Vec) { } impl TextFragment for StatusDisplay { - fn render(&self, width: usize) -> Vec { + fn render_highlighted(&self, width: usize, highlight: Option) + -> Vec + { 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 { + fn render_highlighted(&self, width: usize, highlight: Option) + -> Vec + { 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 { + fn render_highlighted(&self, width: usize, _highlight: Option) + -> Vec + { let mut lines = Vec::new(); push_fragment(&mut lines, self.name.render(width));