Skip to main content

densa_grid/
grid.rs

1use densa_core::{Color, Point, Rect};
2use densa_draw::Canvas;
3use densa_fmt::FmtBuf;
4use densa_text::{Font, GlyphCache, GlyphMap, SUBPIXEL_STEPS};
5
6use crate::{Align, Column, GridLayout, Palette};
7
8/// Bytes a cell's text may take. [`FmtBuf`] appends are all-or-nothing, so a write that
9/// does not fit leaves the cell as it was (blank if nothing was written yet), not truncated.
10pub const CELL_BYTES: usize = 64;
11
12/// The stack buffer a [`GridSource`] writes one cell's text into.
13pub type CellText = FmtBuf<CELL_BYTES>;
14
15/// How one cell is drawn: `CellStyle::new(color)`, with
16/// [`with_background`](CellStyle::with_background) for a fill.
17#[derive(Debug, Clone, Copy, PartialEq)]
18#[non_exhaustive]
19pub struct CellStyle {
20    /// Text color.
21    pub color: Color,
22    /// Cell fill drawn under the text; `None` for none.
23    pub background: Option<Color>,
24}
25
26impl CellStyle {
27    /// Text in `color`, with no fill.
28    pub const fn new(color: Color) -> Self {
29        Self {
30            color,
31            background: None,
32        }
33    }
34
35    /// The style with the cell filled with `background` (a color, or `None` for no fill).
36    #[must_use]
37    pub fn with_background(mut self, background: impl Into<Option<Color>>) -> Self {
38        self.background = background.into();
39        self
40    }
41}
42
43/// The data a [`GridPainter`] draws: one call per visible cell, every frame.
44///
45/// Implementations format straight into `text` (see `densa_fmt`) so a frame allocates
46/// nothing.
47pub trait GridSource {
48    /// Rows available; the painter draws at most as many as its layout holds.
49    fn rows(&self) -> usize;
50
51    /// Writes the text of the cell at `row`, `column` into `text` (empty on entry) and
52    /// returns its style.
53    ///
54    /// Text over [`CELL_BYTES`] does not fit: write a placeholder instead (for example a
55    /// run of `#`, which the painter shows as overflow) rather than ignoring the error.
56    fn cell(&self, row: usize, column: usize, text: &mut CellText) -> CellStyle;
57}
58
59/// Draws a [`GridSource`] onto a [`Canvas`].
60///
61/// Owns the column layout and the font's character map; glyphs come from the canvas's
62/// shared cache. Painting a frame whose characters have all been drawn (or
63/// [prewarmed](Self::prewarm)) before allocates nothing; a character seen for the first time
64/// is mapped and rasterized, allocating once.
65#[derive(Debug)]
66pub struct GridPainter {
67    columns: Box<[Column]>,
68    layout: GridLayout,
69    palette: Palette,
70    text: TextPen,
71    cell: CellText,
72}
73
74impl GridPainter {
75    /// A painter for `rows` data rows of `columns`, in `font` at `px` pixels per em.
76    pub fn new(columns: Vec<Column>, rows: u32, font: Font, px: f32, palette: Palette) -> Self {
77        let mut glyphs = GlyphMap::new(font, px);
78        let layout = GridLayout::new(&columns, rows, &mut glyphs);
79        Self {
80            columns: columns.into_boxed_slice(),
81            layout,
82            palette,
83            text: TextPen { glyphs },
84            cell: CellText::new(),
85        }
86    }
87
88    /// The geometry this painter draws with.
89    pub fn layout(&self) -> &GridLayout {
90        &self.layout
91    }
92
93    /// The colors this painter draws with.
94    pub fn palette(&self) -> &Palette {
95        &self.palette
96    }
97
98    /// The columns, left to right.
99    pub fn columns(&self) -> &[Column] {
100        &self.columns
101    }
102
103    /// Rasterizes every character of `chars` into `cache` at every subpixel position now,
104    /// so frames that only use them never rasterize (or allocate). Updates queue as usual.
105    ///
106    /// Returns how many glyphs could not be cached (the atlas is too small for the set, or a
107    /// glyph is larger than a page); they would be retried, and allocate, while painting.
108    pub fn prewarm(&mut self, cache: &mut GlyphCache, chars: &str) -> usize {
109        let glyphs = &mut self.text.glyphs;
110        let mut missing = 0;
111        for ch in chars.chars() {
112            let Some(mapped) = glyphs.get(ch) else {
113                continue;
114            };
115            for bin in 0..SUBPIXEL_STEPS {
116                let cached = cache.get(glyphs.font(), mapped.id, glyphs.px(), bin);
117                missing += usize::from(cached.is_err());
118            }
119        }
120        missing
121    }
122
123    /// Draws the grid for `source` onto `canvas`.
124    pub fn paint<S: GridSource + ?Sized>(&mut self, source: &S, canvas: &mut Canvas<'_>) {
125        self.paint_columns(source, canvas, 0..self.columns.len());
126    }
127
128    /// Draws the grid for `source` onto `canvas`, asking for and drawing the cells of
129    /// `columns` only (the header bar and row stripes still span the grid): for a widget
130    /// that shows some columns scrolled and others frozen.
131    pub fn paint_columns<S: GridSource + ?Sized>(
132        &mut self,
133        source: &S,
134        canvas: &mut Canvas<'_>,
135        columns: std::ops::Range<usize>,
136    ) {
137        let columns = columns.start.min(self.columns.len())..columns.end.min(self.columns.len());
138        let width = self.layout.width() as f32;
139        let row_height = self.layout.row_height() as f32;
140
141        let header_y = self.layout.row_y(0) as f32;
142        canvas.fill_rect(
143            Rect::new(0.0, header_y, width, row_height),
144            self.palette.header_background,
145        );
146        let baseline = self.layout.baseline_y(0);
147        for (index, column) in self
148            .columns
149            .iter()
150            .enumerate()
151            .take(columns.end)
152            .skip(columns.start)
153        {
154            let cell = Cell {
155                index,
156                align: column.align,
157                baseline,
158                color: self.palette.header_text,
159            };
160            self.text.draw(&column.title, cell, &self.layout, canvas);
161        }
162
163        let visible = source.rows().min(self.layout.rows() as usize);
164        for (row, grid_row) in (0..visible).zip(1u32..) {
165            let y = self.layout.row_y(grid_row) as f32;
166            if grid_row.is_multiple_of(2) {
167                canvas.fill_rect(Rect::new(0.0, y, width, row_height), self.palette.stripe);
168            }
169            let baseline = self.layout.baseline_y(grid_row);
170            for (index, column) in self
171                .columns
172                .iter()
173                .enumerate()
174                .take(columns.end)
175                .skip(columns.start)
176            {
177                self.cell.clear();
178                let style = source.cell(row, index, &mut self.cell);
179                if let Some(background) = style.background {
180                    canvas.fill_rect(self.cell_rect(index, y), background);
181                }
182                let cell = Cell {
183                    index,
184                    align: column.align,
185                    baseline,
186                    color: style.color,
187                };
188                self.text
189                    .draw(self.cell.as_str(), cell, &self.layout, canvas);
190            }
191        }
192    }
193
194    /// Background rectangle of one cell: the column plus half the gap on each side, so
195    /// neighbouring cell backgrounds meet without overlapping.
196    fn cell_rect(&self, column: usize, y: f32) -> Rect {
197        let margin = self.layout.gap() as f32 / 2.0;
198        Rect::new(
199            self.layout.column_x(column) as f32 - margin,
200            y,
201            self.layout.column_width(column) as f32 + 2.0 * margin,
202            self.layout.row_height() as f32,
203        )
204    }
205}
206
207/// Where and how one cell's text goes.
208#[derive(Debug, Clone, Copy)]
209struct Cell {
210    index: usize,
211    align: Align,
212    baseline: f32,
213    color: Color,
214}
215
216/// The grid font's character map: lays out cell text and draws it glyph by glyph.
217#[derive(Debug)]
218struct TextPen {
219    glyphs: GlyphMap,
220}
221
222/// Text that cannot be measured more precisely than this is treated as fitting.
223const FIT_EPSILON: f32 = 0.01;
224
225impl TextPen {
226    /// Draws `text` aligned in its column; text wider than the column (in pixels or in
227    /// characters) becomes `#` fill, like a spreadsheet, rather than a misleading truncated
228    /// number.
229    fn draw(&mut self, text: &str, cell: Cell, layout: &GridLayout, canvas: &mut Canvas<'_>) {
230        let left = layout.column_x(cell.index) as f32;
231        let room = layout.column_width(cell.index) as f32;
232        let max_chars = layout.column_chars(cell.index) as usize;
233
234        let width = self.glyphs.measure(text);
235        if width <= room + FIT_EPSILON && text.chars().count() <= max_chars {
236            self.run(text.chars(), width, left, room, cell, canvas);
237            return;
238        }
239        let hash = self
240            .glyphs
241            .get('#')
242            .map_or(layout.char_width(), |g| g.advance);
243        // Lossless: column widths are a few hundred pixels at most.
244        let count = ((room / hash) as usize).min(max_chars);
245        let fill = std::iter::repeat_n('#', count);
246        self.run(fill, count as f32 * hash, left, room, cell, canvas);
247    }
248
249    fn run(
250        &mut self,
251        chars: impl Iterator<Item = char>,
252        width: f32,
253        left: f32,
254        room: f32,
255        cell: Cell,
256        canvas: &mut Canvas<'_>,
257    ) {
258        let mut pen = match cell.align {
259            Align::Left => left,
260            Align::Right => left + room - width,
261        };
262        for ch in chars {
263            let Some(mapped) = self.glyphs.get(ch) else {
264                continue;
265            };
266            let (font, px) = (self.glyphs.font(), self.glyphs.px());
267            let at = Point::new(pen, cell.baseline);
268            canvas.draw_glyph(font, mapped.id, px, at, cell.color);
269            pen += mapped.advance;
270        }
271    }
272}