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;
59}
60
61#[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 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 pub fn layout(&self) -> &GridLayout {
92 &self.layout
93 }
94
95 pub fn palette(&self) -> &Palette {
97 &self.palette
98 }
99
100 pub fn columns(&self) -> &[Column] {
102 &self.columns
103 }
104
105 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 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 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 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#[derive(Debug, Clone, Copy)]
211struct Cell {
212 index: usize,
213 align: Align,
214 baseline: f32,
215 color: Color,
216}
217
218#[derive(Debug)]
220struct TextPen {
221 glyphs: GlyphMap,
222}
223
224const FIT_EPSILON: f32 = 0.01;
226
227impl TextPen {
228 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 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}