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}