use densa_core::{Color, Point, Rect};
use densa_draw::Canvas;
use densa_fmt::FmtBuf;
use densa_text::{Font, GlyphCache, GlyphMap, SUBPIXEL_STEPS};
use crate::{Align, Column, GridLayout, Palette};
pub const CELL_BYTES: usize = 64;
pub type CellText = FmtBuf<CELL_BYTES>;
#[derive(Debug, Clone, Copy, PartialEq)]
#[non_exhaustive]
pub struct CellStyle {
pub color: Color,
pub background: Option<Color>,
}
impl CellStyle {
pub const fn new(color: Color) -> Self {
Self {
color,
background: None,
}
}
#[must_use]
pub fn with_background(mut self, background: impl Into<Option<Color>>) -> Self {
self.background = background.into();
self
}
}
pub trait GridSource {
fn rows(&self) -> usize;
fn cell(&self, row: usize, column: usize, text: &mut CellText) -> CellStyle;
}
#[derive(Debug)]
pub struct GridPainter {
columns: Box<[Column]>,
layout: GridLayout,
palette: Palette,
text: TextPen,
cell: CellText,
}
impl GridPainter {
pub fn new(columns: Vec<Column>, rows: u32, font: Font, px: f32, palette: Palette) -> Self {
let mut glyphs = GlyphMap::new(font, px);
let layout = GridLayout::new(&columns, rows, &mut glyphs);
Self {
columns: columns.into_boxed_slice(),
layout,
palette,
text: TextPen { glyphs },
cell: CellText::new(),
}
}
pub fn layout(&self) -> &GridLayout {
&self.layout
}
pub fn palette(&self) -> &Palette {
&self.palette
}
pub fn columns(&self) -> &[Column] {
&self.columns
}
pub fn prewarm(&mut self, cache: &mut GlyphCache, chars: &str) -> usize {
let glyphs = &mut self.text.glyphs;
let mut missing = 0;
for ch in chars.chars() {
let Some(mapped) = glyphs.get(ch) else {
continue;
};
for bin in 0..SUBPIXEL_STEPS {
let cached = cache.get(glyphs.font(), mapped.id, glyphs.px(), bin);
missing += usize::from(cached.is_err());
}
}
missing
}
pub fn paint<S: GridSource + ?Sized>(&mut self, source: &S, canvas: &mut Canvas<'_>) {
self.paint_columns(source, canvas, 0..self.columns.len());
}
pub fn paint_columns<S: GridSource + ?Sized>(
&mut self,
source: &S,
canvas: &mut Canvas<'_>,
columns: std::ops::Range<usize>,
) {
let columns = columns.start.min(self.columns.len())..columns.end.min(self.columns.len());
let width = self.layout.width() as f32;
let row_height = self.layout.row_height() as f32;
let header_y = self.layout.row_y(0) as f32;
canvas.fill_rect(
Rect::new(0.0, header_y, width, row_height),
self.palette.header_background,
);
let baseline = self.layout.baseline_y(0);
for (index, column) in self
.columns
.iter()
.enumerate()
.take(columns.end)
.skip(columns.start)
{
let cell = Cell {
index,
align: column.align,
baseline,
color: self.palette.header_text,
};
self.text.draw(&column.title, cell, &self.layout, canvas);
}
let visible = source.rows().min(self.layout.rows() as usize);
for (row, grid_row) in (0..visible).zip(1u32..) {
let y = self.layout.row_y(grid_row) as f32;
if grid_row.is_multiple_of(2) {
canvas.fill_rect(Rect::new(0.0, y, width, row_height), self.palette.stripe);
}
let baseline = self.layout.baseline_y(grid_row);
for (index, column) in self
.columns
.iter()
.enumerate()
.take(columns.end)
.skip(columns.start)
{
self.cell.clear();
let style = source.cell(row, index, &mut self.cell);
if let Some(background) = style.background {
canvas.fill_rect(self.cell_rect(index, y), background);
}
let cell = Cell {
index,
align: column.align,
baseline,
color: style.color,
};
self.text
.draw(self.cell.as_str(), cell, &self.layout, canvas);
}
}
}
fn cell_rect(&self, column: usize, y: f32) -> Rect {
let margin = self.layout.gap() as f32 / 2.0;
Rect::new(
self.layout.column_x(column) as f32 - margin,
y,
self.layout.column_width(column) as f32 + 2.0 * margin,
self.layout.row_height() as f32,
)
}
}
#[derive(Debug, Clone, Copy)]
struct Cell {
index: usize,
align: Align,
baseline: f32,
color: Color,
}
#[derive(Debug)]
struct TextPen {
glyphs: GlyphMap,
}
const FIT_EPSILON: f32 = 0.01;
impl TextPen {
fn draw(&mut self, text: &str, cell: Cell, layout: &GridLayout, canvas: &mut Canvas<'_>) {
let left = layout.column_x(cell.index) as f32;
let room = layout.column_width(cell.index) as f32;
let max_chars = layout.column_chars(cell.index) as usize;
let width = self.glyphs.measure(text);
if width <= room + FIT_EPSILON && text.chars().count() <= max_chars {
self.run(text.chars(), width, left, room, cell, canvas);
return;
}
let hash = self
.glyphs
.get('#')
.map_or(layout.char_width(), |g| g.advance);
let count = ((room / hash) as usize).min(max_chars);
let fill = std::iter::repeat_n('#', count);
self.run(fill, count as f32 * hash, left, room, cell, canvas);
}
fn run(
&mut self,
chars: impl Iterator<Item = char>,
width: f32,
left: f32,
room: f32,
cell: Cell,
canvas: &mut Canvas<'_>,
) {
let mut pen = match cell.align {
Align::Left => left,
Align::Right => left + room - width,
};
for ch in chars {
let Some(mapped) = self.glyphs.get(ch) else {
continue;
};
let (font, px) = (self.glyphs.font(), self.glyphs.px());
let at = Point::new(pen, cell.baseline);
canvas.draw_glyph(font, mapped.id, px, at, cell.color);
pen += mapped.advance;
}
}
}