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