1use derive_where::derive_where;
5use toasty::stmt::OrderByExpr;
6use topcoat::{context::Cx, view::*};
7
8use super::{Column, ColumnBase, ColumnWidth, boolean::boolean_cell, sort_order};
9use crate::{Lens, schema::Options, values::FormScalar};
10
11mod view;
12
13#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15pub enum Editor {
16 Toggle,
18 Select,
20 Text,
22}
23
24impl Editor {
25 pub(crate) fn saves_on_change(self) -> bool {
27 matches!(self, Self::Toggle | Self::Select)
28 }
29
30 pub(crate) fn control(self) -> &'static str {
32 match self {
33 Self::Toggle => "a checkbox or switch",
34 Self::Select => "a choice over static options",
35 Self::Text => "a one-line text field",
36 }
37 }
38}
39
40#[derive_where(Clone, Debug)]
67pub struct ToggleColumn<M> {
68 lens: Lens<M, bool>,
69 base: ColumnBase,
70 sortable: bool,
71}
72
73impl<M> ToggleColumn<M>
74where
75 M: toasty::schema::Model,
76{
77 pub fn new(lens: Lens<M, bool>) -> Self {
79 Self {
80 base: ColumnBase::new(lens.path(), ColumnWidth::Auto),
81 lens,
82 sortable: false,
83 }
84 }
85
86 base_builders!(sortable);
87}
88
89impl<M> Column<M> for ToggleColumn<M>
90where
91 M: toasty::schema::Model + Send + Sync + 'static,
92{
93 base_column_methods!();
94
95 fn text(&self, cx: &Cx, row: &M) -> String {
96 crate::Text::message(if *self.lens.read(row) {
97 "tablo-yes"
98 } else {
99 "tablo-no"
100 })
101 .localize(cx)
102 }
103
104 fn cell<'a>(&self, cx: &'a Cx, row: &M) -> BoxView<'a> {
105 boolean_cell(cx, *self.lens.read(row), self.text(cx, row))
106 }
107
108 fn order_by(&self, descending: bool) -> Option<OrderByExpr> {
109 self.sortable
110 .then(|| sort_order(self.lens.path(), descending))
111 }
112}
113
114impl<M> crate::column::ColumnInternals<M> for ToggleColumn<M>
115where
116 M: toasty::schema::Model + Send + Sync + 'static,
117{
118 base_internals!(binding);
119
120 fn edits(&self) -> Option<Editor> {
121 Some(Editor::Toggle)
122 }
123
124 fn editor<'a>(&self, cx: &'a Cx, row: &M) -> Option<BoxView<'a>> {
125 Some(view::toggle(
126 cx,
127 self.base.name(),
128 self.base.label().localize(cx),
129 *self.lens.read(row),
130 ))
131 }
132}
133
134#[derive_where(Clone, Debug)]
147pub struct SelectColumn<M, T> {
148 lens: Lens<M, T>,
149 base: ColumnBase,
150 sortable: bool,
151}
152
153impl<M, T> SelectColumn<M, T>
154where
155 M: toasty::schema::Model,
156 T: Options + FormScalar + Send + Sync + 'static,
157{
158 pub fn new(lens: Lens<M, T>) -> Self {
160 Self {
161 base: ColumnBase::new(lens.path(), ColumnWidth::Auto),
162 lens,
163 sortable: false,
164 }
165 }
166
167 base_builders!(sortable);
168}
169
170impl<M, T> Column<M> for SelectColumn<M, T>
171where
172 M: toasty::schema::Model + Send + Sync + 'static,
173 T: Options + FormScalar + Send + Sync + 'static,
174{
175 base_column_methods!();
176
177 fn text(&self, cx: &Cx, row: &M) -> String {
178 T::label_of(&self.lens.read(row).to_form()).localize(cx)
179 }
180
181 fn order_by(&self, descending: bool) -> Option<OrderByExpr> {
182 self.sortable
183 .then(|| sort_order(self.lens.path(), descending))
184 }
185}
186
187impl<M, T> crate::column::ColumnInternals<M> for SelectColumn<M, T>
188where
189 M: toasty::schema::Model + Send + Sync + 'static,
190 T: Options + FormScalar + Send + Sync + 'static,
191{
192 base_internals!(binding);
193
194 fn edits(&self) -> Option<Editor> {
195 Some(Editor::Select)
196 }
197
198 fn edit_options(&self) -> Vec<(String, crate::Text)> {
199 T::options()
200 }
201
202 fn editor<'a>(&self, cx: &'a Cx, row: &M) -> Option<BoxView<'a>> {
203 let options = T::options()
204 .into_iter()
205 .map(|(value, label)| (value, label.localize(cx)))
206 .collect();
207 Some(view::select(
208 cx,
209 self.base.name(),
210 self.base.label().localize(cx),
211 self.lens.read(row).to_form(),
212 options,
213 ))
214 }
215}
216
217#[derive_where(Clone, Debug)]
229pub struct TextInputColumn<M, T> {
230 lens: Lens<M, T>,
231 base: ColumnBase,
232 sortable: bool,
233}
234
235impl<M, T> TextInputColumn<M, T>
236where
237 M: toasty::schema::Model,
238 T: FormScalar + Send + Sync + 'static,
239{
240 pub fn new(lens: Lens<M, T>) -> Self {
242 Self {
243 base: ColumnBase::new(lens.path(), ColumnWidth::Grow),
244 lens,
245 sortable: false,
246 }
247 }
248
249 base_builders!(sortable);
250}
251
252impl<M, T> Column<M> for TextInputColumn<M, T>
253where
254 M: toasty::schema::Model + Send + Sync + 'static,
255 T: FormScalar + Send + Sync + 'static,
256{
257 base_column_methods!();
258
259 fn text(&self, cx: &Cx, row: &M) -> String {
260 self.lens.read(row).to_label().localize(cx)
261 }
262
263 fn order_by(&self, descending: bool) -> Option<OrderByExpr> {
264 self.sortable
265 .then(|| sort_order(self.lens.path(), descending))
266 }
267}
268
269impl<M, T> crate::column::ColumnInternals<M> for TextInputColumn<M, T>
270where
271 M: toasty::schema::Model + Send + Sync + 'static,
272 T: FormScalar + Send + Sync + 'static,
273{
274 base_internals!(binding);
275
276 fn edits(&self) -> Option<Editor> {
277 Some(Editor::Text)
278 }
279
280 fn editor<'a>(&self, cx: &'a Cx, row: &M) -> Option<BoxView<'a>> {
281 let value = T::input_value(&self.lens.read(row).to_form(), &crate::time_zone(cx));
282 Some(view::text_input(
283 cx,
284 view::TextInput {
285 name: self.base.name(),
286 label: self.base.label().localize(cx),
287 value,
288 input_type: T::INPUT_TYPE,
289 step: T::STEP,
290 },
291 ))
292 }
293}