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    /// Which columns the user resized: their widths outlast the model's new ones.
89    dragged: Vec<bool>,
90}
91
92impl<M: GridModel> DataGrid<M> {
93    /// A grid showing `model`.
94    pub fn new(model: M) -> Self {
95        let columns = model.columns().to_vec();
96        Self {
97            revisions: (model.columns_revision(), model.generation()),
98            dragged: vec![false; columns.len()],
99            model,
100            columns,
101            frozen: 0,
102            painter: None,
103            first_row: 0,
104            row_rest: 0.0,
105            scroll_x: 0.0,
106            selected: None,
107            anchor: None,
108            all: false,
109            sort: None,
110            drag: None,
111            built: None,
112        }
113    }
114
115    /// The grid with its first `columns` columns kept in place while the others scroll
116    /// sideways (all of them, if there are fewer).
117    #[must_use]
118    pub fn with_frozen_columns(mut self, columns: usize) -> Self {
119        self.frozen = columns;
120        self
121    }
122
123    /// A grid filling its parent's free space, allowed to be smaller than its rows, and
124    /// clipping them.
125    pub fn style() -> Style {
126        Style::column()
127            .grow(1.0)
128            .min_size(Length::Px(0.0), Length::Px(0.0))
129            .overflow(Overflow::Clip)
130    }
131
132    /// The data.
133    pub fn model(&self) -> &M {
134        &self.model
135    }
136
137    /// The data, to change (from an [`Ui::update`](densa_ui::Ui::update) on the grid): the
138    /// grid repaints, and keeps its selection and scroll within the rows there are.
139    pub fn model_mut(&mut self, cx: &mut EventCx<'_>) -> &mut M {
140        cx.request_paint();
141        &mut self.model
142    }
143
144    /// Reads the model's columns again (after it changed them), dropping the user's widths,
145    /// and a sort or a drag on a column that is gone.
146    pub fn reset_columns(&mut self, cx: &mut EventCx<'_>) {
147        self.read_columns(false);
148        self.built = None;
149        cx.request_layout();
150        cx.request_paint();
151    }
152
153    /// The selected row.
154    pub fn selected(&self) -> Option<usize> {
155        self.selected.map(|s| s.row)
156    }
157
158    /// Selects `row` alone (none for `None`) and scrolls it into view. For the row already
159    /// selected, the rows selected with it stay selected (an application echoing
160    /// [`ActionKind::Selected`](densa_ui::ActionKind::Selected) back does not end a range);
161    /// [`select_rows`](Self::select_rows) with the row twice selects it alone.
162    pub fn select(&mut self, cx: &mut EventCx<'_>, row: Option<usize>) {
163        if row.is_some() && row == self.selected() {
164            if let Some(row) = row {
165                self.reveal_row(row);
166            }
167            cx.request_paint();
168            return;
169        }
170        self.anchor = None;
171        self.all = false;
172        self.selected = row
173            .filter(|r| *r < self.model.rows())
174            .map(|row| self.selection_of(row));
175        if let Some(row) = row {
176            self.reveal_row(row);
177        }
178        cx.request_paint();
179    }
180
181    /// The first row on screen.
182    pub fn first_row(&self) -> usize {
183        self.first_row
184    }
185
186    /// Scrolls so that `row` is the first on screen, or as near as the rows allow.
187    pub fn scroll_to_row(&mut self, cx: &mut EventCx<'_>, row: usize) {
188        self.first_row = row.min(self.last_first_row());
189        self.row_rest = 0.0;
190        cx.request_paint();
191    }
192
193    /// The column and order the rows are sorted by: the model's own, if it reports it, else
194    /// the last one a click asked for.
195    pub fn sort_order(&self) -> Option<(usize, SortOrder)> {
196        self.model.sort_state().or(self.sort)
197    }
198
199    /// The columns as shown, with the user's widths.
200    pub fn columns(&self) -> &[Column] {
201        &self.columns
202    }
203
204    /// How many rows are painted, the last one possibly cut off (0 before the first layout).
205    pub fn visible_rows(&self) -> usize {
206        self.painter
207            .as_ref()
208            .map_or(0, |p| p.layout().rows() as usize)
209    }
210
211    /// How many rows fit whole: what a page is, and how many the last screen shows.
212    pub fn whole_rows(&self) -> usize {
213        let (Some(layout), Some((size, _, _))) = (self.layout_of(), self.built) else {
214            return 0;
215        };
216        let header = (layout.padding() + layout.row_height()) as f32;
217        let rows = ((size.height - header) / layout.row_height() as f32).floor();
218        // Lossless: a screen holds a few hundred rows at most; negative and NaN become 0.
219        rows.max(0.0) as usize
220    }
221
222    fn layout_of(&self) -> Option<&GridLayout> {
223        self.painter.as_ref().map(GridPainter::layout)
224    }
225
226    /// The highest first row: the last rows fill the screen.
227    fn last_first_row(&self) -> usize {
228        self.model.rows().saturating_sub(self.whole_rows().max(1))
229    }
230
231    /// Scrolls the least that shows `row` whole.
232    fn reveal_row(&mut self, row: usize) {
233        let whole = self.whole_rows().max(1);
234        if row < self.first_row {
235            self.first_row = row;
236        } else if row >= self.first_row + whole {
237            self.first_row = row + 1 - whole;
238        }
239        self.row_rest = 0.0;
240    }
241
242    /// The selection of `row`, with its record's key.
243    fn selection_of(&self, row: usize) -> Selection {
244        Selection {
245            row,
246            key: self.model.row_key(row),
247        }
248    }
249
250    /// The model's columns again, ending a sort on a column that is gone. With `keep_dragged`
251    /// and the titles shown before, widths the user dragged stay, and so does a drag in
252    /// progress; otherwise every width is the model's and a drag ends.
253    fn read_columns(&mut self, keep_dragged: bool) {
254        let mut columns = self.model.columns().to_vec();
255        let same = columns.len() == self.columns.len()
256            && columns
257                .iter()
258                .zip(&self.columns)
259                .all(|(a, b)| a.title == b.title);
260        if keep_dragged && same {
261            for ((new, old), dragged) in columns.iter_mut().zip(&self.columns).zip(&self.dragged) {
262                if *dragged {
263                    new.chars = old.chars;
264                }
265            }
266        } else {
267            self.dragged.clear();
268            self.drag = None;
269        }
270        self.columns = columns;
271        let count = self.columns.len();
272        self.dragged.resize(count, false);
273        self.sort = self.sort.filter(|(column, _)| *column < count);
274    }
275
276    /// Takes in what the model replaced since the last frame: its columns (the painter is
277    /// rebuilt), or all its rows (the selection and scroll start over).
278    fn follow_model(&mut self) {
279        let (columns, generation) = (self.model.columns_revision(), self.model.generation());
280        if columns != self.revisions.0 {
281            self.read_columns(true);
282            if let Some((size, px, palette)) = self.built {
283                self.build(size, px, palette);
284            }
285        }
286        if generation != self.revisions.1 {
287            // The anchor goes with the selection in `clamp`.
288            self.selected = None;
289            self.all = false;
290            self.sort = None;
291            self.drag = None;
292            self.first_row = 0;
293            self.row_rest = 0.0;
294            self.scroll_x = 0.0;
295        }
296        self.revisions = (columns, generation);
297    }
298
299    /// Keeps the selection on its record, and the scroll within the rows and columns there
300    /// are. A range whose start is gone is the selected row alone.
301    fn clamp(&mut self) {
302        self.selected = self.selected.and_then(|s| self.find(s));
303        self.anchor = self
304            .anchor
305            .and_then(|s| self.find(s))
306            .filter(|_| self.selected.is_some());
307        self.first_row = self.first_row.min(self.last_first_row());
308        if let Some((size, _, _)) = self.built {
309            self.scroll_x = self.scroll_x.clamp(0.0, self.max_scroll_x(size.width));
310        }
311    }
312
313    /// Where `selection`'s record is now, if it is still within the rows.
314    fn find(&self, selection: Selection) -> Option<Selection> {
315        let row = match selection.key {
316            Some(key) => self.model.row_of_key(key)?,
317            None => selection.row,
318        };
319        (row < self.model.rows()).then_some(Selection { row, ..selection })
320    }
321
322    /// The painter for `size` at `px`, or none if the bundled font cannot be read.
323    fn build(&mut self, size: Size, px: f32, palette: Palette) {
324        let Ok(font) = Font::bundled_mono() else {
325            self.painter = None;
326            return;
327        };
328        // One row first, to learn the row height the font gives.
329        let probe = GridPainter::new(self.columns.clone(), 1, font.clone(), px, palette);
330        let layout = probe.layout();
331        let header = (layout.padding() + layout.row_height()) as f32;
332        let rows = ((size.height - header) / layout.row_height() as f32).ceil();
333        // Lossless: a screen holds a few hundred rows at most; negative and NaN become 0.
334        let rows = rows.max(0.0) as u32;
335        self.painter = Some(GridPainter::new(
336            self.columns.clone(),
337            rows,
338            font,
339            px,
340            palette,
341        ));
342        self.built = Some((size, px, palette));
343    }
344}
345
346/// The grid's colors from the theme.
347fn palette(theme: &Theme) -> Palette {
348    let mut palette = Palette::new();
349    palette.background = theme.surface;
350    palette.header_background = theme.background;
351    palette.header_text = theme.text_dim;
352    palette.stripe = theme.text.with_alpha(0.035);
353    palette.text = theme.text;
354    palette.dim_text = theme.text_dim;
355    // Darker green and red on a light surface, where the dark theme's are too faint.
356    if luminance(theme.surface) > 0.5 {
357        palette.positive = Color::srgb(0x1A7F37);
358        palette.negative = Color::srgb(0xCF222E);
359    }
360    palette
361}
362
363/// The rows on screen, as the painter reads them: from the first row, the selected ones
364/// highlighted where the model draws no fill of its own (a flash stays visible).
365struct Shown<'a, M> {
366    model: &'a M,
367    first: usize,
368    selected: std::ops::Range<usize>,
369    highlight: Color,
370    palette: Palette,
371}
372
373impl<M: GridModel> GridSource for Shown<'_, M> {
374    fn rows(&self) -> usize {
375        self.model.rows().saturating_sub(self.first)
376    }
377
378    fn cell(&self, row: usize, column: usize, text: &mut CellText) -> CellStyle {
379        let row = self.first + row;
380        let style = self.model.cell(row, column, text, &self.palette);
381        if self.selected.contains(&row) && style.background.is_none() {
382            return style.with_background(self.highlight);
383        }
384        style
385    }
386}
387
388impl<M: GridModel + 'static> Widget for DataGrid<M> {
389    fn layout(&mut self, cx: &mut LayoutCx<'_>, size: Size) {
390        let px = cx.px(cx.theme().font_size);
391        let palette = palette(cx.theme());
392        if self.built != Some((size, px, palette)) {
393            self.build(size, px, palette);
394        }
395        self.follow_model();
396        self.clamp();
397    }
398
399    fn paint(&mut self, cx: &PaintCx<'_>, canvas: &mut Canvas<'_>) {
400        self.model.prepare();
401        self.follow_model();
402        self.clamp();
403        self.paint_grid(cx, canvas);
404    }
405
406    fn event(&mut self, cx: &mut EventCx<'_>, event: &Event<'_>) -> Handled {
407        self.handle(cx, event)
408    }
409
410    fn accepts_focus(&self) -> bool {
411        true
412    }
413
414    fn accessibility(&self, cx: &PaintCx<'_>, node: &mut AccessNode) {
415        self.describe(cx, node);
416    }
417
418    fn access_action(&mut self, cx: &mut EventCx<'_>, request: &AccessRequest<'_>) -> Handled {
419        self.act(cx, request)
420    }
421}
422
423/// Where the grid's parts are within `rect`, in physical pixels.
424#[derive(Debug, Clone, Copy)]
425struct Frame {
426    /// The whole widget.
427    rect: Rect,
428    /// The header row, across the widget.
429    header: Rect,
430    /// The rows below the header, across the widget.
431    body: Rect,
432    /// Where the frozen columns end, from the widget's left (0 when none are frozen).
433    frozen: f32,
434}