Skip to main content

fret_ui_headless/table/
column_sizing.rs

1use std::collections::HashMap;
2
3use super::column_sizing_info::ColumnSizingInfoState;
4use super::{ColumnDef, ColumnId};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum ColumnSizingRegion {
8    All,
9    Left,
10    Center,
11    Right,
12}
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum ColumnResizeMode {
16    OnChange,
17    OnEnd,
18}
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum ColumnResizeDirection {
22    Ltr,
23    Rtl,
24}
25
26/// TanStack-compatible column sizing map: `column_id -> size`.
27pub type ColumnSizingState = HashMap<ColumnId, f32>;
28
29pub fn column_size(state: &ColumnSizingState, column: &ColumnId) -> Option<f32> {
30    state.get(column).copied()
31}
32
33pub fn resolved_column_size<TData>(state: &ColumnSizingState, column: &ColumnDef<TData>) -> f32 {
34    let raw = state.get(&column.id).copied().unwrap_or(column.size);
35    raw.clamp(column.min_size, column.max_size)
36}
37
38pub fn column_can_resize<TData>(options: super::TableOptions, column: &ColumnDef<TData>) -> bool {
39    options.enable_column_resizing && column.enable_resizing
40}
41
42fn round_column_size(size: f32) -> f32 {
43    (size * 100.0).round() / 100.0
44}
45
46pub fn column_resize_preview_size(info: &ColumnSizingInfoState, column: &ColumnId) -> Option<f32> {
47    let delta_percentage = info.delta_percentage?;
48    let (_, start) = info
49        .column_sizing_start
50        .iter()
51        .find(|(id, _)| id.as_ref() == column.as_ref())?;
52    Some(round_column_size(
53        (*start + *start * delta_percentage).max(0.0),
54    ))
55}
56
57pub fn begin_column_resize(
58    info: &mut ColumnSizingInfoState,
59    resizing_column: ColumnId,
60    start_offset: f32,
61    start_size: f32,
62    column_sizing_start: Vec<(ColumnId, f32)>,
63) {
64    *info = ColumnSizingInfoState {
65        start_offset: Some(start_offset),
66        start_size: Some(start_size),
67        delta_offset: Some(0.0),
68        delta_percentage: Some(0.0),
69        is_resizing_column: Some(resizing_column),
70        column_sizing_start,
71    };
72}
73
74#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75enum ResizeEvent {
76    Move,
77    End,
78}
79
80fn update_column_resize(
81    mode: ColumnResizeMode,
82    direction: ColumnResizeDirection,
83    sizing: &mut ColumnSizingState,
84    info: &mut ColumnSizingInfoState,
85    event: ResizeEvent,
86    client_x: Option<f32>,
87) {
88    let Some(client_x) = client_x else { return };
89
90    let Some(start_offset) = info.start_offset else {
91        return;
92    };
93    let Some(start_size) = info.start_size else {
94        return;
95    };
96    if start_size <= 0.0 {
97        return;
98    }
99
100    let direction_mul = match direction {
101        ColumnResizeDirection::Ltr => 1.0,
102        ColumnResizeDirection::Rtl => -1.0,
103    };
104
105    let delta_offset = (client_x - start_offset) * direction_mul;
106    let delta_percentage = (delta_offset / start_size).max(-0.999_999);
107
108    info.delta_offset = Some(delta_offset);
109    info.delta_percentage = Some(delta_percentage);
110
111    if !(mode == ColumnResizeMode::OnChange || event == ResizeEvent::End) {
112        return;
113    }
114
115    for (column_id, header_size) in &info.column_sizing_start {
116        let next = (header_size + header_size * delta_percentage).max(0.0);
117        sizing.insert(column_id.clone(), round_column_size(next));
118    }
119}
120
121pub fn drag_column_resize(
122    mode: ColumnResizeMode,
123    direction: ColumnResizeDirection,
124    sizing: &mut ColumnSizingState,
125    info: &mut ColumnSizingInfoState,
126    client_x: f32,
127) {
128    update_column_resize(
129        mode,
130        direction,
131        sizing,
132        info,
133        ResizeEvent::Move,
134        Some(client_x),
135    );
136}
137
138pub fn end_column_resize(
139    mode: ColumnResizeMode,
140    direction: ColumnResizeDirection,
141    sizing: &mut ColumnSizingState,
142    info: &mut ColumnSizingInfoState,
143    client_x: Option<f32>,
144) {
145    update_column_resize(mode, direction, sizing, info, ResizeEvent::End, client_x);
146    *info = ColumnSizingInfoState::default();
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152    use crate::table::TableOptions;
153
154    #[test]
155    fn column_size_reads_from_map() {
156        let mut state = ColumnSizingState::default();
157        state.insert(ColumnId::from("a"), 123.0);
158
159        assert_eq!(column_size(&state, &ColumnId::from("a")), Some(123.0));
160        assert_eq!(column_size(&state, &ColumnId::from("b")), None);
161    }
162
163    #[test]
164    fn resolved_column_size_falls_back_to_column_default_and_clamps() {
165        #[derive(Debug)]
166        struct Item;
167
168        let col = ColumnDef::<Item>::new("a")
169            .size(100.0)
170            .min_size(60.0)
171            .max_size(80.0);
172
173        let state = ColumnSizingState::default();
174        assert_eq!(resolved_column_size(&state, &col), 80.0);
175
176        let mut state = ColumnSizingState::default();
177        state.insert(ColumnId::from("a"), 10.0);
178        assert_eq!(resolved_column_size(&state, &col), 60.0);
179    }
180
181    #[test]
182    fn column_can_resize_respects_table_and_column_flags() {
183        #[derive(Debug)]
184        struct Item;
185
186        let col = ColumnDef::<Item>::new("a").enable_resizing(false);
187        assert!(!column_can_resize(TableOptions::default(), &col));
188
189        let mut options = TableOptions::default();
190        options.enable_column_resizing = false;
191        let col = ColumnDef::<Item>::new("a").enable_resizing(true);
192        assert!(!column_can_resize(options, &col));
193    }
194
195    #[test]
196    fn column_resize_on_change_updates_sizing_on_move_and_resets_on_end() {
197        let mut sizing = ColumnSizingState::default();
198        let mut info = ColumnSizingInfoState::default();
199
200        begin_column_resize(
201            &mut info,
202            ColumnId::from("a"),
203            10.0,
204            100.0,
205            vec![(ColumnId::from("a"), 100.0)],
206        );
207
208        drag_column_resize(
209            ColumnResizeMode::OnChange,
210            ColumnResizeDirection::Ltr,
211            &mut sizing,
212            &mut info,
213            60.0,
214        );
215
216        assert_eq!(sizing.get(&ColumnId::from("a")).copied(), Some(150.0));
217        assert_eq!(info.delta_offset, Some(50.0));
218        assert_eq!(info.delta_percentage, Some(0.5));
219        assert_eq!(
220            info.is_resizing_column.as_ref().map(|s| s.as_ref()),
221            Some("a")
222        );
223
224        end_column_resize(
225            ColumnResizeMode::OnChange,
226            ColumnResizeDirection::Ltr,
227            &mut sizing,
228            &mut info,
229            Some(60.0),
230        );
231
232        assert_eq!(sizing.get(&ColumnId::from("a")).copied(), Some(150.0));
233        assert!(info.is_resizing_column.is_none());
234        assert!(info.start_offset.is_none());
235        assert!(info.start_size.is_none());
236        assert!(info.delta_offset.is_none());
237        assert!(info.delta_percentage.is_none());
238        assert!(info.column_sizing_start.is_empty());
239    }
240
241    #[test]
242    fn column_resize_on_end_only_writes_sizing_at_end() {
243        let mut sizing = ColumnSizingState::default();
244        let mut info = ColumnSizingInfoState::default();
245
246        begin_column_resize(
247            &mut info,
248            ColumnId::from("a"),
249            0.0,
250            100.0,
251            vec![(ColumnId::from("a"), 100.0)],
252        );
253
254        drag_column_resize(
255            ColumnResizeMode::OnEnd,
256            ColumnResizeDirection::Ltr,
257            &mut sizing,
258            &mut info,
259            50.0,
260        );
261
262        assert!(sizing.is_empty());
263        assert_eq!(info.delta_offset, Some(50.0));
264        assert_eq!(info.delta_percentage, Some(0.5));
265
266        end_column_resize(
267            ColumnResizeMode::OnEnd,
268            ColumnResizeDirection::Ltr,
269            &mut sizing,
270            &mut info,
271            Some(50.0),
272        );
273
274        assert_eq!(sizing.get(&ColumnId::from("a")).copied(), Some(150.0));
275        assert!(info.is_resizing_column.is_none());
276    }
277
278    #[test]
279    fn begin_column_resize_uses_resizing_column_size_for_start_size_when_present() {
280        let mut info = ColumnSizingInfoState::default();
281
282        begin_column_resize(
283            &mut info,
284            ColumnId::from("ab"),
285            0.0,
286            150.0,
287            vec![
288                (ColumnId::from("a"), 100.0),
289                (ColumnId::from("b"), 50.0),
290                (ColumnId::from("ab"), 150.0),
291            ],
292        );
293
294        assert_eq!(info.start_size, Some(150.0));
295    }
296
297    #[test]
298    fn column_resize_rtl_flips_delta_direction() {
299        let mut sizing = ColumnSizingState::default();
300        let mut info = ColumnSizingInfoState::default();
301
302        begin_column_resize(
303            &mut info,
304            ColumnId::from("a"),
305            0.0,
306            100.0,
307            vec![(ColumnId::from("a"), 100.0)],
308        );
309
310        drag_column_resize(
311            ColumnResizeMode::OnChange,
312            ColumnResizeDirection::Rtl,
313            &mut sizing,
314            &mut info,
315            50.0,
316        );
317
318        assert_eq!(sizing.get(&ColumnId::from("a")).copied(), Some(50.0));
319        assert_eq!(info.delta_offset, Some(-50.0));
320        assert_eq!(info.delta_percentage, Some(-0.5));
321    }
322
323    #[test]
324    fn column_resize_preview_reads_from_info_without_touching_sizing() {
325        let mut sizing = ColumnSizingState::default();
326        let mut info = ColumnSizingInfoState::default();
327
328        begin_column_resize(
329            &mut info,
330            ColumnId::from("a"),
331            0.0,
332            100.0,
333            vec![(ColumnId::from("a"), 100.0)],
334        );
335
336        drag_column_resize(
337            ColumnResizeMode::OnEnd,
338            ColumnResizeDirection::Ltr,
339            &mut sizing,
340            &mut info,
341            50.0,
342        );
343
344        assert!(sizing.is_empty());
345        assert_eq!(
346            column_resize_preview_size(&info, &ColumnId::from("a")),
347            Some(150.0)
348        );
349    }
350}