Skip to main content

oxiui_table/
header.rs

1//! Header-level UI state: sort indicator, column reordering, and memoised
2//! index computation.
3//!
4//! This module is intentionally decoupled from the renderer backends so that
5//! both egui and iced can share the same state types.
6
7use crate::{ColumnFilter, RowSource, SortDirection, SortState};
8
9// ── HeaderSortState ──────────────────────────────────────────────────────────
10
11/// UI sort state tracked per column-header click.
12///
13/// Unlike the lower-level [`SortState`] (which records which column and an
14/// enum direction), `HeaderSortState` uses a simple `ascending: bool` toggle
15/// and an `Option<usize>` for the active column, matching the UI interaction
16/// model (click to sort ascending, click again to reverse).
17///
18/// Use [`HeaderSortState::as_sort_state`] to convert into the algorithm-level
19/// [`SortState`] accepted by `sort_indices`.
20#[derive(Clone, Debug, Default)]
21pub struct HeaderSortState {
22    /// The column currently sorted, or `None` when unsorted.
23    pub column: Option<usize>,
24    /// `true` for ascending, `false` for descending.
25    pub ascending: bool,
26}
27
28impl HeaderSortState {
29    /// Create a new, unsorted [`HeaderSortState`].
30    pub fn new() -> Self {
31        Self {
32            column: None,
33            ascending: true,
34        }
35    }
36
37    /// Toggle the sort for `col`:
38    /// - If `col` is not currently sorted, sort it ascending.
39    /// - If `col` is currently sorted ascending, switch to descending.
40    /// - If `col` is currently sorted descending, clear the sort.
41    pub fn toggle(&mut self, col: usize) {
42        match self.column {
43            Some(c) if c == col => {
44                if self.ascending {
45                    self.ascending = false;
46                } else {
47                    // Descending → clear
48                    self.column = None;
49                    self.ascending = true;
50                }
51            }
52            _ => {
53                self.column = Some(col);
54                self.ascending = true;
55            }
56        }
57    }
58
59    /// Return the indicator symbol for `col`:
60    /// - `"▲"` if this column is sorted ascending
61    /// - `"▼"` if this column is sorted descending
62    /// - `""` otherwise
63    pub fn indicator(&self, col: usize) -> &'static str {
64        match self.column {
65            Some(c) if c == col => {
66                if self.ascending {
67                    "▲"
68                } else {
69                    "▼"
70                }
71            }
72            _ => "",
73        }
74    }
75
76    /// Convert to the algorithm-level [`SortState`] for use with `sort_indices`.
77    /// Returns `None` when unsorted.
78    pub fn as_sort_state(&self) -> Option<SortState> {
79        self.column.map(|col| {
80            let dir = if self.ascending {
81                SortDirection::Ascending
82            } else {
83                SortDirection::Descending
84            };
85            SortState::new(col, dir)
86        })
87    }
88}
89
90// ── Column reordering ────────────────────────────────────────────────────────
91
92/// Apply a column move: move the column at index `from` in `order` to position
93/// `to`. Other columns shift to fill the gap.
94///
95/// Out-of-bounds indices are silently ignored.
96pub fn move_column(order: &mut Vec<usize>, from: usize, to: usize) {
97    if from >= order.len() || to >= order.len() {
98        return;
99    }
100    let col = order.remove(from);
101    order.insert(to, col);
102}
103
104// ── TableIndex ───────────────────────────────────────────────────────────────
105
106/// Memoised sort-and-filter index to avoid recomputing expensive permutations
107/// on every frame.
108///
109/// The dirty flags track whether the cached index is still valid. Call
110/// [`TableIndex::invalidate_sort`] after a sort-order change and
111/// [`TableIndex::invalidate_filter`] after a filter change (sorting a different
112/// column also implicitly invalidates the filter index).
113#[derive(Clone, Debug)]
114pub struct TableIndex {
115    sort_index: Vec<usize>,
116    filter_index: Vec<usize>,
117    sort_dirty: bool,
118    filter_dirty: bool,
119}
120
121impl TableIndex {
122    /// Create a new, empty (dirty) [`TableIndex`].
123    pub fn new() -> Self {
124        Self {
125            sort_index: Vec::new(),
126            filter_index: Vec::new(),
127            sort_dirty: true,
128            filter_dirty: true,
129        }
130    }
131
132    /// Mark the sort index (and consequently the filter index) as stale.
133    pub fn invalidate_sort(&mut self) {
134        self.sort_dirty = true;
135        self.filter_dirty = true;
136    }
137
138    /// Mark only the filter index as stale (sort order is unchanged).
139    pub fn invalidate_filter(&mut self) {
140        self.filter_dirty = true;
141    }
142
143    /// Returns `true` if the sort index needs to be recomputed.
144    pub fn is_sort_dirty(&self) -> bool {
145        self.sort_dirty
146    }
147
148    /// Returns `true` if the filter index needs to be recomputed.
149    pub fn is_filter_dirty(&self) -> bool {
150        self.filter_dirty
151    }
152
153    /// Return the current sort index, recomputing it if dirty.
154    ///
155    /// The returned slice holds row indices in sorted order.
156    pub fn sort_index(&mut self, rows: &dyn RowSource, sort: &HeaderSortState) -> &[usize] {
157        if self.sort_dirty {
158            self.sort_index = match sort.as_sort_state() {
159                Some(st) => crate::sort_indices(rows, st.column, st.direction),
160                None => (0..rows.row_count()).collect(),
161            };
162            self.sort_dirty = false;
163        }
164        &self.sort_index
165    }
166
167    /// Return the current filter index, recomputing it if dirty.
168    ///
169    /// The returned slice holds a subset of the sort index that passes every
170    /// active column filter.
171    pub fn filter_index(
172        &mut self,
173        rows: &dyn RowSource,
174        sort: &HeaderSortState,
175        filters: &[ColumnFilter],
176    ) -> &[usize] {
177        // Ensure sort is up-to-date first (filter depends on it).
178        self.sort_index(rows, sort);
179        if self.filter_dirty {
180            let active: Vec<&ColumnFilter> = filters.iter().filter(|f| !f.is_inactive()).collect();
181            if active.is_empty() {
182                self.filter_index = self.sort_index.clone();
183            } else {
184                self.filter_index = self
185                    .sort_index
186                    .iter()
187                    .copied()
188                    .filter(|&i| {
189                        let row = rows.row(i);
190                        active.iter().all(|f| f.matches(&row))
191                    })
192                    .collect();
193            }
194            self.filter_dirty = false;
195        }
196        &self.filter_index
197    }
198}
199
200impl Default for TableIndex {
201    fn default() -> Self {
202        Self::new()
203    }
204}
205
206// ── Row selection helper ─────────────────────────────────────────────────────
207
208use crate::SelectionModel;
209
210/// Handle a row-click event, delegating to the appropriate [`SelectionModel`]
211/// method.
212///
213/// - `ctrl`: ctrl/cmd held (toggle).
214/// - `shift`: shift held (range-select from last clicked).
215/// - `last_clicked`: updated with `row` after every call.
216pub fn handle_row_click(
217    selection: &mut SelectionModel,
218    row: usize,
219    ctrl: bool,
220    shift: bool,
221    last_clicked: &mut Option<usize>,
222) {
223    if shift {
224        selection.shift_click(row);
225    } else if ctrl {
226        selection.ctrl_click(row);
227    } else {
228        selection.click(row);
229    }
230    *last_clicked = Some(row);
231}
232
233#[cfg(test)]
234mod tests {
235    use super::*;
236    use crate::{Cell, ColumnDef, SelectionMode};
237
238    // ── Sort state tests ─────────────────────────────────────────────────────
239
240    #[test]
241    fn sort_toggle_new_column() {
242        let mut s = HeaderSortState::new();
243        s.toggle(0);
244        assert_eq!(s.column, Some(0));
245        assert!(s.ascending);
246    }
247
248    #[test]
249    fn sort_toggle_same_column() {
250        let mut s = HeaderSortState::new();
251        s.toggle(0); // ascending
252        s.toggle(0); // descending
253        assert_eq!(s.column, Some(0));
254        assert!(!s.ascending);
255        s.toggle(0); // clear
256        assert_eq!(s.column, None);
257    }
258
259    #[test]
260    fn sort_indicator_active() {
261        let mut s = HeaderSortState::new();
262        s.toggle(0);
263        assert_eq!(s.indicator(0), "▲");
264        s.toggle(0);
265        assert_eq!(s.indicator(0), "▼");
266    }
267
268    #[test]
269    fn sort_indicator_inactive() {
270        let mut s = HeaderSortState::new();
271        s.toggle(0);
272        assert_eq!(s.indicator(1), "");
273    }
274
275    // ── Column reorder tests ─────────────────────────────────────────────────
276
277    #[test]
278    fn move_column_forward() {
279        let mut order = vec![0usize, 1, 2];
280        move_column(&mut order, 0, 2);
281        assert_eq!(order, vec![1, 2, 0]);
282    }
283
284    #[test]
285    fn move_column_backward() {
286        let mut order = vec![0usize, 1, 2];
287        move_column(&mut order, 2, 0);
288        assert_eq!(order, vec![2, 0, 1]);
289    }
290
291    #[test]
292    fn move_column_no_op() {
293        let mut order = vec![0usize, 1, 2];
294        move_column(&mut order, 1, 1);
295        assert_eq!(order, vec![0, 1, 2]);
296    }
297
298    // ── TableIndex tests ─────────────────────────────────────────────────────
299
300    struct SimpleData {
301        rows: Vec<Vec<Cell>>,
302        cols: Vec<ColumnDef>,
303    }
304    impl RowSource for SimpleData {
305        fn row_count(&self) -> usize {
306            self.rows.len()
307        }
308        fn row(&self, i: usize) -> Vec<Cell> {
309            self.rows[i].clone()
310        }
311        fn column_defs(&self) -> &[ColumnDef] {
312            &self.cols
313        }
314    }
315
316    fn make_data() -> SimpleData {
317        SimpleData {
318            rows: vec![vec![Cell::Int(3)], vec![Cell::Int(1)], vec![Cell::Int(2)]],
319            cols: vec![],
320        }
321    }
322
323    #[test]
324    fn sort_index_invalidate() {
325        let mut idx = TableIndex::new();
326        // Force computation.
327        let data = make_data();
328        let sort = HeaderSortState::new();
329        let _ = idx.sort_index(&data, &sort);
330        assert!(!idx.is_sort_dirty());
331        idx.invalidate_sort();
332        assert!(idx.is_sort_dirty());
333    }
334
335    #[test]
336    fn filter_index_invalidate() {
337        let mut idx = TableIndex::new();
338        let data = make_data();
339        let sort = HeaderSortState::new();
340        let _ = idx.filter_index(&data, &sort, &[]);
341        assert!(!idx.is_filter_dirty());
342        idx.invalidate_filter();
343        assert!(idx.is_filter_dirty());
344    }
345
346    // ── Row selection via handle_row_click ───────────────────────────────────
347
348    #[test]
349    fn handle_click_selects_row() {
350        let mut sel = SelectionModel::new(SelectionMode::Multi);
351        let mut last = None;
352        handle_row_click(&mut sel, 3, false, false, &mut last);
353        assert!(sel.is_selected(3));
354        assert_eq!(last, Some(3));
355    }
356
357    #[test]
358    fn handle_ctrl_click_toggles() {
359        let mut sel = SelectionModel::new(SelectionMode::Multi);
360        let mut last = None;
361        handle_row_click(&mut sel, 3, true, false, &mut last);
362        assert!(sel.is_selected(3));
363        // Second ctrl+click deselects.
364        handle_row_click(&mut sel, 3, true, false, &mut last);
365        assert!(!sel.is_selected(3));
366    }
367
368    #[test]
369    fn handle_shift_click_range() {
370        let mut sel = SelectionModel::new(SelectionMode::Multi);
371        let mut last = None;
372        handle_row_click(&mut sel, 2, false, false, &mut last);
373        handle_row_click(&mut sel, 5, false, true, &mut last);
374        assert_eq!(sel.selected_sorted(), vec![2, 3, 4, 5]);
375    }
376}