Skip to main content

tablo_core/column/
icon.rs

1//! [`IconColumn`]: one field shown as its icon.
2
3use std::sync::Arc;
4
5use derive_where::derive_where;
6use toasty::stmt::OrderByExpr;
7use topcoat::{context::Cx, icon::IconData, view::*};
8
9use super::{Column, ColumnBase, ColumnWidth, ReadFn, cell::icon_view, sort_order};
10use crate::{Lens, display::Color, values::FormScalar};
11
12/// A column of one field shown as an icon in its color, its label read to a screen reader and
13/// shown when the pointer rests on it.
14///
15/// The icon and the color are an `Options` enum's `#[option(icon = …, color = "…")]`, unless
16/// [`icon`](Self::icon) and [`color`](Self::color) say otherwise; a value with no icon shows its
17/// label.
18///
19/// ```rust
20/// # #[derive(Debug, Clone, Copy, PartialEq, Eq, toasty::Embed, tablo_core::Options)]
21/// # enum Status {
22/// #     #[option(color = "warning", icon = tablo_ui::icons::INFO)] Draft,
23/// #     #[option(color = "success", icon = tablo_ui::icons::CIRCLE_CHECK)] Published,
24/// # }
25/// # #[derive(Debug, Clone, toasty::Model, tablo_core::ModelColumns)]
26/// # struct Post { #[key] #[auto] id: uuid::Uuid, status: Status }
27/// tablo_core::IconColumn::new(tablo_core::lens!(Post.status)).sortable();
28/// ```
29#[derive_where(Clone, Debug)]
30pub struct IconColumn<M, T> {
31    lens: Lens<M, T>,
32    base: ColumnBase,
33    sortable: bool,
34    #[derive_where(skip(Debug))]
35    icon: ReadFn<T, Option<IconData>>,
36    #[derive_where(skip(Debug))]
37    color: ReadFn<T, Option<Color>>,
38}
39
40impl<M, T> IconColumn<M, T>
41where
42    M: toasty::schema::Model,
43    T: FormScalar + Send + Sync + 'static,
44{
45    /// Bind the column to the field `lens` reads.
46    pub fn new(lens: Lens<M, T>) -> Self {
47        Self {
48            base: ColumnBase::new(lens.path(), ColumnWidth::Auto),
49            lens,
50            sortable: false,
51            icon: Arc::new(T::icon),
52            color: Arc::new(T::color),
53        }
54    }
55
56    /// Show `icon(value)` for the value.
57    pub fn icon(mut self, icon: impl Fn(&T) -> Option<IconData> + Send + Sync + 'static) -> Self {
58        self.icon = Arc::new(icon);
59        self
60    }
61
62    /// Color the icon by `color(value)`.
63    pub fn color(mut self, color: impl Fn(&T) -> Option<Color> + Send + Sync + 'static) -> Self {
64        self.color = Arc::new(color);
65        self
66    }
67
68    base_builders!(sortable);
69}
70
71impl<M, T> Column<M> for IconColumn<M, T>
72where
73    M: toasty::schema::Model + Send + Sync + 'static,
74    T: FormScalar + Send + Sync + 'static,
75{
76    base_column_methods!();
77
78    fn text(&self, cx: &Cx, row: &M) -> String {
79        self.lens.read(row).to_label().localize(cx)
80    }
81
82    fn cell<'a>(&self, cx: &'a Cx, row: &M) -> BoxView<'a> {
83        let value = self.lens.read(row);
84        let label = value.to_label().localize(cx);
85        match (self.icon)(value) {
86            Some(data) => icon_cell(cx, label, icon_view(cx, data, (self.color)(value))),
87            None => crate::schema::value_cell(cx, &label),
88        }
89    }
90
91    fn order_by(&self, descending: bool) -> Option<OrderByExpr> {
92        self.sortable
93            .then(|| sort_order(self.lens.path(), descending))
94    }
95}
96
97impl<M, T> crate::column::ColumnInternals<M> for IconColumn<M, T>
98where
99    M: toasty::schema::Model + Send + Sync + 'static,
100    T: FormScalar + Send + Sync + 'static,
101{
102    base_internals!(binding);
103}
104
105/// `icon` titled `label`, which a screen reader reads in its place.
106fn icon_cell<'a>(cx: &'a Cx, label: String, glyph: BoxView<'a>) -> BoxView<'a> {
107    view! {
108        <span class="inline-flex items-center" title=(label.clone())>
109            (glyph)
110            <span class="sr-only">(label)</span>
111        </span>
112    }
113    .boxed()
114}