dioxus_sortable/use_sorter.rs
1use dioxus::prelude::*;
2use std::cmp::Ordering;
3
4/// Stores Dioxus hooks and state of our sortable items.
5#[derive(Copy, Clone, Debug, PartialEq)]
6pub struct UseSorter<'a, F: 'static> {
7 field: &'a UseState<F>,
8 direction: &'a UseState<Direction>,
9}
10
11/// Trait used by [UseSorter](UseSorter) to sort a struct by a specific field. This must be implemented on the field enum. Type `T` represents the struct (table row) that is being sorted.
12///
13/// The implementation should use the [`PartialOrd::partial_cmp`] trait to compare the field values and return the result. For example:
14/// ```rust
15/// # use dioxus_sortable::PartialOrdBy;
16/// # #[derive(PartialEq)]
17/// struct MyStruct {
18/// first: String,
19/// second: f64, // <- Note: can return None if f64::NAN
20/// }
21///
22/// # #[derive(Copy, Clone, Debug, PartialEq)]
23/// enum MyStructField {
24/// First,
25/// Second,
26/// }
27///
28/// impl PartialOrdBy<MyStruct> for MyStructField {
29/// fn partial_cmp_by(&self, a: &MyStruct, b: &MyStruct) -> Option<std::cmp::Ordering> {
30/// match self {
31/// MyStructField::First => a.first.partial_cmp(&b.first),
32/// MyStructField::Second => a.second.partial_cmp(&b.second),
33/// }
34/// }
35/// }
36/// ```
37///
38/// Be careful when using [`Option::None`] or a custom enum to represent missing data (`NULL` values). As `partial_cmp` as `None` is less than `Some`:
39///
40/// ```rust
41/// # use std::cmp::Ordering;
42/// assert_eq!(Ordering::Less, None.cmp(&Some(0)));
43/// ```
44///
45pub trait PartialOrdBy<T>: PartialEq {
46 /// Compare two values of type `T` by the field's enum. Return values of `None` are treated as `NULL` values. See [`Sortable`] for more information.
47 ///
48 /// Be careful when comparing types like `Option` which implement `Ord`. This means that `None` and `Some` have an order where we might use them as unknown / `NULL` values. This can be a surprise.
49 ///
50 /// Another issue is `f64` only implements `PartialOrd` and not `Ord` because a value can hold `f64::NAN`. In this situation `partial_cmp` will return `None` and we'll treat these values as `NULL` as expected.
51 fn partial_cmp_by(&self, a: &T, b: &T) -> Option<Ordering>;
52}
53
54/// Trait used to describe how a field can be sorted. This must be implemented on the field enum.
55///
56/// Our [`PartialOrdBy`] fn may result in `None` values which we refer to as `NULL`. We borrow from SQL here to handle these values in a similar way to the [SQL ORDER BY clause](https://www.postgresql.org/docs/current/sql-select.html#SQL-ORDERBY). The PostgreSQL general form is `ORDER BY expression [ ASC | DESC | USING operator ] [ NULLS { FIRST | LAST } ] [, ...]` where:
57/// - `expression` is the field being sorted.
58/// - `ASC` and `DESC` are the sort [`Direction`].
59/// - `USING operator` is implied by [`PartialOrdBy`].
60/// - `NULLS { FIRST | LAST }` corresponds to [`NullHandling`].
61/// Meaning you can sort by ascending or descending and optionally specify `NULL` ordering.
62pub trait Sortable: PartialEq {
63 /// Describes how this field can be sorted.
64 fn sort_by(&self) -> Option<SortBy>;
65
66 /// Describes how `NULL` values (when [`PartialOrdBy`] returns `None`) should be ordered when sorting. Either all at the start or the end.
67 ///
68 /// Provided implementation relies on the default (all at the end) and should be overridden if you want to change this generally or on a per-field basis.
69 fn null_handling(&self) -> NullHandling {
70 NullHandling::default()
71 }
72}
73
74/// Describes how a field should be sorted. Returned by [`Sortable::sort_by`].
75#[derive(Copy, Clone, Debug, PartialEq)]
76pub enum SortBy {
77 /// This field is limited to being sorted in the one direction specified.
78 Fixed(Direction),
79 /// This field can be sorted in either direction. The direction specifies the initial direction. Fields of this sort can be toggled between directions.
80 Reversible(Direction),
81}
82
83/// Sort direction. Does not have a default -- implied by the field via [`SortBy`].
84///
85/// Actual sorting is done by [`PartialOrdBy`].
86#[derive(Copy, Clone, Debug, PartialEq)]
87pub enum Direction {
88 /// Ascending sort. A-Z, 0-9, little to big, etc.
89 Ascending,
90 /// Descending sort. Z-A, opposite of ascending.
91 Descending,
92}
93
94impl Direction {
95 /// Inverts the direction.
96 pub fn invert(&self) -> Self {
97 match self {
98 Self::Ascending => Self::Descending,
99 Self::Descending => Self::Ascending,
100 }
101 }
102
103 fn from_field<F: Sortable>(field: &F) -> Direction {
104 field.sort_by().unwrap_or_default().direction()
105 }
106}
107
108/// Describes how `NULL` values should be ordered when sorting. We refer to `None` values returned from [`PartialOrdBy::partial_cmp_by`] as `NULL`. Warning: Rust's `Option::None` is not strictly equivalent to SQL's `NULL` but we borrow from SQL terminology to handle them.
109#[derive(Copy, Clone, Debug, Default, PartialEq)]
110pub enum NullHandling {
111 /// Places all `NULL` values first.
112 First,
113 /// Places all `NULL` values last. The default.
114 #[default]
115 Last,
116}
117
118impl Default for SortBy {
119 fn default() -> SortBy {
120 Self::increasing_or_decreasing().unwrap()
121 }
122}
123
124impl SortBy {
125 /// Field may not be sorted. Convenience fn for specifying how a field may be sorted.
126 pub fn unsortable() -> Option<Self> {
127 None
128 }
129 /// Field may only be sorted in ascending order.
130 pub fn increasing() -> Option<Self> {
131 Some(Self::Fixed(Direction::Ascending))
132 }
133 /// Field may only be sorted in descending order.
134 pub fn decreasing() -> Option<Self> {
135 Some(Self::Fixed(Direction::Descending))
136 }
137 /// Field may be sorted in either direction. The initial direction is ascending. This is the default.
138 pub fn increasing_or_decreasing() -> Option<Self> {
139 Some(Self::Reversible(Direction::Ascending))
140 }
141 /// Field may be sorted in either direction. The initial direction is descending.
142 pub fn decreasing_or_increasing() -> Option<Self> {
143 Some(Self::Reversible(Direction::Descending))
144 }
145
146 /// Returns the initial / implied direction of the sort.
147 pub fn direction(&self) -> Direction {
148 match self {
149 Self::Fixed(dir) => *dir,
150 Self::Reversible(dir) => *dir,
151 }
152 }
153
154 fn ensure_direction(&self, dir: Direction) -> Direction {
155 use SortBy::*;
156 match self {
157 // Must match allowed
158 Fixed(allowed) if *allowed == dir => dir,
159 // Did not match allowed
160 Fixed(allowed) => *allowed,
161 // Any allowed
162 Reversible(_) => dir,
163 }
164 }
165}
166
167/// Builder for [UseSorter](UseSorter). Use this to specify the field and direction of the sorter. For example by passing sort state from URL parameters.
168///
169/// Ordering of [`Self::with_field`] and [`Self::with_direction`] matters as the builder will ignore invalid combinations specified by the field's [`Sortable`]. This is to prevent the user from specifying a direction that is not allowed by the field.
170#[derive(Copy, Clone, Debug, PartialEq)]
171pub struct UseSorterBuilder<F> {
172 field: F,
173 direction: Direction,
174}
175
176impl<F: Default + Sortable> Default for UseSorterBuilder<F> {
177 fn default() -> Self {
178 let field = F::default();
179 let direction = Direction::from_field(&field);
180 Self { field, direction }
181 }
182}
183
184impl<F: Copy + Default + Sortable> UseSorterBuilder<F> {
185 /// Optionally sets the initial field to sort by.
186 pub fn with_field(&self, field: F) -> Self {
187 Self { field, ..*self }
188 }
189
190 /// Optionally sets the initial direction to sort by.[`Direction::Ascending`] can be set.
191 pub fn with_direction(&self, direction: Direction) -> Self {
192 Self { direction, ..*self }
193 }
194
195 /// Creates Dioxus hooks to manage state. Must follow Dioxus hook rules and be called unconditionally in the same order as other hooks. See [use_sorter()] for simple usage.
196 ///
197 /// This fn (or [`Self::use_sorter`]) *must* be called or never used. See the docs on [`UseSorter::sort`] on using conditions.
198 ///
199 /// If the field or direction has not been set then the default values will be used.
200 pub fn use_sorter(self, cx: &ScopeState) -> UseSorter<F> {
201 let sorter = use_sorter(cx);
202 sorter.set_field(self.field, self.direction);
203 sorter
204 }
205}
206
207/// Creates Dioxus hooks to manage state. Must follow Dioxus hook rules and be called unconditionally in the same order as other hooks. See [UseSorterBuilder](UseSorterBuilder) for more advanced usage.
208///
209/// This fn (or [`UseSorterBuilder::use_sorter`]) *must* be called or never used. See the docs on [`UseSorter::sort`] on using conditions.
210///
211/// Relies on `F::default()` for the initial value.
212pub fn use_sorter<F: Copy + Default + Sortable>(cx: &ScopeState) -> UseSorter<'_, F> {
213 let field = F::default();
214 UseSorter {
215 field: use_state(cx, || field),
216 direction: use_state(cx, || Direction::from_field(&field)),
217 }
218}
219
220impl<'a, F> UseSorter<'a, F> {
221 /// Returns the current field and direction. Can be used to recreate state with [UseSorterBuilder](UseSorterBuilder).
222 pub fn get_state(&self) -> (&F, &Direction) {
223 (self.field.get(), self.direction.get())
224 }
225
226 /// Sets the sort field and toggles the direction (if applicable). Ignores unsortable fields.
227 pub fn toggle_field(&self, field: F)
228 where
229 F: Sortable,
230 {
231 match field.sort_by() {
232 None => (), // Do nothing, don't switch to unsortable
233 Some(sort_by) => {
234 use SortBy::*;
235 match sort_by {
236 Fixed(dir) => self.direction.set(dir),
237 Reversible(dir) => {
238 // Invert direction if the same field
239 let dir = if *self.field.get() == field {
240 self.direction.get().invert()
241 } else {
242 // Reset state to new field
243 dir
244 };
245 self.direction.set(dir);
246 }
247 }
248 self.field.set(field);
249 }
250 }
251 }
252
253 /// Sets the sort field and direction state directly. Ignores unsortable fields. Ignores the direction if not valid for a field.
254 pub fn set_field(&self, field: F, dir: Direction)
255 where
256 F: Sortable,
257 {
258 match field.sort_by() {
259 None => (), // Do nothing, ignore unsortable
260 Some(sort_by) => {
261 // Set state but ensure direction is valid
262 let dir = sort_by.ensure_direction(dir);
263 self.field.set(field);
264 self.direction.set(dir);
265 }
266 }
267 }
268
269 /// Sorts items according to the current field and direction.
270 ///
271 /// This is not a hook and may be called conditionally. For example:
272 /// - If data is coming from a `use_future` then you can call this fn once it has completed.
273 /// - If you need to apply a filter, do so before calling this fn.
274 pub fn sort<T>(&self, items: &mut [T])
275 where
276 F: PartialOrdBy<T> + Sortable,
277 {
278 let (field, dir) = self.get_state();
279 sort_by(field, *dir, field.null_handling(), items);
280 }
281}
282
283fn sort_by<T, F: PartialOrdBy<T>>(
284 sort_by: &F,
285 dir: Direction,
286 nulls: NullHandling,
287 items: &mut [T],
288) {
289 items.sort_by(|a, b| {
290 let partial = sort_by.partial_cmp_by(a, b);
291 partial.map_or_else(
292 || {
293 let a_is_null = sort_by.partial_cmp_by(a, a).is_none();
294 let b_is_null = sort_by.partial_cmp_by(b, b).is_none();
295 match (a_is_null, b_is_null) {
296 (true, true) => Ordering::Equal,
297 (true, false) => match nulls {
298 NullHandling::First => Ordering::Less,
299 NullHandling::Last => Ordering::Greater,
300 },
301 (false, true) => match nulls {
302 NullHandling::First => Ordering::Greater,
303 NullHandling::Last => Ordering::Less,
304 },
305 // Uh-oh, first partial_cmp_by should not have returned None
306 (false, false) => unreachable!(),
307 }
308 },
309 // Reversal must be applied per item to avoid ordering NULLs
310 |o| match dir {
311 Direction::Ascending => o,
312 Direction::Descending => o.reverse(),
313 },
314 )
315 });
316}
317
318#[cfg(test)]
319mod tests {
320 use super::*;
321
322 #[derive(Clone, Debug, Default, PartialEq)]
323 struct Row(f64);
324
325 #[derive(Copy, Clone, Debug, Default, PartialEq)]
326 enum RowField {
327 #[default]
328 Value,
329 }
330
331 impl PartialOrdBy<Row> for RowField {
332 fn partial_cmp_by(&self, a: &Row, b: &Row) -> Option<Ordering> {
333 match self {
334 Self::Value => a.0.partial_cmp(&b.0),
335 }
336 }
337 }
338
339 #[test]
340 fn test_sort_by() {
341 use Direction::*;
342 use NullHandling::*;
343 use RowField::*;
344
345 // Ascending
346 let mut rows = vec![Row(2.0), Row(1.0), Row(3.0)];
347 sort_by(&Value, Ascending, First, rows.as_mut_slice());
348 assert_eq!(rows, vec![Row(1.0), Row(2.0), Row(3.0)]);
349 // Descending
350 sort_by(&Value, Descending, First, rows.as_mut_slice());
351 assert_eq!(rows, vec![Row(3.0), Row(2.0), Row(1.0)]);
352
353 // Nulls last, ascending
354 let mut rows = vec![Row(f64::NAN), Row(f64::NAN), Row(2.0), Row(1.0), Row(3.0)];
355 sort_by(&Value, Ascending, Last, rows.as_mut_slice());
356 assert_eq!(rows[0], Row(1.0));
357 assert_eq!(rows[1], Row(2.0));
358 assert_eq!(rows[2], Row(3.0));
359 assert!(rows[3].0.is_nan());
360 assert!(rows[4].0.is_nan());
361 // Nulls first, ascending
362 sort_by(&Value, Ascending, First, rows.as_mut_slice());
363 assert!(rows[0].0.is_nan());
364 assert!(rows[1].0.is_nan());
365 assert_eq!(rows[2], Row(1.0));
366 assert_eq!(rows[3], Row(2.0));
367 assert_eq!(rows[4], Row(3.0));
368
369 // Nulls last, descending
370 sort_by(&Value, Descending, Last, rows.as_mut_slice());
371 assert_eq!(rows[0], Row(3.0));
372 assert_eq!(rows[1], Row(2.0));
373 assert_eq!(rows[2], Row(1.0));
374 assert!(rows[3].0.is_nan());
375 assert!(rows[4].0.is_nan());
376 // Nulls first, descending
377 sort_by(&Value, Descending, First, rows.as_mut_slice());
378 assert!(rows[0].0.is_nan());
379 assert!(rows[1].0.is_nan());
380 assert_eq!(rows[2], Row(3.0));
381 assert_eq!(rows[3], Row(2.0));
382 assert_eq!(rows[4], Row(1.0));
383 }
384}