Skip to main content

denise_ui/widgets/
describe.rs

1//! What a widget's properties *are*, described by the widget itself.
2//!
3//! A form file names a widget and a property — `button` and `role=primary` — and
4//! something has to turn those strings into `Button::set_role(Role::Primary)`.
5//! The obvious way is a table in whatever does the turning. There would then be
6//! two of them, because the form designer's property inspector needs the same
7//! knowledge in order to show an editor per property, and both would drift from
8//! the widgets and from each other the first time a widget grew a setting.
9//!
10//! So the widget owns the list. [`Describe`] is implemented next to each widget,
11//! in the same file, and everything else reads it:
12//!
13//! - `denise-forms` builds a tree from a `.dform` by calling [`Describe::set`]
14//!   once per property in the file.
15//! - The designer's inspector renders one editor per [`Property`], choosing which
16//!   from the [`PropertyKind`].
17//! - [`all`] lists every widget that ships, so a palette does not name them —
18//!   with [`Describe::DOC`] saying what each one *is*, [`Describe::GROUP`]
19//!   saying which shelf it belongs on, and [`Describe::ICON`] giving it a face,
20//!   so the palette does not describe, file or draw them either.
21//!
22//! # The two properties a widget cannot hold
23//!
24//! Most of a widget's settings are its own. Two are not, and
25//! [`Property::is_settable`] is how they say so.
26//!
27//! A **message** is a value of the application's type. A `Button<M>` holds an `M`
28//! and this crate has never seen `M`, so no `Value` can carry one. The engine
29//! resolves a name from the file into the application's message and hands it to
30//! the constructor.
31//!
32//! An **asset** is a path. `Image` holds decoded pixels, not a filename, and this
33//! crate does not decode anything. The engine loads the path and constructs from
34//! the pixels.
35//!
36//! Both are still *described*, because the inspector must offer them and the
37//! engine must not report them as typos. [`Describe::set`] refuses them with
38//! [`Mismatch::Supplied`].
39//!
40//! # Ranges are for editors, not for validation
41//!
42//! `PropertyKind::Float { min, max }` tells an inspector to draw a slider between
43//! two numbers. It is not a gate: a widget that clamps — and most do — clamps a
44//! value from [`Describe::set`] exactly as it clamps one from its own setter, so
45//! there is one rule about what a `Progress` of `2.0` means rather than two.
46//!
47//! # Example
48//!
49//! ```
50//! use denise_ui::widgets::{Button, Describe, Value};
51//! use denise_ui::Void;
52//!
53//! let mut button = Button::<Void>::inert("Save");
54//! button.set("text", Value::text("Apply")).unwrap();
55//! assert_eq!(button.get("text"), Some(Value::text("Apply")));
56//!
57//! // The list is the widget's, not ours.
58//! assert!(Button::<Void>::PROPERTIES.iter().any(|p| p.name == "role"));
59//!
60//! // A typo names the widget, the property and what would have been accepted.
61//! let error = button.set("colour", Value::text("red")).unwrap_err();
62//! assert!(error.to_string().contains("button"));
63//! assert!(error.to_string().contains("colour"));
64//! ```
65
66use alloc::string::{String, ToString};
67use alloc::vec::Vec;
68use core::fmt;
69
70use denise::icon::Icon;
71use denise::{Radius, Role};
72
73use super::{Align, Fit, Orientation, avatar::Presence};
74
75// ---------------------------------------------------------------- name tables
76
77/// Every [`Role`], in the spelling a form file uses.
78pub const ROLES: &[&str] = &[
79    "base-100",
80    "base-200",
81    "base-300",
82    "base-content",
83    "primary",
84    "primary-content",
85    "secondary",
86    "secondary-content",
87    "accent",
88    "accent-content",
89    "neutral",
90    "neutral-content",
91    "info",
92    "info-content",
93    "success",
94    "success-content",
95    "warning",
96    "warning-content",
97    "error",
98    "error-content",
99];
100
101/// Every [`Radius`] token.
102pub const RADII: &[&str] = &["selector", "field", "box"];
103
104/// Every [`Align`].
105pub const ALIGNMENTS: &[&str] = &["start", "center", "end"];
106
107/// Every [`Orientation`].
108pub const ORIENTATIONS: &[&str] = &["horizontal", "vertical"];
109
110/// Every [`Fit`].
111pub const FITS: &[&str] = &["fill", "contain", "cover", "center"];
112
113/// Every [`Presence`].
114pub const PRESENCES: &[&str] = &["online", "offline", "busy"];
115
116/// Every [`Side`](crate::Side).
117///
118/// No widget takes one: the side a drawer or a shelf comes in from is a
119/// property of the *form*, which is why the name table lives here with the
120/// others rather than on a widget that would never use it.
121pub const SIDES: &[&str] = &["above", "below", "before", "after"];
122
123/// The [`Role`] a name stands for, and back again.
124///
125/// The table and the mapping are next to each other on purpose: a role added to
126/// one and not the other is a compile error, not a name that silently fails to
127/// parse.
128pub const fn role_from_name(name: &str) -> Option<Role> {
129    // `match` on a string is not `const`, so this walks the table.
130    let bytes = name.as_bytes();
131    let mut i = 0;
132    while i < ROLES.len() {
133        if const_eq(ROLES[i].as_bytes(), bytes) {
134            return Some(ROLE_VALUES[i]);
135        }
136        i += 1;
137    }
138    None
139}
140
141const ROLE_VALUES: [Role; 20] = [
142    Role::Base100,
143    Role::Base200,
144    Role::Base300,
145    Role::BaseContent,
146    Role::Primary,
147    Role::PrimaryContent,
148    Role::Secondary,
149    Role::SecondaryContent,
150    Role::Accent,
151    Role::AccentContent,
152    Role::Neutral,
153    Role::NeutralContent,
154    Role::Info,
155    Role::InfoContent,
156    Role::Success,
157    Role::SuccessContent,
158    Role::Warning,
159    Role::WarningContent,
160    Role::Error,
161    Role::ErrorContent,
162];
163
164const fn const_eq(a: &[u8], b: &[u8]) -> bool {
165    if a.len() != b.len() {
166        return false;
167    }
168    let mut i = 0;
169    while i < a.len() {
170        if a[i] != b[i] {
171            return false;
172        }
173        i += 1;
174    }
175    true
176}
177
178/// The name for a [`Role`].
179pub const fn role_name(role: Role) -> &'static str {
180    ROLES[role as usize]
181}
182
183/// The name for a [`Radius`].
184pub const fn radius_name(radius: Radius) -> &'static str {
185    match radius {
186        Radius::Selector => "selector",
187        Radius::Field => "field",
188        Radius::Box => "box",
189    }
190}
191
192/// The [`Radius`] a name stands for.
193pub fn radius_from_name(name: &str) -> Option<Radius> {
194    Some(match name {
195        "selector" => Radius::Selector,
196        "field" => Radius::Field,
197        "box" => Radius::Box,
198        _ => return None,
199    })
200}
201
202/// The name for an [`Align`].
203pub const fn align_name(align: Align) -> &'static str {
204    match align {
205        Align::Start => "start",
206        Align::Center => "center",
207        Align::End => "end",
208    }
209}
210
211/// The [`Align`] a name stands for.
212pub fn align_from_name(name: &str) -> Option<Align> {
213    Some(match name {
214        "start" => Align::Start,
215        "center" => Align::Center,
216        "end" => Align::End,
217        _ => return None,
218    })
219}
220
221/// The name for an [`Orientation`].
222pub const fn orientation_name(orientation: Orientation) -> &'static str {
223    match orientation {
224        Orientation::Horizontal => "horizontal",
225        Orientation::Vertical => "vertical",
226    }
227}
228
229/// The [`Orientation`] a name stands for.
230pub fn orientation_from_name(name: &str) -> Option<Orientation> {
231    Some(match name {
232        "horizontal" => Orientation::Horizontal,
233        "vertical" => Orientation::Vertical,
234        _ => return None,
235    })
236}
237
238/// The name for a [`Fit`].
239pub const fn fit_name(fit: Fit) -> &'static str {
240    match fit {
241        Fit::Fill => "fill",
242        Fit::Contain => "contain",
243        Fit::Cover => "cover",
244        Fit::Center => "center",
245    }
246}
247
248/// The [`Fit`] a name stands for.
249pub fn fit_from_name(name: &str) -> Option<Fit> {
250    Some(match name {
251        "fill" => Fit::Fill,
252        "contain" => Fit::Contain,
253        "cover" => Fit::Cover,
254        "center" => Fit::Center,
255        _ => return None,
256    })
257}
258
259/// The name for a [`Presence`].
260pub const fn presence_name(presence: Presence) -> &'static str {
261    match presence {
262        Presence::Online => "online",
263        Presence::Offline => "offline",
264        Presence::Busy => "busy",
265    }
266}
267
268/// The [`Presence`] a name stands for.
269pub fn presence_from_name(name: &str) -> Option<Presence> {
270    Some(match name {
271        "online" => Presence::Online,
272        "offline" => Presence::Offline,
273        "busy" => Presence::Busy,
274        _ => return None,
275    })
276}
277
278/// What a form file calls a [`Side`](crate::Side).
279pub const fn side_name(side: crate::Side) -> &'static str {
280    use crate::Side;
281    match side {
282        Side::Above => "above",
283        Side::Below => "below",
284        Side::Before => "before",
285        Side::After => "after",
286    }
287}
288
289/// The [`Side`](crate::Side) a name stands for.
290pub fn side_from_name(name: &str) -> Option<crate::Side> {
291    use crate::Side;
292    Some(match name {
293        "above" => Side::Above,
294        "below" => Side::Below,
295        "before" => Side::Before,
296        "after" => Side::After,
297        _ => return None,
298    })
299}
300
301// -------------------------------------------------------------------- payload
302
303/// What a widget hands its message constructor when it fires.
304///
305/// A `Button` holds an `M`. A `Checkbox` holds a `fn(bool) -> M`, a `List` a
306/// `fn(usize) -> M`, a `Slider` a `fn(f32) -> M`. An engine resolving a name from
307/// a file into the application's message type has to know which, so the
308/// descriptor says.
309#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
310pub enum Payload {
311    /// The widget holds the message itself: `M`.
312    None,
313    /// `fn(bool) -> M` — a checkbox, a toggle, a collapse.
314    Bool,
315    /// `fn(usize) -> M` — anything that selects one of several.
316    Index,
317    /// `fn(f32) -> M` — a slider, a rating.
318    Number,
319}
320
321// --------------------------------------------------------------------- schema
322
323/// What a property takes, and what an editor should offer for it.
324#[derive(Clone, Copy, Debug, PartialEq)]
325#[non_exhaustive]
326pub enum PropertyKind {
327    /// A string.
328    Text,
329    /// A checkbox.
330    Bool,
331    /// A whole number. The bounds are what an editor should offer; a widget that
332    /// clamps still clamps.
333    Int {
334        /// Lowest sensible value.
335        min: i32,
336        /// Highest sensible value.
337        max: i32,
338    },
339    /// A real number, likewise advisory.
340    Float {
341        /// Lowest sensible value.
342        min: f32,
343        /// Highest sensible value.
344        max: f32,
345    },
346    /// One of a fixed set of names — a [`Role`], an [`Align`], a [`Fit`].
347    Enum(&'static [&'static str]),
348    /// A message name the application resolves, with the shape it must resolve
349    /// to. Never settable here; see the [module docs](self).
350    Message(Payload),
351    /// A path relative to the form file. Never settable here; see the
352    /// [module docs](self).
353    Asset,
354    /// A literal colour, written `#RRGGBB`.
355    ///
356    /// Carried as a [`Value::Text`], because that is what a form file holds and
357    /// what an inspector's field edits; the kind is separate from `Text` so that
358    /// an inspector knows to offer a swatch, and so that a widget rejecting
359    /// `"chartreuse"` can say what it wanted.
360    ///
361    /// **The only widget with one is `video`**, whose ground is drawn behind a
362    /// hardware plane and so is never composited with themed content. Everything
363    /// else names a [`Role`] and lets the theme decide, which is what keeps a
364    /// theme swap from leaving one widget the wrong colour.
365    Color,
366    /// The widget's **collection**, written as child nodes rather than a value.
367    ///
368    /// A `select` holds `option`s, a `tabs` holds `tab`s. The property's name is
369    /// the child node's name, and its items are the nodes' arguments — so a
370    /// property called `option` means *the `option` nodes under this one*.
371    ///
372    /// Never settable here, for the third of the reasons in the [module
373    /// docs](self): the items are not one value but a run of nodes, each with
374    /// its own place in the file and its own comments above it. An inspector
375    /// edits them where they live — `Edit::Argument` for one item's text,
376    /// `Insert`, `Remove` and `Move` for the rest — which is what keeps a
377    /// comment written above the third option above the third option.
378    ///
379    /// Which collections are a widget's **real data** and which are a
380    /// designer's placeholder is a question per widget, not per kind; see
381    /// `docs/forms.md` and [`PropertyKind::Placeholder`].
382    List,
383    /// A collection the **designer** supplies and the application replaces:
384    /// written as child nodes, like [`List`](PropertyKind::List), but inside a
385    /// `design { … }` block that the engine skips unless it is asked for it.
386    ///
387    /// A `table`'s columns are its shape and a `List`; its rows are four names
388    /// somebody typed so the table looks like a table on a canvas, and are
389    /// this. The rows a kiosk shows come from the application at run time, so
390    /// carrying the designer's into flash is dead weight at best and a leak of
391    /// whatever was used as sample data at worst.
392    ///
393    /// Never settable here, for the same reason as `List`: the items are a run
394    /// of nodes rather than one value, and an inspector edits them where they
395    /// live.
396    Placeholder,
397}
398
399impl PropertyKind {
400    /// Whether this is a collection written as child nodes.
401    ///
402    /// True for [`List`](PropertyKind::List) and
403    /// [`Placeholder`](PropertyKind::Placeholder), which differ in where they
404    /// are written and whether the engine builds them, and not at all in what
405    /// an inspector does with one: both are a run of nodes edited where they
406    /// live rather than a value typed into a field.
407    ///
408    /// ```
409    /// # use denise_ui::widgets::describe::PropertyKind;
410    /// assert!(PropertyKind::List.is_collection());
411    /// assert!(PropertyKind::Placeholder.is_collection());
412    /// assert!(!PropertyKind::Text.is_collection());
413    /// ```
414    pub const fn is_collection(self) -> bool {
415        matches!(self, PropertyKind::List | PropertyKind::Placeholder)
416    }
417
418    /// A short name for this kind, for error messages.
419    pub const fn noun(self) -> &'static str {
420        match self {
421            PropertyKind::Text => "a string",
422            PropertyKind::Bool => "true or false",
423            PropertyKind::Int { .. } => "a whole number",
424            PropertyKind::Float { .. } => "a number",
425            PropertyKind::Enum(_) => "one of the listed names",
426            PropertyKind::Message(_) => "a message name",
427            PropertyKind::Asset => "a path",
428            PropertyKind::Color => "a colour like #RRGGBB",
429            PropertyKind::List => "a run of child nodes",
430            PropertyKind::Placeholder => "a run of child nodes in a `design` block",
431        }
432    }
433}
434
435/// One setting a widget has.
436#[derive(Clone, Copy, Debug, PartialEq)]
437pub struct Property {
438    /// The name a form file and an inspector use. Kebab-case.
439    pub name: &'static str,
440    /// What it takes.
441    pub kind: PropertyKind,
442    /// One line, shown as a tooltip in the inspector and rendered into the
443    /// widget's documentation.
444    pub doc: &'static str,
445    /// Whether the number is a **length in logical pixels**, and so multiplies
446    /// with the scale factor.
447    ///
448    /// A widget's numbers are not all the same kind of thing. A `Label`'s `size`
449    /// is 16 logical pixels and is 32 at 2×; a `Carousel`'s `auto-advance-ms` is
450    /// 4000 milliseconds and is 4000 at every scale; a `List`'s `selected` is
451    /// the third row and is the third row on a wall. Only the widget knows
452    /// which of its own numbers are lengths, so only the widget can say — the
453    /// same reason the rest of this descriptor exists rather than a table
454    /// somewhere central.
455    ///
456    /// [`Form::build_scaled`] is what reads it.
457    ///
458    /// [`Form::build_scaled`]: https://docs.rs/denise-forms/latest/denise_forms/struct.Form.html#method.build_scaled
459    pub pixels: bool,
460}
461
462impl Property {
463    /// A property. Not a length unless [`in_pixels`](Property::in_pixels) says so.
464    pub const fn new(name: &'static str, kind: PropertyKind, doc: &'static str) -> Self {
465        Self {
466            name,
467            kind,
468            doc,
469            pixels: false,
470        }
471    }
472
473    /// This property is a length in logical pixels.
474    ///
475    /// Say it about a number that should be twice as many at 2× — a text size, a
476    /// row height, a border width — and not about a count, a duration, an index
477    /// or a proportion. See [`Property::pixels`].
478    ///
479    /// ```
480    /// # use denise_ui::widgets::{Property, PropertyKind};
481    /// const SIZE: Property = Property::new(
482    ///     "size",
483    ///     PropertyKind::Int { min: 6, max: 96 },
484    ///     "Text size in logical pixels.",
485    /// )
486    /// .in_pixels();
487    ///
488    /// assert!(SIZE.pixels);
489    /// assert!(!Property::new("selected", PropertyKind::Int { min: 0, max: 99 }, "").pixels);
490    /// ```
491    #[must_use]
492    pub const fn in_pixels(mut self) -> Self {
493        self.pixels = true;
494        self
495    }
496
497    /// Whether [`Describe::set`] can apply this property.
498    ///
499    /// False for a message and for an asset, which the engine supplies at
500    /// construction because this crate can hold neither. See the
501    /// [module docs](self).
502    pub const fn is_settable(&self) -> bool {
503        !matches!(
504            self.kind,
505            PropertyKind::Message(_)
506                | PropertyKind::Asset
507                | PropertyKind::List
508                | PropertyKind::Placeholder
509        )
510    }
511}
512
513// ---------------------------------------------------------------------- value
514
515/// A property's value, owned and untyped.
516///
517/// The bridge between a string in a file and a typed call on a widget. Small on
518/// purpose: everything a form can say is one of these.
519#[derive(Clone, Debug, PartialEq)]
520#[non_exhaustive]
521pub enum Value {
522    /// [`PropertyKind::Text`].
523    Text(String),
524    /// [`PropertyKind::Bool`].
525    Bool(bool),
526    /// [`PropertyKind::Int`].
527    Int(i32),
528    /// [`PropertyKind::Float`].
529    Float(f32),
530    /// [`PropertyKind::Enum`] — always one of the names in the property's table,
531    /// which is why it is `'static`: the caller has already found it there.
532    Enum(&'static str),
533}
534
535impl Value {
536    /// A text value.
537    pub fn text(text: impl Into<String>) -> Self {
538        Value::Text(text.into())
539    }
540
541    /// The name of a [`Role`].
542    pub const fn role(role: Role) -> Self {
543        Value::Enum(role_name(role))
544    }
545
546    /// The string, or a mismatch.
547    pub fn as_text(self) -> Result<String, Mismatch> {
548        match self {
549            Value::Text(text) => Ok(text),
550            _ => Err(Mismatch::wrong(PropertyKind::Text)),
551        }
552    }
553
554    /// The boolean, or a mismatch.
555    pub fn as_bool(self) -> Result<bool, Mismatch> {
556        match self {
557            Value::Bool(value) => Ok(value),
558            _ => Err(Mismatch::wrong(PropertyKind::Bool)),
559        }
560    }
561
562    /// The whole number, or a mismatch.
563    pub fn as_int(self) -> Result<i32, Mismatch> {
564        match self {
565            Value::Int(value) => Ok(value),
566            _ => Err(Mismatch::wrong(PropertyKind::Int {
567                min: i32::MIN,
568                max: i32::MAX,
569            })),
570        }
571    }
572
573    /// The number, or a mismatch. A whole number is accepted, because a form file
574    /// writes `value=1` for a float as readily as `value=1.0`.
575    pub fn as_float(self) -> Result<f32, Mismatch> {
576        match self {
577            Value::Float(value) => Ok(value),
578            Value::Int(value) => Ok(value as f32),
579            _ => Err(Mismatch::wrong(PropertyKind::Float {
580                min: f32::MIN,
581                max: f32::MAX,
582            })),
583        }
584    }
585
586    /// The name, or a mismatch.
587    pub fn as_name(self) -> Result<&'static str, Mismatch> {
588        match self {
589            Value::Enum(name) => Ok(name),
590            _ => Err(Mismatch::wrong(PropertyKind::Enum(&[]))),
591        }
592    }
593
594    /// A whole number narrowed to a text size, clamped rather than wrapped.
595    pub fn as_size(self) -> Result<u16, Mismatch> {
596        Ok(self.as_int()?.clamp(1, u16::MAX as i32) as u16)
597    }
598
599    /// A whole number narrowed to a count, clamped at zero.
600    pub fn as_count(self) -> Result<u32, Mismatch> {
601        Ok(self.as_int()?.max(0) as u32)
602    }
603
604    /// A whole number narrowed to a duration in milliseconds.
605    pub fn as_millis(self) -> Result<u64, Mismatch> {
606        Ok(self.as_int()?.max(0) as u64)
607    }
608
609    /// A whole number narrowed to an index, clamped at zero.
610    pub fn as_index(self) -> Result<usize, Mismatch> {
611        Ok(self.as_int()?.max(0) as usize)
612    }
613
614    /// The name of an [`Align`].
615    pub const fn align(align: Align) -> Self {
616        Value::Enum(align_name(align))
617    }
618
619    /// The name of a [`Radius`].
620    pub const fn radius(radius: Radius) -> Self {
621        Value::Enum(radius_name(radius))
622    }
623
624    /// The name of an [`Orientation`].
625    pub const fn orientation(orientation: Orientation) -> Self {
626        Value::Enum(orientation_name(orientation))
627    }
628
629    /// The name of a [`Fit`].
630    pub const fn fit(fit: Fit) -> Self {
631        Value::Enum(fit_name(fit))
632    }
633
634    /// The name of a [`Presence`].
635    pub const fn presence(presence: Presence) -> Self {
636        Value::Enum(presence_name(presence))
637    }
638
639    /// The [`Role`] this name stands for, or a mismatch.
640    pub fn as_role(self) -> Result<Role, Mismatch> {
641        role_from_name(self.as_name()?).ok_or_else(|| Mismatch::wrong(PropertyKind::Enum(ROLES)))
642    }
643
644    /// The [`Align`] this name stands for, or a mismatch.
645    pub fn as_align(self) -> Result<Align, Mismatch> {
646        align_from_name(self.as_name()?)
647            .ok_or_else(|| Mismatch::wrong(PropertyKind::Enum(ALIGNMENTS)))
648    }
649
650    /// The [`Radius`] this name stands for, or a mismatch.
651    pub fn as_radius(self) -> Result<Radius, Mismatch> {
652        radius_from_name(self.as_name()?).ok_or_else(|| Mismatch::wrong(PropertyKind::Enum(RADII)))
653    }
654
655    /// The [`Orientation`] this name stands for, or a mismatch.
656    pub fn as_orientation(self) -> Result<Orientation, Mismatch> {
657        orientation_from_name(self.as_name()?)
658            .ok_or_else(|| Mismatch::wrong(PropertyKind::Enum(ORIENTATIONS)))
659    }
660
661    /// The [`Fit`] this name stands for, or a mismatch.
662    pub fn as_fit(self) -> Result<Fit, Mismatch> {
663        fit_from_name(self.as_name()?).ok_or_else(|| Mismatch::wrong(PropertyKind::Enum(FITS)))
664    }
665
666    /// The [`Presence`] this name stands for, or a mismatch.
667    pub fn as_presence(self) -> Result<Presence, Mismatch> {
668        presence_from_name(self.as_name()?)
669            .ok_or_else(|| Mismatch::wrong(PropertyKind::Enum(PRESENCES)))
670    }
671}
672
673// --------------------------------------------------------------------- errors
674
675/// Why a widget would not take a value, without saying which widget.
676///
677/// [`Describe::apply`] returns this and [`Describe::set`] turns it into a
678/// [`PropertyError`] that names the widget and the property. The split exists so
679/// that twenty-eight `apply` implementations do not each repeat the context they
680/// all share.
681#[derive(Clone, Copy, Debug, PartialEq)]
682pub enum Mismatch {
683    /// No such property on this widget.
684    Unknown,
685    /// The property exists; the value was the wrong shape.
686    WrongType {
687        /// What the property takes.
688        expected: PropertyKind,
689    },
690    /// The property exists and the widget cannot hold it — a message needs the
691    /// application's type, an asset needs a loader. See the [module docs](self).
692    Supplied,
693}
694
695impl Mismatch {
696    const fn wrong(expected: PropertyKind) -> Self {
697        Mismatch::WrongType { expected }
698    }
699}
700
701/// A property that could not be set, and everything needed to say so usefully.
702#[derive(Clone, Debug, PartialEq)]
703pub struct PropertyError {
704    /// The widget's kind, as a form file spells it.
705    pub kind: &'static str,
706    /// The property that was asked for.
707    pub name: String,
708    /// What went wrong.
709    pub mismatch: Mismatch,
710    /// Everything this widget does accept, for the "expected one of" line.
711    pub accepted: &'static [Property],
712}
713
714impl fmt::Display for PropertyError {
715    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
716        match self.mismatch {
717            Mismatch::Unknown => {
718                write!(f, "`{}` has no property `{}`", self.kind, self.name)?;
719                if !self.accepted.is_empty() {
720                    let names: Vec<&str> = self.accepted.iter().map(|p| p.name).collect();
721                    write!(f, "; it accepts {}", names.join(", "))?;
722                }
723                Ok(())
724            }
725            Mismatch::WrongType { expected } => write!(
726                f,
727                "`{}` on `{}` takes {}",
728                self.name,
729                self.kind,
730                expected.noun()
731            ),
732            Mismatch::Supplied => write!(
733                f,
734                "`{}` on `{}` is supplied when the widget is built, not set afterwards",
735                self.name, self.kind
736            ),
737        }
738    }
739}
740
741impl core::error::Error for PropertyError {}
742
743// ------------------------------------------------------------------- describe
744
745/// A widget that knows its own properties.
746///
747/// Implemented beside each widget. See the [module docs](self) for why the list
748/// lives here rather than in whatever reads it.
749pub trait Describe {
750    /// The name a form file uses for this widget. Kebab-case.
751    const KIND: &'static str;
752
753    /// One line saying what this widget **is**, for somebody choosing one.
754    ///
755    /// A designer's palette shows it as a tooltip, which is the difference
756    /// between twenty-five bare names and a catalogue. Not a description of the
757    /// API and not a sentence about this type — a sentence about the thing on
758    /// screen, in the words of a person deciding whether they want it.
759    ///
760    /// Deliberately required rather than defaulted: adding a widget without one
761    /// should not compile, because a widget nobody can identify in the palette
762    /// is a widget nobody reaches for.
763    const DOC: &'static str;
764
765    /// Which shelf of the catalogue this belongs on.
766    const GROUP: Group;
767
768    /// The widget's glyph: a small portrait of the thing, for a palette to
769    /// draw beside — or instead of — its name.
770    ///
771    /// Drawn in [`denise::icon`]'s format rather than looked up in a
772    /// font, for the reason that module gives: a picture that depends on the
773    /// installed font is a box on the machine least able to spare one. The
774    /// glyphs themselves live in [`icons`](super::icons), which also says what
775    /// makes one read well at sixteen pixels.
776    ///
777    /// Required rather than defaulted, like [`DOC`](Describe::DOC) and for the
778    /// same reason: a widget the palette cannot draw should not compile.
779    const ICON: &'static Icon;
780
781    /// Every property, in the order an inspector should show them.
782    const PROPERTIES: &'static [Property];
783
784    /// The current value.
785    ///
786    /// `None` for three different situations, which the caller tells apart by
787    /// consulting [`PROPERTIES`](Describe::PROPERTIES): a property this widget
788    /// does not have, one it cannot report (a message or an asset — see the
789    /// [module docs](self)), and one that is simply not set, such as the
790    /// selection of a `Select` with nothing selected. The third is what makes
791    /// "a property at its default is not written to the file" implementable:
792    /// nothing to report, nothing to write.
793    fn get(&self, name: &str) -> Option<Value>;
794
795    /// Applies a value, reporting only what went wrong.
796    ///
797    /// Implement this one. Call [`Describe::set`], which adds the widget's name,
798    /// the property's name and the list of what would have been accepted.
799    fn apply(&mut self, name: &str, value: Value) -> Result<(), Mismatch>;
800
801    /// Applies a value, reporting what went wrong and where.
802    fn set(&mut self, name: &str, value: Value) -> Result<(), PropertyError> {
803        self.apply(name, value).map_err(|mismatch| PropertyError {
804            kind: Self::KIND,
805            name: name.to_string(),
806            mismatch,
807            accepted: Self::PROPERTIES,
808        })
809    }
810}
811
812/// [`Describe`], reachable through a `dyn Widget<M>`.
813///
814/// [`Describe`] has associated constants, so it is not object-safe, and the tree
815/// stores widgets boxed. This is the same four questions asked of a trait object,
816/// blanket-implemented for everything that describes itself — never implement it
817/// by hand.
818///
819/// [`Ui::set_property`](crate::Ui::set_property) is what calls it.
820pub trait DynDescribe {
821    /// See [`Describe::KIND`].
822    fn kind(&self) -> &'static str;
823    /// See [`Describe::PROPERTIES`].
824    fn properties(&self) -> &'static [Property];
825    /// See [`Describe::get`].
826    fn get_property(&self, name: &str) -> Option<Value>;
827    /// See [`Describe::set`].
828    fn set_property(&mut self, name: &str, value: Value) -> Result<(), PropertyError>;
829}
830
831impl<T: Describe> DynDescribe for T {
832    fn kind(&self) -> &'static str {
833        T::KIND
834    }
835    fn properties(&self) -> &'static [Property] {
836        T::PROPERTIES
837    }
838    fn get_property(&self, name: &str) -> Option<Value> {
839        self.get(name)
840    }
841    fn set_property(&mut self, name: &str, value: Value) -> Result<(), PropertyError> {
842        self.set(name, value)
843    }
844}
845
846// ------------------------------------------------------------------- registry
847
848/// One widget in the catalogue [`all`] returns.
849#[derive(Clone, Copy, Debug, PartialEq)]
850pub struct WidgetInfo {
851    /// The name a form file uses.
852    pub kind: &'static str,
853    /// One line saying what it is. See [`Describe::DOC`].
854    pub doc: &'static str,
855    /// Which shelf of the catalogue it belongs on.
856    pub group: Group,
857    /// Its glyph. See [`Describe::ICON`].
858    pub icon: &'static Icon,
859    /// What it accepts.
860    pub properties: &'static [Property],
861}
862
863impl WidgetInfo {
864    /// The entry for a widget.
865    pub const fn of<W: Describe>() -> Self {
866        Self {
867            kind: W::KIND,
868            doc: W::DOC,
869            group: W::GROUP,
870            icon: W::ICON,
871            properties: W::PROPERTIES,
872        }
873    }
874
875    /// The property of this name, if it has one.
876    pub fn property(&self, name: &str) -> Option<&'static Property> {
877        self.properties.iter().find(|p| p.name == name)
878    }
879}
880
881/// The shelves a catalogue of widgets is arranged on.
882///
883/// Six, and deliberately few: a palette that has to be *read* to be searched has
884/// failed, and the point of grouping twenty-five rows is that the eye lands on
885/// the right handful. The order here is the order a palette should show them,
886/// which is roughly how often somebody reaches for one.
887///
888/// A widget declares its own through [`Describe::GROUP`], for the same reason it
889/// declares its own properties: there is no table of widgets anywhere in this
890/// workspace and this is not the place to start one.
891#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
892pub enum Group {
893    /// Something a person operates: it takes a message and emits one.
894    Input,
895    /// Something a person reads. Text, and the decorations around text.
896    Display,
897    /// Something that says how far along, how busy, or how much.
898    Indicator,
899    /// Something other widgets go inside.
900    Container,
901    /// Rows and columns of content, with a selection.
902    Data,
903    /// Pictures and video.
904    Media,
905}
906
907impl Group {
908    /// Every one, in the order a palette shows them.
909    pub const ALL: [Self; 6] = [
910        Self::Input,
911        Self::Display,
912        Self::Indicator,
913        Self::Container,
914        Self::Data,
915        Self::Media,
916    ];
917
918    /// The heading a palette writes above the shelf.
919    ///
920    /// ```
921    /// # use denise_ui::widgets::Group;
922    /// assert_eq!(Group::Input.name(), "input");
923    /// // Every group has one, and no two share it.
924    /// let mut names: Vec<&str> = Group::ALL.iter().map(|g| g.name()).collect();
925    /// names.sort_unstable();
926    /// names.dedup();
927    /// assert_eq!(names.len(), Group::ALL.len());
928    /// ```
929    pub const fn name(self) -> &'static str {
930        match self {
931            Self::Input => "input",
932            Self::Display => "display",
933            Self::Indicator => "indicator",
934            Self::Container => "container",
935            Self::Data => "data",
936            Self::Media => "media",
937        }
938    }
939}
940
941/// Every widget that ships with this crate.
942///
943/// A palette lists these rather than naming widgets itself, so the twenty-ninth
944/// widget appears in the designer without the designer changing. A test asserts
945/// that this and [`widgets`](super) hold the same set, so joining it is not
946/// something a new widget can be merged without.
947pub fn all() -> &'static [WidgetInfo] {
948    ALL
949}
950
951/// The catalogue [`all`] returns.
952///
953/// `Void` stands in for the message type, which none of the descriptions depend
954/// on.
955static ALL: &[WidgetInfo] = &[
956    WidgetInfo::of::<super::Alert>(),
957    WidgetInfo::of::<super::Avatar>(),
958    WidgetInfo::of::<super::Badge>(),
959    WidgetInfo::of::<super::Button<crate::Void>>(),
960    WidgetInfo::of::<super::Carousel<crate::Void>>(),
961    WidgetInfo::of::<super::Checkbox<crate::Void>>(),
962    WidgetInfo::of::<super::Collapse<crate::Void>>(),
963    WidgetInfo::of::<super::Divider>(),
964    WidgetInfo::of::<super::Image>(),
965    WidgetInfo::of::<super::Label>(),
966    WidgetInfo::of::<super::List<crate::Void>>(),
967    WidgetInfo::of::<super::MenuBar<crate::Void>>(),
968    WidgetInfo::of::<super::Panel>(),
969    WidgetInfo::of::<super::Progress>(),
970    WidgetInfo::of::<super::RadialProgress>(),
971    WidgetInfo::of::<super::RadioGroup<crate::Void>>(),
972    WidgetInfo::of::<super::Rating<crate::Void>>(),
973    WidgetInfo::of::<super::Select<crate::Void>>(),
974    WidgetInfo::of::<super::Slider<crate::Void>>(),
975    WidgetInfo::of::<super::Spinner>(),
976    WidgetInfo::of::<super::Table<crate::Void>>(),
977    WidgetInfo::of::<super::Tabs<crate::Void>>(),
978    WidgetInfo::of::<super::TextArea<crate::Void>>(),
979    WidgetInfo::of::<super::TextInput<crate::Void>>(),
980    WidgetInfo::of::<super::Timeline>(),
981    WidgetInfo::of::<super::Toggle<crate::Void>>(),
982    WidgetInfo::of::<super::Tree<crate::Void>>(),
983    WidgetInfo::of::<super::Video>(),
984];
985
986#[cfg(test)]
987mod tests {
988    use super::*;
989
990    #[test]
991    fn every_role_round_trips_through_its_name() {
992        for (index, name) in ROLES.iter().enumerate() {
993            let role = role_from_name(name).expect("a name in the table names a role");
994            assert_eq!(role as usize, index, "{name} is out of order");
995            assert_eq!(role_name(role), *name);
996        }
997    }
998
999    #[test]
1000    fn a_name_outside_the_table_is_not_a_role() {
1001        assert_eq!(role_from_name("puce"), None);
1002        assert_eq!(role_from_name(""), None);
1003        assert_eq!(role_from_name("primary-"), None);
1004    }
1005
1006    #[test]
1007    fn the_catalogue_names_are_unique_and_sorted() {
1008        let mut names: Vec<&str> = all().iter().map(|w| w.kind).collect();
1009        let count = names.len();
1010        names.sort_unstable();
1011        names.dedup();
1012        assert_eq!(names.len(), count, "two widgets share a kind");
1013    }
1014
1015    #[test]
1016    fn no_widget_declares_the_same_property_twice() {
1017        for widget in all() {
1018            let mut names: Vec<&str> = widget.properties.iter().map(|p| p.name).collect();
1019            let count = names.len();
1020            names.sort_unstable();
1021            names.dedup();
1022            assert_eq!(names.len(), count, "{} repeats a property", widget.kind);
1023        }
1024    }
1025
1026    #[test]
1027    fn every_property_is_kebab_case_and_documented() {
1028        for widget in all() {
1029            for property in widget.properties {
1030                assert!(
1031                    !property.name.is_empty()
1032                        && property
1033                            .name
1034                            .bytes()
1035                            .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-'),
1036                    "{}.{} is not kebab-case",
1037                    widget.kind,
1038                    property.name
1039                );
1040                assert!(
1041                    !property.doc.is_empty(),
1042                    "{}.{} has no documentation",
1043                    widget.kind,
1044                    property.name
1045                );
1046            }
1047        }
1048    }
1049
1050    #[test]
1051    fn every_widget_says_in_one_line_what_it_is() {
1052        for widget in all() {
1053            let doc = widget.doc;
1054            assert!(!doc.is_empty(), "{} says nothing about itself", widget.kind);
1055            // One line, because it is a tooltip.
1056            assert!(
1057                !doc.contains('\n'),
1058                "{}'s line is more than one",
1059                widget.kind
1060            );
1061            // A sentence a person reads, not a fragment: a capital and a stop.
1062            assert!(
1063                doc.starts_with(|c: char| c.is_uppercase()),
1064                "{}: `{doc}` does not start a sentence",
1065                widget.kind,
1066            );
1067            assert!(
1068                doc.ends_with('.'),
1069                "{}: `{doc}` does not end one",
1070                widget.kind
1071            );
1072            // Long enough to say something, short enough to read at a glance.
1073            assert!(
1074                (20..=100).contains(&doc.len()),
1075                "{}: `{doc}` is {} characters",
1076                widget.kind,
1077                doc.len(),
1078            );
1079        }
1080    }
1081
1082    #[test]
1083    fn no_two_widgets_describe_themselves_the_same_way() {
1084        // Two identical lines means one of them is wrong: the whole point is
1085        // telling a `checkbox` from a `toggle` while choosing between them.
1086        let mut docs: Vec<&str> = all().iter().map(|w| w.doc).collect();
1087        let count = docs.len();
1088        docs.sort_unstable();
1089        docs.dedup();
1090        assert_eq!(docs.len(), count, "two widgets say the same thing");
1091    }
1092
1093    #[test]
1094    fn every_group_has_something_on_it() {
1095        // A shelf with nothing on it is a heading a palette would draw over
1096        // nothing, and a sign that the set of groups drifted from the widgets.
1097        for group in Group::ALL {
1098            assert!(
1099                all().iter().any(|w| w.group == group),
1100                "nothing is `{}`",
1101                group.name(),
1102            );
1103        }
1104        // And every widget is on one of them, which the type already promises;
1105        // this catches a group added to the enum and left out of `ALL`.
1106        for widget in all() {
1107            assert!(
1108                Group::ALL.contains(&widget.group),
1109                "{} is in a group `Group::ALL` does not list",
1110                widget.kind,
1111            );
1112        }
1113    }
1114
1115    #[test]
1116    fn an_enum_property_offers_names_it_would_accept() {
1117        for widget in all() {
1118            for property in widget.properties {
1119                if let PropertyKind::Enum(names) = property.kind {
1120                    assert!(
1121                        !names.is_empty(),
1122                        "{}.{} offers no names",
1123                        widget.kind,
1124                        property.name
1125                    );
1126                }
1127            }
1128        }
1129    }
1130}