Skip to main content

tablo_core/column/
image.rs

1//! [`ImageColumn`]: an uploaded image's path rendered as a thumbnail.
2
3use derive_where::derive_where;
4use topcoat::{context::Cx, view::*};
5
6use super::{Column, ColumnBase, ColumnWidth};
7use crate::{Lens, values::FormScalar};
8
9/// A column of a `String` field holding an uploaded image's path, rendered as a thumbnail.
10///
11/// ```rust
12/// # #[derive(Debug, Clone, toasty::Model, tablo_core::ModelColumns)]
13/// # struct Product { #[key] #[auto] id: uuid::Uuid, image: String }
14/// tablo_core::ImageColumn::new(tablo_core::lens!(Product.image)).circular();
15/// ```
16///
17/// A rooted path, such as an upload's, shows as a 40px square thumbnail; an empty value shows
18/// the table's dash, and any other value its text. An absolute `http(s)` URL shows as an image
19/// only once [`remote`](Self::remote) allows it.
20#[derive_where(Clone, Debug)]
21pub struct ImageColumn<M, T> {
22    lens: Lens<M, T>,
23    base: ColumnBase,
24    size: u16,
25    circular: bool,
26    remote: bool,
27}
28
29impl<M, T> ImageColumn<M, T>
30where
31    M: toasty::schema::Model,
32{
33    /// Bind the column to the `String` or `Option<String>` field `lens` reads.
34    pub fn new(lens: Lens<M, T>) -> Self
35    where
36        T: toasty::schema::Field<Inner = String>,
37    {
38        Self {
39            base: ColumnBase::new(lens.path(), ColumnWidth::Auto),
40            lens,
41            size: 40,
42            circular: false,
43            remote: false,
44        }
45    }
46
47    /// Show the thumbnail `pixels` wide and tall.
48    pub fn size(mut self, pixels: u16) -> Self {
49        self.size = pixels;
50        self
51    }
52
53    /// Crop the thumbnail to a circle, for an avatar.
54    pub fn circular(mut self) -> Self {
55        self.circular = true;
56        self
57    }
58
59    /// Also show an absolute `http(s)` URL as an image. Every browser that renders the table
60    /// then loads it from that host as the page opens, so whoever can write the field learns who
61    /// views the list, and when: allow it for a field only trusted users write.
62    pub fn remote(mut self) -> Self {
63        self.remote = true;
64        self
65    }
66
67    base_builders!();
68}
69
70impl<M, T> Column<M> for ImageColumn<M, T>
71where
72    M: toasty::schema::Model + Send + Sync + 'static,
73    T: FormScalar + Send + Sync + 'static,
74{
75    base_column_methods!();
76
77    fn text(&self, _cx: &Cx, row: &M) -> String {
78        self.lens.read(row).to_form()
79    }
80
81    fn cell<'a>(&self, cx: &'a Cx, row: &M) -> BoxView<'a> {
82        let src = self.text(cx, row).trim().to_string();
83        let shown = if self.remote {
84            crate::schema::is_linkable(&src)
85        } else {
86            crate::schema::is_rooted(&src)
87        };
88        if !shown {
89            return crate::schema::value_cell(cx, &src);
90        }
91        let src = crate::schema::file_href(cx, &src);
92        let class = if self.circular {
93            "shrink-0 rounded-full object-cover ring-1 ring-foreground/10"
94        } else {
95            "shrink-0 rounded-md object-cover ring-1 ring-foreground/10"
96        };
97        let size = self.size.to_string();
98        view! {
99            <img
100                src=(src)
101                alt=""
102                loading="lazy"
103                referrerpolicy="no-referrer"
104                width=(size.clone())
105                height=(size)
106                class=(class)
107            >
108        }
109        .boxed()
110    }
111}
112
113impl<M, T> crate::column::ColumnInternals<M> for ImageColumn<M, T>
114where
115    M: toasty::schema::Model + Send + Sync + 'static,
116    T: FormScalar + Send + Sync + 'static,
117{
118    base_internals!(binding);
119}