chiark / gitweb /
Build paragraphs, in a bodgy slow way.
authorSimon Tatham <anakin@pobox.com>
Mon, 25 Dec 2023 10:42:14 +0000 (10:42 +0000)
committerSimon Tatham <anakin@pobox.com>
Mon, 25 Dec 2023 11:59:48 +0000 (11:59 +0000)
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
src/text.rs

index 080679984b3231c32b5699d3642b1f890701a5a5..6a3f96590f1fcbdf1553e76bbd8776c570f04ad4 100644 (file)
@@ -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<ColouredString> 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<Self::Item> {
+        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<Self::Item> {
@@ -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");
index 05f471149d4026edf42e49b56a5b0dddcd063e99..2e7c95f19aada25ecbc225a446e9039f3b3e5677 100644 (file)
@@ -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<DateTime<Utc>>,
     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<dyn TextFragment> {
@@ -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<ColouredString>,
+    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<dyn TextFragment> { 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<ColouredString> {
+        vec! {
+            ColouredString::plain("FIXME"),
+        }
+    }
+}