mod access;
mod input;
mod paint;
mod selection;
use densa_core::{Color, Point, Rect, Size};
use densa_draw::Canvas;
use densa_grid::{
CellStyle, CellText, Column, GridLayout, GridModel, GridPainter, GridSource, Palette, SortOrder,
};
use densa_text::Font;
use densa_ui::{
AccessNode, AccessRequest, Event, EventCx, Handled, LayoutCx, Length, Overflow, PaintCx, Style,
Theme, Widget,
};
use crate::paint::luminance;
#[derive(Debug, Clone, Copy, PartialEq)]
enum Drag {
Title { column: usize, at: Point },
Column { column: usize, chars: u32, x: f32 },
Thumb { grab: f32 },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Selection {
row: usize,
key: Option<u64>,
}
#[derive(Debug)]
pub struct DataGrid<M> {
model: M,
columns: Vec<Column>,
frozen: usize,
painter: Option<GridPainter>,
first_row: usize,
row_rest: f32,
scroll_x: f32,
selected: Option<Selection>,
anchor: Option<Selection>,
all: bool,
sort: Option<(usize, SortOrder)>,
drag: Option<Drag>,
built: Option<(Size, f32, Palette)>,
revisions: (u64, u64),
}
impl<M: GridModel> DataGrid<M> {
pub fn new(model: M) -> Self {
let columns = model.columns().to_vec();
Self {
revisions: (model.columns_revision(), model.generation()),
model,
columns,
frozen: 0,
painter: None,
first_row: 0,
row_rest: 0.0,
scroll_x: 0.0,
selected: None,
anchor: None,
all: false,
sort: None,
drag: None,
built: None,
}
}
#[must_use]
pub fn with_frozen_columns(mut self, columns: usize) -> Self {
self.frozen = columns;
self
}
pub fn style() -> Style {
Style::column()
.grow(1.0)
.min_size(Length::Px(0.0), Length::Px(0.0))
.overflow(Overflow::Clip)
}
pub fn model(&self) -> &M {
&self.model
}
pub fn model_mut(&mut self, cx: &mut EventCx<'_>) -> &mut M {
cx.request_paint();
&mut self.model
}
pub fn reset_columns(&mut self, cx: &mut EventCx<'_>) {
self.read_columns();
self.built = None;
cx.request_layout();
cx.request_paint();
}
pub fn selected(&self) -> Option<usize> {
self.selected.map(|s| s.row)
}
pub fn select(&mut self, cx: &mut EventCx<'_>, row: Option<usize>) {
if row.is_some() && row == self.selected() {
if let Some(row) = row {
self.reveal_row(row);
}
cx.request_paint();
return;
}
self.anchor = None;
self.all = false;
self.selected = row
.filter(|r| *r < self.model.rows())
.map(|row| self.selection_of(row));
if let Some(row) = row {
self.reveal_row(row);
}
cx.request_paint();
}
pub fn first_row(&self) -> usize {
self.first_row
}
pub fn scroll_to_row(&mut self, cx: &mut EventCx<'_>, row: usize) {
self.first_row = row.min(self.last_first_row());
self.row_rest = 0.0;
cx.request_paint();
}
pub fn sort_order(&self) -> Option<(usize, SortOrder)> {
self.model.sort_state().or(self.sort)
}
pub fn columns(&self) -> &[Column] {
&self.columns
}
pub fn visible_rows(&self) -> usize {
self.painter
.as_ref()
.map_or(0, |p| p.layout().rows() as usize)
}
pub fn whole_rows(&self) -> usize {
let (Some(layout), Some((size, _, _))) = (self.layout_of(), self.built) else {
return 0;
};
let header = (layout.padding() + layout.row_height()) as f32;
let rows = ((size.height - header) / layout.row_height() as f32).floor();
rows.max(0.0) as usize
}
fn layout_of(&self) -> Option<&GridLayout> {
self.painter.as_ref().map(GridPainter::layout)
}
fn last_first_row(&self) -> usize {
self.model.rows().saturating_sub(self.whole_rows().max(1))
}
fn reveal_row(&mut self, row: usize) {
let whole = self.whole_rows().max(1);
if row < self.first_row {
self.first_row = row;
} else if row >= self.first_row + whole {
self.first_row = row + 1 - whole;
}
self.row_rest = 0.0;
}
fn selection_of(&self, row: usize) -> Selection {
Selection {
row,
key: self.model.row_key(row),
}
}
fn read_columns(&mut self) {
self.columns = self.model.columns().to_vec();
let count = self.columns.len();
self.drag = None;
self.sort = self.sort.filter(|(column, _)| *column < count);
}
fn follow_model(&mut self) {
let (columns, generation) = (self.model.columns_revision(), self.model.generation());
if columns != self.revisions.0 {
self.read_columns();
if let Some((size, px, palette)) = self.built {
self.build(size, px, palette);
}
}
if generation != self.revisions.1 {
self.selected = None;
self.all = false;
self.sort = None;
self.drag = None;
self.first_row = 0;
self.row_rest = 0.0;
self.scroll_x = 0.0;
}
self.revisions = (columns, generation);
}
fn clamp(&mut self) {
self.selected = self.selected.and_then(|s| self.find(s));
self.anchor = self
.anchor
.and_then(|s| self.find(s))
.filter(|_| self.selected.is_some());
self.first_row = self.first_row.min(self.last_first_row());
if let Some((size, _, _)) = self.built {
self.scroll_x = self.scroll_x.clamp(0.0, self.max_scroll_x(size.width));
}
}
fn find(&self, selection: Selection) -> Option<Selection> {
let row = match selection.key {
Some(key) => self.model.row_of_key(key)?,
None => selection.row,
};
(row < self.model.rows()).then_some(Selection { row, ..selection })
}
fn build(&mut self, size: Size, px: f32, palette: Palette) {
let Ok(font) = Font::bundled_mono() else {
self.painter = None;
return;
};
let probe = GridPainter::new(self.columns.clone(), 1, font.clone(), px, palette);
let layout = probe.layout();
let header = (layout.padding() + layout.row_height()) as f32;
let rows = ((size.height - header) / layout.row_height() as f32).ceil();
let rows = rows.max(0.0) as u32;
self.painter = Some(GridPainter::new(
self.columns.clone(),
rows,
font,
px,
palette,
));
self.built = Some((size, px, palette));
}
}
fn palette(theme: &Theme) -> Palette {
let mut palette = Palette::new();
palette.background = theme.surface;
palette.header_background = theme.background;
palette.header_text = theme.text_dim;
palette.stripe = theme.text.with_alpha(0.035);
palette.text = theme.text;
palette.dim_text = theme.text_dim;
if luminance(theme.surface) > 0.5 {
palette.positive = Color::srgb(0x1A7F37);
palette.negative = Color::srgb(0xCF222E);
}
palette
}
struct Shown<'a, M> {
model: &'a M,
first: usize,
selected: std::ops::Range<usize>,
highlight: Color,
palette: Palette,
}
impl<M: GridModel> GridSource for Shown<'_, M> {
fn rows(&self) -> usize {
self.model.rows().saturating_sub(self.first)
}
fn cell(&self, row: usize, column: usize, text: &mut CellText) -> CellStyle {
let row = self.first + row;
let style = self.model.cell(row, column, text, &self.palette);
if self.selected.contains(&row) && style.background.is_none() {
return style.with_background(self.highlight);
}
style
}
}
impl<M: GridModel + 'static> Widget for DataGrid<M> {
fn layout(&mut self, cx: &mut LayoutCx<'_>, size: Size) {
let px = cx.px(cx.theme().font_size);
let palette = palette(cx.theme());
if self.built != Some((size, px, palette)) {
self.build(size, px, palette);
}
self.follow_model();
self.clamp();
}
fn paint(&mut self, cx: &PaintCx<'_>, canvas: &mut Canvas<'_>) {
self.model.prepare();
self.follow_model();
self.clamp();
self.paint_grid(cx, canvas);
}
fn event(&mut self, cx: &mut EventCx<'_>, event: &Event<'_>) -> Handled {
self.handle(cx, event)
}
fn accepts_focus(&self) -> bool {
true
}
fn accessibility(&self, cx: &PaintCx<'_>, node: &mut AccessNode) {
self.describe(cx, node);
}
fn access_action(&mut self, cx: &mut EventCx<'_>, request: &AccessRequest<'_>) -> Handled {
self.act(cx, request)
}
}
#[derive(Debug, Clone, Copy)]
struct Frame {
rect: Rect,
header: Rect,
body: Rect,
frozen: f32,
}