work?
authorBen Pfaff <blp@cs.stanford.edu>
Sun, 23 Feb 2025 18:45:17 +0000 (10:45 -0800)
committerBen Pfaff <blp@cs.stanford.edu>
Sun, 23 Feb 2025 18:45:17 +0000 (10:45 -0800)
rust/Cargo.lock
rust/pspp/Cargo.toml
rust/pspp/src/output/render.rs
rust/pspp/src/output/text.rs

index 33edf04474d225ed33ff4b97dc36bf83f9d93ac3..10d655058b507da7c4aa4e285ac45168c66bdb0a 100644 (file)
@@ -922,6 +922,7 @@ dependencies = [
  "smallvec",
  "thiserror",
  "unicase",
+ "unicode-linebreak",
  "unicode-width",
  "utf8-decode",
  "windows-sys 0.48.0",
@@ -1342,6 +1343,12 @@ version = "1.0.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
 
+[[package]]
+name = "unicode-linebreak"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f"
+
 [[package]]
 name = "unicode-normalization"
 version = "0.1.23"
index 0536ee4ec05669917d9ab70858656ed6e0042895..526073e6bfd9471534a0a83c5040c4788efd34ff 100644 (file)
@@ -35,6 +35,7 @@ smallvec = { version = "1.13.2", features = ["const_generics", "write"] }
 libm = "0.2.11"
 smallstr = "0.3.0"
 itertools = "0.14.0"
+unicode-linebreak = "0.1.5"
 
 [target.'cfg(windows)'.dependencies]
 windows-sys = { version = "0.48.0", features = ["Win32_Globalization"] }
index b4937f707ed0c1980ed5639d0dd6332a0f108a1e..0e19ccef08ea7a84dacf4725c5149e05dddc894c 100644 (file)
@@ -71,6 +71,13 @@ pub struct Params {
     pub can_scale: bool,
 }
 
+impl Params {
+    /// Returns a small but visible width.
+    fn em(&self) -> usize {
+        self.font_size[Axis2::X]
+    }
+}
+
 pub trait Device {
     fn params(&self) -> &Params;
 
@@ -79,10 +86,10 @@ pub trait Device {
     /// single word across multiple lines (normally, this is the width of the
     /// longest word in the cell) and `max_width` is the minimum width required
     /// to avoid line breaks other than at new-lines.
-    fn measure_cell_width(&self, cell: &CellInner) -> [usize; 2];
+    fn measure_cell_width(&self, cell: &DrawCell) -> [usize; 2];
 
     /// Returns the height required to render `cell` given a width of `width`.
-    fn measure_cell_height(&self, cell: &CellInner, width: usize) -> usize;
+    fn measure_cell_height(&self, cell: &DrawCell, width: usize) -> usize;
 
     /// Given that there is space measuring `size` to render `cell`, where
     /// `size.y()` is insufficient to render the entire height of the cell,
@@ -95,9 +102,7 @@ pub trait Device {
     /// Optional.  If [RenderParams::can_adjust_break] is false, the rendering
     /// engine assumes that all breakpoints are acceptable.
     fn adjust_break(&self, cell: &Content, size: Coord2) -> usize;
-}
 
-pub trait Draw {
     /// Draws a generalized intersection of lines in `bb`.
     ///
     /// `styles` is interpreted this way:
@@ -146,6 +151,27 @@ pub struct DrawCell<'a> {
     pub footnotes: &'a [Arc<Footnote>],
 }
 
+impl<'a> DrawCell<'a> {
+    fn new(inner: &'a CellInner, table: &'a Table) -> Self {
+        let (style, subscripts, footnotes) = if let Some(styling) = inner.value.styling.as_ref() {
+            (
+                &styling.style,
+                styling.subscripts.as_slice(),
+                styling.footnotes.as_slice(),
+            )
+        } else {
+            (&table.areas[inner.area], [].as_slice(), [].as_slice())
+        };
+        Self {
+            rotate: inner.rotate,
+            inner: &inner.value.inner,
+            style,
+            subscripts,
+            footnotes,
+        }
+    }
+}
+
 /// A layout for rendering a specific table on a specific device.
 ///
 /// May represent the layout of an entire table presented to [Pager::new], or a
@@ -162,7 +188,6 @@ pub struct DrawCell<'a> {
 /// The vertical cells rendered are the topmost `h[Y]`, then `r[Y]`.
 /// `n[i]` is the sum of `h[i]` and `r[i].len()`.
 struct Page {
-    device: Arc<dyn Device>,
     table: Arc<Table>,
 
     /// Size of the table in cells.
@@ -342,7 +367,7 @@ impl Page {
     /// The new [Page] will be suitable for rendering on a device whose page
     /// size is `params.size`, but the caller is responsible for actually
     /// breaking it up to fit on such a device, using the [Break] abstraction.
-    fn new(table: Arc<Table>, device: Arc<dyn Device>, min_width: usize, look: &Look) -> Self {
+    fn new(table: Arc<Table>, device: &dyn Device, min_width: usize, look: &Look) -> Self {
         use Axis2::*;
 
         let n = table.n.clone();
@@ -350,7 +375,7 @@ impl Page {
         // Figure out rule widths.
         let rules = EnumMap::from_fn(|axis| {
             (0..n[axis])
-                .map(|z| measure_rule(&*table, &*device, axis, z))
+                .map(|z| measure_rule(&*device, &*table, axis, z))
                 .collect::<Vec<_>>()
         });
 
@@ -364,7 +389,7 @@ impl Page {
         // multiple columns.
         let mut unspanned_columns = [vec![0; n.x()], vec![0; n.x()]];
         for cell in table.cells().filter(|cell| cell.col_span() == 1) {
-            let mut w = device.measure_cell_width(cell.inner());
+            let mut w = device.measure_cell_width(&DrawCell::new(cell.inner(), &*table));
             if device.params().px_size.is_some() {
                 if let Some(region) = table.heading_region(cell.coord) {
                     let wr = &heading_widths[region];
@@ -395,7 +420,7 @@ impl Page {
         for cell in table.cells().filter(|cell| cell.col_span() > 1) {
             let rect = cell.rect();
 
-            let w = device.measure_cell_width(cell.inner());
+            let w = device.measure_cell_width(&DrawCell::new(cell.inner(), &*table));
             for i in 0..2 {
                 distribute_spanned_width(
                     w[i],
@@ -458,7 +483,7 @@ impl Page {
             let rect = cell.rect();
 
             let w = joined_width(&cp_x, rect[X].clone());
-            let h = device.measure_cell_height(cell.inner(), w);
+            let h = device.measure_cell_height(&DrawCell::new(cell.inner(), &*table), w);
 
             let row = &mut unspanned_rows[cell.coord.y()];
             if h > *row {
@@ -471,7 +496,7 @@ impl Page {
         for cell in table.cells().filter(|cell| cell.row_span() > 1) {
             let rect = cell.rect();
             let w = joined_width(&cp_x, rect[X].clone());
-            let h = device.measure_cell_height(cell.inner(), w);
+            let h = device.measure_cell_height(&DrawCell::new(cell.inner(), &*table), w);
             distribute_spanned_width(
                 h,
                 &unspanned_rows[rect[Y].clone()],
@@ -499,7 +524,6 @@ impl Page {
         let r = Rect2::new(h[X]..n[X], h[Y]..n[Y]);
         let maps = Self::new_mappings(h, &r);
         Self {
-            device,
             table,
             n,
             h,
@@ -799,7 +823,6 @@ impl Page {
 
         let maps = Self::new_mappings(h, &r);
         Arc::new(Self {
-            device: self.device.clone(),
             table: self.table.clone(),
             n,
             h,
@@ -815,23 +838,23 @@ impl Page {
         self.cp[axis].last().copied().unwrap()
     }
 
-    fn draw(&self, draw: &mut dyn Draw, ofs: Coord2) {
+    fn draw(&self, device: &mut dyn Device, ofs: Coord2) {
         use Axis2::*;
         self.draw_cells(
+            device,
             ofs,
-            draw,
             Rect2::new(0..self.n[X] * 2 + 1, 0..self.n[Y] * 2 + 1),
         );
     }
 
-    fn draw_cells(&self, ofs: Coord2, draw: &mut dyn Draw, cells: Rect2) {
+    fn draw_cells(&self, device: &mut dyn Device, ofs: Coord2, cells: Rect2) {
         use Axis2::*;
         for y in cells[Y].clone() {
             let mut x = cells[X].start;
             while x < cells[X].end {
                 if !is_rule(x) && !is_rule(y) {
                     let cell = self.get_cell(Coord2::new(x, y));
-                    self.draw_cell(ofs, draw, &cell);
+                    self.draw_cell(device, ofs, &cell);
                     x = rule_ofs(cell.rect[X].end);
                 } else {
                     x += 1;
@@ -839,17 +862,16 @@ impl Page {
             }
         }
 
-        /*
-        for y in cells[Y] {
-            for x in cells[X] {
+        for y in cells[Y].clone() {
+            for x in cells[X].clone() {
                 if is_rule(x) && is_rule(y) {
-                    self.draw_rule(ofs, draw, Coord2::new(x, y));
+                    self.draw_rule(device, ofs, Coord2::new(x, y));
                 }
             }
-        }*/
+        }
     }
 
-    fn draw_rule(&self, ofs: Coord2, draw: &mut dyn Draw, coord: Coord2) {
+    fn draw_rule(&self, device: &mut dyn Device, ofs: Coord2, coord: Coord2) {
         const NO_BORDER: BorderStyle = BorderStyle::none();
         let styles = EnumMap::from_fn(|a: Axis2| {
             let b = !a;
@@ -886,7 +908,7 @@ impl Page {
         {
             let bb =
                 Rect2::from_fn(|a| self.cp[a][coord[a]]..self.cp[a][coord[a] + 1]).translate(ofs);
-            draw.draw_line(bb, styles);
+            device.draw_line(bb, styles);
         }
     }
 
@@ -905,31 +927,14 @@ impl Page {
         }
     }
 
-    fn extra_height(&self, bb: &Rect2, inner: &CellInner) -> usize {
+    fn extra_height(&self, device: &dyn Device, bb: &Rect2, cell: &DrawCell) -> usize {
         use Axis2::*;
-        let height = self.device.measure_cell_height(inner, bb[X].len());
+        let height = device.measure_cell_height(cell, bb[X].len());
         usize::saturating_sub(bb[Y].len(), height)
     }
-    fn draw_cell(&self, ofs: Coord2, draw: &mut dyn Draw, cell: &RenderCell) {
+    fn draw_cell(&self, device: &mut dyn Device, ofs: Coord2, cell: &RenderCell) {
         use Axis2::*;
 
-        let inner = cell.content.inner();
-        let (style, subscripts, footnotes) = if let Some(styling) = inner.value.styling.as_ref() {
-            (
-                &styling.style,
-                styling.subscripts.as_slice(),
-                styling.footnotes.as_slice(),
-            )
-        } else {
-            (&self.table.areas[inner.area], [].as_slice(), [].as_slice())
-        };
-        let draw_cell = DrawCell {
-            rotate: inner.rotate,
-            inner: &inner.value.inner,
-            style,
-            subscripts,
-            footnotes,
-        };
         let mut bb = Rect2::from_fn(|a| {
             self.cp[a][cell.rect[a].start * 2 + 1]..self.cp[a][cell.rect[a].end * 2]
         })
@@ -964,17 +969,17 @@ impl Page {
             bb.clone()
         };
 
-        let valign_offset = match style.cell_style.vert_align {
-            VertAlign::Top => 0,
-            VertAlign::Middle => self.extra_height(&bb, inner) / 2,
-            VertAlign::Bottom => self.extra_height(&bb, inner),
-        };
-
         // Header rows are never alternate rows.
         let alternate_row =
             usize::checked_sub(cell.rect[Y].start, self.h[Y]).is_some_and(|row| row % 2 == 1);
 
-        draw.draw_cell(&draw_cell, alternate_row, &bb, valign_offset, spill, &clip)
+        let draw_cell = DrawCell::new(cell.content.inner(), &*self.table);
+        let valign_offset = match draw_cell.style.cell_style.vert_align {
+            VertAlign::Top => 0,
+            VertAlign::Middle => self.extra_height(device, &bb, &draw_cell) / 2,
+            VertAlign::Bottom => self.extra_height(device, &bb, &draw_cell),
+        };
+        device.draw_cell(&draw_cell, alternate_row, &bb, valign_offset, spill, &clip)
     }
 }
 
@@ -1097,7 +1102,7 @@ fn distribute_spanned_width(
 
 /// Returns the width of the rule in `table` that is at offset `z` along axis
 /// `a`, if rendered on `device`.
-fn measure_rule(table: &Table, device: &dyn Device, a: Axis2, z: usize) -> usize {
+fn measure_rule(device: &dyn Device, table: &Table, a: Axis2, z: usize) -> usize {
     let b = !a;
 
     // Determine the types of rules that are present.
@@ -1213,29 +1218,25 @@ impl Break {
     /// completely broken up, or if `size` is too small to reasonably render any
     /// cells.  The latter will never happen if `size` is at least as large as
     /// the page size passed to [Page::new] along the axis using for breaking.
-    fn next(&mut self, size: usize) -> Option<Arc<Page>> {
+    fn next(&mut self, device: &dyn Device, size: usize) -> Option<Arc<Page>> {
         if !self.has_next() {
             return None;
         }
 
-        self.find_breakpoint(size).map(|(z, pixel)| match pixel {
-            0 => self.page.select(self.axis, self.z..z, self.pixel, 0),
-            pixel => self.page.select(
-                self.axis,
-                self.z..z + 1,
-                pixel,
-                self.page.cell_width(self.axis, z) - pixel,
-            ),
-        })
-    }
-
-    /// Returns a small but visible width.
-    fn em(&self) -> usize {
-        self.page.device.params().font_size[Axis2::X]
+        self.find_breakpoint(device, size)
+            .map(|(z, pixel)| match pixel {
+                0 => self.page.select(self.axis, self.z..z, self.pixel, 0),
+                pixel => self.page.select(
+                    self.axis,
+                    self.z..z + 1,
+                    pixel,
+                    self.page.cell_width(self.axis, z) - pixel,
+                ),
+            })
     }
 
-    fn break_cell(&self, z: usize, overflow: usize) -> usize {
-        if self.cell_is_breakable(z) {
+    fn break_cell(&self, device: &dyn Device, z: usize, overflow: usize) -> usize {
+        if self.cell_is_breakable(device, z) {
             // If there is no right header and we render a partial cell
             // on the right side of the body, then we omit the rightmost
             // rule of the body.  Otherwise the rendering is deceptive
@@ -1270,7 +1271,7 @@ impl Break {
             // If there would be only a tiny amount of the cell left
             // after rendering it partially, reduce the amount rendered
             // slightly to make the output look a little better.
-            let em = self.em();
+            let em = device.params().em();
             if pixel + em > cell_size {
                 pixel = pixel.saturating_sub(em);
             }
@@ -1280,11 +1281,11 @@ impl Break {
             // the exact number of pixels available, which might look
             // bad e.g. because it breaks in the middle of a line of
             // text.
-            if self.axis == Axis2::Y && self.page.device.params().can_adjust_break {
+            if self.axis == Axis2::Y && device.params().can_adjust_break {
                 let mut x = 0;
                 while x < self.page.n[Axis2::X] {
                     let cell = self.page.get_cell(Coord2::new(x, z));
-                    let better_pixel = self.page.device.adjust_break(
+                    let better_pixel = device.adjust_break(
                         cell.content,
                         Coord2::new(
                             self.page
@@ -1313,11 +1314,11 @@ impl Break {
         }
     }
 
-    fn find_breakpoint(&mut self, size: usize) -> Option<(usize, usize)> {
+    fn find_breakpoint(&mut self, device: &dyn Device, size: usize) -> Option<(usize, usize)> {
         for z in self.z..self.page.n[self.axis] {
             let needed = self.needed_size(z + 1);
             if needed > size {
-                let pixel = self.break_cell(z, needed - size);
+                let pixel = self.break_cell(device, z, needed - size);
                 if z == self.z && pixel == 0 {
                     return None;
                 } else {
@@ -1333,13 +1334,12 @@ impl Break {
     ///
     /// This is just a heuristic.  Breaking cells across page boundaries can
     /// save space, but it looks ugly.
-    fn cell_is_breakable(&self, cell: usize) -> bool {
-        self.page.cell_width(self.axis, cell) >= self.page.device.params().min_break[self.axis]
+    fn cell_is_breakable(&self, device: &dyn Device, cell: usize) -> bool {
+        self.page.cell_width(self.axis, cell) >= device.params().min_break[self.axis]
     }
 }
 
 pub struct Pager {
-    device: Arc<dyn Device>,
     scale: f64,
 
     /// [Page]s to be rendered, in order, vertically.  There may be up to 5
@@ -1353,7 +1353,7 @@ pub struct Pager {
 
 impl Pager {
     pub fn new(
-        device: Arc<dyn Device>,
+        device: &dyn Device,
         pivot_table: &PivotTable,
         layer_indexes: Option<&[usize]>,
     ) -> Self {
@@ -1364,7 +1364,7 @@ impl Pager {
 
         // Figure out the width of the body of the table. Use this to determine
         // the base scale.
-        let body_page = Page::new(Arc::new(output.body), device.clone(), 0, &pivot_table.look);
+        let body_page = Page::new(Arc::new(output.body), device, 0, &pivot_table.look);
         let body_width = body_page.width(Axis2::X);
         let mut scale = if body_width > device.params().size[Axis2::X]
             && pivot_table.look.shrink_to_fit[Axis2::X]
@@ -1379,7 +1379,7 @@ impl Pager {
         for table in [output.title, output.layers].into_iter().flatten() {
             pages.push(Arc::new(Page::new(
                 Arc::new(table),
-                device.clone(),
+                device,
                 body_width,
                 &pivot_table.look,
             )));
@@ -1388,7 +1388,7 @@ impl Pager {
         for table in [output.caption, output.footnotes].into_iter().flatten() {
             pages.push(Arc::new(Page::new(
                 Arc::new(table),
-                device.clone(),
+                device,
                 0,
                 &pivot_table.look,
             )));
@@ -1416,7 +1416,6 @@ impl Pager {
         }
 
         Self {
-            device,
             scale,
             pages,
             x_break: None,
@@ -1425,7 +1424,7 @@ impl Pager {
     }
 
     /// True if there's content left to rnder.
-    pub fn has_next(&mut self) -> bool {
+    pub fn has_next(&mut self, device: &dyn Device) -> bool {
         while self
             .y_break
             .as_mut()
@@ -1436,7 +1435,8 @@ impl Pager {
                 .as_mut()
                 .and_then(|x_break| {
                     x_break.next(
-                        (self.device.params().size[Axis2::X] as f64 / self.scale as f64) as usize,
+                        device,
+                        (device.params().size[Axis2::X] as f64 / self.scale as f64) as usize,
                     )
                 })
                 .map(|page| Break::new(page, Axis2::Y));
@@ -1455,27 +1455,27 @@ impl Pager {
     /// Returns the amount of vertical space actually used by the rendered
     /// chunk, which will be 0 if `space` is too small to render anything or if
     /// no content remains (use [Self::has_next] to distinguish these cases).
-    pub fn draw_next(&mut self, mut space: usize, draw: &mut dyn Draw) -> usize {
+    pub fn draw_next(&mut self, device: &mut dyn Device, mut space: usize) -> usize {
         use Axis2::*;
 
         if self.scale != 1.0 {
-            draw.scale(self.scale);
+            device.scale(self.scale);
             space = (space as f64 / self.scale) as usize;
         }
 
         let mut ofs = Coord2::new(0, 0);
         let mut n_pages = None;
-        while self.has_next() && n_pages == Some(self.pages.len()) {
+        while self.has_next(device) && n_pages == Some(self.pages.len()) {
             n_pages = Some(self.pages.len());
 
             let Some(page) = self
                 .y_break
                 .as_mut()
-                .and_then(|y_break| y_break.next(space - ofs[Y]))
+                .and_then(|y_break| y_break.next(device, space - ofs[Y]))
             else {
                 break;
             };
-            page.draw(draw, ofs);
+            page.draw(device, ofs);
             ofs[Y] += page.total_size(Y);
         }
 
index 256dc8bdb3ce3e95fb3efbb60da4f7d2ca8969d2..50e4eaa9d6f678ce88fefe9d91ad1ba1b242ee60 100644 (file)
@@ -2,15 +2,20 @@ use std::{
     borrow::Cow,
     fs::File,
     io::{BufWriter, Write},
+    ops::Range,
     sync::{Arc, LazyLock},
 };
 
 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, Draw, DrawCell, Pager, Params},
+    render::{Device, DrawCell, Pager, Params},
     table::{CellInner, Content},
     text_line::TextLine,
     Details, Item,
@@ -252,16 +257,176 @@ impl TextDriver {
 
     fn output_table(&mut self, table: &PivotTable) {
         for layer_indexes in table.layers(true) {
-            let pager = Pager::new(todo!(), table, Some(layer_indexes.as_slice()));
-            while pager.has_next() {
+            let mut pager = Pager::new(self, table, Some(layer_indexes.as_slice()));
+            while pager.has_next(self) {
                 if self.n_objects > 0 {
                     writeln!(&mut self.file).unwrap();
                 }
                 self.n_objects += 1;
 
-                pager.draw_next(usize::MAX, self);
+                pager.draw_next(self, usize::MAX);
+            }
+        }
+    }
+
+    fn layout_cell(&self, cell: &DrawCell, mut text: &str, bb: Rect2) -> Coord2 {
+        if text.is_empty() {
+            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!()
+        }
+             */
+        todo!()
+    }
+}
+
+struct LineBreaks<'a, B>
+where
+    B: Iterator<Item = (usize, BreakOpportunity)> + Clone + 'a,
+{
+    text: &'a str,
+    max_width: usize,
+    indexes: Range<usize>,
+    width: usize,
+    saved: Option<(usize, BreakOpportunity)>,
+    breaks: B,
+}
+
+impl<'a, B> Iterator for LineBreaks<'a, B>
+where
+    B: Iterator<Item = (usize, BreakOpportunity)> + Clone + 'a,
+{
+    type Item = &'a str;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        while let Some((postindex, opportunity)) = self.saved.take().or_else(|| self.breaks.next())
+        {
+            let index = if postindex != self.text.len() {
+                self.text[..postindex].char_indices().next_back().unwrap().0
+            } else {
+                postindex
+            };
+            println!("index={index} {:?}", &self.text[index..]);
+            if index <= self.indexes.end {
+                dbg!();
+                continue;
+            }
+
+            let segment_width = self.text[self.indexes.end..index].width();
+            if self.width == 0 || self.width + segment_width <= self.max_width {
+                dbg!();
+                // Add this segment to the current line.
+                self.width += segment_width;
+                self.indexes.end = index;
+
+                // If this was a new-line, we're done.
+                if opportunity == BreakOpportunity::Mandatory {
+                    dbg!();
+                    let segment = self.text[self.indexes.clone()].trim_end_matches('\n');
+                    self.indexes = postindex..postindex;
+                    self.width = 0;
+                    return Some(segment);
+                }
+            } else {
+                // Won't fit. Return what we've got and save this segment for next time.
+                //
+                // We trim trailing spaces from the line we return, and leading
+                // spaces from the position where we resume.
+                dbg!();
+                let segment = self.text[self.indexes.clone()].trim_end();
+
+                let start = self.text[self.indexes.end..].trim_start_matches([' ', '\t']);
+                let start_index = self.text.len() - start.len();
+                self.indexes = start_index..start_index;
+                self.width = 0;
+                self.saved = Some((postindex, opportunity));
+                return Some(segment);
             }
         }
+        None
+    }
+}
+
+fn new_line_breaks<'a>(
+    text: &'a str,
+    width: usize,
+) -> LineBreaks<'a, impl Iterator<Item = (usize, BreakOpportunity)> + Clone + 'a> {
+    LineBreaks {
+        text,
+        max_width: width,
+        indexes: 0..0,
+        width: 0,
+        saved: None,
+        breaks: linebreaks(text),
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
+
+    use crate::output::text::new_line_breaks;
+
+    #[test]
+    fn unicode_width() {
+        // `\n` is a control character, so [UnicodeWidthChar] considers it to
+        // have no width.
+        assert_eq!('\n'.width(), None);
+
+        // But [UnicodeWidthStr] has a different idea.
+        assert_eq!("\n".width(), 1);
+        assert_eq!("\r\n".width(), 1);
+    }
+
+    #[track_caller]
+    fn test_line_breaks(input: &str, width: usize, expected: Vec<&str>) {
+        let actual = new_line_breaks(input, width).collect::<Vec<_>>();
+        if expected != actual {
+            panic!("filling {input:?} to {width} columns:\nexpected: {expected:?}\nactual:   {actual:?}");
+        }
+    }
+    #[test]
+    fn line_breaks() {
+        for width in 0..=6 {
+            test_line_breaks("abc def ghi", width, vec!["abc", "def", "ghi"]);
+        }
+        for width in 7..=10 {
+            test_line_breaks("abc def ghi", width, vec!["abc def", "ghi"]);
+        }
+        test_line_breaks("abc def ghi", 11, vec!["abc def ghi"]);
+
+        for width in 0..=6 {
+            test_line_breaks("abc  def ghi", width, vec!["abc", "def", "ghi"]);
+        }
+        test_line_breaks("abc  def ghi", 7, vec!["abc", "def ghi"]);
+        for width in 8..=11 {
+            test_line_breaks("abc  def ghi", width, vec!["abc  def", "ghi"]);
+        }
+        test_line_breaks("abc  def ghi", 12, vec!["abc  def ghi"]);
+
+        test_line_breaks("abc\ndef\nghi", 2, vec!["abc", "def", "ghi"]);
     }
 }
 
@@ -283,7 +448,32 @@ impl Driver for TextDriver {
     }
 }
 
-impl Draw for TextDriver {
+impl Device for TextDriver {
+    fn params(&self) -> &Params {
+        &self.params
+    }
+
+    fn measure_cell_width(&self, cell: &DrawCell) -> [usize; 2] {
+        let text = cell
+            .inner
+            .display(() /* XXX */)
+            .with_font_style(&cell.style.font_style)
+            .with_subscripts(cell.subscripts)
+            .with_footnotes(cell.footnotes)
+            .to_string();
+        let max_width = self.layout_cell(cell, &text, Rect2::new(0..usize::MAX, 0..usize::MAX));
+        let min_width = self.layout_cell(cell, &text, Rect2::new(0..1, 0..usize::MAX));
+        [min_width.x(), max_width.x()]
+    }
+
+    fn measure_cell_height(&self, cell: &DrawCell, width: usize) -> usize {
+        todo!()
+    }
+
+    fn adjust_break(&self, cell: &Content, size: Coord2) -> usize {
+        unreachable!()
+    }
+
     fn draw_line(&mut self, bb: Rect2, styles: EnumMap<Axis2, [BorderStyle; 2]>) {
         todo!()
     }
@@ -301,24 +491,6 @@ impl Draw for TextDriver {
     }
 
     fn scale(&mut self, factor: f64) {
-        unreachable!()
-    }
-}
-
-impl Device for TextDriver {
-    fn params(&self) -> &Params {
-        todo!()
-    }
-
-    fn measure_cell_width(&self, cell: &CellInner) -> [usize; 2] {
-        todo!()
-    }
-
-    fn measure_cell_height(&self, cell: &CellInner, width: usize) -> usize {
-        todo!()
-    }
-
-    fn adjust_break(&self, cell: &Content, size: Coord2) -> usize {
         todo!()
     }
 }