Skip to main content

fret_ui_headless/table/
tanstack_auto_reset.rs

1//! TanStack Table v8 auto-reset scheduling helpers.
2//!
3//! TanStack's engine uses a per-table `_queue` microtask scheduler to coalesce multiple
4//! `_autoReset*` calls within a single render pass (derived model recompute).
5//!
6//! Fret's headless table is "pure" (state is owned externally), so we expose an explicit queue
7//! that callers can keep outside ephemeral table instances (including rebuild-each-frame setups).
8
9use super::{Table, TableState};
10
11/// Explicit auto-reset queue that models TanStack's `_queue` coalescing behavior.
12///
13/// Usage pattern:
14/// - Keep this queue outside ephemeral `Table` instances.
15/// - At the start of a render pass, call `begin_render_pass()`.
16/// - When a derived row model is recomputed and would call TanStack `_autoReset*`, call the
17///   corresponding `auto_reset_*` method (you may call it multiple times; it will coalesce).
18/// - At the end of the pass, call `flush(...)` to apply any pending resets.
19#[derive(Debug, Default, Clone)]
20pub struct TanStackAutoResetQueue {
21    page_index_registered: bool,
22    expanded_registered: bool,
23    pending_page_index_register: bool,
24    pending_expanded_register: bool,
25    pending_page_index_reset: bool,
26    pending_expanded_reset: bool,
27}
28
29impl TanStackAutoResetQueue {
30    /// Starts a new "render pass" (logical tick). Pending resets are cleared, registration is kept.
31    pub fn begin_render_pass(&mut self) {
32        self.pending_page_index_register = false;
33        self.pending_expanded_register = false;
34        self.pending_page_index_reset = false;
35        self.pending_expanded_reset = false;
36    }
37
38    /// TanStack-aligned: queues `_autoResetPageIndex()` (register-first, coalesced).
39    pub fn auto_reset_page_index<TData>(&mut self, table: &Table<'_, TData>) {
40        if !self.page_index_registered {
41            // TanStack sets `registered=true` via `_queue` (microtask), so multiple calls in the
42            // same render pass still count as "first register" and should not schedule a reset.
43            self.pending_page_index_register = true;
44            return;
45        }
46        if table.should_auto_reset_page_index() {
47            self.pending_page_index_reset = true;
48        }
49    }
50
51    /// TanStack-aligned: queues `_autoResetExpanded()` (register-first, coalesced).
52    pub fn auto_reset_expanded<TData>(&mut self, table: &Table<'_, TData>) {
53        if !self.expanded_registered {
54            self.pending_expanded_register = true;
55            return;
56        }
57        if table.should_auto_reset_expanded() {
58            self.pending_expanded_reset = true;
59        }
60    }
61
62    /// Applies any pending auto resets to `state` (at most once per kind per render pass).
63    ///
64    /// Notes:
65    /// - TanStack resets to `initialState`, not the feature default state, so we use
66    ///   `reset_*(default_state=false)`.
67    pub fn flush<TData>(&mut self, table: &Table<'_, TData>, state: &mut TableState) {
68        if self.pending_expanded_reset {
69            state.expanding = table.reset_expanded(false);
70        }
71        if self.pending_page_index_reset {
72            state.pagination = table.reset_page_index(false);
73        }
74        if self.pending_expanded_register {
75            self.expanded_registered = true;
76        }
77        if self.pending_page_index_register {
78            self.page_index_registered = true;
79        }
80        self.pending_expanded_register = false;
81        self.pending_page_index_register = false;
82        self.pending_expanded_reset = false;
83        self.pending_page_index_reset = false;
84    }
85
86    pub fn expanded_registered(&self) -> bool {
87        self.expanded_registered
88    }
89
90    pub fn page_index_registered(&self) -> bool {
91        self.page_index_registered
92    }
93
94    pub fn pending_expanded_reset(&self) -> bool {
95        self.pending_expanded_reset
96    }
97
98    pub fn pending_page_index_reset(&self) -> bool {
99        self.pending_page_index_reset
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106    use crate::table::{
107        ColumnDef, ExpandingState, FilteringFnSpec, PaginationState, RowId, RowKey,
108    };
109
110    #[derive(Debug, Clone)]
111    struct Row {
112        id: u64,
113        cpu: u64,
114    }
115
116    fn build_table(
117        data: &[Row],
118        state: TableState,
119        initial: TableState,
120        manual_pagination: bool,
121    ) -> Table<'_, Row> {
122        let columns: Vec<ColumnDef<Row>> = vec![
123            ColumnDef::<Row>::new("cpu")
124                .sort_value_by(|r: &Row| crate::table::TanStackValue::Number(r.cpu as f64))
125                .sorting_fn_auto()
126                .filtering_fn_auto(),
127        ];
128
129        Table::builder(data)
130            .columns(columns)
131            .get_row_key(|row, _idx, _parent| RowKey(row.id))
132            .get_row_id(|row, _idx, _parent| RowId::new(row.id.to_string()))
133            .state(state)
134            .initial_state(initial)
135            .options(crate::table::TableOptions {
136                manual_pagination,
137                ..Default::default()
138            })
139            .global_filter_fn(FilteringFnSpec::Auto)
140            .build()
141    }
142
143    #[test]
144    fn page_index_register_first_then_reset_coalesces() {
145        let data = vec![Row { id: 1, cpu: 10 }, Row { id: 2, cpu: 20 }];
146
147        let mut state = TableState::default();
148        state.pagination = PaginationState {
149            page_index: 1,
150            page_size: 2,
151        };
152
153        let initial = TableState::default();
154
155        let table = build_table(&data, state.clone(), initial.clone(), false);
156
157        let mut q = TanStackAutoResetQueue::default();
158
159        // First pass registers; no reset even if we call it multiple times.
160        q.begin_render_pass();
161        q.auto_reset_page_index(&table);
162        q.auto_reset_page_index(&table);
163        q.flush(&table, &mut state);
164        assert!(q.page_index_registered());
165        assert_eq!(state.pagination.page_index, 1);
166
167        // Second pass schedules reset; multiple calls are coalesced.
168        q.begin_render_pass();
169        q.auto_reset_page_index(&table);
170        q.auto_reset_page_index(&table);
171        q.auto_reset_page_index(&table);
172        assert!(q.pending_page_index_reset());
173        q.flush(&table, &mut state);
174        assert_eq!(state.pagination.page_index, initial.pagination.page_index);
175        assert!(!q.pending_page_index_reset());
176    }
177
178    #[test]
179    fn manual_pagination_disables_auto_reset_by_default() {
180        let data = vec![Row { id: 1, cpu: 10 }, Row { id: 2, cpu: 20 }];
181
182        let mut state = TableState::default();
183        state.pagination.page_index = 3;
184
185        let initial = TableState::default();
186        let table = build_table(&data, state.clone(), initial.clone(), true);
187
188        let mut q = TanStackAutoResetQueue::default();
189
190        // Register.
191        q.begin_render_pass();
192        q.auto_reset_page_index(&table);
193        q.flush(&table, &mut state);
194        assert_eq!(state.pagination.page_index, 3);
195
196        // Should not schedule because `manualPagination=true` and no override.
197        q.begin_render_pass();
198        q.auto_reset_page_index(&table);
199        q.flush(&table, &mut state);
200        assert_eq!(state.pagination.page_index, 3);
201    }
202
203    #[test]
204    fn expanded_register_first_then_reset() {
205        let data = vec![Row { id: 1, cpu: 10 }, Row { id: 2, cpu: 20 }];
206
207        let mut state = TableState::default();
208        state.expanding = ExpandingState::All;
209
210        let mut initial = TableState::default();
211        initial.expanding = ExpandingState::default();
212
213        let table = build_table(&data, state.clone(), initial.clone(), false);
214
215        let mut q = TanStackAutoResetQueue::default();
216
217        q.begin_render_pass();
218        q.auto_reset_expanded(&table);
219        q.flush(&table, &mut state);
220        assert_eq!(state.expanding, ExpandingState::All);
221
222        q.begin_render_pass();
223        q.auto_reset_expanded(&table);
224        q.flush(&table, &mut state);
225        assert_eq!(state.expanding, initial.expanding);
226    }
227}