progress on text_line tests
authorBen Pfaff <blp@cs.stanford.edu>
Tue, 25 Feb 2025 03:41:20 +0000 (19:41 -0800)
committerBen Pfaff <blp@cs.stanford.edu>
Tue, 25 Feb 2025 03:41:20 +0000 (19:41 -0800)
rust/pspp/src/output/text.rs
rust/pspp/src/output/text_line.rs

index 50e4eaa9d6f678ce88fefe9d91ad1ba1b242ee60..9f788bd07da695ededdc4eadc4153ea5b72fbed0 100644 (file)
@@ -10,13 +10,11 @@ use enum_map::{Enum, EnumMap};
 use unicode_linebreak::{linebreaks, BreakOpportunity};
 use unicode_width::UnicodeWidthStr;
 
-use crate::output::pivot::DisplayValue;
-
 use super::{
     driver::Driver,
     pivot::{Axis2, BorderStyle, Coord2, PivotTable, Rect2, Stroke},
     render::{Device, DrawCell, Pager, Params},
-    table::{CellInner, Content},
+    table::Content,
     text_line::TextLine,
     Details, Item,
 };
@@ -274,30 +272,11 @@ impl TextDriver {
             return Coord2::default();
         }
 
-        /*
-           let mut breaks = linebreaks(text);
-            let bb_w = bb[Axis2::X].len();
-            let bb_h = bb[Axis2::Y].len();
-            let mut pos = 0;
-            for _ in 0..bb_h {
-                let mut w = 0;
-                loop {
-                    let (index, opportunity) = breaks.next().unwrap();
-                    match opportunity {
-                        BreakOpportunity::Mandatory => break index,
-                        BreakOpportunity::Allowed => {
-                            let segment_width = text[pos..index].width();
-                            if w > 0 && w + segment_width > bb_w {
-                                break index;
-                            }
-                            pos = index;
-                            w += segment_width;
-                        }
-                    }
-                }
-                todo!()
+        let mut breaks = new_line_breaks(text, bb[Axis2::X].len());
+        let mut size = Coord2::new(0, 0);
+        for text in breaks {
+            
         }
-             */
         todo!()
     }
 }
index 79f1164c68952fe2f0a634e5b2d9079fdea1d441..33d9419684a26b4a538ab9a80d6e1d384f930b1a 100644 (file)
@@ -1,6 +1,7 @@
-use std::ops::Range;
+use enum_iterator::Sequence;
+use std::{borrow::Cow, fmt::Debug, ops::Range};
 
-use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
+use unicode_width::UnicodeWidthChar;
 
 /// A line of text, encoded in UTF-8, with support functions that properly
 /// handle double-width characters and backspaces.
@@ -43,7 +44,8 @@ impl TextLine {
     }
 
     pub fn put(&mut self, x0: usize, s: &str) {
-        let w = s.width();
+        let w = Widths::new(s).sum::<usize>();
+        dbg!(w);
         let x1 = x0 + w;
         if w == 0 {
             // Nothing to do.
@@ -62,7 +64,10 @@ impl TextLine {
             self.string.push_str(s);
             self.width = x1;
         } else {
+            dbg!(x0);
+            dbg!(x1);
             let span = self.find_span(x0, x1);
+            dbg!(&span);
             if span.columns.start < x0 || span.columns.end > x1 {
                 let tail = self.string.split_off(span.offsets.end);
                 self.string.truncate(span.offsets.start);
@@ -79,7 +84,7 @@ impl TextLine {
     fn find_span(&self, x0: usize, x1: usize) -> Position {
         let p0 = self.find_pos(x0);
         let p1 = self.find_pos(x1);
-        if p0.columns.start >= x1 {
+        if true || p0.columns.start >= x1 {
             Position {
                 columns: p0.columns.start..p1.columns.start,
                 offsets: p0.offsets.start..p1.offsets.start,
@@ -92,6 +97,7 @@ impl TextLine {
         }
     }
 
+    // Returns the [Position] that contains column `target_x`.
     fn find_pos(&self, target_x: usize) -> Position {
         let mut x = 0;
         let mut ofs = 0;
@@ -113,9 +119,14 @@ impl TextLine {
             offsets: ofs..ofs,
         }
     }
+
+    fn str(&self) -> &str {
+        &self.string
+    }
 }
 
 /// Position of one or more characters within a [TextLine].
+#[derive(Debug)]
 struct Position {
     /// 0-based display columns.
     columns: Range<usize>,
@@ -124,6 +135,7 @@ struct Position {
     offsets: Range<usize>,
 }
 
+/// Iterates through the column widths in a string.
 struct Widths<'a> {
     s: &'a str,
     base: &'a str,
@@ -134,10 +146,13 @@ impl<'a> Widths<'a> {
         Self { s, base: s }
     }
 
+    /// Returns the amount of the string remaining to be visited.
     fn as_str(&self) -> &str {
         self.s
     }
 
+    // Returns the offset into the original string of the characters remaining
+    // to be visited.
     fn offset(&self) -> usize {
         self.base.len() - self.s.len()
     }
@@ -149,7 +164,7 @@ impl<'a> Iterator for Widths<'a> {
     fn next(&mut self) -> Option<Self::Item> {
         let mut iter = self.s.char_indices();
         let (_, mut c) = iter.next()?;
-        if iter.as_str().starts_with('\x08') {
+        while iter.as_str().starts_with('\x08') {
             iter.next();
             c = match iter.next() {
                 Some((_, c)) => c,
@@ -176,3 +191,242 @@ impl<'a> Iterator for Widths<'a> {
         Some(w)
     }
 }
+
+#[derive(Copy, Clone, PartialEq, Eq, Sequence)]
+struct Emphasis {
+    pub bold: bool,
+    pub underline: bool,
+}
+
+impl Emphasis {
+    const fn plain() -> Self {
+        Self {
+            bold: false,
+            underline: false,
+        }
+    }
+    fn is_plain(&self) -> bool {
+        *self == Self::plain()
+    }
+    fn apply<'a>(&self, s: &'a str) -> Cow<'a, str> {
+        if self.is_plain() {
+            Cow::from(s)
+        } else {
+            let mut output = String::with_capacity(
+                s.len() * (1 + self.bold as usize * 2 + self.underline as usize * 2),
+            );
+            for c in s.chars() {
+                if self.bold {
+                    output.push(c);
+                    output.push('\x08');
+                }
+                if self.underline {
+                    output.push('_');
+                    output.push('\x08');
+                }
+                output.push(c);
+            }
+            Cow::from(output)
+        }
+    }
+}
+
+impl Debug for Emphasis {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(
+            f,
+            "{}",
+            match self {
+                Self {
+                    bold: false,
+                    underline: false,
+                } => "plain",
+                Self {
+                    bold: true,
+                    underline: false,
+                } => "bold",
+                Self {
+                    bold: false,
+                    underline: true,
+                } => "underline",
+                Self {
+                    bold: true,
+                    underline: true,
+                } => "bold+underline",
+            }
+        )
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use super::{Emphasis, TextLine};
+    use enum_iterator::all;
+
+    #[test]
+    fn overwrite_rest_of_line() {
+        for lowercase in all::<Emphasis>() {
+            for uppercase in all::<Emphasis>() {
+                let mut line = TextLine::new();
+                line.put(0, &lowercase.apply("abc"));
+                line.put(1, &uppercase.apply("BCD"));
+                assert_eq!(
+                    line.str(),
+                    &format!("{}{}", lowercase.apply("a"), uppercase.apply("BCD")),
+                    "uppercase={uppercase:?} lowercase={lowercase:?}"
+                );
+            }
+        }
+    }
+
+    #[test]
+    fn overwrite_partial_line() {
+        for lowercase in all::<Emphasis>() {
+            for uppercase in all::<Emphasis>() {
+                let mut line = TextLine::new();
+                // Produces `AbCDEf`.
+                line.put(0, &lowercase.apply("abcdef"));
+                line.put(0, &uppercase.apply("A"));
+                line.put(2, &uppercase.apply("CDE"));
+                assert_eq!(
+                    line.str().replace('\x08', "#"),
+                    format!(
+                        "{}{}{}{}",
+                        uppercase.apply("A"),
+                        lowercase.apply("b"),
+                        uppercase.apply("CDE"),
+                        lowercase.apply("f")
+                    )
+                    .replace('\x08', "#"),
+                    "uppercase={uppercase:?} lowercase={lowercase:?}"
+                );
+            }
+        }
+    }
+
+    #[test]
+    fn overwrite_rest_with_double_width() {
+        for lowercase in all::<Emphasis>() {
+            for hiragana in all::<Emphasis>() {
+                let mut line = TextLine::new();
+                // Produces `kaきくけ"`.
+                line.put(0, &lowercase.apply("kakiku"));
+                line.put(2, &hiragana.apply("きくけ"));
+                assert_eq!(
+                    line.str(),
+                    &format!("{}{}", lowercase.apply("ka"), hiragana.apply("きくけ")),
+                    "lowercase={lowercase:?} hiragana={hiragana:?}"
+                );
+            }
+        }
+    }
+
+    #[test]
+    fn overwrite_partial_with_double_width() {
+        for lowercase in all::<Emphasis>() {
+            for hiragana in all::<Emphasis>() {
+                let mut line = TextLine::new();
+                // Produces `かkiくけko".
+                line.put(0, &lowercase.apply("kakikukeko"));
+                line.put(0, &hiragana.apply("か"));
+                line.put(4, &hiragana.apply("くけ"));
+                assert_eq!(
+                    line.str(),
+                    &format!(
+                        "{}{}{}{}",
+                        hiragana.apply("か"),
+                        lowercase.apply("ki"),
+                        hiragana.apply("くけ"),
+                        lowercase.apply("ko")
+                    ),
+                    "lowercase={lowercase:?} hiragana={hiragana:?}"
+                );
+            }
+        }
+    }
+
+    /// Overwrite rest of line, aligned double-width over double-width
+    #[test]
+    fn aligned_double_width_rest_of_line() {
+        for bottom in all::<Emphasis>() {
+            for top in all::<Emphasis>() {
+                let mut line = TextLine::new();
+                // Produces `あきくけ`.
+                line.put(0, &bottom.apply("あいう"));
+                line.put(2, &top.apply("きくけ"));
+                assert_eq!(
+                    line.str(),
+                    &format!("{}{}", bottom.apply("あ"), top.apply("きくけ")),
+                    "bottom={bottom:?} top={top:?}"
+                );
+            }
+        }
+    }
+
+    /// Overwrite rest of line, misaligned double-width over double-width
+    #[test]
+    fn misaligned_double_width_rest_of_line() {
+        for bottom in all::<Emphasis>() {
+            for top in all::<Emphasis>() {
+                let mut line = TextLine::new();
+                // Produces `あきくけ`.
+                line.put(0, &bottom.apply("あいう"));
+                line.put(3, &top.apply("きくけ"));
+                assert_eq!(
+                    line.str(),
+                    &format!("{}?{}", bottom.apply("あ"), top.apply("きくけ")),
+                    "bottom={bottom:?} top={top:?}"
+                );
+            }
+        }
+    }
+
+    /// Overwrite partial line, aligned double-width over double-width
+    #[test]
+    fn aligned_double_width_partial() {
+        for bottom in all::<Emphasis>() {
+            for top in all::<Emphasis>() {
+                let mut line = TextLine::new();
+                // Produces `かいくけお`.
+                line.put(0, &bottom.apply("あいうえお"));
+                line.put(0, &top.apply("か"));
+                line.put(4, &top.apply("くけ"));
+                assert_eq!(
+                    line.str(),
+                    &format!(
+                        "{}{}{}{}",
+                        top.apply("か"),
+                        bottom.apply("い"),
+                        top.apply("くけ"),
+                        bottom.apply("お")
+                    ),
+                    "bottom={bottom:?} top={top:?}"
+                );
+            }
+        }
+    }
+
+    /// Overwrite partial line, misaligned double-width over double-width
+    #[test]
+    fn misaligned_double_width_partial() {
+        for bottom in all::<Emphasis>() {
+            for top in all::<Emphasis>() {
+                let mut line = TextLine::new();
+                // Produces `?か??くけ?さ`.
+                line.put(0, &bottom.apply("あいうえお"));
+                line.put(1, &top.apply("か"));
+                //line.put(5, &top.apply("くけ"));
+                assert_eq!(
+                    line.str(),
+                    &format!(
+                        "?{}??{}{}",
+                        top.apply("か"),
+                        top.apply("くけ"),
+                        bottom.apply("さ")
+                    ),
+                    "bottom={bottom:?} top={top:?}"
+                );
+            }
+        }
+    }
+}