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