Skip to main content

fret_ui_headless/table/
column.rs

1use std::cmp::Ordering;
2use std::sync::Arc;
3
4use serde_json::Value;
5
6use super::{Aggregation, AggregationFnSpec, BuiltInAggregationFn};
7
8pub type ColumnId = Arc<str>;
9
10pub type SortCmpFn<TData> = Arc<dyn Fn(&TData, &TData) -> Ordering>;
11pub type SortIsUndefinedFn<TData> = Arc<dyn Fn(&TData) -> bool>;
12pub type SortValueFn<TData> = Arc<dyn Fn(&TData) -> TanStackValue>;
13pub type UniqueValuesFn<TData> = Arc<dyn Fn(&TData, usize) -> Vec<TanStackValue>>;
14pub type FilterFn<TData> = Arc<dyn Fn(&TData, &Value) -> bool>;
15pub type FilterFnWithMeta<TData> = Arc<dyn Fn(&TData, &Value, &mut dyn FnMut(Value)) -> bool>;
16pub type FacetKeyFn<TData> = Arc<dyn Fn(&TData) -> u64>;
17pub type FacetStrFn<TData> = Arc<dyn for<'r> Fn(&'r TData) -> &'r str>;
18pub type ValueU64Fn<TData> = Arc<dyn Fn(&TData) -> u64>;
19
20/// A TanStack-like “cell value” representation used by built-in sorting functions.
21///
22/// This exists because TanStack Table’s built-in sorting functions operate over untyped JS
23/// values (including `undefined`). In Rust, we need a stable representation to express those
24/// behaviors.
25#[derive(Debug, Clone, PartialEq)]
26pub enum TanStackValue {
27    Undefined,
28    Null,
29    Bool(bool),
30    Number(f64),
31    String(Arc<str>),
32    Array(Vec<TanStackValue>),
33    /// Stored as milliseconds since epoch (JS `Date.valueOf()`).
34    DateTime(f64),
35}
36
37impl From<bool> for TanStackValue {
38    fn from(value: bool) -> Self {
39        TanStackValue::Bool(value)
40    }
41}
42
43impl From<f32> for TanStackValue {
44    fn from(value: f32) -> Self {
45        TanStackValue::Number(value as f64)
46    }
47}
48
49impl From<f64> for TanStackValue {
50    fn from(value: f64) -> Self {
51        TanStackValue::Number(value)
52    }
53}
54
55impl From<i32> for TanStackValue {
56    fn from(value: i32) -> Self {
57        TanStackValue::Number(value as f64)
58    }
59}
60
61impl From<i64> for TanStackValue {
62    fn from(value: i64) -> Self {
63        TanStackValue::Number(value as f64)
64    }
65}
66
67impl From<u32> for TanStackValue {
68    fn from(value: u32) -> Self {
69        TanStackValue::Number(value as f64)
70    }
71}
72
73impl From<u64> for TanStackValue {
74    fn from(value: u64) -> Self {
75        TanStackValue::Number(value as f64)
76    }
77}
78
79impl From<usize> for TanStackValue {
80    fn from(value: usize) -> Self {
81        TanStackValue::Number(value as f64)
82    }
83}
84
85impl From<String> for TanStackValue {
86    fn from(value: String) -> Self {
87        TanStackValue::String(Arc::<str>::from(value))
88    }
89}
90
91impl From<Arc<str>> for TanStackValue {
92    fn from(value: Arc<str>) -> Self {
93        TanStackValue::String(value)
94    }
95}
96
97impl From<&str> for TanStackValue {
98    fn from(value: &str) -> Self {
99        TanStackValue::String(Arc::<str>::from(value))
100    }
101}
102
103impl From<Vec<TanStackValue>> for TanStackValue {
104    fn from(value: Vec<TanStackValue>) -> Self {
105        TanStackValue::Array(value)
106    }
107}
108
109#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
110pub enum BuiltInSortingFn {
111    Alphanumeric,
112    AlphanumericCaseSensitive,
113    Text,
114    TextCaseSensitive,
115    Datetime,
116    Basic,
117}
118
119#[derive(Debug, Clone, PartialEq, Eq, Hash)]
120pub enum SortingFnSpec {
121    /// TanStack `sortingFn: 'auto'`.
122    Auto,
123    /// TanStack built-in sorting fn key.
124    BuiltIn(BuiltInSortingFn),
125    /// TanStack `sortingFn: <string>` resolved via `options.sortingFns[key] ?? builtIn[key]`.
126    Named(Arc<str>),
127}
128
129#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
130pub enum BuiltInFilterFn {
131    IncludesString,
132    IncludesStringSensitive,
133    EqualsString,
134    ArrIncludes,
135    ArrIncludesAll,
136    ArrIncludesSome,
137    Equals,
138    WeakEquals,
139    InNumberRange,
140}
141
142#[derive(Debug, Clone, PartialEq, Eq, Hash)]
143pub enum FilteringFnSpec {
144    /// TanStack `filterFn: 'auto'`.
145    Auto,
146    /// TanStack built-in filter fn key.
147    BuiltIn(BuiltInFilterFn),
148    /// TanStack `filterFn: <string>` resolved via `options.filterFns[key] ?? builtIn[key]`.
149    Named(Arc<str>),
150}
151
152#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
153pub enum SortUndefined {
154    /// TanStack `sortUndefined: false` (disable the pre-pass undefined ordering).
155    Disabled,
156    First,
157    Last,
158    /// `-1` or `1` in TanStack Table. Applied before `desc`/`invertSorting` multipliers.
159    Dir(i8),
160}
161
162pub struct ColumnDef<TData> {
163    pub id: ColumnId,
164    /// Child columns for TanStack-style grouped column definitions.
165    ///
166    /// When non-empty, this column is treated as a “group” column for header group generation.
167    pub columns: Vec<ColumnDef<TData>>,
168    pub sort_cmp: Option<SortCmpFn<TData>>,
169    pub sorting_fn: Option<SortingFnSpec>,
170    pub sort_value: Option<SortValueFn<TData>>,
171    pub sort_undefined: Option<SortUndefined>,
172    pub sort_is_undefined: Option<SortIsUndefinedFn<TData>>,
173    pub filtering_fn: Option<FilteringFnSpec>,
174    pub filter_fn: Option<FilterFn<TData>>,
175    pub filter_fn_with_meta: Option<FilterFnWithMeta<TData>>,
176    pub facet_key_fn: Option<FacetKeyFn<TData>>,
177    pub facet_str_fn: Option<FacetStrFn<TData>>,
178    pub value_u64_fn: Option<ValueU64Fn<TData>>,
179    pub unique_values_fn: Option<UniqueValuesFn<TData>>,
180    pub invert_sorting: bool,
181    pub sort_desc_first: Option<bool>,
182    pub enable_sorting: bool,
183    pub enable_multi_sort: bool,
184    pub enable_column_filter: bool,
185    pub enable_global_filter: bool,
186    pub aggregation: Aggregation,
187    pub aggregation_fn: AggregationFnSpec,
188    pub enable_hiding: bool,
189    pub enable_ordering: bool,
190    pub enable_pinning: bool,
191    pub enable_resizing: bool,
192    pub enable_grouping: bool,
193    pub size: f32,
194    pub min_size: f32,
195    pub max_size: f32,
196}
197
198impl<TData> Clone for ColumnDef<TData> {
199    fn clone(&self) -> Self {
200        Self {
201            id: self.id.clone(),
202            columns: self.columns.clone(),
203            sort_cmp: self.sort_cmp.clone(),
204            sorting_fn: self.sorting_fn.clone(),
205            sort_value: self.sort_value.clone(),
206            sort_undefined: self.sort_undefined,
207            sort_is_undefined: self.sort_is_undefined.clone(),
208            filtering_fn: self.filtering_fn.clone(),
209            filter_fn: self.filter_fn.clone(),
210            filter_fn_with_meta: self.filter_fn_with_meta.clone(),
211            facet_key_fn: self.facet_key_fn.clone(),
212            facet_str_fn: self.facet_str_fn.clone(),
213            value_u64_fn: self.value_u64_fn.clone(),
214            unique_values_fn: self.unique_values_fn.clone(),
215            invert_sorting: self.invert_sorting,
216            sort_desc_first: self.sort_desc_first,
217            enable_sorting: self.enable_sorting,
218            enable_multi_sort: self.enable_multi_sort,
219            enable_column_filter: self.enable_column_filter,
220            enable_global_filter: self.enable_global_filter,
221            aggregation: self.aggregation,
222            aggregation_fn: self.aggregation_fn.clone(),
223            enable_hiding: self.enable_hiding,
224            enable_ordering: self.enable_ordering,
225            enable_pinning: self.enable_pinning,
226            enable_resizing: self.enable_resizing,
227            enable_grouping: self.enable_grouping,
228            size: self.size,
229            min_size: self.min_size,
230            max_size: self.max_size,
231        }
232    }
233}
234
235impl<TData> std::fmt::Debug for ColumnDef<TData> {
236    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
237        f.debug_struct("ColumnDef")
238            .field("id", &self.id)
239            .finish_non_exhaustive()
240    }
241}
242
243impl<TData> ColumnDef<TData> {
244    pub fn new(id: impl Into<ColumnId>) -> Self {
245        Self {
246            id: id.into(),
247            columns: Vec::new(),
248            sort_cmp: None,
249            sorting_fn: None,
250            sort_value: None,
251            sort_undefined: None,
252            sort_is_undefined: None,
253            filtering_fn: None,
254            filter_fn: None,
255            filter_fn_with_meta: None,
256            facet_key_fn: None,
257            facet_str_fn: None,
258            value_u64_fn: None,
259            unique_values_fn: None,
260            invert_sorting: false,
261            sort_desc_first: None,
262            enable_sorting: true,
263            enable_multi_sort: true,
264            enable_column_filter: true,
265            enable_global_filter: true,
266            aggregation: Aggregation::None,
267            aggregation_fn: AggregationFnSpec::Auto,
268            enable_hiding: true,
269            enable_ordering: true,
270            enable_pinning: true,
271            enable_resizing: true,
272            enable_grouping: true,
273            size: 150.0,
274            min_size: 20.0,
275            max_size: f32::MAX,
276        }
277    }
278
279    pub fn sort_by(mut self, cmp: impl Fn(&TData, &TData) -> Ordering + 'static) -> Self {
280        self.sort_cmp = Some(Arc::new(cmp));
281        self
282    }
283
284    /// TanStack-aligned: configure `columns` for grouped column definitions.
285    pub fn columns(mut self, columns: Vec<ColumnDef<TData>>) -> Self {
286        self.columns = columns;
287        self
288    }
289
290    /// Provide a TanStack-like `getValue(columnId)` accessor for built-in sortingFn behaviors.
291    pub fn sort_value_by(mut self, get_value: impl Fn(&TData) -> TanStackValue + 'static) -> Self {
292        self.sort_value = Some(Arc::new(get_value));
293        self
294    }
295
296    /// TanStack-aligned: configure `columnDef.getUniqueValues` for `row.getUniqueValues(columnId)`.
297    pub fn unique_values_by(
298        mut self,
299        get_unique_values: impl Fn(&TData, usize) -> Vec<TanStackValue> + 'static,
300    ) -> Self {
301        self.unique_values_fn = Some(Arc::new(get_unique_values));
302        self
303    }
304
305    /// TanStack-aligned: configure `aggregationFn: 'auto'`.
306    pub fn aggregation_fn_auto(mut self) -> Self {
307        self.aggregation_fn = AggregationFnSpec::Auto;
308        self
309    }
310
311    /// TanStack-aligned: configure a built-in aggregation function key.
312    pub fn aggregation_fn_builtin(mut self, agg: BuiltInAggregationFn) -> Self {
313        self.aggregation_fn = AggregationFnSpec::BuiltIn(agg);
314        self
315    }
316
317    /// TanStack-aligned: configure `aggregationFn: <string>` resolved via `options.aggregationFns`.
318    pub fn aggregation_fn_named(mut self, key: impl Into<Arc<str>>) -> Self {
319        self.aggregation_fn = AggregationFnSpec::Named(key.into());
320        self
321    }
322
323    /// Disable aggregation for this column.
324    pub fn aggregation_fn_none(mut self) -> Self {
325        self.aggregation_fn = AggregationFnSpec::None;
326        self
327    }
328
329    /// TanStack-aligned: configure `sortingFn: 'auto'`.
330    pub fn sorting_fn_auto(mut self) -> Self {
331        self.sorting_fn = Some(SortingFnSpec::Auto);
332        self
333    }
334
335    /// TanStack-aligned: configure a built-in sorting function key.
336    pub fn sorting_fn_builtin(mut self, sorting_fn: BuiltInSortingFn) -> Self {
337        self.sorting_fn = Some(SortingFnSpec::BuiltIn(sorting_fn));
338        self
339    }
340
341    /// TanStack-aligned: configure `sortingFn: <string>` resolved via table options.
342    pub fn sorting_fn_named(mut self, key: impl Into<Arc<str>>) -> Self {
343        self.sorting_fn = Some(SortingFnSpec::Named(key.into()));
344        self
345    }
346
347    /// TanStack-aligned: configure `filterFn: 'auto'`.
348    pub fn filtering_fn_auto(mut self) -> Self {
349        self.filtering_fn = Some(FilteringFnSpec::Auto);
350        self
351    }
352
353    /// TanStack-aligned: configure a built-in filter function key.
354    pub fn filtering_fn_builtin(mut self, filter_fn: BuiltInFilterFn) -> Self {
355        self.filtering_fn = Some(FilteringFnSpec::BuiltIn(filter_fn));
356        self
357    }
358
359    /// TanStack-aligned: configure `filterFn: <string>` resolved via table options.
360    pub fn filtering_fn_named(mut self, key: impl Into<Arc<str>>) -> Self {
361        self.filtering_fn = Some(FilteringFnSpec::Named(key.into()));
362        self
363    }
364
365    /// TanStack-aligned: configure `sortUndefined` semantics for this column.
366    ///
367    /// `is_undefined` must match the column's `getValue(column_id) === undefined` behavior in
368    /// TanStack.
369    pub fn sort_undefined_by(
370        mut self,
371        sort_undefined: SortUndefined,
372        is_undefined: impl Fn(&TData) -> bool + 'static,
373    ) -> Self {
374        self.sort_undefined = Some(sort_undefined);
375        self.sort_is_undefined = Some(Arc::new(is_undefined));
376        self
377    }
378
379    /// TanStack-aligned: `sortUndefined: false` (disable undefined pre-pass ordering).
380    pub fn sort_undefined_disabled(mut self) -> Self {
381        self.sort_undefined = Some(SortUndefined::Disabled);
382        self.sort_is_undefined = None;
383        self
384    }
385
386    /// TanStack-aligned: invert the meaning of `asc` vs `desc` for this column.
387    ///
388    /// This mirrors `columnDef.invertSorting` in TanStack Table v8: after the base sorting
389    /// function yields an ordering, the result is inverted.
390    pub fn invert_sorting(mut self, invert: bool) -> Self {
391        self.invert_sorting = invert;
392        self
393    }
394
395    /// TanStack-aligned: start sort toggles in descending order for this column.
396    ///
397    /// This mirrors `columnDef.sortDescFirst` in TanStack Table v8.
398    pub fn sort_desc_first(mut self, enabled: bool) -> Self {
399        self.sort_desc_first = Some(enabled);
400        self
401    }
402
403    /// TanStack-aligned: enable/disable sorting for this column.
404    ///
405    /// This mirrors `columnDef.enableSorting` in TanStack Table v8.
406    pub fn enable_sorting(mut self, enabled: bool) -> Self {
407        self.enable_sorting = enabled;
408        self
409    }
410
411    /// TanStack-aligned: enable/disable multi-sort for this column.
412    ///
413    /// This mirrors `columnDef.enableMultiSort` in TanStack Table v8.
414    pub fn enable_multi_sort(mut self, enabled: bool) -> Self {
415        self.enable_multi_sort = enabled;
416        self
417    }
418
419    pub fn enable_column_filter(mut self, enabled: bool) -> Self {
420        self.enable_column_filter = enabled;
421        self
422    }
423
424    pub fn enable_global_filter(mut self, enabled: bool) -> Self {
425        self.enable_global_filter = enabled;
426        self
427    }
428
429    pub fn filter_by(mut self, f: impl Fn(&TData, &str) -> bool + 'static) -> Self {
430        let f = Arc::new(f);
431        self.filter_fn_with_meta = None;
432        self.filter_fn = Some(Arc::new(move |row, value| {
433            let Some(s) = value.as_str() else {
434                return false;
435            };
436            f(row, s)
437        }));
438        self
439    }
440
441    /// Configure a custom filterFn with TanStack-like `addMeta` support.
442    pub fn filter_by_with_meta(
443        mut self,
444        f: impl Fn(&TData, &Value, &mut dyn FnMut(Value)) -> bool + 'static,
445    ) -> Self {
446        self.filter_fn = None;
447        self.filter_fn_with_meta = Some(Arc::new(f));
448        self
449    }
450
451    /// Provide a stable `u64` facet key for this column (TanStack-aligned faceting, Rust-native).
452    pub fn facet_key_by(mut self, f: impl Fn(&TData) -> u64 + 'static) -> Self {
453        self.facet_key_fn = Some(Arc::new(f));
454        self
455    }
456
457    /// Provide a string view for this column's facet value (borrowed from row data; no allocation).
458    pub fn facet_str_by(mut self, f: impl for<'r> Fn(&'r TData) -> &'r str + 'static) -> Self {
459        self.facet_str_fn = Some(Arc::new(f));
460        self
461    }
462
463    /// Provide a stable numeric value for this column.
464    ///
465    /// This is the preferred input for numeric aggregation (and future numeric sorting/filtering).
466    /// It is intentionally separate from `facet_key_by`, which is reserved for grouping/faceting.
467    pub fn value_u64_by(mut self, f: impl Fn(&TData) -> u64 + 'static) -> Self {
468        self.value_u64_fn = Some(Arc::new(f));
469        self
470    }
471
472    pub fn aggregate(mut self, aggregation: Aggregation) -> Self {
473        self.aggregation = aggregation;
474        self
475    }
476
477    pub fn enable_hiding(mut self, enabled: bool) -> Self {
478        self.enable_hiding = enabled;
479        self
480    }
481
482    pub fn enable_ordering(mut self, enabled: bool) -> Self {
483        self.enable_ordering = enabled;
484        self
485    }
486
487    pub fn enable_pinning(mut self, enabled: bool) -> Self {
488        self.enable_pinning = enabled;
489        self
490    }
491
492    pub fn enable_resizing(mut self, enabled: bool) -> Self {
493        self.enable_resizing = enabled;
494        self
495    }
496
497    pub fn enable_grouping(mut self, enabled: bool) -> Self {
498        self.enable_grouping = enabled;
499        self
500    }
501
502    pub fn size(mut self, size: f32) -> Self {
503        self.size = size;
504        self
505    }
506
507    pub fn min_size(mut self, min_size: f32) -> Self {
508        self.min_size = min_size;
509        self
510    }
511
512    pub fn max_size(mut self, max_size: f32) -> Self {
513        self.max_size = max_size;
514        self
515    }
516}
517
518#[derive(Debug, Clone, Copy)]
519pub struct ColumnHelper<TData> {
520    _marker: std::marker::PhantomData<TData>,
521}
522
523pub fn create_column_helper<TData>() -> ColumnHelper<TData> {
524    ColumnHelper {
525        _marker: std::marker::PhantomData,
526    }
527}
528
529impl<TData> ColumnHelper<TData> {
530    pub fn accessor<V>(
531        self,
532        id: impl Into<ColumnId>,
533        accessor: impl Fn(&TData) -> V + 'static,
534    ) -> ColumnDef<TData>
535    where
536        V: Ord + Into<TanStackValue>,
537    {
538        let accessor = Arc::new(accessor);
539        let sort_accessor = accessor.clone();
540        let value_accessor = accessor.clone();
541        ColumnDef::new(id)
542            .sort_by(move |a, b| sort_accessor(a).cmp(&sort_accessor(b)))
543            .sort_value_by(move |row| value_accessor(row).into())
544    }
545
546    pub fn accessor_str(
547        self,
548        id: impl Into<ColumnId>,
549        accessor: impl for<'r> Fn(&'r TData) -> &'r str + 'static,
550    ) -> ColumnDef<TData>
551    where
552        TData: 'static,
553    {
554        let accessor: Arc<dyn for<'r> Fn(&'r TData) -> &'r str> = Arc::new(accessor);
555        let sort_accessor = accessor.clone();
556        let facet_accessor = accessor.clone();
557        let value_accessor = accessor.clone();
558        ColumnDef::new(id)
559            .sort_by(move |a, b| sort_accessor(a).cmp(sort_accessor(b)))
560            .sort_value_by(move |row| TanStackValue::String(Arc::<str>::from(value_accessor(row))))
561            .facet_str_by(move |row| facet_accessor(row))
562    }
563}