From bb138cc38e6bd41b11235abeb3f5bb9164d7f03f Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Mon, 25 Dec 2023 10:42:14 +0000 Subject: [PATCH] Build paragraphs, in a bodgy slow way. For the moment, I've made a CharIterator that returns each coloured character of a ColouredString one by one. That's nasty, but easy. I think more ideally I'd want an iterator that returned contiguous chunks of a ColouredString according to a filter function passed in that classifies characters as wanting to be in the same chunk. But that involves function parameters, so I can try that later. --- src/coloured_string.rs | 59 ++++++++++++ src/text.rs | 202 ++++++++++++++++++++++++++++++++++------- 2 files changed, 230 insertions(+), 31 deletions(-) diff --git a/src/coloured_string.rs b/src/coloured_string.rs index 0806799..6a3f965 100644 --- a/src/coloured_string.rs +++ b/src/coloured_string.rs @@ -57,6 +57,11 @@ impl ColouredString { pub fn truncate(&self, width: usize) -> ColouredStringSlice { self.split(width).next().unwrap() } + + pub fn push_str(&mut self, more: &ColouredStringSlice<'_>) { + self.text.push_str(more.text); + self.colour.push_str(more.colour); + } } impl<'a> ColouredStringSlice<'a> { @@ -135,6 +140,12 @@ impl std::ops::Add for &str { } } +pub struct ColouredStringCharIterator<'a> { + cs: ColouredStringSlice<'a>, + textpos: usize, + colourpos: usize, +} + pub struct ColouredStringFragIterator<'a> { cs: ColouredStringSlice<'a>, textpos: usize, @@ -150,6 +161,13 @@ pub struct ColouredStringSplitIterator<'a> { } impl<'a> ColouredStringSlice<'a> { + pub fn chars(&self) -> ColouredStringCharIterator<'a> { + ColouredStringCharIterator { + cs: self.clone(), + textpos: 0, + colourpos: 0, + } + } pub fn frags(&self) -> ColouredStringFragIterator<'a> { ColouredStringFragIterator { cs: self.clone(), @@ -169,6 +187,13 @@ impl<'a> ColouredStringSlice<'a> { } impl<'a> ColouredString { + pub fn chars(&'a self) -> ColouredStringCharIterator<'a> { + ColouredStringCharIterator { + cs: self.slice(), + textpos: 0, + colourpos: 0, + } + } pub fn frags(&'a self) -> ColouredStringFragIterator<'a> { ColouredStringFragIterator { cs: self.slice(), @@ -187,6 +212,30 @@ impl<'a> ColouredString { } } +impl<'a> Iterator for ColouredStringCharIterator<'a> { + type Item = ColouredStringSlice<'a>; + fn next(&mut self) -> Option { + let textslice = &self.cs.text[self.textpos..]; + let mut textit = textslice.char_indices(); + let colourslice = &self.cs.colour[self.colourpos..]; + let mut colourit = colourslice.char_indices(); + if let (Some(_), Some(_)) = (textit.next(), colourit.next()) { + let (textend, colourend) = match (textit.next(), colourit.next()) { + (Some((tpos, _)), Some((cpos, _))) => (tpos, cpos), + _ => (textslice.len(), colourslice.len()), + }; + self.textpos += textend; + self.colourpos += colourend; + Some(ColouredStringSlice { + text: &textslice[..textend], + colour: &colourslice[..colourend], + }) + } else { + None + } + } +} + impl<'a> Iterator for ColouredStringFragIterator<'a> { type Item = (&'a str, char); fn next(&mut self) -> Option { @@ -310,6 +359,16 @@ fn test_lengths() { assert_eq!(ColouredString::general("xy\u{302}z", "pqqr").width(), 3); } +#[test] +fn test_chars() { + let cs = ColouredString::general("ábé", "xyz"); + let mut chars = cs.chars(); + assert_eq!(chars.next(), Some(ColouredStringSlice::general("á", "x"))); + assert_eq!(chars.next(), Some(ColouredStringSlice::general("b", "y"))); + assert_eq!(chars.next(), Some(ColouredStringSlice::general("é", "z"))); + assert_eq!(chars.next(), None); +} + #[test] fn test_frags() { let cs = ColouredString::general("stóat wèasël", "uuuuu vvvvvv"); diff --git a/src/text.rs b/src/text.rs index 05f4711..2e7c95f 100644 --- a/src/text.rs +++ b/src/text.rs @@ -1,7 +1,7 @@ use chrono::{DateTime,Utc,Local}; use core::cmp::max; -use super::coloured_string::ColouredString; +use super::coloured_string::{ColouredString, ColouredStringSlice}; pub trait TextFragment { // FIXME: we will also need ... @@ -25,6 +25,13 @@ impl TextFragment for BlankLine { } } +#[test] +fn test_blank() { + assert_eq!(blank().render(40), vec! { + ColouredString::plain("") + }); +} + pub struct SeparatorLine { timestamp: Option>, favourited: bool, @@ -77,6 +84,19 @@ impl TextFragment for SeparatorLine { } } +#[test] +fn test_separator() { + // Would be nice to test time formatting here, but I'd have to + // think of a way to test it independently of TZ + assert_eq!(separator(None, true, false) + .render(60), vec! { + ColouredString::general( + "------------------------------------------------------[F]--", + "SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSDSSS", + ) + }); +} + pub struct EditorHeaderSeparator {} pub fn editorsep() -> Box { @@ -94,6 +114,16 @@ impl TextFragment for EditorHeaderSeparator { } } +#[test] +fn test_editorsep() { + assert_eq!(editorsep().render(5), vec! { + ColouredString::general( + "---|", + "----", + ) + }); +} + pub struct UsernameHeader { header: String, colour: char, @@ -130,36 +160,6 @@ impl TextFragment for UsernameHeader { } } -#[test] -fn test_blank() { - assert_eq!(blank().render(40), vec! { - ColouredString::plain("") - }); -} - -#[test] -fn test_separator() { - // Would be nice to test time formatting here, but I'd have to - // think of a way to test it independently of TZ - assert_eq!(separator(None, true, false) - .render(60), vec! { - ColouredString::general( - "------------------------------------------------------[F]--", - "SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSDSSS", - ) - }); -} - -#[test] -fn test_editorsep() { - assert_eq!(editorsep().render(5), vec! { - ColouredString::general( - "---|", - "----", - ) - }); -} - #[test] fn test_userheader() { assert_eq!(fromline("stoat@example.com", "Some Person").render(80), vec! { @@ -176,3 +176,143 @@ fn test_userheader() { ) }); } + +#[derive(PartialEq, Eq, Debug)] +pub struct Paragraph { + words: Vec, + firstindent: usize, + laterindent: usize, + wrap: bool, +} + +impl ColouredString { + fn is_space(&self) -> bool { + if let Some(ch) = self.text().chars().next() { + ch == ' ' + } else { + false + } + } +} + +impl<'a> ColouredStringSlice<'a> { + fn is_space(&self) -> bool { + if let Some(ch) = self.text().chars().next() { + ch == ' ' + } else { + false + } + } +} + +impl Paragraph { + pub fn new() -> Self { + Paragraph { + words: Vec::new(), + firstindent: 0, + laterindent: 0, + wrap: true, + } + } + + pub fn set_wrap(mut self, wrap: bool) -> Self { + self.wrap = wrap; + self + } + + pub fn set_indent(mut self, firstindent: usize, + laterindent: usize) -> Self { + self.firstindent = firstindent; + self.laterindent = laterindent; + self + } + + pub fn add(mut self, text: &ColouredString) -> Self { + for ch in text.chars() { + if let Some(curr_word) = self.words.last_mut() { + if ch.is_space() == curr_word.is_space() { + curr_word.push_str(&ch); + continue; + } + } + self.words.push(ch.to_owned()); + } + self + } + + pub fn into_box(self) -> Box { Box::new(self) } +} + +#[test] +fn test_para_build() { + assert_eq!(Paragraph::new(), Paragraph { + words: Vec::new(), + firstindent: 0, + laterindent: 0, + wrap: true, + }); + assert_eq!(Paragraph::new().set_wrap(false), Paragraph { + words: Vec::new(), + firstindent: 0, + laterindent: 0, + wrap: false, + }); + assert_eq!(Paragraph::new().set_indent(3, 4), Paragraph { + words: Vec::new(), + firstindent: 3, + laterindent: 4, + wrap: true, + }); + assert_eq!(Paragraph::new().add(&ColouredString::plain("foo bar baz")), + Paragraph { + words: vec! { + ColouredString::plain("foo"), + ColouredString::plain(" "), + ColouredString::plain("bar"), + ColouredString::plain(" "), + ColouredString::plain("baz"), + }, + firstindent: 0, + laterindent: 0, + wrap: true, + }); + assert_eq!(Paragraph::new() + .add(&ColouredString::plain("foo ba")) + .add(&ColouredString::plain("r baz")), + Paragraph { + words: vec! { + ColouredString::plain("foo"), + ColouredString::plain(" "), + ColouredString::plain("bar"), + ColouredString::plain(" "), + ColouredString::plain("baz"), + }, + firstindent: 0, + laterindent: 0, + wrap: true, + }); + assert_eq!(Paragraph::new().add(&ColouredString::general( + " foo bar baz ", "abcdefghijklmnopq")), + Paragraph { + words: vec! { + ColouredString::general(" ", "ab"), + ColouredString::general("foo", "cde"), + ColouredString::general(" ", "fg"), + ColouredString::general("bar", "hij"), + ColouredString::general(" ", "kl"), + ColouredString::general("baz", "mno"), + ColouredString::general(" ", "pq"), + }, + firstindent: 0, + laterindent: 0, + wrap: true, + }); +} + +impl TextFragment for Paragraph { + fn render(&self, _width: usize) -> Vec { + vec! { + ColouredString::plain("FIXME"), + } + } +} -- 2.30.2