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
8pub const CELL_BYTES: usize = 64;
11
12pub type CellText = FmtBuf<CELL_BYTES>;
14
15#[derive(Debug, Clone, Copy, PartialEq)]
18#[non_exhaustive]
19pub struct CellStyle {
20 pub color: Color,
22 pub background: Option<Color>,
24}
25
26impl CellStyle {
27 pub const fn new(color: Color) -> Self {
29 Self {
30 color,
31 background: None,
32 }
33 }
34
35 #[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
43pub trait GridSource {
48 fn rows(&self) -> usize;
50
51 fn cell(&self, row: usize, column: usize, text: &mut CellText) -> CellStyle;
57}
58
59#[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 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 pub fn layout(&self) -> &GridLayout {
90 &self.layout
91 }
92
93 pub fn palette(&self) -> &Palette {
95 &self.palette
96 }
97
98 pub fn columns(&self) -> &[Column] {
100 &self.columns
101 }
102
103 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 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 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 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#[derive(Debug, Clone, Copy)]
209struct Cell {
210 index: usize,
211 align: Align,
212 baseline: f32,
213 color: Color,
214}
215
216#[derive(Debug)]
218struct TextPen {
219 glyphs: GlyphMap,
220}
221
222const FIT_EPSILON: f32 = 0.01;
224
225impl TextPen {
226 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 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}