denise_forms/build.rs
1//! Turning a parsed form into a live widget tree.
2
3use std::collections::HashMap;
4
5use denise::{Rect, Size};
6use denise_ui::widgets::describe::{
7 ALIGNMENTS, FITS, ORIENTATIONS, PRESENCES, Payload, Property, PropertyKind, RADII, ROLES,
8 Value, WidgetInfo, role_from_name,
9};
10use denise_ui::widgets::{
11 Alert, Avatar, Badge, Button, Carousel, Checkbox, Collapse, Column, Divider, Fit, Image, Label,
12 List, ListItem, MenuBar, Panel, Progress, RadialProgress, RadioGroup, Rating, Select, Slider,
13 Spinner, Table, Tabs, TextInput, Timeline, TimelineItem, Toggle, Tree, TreeItem, Video,
14};
15use denise_ui::{Anchors, Dock, NodeId, Ui};
16use kdl::{KdlDocument, KdlNode, KdlValue};
17
18use crate::error::{At, Error, Reason};
19use crate::form::{Form, FormKind, MAX_DEPTH, Placement};
20
21/// Pixels for a picture a form named, as [`Wiring::asset`] hands them back.
22#[derive(Clone, Debug)]
23pub struct Picture {
24 /// Premultiplied `0xAARRGGBB`, which is `denise-ui`'s contract exactly.
25 pub pixels: Vec<u32>,
26 /// The picture's own size.
27 pub size: Size,
28}
29
30/// A message a form named, in the shape the widget holding it needs.
31///
32/// Widgets do not all take a message the same way, and none of them takes a
33/// closure: a `Button` holds an `M`, a `Checkbox` a `fn(bool) -> M`, a `List` a
34/// `fn(usize) -> M`, a `Slider` a `fn(f32) -> M`. Those are **function
35/// pointers**, so nothing this crate could build from a name would fit — but an
36/// enum's tuple variant already is one:
37///
38/// ```
39/// # use denise_forms::Handler;
40/// #[derive(Clone, Copy)]
41/// enum Message {
42/// Save,
43/// Notify(bool),
44/// }
45///
46/// let save = Handler::Plain(Message::Save);
47/// // `Message::Notify` *is* a `fn(bool) -> Message`.
48/// let notify = Handler::Bool(Message::Notify);
49/// # let _ = (save, notify);
50/// ```
51#[derive(Clone, Copy, Debug)]
52pub enum Handler<M> {
53 /// The message itself, for a widget that holds one: a button, a select, a
54 /// text field's submit.
55 Plain(M),
56 /// `fn(bool) -> M` — a checkbox, a toggle, a collapse.
57 Bool(fn(bool) -> M),
58 /// `fn(usize) -> M` — anything that selects one of several.
59 Index(fn(usize) -> M),
60 /// `fn(f32) -> M` — a slider, a rating.
61 Number(fn(f32) -> M),
62}
63
64impl<M> Handler<M> {
65 fn wanted(payload: Payload) -> &'static str {
66 match payload {
67 Payload::None => "the message itself",
68 Payload::Bool => "a `fn(bool) -> M`",
69 Payload::Index => "a `fn(usize) -> M`",
70 Payload::Number => "a `fn(f32) -> M`",
71 }
72 }
73}
74
75/// What an application supplies a form that this crate cannot: its own message
76/// type, and its own pictures.
77///
78/// A plain closure implements this, which is all most forms need. Implement it on
79/// a type when the form also names pictures.
80pub trait Wiring<M> {
81 /// Turns a message name from the file into a message of the application's
82 /// own type. `payload` says which shape the widget needs.
83 fn message(&mut self, name: &str, payload: Payload) -> Option<Handler<M>>;
84
85 /// Loads a picture, by a path **relative to the form file**.
86 ///
87 /// The default has none, so a form naming a picture in an application that
88 /// supplied no loader fails with the path in the message rather than drawing
89 /// a hole. This crate decodes nothing and does not depend on `denise-image`:
90 /// that keeps a board with its pictures compiled in from linking a decoder it
91 /// will never call.
92 fn asset(&mut self, path: &str) -> Option<Picture> {
93 let _ = path;
94 None
95 }
96}
97
98impl<M, F> Wiring<M> for F
99where
100 F: FnMut(&str, Payload) -> Option<Handler<M>>,
101{
102 fn message(&mut self, name: &str, payload: Payload) -> Option<Handler<M>> {
103 self(name, payload)
104 }
105}
106
107/// One node the form put in the tree, and where in the file it came from.
108///
109/// A designer needs both halves: the [`NodeId`] to hit-test and draw a selection
110/// around, and the [`path`](Placed::path) to edit when the selection moves. The
111/// path is a list of child indices from the `form` node down, which is stable
112/// across a rebuild in a way a byte offset is not — every edit shifts the offsets
113/// after it, and the whole point is to edit and carry on.
114#[derive(Clone, Debug, PartialEq, Eq)]
115pub struct Placed {
116 /// The node in the tree.
117 pub id: NodeId,
118 /// Its parent in the tree, or `None` for a node directly under the form.
119 pub parent: Option<NodeId>,
120 /// What kind of widget it is.
121 pub kind: &'static str,
122 /// The name the file gave it, if it gave one.
123 pub name: Option<String>,
124 /// Child indices from the `form` node's children down to this node.
125 pub path: Vec<usize>,
126}
127
128/// What a form built, so an application can find what it made.
129#[derive(Clone, Debug, Default)]
130pub struct Built {
131 names: HashMap<String, NodeId>,
132 placed: Vec<Placed>,
133 pages: Vec<Page>,
134}
135
136/// One `tab`'s page: the container its subtree was built into.
137///
138/// Only a `tab` carrying children has one. A designer needs these because a
139/// page that is not showing is not in the tree's order at all — nothing in it
140/// paints, answers a press or takes the caret — so reaching the second tab's
141/// contents means showing that page first.
142#[derive(Clone, Debug)]
143pub struct Page {
144 /// The `tab` node's path in the document.
145 pub path: Vec<usize>,
146 /// Which tab it is, counting every `tab` in the strip. What `selected`
147 /// names.
148 pub ordinal: usize,
149 /// The container the page's widgets were built into.
150 pub id: NodeId,
151}
152
153impl Built {
154 /// See [`Form::build`] for one of these being made and read.
155 /// The node a form gave this name, if it gave one that name.
156 pub fn node(&self, name: &str) -> Option<NodeId> {
157 self.names.get(name).copied()
158 }
159
160 /// See [`Form::build`] for one of these being made and read.
161 /// Every name the form gave a node, in no particular order.
162 pub fn names(&self) -> impl Iterator<Item = (&str, NodeId)> {
163 self.names.iter().map(|(name, &id)| (name.as_str(), id))
164 }
165
166 /// Every `tab` page the form built, in file order.
167 ///
168 /// Empty for a form whose tabs are bare labels, which is every form written
169 /// before a `tab` could hold anything. See [`Page`].
170 pub fn pages(&self) -> &[Page] {
171 &self.pages
172 }
173
174 /// Every node the form built, in file order.
175 ///
176 /// See [`Form::build`] for one of these being made and read.
177 /// Includes the ones with no name: a designer selects what a person clicked
178 /// on, and most of what a person clicks on was never named.
179 pub fn placed(&self) -> &[Placed] {
180 &self.placed
181 }
182
183 /// See [`Form::build`] for one of these being made and read.
184 /// The node at a path, if the form put one there.
185 pub fn at(&self, path: &[usize]) -> Option<&Placed> {
186 self.placed.iter().find(|p| p.path == path)
187 }
188
189 /// See [`Form::build`] for one of these being made and read.
190 /// How many nodes were named.
191 pub fn len(&self) -> usize {
192 self.names.len()
193 }
194
195 /// See [`Form::build`] for one of these being made and read.
196 /// Whether the form named nothing.
197 pub fn is_empty(&self) -> bool {
198 self.names.is_empty()
199 }
200}
201
202const ANCHOR_EDGES: &[&str] = &["left", "top", "right", "bottom"];
203const DOCK_SIDES: &[&str] = &["top", "bottom", "left", "right", "fill"];
204
205/// Anywhere on a form's surface, and then some.
206///
207/// A rectangle is advice to an editor, not a rule: a node may sit outside its
208/// parent and the tree will clip it, which is occasionally what somebody means.
209const ANYWHERE: PropertyKind = PropertyKind::Int {
210 min: -8192,
211 max: 8192,
212};
213
214/// The properties the `form` node itself carries, whatever kind it is.
215///
216/// Not `version`, which is the file format's rather than the form's and is not
217/// somebody's to edit; and not the title, which is the node's *argument* rather
218/// than a property. Everything else about a form is here, which is what lets an
219/// inspector show a form the same way it shows a widget — from a descriptor,
220/// with no list of its own.
221pub const FORM_PROPERTIES: &[Property] = &[
222 Property::new(
223 "name",
224 PropertyKind::Text,
225 "What the application calls this form. Names what the typed layer generates.",
226 ),
227 Property::new(
228 "kind",
229 PropertyKind::Enum(FormKind::NAMES),
230 "What this form is for: a screen, a window, a dialog, a drawer, a shelf, or a fragment.",
231 ),
232 Property::new(
233 "width",
234 PropertyKind::Int { min: 1, max: 8192 },
235 "The width the form was designed at, in logical pixels.",
236 ),
237 Property::new(
238 "height",
239 PropertyKind::Int { min: 1, max: 8192 },
240 "The height the form was designed at, in logical pixels.",
241 ),
242 Property::new(
243 "theme",
244 PropertyKind::Enum(crate::form::THEMES),
245 "Which built-in theme the form is drawn with.",
246 ),
247 Property::new(
248 "background",
249 PropertyKind::Enum(denise_ui::widgets::ROLES),
250 "The surface the form is drawn on.",
251 ),
252 Property::new(
253 "scaling",
254 PropertyKind::Enum(crate::form::Scaling::NAMES),
255 "Whether this form may be drawn at another size: none, proportional or stretch.",
256 ),
257];
258
259/// What only a window has.
260const WINDOW_PROPERTIES: &[Property] = &[
261 Property::new(
262 "resizable",
263 PropertyKind::Bool,
264 "Whether the window may be resized. Windows only.",
265 ),
266 Property::new(
267 "min-width",
268 PropertyKind::Int { min: 0, max: 8192 },
269 "The narrowest the window may be made. Windows only.",
270 ),
271 Property::new(
272 "min-height",
273 PropertyKind::Int { min: 0, max: 8192 },
274 "The shortest the window may be made. Windows only.",
275 ),
276];
277
278/// What only a dialog has.
279const DIALOG_PROPERTIES: &[Property] = &[Property::new(
280 "dim",
281 PropertyKind::Int { min: 0, max: 255 },
282 "How dark the backdrop behind the dialog is, 0 to 255. Dialogs only.",
283)];
284
285/// What comes in from an edge: a drawer and a shelf, which differ in modality
286/// and not in shape.
287const EDGE_PROPERTIES: &[Property] = &[
288 Property::new(
289 "side",
290 PropertyKind::Enum(denise_ui::widgets::SIDES),
291 "Which edge it comes in from.",
292 ),
293 Property::new(
294 "extent",
295 PropertyKind::Int { min: 1, max: 8192 },
296 "How far it comes in. Required; across the other axis it covers the surface.",
297 ),
298];
299
300/// The properties a form of this kind carries **and no other kind does**.
301///
302/// A `resizable` on a screen is not a property with no effect; it is a mistake,
303/// and saying so is the whole reason this is a function of the kind rather than
304/// one long list.
305/// ```
306/// # use denise_forms::{FORM_PROPERTIES, FormKind, form_property, kind_properties};
307/// // Everything every form has.
308/// assert!(FORM_PROPERTIES.iter().any(|it| it.name == "width"));
309///
310/// // And what only this kind has.
311/// assert!(kind_properties(FormKind::Window).iter().any(|it| it.name == "resizable"));
312/// assert!(kind_properties(FormKind::Screen).is_empty());
313///
314/// // `form_property` is the two together, which is what "may a form of this
315/// // kind say this?" means.
316/// assert!(form_property(FormKind::Window, "resizable").is_some());
317/// assert!(form_property(FormKind::Screen, "resizable").is_none());
318/// assert!(form_property(FormKind::Screen, "width").is_some());
319/// ```
320pub const fn kind_properties(kind: FormKind) -> &'static [Property] {
321 match kind {
322 FormKind::Window => WINDOW_PROPERTIES,
323 FormKind::Dialog => DIALOG_PROPERTIES,
324 FormKind::Drawer | FormKind::Shelf => EDGE_PROPERTIES,
325 FormKind::Screen | FormKind::Fragment => &[],
326 }
327}
328
329/// Whether the `form` node may carry this property, given its kind.
330/// See [`kind_properties`].
331pub fn form_property(kind: FormKind, name: &str) -> Option<&'static Property> {
332 FORM_PROPERTIES
333 .iter()
334 .chain(kind_properties(kind))
335 .find(|property| property.name == name)
336}
337
338/// The properties the *tree* owns rather than the widget.
339///
340/// Geometry, visibility, ordering, placement. A widget's descriptor never
341/// mentions them, so they are checked against this list before a widget is asked
342/// whether it has heard of them.
343///
344/// Described the same way a widget describes its own, and for the same reason:
345/// the designer's inspector draws an editor per [`Property`] and has no table of
346/// its own, so `x` and `dock` get one from here exactly as `role` gets one from
347/// the widget.
348pub const NODE_PROPERTIES: &[Property] = &[
349 Property::new(
350 "name",
351 PropertyKind::Text,
352 "What the application calls this node. Unique within the form.",
353 ),
354 Property::new("x", ANYWHERE, "Left edge, relative to the parent."),
355 Property::new("y", ANYWHERE, "Top edge, relative to the parent."),
356 Property::new(
357 "w",
358 PropertyKind::Int { min: 0, max: 8192 },
359 "Width in pixels.",
360 ),
361 Property::new(
362 "h",
363 PropertyKind::Int { min: 0, max: 8192 },
364 "Height in pixels.",
365 ),
366 Property::new(
367 "visible",
368 PropertyKind::Bool,
369 "Drawn and able to be touched, or neither.",
370 ),
371 Property::new(
372 "enabled",
373 PropertyKind::Bool,
374 "Takes input, or is greyed out and does not.",
375 ),
376 Property::new(
377 "z",
378 PropertyKind::Int {
379 min: -1000,
380 max: 1000,
381 },
382 "Paint order among siblings; higher is nearer the front.",
383 ),
384 Property::new(
385 "tooltip",
386 PropertyKind::Text,
387 "What resting the pointer on this node says.",
388 ),
389 Property::new(
390 "scroll",
391 PropertyKind::Bool,
392 "Whether children reaching past this node can be scrolled to.",
393 ),
394 Property::new(
395 "stack",
396 PropertyKind::Int { min: 0, max: 1000 },
397 "Stacks the children down the node with this many pixels between them.",
398 ),
399 Property::new(
400 "focus",
401 PropertyKind::Bool,
402 "Whether this node holds the caret when the form opens. One per form.",
403 ),
404 Property::new(
405 "anchor",
406 PropertyKind::Text,
407 "Edges held as the parent resizes: any of left, top, right, bottom.",
408 ),
409 Property::new(
410 "dock",
411 PropertyKind::Enum(DOCK_SIDES),
412 "An edge of the parent this node takes for itself, before the rest are placed.",
413 ),
414];
415
416/// The tree-owned property of this name, if there is one.
417/// ```
418/// # use denise_forms::node_property;
419/// // The tree owns geometry and visibility; no widget declares them.
420/// assert!(node_property("x").is_some());
421/// assert!(node_property("dock").is_some());
422/// // A widget's own property is not one of these.
423/// assert!(node_property("role").is_none());
424/// ```
425pub fn node_property(name: &str) -> Option<&'static Property> {
426 NODE_PROPERTIES.iter().find(|p| p.name == name)
427}
428
429/// Child nodes that are a parent's *content* rather than nodes of their own.
430const COLLECTIONS: &[&str] = &[
431 "option", "item", "column", "row", "event", "picture", "tab", "title",
432];
433
434/// The block a designer's placeholder content lives in.
435///
436/// A `table`'s rows and a `timeline`'s events are four names somebody typed so
437/// the widget looks like itself on a canvas; the application supplies the real
438/// ones. Written here, they are skipped by every build except a designer's, so
439/// they never reach a kiosk. See [`PropertyKind::Placeholder`].
440pub const DESIGN: &str = "design";
441
442/// Whether `kind` reads a collection called `name` from a `design` block.
443///
444/// Asked of the widget's own descriptor rather than a table kept here, which is
445/// the same rule the rest of the schema follows: a widget publishes what it
446/// takes, and nothing enumerates widgets.
447pub(crate) fn is_placeholder(kind: &str, name: &str) -> bool {
448 denise_ui::widgets::all()
449 .iter()
450 .find(|info| info.kind == kind)
451 .is_some_and(|info| {
452 info.properties
453 .iter()
454 .any(|p| p.name == name && p.kind == PropertyKind::Placeholder)
455 })
456}
457
458/// Whether a widget of this kind can hold nodes of their own.
459///
460/// Two do. Everything else either has no children or has *content* — a `select`
461/// holds `option`s, a `table` holds `column`s — which is not the same thing: a
462/// designer dropping a button on a `select` has missed, and dropping one on a
463/// `panel` means it.
464/// ```
465/// # use denise_forms::owns_children;
466/// assert!(owns_children("panel"));
467/// assert!(owns_children("collapse"));
468/// // Content is not children: a `select` holds options, and dropping a button
469/// // on one has missed.
470/// assert!(!owns_children("select"));
471/// assert!(!owns_children("label"));
472/// ```
473pub fn owns_children(kind: &str) -> bool {
474 matches!(kind, "panel" | "collapse")
475}
476
477/// The kinds that carry their text as the node's argument.
478///
479/// `label "Heading"` rather than `label text="Heading"`. Both build the same
480/// thing; the first is how every form in this repository is written, and is what
481/// [`seed`] produces.
482const ARGUMENT: &[&str] = &[
483 "label", "badge", "divider", "alert", "button", "checkbox", "toggle", "collapse",
484];
485
486/// How big a new widget of this kind should start out.
487///
488/// **Authoring defaults, not intrinsic sizes.** This toolkit has no layout engine
489/// and nothing here has a size of its own: a button is whatever rectangle the
490/// form gives it. These are the rectangles that make a dropped widget look like
491/// what it is, so that somebody can see what they placed before they resize it —
492/// which is a question about writing forms, and so this crate's, rather than a
493/// question about widgets.
494/// ```
495/// # use denise_forms::default_size;
496/// // A button is wider than it is tall; an avatar is square.
497/// let button = default_size("button");
498/// assert!(button.width > button.height);
499/// let avatar = default_size("avatar");
500/// assert_eq!(avatar.width, avatar.height);
501/// // A kind nobody has heard of still gets something you can see and click.
502/// assert!(default_size("banana").width > 0);
503/// ```
504pub fn default_size(kind: &str) -> Size {
505 let (width, height) = match kind {
506 "alert" => (320, 36),
507 "avatar" => (40, 40),
508 "badge" => (60, 20),
509 "button" => (100, 32),
510 "carousel" => (224, 120),
511 "checkbox" | "toggle" => (200, 24),
512 "collapse" => (224, 40),
513 "divider" => (160, 16),
514 "image" => (120, 90),
515 "list" => (200, 160),
516 "panel" => (200, 120),
517 "progress" => (200, 8),
518 "radial-progress" => (48, 48),
519 "radio-group" => (220, 76),
520 "rating" => (140, 24),
521 "select" | "text-input" => (220, 34),
522 "slider" => (200, 24),
523 "spinner" => (24, 24),
524 "table" => (320, 180),
525 "tree" => (220, 180),
526 "tabs" => (320, 36),
527 "menubar" => (320, 28),
528 "timeline" => (220, 140),
529 "video" => (160, 90),
530 // `label`, and anything this list has not heard of.
531 _ => (120, 20),
532 };
533 Size::new(width, height)
534}
535
536/// The smallest node of this kind that a form can actually hold, as file text.
537///
538/// What a designer writes when somebody drops a widget on the canvas. A rectangle
539/// is the most of it — but "a rect and nothing else" is not true of every widget,
540/// because three of them have a property the builder *requires*: an `alert` has
541/// no colour to draw itself in without a `role`, a `slider` has no range without
542/// `min` and `max`, and an `image` has nothing to draw without a `src`. A node
543/// missing one of those parses and then will not build, so a designer that wrote
544/// one would place a widget and break the form.
545///
546/// `select` and `collapse` were a fourth and fifth until #118, and they were the
547/// awkward ones: what they lacked was not a number but a *message*, so the seed
548/// had to invent a name nobody had asked for. Both have an inert constructor
549/// now, so a dropped one carries no message at all.
550///
551/// This lives beside the code that raises those requirements, so the two cannot
552/// drift; a test seeds every widget in [`all`](denise_ui::widgets::all), builds
553/// the result, and fails if a new one needs something this does not give it.
554///
555/// ```
556/// # use denise_forms::{seed, Form};
557/// use denise::Rect;
558///
559/// assert_eq!(
560/// seed("button", Rect::new(16, 24, 100, 32)),
561/// r#"button "button" x=16 y=24 w=100 h=32"#,
562/// );
563/// ```
564pub fn seed(kind: &str, rect: Rect) -> String {
565 let mut node = String::from(kind);
566 if ARGUMENT.contains(&kind) {
567 // The kind, as a placeholder. A label dropped with nothing to say draws
568 // nothing, and a widget you cannot see is a widget you cannot find
569 // again the moment you click somewhere else.
570 node.push_str(&format!(" {:?}", kind));
571 }
572 node.push_str(&format!(
573 " x={} y={} w={} h={}",
574 rect.x, rect.y, rect.width, rect.height
575 ));
576 // Only what the engine *requires*, and nothing a person would have to
577 // delete. `select` and `collapse` were here until #118 gave them inert
578 // constructors: they had to be seeded with a message nobody wanted, named
579 // after nothing, because a form file could not build either without one.
580 node.push_str(match kind {
581 "alert" => " role=info",
582 "slider" => " min=0 max=100",
583 // A path that is not there yet. An engine that cannot load it says so;
584 // a designer draws a hole and carries on.
585 "image" => " src=\"picture.png\"",
586 _ => "",
587 });
588 node
589}
590
591/// A whole form file with nothing in it yet, writing **only** what is not a
592/// default.
593///
594/// What *File → New* produces. A form that spelled out every default would read
595/// as a form somebody had made decisions about, and the next person would have
596/// to check each one against the schema to find out that none of them meant
597/// anything. The exception is `extent`, which a drawer and a shelf must say:
598/// this picks a third of the axis it comes in along, which is a drawer somebody
599/// will recognise rather than one they have to fix before they can see it.
600/// ```
601/// # use denise::Size;
602/// # use denise_forms::{Form, FormKind, seed_form};
603/// // A screen is every default but its size, so it says nothing else.
604/// let screen = seed_form("Untitled", FormKind::Screen, Size::new(800, 480));
605/// assert_eq!(screen, "form \"Untitled\" version=1 width=800 height=480\n");
606///
607/// // What comes in from an edge has to say how far, so this picks one.
608/// let drawer = seed_form("Filters", FormKind::Drawer, Size::new(1024, 600));
609/// let form = Form::parse(&drawer)?;
610/// assert_eq!(form.kind(), FormKind::Drawer);
611/// assert_eq!(form.extent(), 1024 / 3);
612/// # Ok::<(), denise_forms::Error>(())
613/// ```
614pub fn seed_form(title: &str, kind: FormKind, size: Size) -> String {
615 let mut out = format!("form {title:?} version={}", crate::form::VERSION);
616 if kind != FormKind::Screen {
617 out.push_str(&format!(" kind={}", FormKind::NAMES[kind as usize]));
618 }
619 out.push_str(&format!(" width={} height={}", size.width, size.height));
620 if matches!(kind, FormKind::Drawer | FormKind::Shelf) {
621 let along = match kind.default_side() {
622 denise_ui::Side::Above | denise_ui::Side::Below => size.height,
623 denise_ui::Side::Before | denise_ui::Side::After => size.width,
624 };
625 out.push_str(&format!(" extent={}", (along / 3).max(1)));
626 }
627 out.push('\n');
628 out
629}
630
631impl Form {
632 /// ```
633 /// # use denise_forms::{Form, Handler, Payload};
634 /// # use denise_ui::Ui;
635 /// #[derive(Clone, Copy, PartialEq, Debug)]
636 /// enum Message {
637 /// Greet,
638 /// }
639 ///
640 /// let form = Form::parse(
641 /// r#"form "Hello" version=1 width=320 height=120 { button "Greet" name=go x=8 y=8 w=90 h=30 on-press=greet }"#,
642 /// )?;
643 ///
644 /// let mut ui: Ui<Message> = Ui::new(form.size(), form.theme());
645 /// let root = ui.root();
646 ///
647 /// // The one thing a file cannot hold: this application's own message type.
648 /// let built = form.build(&mut ui, root, &mut |name: &str, payload: Payload| {
649 /// match (name, payload) {
650 /// ("greet", Payload::None) => Some(Handler::Plain(Message::Greet)),
651 /// _ => None,
652 /// }
653 /// })?;
654 ///
655 /// // What the file named, by the name it used.
656 /// let button = built.node("go").expect("the form names it `go`");
657 /// assert_eq!(built.len(), 1);
658 /// assert!(!built.is_empty());
659 ///
660 /// // And everything it put on screen, named or not, in file order.
661 /// assert_eq!(built.placed().len(), 1);
662 /// assert_eq!(built.at(&[0]).map(|node| node.kind), Some("button"));
663 /// assert_eq!(built.at(&[0]).map(|node| node.id), Some(button));
664 /// assert_eq!(
665 /// built.names().map(|(name, _)| name).collect::<Vec<_>>(),
666 /// vec!["go"],
667 /// );
668 /// # Ok::<(), denise_forms::Error>(())
669 /// ```
670 ///
671 /// Builds this form into `ui` under `parent`.
672 ///
673 /// Nodes are added in file order, so paint order is file order. See the
674 /// [crate documentation](crate) for what `wiring` supplies and why.
675 ///
676 /// # Errors
677 ///
678 /// Every failure carries a line and a column. See [`Reason`](crate::Reason)
679 /// for the whole list.
680 pub fn build<M: Clone + 'static>(
681 &self,
682 ui: &mut Ui<M>,
683 parent: NodeId,
684 wiring: &mut impl Wiring<M>,
685 ) -> Result<Built, Error> {
686 self.build_fitted(
687 ui,
688 parent,
689 Placement {
690 x: 1.0,
691 y: 1.0,
692 rect: Rect::from_size(self.size()),
693 },
694 wiring,
695 )
696 }
697
698 /// Builds this form at `scale`: every rectangle and every length in it
699 /// multiplied once, on the way in.
700 ///
701 /// The DPI answer this toolkit gives, for a form. An application computing
702 /// its own rectangles multiplies them itself — three lines, and
703 /// `examples/hello` is those three lines. A form file has no application
704 /// doing that, so the multiplying goes where the rectangles are computed,
705 /// which is here.
706 ///
707 /// **Two things the caller still has to do**, because neither belongs to a
708 /// subtree:
709 ///
710 /// ```no_run
711 /// # use denise::{Size, theme};
712 /// # use denise_forms::Form;
713 /// # use denise_ui::{Ui, Void};
714 /// # let form = Form::parse("").unwrap();
715 /// # let scale = 2.0;
716 /// // The theme's metrics, or every widget is the old size inside a new
717 /// // rectangle — a 2x button with a 6px corner on it.
718 /// let mut ui: Ui<Void> = Ui::new(Size::new(1920, 1080), form.theme().scaled(scale));
719 /// ```
720 ///
721 /// ...and putting the form where it goes, which is [`Form::fit`].
722 ///
723 /// **Text scales like a rectangle here, and that is a choice.** A 1024x600
724 /// form on a 1920x1080 panel gets 16 px text at 30 px. That is right when
725 /// the panel is the same screen at a higher density and wrong when it is a
726 /// bigger screen meant to show more. This does the first one. The second is
727 /// not a multiplication and no file can express it.
728 ///
729 /// # Errors
730 ///
731 /// The same as [`Form::build`]; scaling adds no failure of its own.
732 pub fn build_scaled<M: Clone + 'static>(
733 &self,
734 ui: &mut Ui<M>,
735 parent: NodeId,
736 scale: f32,
737 wiring: &mut impl Wiring<M>,
738 ) -> Result<Built, Error> {
739 self.build_fitted(
740 ui,
741 parent,
742 Placement {
743 x: scale,
744 y: scale,
745 rect: Rect::from_size(self.size()).scaled(scale),
746 },
747 wiring,
748 )
749 }
750
751 /// Builds the form **and the placeholder content a designer needs to see**.
752 ///
753 /// Every other build skips `design { … }` blocks, so a `table` comes up with
754 /// its columns and no rows and a `timeline` with no events: those are the
755 /// application's to supply, and a kiosk should not carry four names somebody
756 /// typed to make a canvas look right. A designer is the one caller that
757 /// wants them, because a table drawn with no rows is not a table anybody can
758 /// lay out against.
759 ///
760 /// `scale` is [`Form::build_scaled`]'s, so a designer's canvas magnifies the
761 /// same way.
762 ///
763 /// ```
764 /// # use denise_forms::{Form, Payload, Handler, Wiring};
765 /// # use denise_ui::{Ui, Void};
766 /// let source = r#"
767 /// form "F" version=1 width=200 height=80 {
768 /// table name=t x=0 y=0 w=200 h=80 {
769 /// column "Name"
770 /// design {
771 /// row "Ada"
772 /// }
773 /// }
774 /// }
775 /// "#;
776 /// let form = Form::parse(source)?;
777 /// let mut wiring = |_: &str, _: Payload| None::<Handler<Void>>;
778 ///
779 /// // What ships: the column, and no rows at all.
780 /// let mut ui: Ui<Void> = Ui::new(form.size(), form.theme());
781 /// let root = ui.root();
782 /// form.build(&mut ui, root, &mut wiring)?;
783 ///
784 /// // What the designer draws.
785 /// let mut canvas: Ui<Void> = Ui::new(form.size(), form.theme());
786 /// let root = canvas.root();
787 /// form.build_with_design(&mut canvas, root, 1.0, &mut wiring)?;
788 /// # Ok::<(), denise_forms::Error>(())
789 /// ```
790 pub fn build_with_design<M: Clone + 'static>(
791 &self,
792 ui: &mut Ui<M>,
793 parent: NodeId,
794 scale: f32,
795 wiring: &mut impl Wiring<M>,
796 ) -> Result<Built, Error> {
797 self.build_inner(
798 ui,
799 parent,
800 Placement {
801 x: scale,
802 y: scale,
803 rect: Rect::from_size(self.size()).scaled(scale),
804 },
805 wiring,
806 true,
807 )
808 }
809
810 /// Builds this form at a [`Fit`] — a factor per axis, which is what
811 /// [`Scaling::Stretch`](crate::Scaling::Stretch) needs and [`Form::fit`] works
812 /// out.
813 ///
814 /// Only [`Placement::x`] and [`Placement::y`] are read. [`Placement::rect`] is where the
815 /// *caller* puts the node this builds into, and is none of this method's
816 /// business: a form is built under whatever `parent` it is given.
817 ///
818 /// ```
819 /// # use denise::{Rect, Size, theme};
820 /// # use denise_forms::Form;
821 /// # use denise_ui::{Ui, Void, widgets::Panel};
822 /// let form = Form::parse(
823 /// r#"form "F" version=1 width=200 height=100 scaling=proportional {
824 /// label "Hi" name=hi x=10 y=10 w=100 h=20 size=16
825 /// }"#,
826 /// )?;
827 ///
828 /// let surface = Size::new(400, 400);
829 /// let fit = form.fit(surface);
830 ///
831 /// // The theme is scaled once, here, and the form is built into a panel at
832 /// // the rectangle the fit worked out.
833 /// let mut ui: Ui<Void> = Ui::new(surface, form.theme().scaled(fit.uniform()));
834 /// let root = ui.root();
835 /// let stage = ui.add(root, Panel::filled(form.background()), fit.rect).unwrap();
836 /// let mut nothing = |_: &str, _: denise_forms::Payload| None;
837 /// let built = form.build_fitted(&mut ui, stage, fit, &mut nothing)?;
838 ///
839 /// let hi = built.node("hi").expect("the form names it");
840 /// assert_eq!(ui.layout(hi), Some(Rect::new(20, 20, 200, 40)), "twice as big");
841 /// assert_eq!(
842 /// ui.get_property(hi, "size"),
843 /// Some(denise_ui::widgets::Value::Int(32)),
844 /// "and so is the text",
845 /// );
846 /// # Ok::<(), denise_forms::Error>(())
847 /// ```
848 ///
849 /// # Errors
850 ///
851 /// The same as [`Form::build`].
852 pub fn build_fitted<M: Clone + 'static>(
853 &self,
854 ui: &mut Ui<M>,
855 parent: NodeId,
856 fit: Placement,
857 wiring: &mut impl Wiring<M>,
858 ) -> Result<Built, Error> {
859 self.build_inner(ui, parent, fit, wiring, false)
860 }
861
862 /// See [`Form::build_fitted`] and [`Form::build_with_design`].
863 fn build_inner<M: Clone + 'static>(
864 &self,
865 ui: &mut Ui<M>,
866 parent: NodeId,
867 fit: Placement,
868 wiring: &mut impl Wiring<M>,
869 designing: bool,
870 ) -> Result<Built, Error> {
871 let mut builder = Builder {
872 form: self,
873 ui,
874 wiring,
875 fit,
876 designing,
877 built: Built::default(),
878 focused: None,
879 };
880 let children: Vec<&KdlNode> = self
881 .root()
882 .children()
883 .map(|d| d.nodes().iter().collect())
884 .unwrap_or_default();
885 for (index, node) in children.into_iter().enumerate() {
886 builder.node(node, parent, 0, &[index])?;
887 }
888 // The caret goes last, once every node exists: a form may name a field
889 // that appears after the one before it in the file.
890 let focused = builder.focused;
891 let built = builder.built;
892 if let Some(id) = focused {
893 ui.focus(Some(id));
894 }
895 Ok(built)
896 }
897}
898
899struct Builder<'a, M: 'static, W> {
900 form: &'a Form,
901 ui: &'a mut Ui<M>,
902 wiring: &'a mut W,
903 /// What every rectangle and every length is multiplied by on the way in.
904 /// `1.0` on both axes for [`Form::build`], which is why that is this with
905 /// nothing else said.
906 fit: Placement,
907 /// Whether the `design` blocks are read. False for every build but a
908 /// designer's, which is what keeps placeholder rows out of a kiosk.
909 designing: bool,
910 built: Built,
911 focused: Option<NodeId>,
912}
913
914impl<M: Clone + 'static, W: Wiring<M>> Builder<'_, M, W> {
915 fn err(&self, node: &KdlNode, reason: Reason) -> Error {
916 Error::new(self.form.at_node(node), reason)
917 }
918
919 /// Builds one node and everything under it.
920 fn node(
921 &mut self,
922 node: &KdlNode,
923 parent: NodeId,
924 depth: usize,
925 path: &[usize],
926 ) -> Result<(), Error> {
927 if depth >= MAX_DEPTH {
928 return Err(self.err(node, Reason::TooDeep { limit: MAX_DEPTH }));
929 }
930 let kind = node.name().value();
931 if COLLECTIONS.contains(&kind) {
932 // Reaching here means a collection node is somewhere its parent does
933 // not read it — `option` outside a `select`, say — which is a typo
934 // that would otherwise vanish silently.
935 return Err(self.err(
936 node,
937 Reason::UnexpectedChild {
938 parent: String::from("form"),
939 found: kind.to_string(),
940 },
941 ));
942 }
943 let info = *denise_ui::widgets::all()
944 .iter()
945 .find(|w| w.kind == kind)
946 .ok_or_else(|| {
947 self.err(
948 node,
949 Reason::UnknownWidget {
950 found: kind.to_string(),
951 },
952 )
953 })?;
954
955 self.check_properties(node, &info)?;
956 let rect = self.rect(node)?;
957 let id = self.construct(node, &info, parent, rect)?;
958 self.apply_properties(node, &info, id)?;
959 self.apply_node_properties(node, id)?;
960 self.built.placed.push(Placed {
961 id,
962 parent: (depth > 0).then_some(parent),
963 kind: info.kind,
964 name: self.string(node, "name"),
965 path: path.to_vec(),
966 });
967
968 // Children that are not the parent's own content are nodes in their own
969 // right. A widget that cannot lay children out says so.
970 if let Some(children) = node.children() {
971 let owns_children = owns_children(kind);
972 // Which `tab` this is, counted over **every** tab and not only the
973 // ones carrying a page: `selected` is an index into the strip, so a
974 // file part-way through gaining pages must still show the right
975 // one.
976 let mut tabs_seen = 0usize;
977 for (index, child) in children.nodes().iter().enumerate() {
978 let name = child.name().value();
979 if name == DESIGN {
980 self.check_design(child, kind)?;
981 continue;
982 }
983 // Placeholder content written where the engine would load it,
984 // which is the shape this format used to have and the one thing
985 // `design` exists to stop.
986 if is_placeholder(kind, name) {
987 return Err(self.err(
988 child,
989 Reason::PlaceholderOutside {
990 kind: kind.to_string(),
991 found: name.to_string(),
992 },
993 ));
994 }
995 // A `tab` carrying children is a **page**: the labels are the
996 // strip's content, and what is nested under one is a subtree
997 // the file now describes. Everything else in COLLECTIONS is
998 // content and has no children to walk.
999 if name == "tab" && kind == "tabs" {
1000 let ordinal = tabs_seen;
1001 tabs_seen += 1;
1002 if child.children().is_some_and(|b| !b.nodes().is_empty()) {
1003 let mut below = path.to_vec();
1004 below.push(index);
1005 self.page(child, node, id, depth, &below, ordinal)?;
1006 }
1007 continue;
1008 }
1009 if COLLECTIONS.contains(&name) {
1010 continue;
1011 }
1012 if !owns_children {
1013 return Err(self.err(
1014 child,
1015 Reason::UnexpectedChild {
1016 parent: kind.to_string(),
1017 found: name.to_string(),
1018 },
1019 ));
1020 }
1021 let mut below = path.to_vec();
1022 below.push(index);
1023 self.node(child, id, depth + 1, &below)?;
1024 }
1025 }
1026 Ok(())
1027 }
1028
1029 /// A `design` block holds this widget's placeholder collections and nothing
1030 /// else.
1031 ///
1032 /// Narrow on purpose. `design` is not a general "ignore this" block — a
1033 /// widget hidden in one would be a widget the file describes and the engine
1034 /// never builds, which is a bigger idea than #160 asked for and a worse one
1035 /// to discover by accident.
1036 fn check_design(&self, block: &KdlNode, kind: &str) -> Result<(), Error> {
1037 let Some(children) = block.children() else {
1038 return Ok(());
1039 };
1040 for child in children.nodes() {
1041 let name = child.name().value();
1042 if !is_placeholder(kind, name) {
1043 return Err(self.err(
1044 child,
1045 Reason::UnexpectedChild {
1046 parent: format!("{kind}'s `design`"),
1047 found: name.to_string(),
1048 },
1049 ));
1050 }
1051 }
1052 Ok(())
1053 }
1054
1055 /// Every property in the file is one the tree owns or one the widget declares.
1056 fn check_properties(&self, node: &KdlNode, info: &WidgetInfo) -> Result<(), Error> {
1057 for entry in node.entries() {
1058 let Some(name) = entry.name() else {
1059 continue;
1060 };
1061 let name = name.value();
1062 if node_property(name).is_some() || info.property(name).is_some() {
1063 continue;
1064 }
1065 return Err(Error::new(
1066 self.form.at(entry.span().offset()),
1067 Reason::UnknownProperty {
1068 kind: info.kind,
1069 found: name.to_string(),
1070 accepted: info.properties,
1071 },
1072 ));
1073 }
1074 Ok(())
1075 }
1076
1077 fn rect(&self, node: &KdlNode) -> Result<Rect, Error> {
1078 let mut axes = [0i32; 4];
1079 for (slot, name) in axes.iter_mut().zip(["x", "y", "w", "h"]) {
1080 let value = node
1081 .get(name)
1082 .and_then(KdlValue::as_integer)
1083 .ok_or_else(|| {
1084 self.err(
1085 node,
1086 Reason::Missing {
1087 kind: node.name().value().to_string(),
1088 name: match name {
1089 "x" => "x",
1090 "y" => "y",
1091 "w" => "w",
1092 _ => "h",
1093 },
1094 },
1095 )
1096 })?;
1097 *slot = i32::try_from(value).unwrap_or(i32::MAX);
1098 }
1099 // By its edges rather than its width and height: two panels designed to
1100 // touch still touch at a fractional scale. See [`Rect::scaled_by`].
1101 Ok(Rect::new(axes[0], axes[1], axes[2], axes[3]).scaled_by(self.fit.x, self.fit.y))
1102 }
1103
1104 /// The node's single positional argument, as a string.
1105 fn arg(&self, node: &KdlNode) -> Option<String> {
1106 node.entries()
1107 .iter()
1108 .find(|e| e.name().is_none())
1109 .and_then(|e| e.value().as_string())
1110 .map(str::to_string)
1111 }
1112
1113 fn string(&self, node: &KdlNode, name: &str) -> Option<String> {
1114 node.get(name)
1115 .and_then(KdlValue::as_string)
1116 .map(str::to_string)
1117 }
1118
1119 fn number(&self, node: &KdlNode, name: &str) -> Option<f32> {
1120 node.get(name).and_then(|v| {
1121 v.as_float()
1122 .map(|f| f as f32)
1123 .or_else(|| v.as_integer().map(|i| i as f32))
1124 })
1125 }
1126
1127 /// A message the file named, in the shape this widget needs.
1128 fn handler(
1129 &mut self,
1130 node: &KdlNode,
1131 property: &str,
1132 payload: Payload,
1133 ) -> Result<Option<Handler<M>>, Error> {
1134 let Some(name) = self.string(node, property) else {
1135 return Ok(None);
1136 };
1137 match self.wiring.message(&name, payload) {
1138 Some(handler) => Ok(Some(handler)),
1139 None => Err(self.err(node, Reason::UnknownMessage { found: name })),
1140 }
1141 }
1142
1143 fn plain(&self, node: &KdlNode, name: &str, handler: Handler<M>) -> Result<M, Error> {
1144 match handler {
1145 Handler::Plain(message) => Ok(message),
1146 _ => Err(self.wrong(node, name, Payload::None)),
1147 }
1148 }
1149
1150 fn on_bool(
1151 &self,
1152 node: &KdlNode,
1153 name: &str,
1154 handler: Handler<M>,
1155 ) -> Result<fn(bool) -> M, Error> {
1156 match handler {
1157 Handler::Bool(f) => Ok(f),
1158 _ => Err(self.wrong(node, name, Payload::Bool)),
1159 }
1160 }
1161
1162 fn on_index(
1163 &self,
1164 node: &KdlNode,
1165 name: &str,
1166 handler: Handler<M>,
1167 ) -> Result<fn(usize) -> M, Error> {
1168 match handler {
1169 Handler::Index(f) => Ok(f),
1170 _ => Err(self.wrong(node, name, Payload::Index)),
1171 }
1172 }
1173
1174 fn on_number(
1175 &self,
1176 node: &KdlNode,
1177 name: &str,
1178 handler: Handler<M>,
1179 ) -> Result<fn(f32) -> M, Error> {
1180 match handler {
1181 Handler::Number(f) => Ok(f),
1182 _ => Err(self.wrong(node, name, Payload::Number)),
1183 }
1184 }
1185
1186 fn wrong(&self, node: &KdlNode, property: &str, payload: Payload) -> Error {
1187 self.err(
1188 node,
1189 Reason::WrongMessage {
1190 found: self.string(node, property).unwrap_or_default(),
1191 wanted: Handler::<M>::wanted(payload),
1192 },
1193 )
1194 }
1195
1196 fn required(&self, node: &KdlNode, name: &'static str) -> Error {
1197 self.err(
1198 node,
1199 Reason::Missing {
1200 kind: node.name().value().to_string(),
1201 name,
1202 },
1203 )
1204 }
1205
1206 /// The child nodes of one collection kind.
1207 /// The child nodes of `node` called `name`.
1208 ///
1209 /// A placeholder collection lives one level down, in the `design` block,
1210 /// and is read only when the caller asked for it — so an application's
1211 /// build sees no rows at all and a kiosk carries none.
1212 fn collection<'n>(&self, node: &'n KdlNode, name: &str) -> Vec<&'n KdlNode> {
1213 let holder = if is_placeholder(node.name().value(), name) {
1214 if !self.designing {
1215 return Vec::new();
1216 }
1217 let Some(design) = self.design_block(node) else {
1218 return Vec::new();
1219 };
1220 design
1221 } else {
1222 let Some(children) = node.children() else {
1223 return Vec::new();
1224 };
1225 children
1226 };
1227 holder
1228 .nodes()
1229 .iter()
1230 .filter(|n| n.name().value() == name)
1231 .collect()
1232 }
1233
1234 /// Builds one tab's page: a container under the strip, and its subtree.
1235 ///
1236 /// The page fills what is left of the `tabs` node below the strip band, the
1237 /// way a `collapse`'s body fills what is left below its header. So a
1238 /// widget written at `y=0` inside a tab sits just under the strip, and a
1239 /// tab's rectangles are read the same way as any other container's.
1240 ///
1241 /// Only the selected page is visible. `selected` is the tab the
1242 /// *application* starts on; which page a designer is looking at is the
1243 /// designer's business and stays out of the file.
1244 fn page(
1245 &mut self,
1246 tab: &KdlNode,
1247 tabs: &KdlNode,
1248 strip: NodeId,
1249 depth: usize,
1250 path: &[usize],
1251 ordinal: usize,
1252 ) -> Result<(), Error> {
1253 let Some(bounds) = self.ui.bounds(strip) else {
1254 return Ok(());
1255 };
1256 // The band the strip draws in, read from the same place the widget
1257 // reads it: `Tabs::strip_height` is the theme's field height, and the
1258 // theme this tree holds is already in the units these rectangles are
1259 // in. Scaling it again here would put every page at twice the offset
1260 // the strip is drawn at.
1261 let band = self.ui.theme().metrics.size_field.max(1);
1262 let rect = Rect::new(0, band, bounds.width, (bounds.height - band).max(0));
1263 let page = self
1264 .ui
1265 .add(strip, Panel::bare(), rect)
1266 .ok_or_else(|| self.err(tab, Reason::TreeRefused))?;
1267
1268 if let Some(children) = tab.children() {
1269 for (index, child) in children.nodes().iter().enumerate() {
1270 let mut below = path.to_vec();
1271 below.push(index);
1272 self.node(child, page, depth + 1, &below)?;
1273 }
1274 }
1275
1276 // After the children, not before: hiding a node propagates to the
1277 // subtree it has *at the time*, and one hidden first would have its
1278 // pages added back into view behind it.
1279 let selected = tabs
1280 .get("selected")
1281 .and_then(KdlValue::as_integer)
1282 .unwrap_or(0);
1283 let shown = usize::try_from(selected).unwrap_or(0) == ordinal;
1284 self.ui.set_visible(page, shown);
1285 self.built.pages.push(Page {
1286 path: path.to_vec(),
1287 ordinal,
1288 id: page,
1289 });
1290 Ok(())
1291 }
1292
1293 /// Whether any `tab` under `node` carries a page of its own.
1294 fn has_pages(&self, node: &KdlNode) -> bool {
1295 self.collection(node, "tab").into_iter().any(|tab| {
1296 tab.children()
1297 .is_some_and(|block| !block.nodes().is_empty())
1298 })
1299 }
1300
1301 /// The `design` block of `node`, if it wrote one.
1302 fn design_block<'n>(&self, node: &'n KdlNode) -> Option<&'n KdlDocument> {
1303 node.children()?
1304 .nodes()
1305 .iter()
1306 .find(|n| n.name().value() == DESIGN)?
1307 .children()
1308 }
1309
1310 fn strings(&self, node: &KdlNode, name: &str) -> Vec<String> {
1311 self.collection(node, name)
1312 .into_iter()
1313 .map(|n| self.arg(n).unwrap_or_default())
1314 .collect()
1315 }
1316
1317 fn picture(&mut self, node: &KdlNode, path: &str) -> Result<Picture, Error> {
1318 self.wiring.asset(path).ok_or_else(|| {
1319 Error::new(
1320 self.form.at_node(node),
1321 Reason::Asset {
1322 path: path.to_string(),
1323 },
1324 )
1325 })
1326 }
1327}
1328
1329// The construction match is long because there are twenty-five widgets and no
1330// two constructors are alike. It is deliberately not clever: a table of
1331// constructors would need one type for all of them, and they differ in exactly
1332// the way that would make that a lie.
1333impl<M: Clone + 'static, W: Wiring<M>> Builder<'_, M, W> {
1334 fn construct(
1335 &mut self,
1336 node: &KdlNode,
1337 info: &WidgetInfo,
1338 parent: NodeId,
1339 rect: Rect,
1340 ) -> Result<NodeId, Error> {
1341 let text = self.arg(node).unwrap_or_default();
1342 let id = match info.kind {
1343 "label" => self.ui.add(parent, Label::new(text), rect),
1344 "panel" => self.ui.add(parent, Panel::default(), rect),
1345 "badge" => self.ui.add(parent, Badge::new(text), rect),
1346 "divider" => {
1347 let divider = if self.arg(node).is_some() {
1348 Divider::labelled(text)
1349 } else {
1350 Divider::new()
1351 };
1352 self.ui.add(parent, divider, rect)
1353 }
1354 "alert" => {
1355 let role = self
1356 .string(node, "role")
1357 .ok_or_else(|| self.required(node, "role"))?;
1358 let role = role_from_name(&role).ok_or_else(|| {
1359 self.err(
1360 node,
1361 Reason::NotAName {
1362 name: String::from("colour role"),
1363 found: role.clone(),
1364 accepted: ROLES,
1365 },
1366 )
1367 })?;
1368 self.ui.add(parent, Alert::new(role, text), rect)
1369 }
1370 "spinner" => self.ui.add(parent, Spinner::new(), rect),
1371 "video" => self.ui.add(parent, Video::new(), rect),
1372 "progress" => {
1373 let value = self.number(node, "value").unwrap_or(0.0);
1374 self.ui.add(parent, Progress::new(value), rect)
1375 }
1376 "radial-progress" => {
1377 let value = self.number(node, "value").unwrap_or(0.0);
1378 self.ui.add(parent, RadialProgress::new(value), rect)
1379 }
1380 "button" => {
1381 let button = match self.handler(node, "on-press", Payload::None)? {
1382 Some(h) => Button::new(text, self.plain(node, "on-press", h)?),
1383 None => Button::inert(text),
1384 };
1385 self.ui.add(parent, button, rect)
1386 }
1387 "text-input" => {
1388 let mut field = TextInput::<M>::new();
1389 if let Some(h) = self.handler(node, "on-submit", Payload::None)? {
1390 field = field.with_submit(self.plain(node, "on-submit", h)?);
1391 }
1392 self.ui.add(parent, field, rect)
1393 }
1394 "checkbox" => {
1395 let widget = match self.handler(node, "on-change", Payload::Bool)? {
1396 Some(h) => Checkbox::new(text, self.on_bool(node, "on-change", h)?),
1397 None => Checkbox::inert(text),
1398 };
1399 self.ui.add(parent, widget, rect)
1400 }
1401 "toggle" => {
1402 let widget = match self.handler(node, "on-change", Payload::Bool)? {
1403 Some(h) => Toggle::new(text, self.on_bool(node, "on-change", h)?),
1404 None => Toggle::inert(text),
1405 };
1406 self.ui.add(parent, widget, rect)
1407 }
1408 "slider" => {
1409 let min = self
1410 .number(node, "min")
1411 .ok_or_else(|| self.required(node, "min"))?;
1412 let max = self
1413 .number(node, "max")
1414 .ok_or_else(|| self.required(node, "max"))?;
1415 let value = self.number(node, "value").unwrap_or(min);
1416 let widget = match self.handler(node, "on-change", Payload::Number)? {
1417 Some(h) => Slider::new(min, max, value, self.on_number(node, "on-change", h)?),
1418 None => Slider::inert(min, max, value),
1419 };
1420 self.ui.add(parent, widget, rect)
1421 }
1422 "rating" => {
1423 let value = self.number(node, "value").unwrap_or(0.0);
1424 let widget = match self.handler(node, "on-change", Payload::Number)? {
1425 Some(h) => Rating::new(value, self.on_number(node, "on-change", h)?),
1426 None => Rating::display(value),
1427 };
1428 self.ui.add(parent, widget, rect)
1429 }
1430 "radio-group" => {
1431 let options = self.strings(node, "option");
1432 let widget = match self.handler(node, "on-change", Payload::Index)? {
1433 Some(h) => RadioGroup::new(options, self.on_index(node, "on-change", h)?),
1434 None => RadioGroup::inert(options),
1435 };
1436 self.ui.add(parent, widget, rect)
1437 }
1438 "menubar" => {
1439 let titles = self.strings(node, "title");
1440 // Without `on-open` there is nothing to press: the bar reports
1441 // which title was chosen and the application opens the menu, so
1442 // an inert one is titles and nothing else. See `MenuBar::inert`.
1443 let widget = match self.handler(node, "on-open", Payload::Index)? {
1444 Some(h) => MenuBar::new(titles, self.on_index(node, "on-open", h)?),
1445 None => MenuBar::inert(titles),
1446 };
1447 self.ui.add(parent, widget, rect)
1448 }
1449 "tabs" => {
1450 let labels = self.strings(node, "tab");
1451 let widget = match self.handler(node, "on-change", Payload::Index)? {
1452 Some(h) => Tabs::new(labels, self.on_index(node, "on-change", h)?),
1453 None => Tabs::inert(labels),
1454 };
1455 // A `tab` carrying children makes this a strip *over a page*,
1456 // drawn in a band along the top with the page below it. A file
1457 // whose tabs are bare labels is what a `tabs` node has always
1458 // been, and is untouched. See `Tabs::over_pages`.
1459 let widget = if self.has_pages(node) {
1460 widget.over_pages()
1461 } else {
1462 widget
1463 };
1464 self.ui.add(parent, widget, rect)
1465 }
1466 "select" => {
1467 let options = self.strings(node, "option");
1468 // Without `on-change` the list cannot be opened — the popup is a
1469 // scene the application pushes — so an inert one shows what is
1470 // chosen and stays shut. See `Select::inert`.
1471 let widget = match self.handler(node, "on-change", Payload::None)? {
1472 Some(h) => Select::new(options, self.plain(node, "on-change", h)?),
1473 None => Select::inert(options),
1474 };
1475 self.ui.add(parent, widget, rect)
1476 }
1477 "collapse" => {
1478 // An inert one folds itself, so a decorative section needs no
1479 // message. See `Collapse::inert`.
1480 let widget = match self.handler(node, "on-toggle", Payload::Bool)? {
1481 Some(h) => Collapse::new(text, self.on_bool(node, "on-toggle", h)?),
1482 None => Collapse::inert(text),
1483 };
1484 self.ui.add(parent, widget, rect)
1485 }
1486 "list" => {
1487 let items: Vec<ListItem> = self
1488 .collection(node, "item")
1489 .into_iter()
1490 .map(|n| {
1491 let mut item = ListItem::new(self.arg(n).unwrap_or_default());
1492 if let Some(leading) = self.string(n, "leading") {
1493 item = item.with_leading(leading);
1494 }
1495 if let Some(trailing) = self.string(n, "trailing") {
1496 item = item.with_trailing(trailing);
1497 }
1498 if n.get("enabled").and_then(KdlValue::as_bool) == Some(false) {
1499 item = item.disabled();
1500 }
1501 item
1502 })
1503 .collect();
1504 let mut widget = match self.handler(node, "on-select", Payload::Index)? {
1505 Some(h) => List::new(items, self.on_index(node, "on-select", h)?),
1506 None => List::inert(items),
1507 };
1508 if let Some(h) = self.handler(node, "on-activate", Payload::Index)? {
1509 widget = widget.on_activate(self.on_index(node, "on-activate", h)?);
1510 }
1511 self.ui.add(parent, widget, rect)
1512 }
1513 "tree" => {
1514 let items: Vec<TreeItem> = self
1515 .collection(node, "item")
1516 .into_iter()
1517 .map(|n| {
1518 let mut item = TreeItem::new(self.arg(n).unwrap_or_default());
1519 if let Some(depth) = n.get("depth").and_then(KdlValue::as_integer) {
1520 item = item.at_depth(depth.clamp(0, i128::from(u16::MAX)) as u16);
1521 }
1522 if n.get("open").and_then(KdlValue::as_bool) == Some(false) {
1523 item = item.shut();
1524 }
1525 if let Some(leading) = self.string(n, "leading") {
1526 item = item.with_leading(leading);
1527 }
1528 if let Some(trailing) = self.string(n, "trailing") {
1529 item = item.with_trailing(trailing);
1530 }
1531 if n.get("enabled").and_then(KdlValue::as_bool) == Some(false) {
1532 item = item.disabled();
1533 }
1534 item
1535 })
1536 .collect();
1537 let mut widget = match self.handler(node, "on-select", Payload::Index)? {
1538 Some(h) => Tree::new(items, self.on_index(node, "on-select", h)?),
1539 None => Tree::inert(items),
1540 };
1541 if let Some(h) = self.handler(node, "on-activate", Payload::Index)? {
1542 widget = widget.on_activate(self.on_index(node, "on-activate", h)?);
1543 }
1544 if let Some(h) = self.handler(node, "on-toggle", Payload::Index)? {
1545 widget = widget.on_toggle(self.on_index(node, "on-toggle", h)?);
1546 }
1547 self.ui.add(parent, widget, rect)
1548 }
1549 "table" => {
1550 let columns: Vec<Column> = self
1551 .collection(node, "column")
1552 .into_iter()
1553 .map(|n| {
1554 let title = self.arg(n).unwrap_or_default();
1555 let mut column = match n.get("width").and_then(KdlValue::as_integer) {
1556 Some(width) => Column::new(title, width as i32),
1557 None => Column::flex(title),
1558 };
1559 match n.get("align").and_then(KdlValue::as_string) {
1560 Some("end") => column = column.align_end(),
1561 Some("center") => column = column.align_center(),
1562 _ => {}
1563 }
1564 column
1565 })
1566 .collect();
1567 let rows: Vec<Vec<String>> = self
1568 .collection(node, "row")
1569 .into_iter()
1570 .map(|n| {
1571 n.entries()
1572 .iter()
1573 .filter(|e| e.name().is_none())
1574 .map(|e| e.value().as_string().unwrap_or_default().to_string())
1575 .collect()
1576 })
1577 .collect();
1578 let mut widget = match self.handler(node, "on-select", Payload::Index)? {
1579 Some(h) => Table::new(columns, self.on_index(node, "on-select", h)?),
1580 None => Table::inert(columns),
1581 };
1582 widget = widget.with_rows(rows);
1583 if let Some(h) = self.handler(node, "on-activate", Payload::Index)? {
1584 widget = widget.on_activate(self.on_index(node, "on-activate", h)?);
1585 }
1586 self.ui.add(parent, widget, rect)
1587 }
1588 "timeline" => {
1589 let events: Vec<TimelineItem> = self
1590 .collection(node, "event")
1591 .into_iter()
1592 .map(|n| {
1593 let mut item = TimelineItem::new(self.arg(n).unwrap_or_default());
1594 if let Some(time) = self.string(n, "time") {
1595 item = item.with_time(time);
1596 }
1597 if let Some(role) =
1598 self.string(n, "role").as_deref().and_then(role_from_name)
1599 {
1600 item = item.with_role(role);
1601 }
1602 if n.get("pending").and_then(KdlValue::as_bool) == Some(true) {
1603 item = item.pending();
1604 }
1605 item
1606 })
1607 .collect();
1608 self.ui.add(parent, Timeline::new(events), rect)
1609 }
1610 "image" => {
1611 let path = self
1612 .string(node, "src")
1613 .ok_or_else(|| self.required(node, "src"))?;
1614 let picture = self.picture(node, &path)?;
1615 self.ui
1616 .add(parent, Image::new(picture.pixels, picture.size), rect)
1617 }
1618 "avatar" => {
1619 let avatar = match self.string(node, "src") {
1620 Some(path) => {
1621 let picture = self.picture(node, &path)?;
1622 Avatar::new(picture.pixels, picture.size)
1623 }
1624 None => {
1625 Avatar::initials(self.string(node, "initials").unwrap_or_default().as_str())
1626 }
1627 };
1628 self.ui.add(parent, avatar, rect)
1629 }
1630 "carousel" => {
1631 let mut widget = match self.handler(node, "on-change", Payload::Index)? {
1632 Some(h) => Carousel::new(self.on_index(node, "on-change", h)?),
1633 None => Carousel::inert(),
1634 };
1635 for picture_node in self.collection(node, "picture") {
1636 let path = self
1637 .string(picture_node, "src")
1638 .ok_or_else(|| self.required(picture_node, "src"))?;
1639 let picture = self.picture(picture_node, &path)?;
1640 let fit = match self.string(picture_node, "fit").as_deref() {
1641 Some("fill") => Fit::Fill,
1642 Some("cover") => Fit::Cover,
1643 Some("center") => Fit::Center,
1644 _ => Fit::Contain,
1645 };
1646 widget = widget.with_picture_fit(picture.pixels, picture.size, fit);
1647 }
1648 self.ui.add(parent, widget, rect)
1649 }
1650 other => {
1651 // `all()` matched a kind this match does not, which means a
1652 // widget joined the catalogue and not the builder.
1653 return Err(self.err(
1654 node,
1655 Reason::UnknownWidget {
1656 found: other.to_string(),
1657 },
1658 ));
1659 }
1660 };
1661 id.ok_or_else(|| self.err(node, Reason::TreeRefused))
1662 }
1663
1664 /// Applies every widget property the file gives, **in descriptor order**.
1665 ///
1666 /// Descriptor order rather than file order, and deliberately: a slider's
1667 /// `value` is clamped into its `min`/`max`, so a file that wrote them the
1668 /// other way round would otherwise land somewhere else than one that did not.
1669 /// The widget publishes the order it wants to be told things in, and this
1670 /// obeys it — so two files that say the same thing build the same tree.
1671 fn apply_properties(
1672 &mut self,
1673 node: &KdlNode,
1674 info: &WidgetInfo,
1675 id: NodeId,
1676 ) -> Result<(), Error> {
1677 for property in info.properties {
1678 if !property.is_settable() {
1679 // A message or an asset; both were given to the constructor.
1680 continue;
1681 }
1682 let Some(entry) = node
1683 .entries()
1684 .iter()
1685 .find(|e| e.name().map(kdl::KdlIdentifier::value) == Some(property.name))
1686 else {
1687 continue;
1688 };
1689 let at = self.form.at(entry.span().offset());
1690 let mut value = self.convert(at, info.kind, property, entry.value())?;
1691 if property.pixels {
1692 value = self.lengthened(value);
1693 }
1694 if let Some(Err(error)) = self.ui.set_property(id, property.name, value) {
1695 return Err(Error::new(
1696 at,
1697 Reason::WrongType {
1698 kind: info.kind,
1699 name: property.name.to_string(),
1700 wanted: match error.mismatch {
1701 denise_ui::widgets::Mismatch::WrongType { expected } => expected.noun(),
1702 _ => "something else",
1703 },
1704 },
1705 ));
1706 }
1707 }
1708 Ok(())
1709 }
1710
1711 /// One length, at the scale this form is being built at.
1712 ///
1713 /// Which numbers are lengths is the **widget's** to say, not this crate's:
1714 /// see [`Property::pixels`](denise_ui::widgets::Property::pixels). A text
1715 /// size is a length and doubles at 2x; a duration in milliseconds is not and
1716 /// does not; a selected index is not and would be nonsense if it did.
1717 ///
1718 /// [`Placement::uniform`] rather than the axis factors, because none of these is
1719 /// horizontal or vertical: a text size is a size, and a border is as thick
1720 /// on the top as on the left.
1721 fn lengthened(&self, value: Value) -> Value {
1722 let scale = self.fit.uniform();
1723 match value {
1724 // At least one: a border that rounded to nothing at 0.75x has been
1725 // deleted rather than scaled, and the same for a one-pixel divider.
1726 // Zero stays zero, because zero was somebody saying "none".
1727 Value::Int(n) if n != 0 => {
1728 let scaled = (n as f32 * scale + 0.5) as i32;
1729 Value::Int(if n > 0 { scaled.max(1) } else { scaled.min(-1) })
1730 }
1731 Value::Float(f) => Value::Float(f * scale),
1732 other => other,
1733 }
1734 }
1735
1736 /// A value from the file, in the shape the property takes.
1737 fn convert(
1738 &self,
1739 at: At,
1740 kind: &'static str,
1741 property: &Property,
1742 value: &KdlValue,
1743 ) -> Result<Value, Error> {
1744 let wrong = |wanted: &'static str| {
1745 Error::new(
1746 at,
1747 Reason::WrongType {
1748 kind,
1749 name: property.name.to_string(),
1750 wanted,
1751 },
1752 )
1753 };
1754 Ok(match property.kind {
1755 PropertyKind::Text | PropertyKind::Color => {
1756 Value::text(value.as_string().ok_or_else(|| wrong("a string"))?)
1757 }
1758 PropertyKind::Bool => {
1759 Value::Bool(value.as_bool().ok_or_else(|| wrong("true or false"))?)
1760 }
1761 PropertyKind::Int { .. } => {
1762 let number = value.as_integer().ok_or_else(|| wrong("a whole number"))?;
1763 Value::Int(i32::try_from(number).map_err(|_| wrong("a whole number"))?)
1764 }
1765 PropertyKind::Float { .. } => {
1766 let number = value
1767 .as_float()
1768 .map(|f| f as f32)
1769 .or_else(|| value.as_integer().map(|i| i as f32))
1770 .ok_or_else(|| wrong("a number"))?;
1771 Value::Float(number)
1772 }
1773 PropertyKind::Enum(names) => {
1774 let found = value
1775 .as_string()
1776 .ok_or_else(|| wrong("one of the listed names"))?;
1777 let name = names.iter().copied().find(|n| *n == found).ok_or_else(|| {
1778 Error::new(
1779 at,
1780 Reason::NotAName {
1781 name: property.name.to_string(),
1782 found: found.to_string(),
1783 accepted: names,
1784 },
1785 )
1786 })?;
1787 Value::Enum(name)
1788 }
1789 // Filtered out by `is_settable` before this is reached.
1790 PropertyKind::Message(_) | PropertyKind::Asset => return Err(wrong("nothing here")),
1791 _ => return Err(wrong("a value this crate does not know")),
1792 })
1793 }
1794
1795 /// The properties the tree owns.
1796 fn apply_node_properties(&mut self, node: &KdlNode, id: NodeId) -> Result<(), Error> {
1797 if let Some(name) = self.string(node, "name") {
1798 if self.built.names.contains_key(&name) {
1799 return Err(self.err(node, Reason::DuplicateName { name }));
1800 }
1801 self.built.names.insert(name, id);
1802 }
1803 if let Some(text) = self.string(node, "tooltip") {
1804 self.ui.set_tooltip(id, text);
1805 }
1806 if let Some(z) = node.get("z").and_then(KdlValue::as_integer) {
1807 self.ui.set_z(id, z as i32);
1808 }
1809 if node.get("scroll").and_then(KdlValue::as_bool) == Some(true) {
1810 self.ui.set_scrollable(id, true);
1811 }
1812 if let Some(spacing) = node.get("stack").and_then(KdlValue::as_integer) {
1813 self.ui.set_stack(id, spacing as i32);
1814 }
1815 if let Some(anchor) = self.string(node, "anchor") {
1816 let mut anchors = Anchors::new(false, false, false, false);
1817 for edge in anchor.split_whitespace() {
1818 match edge {
1819 "left" => anchors.left = true,
1820 "top" => anchors.top = true,
1821 "right" => anchors.right = true,
1822 "bottom" => anchors.bottom = true,
1823 other => {
1824 return Err(self.err(
1825 node,
1826 Reason::NotAName {
1827 name: String::from("anchor edge"),
1828 found: other.to_string(),
1829 accepted: ANCHOR_EDGES,
1830 },
1831 ));
1832 }
1833 }
1834 }
1835 self.ui.set_anchors(id, anchors);
1836 }
1837 if let Some(dock) = self.string(node, "dock") {
1838 let side = match dock.as_str() {
1839 "top" => Dock::Top,
1840 "bottom" => Dock::Bottom,
1841 "left" => Dock::Left,
1842 "right" => Dock::Right,
1843 "fill" => Dock::Fill,
1844 other => {
1845 return Err(self.err(
1846 node,
1847 Reason::NotAName {
1848 name: String::from("dock side"),
1849 found: other.to_string(),
1850 accepted: DOCK_SIDES,
1851 },
1852 ));
1853 }
1854 };
1855 self.ui.set_dock(id, Some(side));
1856 }
1857 if node.get("enabled").and_then(KdlValue::as_bool) == Some(false) {
1858 self.ui.set_enabled(id, false);
1859 }
1860 if node.get("focus").and_then(KdlValue::as_bool) == Some(true) {
1861 if self.focused.is_some() {
1862 return Err(self.err(node, Reason::TwoFocuses));
1863 }
1864 self.focused = Some(id);
1865 }
1866 // Last: a hidden node's children still had to be built and placed, and
1867 // hiding it first would have them laid out against a node with no bounds.
1868 if node.get("visible").and_then(KdlValue::as_bool) == Some(false) {
1869 self.ui.set_visible(id, false);
1870 }
1871 Ok(())
1872 }
1873}
1874
1875// Unused-import guard: these name tables are the ones the schema documents, and
1876// referencing them here keeps a rename in `denise-ui` from silently drifting.
1877const _: &[&[&str]] = &[ALIGNMENTS, FITS, ORIENTATIONS, PRESENCES, RADII];