distinguish cells from plane locations
authorBen Pfaff <blp@cs.stanford.edu>
Tue, 2 Dec 2025 02:21:25 +0000 (18:21 -0800)
committerBen Pfaff <blp@cs.stanford.edu>
Tue, 2 Dec 2025 02:21:25 +0000 (18:21 -0800)
rust/pspp/src/output/drivers/csv.rs
rust/pspp/src/output/drivers/html.rs
rust/pspp/src/output/pivot.rs
rust/pspp/src/output/pivot/output.rs
rust/pspp/src/output/render.rs
rust/pspp/src/output/table.rs

index d0963e37ffc66751d0d5468f78089f16f9e632c5..f756b36b2bffaf38fe9900f8faa220d46b133031 100644 (file)
@@ -31,7 +31,11 @@ use crate::{
     data::{ByteString, Case, Datum, WithEncoding},
     dictionary::Dictionary,
     format::{DisplayPlain, Type},
-    output::{Item, drivers::Driver, pivot::Coord2},
+    output::{
+        Item,
+        drivers::Driver,
+        pivot::{ Index2},
+    },
     util::ToSmallString as _,
     variable::Variable,
 };
@@ -332,7 +336,7 @@ impl CsvDriver {
                     write!(&mut self.file, "{}", self.options.delimiter)?;
                 }
 
-                let coord = Coord2::new(x, y);
+                let coord = Index2::new(x, y);
                 let content = table.get(coord);
                 if content.is_top_left() {
                     let display = content.inner().value.display(pivot_table);
index a4c797a8cc09238fe214d279c56e781c795cd734..4ea1a8e6382aa0c00cbfdea5428ade1e0a9d6bd1 100644 (file)
@@ -27,10 +27,9 @@ use serde::{Deserialize, Serialize};
 use smallstr::SmallString;
 
 use crate::output::{
-    Details, Item,
-    drivers::Driver,
-    pivot::{Axis2, BorderStyle, Color, Coord2, HorzAlign, PivotTable, Rect2, Stroke, VertAlign},
-    table::{DrawCell, Table},
+    drivers::Driver, pivot::{
+        Axis2, BorderStyle, Color, HorzAlign, Index2, IndexRect2, PivotTable, Stroke, VertAlign
+    }, table::{DrawCell, Table}, Details, Item
 };
 
 #[derive(Clone, Debug, Deserialize, Serialize)]
@@ -86,10 +85,10 @@ where
             writeln!(&mut self.writer, ">")?;
 
             if let Some(title) = output.title {
-                let cell = title.get(Coord2::new(0, 0));
+                let cell = title.get(Index2::new(0, 0));
                 self.put_cell(
                     DrawCell::new(cell.inner(), &title),
-                    Rect2::new(0..1, 0..1),
+                    IndexRect2::new(0..1, 0..1),
                     "caption",
                     None,
                 )?;
@@ -101,7 +100,7 @@ where
                     writeln!(&mut self.writer, "<tr>")?;
                     self.put_cell(
                         DrawCell::new(cell.inner(), &layers),
-                        Rect2::new(0..output.body.n[Axis2::X], 0..1),
+                        IndexRect2::new(0..output.body.n[Axis2::X], 0..1),
                         "td",
                         None,
                     )?;
@@ -114,7 +113,7 @@ where
             for y in 0..output.body.n.y() {
                 writeln!(&mut self.writer, "<tr>")?;
                 for x in output.body.iter_x(y) {
-                    let cell = output.body.get(Coord2::new(x, y));
+                    let cell = output.body.get(Index2::new(x, y));
                     if cell.is_top_left() {
                         let is_header = x < output.body.h[Axis2::X] || y < output.body.h[Axis2::Y];
                         let tag = if is_header { "th" } else { "td" };
@@ -135,8 +134,8 @@ where
                 writeln!(&mut self.writer, "<tr>")?;
                 if let Some(caption) = output.caption {
                     self.put_cell(
-                        DrawCell::new(caption.get(Coord2::new(0, 0)).inner(), &caption),
-                        Rect2::new(0..output.body.n[Axis2::X], 0..1),
+                        DrawCell::new(caption.get(Index2::new(0, 0)).inner(), &caption),
+                        IndexRect2::new(0..output.body.n[Axis2::X], 0..1),
                         "td",
                         None,
                     )?;
@@ -148,7 +147,7 @@ where
                         writeln!(&mut self.writer, "<tr>")?;
                         self.put_cell(
                             DrawCell::new(cell.inner(), &footnotes),
-                            Rect2::new(0..output.body.n[Axis2::X], 0..1),
+                            IndexRect2::new(0..output.body.n[Axis2::X], 0..1),
                             "td",
                             None,
                         )?;
@@ -164,7 +163,7 @@ where
     fn put_cell(
         &mut self,
         cell: DrawCell<'_, '_>,
-        rect: Rect2,
+        rect: IndexRect2,
         tag: &str,
         table: Option<&Table>,
     ) -> std::io::Result<()> {
@@ -237,7 +236,7 @@ where
                     &mut style,
                     table.get_rule(
                         Axis2::X,
-                        Coord2::new(rect[Axis2::X].end, rect[Axis2::Y].start),
+                        Index2::new(rect[Axis2::X].end, rect[Axis2::Y].start),
                     ),
                     "right",
                 );
@@ -247,7 +246,7 @@ where
                     &mut style,
                     table.get_rule(
                         Axis2::Y,
-                        Coord2::new(rect[Axis2::X].start, rect[Axis2::Y].end),
+                        Index2::new(rect[Axis2::X].start, rect[Axis2::Y].end),
                     ),
                     "bottom",
                 );
index 6da0d028d1782daef59114a89300a3da3f5abb77..6e8c90960a44bfd43774cd6dd6bde239d811a8a7 100644 (file)
@@ -1606,6 +1606,65 @@ impl TryFrom<Axis3> for Axis2 {
     }
 }
 
+#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
+pub struct Index2(pub EnumMap<Axis2, usize>);
+
+impl Index2 {
+    pub fn new(x: usize, y: usize) -> Self {
+        use Axis2::*;
+        Self(enum_map! {
+            X => x,
+            Y => y
+        })
+    }
+
+    pub fn for_axis((a, az): (Axis2, usize), bz: usize) -> Self {
+        let mut coord = Self::default();
+        coord[a] = az;
+        coord[!a] = bz;
+        coord
+    }
+
+    pub fn from_fn<F>(f: F) -> Self
+    where
+        F: FnMut(Axis2) -> usize,
+    {
+        Self(EnumMap::from_fn(f))
+    }
+
+    pub fn x(&self) -> usize {
+        self.0[Axis2::X]
+    }
+
+    pub fn y(&self) -> usize {
+        self.0[Axis2::Y]
+    }
+
+    pub fn get(&self, axis: Axis2) -> usize {
+        self.0[axis]
+    }
+}
+
+impl From<EnumMap<Axis2, usize>> for Index2 {
+    fn from(value: EnumMap<Axis2, usize>) -> Self {
+        Self(value)
+    }
+}
+
+impl Index<Axis2> for Index2 {
+    type Output = usize;
+
+    fn index(&self, index: Axis2) -> &Self::Output {
+        &self.0[index]
+    }
+}
+
+impl IndexMut<Axis2> for Index2 {
+    fn index_mut(&mut self, index: Axis2) -> &mut Self::Output {
+        &mut self.0[index]
+    }
+}
+
 /// A 2-dimensional `(x,y)` pair.
 #[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
 pub struct Coord2(pub EnumMap<Axis2, usize>);
@@ -1724,6 +1783,64 @@ impl IndexMut<Axis2> for Rect2 {
     }
 }
 
+#[derive(Clone, Debug, Default)]
+pub struct IndexRect2(pub EnumMap<Axis2, Range<usize>>);
+
+impl IndexRect2 {
+    pub fn new(x_range: Range<usize>, y_range: Range<usize>) -> Self {
+        Self(enum_map! {
+            Axis2::X => x_range.clone(),
+            Axis2::Y => y_range.clone(),
+        })
+    }
+    pub fn for_cell(cell: Index2) -> Self {
+        Self::new(cell.x()..cell.x() + 1, cell.y()..cell.y() + 1)
+    }
+    pub fn for_ranges((a, a_range): (Axis2, Range<usize>), b_range: Range<usize>) -> Self {
+        let b = !a;
+        let mut ranges = EnumMap::default();
+        ranges[a] = a_range;
+        ranges[b] = b_range;
+        Self(ranges)
+    }
+    pub fn top_left(&self) -> Index2 {
+        use Axis2::*;
+        Index2::new(self[X].start, self[Y].start)
+    }
+    pub fn from_fn<F>(f: F) -> Self
+    where
+        F: FnMut(Axis2) -> Range<usize>,
+    {
+        Self(EnumMap::from_fn(f))
+    }
+    pub fn translate(self, offset: Index2) -> IndexRect2 {
+        Self::from_fn(|axis| self[axis].start + offset[axis]..self[axis].end + offset[axis])
+    }
+    pub fn is_empty(&self) -> bool {
+        self[Axis2::X].is_empty() || self[Axis2::Y].is_empty()
+    }
+}
+
+impl From<EnumMap<Axis2, Range<usize>>> for IndexRect2 {
+    fn from(value: EnumMap<Axis2, Range<usize>>) -> Self {
+        Self(value)
+    }
+}
+
+impl Index<Axis2> for IndexRect2 {
+    type Output = Range<usize>;
+
+    fn index(&self, index: Axis2) -> &Self::Output {
+        &self.0[index]
+    }
+}
+
+impl IndexMut<Axis2> for IndexRect2 {
+    fn index_mut(&mut self, index: Axis2) -> &mut Self::Output {
+        &mut self.0[index]
+    }
+}
+
 #[derive(Copy, Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
 #[serde(rename_all = "camelCase")]
 pub enum FootnoteMarkerType {
index a2427b0d35dffee5985f8a5539bbd1065f085c9e..b19fa9ae927e586144db1a2bc04edd129441226f 100644 (file)
@@ -20,13 +20,13 @@ use enum_map::{EnumMap, enum_map};
 use itertools::Itertools;
 
 use crate::output::{
-    pivot::{HeadingRegion, LabelPosition, Path, RowParity},
+    pivot::{HeadingRegion, Index2, IndexRect2, LabelPosition, Path, RowParity},
     table::{CellInner, Table},
 };
 
 use super::{
-    Area, Axis2, Axis3, Border, BorderStyle, BoxBorder, Color, Coord2, Dimension, Footnote,
-    IntoValueOptions, PivotTable, Rect2, RowColBorder, Stroke, Value,
+    Area, Axis2, Axis3, Border, BorderStyle, BoxBorder, Color, Dimension, Footnote,
+    IntoValueOptions, PivotTable, RowColBorder, Stroke, Value,
 };
 
 /// All of the combinations of dimensions along an axis.
@@ -139,15 +139,15 @@ impl PivotTable {
         I: Iterator<Item = Box<Value>> + ExactSizeIterator,
     {
         let mut table = Table::new(
-            Coord2::new(1, rows.len()),
-            Coord2::new(0, 0),
+            Index2::new(1, rows.len()),
+            Index2::new(0, 0),
             self.style.look.areas.clone(),
             self.borders(false),
             self.into_value_options(),
         );
         for (y, row) in rows.enumerate() {
             table.put(
-                Rect2::for_cell(Coord2::new(0, y)),
+                IndexRect2::for_cell(Index2::new(0, y)),
                 CellInner::new(area, row),
             );
         }
@@ -178,13 +178,13 @@ impl PivotTable {
     pub fn output_body(&self, layer_indexes: &[usize], printing: bool) -> Table {
         let headings = EnumMap::from_fn(|axis| Headings::new(self, axis, layer_indexes));
 
-        let data = Coord2::from_fn(|axis| headings[axis].width());
-        let mut stub = Coord2::from_fn(|axis| headings[!axis].height());
+        let data = Index2::from_fn(|axis| headings[axis].width());
+        let mut stub = Index2::from_fn(|axis| headings[!axis].height());
         if headings[Axis2::Y].row_label_position == LabelPosition::Corner && stub.y() == 0 {
             stub[Axis2::Y] = 1;
         }
         let mut body = Table::new(
-            Coord2::from_fn(|axis| data[axis] + stub[axis]),
+            Index2::from_fn(|axis| data[axis] + stub[axis]),
             stub,
             self.style.look.areas.clone(),
             self.borders(printing),
@@ -205,7 +205,7 @@ impl PivotTable {
                 let data_indexes = self.convert_indexes_ptod(presentation_indexes);
                 let value = self.get(&*data_indexes);
                 body.put(
-                    Rect2::new(x..x + 1, y..y + 1),
+                    IndexRect2::new(x..x + 1, y..y + 1),
                     CellInner::new(
                         Area::Data(RowParity::from(y - stub[Axis2::Y])),
                         Box::new(value.cloned().unwrap_or_default()),
@@ -222,7 +222,7 @@ impl PivotTable {
             && stub.y() > 0
         {
             body.put(
-                Rect2::new(0..stub.x(), 0..stub.y()),
+                IndexRect2::new(0..stub.x(), 0..stub.y()),
                 CellInner::new(
                     Area::Corner,
                     self.metadata.corner_text.clone().unwrap_or_default(),
@@ -466,7 +466,7 @@ impl<'a> Heading<'a> {
                 let rotate =
                     (rotate_inner_labels && is_inner_row) || (rotate_outer_labels && is_outer_row);
                 table.put(
-                    Rect2::for_ranges((h, x1 + h_ofs..x2 + h_ofs), y1..y2),
+                    IndexRect2::for_ranges((h, x1 + h_ofs..x2 + h_ofs), y1..y2),
                     CellInner::new(Area::Labels(h), Box::new(name.clone())).with_rotate(rotate),
                 );
 
@@ -546,7 +546,7 @@ impl<'a> Heading<'a> {
 
         if dimension_label_position == LabelPosition::Corner {
             table.put(
-                Rect2::new(v_ofs..v_ofs + 1, 0..h_ofs),
+                IndexRect2::new(v_ofs..v_ofs + 1, 0..h_ofs),
                 CellInner::new(Area::Corner, self.dimension.root.name.clone()),
             );
         }
index 9b4d71cc5799e65751f12e955298a083baa717dc..14a5959621054d7627ecabcbe24ef534a3ac6543 100644 (file)
@@ -25,7 +25,7 @@ use itertools::interleave;
 use num::Integer;
 use smallvec::SmallVec;
 
-use crate::output::pivot::VertAlign;
+use crate::output::pivot::{Index2, IndexRect2, VertAlign};
 use crate::output::table::DrawCell;
 
 use super::pivot::{Axis2, BorderStyle, Coord2, Look, PivotTable, Rect2, Stroke};
@@ -187,13 +187,13 @@ struct Page {
 
     /// Size of the table in cells.
     ///
-    n: Coord2,
+    n: Index2,
 
     /// Header size.  Cells `0..h[X]` are rendered horizontally, and `0..h[Y]` vertically.
-    h: Coord2,
+    h: Index2,
 
     /// Main region of cells to render.
-    r: Rect2,
+    r: IndexRect2,
 
     /// Mappings from [Page] positions to those in the underlying [Table].
     maps: EnumMap<Axis2, [Map; 2]>,
@@ -293,7 +293,7 @@ struct Page {
     /// ```
     /// Each entry maps from a cell that overflows to the space that has been
     /// trimmed off the cell.
-    overflows: HashMap<Coord2, EnumMap<Axis2, [usize; 2]>>,
+    overflows: HashMap<Index2, EnumMap<Axis2, [usize; 2]>>,
 
     /// If a single column (or row) is too wide (or tall) to fit on a page
     /// reasonably, then [Break::next] will split a single row or column across
@@ -354,7 +354,7 @@ fn is_rule(z: usize) -> bool {
 
 #[derive(Clone)]
 pub struct RenderCell<'a> {
-    rect: Rect2,
+    rect: IndexRect2,
     content: &'a Content,
 }
 
@@ -395,7 +395,7 @@ impl Page {
         for cell in table.cells().filter(|cell| cell.col_span() == 1) {
             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) {
+                if let Some(region) = table.heading_region(cell.pos) {
                     let wr = &heading_widths[region];
                     if w[Min] < wr[Min] {
                         w[Min] = wr[Min];
@@ -411,7 +411,7 @@ impl Page {
                 }
             }
 
-            let x = cell.coord[X];
+            let x = cell.pos[X];
             for ext in [Min, Max] {
                 if unspanned_columns[ext][x] < w[ext] {
                     unspanned_columns[ext][x] = w[ext];
@@ -483,7 +483,7 @@ impl Page {
             let w = joined_width(&cp_x, rect[X].clone());
             let h = device.measure_cell_height(&DrawCell::new(cell.inner(), &table), w);
 
-            let row = &mut unspanned_rows[cell.coord.y()];
+            let row = &mut unspanned_rows[cell.pos.y()];
             if h > *row {
                 *row = h;
             }
@@ -519,7 +519,7 @@ impl Page {
                 h[axis] = 0;
             }
         }
-        let r = Rect2::new(h[X]..n[X], h[Y]..n[Y]);
+        let r = IndexRect2::new(h[X]..n[X], h[Y]..n[Y]);
         let maps = Self::new_mappings(h, &r);
         Self {
             table,
@@ -620,7 +620,7 @@ impl Page {
         *self.cp[axis].last().unwrap()
     }
 
-    fn new_mappings(h: Coord2, r: &Rect2) -> EnumMap<Axis2, [Map; 2]> {
+    fn new_mappings(h: Index2, r: &IndexRect2) -> EnumMap<Axis2, [Map; 2]> {
         EnumMap::from_fn(|axis| {
             [
                 Map {
@@ -651,15 +651,15 @@ impl Page {
         z + self.get_map(axis, z).ofs
     }
 
-    fn map_coord(&self, coord: Coord2) -> Coord2 {
-        Coord2::from_fn(|a| self.map_z(a, coord[a]))
+    fn map_coord(&self, coord: Index2) -> Index2 {
+        Index2::from_fn(|a| self.map_z(a, coord[a]))
     }
 
-    fn get_cell(&self, coord: Coord2) -> RenderCell<'_> {
+    fn get_cell(&self, coord: Index2) -> RenderCell<'_> {
         let maps = EnumMap::from_fn(|axis| self.get_map(axis, coord[axis]));
         let cell = self.table.get(self.map_coord(coord));
         RenderCell {
-            rect: Rect2(cell.rect().0.map(|axis, Range { start, end }| {
+            rect: IndexRect2(cell.rect().0.map(|axis, Range { start, end }| {
                 let m = maps[axis];
                 max(m.p0, start - m.ofs)..min(m.p0 + m.n, end - m.ofs)
             })),
@@ -760,7 +760,7 @@ impl Page {
         if self.h[a] == 0 || z0 > self.h[a] || pixel0 > 0 {
             let mut z = 0;
             while z < self.n[b] {
-                let d = Coord2::for_axis((a, z0), z);
+                let d = Index2::for_axis((a, z0), z);
                 let cell = self.get_cell(d);
                 let overflow0 = pixel0 > 0 || cell.rect[a].start < z0;
                 let overflow1 = cell.rect[a].end > z1 || (cell.rect[a].end == z1 && pixel1 > 0);
@@ -787,7 +787,7 @@ impl Page {
         // Add overflows along the right side.
         let mut z = 0;
         while z < self.n[b] {
-            let d = Coord2::for_axis((a, z1 - 1), z);
+            let d = Index2::for_axis((a, z1 - 1), z);
             let cell = self.get_cell(d);
             if cell.rect[a].end > z1
                 || (cell.rect[a].end == z1 && pixel1 > 0)
@@ -839,17 +839,17 @@ impl Page {
         self.draw_cells(
             device,
             ofs,
-            Rect2::new(0..self.n[X] * 2 + 1, 0..self.n[Y] * 2 + 1),
+            IndexRect2::new(0..self.n[X] * 2 + 1, 0..self.n[Y] * 2 + 1),
         );
     }
 
-    fn draw_cells(&self, device: &mut dyn Device, ofs: Coord2, cells: Rect2) {
+    fn draw_cells(&self, device: &mut dyn Device, ofs: Coord2, cells: IndexRect2) {
         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 / 2, y / 2));
+                    let cell = self.get_cell(Index2::new(x / 2, y / 2));
                     self.draw_cell(device, ofs, &cell);
                     x = rule_ofs(cell.rect[X].end);
                 } else {
@@ -861,13 +861,13 @@ impl Page {
         for y in cells[Y].clone() {
             for x in cells[X].clone() {
                 if is_rule(x) || is_rule(y) {
-                    self.draw_rule(device, ofs, Coord2::new(x, y));
+                    self.draw_rule(device, ofs, Index2::new(x, y));
                 }
             }
         }
     }
 
-    fn draw_rule(&self, device: &mut dyn Device, ofs: Coord2, coord: Coord2) {
+    fn draw_rule(&self, device: &mut dyn Device, ofs: Coord2, coord: Index2) {
         const NO_BORDER: BorderStyle = BorderStyle::none();
         let styles = EnumMap::from_fn(|a: Axis2| {
             let b = !a;
@@ -908,15 +908,15 @@ impl Page {
         }
     }
 
-    fn get_rule(&self, a: Axis2, coord: Coord2) -> BorderStyle {
-        let coord = Coord2::from_fn(|a| coord[a] / 2);
+    fn get_rule(&self, a: Axis2, coord: Index2) -> BorderStyle {
+        let coord = Index2::from_fn(|a| coord[a] / 2);
         let coord = self.map_coord(coord);
 
         let border = self.table.get_rule(a, coord);
         if self.h[a] > 0 && coord[a] == self.h[a] {
             let border2 = self
                 .table
-                .get_rule(a, Coord2::for_axis((a, self.h[a]), coord[!a]));
+                .get_rule(a, Index2::for_axis((a, self.h[a]), coord[!a]));
             border.combine(border2)
         } else {
             border
@@ -982,7 +982,7 @@ struct Selection {
     z1: usize,
     p0: usize,
     p1: usize,
-    h: Coord2,
+    h: Index2,
 }
 
 impl Selection {
@@ -990,11 +990,11 @@ impl Selection {
     ///
     /// `coord` must be in the selected region or the results will not make
     /// sense.
-    fn coord_to_subpage(&self, coord: Coord2) -> Coord2 {
+    fn coord_to_subpage(&self, coord: Index2) -> Index2 {
         let a = self.a;
         let b = self.b;
         let ha0 = self.h[a];
-        Coord2::for_axis((a, max(coord[a] + ha0 - self.z0, ha0)), coord[b])
+        Index2::for_axis((a, max(coord[a] + ha0 - self.z0, ha0)), coord[b])
     }
 }
 
@@ -1105,7 +1105,7 @@ fn measure_rule(device: &dyn Device, table: &Table, a: Axis2, z: usize) -> usize
     // Determine the types of rules that are present.
     let mut rules = EnumMap::default();
     for w in 0..table.n[b] {
-        let stroke = table.get_rule(a, Coord2::for_axis((a, z), w)).stroke;
+        let stroke = table.get_rule(a, Index2::for_axis((a, z), w)).stroke;
         rules[stroke] = true;
     }
 
@@ -1282,7 +1282,7 @@ impl 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 cell = self.page.get_cell(Index2::new(x, z));
                     let better_pixel = device.adjust_break(
                         cell.content,
                         Coord2::new(
index 77c3fa0fa2422fdb4bb3d8bcab40b2a76d17f60e..174d89d7911dbefa8d688c832b7033fa0a765cdc 100644 (file)
@@ -32,17 +32,20 @@ use enum_map::{EnumMap, enum_map};
 use ndarray::{Array, Array2};
 
 use crate::output::{
-    pivot::{CellStyle, Coord2, DisplayValue, FontStyle, Footnote, HorzAlign, ValueInner},
+    pivot::{
+        CellStyle, DisplayValue, FontStyle, Footnote, HorzAlign, Index2, IndexRect2,
+        ValueInner,
+    },
     spv::html,
 };
 
 use super::pivot::{
-    Area, AreaStyle, Axis2, Border, BorderStyle, HeadingRegion, Rect2, Value, ValueOptions,
+    Area, AreaStyle, Axis2, Border, BorderStyle, HeadingRegion, Value, ValueOptions,
 };
 
 #[derive(Clone, Debug)]
 pub struct CellRef<'a> {
-    pub coord: Coord2,
+    pub pos: Index2,
     pub content: &'a Content,
 }
 
@@ -55,16 +58,16 @@ impl CellRef<'_> {
         self.content.is_empty()
     }
 
-    pub fn rect(&self) -> Rect2 {
-        self.content.rect(self.coord)
+    pub fn rect(&self) -> IndexRect2 {
+        self.content.rect(self.pos)
     }
 
     pub fn next_x(&self) -> usize {
-        self.content.next_x(self.coord.x())
+        self.content.next_x(self.pos.x())
     }
 
     pub fn is_top_left(&self) -> bool {
-        self.content.is_top_left(self.coord)
+        self.content.is_top_left(self.pos)
     }
 
     pub fn span(&self, axis: Axis2) -> usize {
@@ -99,7 +102,7 @@ impl Content {
     /// Returns the rectangle that this cell covers, only if the cell contains
     /// that information. (Joined cells always do, and other cells usually
     /// don't.)
-    pub fn joined_rect(&self) -> Option<&Rect2> {
+    pub fn joined_rect(&self) -> Option<&IndexRect2> {
         match self {
             Content::Join(cell) => Some(&cell.region),
             _ => None,
@@ -107,11 +110,11 @@ impl Content {
     }
 
     /// Returns the rectangle that this cell covers. If the cell doesn't contain
-    /// that information, returns a rectangle containing `coord`.
-    pub fn rect(&self, coord: Coord2) -> Rect2 {
+    /// that information, returns a rectangle containing `pos`.
+    pub fn rect(&self, pos: Index2) -> IndexRect2 {
         match self {
             Content::Join(cell) => cell.region.clone(),
-            _ => Rect2::for_cell(coord),
+            _ => IndexRect2::for_cell(pos),
         }
     }
 
@@ -120,8 +123,8 @@ impl Content {
             .map_or(x + 1, |region| region[Axis2::X].end)
     }
 
-    pub fn is_top_left(&self, coord: Coord2) -> bool {
-        self.joined_rect().is_none_or(|r| coord == r.top_left())
+    pub fn is_top_left(&self, pos: Index2) -> bool {
+        self.joined_rect().is_none_or(|r| pos == r.top_left())
     }
 
     pub fn span(&self, axis: Axis2) -> usize {
@@ -150,11 +153,11 @@ pub struct Cell {
     inner: CellInner,
 
     /// Occupied table region.
-    region: Rect2,
+    region: IndexRect2,
 }
 
 impl Cell {
-    fn new(inner: CellInner, region: Rect2) -> Self {
+    fn new(inner: CellInner, region: IndexRect2) -> Self {
         Self { inner, region }
     }
 }
@@ -192,10 +195,10 @@ impl CellInner {
 #[derive(derive_more::Debug)]
 pub struct Table {
     /// Number of rows and columns.
-    pub n: Coord2,
+    pub n: Index2,
 
     /// Table header rows and columns.
-    pub h: Coord2,
+    pub h: Index2,
 
     pub contents: Array2<Content>,
 
@@ -217,8 +220,8 @@ pub struct Table {
 
 impl Table {
     pub fn new(
-        n: Coord2,
-        headers: Coord2,
+        n: Index2,
+        headers: Index2,
         areas: EnumMap<Area, AreaStyle>,
         borders: EnumMap<Border, BorderStyle>,
         value_options: ValueOptions,
@@ -237,18 +240,18 @@ impl Table {
         }
     }
 
-    pub fn get(&self, coord: Coord2) -> CellRef<'_> {
+    pub fn get(&self, coord: Index2) -> CellRef<'_> {
         CellRef {
-            coord,
+            pos: coord,
             content: &self.contents[[coord.x(), coord.y()]],
         }
     }
 
-    pub fn get_rule(&self, axis: Axis2, pos: Coord2) -> BorderStyle {
+    pub fn get_rule(&self, axis: Axis2, pos: Index2) -> BorderStyle {
         self.rules[axis][[pos.x(), pos.y()]].map_or(BorderStyle::none(), |b| self.borders[b])
     }
 
-    pub fn put(&mut self, region: Rect2, inner: CellInner) {
+    pub fn put(&mut self, region: IndexRect2, inner: CellInner) {
         use Axis2::*;
         if region[X].len() == 1 && region[Y].len() == 1 {
             self.contents[[region[X].start, region[Y].start]] = Content::Value(inner);
@@ -295,7 +298,7 @@ impl Table {
     }
 
     /// The heading region that `pos` is part of, if any.
-    pub fn heading_region(&self, pos: Coord2) -> Option<HeadingRegion> {
+    pub fn heading_region(&self, pos: Index2) -> Option<HeadingRegion> {
         if pos.x() < self.h.x() {
             Some(HeadingRegion::Rows)
         } else if pos.y() < self.h.y() {
@@ -330,7 +333,7 @@ impl Iterator for XIter<'_> {
     fn next(&mut self) -> Option<Self::Item> {
         let next_x = self
             .x
-            .map_or(0, |x| self.table.get(Coord2::new(x, self.y)).next_x());
+            .map_or(0, |x| self.table.get(Index2::new(x, self.y)).next_x());
         if next_x >= self.table.n.x() {
             None
         } else {
@@ -353,7 +356,7 @@ impl<'a> Cells<'a> {
             next: if table.is_empty() {
                 None
             } else {
-                Some(table.get(Coord2::new(0, 0)))
+                Some(table.get(Index2::new(0, 0)))
             },
         }
     }
@@ -370,9 +373,9 @@ impl<'a> Iterator for Cells<'a> {
         self.next = loop {
             let next_x = next.next_x();
             let coord = if next_x < self.table.n[X] {
-                Coord2::new(next_x, next.coord.y())
-            } else if next.coord.y() + 1 < self.table.n[Y] {
-                Coord2::new(0, next.coord.y() + 1)
+                Index2::new(next_x, next.pos.y())
+            } else if next.pos.y() + 1 < self.table.n[Y] {
+                Index2::new(0, next.pos.y() + 1)
             } else {
                 break None;
             };