maia_wasm/ui/
input.rs

1//! Input fields.
2//!
3//! This module defines the trait [`InputElement`] and some structs which
4//! implement it. They are used to manipulate HTML input elements in an
5//! idiomatic way from Rust.
6
7use std::ops::Deref;
8use std::rc::Rc;
9use web_sys::{HtmlInputElement, HtmlSelectElement, HtmlSpanElement};
10
11/// HTML input element.
12///
13/// This trait models an abstract HTML input element whose value can can be get
14/// and setted. The `type T` corresponds to the Rust type of the value of the
15/// input element. The generic type `E` corresponds to the HTML input element
16/// type from [`web_sys`].
17///
18/// This trait is intended to be implement for types that behave like a wrapper
19/// over [`Rc<E>`].
20pub trait InputElement<E>: Deref<Target = E> + From<Rc<E>> + Clone {
21    /// Rust type corresponding to the value of the input element.
22    type T;
23
24    /// Gets the value of the input element.
25    ///
26    /// This function attempts to convert the contents of the input element into
27    /// a Rust value of type `T`. If the conversion is successful, the value is
28    /// returned inside a `Some`. If the format of the contents of the input
29    /// element is not correct and a Rust value cannot be obtained, `None` is
30    /// returned.
31    fn get(&self) -> Option<Self::T>;
32
33    /// Sets the value of the input element.
34    ///
35    /// This function sets the contents of the input element to match that of
36    /// the Rust value given in the `value` argument. After the input element is
37    /// set, its contents might not match exactly the Rust value, for instance
38    /// due to rounding to represent a floating point number with fewer decimals
39    /// in a text field.
40    fn set(&self, value: &Self::T);
41}
42
43/// Number presentation.
44///
45/// This trait defines how to format numbers in a text field.
46///
47/// The `SCALE` corresponds to the units that are used in the text field. The
48/// value of the text field is multiplied by `SCALE` to give a Rust value, and
49/// likewise a Rust value is divided by the `SCALE` to compute the value that is
50/// displayed in the text field. For example, if the Rust value has units of Hz
51/// but the text field displays the value in units of MHz, the `SCALE` is `1e6`.
52///
53/// The `RESOLUTION` is used to limit the number of digits that are used in the
54/// text field when displaying the value. This is done by rounding the Rust
55/// value to the nearest multiple of `RESOLUTION` before displaying it on the
56/// text field. For example, if the Rust value has units of Hz but the text
57/// field has a resolution of kHz, the `RESOLUTION` is `Some(1e3)`. If the
58/// `RESOLUTION` is `None`, then no rounding is done and the exact Rust value is
59/// displayed in the field.
60pub trait NumberPresentation: Clone {
61    /// Scale of the text field.
62    ///
63    /// This indicates how many Rust value units a text field unit consists of.
64    const SCALE: f64;
65    /// Resolution of the text field.
66    ///
67    /// This indicates how many Rust value units correspond to the least
68    /// significant digit of the text field.
69    const RESOLUTION: Option<f64>;
70}
71
72macro_rules! presentation {
73    ($t:ident, $scale:expr, $resolution:expr, $doc:expr) => {
74        #[doc = $doc]
75        #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
76        pub struct $t {}
77
78        impl NumberPresentation for $t {
79            const SCALE: f64 = $scale;
80            const RESOLUTION: Option<f64> = $resolution;
81        }
82    };
83}
84
85presentation!(
86    DefaultPresentation,
87    1.0,
88    None,
89    "Default number presentation.\n\n\
90     The Rust value and the text field use the same units, \
91     and there is no resolution rounding."
92);
93presentation!(
94    IntegerPresentation,
95    1.0,
96    Some(1.0),
97    "Integer number presentation.\n\n\
98     The Rust value and the text field use the same units, \
99     but the text field is rounded to always display an integer."
100);
101presentation!(
102    MHzPresentation,
103    1e6,
104    Some(1e3),
105    "MHz presentation.\n\n\
106     The Rust value has units of Hz, and the text field has units of MHz. \
107     The text field is rounded to the nearest integer kHz (three decimal digits).\n\n\
108     This is often used to display frequencies such as LO frequency in the UI."
109);
110presentation!(
111    KHzPresentation,
112    1e3,
113    Some(1.0),
114    "kHz presentation.\n\n\
115     The Rust value has units of Hz, and the text field has units of kHz. \
116     The text field is rounded to the nearest integer Hz (three decimal digits).\n\n\
117     This is often used to display small frequency up to a few MHz in the UI."
118);
119
120/// Number input.
121///
122/// This struct behaves as a wrapper over `Rc<HtmlInputElement>` and implements
123/// the [`InputElement`] trait. The type generic `T` corresponds to the Rust
124/// type used for the value of the input element. The type generic `P` is the
125/// [`NumberPresentation`] that is used.
126#[derive(Clone)]
127pub struct NumberInput<T, P = DefaultPresentation> {
128    element: Rc<HtmlInputElement>,
129    _phantom: std::marker::PhantomData<(T, P)>,
130}
131
132impl<T, P> From<Rc<HtmlInputElement>> for NumberInput<T, P> {
133    fn from(element: Rc<HtmlInputElement>) -> NumberInput<T, P> {
134        NumberInput {
135            element,
136            _phantom: std::marker::PhantomData,
137        }
138    }
139}
140
141impl<T, P> Deref for NumberInput<T, P> {
142    type Target = HtmlInputElement;
143
144    fn deref(&self) -> &HtmlInputElement {
145        &self.element
146    }
147}
148
149macro_rules! number_input_set {
150    ($t:ty) => {
151        fn set(&self, value: &$t) {
152            let value = value.clone() as f64;
153            let value = if let Some(resolution) = P::RESOLUTION {
154                (value / resolution).round() * resolution
155            } else {
156                value
157            };
158            self.element.set_value_as_number(value / P::SCALE);
159        }
160    };
161}
162
163macro_rules! number_input_int {
164    ($($t:ty),*) => {
165        $(
166            impl<P: NumberPresentation> InputElement<HtmlInputElement> for NumberInput<$t, P> {
167                type T = $t;
168
169                fn get(&self) -> Option<$t> {
170                    let x = (self.element.value_as_number() * P::SCALE).round() as i64;
171                    <$t>::try_from(x).ok()
172                }
173
174                number_input_set!($t);
175            }
176        )*
177    }
178}
179
180macro_rules! number_input_float {
181    ($($t:ty),*) => {
182        $(
183            impl<P: NumberPresentation> InputElement<HtmlInputElement> for NumberInput<$t, P> {
184                type T = $t;
185
186                fn get(&self) -> Option<$t> {
187                    Some((self.element.value_as_number() * P::SCALE) as $t)
188                }
189
190                number_input_set!($t);
191            }
192        )*
193    }
194}
195
196number_input_int!(u64, u32);
197number_input_float!(f64, f32);
198
199/// Number input.
200///
201/// This struct behaves as a wrapper over `Rc<HtmlSpanElement>` and implements
202/// the [`InputElement`] trait. The type generic `T` corresponds to the Rust
203/// type used for the value of the input element. The type generic `P` is the
204/// [`NumberPresentation`] that is used.
205#[derive(Clone)]
206pub struct NumberSpan<T, P = DefaultPresentation> {
207    element: Rc<HtmlSpanElement>,
208    _phantom: std::marker::PhantomData<(T, P)>,
209}
210
211impl<T, P> From<Rc<HtmlSpanElement>> for NumberSpan<T, P> {
212    fn from(element: Rc<HtmlSpanElement>) -> NumberSpan<T, P> {
213        NumberSpan {
214            element,
215            _phantom: std::marker::PhantomData,
216        }
217    }
218}
219
220impl<T, P> Deref for NumberSpan<T, P> {
221    type Target = HtmlSpanElement;
222
223    fn deref(&self) -> &HtmlSpanElement {
224        &self.element
225    }
226}
227
228macro_rules! number_span_set {
229    ($t:ty) => {
230        fn set(&self, value: &$t) {
231            let value = value.clone() as f64;
232            let value = if let Some(resolution) = P::RESOLUTION {
233                (value / resolution).round() * resolution
234            } else {
235                value
236            };
237            let value = value / P::SCALE;
238            self.element.set_text_content(Some(&value.to_string()));
239        }
240    };
241}
242
243macro_rules! number_span_int {
244    ($($t:ty),*) => {
245        $(
246            impl<P: NumberPresentation> InputElement<HtmlSpanElement> for NumberSpan<$t, P> {
247                type T = $t;
248
249                fn get(&self) -> Option<$t> {
250                    let value: f64 = self.element.text_content()?.parse().ok()?;
251                    let value = (value * P::SCALE).round() as i64;
252                    <$t>::try_from(value).ok()
253                }
254
255                number_span_set!($t);
256            }
257        )*
258    }
259}
260
261macro_rules! number_span_float {
262    ($($t:ty),*) => {
263        $(
264            impl<P: NumberPresentation> InputElement<HtmlSpanElement> for NumberSpan<$t, P> {
265                type T = $t;
266
267                fn get(&self) -> Option<$t> {
268                    let value: f64 = self.element.text_content()?.parse().ok()?;
269                    Some((value * P::SCALE) as $t)
270                }
271
272                number_span_set!($t);
273            }
274        )*
275    }
276}
277
278number_span_int!(u64, u32);
279number_span_float!(f64, f32);
280
281/// Text input.
282///
283/// This struct behaves as a wrapper over `Rc<HtmlInputElement>` and implements
284/// the [`InputElement`] trait. It gives access to the text field contents as
285/// Rust [`String`]s.
286#[derive(Clone)]
287pub struct TextInput {
288    element: Rc<HtmlInputElement>,
289}
290
291impl From<Rc<HtmlInputElement>> for TextInput {
292    fn from(element: Rc<HtmlInputElement>) -> TextInput {
293        TextInput { element }
294    }
295}
296
297impl Deref for TextInput {
298    type Target = HtmlInputElement;
299
300    fn deref(&self) -> &HtmlInputElement {
301        &self.element
302    }
303}
304
305impl InputElement<HtmlInputElement> for TextInput {
306    type T = String;
307
308    fn get(&self) -> Option<String> {
309        Some(self.element.value())
310    }
311
312    fn set(&self, value: &String) {
313        self.element.set_value(value)
314    }
315}
316
317/// Enum input.
318///
319/// This struct behaves as a wrapper over `Rc<HtmlSelectElement>` and implements
320/// the [`InputElement`] trait. It maps an HTML select element to a Rust `enum`
321/// type `E`. The implementations of [`FromStr`](std::str::FromStr) and
322/// [`ToString`] of the `enum` `E` are used to convert between Rust values and
323/// the strings used by the HTML select.
324pub struct EnumInput<E> {
325    element: Rc<HtmlSelectElement>,
326    _phantom: std::marker::PhantomData<E>,
327}
328
329impl<E> Clone for EnumInput<E> {
330    fn clone(&self) -> Self {
331        EnumInput {
332            element: Rc::clone(&self.element),
333            _phantom: std::marker::PhantomData,
334        }
335    }
336}
337
338impl<E> From<Rc<HtmlSelectElement>> for EnumInput<E> {
339    fn from(element: Rc<HtmlSelectElement>) -> EnumInput<E> {
340        EnumInput {
341            element,
342            _phantom: std::marker::PhantomData,
343        }
344    }
345}
346
347impl<E> Deref for EnumInput<E> {
348    type Target = HtmlSelectElement;
349
350    fn deref(&self) -> &HtmlSelectElement {
351        &self.element
352    }
353}
354
355impl<E: std::str::FromStr + ToString> InputElement<HtmlSelectElement> for EnumInput<E> {
356    type T = E;
357
358    fn get(&self) -> Option<E> {
359        Some(match self.element.value().parse() {
360            Ok(x) => x,
361            Err(_) => panic!("could not parse HtmlSelectElement value"),
362        })
363    }
364
365    fn set(&self, value: &E) {
366        self.element.set_value(&value.to_string());
367    }
368}
369
370/// Checkbox input.
371///
372/// This struct behaves as a wrapper over `Rc<HtmlInputElement>` and implements
373/// the [`InputElement`] trait. It should be used with checkbox input elements,
374/// and it gives access to the checkbox value as a Rust [`bool`].
375#[derive(Clone)]
376pub struct CheckboxInput {
377    element: Rc<HtmlInputElement>,
378}
379
380impl From<Rc<HtmlInputElement>> for CheckboxInput {
381    fn from(element: Rc<HtmlInputElement>) -> CheckboxInput {
382        CheckboxInput { element }
383    }
384}
385
386impl Deref for CheckboxInput {
387    type Target = HtmlInputElement;
388
389    fn deref(&self) -> &HtmlInputElement {
390        &self.element
391    }
392}
393
394impl InputElement<HtmlInputElement> for CheckboxInput {
395    type T = bool;
396
397    fn get(&self) -> Option<bool> {
398        Some(self.element.checked())
399    }
400
401    fn set(&self, &value: &bool) {
402        self.element.set_checked(value)
403    }
404}