Skip to main content

densa_widgets/grid/
mod.rs

1//! A data grid widget: rows from a [`GridModel`], scrolled, selected and sorted by the user.
2
3mod access;
4mod input;
5mod paint;
6
7use densa_core::{Color, Point, Rect, Size};
8use densa_draw::Canvas;
9use densa_grid::{
10    CellStyle, CellText, Column, GridLayout, GridModel, GridPainter, GridSource, Palette, SortOrder,
11};
12use densa_text::Font;
13use densa_ui::{
14    AccessNode, AccessRequest, Event, EventCx, Handled, LayoutCx, Length, Overflow, PaintCx, Style,
15    Theme, Widget,
16};
17
18use crate::paint::luminance;
19
20/// What a press on the grid is doing while the button is held.
21#[derive(Debug, Clone, Copy, PartialEq)]
22enum Drag {
23    /// A press on `column`'s title at `at`: it sorts on release, unless it moved away.
24    Title { column: usize, at: Point },
25    /// Resizing `column` from `chars` characters, grabbed at `x`.
26    Column { column: usize, chars: u32, x: f32 },
27    /// Dragging the scroll bar's thumb, grabbed `grab` below its top.
28    Thumb { grab: f32 },
29}
30
31/// The selected row, and the model's key for its record (if the model has keys), which keeps
32/// the selection on the record as rows move.
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34struct Selection {
35    row: usize,
36    key: Option<u64>,
37}
38
39/// A grid of rows from a [`GridModel`]: the user scrolls it (wheel, touchpad, scroll bar,
40/// keys), selects a row (click, arrow keys, Page Up and Down, Home, End), sorts it by
41/// clicking a column's title (again to reverse), and resizes columns by dragging the edges
42/// between titles. Leading columns can stay in place while the rest scroll sideways
43/// ([`with_frozen_columns`](Self::with_frozen_columns)).
44///
45/// Only the rows on screen are asked of the model, each frame, and painting allocates nothing
46/// once their characters are cached, however many rows there are. Change the data through
47/// [`model_mut`](Self::model_mut) from an [`Ui::update`](densa_ui::Ui::update) on the grid,
48/// which repaints it. The grid follows the model's [`GridModel::columns_revision`] and
49/// [`GridModel::generation`] each frame.
50///
51/// A selection is a row, kept on its record while rows move if the model gives rows keys
52/// ([`GridModel::row_key`]) and cleared when its record is removed or filtered out (as in
53/// Qt's proxy models); without keys, sorting clears it. It reports
54/// [`ActionKind::Selected`](densa_ui::ActionKind::Selected) with the row when the user
55/// selects one, [`ActionKind::Submitted`](densa_ui::ActionKind::Submitted) when they activate
56/// it (Enter, a double click), and [`ActionKind::Changed`](densa_ui::ActionKind::Changed) when
57/// a click sorted the rows. Give it [`DataGrid::style`].
58#[derive(Debug)]
59pub struct DataGrid<M> {
60    model: M,
61    /// The model's columns, as the user resized them.
62    columns: Vec<Column>,
63    frozen: usize,
64    /// Built at layout for the rows that fit, at the theme's text size.
65    painter: Option<GridPainter>,
66    first_row: usize,
67    /// Rows scrolled by a touchpad but not yet whole, as a fraction of a row.
68    row_rest: f32,
69    /// How far the unfrozen columns are scrolled sideways, in physical pixels.
70    scroll_x: f32,
71    selected: Option<Selection>,
72    /// The sort a click made, for models that do not report theirs.
73    sort: Option<(usize, SortOrder)>,
74    drag: Option<Drag>,
75    /// What the last layout used, to rebuild the painter when it changes.
76    built: Option<(Size, f32, Palette)>,
77    /// The model's [`GridModel::columns_revision`] and [`GridModel::generation`] last seen.
78    revisions: (u64, u64),
79}
80
81impl<M: GridModel> DataGrid<M> {
82    /// A grid showing `model`.
83    pub fn new(model: M) -> Self {
84        let columns = model.columns().to_vec();
85        Self {
86            revisions: (model.columns_revision(), model.generation()),
87            model,
88            columns,
89            frozen: 0,
90            painter: None,
91            first_row: 0,
92            row_rest: 0.0,
93            scroll_x: 0.0,
94            selected: None,
95            sort: None,
96            drag: None,
97            built: None,
98        }
99    }
100
101    /// The grid with its first `columns` columns kept in place while the others scroll
102    /// sideways (all of them, if there are fewer).
103    #[must_use]
104    pub fn with_frozen_columns(mut self, columns: usize) -> Self {
105        self.frozen = columns;
106        self
107    }
108
109    /// A grid filling its parent's free space, allowed to be smaller than its rows, and
110    /// clipping them.
111    pub fn style() -> Style {
112        Style::column()
113            .grow(1.0)
114            .min_size(Length::Px(0.0), Length::Px(0.0))
115            .overflow(Overflow::Clip)
116    }
117
118    /// The data.
119    pub fn model(&self) -> &M {
120        &self.model
121    }
122
123    /// The data, to change (from an [`Ui::update`](densa_ui::Ui::update) on the grid): the
124    /// grid repaints, and keeps its selection and scroll within the rows there are.
125    pub fn model_mut(&mut self, cx: &mut EventCx<'_>) -> &mut M {
126        cx.request_paint();
127        &mut self.model
128    }
129
130    /// Reads the model's columns again (after it changed them), dropping the user's widths,
131    /// and a sort or a drag on a column that is gone.
132    pub fn reset_columns(&mut self, cx: &mut EventCx<'_>) {
133        self.read_columns();
134        self.built = None;
135        cx.request_layout();
136        cx.request_paint();
137    }
138
139    /// The selected row.
140    pub fn selected(&self) -> Option<usize> {
141        self.selected.map(|s| s.row)
142    }
143
144    /// Selects `row` (none for `None`) and scrolls it into view.
145    pub fn select(&mut self, cx: &mut EventCx<'_>, row: Option<usize>) {
146        self.selected = row
147            .filter(|r| *r < self.model.rows())
148            .map(|row| self.selection_of(row));
149        if let Some(row) = row {
150            self.reveal_row(row);
151        }
152        cx.request_paint();
153    }
154
155    /// The first row on screen.
156    pub fn first_row(&self) -> usize {
157        self.first_row
158    }
159
160    /// Scrolls so that `row` is the first on screen, or as near as the rows allow.
161    pub fn scroll_to_row(&mut self, cx: &mut EventCx<'_>, row: usize) {
162        self.first_row = row.min(self.last_first_row());
163        self.row_rest = 0.0;
164        cx.request_paint();
165    }
166
167    /// The column and order the rows are sorted by: the model's own, if it reports it, else
168    /// the last one a click asked for.
169    pub fn sort_order(&self) -> Option<(usize, SortOrder)> {
170        self.model.sort_state().or(self.sort)
171    }
172
173    /// The columns as shown, with the user's widths.
174    pub fn columns(&self) -> &[Column] {
175        &self.columns
176    }
177
178    /// How many rows are painted, the last one possibly cut off (0 before the first layout).
179    pub fn visible_rows(&self) -> usize {
180        self.painter
181            .as_ref()
182            .map_or(0, |p| p.layout().rows() as usize)
183    }
184
185    /// How many rows fit whole: what a page is, and how many the last screen shows.
186    pub fn whole_rows(&self) -> usize {
187        let (Some(layout), Some((size, _, _))) = (self.layout_of(), self.built) else {
188            return 0;
189        };
190        let header = (layout.padding() + layout.row_height()) as f32;
191        let rows = ((size.height - header) / layout.row_height() as f32).floor();
192        // Lossless: a screen holds a few hundred rows at most; negative and NaN become 0.
193        rows.max(0.0) as usize
194    }
195
196    fn layout_of(&self) -> Option<&GridLayout> {
197        self.painter.as_ref().map(GridPainter::layout)
198    }
199
200    /// The highest first row: the last rows fill the screen.
201    fn last_first_row(&self) -> usize {
202        self.model.rows().saturating_sub(self.whole_rows().max(1))
203    }
204
205    /// Scrolls the least that shows `row` whole.
206    fn reveal_row(&mut self, row: usize) {
207        let whole = self.whole_rows().max(1);
208        if row < self.first_row {
209            self.first_row = row;
210        } else if row >= self.first_row + whole {
211            self.first_row = row + 1 - whole;
212        }
213        self.row_rest = 0.0;
214    }
215
216    /// The selection of `row`, with its record's key.
217    fn selection_of(&self, row: usize) -> Selection {
218        Selection {
219            row,
220            key: self.model.row_key(row),
221        }
222    }
223
224    /// The model's columns again, ending a drag and a sort on a column that is gone.
225    fn read_columns(&mut self) {
226        self.columns = self.model.columns().to_vec();
227        let count = self.columns.len();
228        self.drag = None;
229        self.sort = self.sort.filter(|(column, _)| *column < count);
230    }
231
232    /// Takes in what the model replaced since the last frame: its columns (the painter is
233    /// rebuilt), or all its rows (the selection and scroll start over).
234    fn follow_model(&mut self) {
235        let (columns, generation) = (self.model.columns_revision(), self.model.generation());
236        if columns != self.revisions.0 {
237            self.read_columns();
238            if let Some((size, px, palette)) = self.built {
239                self.build(size, px, palette);
240            }
241        }
242        if generation != self.revisions.1 {
243            self.selected = None;
244            self.sort = None;
245            self.drag = None;
246            self.first_row = 0;
247            self.row_rest = 0.0;
248            self.scroll_x = 0.0;
249        }
250        self.revisions = (columns, generation);
251    }
252
253    /// Keeps the selection on its record, and the scroll within the rows and columns there
254    /// are.
255    fn clamp(&mut self) {
256        let rows = self.model.rows();
257        self.selected = self.selected.and_then(|s| match s.key {
258            Some(key) => self.model.row_of_key(key).map(|row| Selection { row, ..s }),
259            None => Some(s),
260        });
261        self.selected = self.selected.filter(|s| s.row < rows);
262        self.first_row = self.first_row.min(self.last_first_row());
263        if let Some((size, _, _)) = self.built {
264            self.scroll_x = self.scroll_x.clamp(0.0, self.max_scroll_x(size.width));
265        }
266    }
267
268    /// The painter for `size` at `px`, or none if the bundled font cannot be read.
269    fn build(&mut self, size: Size, px: f32, palette: Palette) {
270        let Ok(font) = Font::bundled_mono() else {
271            self.painter = None;
272            return;
273        };
274        // One row first, to learn the row height the font gives.
275        let probe = GridPainter::new(self.columns.clone(), 1, font.clone(), px, palette);
276        let layout = probe.layout();
277        let header = (layout.padding() + layout.row_height()) as f32;
278        let rows = ((size.height - header) / layout.row_height() as f32).ceil();
279        // Lossless: a screen holds a few hundred rows at most; negative and NaN become 0.
280        let rows = rows.max(0.0) as u32;
281        self.painter = Some(GridPainter::new(
282            self.columns.clone(),
283            rows,
284            font,
285            px,
286            palette,
287        ));
288        self.built = Some((size, px, palette));
289    }
290}
291
292/// The grid's colors from the theme.
293fn palette(theme: &Theme) -> Palette {
294    let mut palette = Palette::new();
295    palette.background = theme.surface;
296    palette.header_background = theme.background;
297    palette.header_text = theme.text_dim;
298    palette.stripe = theme.text.with_alpha(0.035);
299    palette.text = theme.text;
300    palette.dim_text = theme.text_dim;
301    // Darker green and red on a light surface, where the dark theme's are too faint.
302    if luminance(theme.surface) > 0.5 {
303        palette.positive = Color::srgb(0x1A7F37);
304        palette.negative = Color::srgb(0xCF222E);
305    }
306    palette
307}
308
309/// The rows on screen, as the painter reads them: from the first row, the selected one
310/// highlighted where the model draws no fill of its own (a flash stays visible).
311struct Shown<'a, M> {
312    model: &'a M,
313    first: usize,
314    selected: Option<usize>,
315    highlight: Color,
316    palette: Palette,
317}
318
319impl<M: GridModel> GridSource for Shown<'_, M> {
320    fn rows(&self) -> usize {
321        self.model.rows().saturating_sub(self.first)
322    }
323
324    fn cell(&self, row: usize, column: usize, text: &mut CellText) -> CellStyle {
325        let row = self.first + row;
326        let style = self.model.cell(row, column, text, &self.palette);
327        if self.selected == Some(row) && style.background.is_none() {
328            return style.with_background(self.highlight);
329        }
330        style
331    }
332}
333
334impl<M: GridModel + 'static> Widget for DataGrid<M> {
335    fn layout(&mut self, cx: &mut LayoutCx<'_>, size: Size) {
336        let px = cx.px(cx.theme().font_size);
337        let palette = palette(cx.theme());
338        if self.built != Some((size, px, palette)) {
339            self.build(size, px, palette);
340        }
341        self.follow_model();
342        self.clamp();
343    }
344
345    fn paint(&mut self, cx: &PaintCx<'_>, canvas: &mut Canvas<'_>) {
346        self.model.prepare();
347        self.follow_model();
348        self.clamp();
349        self.paint_grid(cx, canvas);
350    }
351
352    fn event(&mut self, cx: &mut EventCx<'_>, event: &Event<'_>) -> Handled {
353        self.handle(cx, event)
354    }
355
356    fn accepts_focus(&self) -> bool {
357        true
358    }
359
360    fn accessibility(&self, cx: &PaintCx<'_>, node: &mut AccessNode) {
361        self.describe(cx, node);
362    }
363
364    fn access_action(&mut self, cx: &mut EventCx<'_>, request: &AccessRequest<'_>) -> Handled {
365        self.act(cx, request)
366    }
367}
368
369/// Where the grid's parts are within `rect`, in physical pixels.
370#[derive(Debug, Clone, Copy)]
371struct Frame {
372    /// The whole widget.
373    rect: Rect,
374    /// The header row, across the widget.
375    header: Rect,
376    /// The rows below the header, across the widget.
377    body: Rect,
378    /// Where the frozen columns end, from the widget's left (0 when none are frozen).
379    frozen: f32,
380}