use dioxus::prelude::*;
use std::cmp::Ordering;
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct UseSorter<'a, F: 'static> {
field: &'a UseState<F>,
direction: &'a UseState<Direction>,
}
pub trait PartialOrdBy<T>: PartialEq {
fn partial_cmp_by(&self, a: &T, b: &T) -> Option<Ordering>;
}
pub trait Sortable: PartialEq {
fn sort_by(&self) -> Option<SortBy>;
fn null_handling(&self) -> NullHandling {
NullHandling::default()
}
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum SortBy {
Fixed(Direction),
Reversible(Direction),
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum Direction {
Ascending,
Descending,
}
impl 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()
}
}
#[derive(Copy, Clone, Debug, Default, PartialEq)]
pub enum NullHandling {
First,
#[default]
Last,
}
impl Default for SortBy {
fn default() -> SortBy {
Self::increasing_or_decreasing().unwrap()
}
}
impl SortBy {
pub fn unsortable() -> Option<Self> {
None
}
pub fn increasing() -> Option<Self> {
Some(Self::Fixed(Direction::Ascending))
}
pub fn decreasing() -> Option<Self> {
Some(Self::Fixed(Direction::Descending))
}
pub fn increasing_or_decreasing() -> Option<Self> {
Some(Self::Reversible(Direction::Ascending))
}
pub fn decreasing_or_increasing() -> Option<Self> {
Some(Self::Reversible(Direction::Descending))
}
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 {
Fixed(allowed) if *allowed == dir => dir,
Fixed(allowed) => *allowed,
Reversible(_) => dir,
}
}
}
#[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> {
pub fn with_field(&self, field: F) -> Self {
Self { field, ..*self }
}
pub fn with_direction(&self, direction: Direction) -> Self {
Self { direction, ..*self }
}
pub fn use_sorter(self, cx: &ScopeState) -> UseSorter<F> {
let sorter = use_sorter(cx);
sorter.set_field(self.field, self.direction);
sorter
}
}
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> {
pub fn get_state(&self) -> (&F, &Direction) {
(self.field.get(), self.direction.get())
}
pub fn toggle_field(&self, field: F)
where
F: Sortable,
{
match field.sort_by() {
None => (), Some(sort_by) => {
use SortBy::*;
match sort_by {
Fixed(dir) => self.direction.set(dir),
Reversible(dir) => {
let dir = if *self.field.get() == field {
self.direction.get().invert()
} else {
dir
};
self.direction.set(dir);
}
}
self.field.set(field);
}
}
}
pub fn set_field(&self, field: F, dir: Direction)
where
F: Sortable,
{
match field.sort_by() {
None => (), Some(sort_by) => {
let dir = sort_by.ensure_direction(dir);
self.field.set(field);
self.direction.set(dir);
}
}
}
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,
},
(false, false) => unreachable!(),
}
},
|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::*;
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)]);
sort_by(&Value, Descending, First, rows.as_mut_slice());
assert_eq!(rows, vec![Row(3.0), Row(2.0), Row(1.0)]);
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());
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));
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());
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));
}
}