Skip to main content

fret_ui_headless/table/
sorting.rs

1use std::cmp::Ordering;
2use std::collections::HashMap;
3use std::sync::Arc;
4
5use super::{
6    ColumnDef, ColumnId, RowIndex, RowKey, RowModel, SortCmpFn, SortUndefined, TableOptions,
7    column::{BuiltInSortingFn, SortIsUndefinedFn, SortValueFn, SortingFnSpec, TanStackValue},
8};
9
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct SortSpec {
12    pub column: ColumnId,
13    pub desc: bool,
14}
15
16pub type SortingState = Vec<SortSpec>;
17
18#[derive(Clone)]
19pub enum SortingFnDef<TData> {
20    BuiltIn(BuiltInSortingFn),
21    Cmp(SortCmpFn<TData>),
22}
23
24/// A Send+Sync subset of upstream `Column` needed for sorting state transitions.
25///
26/// This exists because `ColumnDef<TData>` carries non-Send/Sync function pointers, but the
27/// headless engine's `Updater<T>` closures are required to be Send+Sync.
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct SortToggleColumn {
30    pub id: ColumnId,
31    pub enable_sorting: bool,
32    pub enable_multi_sort: bool,
33    pub sort_desc_first: Option<bool>,
34    pub has_sort_value_source: bool,
35}
36
37pub fn sort_for_column(sorting: &[SortSpec], id: &str) -> Option<bool> {
38    sorting
39        .iter()
40        .find(|s| s.column.as_ref() == id)
41        .map(|s| s.desc)
42}
43
44pub fn toggle_sort_for_column(sorting: &mut SortingState, column: ColumnId, multi: bool) {
45    let pos = sorting
46        .iter()
47        .position(|s| s.column.as_ref() == column.as_ref());
48
49    let next = match pos.and_then(|i| sorting.get(i).map(|s| s.desc)) {
50        None => Some(false),
51        Some(false) => Some(true),
52        Some(true) => None,
53    };
54
55    if !multi {
56        sorting.clear();
57        if let Some(desc) = next {
58            sorting.push(SortSpec { column, desc });
59        }
60        return;
61    }
62
63    match (pos, next) {
64        (None, Some(desc)) => sorting.push(SortSpec { column, desc }),
65        (Some(i), Some(desc)) => {
66            if let Some(spec) = sorting.get_mut(i) {
67                spec.desc = desc;
68            }
69        }
70        (Some(i), None) => {
71            sorting.remove(i);
72        }
73        (None, None) => {}
74    }
75}
76
77/// TanStack-aligned sorting state transition for `column.toggleSorting(undefined, multi)`.
78///
79/// This implements the upstream action logic (add/toggle/remove/replace) including:
80/// - `sortDescFirst` (table + column)
81/// - `enableSortingRemoval` (note: TanStack’s `toggleSorting` does not pass the `multi` flag to
82///   `getNextSortingOrder`, so `enableMultiRemove` does not affect the direct toggle behavior)
83/// - `enableMultiSort` / `maxMultiSortColCount`
84///
85/// Notes:
86/// - This models the behavior of calling `column.toggleSorting` directly. It does not include UI
87///   gating like `getCanSort()` (which TanStack applies in `getToggleSortingHandler`).
88/// - This currently does not implement TanStack’s `getAutoSortDir()` inference (string => asc, else
89///   desc) when `sortDescFirst` is unset. Until we add a stable “sort value extractor” surface,
90///   the fallback first direction is `asc`.
91pub fn toggle_sorting_state_tanstack(
92    sorting: &mut SortingState,
93    column: &SortToggleColumn,
94    options: TableOptions,
95    multi: bool,
96    auto_sort_dir_desc: bool,
97) {
98    let existing_index = sorting
99        .iter()
100        .position(|s| s.column.as_ref() == column.id.as_ref());
101    let existing_sorting = existing_index.and_then(|i| sorting.get(i)).cloned();
102
103    // TanStack `column.getFirstSortDir()`:
104    //
105    // `sortDescFirst = columnDef.sortDescFirst ?? table.options.sortDescFirst ?? (getAutoSortDir() === 'desc')`
106    let first_sort_direction_desc = column
107        .sort_desc_first
108        .or(options.sort_desc_first)
109        .unwrap_or(auto_sort_dir_desc);
110
111    let is_sorted = existing_sorting
112        .as_ref()
113        .map(|s| if s.desc { "desc" } else { "asc" });
114
115    let enable_sorting_removal = options.enable_sorting_removal;
116
117    let next_sorting_order = match is_sorted {
118        None => Some(if first_sort_direction_desc {
119            "desc"
120        } else {
121            "asc"
122        }),
123        Some(current) => {
124            let first = if first_sort_direction_desc {
125                "desc"
126            } else {
127                "asc"
128            };
129            if current != first && enable_sorting_removal {
130                None
131            } else if current == "desc" {
132                Some("asc")
133            } else {
134                Some("desc")
135            }
136        }
137    };
138
139    let next_desc = next_sorting_order == Some("desc");
140
141    let can_multi_sort = options.enable_multi_sort && column.enable_multi_sort;
142    let multi = multi && can_multi_sort;
143
144    enum SortAction {
145        Add,
146        Remove,
147        Toggle,
148        Replace,
149    }
150
151    let old_len = sorting.len();
152
153    let mut sort_action = if old_len > 0 && multi {
154        if existing_sorting.is_some() {
155            SortAction::Toggle
156        } else {
157            SortAction::Add
158        }
159    } else if old_len > 0 && existing_index.is_some_and(|i| i != old_len.saturating_sub(1)) {
160        SortAction::Replace
161    } else if existing_sorting.is_some() {
162        SortAction::Toggle
163    } else {
164        SortAction::Replace
165    };
166
167    if matches!(sort_action, SortAction::Toggle) && next_sorting_order.is_none() {
168        sort_action = SortAction::Remove;
169    }
170
171    match sort_action {
172        SortAction::Add => {
173            sorting.push(SortSpec {
174                column: column.id.clone(),
175                desc: next_desc,
176            });
177
178            if let Some(max) = options.max_multi_sort_col_count
179                && max > 0
180                && sorting.len() > max
181            {
182                let to_remove = sorting.len().saturating_sub(max);
183                sorting.drain(0..to_remove);
184            }
185        }
186        SortAction::Toggle => {
187            if let Some(i) = existing_index
188                && let Some(spec) = sorting.get_mut(i)
189            {
190                spec.desc = next_desc;
191            }
192        }
193        SortAction::Remove => {
194            if let Some(i) = existing_index {
195                sorting.remove(i);
196            }
197        }
198        SortAction::Replace => {
199            sorting.clear();
200            sorting.push(SortSpec {
201                column: column.id.clone(),
202                desc: next_desc,
203            });
204        }
205    }
206}
207
208pub fn toggle_sorting_tanstack<TData>(
209    sorting: &mut SortingState,
210    column: &ColumnDef<TData>,
211    options: TableOptions,
212    multi: bool,
213    auto_sort_dir_desc: bool,
214) {
215    let col = SortToggleColumn {
216        id: column.id.clone(),
217        enable_sorting: column.enable_sorting,
218        enable_multi_sort: column.enable_multi_sort,
219        sort_desc_first: column.sort_desc_first,
220        has_sort_value_source: column.sort_cmp.is_some() || column.sort_value.is_some(),
221    };
222    toggle_sorting_state_tanstack(sorting, &col, options, multi, auto_sort_dir_desc);
223}
224
225/// TanStack-aligned sorting handler state transition (UI handler semantics).
226///
227/// This mirrors `column.getToggleSortingHandler()` which first checks `getCanSort()` (and thus
228/// respects `enableSorting`) and uses `getCanMultiSort()` + `isMultiSortEvent` to decide whether a
229/// multi-sort toggle is active.
230pub fn toggle_sorting_state_handler_tanstack(
231    sorting: &mut SortingState,
232    column: &SortToggleColumn,
233    options: TableOptions,
234    event_multi: bool,
235    auto_sort_dir_desc: bool,
236) {
237    let can_sort = options.enable_sorting && column.enable_sorting && column.has_sort_value_source;
238    if !can_sort {
239        return;
240    }
241
242    let can_multi_sort = options.enable_multi_sort && column.enable_multi_sort;
243    let multi = if can_multi_sort { event_multi } else { false };
244
245    toggle_sorting_state_tanstack(sorting, column, options, multi, auto_sort_dir_desc);
246}
247
248/// TanStack-aligned sorting handler state transition (UI handler semantics).
249///
250/// This mirrors `column.getToggleSortingHandler()` which first checks `getCanSort()` (and thus
251/// respects `enableSorting`) and uses `getCanMultiSort()` + `isMultiSortEvent` to decide whether a
252/// multi-sort toggle is active.
253pub fn toggle_sorting_handler_tanstack<TData>(
254    sorting: &mut SortingState,
255    column: &ColumnDef<TData>,
256    options: TableOptions,
257    event_multi: bool,
258    auto_sort_dir_desc: bool,
259) {
260    let col = SortToggleColumn {
261        id: column.id.clone(),
262        enable_sorting: column.enable_sorting,
263        enable_multi_sort: column.enable_multi_sort,
264        sort_desc_first: column.sort_desc_first,
265        has_sort_value_source: column.sort_cmp.is_some() || column.sort_value.is_some(),
266    };
267    toggle_sorting_state_handler_tanstack(sorting, &col, options, event_multi, auto_sort_dir_desc);
268}
269
270fn builtin_sorting_fn_key(key: &str) -> Option<BuiltInSortingFn> {
271    Some(match key {
272        "alphanumeric" => BuiltInSortingFn::Alphanumeric,
273        "alphanumericCaseSensitive" => BuiltInSortingFn::AlphanumericCaseSensitive,
274        "text" => BuiltInSortingFn::Text,
275        "textCaseSensitive" => BuiltInSortingFn::TextCaseSensitive,
276        "datetime" => BuiltInSortingFn::Datetime,
277        "basic" => BuiltInSortingFn::Basic,
278        _ => return None,
279    })
280}
281
282fn strict_equal(a: &TanStackValue, b: &TanStackValue) -> bool {
283    match (a, b) {
284        (TanStackValue::Undefined, TanStackValue::Undefined) => true,
285        (TanStackValue::Null, TanStackValue::Null) => true,
286        (TanStackValue::Bool(a), TanStackValue::Bool(b)) => a == b,
287        (TanStackValue::Number(a), TanStackValue::Number(b)) => {
288            if a.is_nan() || b.is_nan() {
289                false
290            } else {
291                a == b
292            }
293        }
294        (TanStackValue::String(a), TanStackValue::String(b)) => a.as_ref() == b.as_ref(),
295        (TanStackValue::DateTime(a), TanStackValue::DateTime(b)) => {
296            if a.is_nan() || b.is_nan() {
297                false
298            } else {
299                a == b
300            }
301        }
302        _ => false,
303    }
304}
305
306fn to_number_for_compare(value: &TanStackValue) -> f64 {
307    match value {
308        TanStackValue::Undefined => f64::NAN,
309        TanStackValue::Null => 0.0,
310        TanStackValue::Bool(b) => {
311            if *b {
312                1.0
313            } else {
314                0.0
315            }
316        }
317        TanStackValue::Number(v) => *v,
318        TanStackValue::DateTime(v) => *v,
319        TanStackValue::String(s) => {
320            let trimmed = s.as_ref().trim();
321            if trimmed.is_empty() {
322                0.0
323            } else {
324                trimmed.parse::<f64>().unwrap_or(f64::NAN)
325            }
326        }
327        TanStackValue::Array(_) => f64::NAN,
328    }
329}
330
331fn abstract_gt(a: &TanStackValue, b: &TanStackValue) -> bool {
332    match (a, b) {
333        (TanStackValue::String(a), TanStackValue::String(b)) => a.as_ref() > b.as_ref(),
334        _ => {
335            let a = to_number_for_compare(a);
336            let b = to_number_for_compare(b);
337            if a.is_nan() || b.is_nan() {
338                false
339            } else {
340                a > b
341            }
342        }
343    }
344}
345
346fn abstract_lt(a: &TanStackValue, b: &TanStackValue) -> bool {
347    match (a, b) {
348        (TanStackValue::String(a), TanStackValue::String(b)) => a.as_ref() < b.as_ref(),
349        _ => {
350            let a = to_number_for_compare(a);
351            let b = to_number_for_compare(b);
352            if a.is_nan() || b.is_nan() {
353                false
354            } else {
355                a < b
356            }
357        }
358    }
359}
360
361fn compare_basic_tanstack(a: &TanStackValue, b: &TanStackValue) -> Ordering {
362    if strict_equal(a, b) {
363        return Ordering::Equal;
364    }
365    if abstract_gt(a, b) {
366        Ordering::Greater
367    } else {
368        Ordering::Less
369    }
370}
371
372fn to_string_for_sorting(value: &TanStackValue) -> String {
373    match value {
374        TanStackValue::Number(v) => {
375            if v.is_nan() || v.is_infinite() {
376                String::new()
377            } else {
378                v.to_string()
379            }
380        }
381        TanStackValue::String(s) => s.as_ref().to_string(),
382        _ => String::new(),
383    }
384}
385
386fn split_alpha_numeric_tokens(s: &str) -> Vec<&str> {
387    let mut out: Vec<&str> = Vec::new();
388    let mut start: Option<usize> = None;
389    let mut current_is_digit: Option<bool> = None;
390
391    for (i, ch) in s.char_indices() {
392        let is_digit = ch.is_ascii_digit();
393        match (start, current_is_digit) {
394            (None, None) => {
395                start = Some(i);
396                current_is_digit = Some(is_digit);
397            }
398            (Some(_), Some(kind)) if kind == is_digit => {}
399            (Some(start_idx), Some(_)) => {
400                if start_idx != i {
401                    out.push(&s[start_idx..i]);
402                }
403                start = Some(i);
404                current_is_digit = Some(is_digit);
405            }
406            _ => {}
407        }
408    }
409
410    if let (Some(start_idx), Some(_)) = (start, current_is_digit)
411        && start_idx < s.len()
412    {
413        out.push(&s[start_idx..]);
414    }
415
416    out.into_iter().filter(|seg| !seg.is_empty()).collect()
417}
418
419fn compare_alphanumeric(a_str: &str, b_str: &str) -> Ordering {
420    let a = split_alpha_numeric_tokens(a_str);
421    let b = split_alpha_numeric_tokens(b_str);
422
423    let mut ai = 0usize;
424    let mut bi = 0usize;
425
426    while ai < a.len() && bi < b.len() {
427        let aa = a[ai];
428        let bb = b[bi];
429        ai += 1;
430        bi += 1;
431
432        let an = aa.parse::<i64>().ok();
433        let bn = bb.parse::<i64>().ok();
434
435        match (an, bn) {
436            (None, None) => {
437                if aa > bb {
438                    return Ordering::Greater;
439                }
440                if bb > aa {
441                    return Ordering::Less;
442                }
443            }
444            (Some(_), None) => return Ordering::Greater,
445            (None, Some(_)) => return Ordering::Less,
446            (Some(an), Some(bn)) => {
447                if an > bn {
448                    return Ordering::Greater;
449                }
450                if bn > an {
451                    return Ordering::Less;
452                }
453            }
454        }
455    }
456
457    a.len().cmp(&b.len())
458}
459
460fn auto_sorting_fn_builtin<'a, TData>(
461    row_model: &RowModel<'a, TData>,
462    column: &ColumnDef<TData>,
463) -> BuiltInSortingFn {
464    let Some(get_value) = column.sort_value.as_ref() else {
465        return BuiltInSortingFn::Basic;
466    };
467
468    // TanStack: `table.getFilteredRowModel().flatRows.slice(10)` (note: skip the first 10 rows).
469    let mut saw_string = false;
470    for &row_index in row_model.flat_rows().iter().skip(10) {
471        let Some(row) = row_model.row(row_index) else {
472            continue;
473        };
474        let value = (get_value)(row.original);
475
476        if matches!(value, TanStackValue::DateTime(_)) {
477            return BuiltInSortingFn::Datetime;
478        }
479
480        if let TanStackValue::String(s) = value {
481            saw_string = true;
482            if split_alpha_numeric_tokens(s.as_ref()).len() > 1 {
483                return BuiltInSortingFn::Alphanumeric;
484            }
485        }
486    }
487
488    if saw_string {
489        BuiltInSortingFn::Text
490    } else {
491        BuiltInSortingFn::Basic
492    }
493}
494
495fn compare_builtin_sort<TData>(
496    kind: BuiltInSortingFn,
497    get_value: &SortValueFn<TData>,
498    a: &TData,
499    b: &TData,
500) -> Ordering {
501    let a = (get_value)(a);
502    let b = (get_value)(b);
503
504    match kind {
505        BuiltInSortingFn::Basic => compare_basic_tanstack(&a, &b),
506        BuiltInSortingFn::Datetime => {
507            if abstract_gt(&a, &b) {
508                Ordering::Greater
509            } else if abstract_lt(&a, &b) {
510                Ordering::Less
511            } else {
512                Ordering::Equal
513            }
514        }
515        BuiltInSortingFn::Text => {
516            let a = to_string_for_sorting(&a).to_lowercase();
517            let b = to_string_for_sorting(&b).to_lowercase();
518            if a == b {
519                Ordering::Equal
520            } else if a > b {
521                Ordering::Greater
522            } else {
523                Ordering::Less
524            }
525        }
526        BuiltInSortingFn::TextCaseSensitive => {
527            let a = to_string_for_sorting(&a);
528            let b = to_string_for_sorting(&b);
529            if a == b {
530                Ordering::Equal
531            } else if a > b {
532                Ordering::Greater
533            } else {
534                Ordering::Less
535            }
536        }
537        BuiltInSortingFn::Alphanumeric => {
538            let a = to_string_for_sorting(&a).to_lowercase();
539            let b = to_string_for_sorting(&b).to_lowercase();
540            compare_alphanumeric(&a, &b)
541        }
542        BuiltInSortingFn::AlphanumericCaseSensitive => {
543            let a = to_string_for_sorting(&a);
544            let b = to_string_for_sorting(&b);
545            compare_alphanumeric(&a, &b)
546        }
547    }
548}
549
550#[derive(Clone)]
551enum ResolvedSortCmp<TData> {
552    Cmp(SortCmpFn<TData>),
553    BuiltIn {
554        kind: BuiltInSortingFn,
555        get_value: SortValueFn<TData>,
556    },
557}
558
559#[derive(Clone)]
560enum ResolvedIsUndefined<TData> {
561    Fn(SortIsUndefinedFn<TData>),
562    Value(SortValueFn<TData>),
563}
564
565fn resolve_sort_cmp_for_column<'a, TData>(
566    row_model: &RowModel<'a, TData>,
567    column: &ColumnDef<TData>,
568    sorting_fns: &HashMap<Arc<str>, SortingFnDef<TData>>,
569) -> Option<ResolvedSortCmp<TData>> {
570    if let Some(cmp) = column.sort_cmp.as_ref() {
571        return Some(ResolvedSortCmp::Cmp(cmp.clone()));
572    }
573
574    let spec = column.sorting_fn.as_ref()?;
575
576    match spec {
577        SortingFnSpec::Auto => {
578            let get_value = column.sort_value.as_ref()?.clone();
579            let builtin = auto_sorting_fn_builtin(row_model, column);
580            Some(ResolvedSortCmp::BuiltIn {
581                kind: builtin,
582                get_value,
583            })
584        }
585        SortingFnSpec::BuiltIn(builtin) => {
586            let get_value = column.sort_value.as_ref()?.clone();
587            Some(ResolvedSortCmp::BuiltIn {
588                kind: *builtin,
589                get_value,
590            })
591        }
592        SortingFnSpec::Named(key) => {
593            if let Some(def) = sorting_fns.get(key.as_ref()) {
594                return Some(match def {
595                    SortingFnDef::Cmp(cmp) => ResolvedSortCmp::Cmp(cmp.clone()),
596                    SortingFnDef::BuiltIn(builtin) => ResolvedSortCmp::BuiltIn {
597                        kind: *builtin,
598                        get_value: column.sort_value.as_ref()?.clone(),
599                    },
600                });
601            }
602
603            let builtin = builtin_sorting_fn_key(key.as_ref())?;
604            let get_value = column.sort_value.as_ref()?.clone();
605            Some(ResolvedSortCmp::BuiltIn {
606                kind: builtin,
607                get_value,
608            })
609        }
610    }
611}
612
613pub fn sort_row_model<'a, TData>(
614    row_model: &RowModel<'a, TData>,
615    columns: &[ColumnDef<TData>],
616    sorting: &[SortSpec],
617    sorting_fns: &HashMap<Arc<str>, SortingFnDef<TData>>,
618) -> RowModel<'a, TData> {
619    if sorting.is_empty() || row_model.root_rows().is_empty() {
620        return row_model.clone();
621    }
622
623    #[derive(Clone)]
624    struct ResolvedSortColumn<TData> {
625        cmp: ResolvedSortCmp<TData>,
626        invert_sorting: bool,
627        sort_undefined: Option<SortUndefined>,
628        sort_is_undefined: Option<ResolvedIsUndefined<TData>>,
629    }
630
631    let sort_cfg_by_id: HashMap<&str, ResolvedSortColumn<TData>> = columns
632        .iter()
633        .filter_map(|c| {
634            let cmp = resolve_sort_cmp_for_column(row_model, c, sorting_fns)?;
635
636            // TanStack default: `sortUndefined: 1` (undefined values are last).
637            //
638            // We can only emulate this default when the column provides a TanStack-like
639            // `getValue` surface (`sort_value_by`), so we can detect `undefined`.
640            let sort_undefined = c.sort_undefined.or_else(|| {
641                if c.sort_value.is_some() {
642                    Some(SortUndefined::Dir(1))
643                } else {
644                    None
645                }
646            });
647
648            let sort_is_undefined = c
649                .sort_is_undefined
650                .clone()
651                .map(ResolvedIsUndefined::Fn)
652                .or_else(|| {
653                    if sort_undefined.is_some() {
654                        c.sort_value.clone().map(ResolvedIsUndefined::Value)
655                    } else {
656                        None
657                    }
658                });
659
660            Some((
661                c.id.as_ref(),
662                ResolvedSortColumn {
663                    cmp,
664                    invert_sorting: c.invert_sorting,
665                    sort_undefined,
666                    sort_is_undefined,
667                },
668            ))
669        })
670        .collect();
671
672    let mut out = row_model.clone();
673
674    fn tiebreaker(a: RowKey, b: RowKey) -> Ordering {
675        a.cmp(&b)
676    }
677
678    fn cmp_rows<TData>(
679        arena: &[super::Row<'_, TData>],
680        sort_cfg_by_id: &HashMap<&str, ResolvedSortColumn<TData>>,
681        sorting: &[SortSpec],
682        a: RowIndex,
683        b: RowIndex,
684    ) -> Ordering {
685        let Some(a_row) = arena.get(a) else {
686            return Ordering::Equal;
687        };
688        let Some(b_row) = arena.get(b) else {
689            return Ordering::Equal;
690        };
691
692        for spec in sorting {
693            let Some(cfg) = sort_cfg_by_id.get(spec.column.as_ref()) else {
694                continue;
695            };
696
697            let mut ord = Ordering::Equal;
698            if let Some(sort_undefined) = cfg.sort_undefined
699                && !matches!(sort_undefined, SortUndefined::Disabled)
700                && let Some(is_undefined) = cfg.sort_is_undefined.as_ref()
701            {
702                let a_undefined = match is_undefined {
703                    ResolvedIsUndefined::Fn(f) => f(a_row.original),
704                    ResolvedIsUndefined::Value(get_value) => {
705                        matches!((get_value)(a_row.original), TanStackValue::Undefined)
706                    }
707                };
708                let b_undefined = match is_undefined {
709                    ResolvedIsUndefined::Fn(f) => f(b_row.original),
710                    ResolvedIsUndefined::Value(get_value) => {
711                        matches!((get_value)(b_row.original), TanStackValue::Undefined)
712                    }
713                };
714
715                if a_undefined || b_undefined {
716                    ord = match sort_undefined {
717                        SortUndefined::First => {
718                            if a_undefined == b_undefined {
719                                Ordering::Equal
720                            } else if a_undefined {
721                                Ordering::Less
722                            } else {
723                                Ordering::Greater
724                            }
725                        }
726                        SortUndefined::Last => {
727                            if a_undefined == b_undefined {
728                                Ordering::Equal
729                            } else if a_undefined {
730                                Ordering::Greater
731                            } else {
732                                Ordering::Less
733                            }
734                        }
735                        SortUndefined::Dir(dir) => {
736                            if a_undefined == b_undefined {
737                                Ordering::Equal
738                            } else if a_undefined {
739                                if dir < 0 {
740                                    Ordering::Less
741                                } else {
742                                    Ordering::Greater
743                                }
744                            } else if dir < 0 {
745                                Ordering::Greater
746                            } else {
747                                Ordering::Less
748                            }
749                        }
750                        SortUndefined::Disabled => Ordering::Equal,
751                    };
752
753                    // TanStack: `first`/`last` returns early (ignores desc/invert multipliers).
754                    if matches!(sort_undefined, SortUndefined::First | SortUndefined::Last)
755                        && ord != Ordering::Equal
756                    {
757                        return ord;
758                    }
759                }
760            }
761
762            if ord == Ordering::Equal {
763                ord = match &cfg.cmp {
764                    ResolvedSortCmp::Cmp(cmp) => cmp(a_row.original, b_row.original),
765                    ResolvedSortCmp::BuiltIn { kind, get_value } => {
766                        compare_builtin_sort(*kind, get_value, a_row.original, b_row.original)
767                    }
768                };
769            }
770
771            if ord != Ordering::Equal {
772                if spec.desc {
773                    ord = ord.reverse();
774                }
775                if cfg.invert_sorting {
776                    ord = ord.reverse();
777                }
778                return ord;
779            }
780        }
781
782        tiebreaker(a_row.key, b_row.key)
783    }
784
785    fn sort_children<TData>(
786        row_model: &mut RowModel<'_, TData>,
787        sort_cfg_by_id: &HashMap<&str, ResolvedSortColumn<TData>>,
788        sorting: &[SortSpec],
789        row: RowIndex,
790    ) {
791        let Some(children) = row_model.row(row).map(|r| r.sub_rows.clone()) else {
792            return;
793        };
794        let arena = row_model.arena.as_slice();
795        let mut sorted = children;
796        sorted.sort_by(|&a, &b| cmp_rows(arena, sort_cfg_by_id, sorting, a, b));
797
798        if let Some(r) = row_model.arena.get_mut(row) {
799            r.sub_rows = sorted.clone();
800        }
801
802        for child in sorted {
803            sort_children(row_model, sort_cfg_by_id, sorting, child);
804        }
805    }
806
807    let arena = out.arena.as_slice();
808    out.root_rows
809        .sort_by(|&a, &b| cmp_rows(arena, &sort_cfg_by_id, sorting, a, b));
810    let roots = out.root_rows.clone();
811    for root in roots {
812        sort_children(&mut out, &sort_cfg_by_id, sorting, root);
813    }
814
815    out.flat_rows.clear();
816    fn push_flat<TData>(row_model: &mut RowModel<'_, TData>, row: RowIndex) {
817        row_model.flat_rows.push(row);
818        let Some(r) = row_model.row(row) else {
819            return;
820        };
821        let children = r.sub_rows.clone();
822        for child in children {
823            push_flat(row_model, child);
824        }
825    }
826    let roots = out.root_rows.clone();
827    for root in roots {
828        push_flat(&mut out, root);
829    }
830
831    out
832}
833
834#[cfg(test)]
835mod tests {
836    use super::*;
837    use crate::table::{Table, TableOptions, create_column_helper};
838
839    #[derive(Debug, Clone)]
840    struct Item {
841        value: i32,
842    }
843
844    #[test]
845    fn sort_row_model_sorts_root_rows_by_spec() {
846        let data = vec![Item { value: 2 }, Item { value: 1 }, Item { value: 3 }];
847        let table = Table::builder(&data).build();
848        let core = table.core_row_model();
849
850        let helper = create_column_helper::<Item>();
851        let columns = vec![helper.accessor("value", |it| it.value)];
852        let sorting = vec![SortSpec {
853            column: "value".into(),
854            desc: false,
855        }];
856
857        let sorting_fns = HashMap::new();
858        let sorted = sort_row_model(core, &columns, &sorting, &sorting_fns);
859        let ids = sorted
860            .root_rows()
861            .iter()
862            .filter_map(|&i| sorted.row(i).map(|r| r.key.0))
863            .collect::<Vec<_>>();
864
865        assert_eq!(ids, vec![1, 0, 2]);
866    }
867
868    #[test]
869    fn sort_row_model_respects_descending() {
870        let data = vec![Item { value: 2 }, Item { value: 1 }, Item { value: 3 }];
871        let table = Table::builder(&data).build();
872        let core = table.core_row_model();
873
874        let helper = create_column_helper::<Item>();
875        let columns = vec![helper.accessor("value", |it| it.value)];
876        let sorting = vec![SortSpec {
877            column: "value".into(),
878            desc: true,
879        }];
880
881        let sorting_fns = HashMap::new();
882        let sorted = sort_row_model(core, &columns, &sorting, &sorting_fns);
883        let ids = sorted
884            .root_rows()
885            .iter()
886            .filter_map(|&i| sorted.row(i).map(|r| r.key.0))
887            .collect::<Vec<_>>();
888
889        assert_eq!(ids, vec![2, 0, 1]);
890    }
891
892    #[test]
893    fn sort_row_model_uses_row_key_tiebreaker_for_determinism() {
894        let data = vec![Item { value: 1 }, Item { value: 1 }, Item { value: 1 }];
895        let table = Table::builder(&data).build();
896        let core = table.core_row_model();
897
898        let helper = create_column_helper::<Item>();
899        let columns = vec![helper.accessor("value", |it| it.value)];
900        let sorting = vec![SortSpec {
901            column: "value".into(),
902            desc: false,
903        }];
904
905        let sorting_fns = HashMap::new();
906        let sorted = sort_row_model(core, &columns, &sorting, &sorting_fns);
907        let ids = sorted
908            .root_rows()
909            .iter()
910            .filter_map(|&i| sorted.row(i).map(|r| r.key.0))
911            .collect::<Vec<_>>();
912
913        assert_eq!(ids, vec![0, 1, 2]);
914    }
915
916    #[test]
917    fn sort_row_model_inverts_sorting_when_column_requests_it() {
918        let data = vec![Item { value: 2 }, Item { value: 1 }, Item { value: 3 }];
919        let table = Table::builder(&data).build();
920        let core = table.core_row_model();
921
922        let helper = create_column_helper::<Item>();
923        let columns = vec![helper.accessor("value", |it| it.value).invert_sorting(true)];
924        let sorting = vec![SortSpec {
925            column: "value".into(),
926            desc: false,
927        }];
928
929        let sorting_fns = HashMap::new();
930        let sorted = sort_row_model(core, &columns, &sorting, &sorting_fns);
931        let ids = sorted
932            .root_rows()
933            .iter()
934            .filter_map(|&i| sorted.row(i).map(|r| r.key.0))
935            .collect::<Vec<_>>();
936
937        assert_eq!(ids, vec![2, 0, 1]);
938    }
939
940    #[test]
941    fn sort_row_model_invert_sorting_composes_with_descending() {
942        let data = vec![Item { value: 2 }, Item { value: 1 }, Item { value: 3 }];
943        let table = Table::builder(&data).build();
944        let core = table.core_row_model();
945
946        let helper = create_column_helper::<Item>();
947        let columns = vec![helper.accessor("value", |it| it.value).invert_sorting(true)];
948        let sorting = vec![SortSpec {
949            column: "value".into(),
950            desc: true,
951        }];
952
953        let sorting_fns = HashMap::new();
954        let sorted = sort_row_model(core, &columns, &sorting, &sorting_fns);
955        let ids = sorted
956            .root_rows()
957            .iter()
958            .filter_map(|&i| sorted.row(i).map(|r| r.key.0))
959            .collect::<Vec<_>>();
960
961        assert_eq!(ids, vec![1, 0, 2]);
962    }
963
964    #[test]
965    fn toggle_sort_for_column_cycles_and_resets_when_single() {
966        let mut sorting: SortingState = Vec::new();
967
968        toggle_sort_for_column(&mut sorting, "a".into(), false);
969        assert_eq!(sorting.len(), 1);
970        assert_eq!(sorting[0].column.as_ref(), "a");
971        assert!(!sorting[0].desc);
972
973        toggle_sort_for_column(&mut sorting, "a".into(), false);
974        assert_eq!(sorting.len(), 1);
975        assert!(sorting[0].desc);
976
977        toggle_sort_for_column(&mut sorting, "a".into(), false);
978        assert!(sorting.is_empty());
979
980        toggle_sort_for_column(&mut sorting, "b".into(), false);
981        assert_eq!(sorting.len(), 1);
982        assert_eq!(sorting[0].column.as_ref(), "b");
983    }
984
985    #[test]
986    fn toggle_sorting_tanstack_respects_sort_desc_first() {
987        let mut sorting: SortingState = Vec::new();
988
989        let helper = create_column_helper::<Item>();
990        let column = helper
991            .accessor("value", |it| it.value)
992            .sort_desc_first(true);
993
994        toggle_sorting_tanstack(&mut sorting, &column, TableOptions::default(), false, false);
995        assert_eq!(sorting.len(), 1);
996        assert_eq!(sorting[0].column.as_ref(), "value");
997        assert!(sorting[0].desc);
998
999        toggle_sorting_tanstack(&mut sorting, &column, TableOptions::default(), false, false);
1000        assert_eq!(sorting.len(), 1);
1001        assert!(!sorting[0].desc);
1002
1003        toggle_sorting_tanstack(&mut sorting, &column, TableOptions::default(), false, false);
1004        assert!(sorting.is_empty());
1005    }
1006}