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