Skip to main content

eave_api/
ui.rs

1//! The closed UI vocabulary widgets build their views from.
2//!
3//! Every node is semantic: it says *what* something is (a label, a heading, a button), never how
4//! it looks. The host maps nodes to pixels using its theme, so every plugin gets the same style,
5//! and the same animations, without being able to break them.
6
7use std::ops::RangeInclusive;
8
9/// The role of a piece of text. The theme decides the color.
10#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
11pub enum Tone {
12    #[default]
13    Normal,
14    /// Secondary information.
15    Dim,
16    /// The thing to notice: today in a calendar, the active workspace.
17    Accent,
18    Warning,
19    Critical,
20    /// Healthy, or gaining: a charging battery.
21    Good,
22}
23
24/// A status symbol. It names what to show and how full it is; the host draws it in the text's
25/// tone, at the text's size. Fractions are 0.0 to 1.0.
26#[derive(Debug, Clone, Copy, PartialEq)]
27pub enum Icon {
28    /// A speaker with sound waves for the level. Above 1.0 is amplified.
29    Volume { level: f32, muted: bool },
30    /// A Wi-Fi fan with as many bars as the signal. `None` when there's no link.
31    Wifi { strength: Option<f32> },
32    /// A wired connection.
33    Wired,
34    /// A battery filled to its charge.
35    Battery { charge: f32, charging: bool },
36    /// A padlock: a secured network.
37    Lock,
38    /// An `i` in a circle: more about this.
39    Info,
40    /// A chevron pointing left: back, or earlier.
41    Back,
42    /// A chevron pointing right: on, or later.
43    Forward,
44    /// A check mark: the one in use.
45    Check,
46    /// A circling arrow: look again.
47    Refresh,
48    /// A cross: close this.
49    Close,
50    /// An eye: show what's hidden.
51    Reveal,
52    /// A lightning bolt: charging.
53    Bolt,
54    /// A power plug: on mains power.
55    Plug,
56    /// A microphone, struck through when muted.
57    Mic { muted: bool },
58    /// A loudspeaker box: built-in speakers.
59    Speaker,
60    /// Headphones, or a headset.
61    Headphones,
62    /// A screen: a monitor or TV that plays sound.
63    Display,
64    /// A bell: notifications, struck through when they're silenced.
65    Bell { muted: bool },
66    /// Three level bars, like an equalizer: media. Uneven while `playing`, low and level when
67    /// not.
68    Media { playing: bool },
69    /// A triangle pointing right: play.
70    Play,
71    /// Two upright bars: pause.
72    Pause,
73    /// A triangle pointing left against a bar: back to the previous track.
74    Previous,
75    /// A triangle pointing right against a bar: on to the next track.
76    Next,
77}
78
79/// A piece of UI. Build it with the functions in this module.
80pub struct Node<M>(Kind<M>);
81
82#[doc(hidden)]
83pub enum Kind<M> {
84    Label {
85        text: String,
86        tone: Option<Tone>,
87    },
88    Heading {
89        text: String,
90        tone: Option<Tone>,
91    },
92    Icon {
93        icon: Icon,
94        tone: Option<Tone>,
95    },
96    AppIcon {
97        app_id: String,
98        tone: Option<Tone>,
99    },
100    Image {
101        file: &'static [u8],
102        tint: bool,
103        tone: Option<Tone>,
104    },
105    Sprite {
106        frames: &'static [&'static [u8]],
107        /// Indices into `frames`, in the order they play.
108        order: &'static [u8],
109        /// Plays for as long as it shows, not only when it shows up or changes.
110        looping: bool,
111        tone: Option<Tone>,
112    },
113    Cover {
114        url: String,
115        /// Shown until the picture is loaded, and instead of it if it can't be.
116        fallback: Box<Node<M>>,
117        /// The main picture of a popover, not an icon.
118        large: bool,
119        tone: Option<Tone>,
120    },
121    Counter {
122        value: i64,
123        range: RangeInclusive<i64>,
124        unit: String,
125        tone: Option<Tone>,
126    },
127    Alternate {
128        options: Vec<Node<M>>,
129        current: usize,
130        /// Shown instead of `current` while the pointer is over the item.
131        hovered: Option<usize>,
132        /// As wide as the option on screen, hovered by its own width, after a rest ([`expand`]).
133        grows: bool,
134    },
135    Row(Vec<Node<M>>),
136    Column(Vec<Node<M>>),
137    Grid {
138        columns: usize,
139        cells: Vec<Node<M>>,
140    },
141    Button {
142        content: Box<Node<M>>,
143        on_press: M,
144        on_secondary: Option<M>,
145        /// The main action of a popover page: filled, so it stands out.
146        prominent: bool,
147    },
148    Switcher {
149        options: Vec<(Node<M>, M)>,
150        current: Option<usize>,
151    },
152    Gulp(Box<Node<M>>),
153    /// Centered across the panel's width.
154    Center(Box<Node<M>>),
155    Separator,
156    Empty,
157    ListItem {
158        title: String,
159        subtitle: Option<String>,
160        leading: Option<Box<Node<M>>>,
161        trailing: Option<Box<Node<M>>>,
162        selected: bool,
163        on_press: Option<M>,
164        on_secondary: Option<M>,
165    },
166    List(Vec<Node<M>>),
167    Section {
168        title: String,
169        trailing: Option<Box<Node<M>>>,
170    },
171    Header {
172        title: String,
173        on_back: Option<M>,
174        trailing: Option<Box<Node<M>>>,
175    },
176    Detail {
177        name: String,
178        value: Box<Node<M>>,
179    },
180    Fill,
181    Slider {
182        value: f32,
183        range: RangeInclusive<f32>,
184        step: f32,
185        on_change: Box<dyn Fn(f32) -> M>,
186        on_release: Option<M>,
187    },
188    Toggle {
189        on: bool,
190        on_toggle: M,
191    },
192    Meter {
193        fraction: f32,
194        /// Where a limit sits along it, like a battery's charge limit.
195        limit: Option<f32>,
196        tone: Option<Tone>,
197    },
198    TextInput {
199        placeholder: String,
200        value: String,
201        on_input: Box<dyn Fn(String) -> M>,
202        on_submit: Option<M>,
203        secret: bool,
204        trailing: Option<Box<Node<M>>>,
205        tone: Option<Tone>,
206    },
207    Display(Box<Node<M>>),
208    Mark(Box<Node<M>>),
209    Badge {
210        text: String,
211        tone: Option<Tone>,
212    },
213    /// Pages and their names. The host shows one at a time and owns which.
214    Tabs(Vec<(String, Node<M>)>),
215    Markdown {
216        text: String,
217        tone: Option<Tone>,
218    },
219    TextArea {
220        placeholder: String,
221        value: String,
222        on_input: Box<dyn Fn(String) -> M>,
223        on_submit: Option<M>,
224        trailing: Option<Box<Node<M>>>,
225    },
226    Scroll {
227        content: Box<Node<M>>,
228        /// Keeps the end in view as content arrives there, like a chat.
229        bottom: bool,
230    },
231}
232
233pub fn label<M>(text: impl Into<String>) -> Node<M> {
234    Node(Kind::Label {
235        text: text.into(),
236        tone: None,
237    })
238}
239
240/// A title, larger than a label. Meant for popovers.
241pub fn heading<M>(text: impl Into<String>) -> Node<M> {
242    Node(Kind::Heading {
243        text: text.into(),
244        tone: None,
245    })
246}
247
248/// A symbol, sized like a label.
249pub fn icon<M>(icon: Icon) -> Node<M> {
250    Node(Kind::Icon { icon, tone: None })
251}
252
253/// An application's icon, found by its app id or window class (`firefox`, `org.gnome.Nautilus`)
254/// in the user's icon theme. Sized like an [`icon`], in the app's own colors: a tone only dims it.
255/// Until the host has found it, or if there is none, it shows the id's first letter.
256pub fn app_icon<M>(app_id: impl Into<String>) -> Node<M> {
257    Node(Kind::AppIcon {
258        app_id: app_id.into(),
259        tone: None,
260    })
261}
262
263/// An icon the plugin ships: an SVG or PNG file compiled into it with `include_bytes!`, told
264/// apart by its first bytes. Sized like an [`icon`], in the file's own colors: a tone only dims
265/// it, as with [`app_icon`]. The host draws it once, off the UI thread, and shows nothing in its
266/// place until then.
267pub fn image<M>(file: &'static [u8]) -> Node<M> {
268    Node(Kind::Image {
269        file,
270        tint: false,
271        tone: None,
272    })
273}
274
275/// Like [`image`], but only the file's shape counts: it's drawn in the text's tone, like an
276/// [`icon`]. For a monochrome symbol the vocabulary doesn't have.
277pub fn symbol<M>(file: &'static [u8]) -> Node<M> {
278    Node(Kind::Image {
279        file,
280        tint: true,
281        tone: None,
282    })
283}
284
285/// A little animation the plugin ships, like a pixel-art mascot: `frames` are PNG or SVG files
286/// compiled in with `include_bytes!`, all the same size, and `order` lists which frame shows when,
287/// by index into `frames`. The host keeps the frames' shape and picks the height (a little under
288/// a label's in the bar, taller in a popover), scaling pixels up whole, never blurred: ship pixel
289/// art at its own size. The host shows a frame for a quarter of a second. It plays the loop a
290/// couple of times when it shows up or changes, then rests on the first frame of `order`;
291/// [`Node::looping`] keeps it going. A tone only dims it.
292pub fn sprite<M>(frames: &'static [&'static [u8]], order: &'static [u8]) -> Node<M> {
293    Node(Kind::Sprite {
294        frames,
295        order,
296        looping: false,
297        tone: None,
298    })
299}
300
301/// A picture a program hands over at runtime, by URL: a track's cover art. `file://` and
302/// `https://` URLs load; the host fetches, crops it square and rounds it, off the UI thread, and
303/// keeps the few latest. Drawn at icon size, or big with [`Node::large`]. Until it's there, or if
304/// it can't be loaded, `fallback` shows in its place. A tone only dims it.
305pub fn cover<M>(url: impl Into<String>, fallback: Node<M>) -> Node<M> {
306    Node(Kind::Cover {
307        url: url.into(),
308        fallback: Box::new(fallback),
309        large: false,
310        tone: None,
311    })
312}
313
314/// A number from `range`, with a unit after it (`"%"`, `"°"`, or `""`), shown like a label. It
315/// keeps room for the widest number in `range`, so it doesn't change width as it changes. When
316/// `value` changes, the host counts to it through every number in between.
317pub fn counter<M>(value: i64, range: RangeInclusive<i64>, unit: impl Into<String>) -> Node<M> {
318    Node(Kind::Counter {
319        value,
320        range,
321        unit: unit.into(),
322        tone: None,
323    })
324}
325
326/// One of `options` at a time: the one at `current`, or the last one if it's past the end. It's as
327/// big as the biggest option, so it doesn't change size when `current` does. The host fades from
328/// one to the other. Keep the options the same from one
329/// view to the next, so the host can tell which one is coming in.
330pub fn alternate<M>(options: impl IntoIterator<Item = Node<M>>, current: usize) -> Node<M> {
331    Node(Kind::Alternate {
332        options: options.into_iter().collect(),
333        current,
334        hovered: None,
335        grows: false,
336    })
337}
338
339/// `normal`, or `hovered` while the pointer is over the item, fading between them like an
340/// [`alternate`]: an icon that shows its percent, a time that shows the date.
341pub fn hover<M>(normal: Node<M>, hovered: Node<M>) -> Node<M> {
342    alternate([normal, hovered], 0).on_hover(1)
343}
344
345/// `collapsed`, or `expanded` once the pointer rests on this node, fading between them like a
346/// [`hover`]. Unlike `hover`, it's only as big as what it shows, so the item grows to make room
347/// and the island springs along, and it reacts to the pointer over it, not the whole item: a
348/// workspace that shows every window when you point at it.
349pub fn expand<M>(collapsed: Node<M>, expanded: Node<M>) -> Node<M> {
350    Node(Kind::Alternate {
351        options: vec![collapsed, expanded],
352        current: 0,
353        hovered: Some(1),
354        grows: true,
355    })
356}
357
358/// Children side by side, vertically centered.
359pub fn row<M>(children: impl IntoIterator<Item = Node<M>>) -> Node<M> {
360    Node(Kind::Row(children.into_iter().collect()))
361}
362
363/// Children stacked top to bottom.
364pub fn column<M>(children: impl IntoIterator<Item = Node<M>>) -> Node<M> {
365    Node(Kind::Column(children.into_iter().collect()))
366}
367
368/// `content` centered across the panel's width: a cover, a title, a row of controls. Text in it
369/// stays on one line, cut and faded at the edges of its room if it's too long. In the bar, it's
370/// just `content`.
371pub fn center<M>(content: Node<M>) -> Node<M> {
372    Node(Kind::Center(Box::new(content)))
373}
374
375/// Cells laid out left to right in rows of `columns`, all the same width. A calendar, a keypad.
376pub fn grid<M>(columns: usize, cells: impl IntoIterator<Item = Node<M>>) -> Node<M> {
377    Node(Kind::Grid {
378        columns: columns.max(1),
379        cells: cells.into_iter().collect(),
380    })
381}
382
383pub fn button<M>(content: Node<M>, on_press: M) -> Node<M> {
384    Node(Kind::Button {
385        content: Box::new(content),
386        on_press,
387        on_secondary: None,
388        prominent: false,
389    })
390}
391
392/// A row of choices, one of them current: workspaces, output devices. Each option sends its
393/// message when clicked. The host marks the current option and moves the mark smoothly when
394/// `current` changes.
395pub fn switcher<M>(
396    options: impl IntoIterator<Item = (Node<M>, M)>,
397    current: Option<usize>,
398) -> Node<M> {
399    Node(Kind::Switcher {
400        options: options.into_iter().collect(),
401        current,
402    })
403}
404
405/// Keeps gulping, like a bottle being poured: the host swells `content` gently and steadily for as
406/// long as it's on screen. For something filling up, like a charging battery's percent.
407pub fn gulp<M>(content: Node<M>) -> Node<M> {
408    Node(Kind::Gulp(Box::new(content)))
409}
410
411/// A thin horizontal line between groups.
412pub fn separator<M>() -> Node<M> {
413    Node(Kind::Separator)
414}
415
416/// Takes up a grid cell, or nothing elsewhere.
417pub fn empty<M>() -> Node<M> {
418    Node(Kind::Empty)
419}
420
421/// A row in a popover list: a network, a device, a window. Give it a [`subtitle`](Node::subtitle),
422/// a [`leading`](Node::leading) icon, a [`trailing`](Node::trailing) node at the far end (a lock,
423/// an info button), mark the one in use with [`selected`](Node::selected), and make it clickable
424/// with [`on_press`](Node::on_press). A long title fades out at the end rather than wrapping.
425pub fn list_item<M>(title: impl Into<String>) -> Node<M> {
426    Node(Kind::ListItem {
427        title: title.into(),
428        subtitle: None,
429        leading: None,
430        trailing: None,
431        selected: false,
432        on_press: None,
433        on_secondary: None,
434    })
435}
436
437/// Rows stacked closely, usually [`list_item`]s. In a popover, a long list scrolls instead of
438/// growing past a few rows.
439pub fn list<M>(rows: impl IntoIterator<Item = Node<M>>) -> Node<M> {
440    Node(Kind::List(rows.into_iter().collect()))
441}
442
443/// A small caption over a group in a popover ("Output", "Health"), with an optional
444/// [`trailing`](Node::trailing) node at the far end.
445pub fn section<M>(title: impl Into<String>) -> Node<M> {
446    Node(Kind::Section {
447        title: title.into(),
448        trailing: None,
449    })
450}
451
452/// The title of a popover page. Sub-pages get a back button with [`on_back`](Node::on_back); a
453/// [`trailing`](Node::trailing) node sits at the far end (a switch, a count).
454pub fn header<M>(title: impl Into<String>) -> Node<M> {
455    Node(Kind::Header {
456        title: title.into(),
457        on_back: None,
458        trailing: None,
459    })
460}
461
462/// A named value on one line: the name dimmed at the start, the value at the end.
463pub fn detail<M>(name: impl Into<String>, value: Node<M>) -> Node<M> {
464    Node(Kind::Detail {
465        name: name.into(),
466        value: Box::new(value),
467    })
468}
469
470/// Takes the free space in a popover row, pushing what follows to the end. Nothing in the bar.
471pub fn fill<M>() -> Node<M> {
472    Node(Kind::Fill)
473}
474
475/// A value dragged along `range`: a volume. Sends `on_change` while dragging; add
476/// [`on_release`](Node::on_release) to act once it's let go, and [`step`](Node::step) to snap.
477pub fn slider<M: 'static>(
478    value: f32,
479    range: RangeInclusive<f32>,
480    on_change: impl Fn(f32) -> M + 'static,
481) -> Node<M> {
482    Node(Kind::Slider {
483        value,
484        range,
485        step: 1.0,
486        on_change: Box::new(on_change),
487        on_release: None,
488    })
489}
490
491/// An on/off switch. Sends `on_toggle` when clicked; the host slides it over.
492pub fn toggle<M>(on: bool, on_toggle: M) -> Node<M> {
493    Node(Kind::Toggle { on, on_toggle })
494}
495
496/// A bar filled to `fraction` (0.0 to 1.0), in its tone: a charge, a signal. The host eases it to
497/// a new value.
498pub fn meter<M>(fraction: f32) -> Node<M> {
499    Node(Kind::Meter {
500        fraction,
501        limit: None,
502        tone: None,
503    })
504}
505
506/// A field to type into, showing `value`. Sends `on_input` with the new text on every change.
507/// It takes the focus when it appears, and while it has it, keys stay in the popover; Esc still
508/// closes it. [`on_submit`](Node::on_submit) is sent on Enter. One per popover page. A
509/// [`Tone::Critical`] tone marks it as wrong.
510pub fn text_input<M: 'static>(
511    placeholder: impl Into<String>,
512    value: impl Into<String>,
513    on_input: impl Fn(String) -> M + 'static,
514) -> Node<M> {
515    Node(Kind::TextInput {
516        placeholder: placeholder.into(),
517        value: value.into(),
518        on_input: Box::new(on_input),
519        on_submit: None,
520        secret: false,
521        trailing: None,
522        tone: None,
523    })
524}
525
526/// A field to type several lines into, showing `value`: a message. It grows with its text up
527/// to a few lines, then scrolls. Sends `on_input` with the new text on every change, and
528/// [`on_submit`](Node::on_submit) on Enter; Shift+Enter starts a new line. In a popover it takes
529/// the focus when it appears, like a [`text_input`]. One per popover page.
530pub fn text_area<M: 'static>(
531    placeholder: impl Into<String>,
532    value: impl Into<String>,
533    on_input: impl Fn(String) -> M + 'static,
534) -> Node<M> {
535    Node(Kind::TextArea {
536        placeholder: placeholder.into(),
537        value: value.into(),
538        on_input: Box::new(on_input),
539        on_submit: None,
540        trailing: None,
541    })
542}
543
544/// Text written in Markdown, as people and programs write messages: paragraphs, emphasis,
545/// `code`, lists, quotes, headings, code blocks and tables. The host sets it in its own type and
546/// colors, wrapped to the width it has. Links show as links but don't open: the bar starts no
547/// programs.
548pub fn markdown<M>(text: impl Into<String>) -> Node<M> {
549    Node(Kind::Markdown {
550        text: text.into(),
551        tone: None,
552    })
553}
554
555/// Its content, scrolling in a popover once it's taller than the host allows a page to grow: a
556/// conversation, a long document. With [`Node::bottom`] it keeps its end in view. In the bar it's
557/// just its content.
558pub fn scroll<M>(content: Node<M>) -> Node<M> {
559    Node(Kind::Scroll {
560        content: Box::new(content),
561        bottom: false,
562    })
563}
564
565/// Its text, large: the one number a popover is about, like a battery's charge.
566pub fn display<M>(content: Node<M>) -> Node<M> {
567    Node(Kind::Display(Box::new(content)))
568}
569
570/// Its content marked with a ring: today in a calendar.
571pub fn mark<M>(content: Node<M>) -> Node<M> {
572    Node(Kind::Mark(Box::new(content)))
573}
574
575/// A short code in a small outlined box: a keyboard layout's `US`.
576pub fn badge<M>(text: impl Into<String>) -> Node<M> {
577    Node(Kind::Badge {
578        text: text.into(),
579        tone: None,
580    })
581}
582
583/// Pages behind a row of tabs, one page showing at a time: a popover's Output, Input and Apps.
584/// The host draws the tabs, switches pages when one is clicked and starts on the first each time
585/// the popover opens, so the plugin keeps no state for it. In the bar, only the first page shows.
586pub fn tabs<M>(pages: impl IntoIterator<Item = (impl Into<String>, Node<M>)>) -> Node<M> {
587    Node(Kind::Tabs(
588        pages
589            .into_iter()
590            .map(|(name, page)| (name.into(), page))
591            .collect(),
592    ))
593}
594
595impl<M> Node<M> {
596    /// Sets the tone of this text, or of every text inside this node that has none yet.
597    pub fn tone(mut self, tone: Tone) -> Self {
598        self.set_tone(tone);
599        self
600    }
601
602    /// For an [`alternate`]: shows the option at `index` while the pointer is over the item,
603    /// whatever `current` is. Other nodes have nothing to show on hover and ignore it.
604    pub fn on_hover(mut self, index: usize) -> Self {
605        if let Kind::Alternate { hovered, .. } = &mut self.0 {
606            *hovered = Some(index);
607        }
608        self
609    }
610
611    /// For a [`button`] or [`list_item`]: sent on a secondary (right) click on it. Other nodes
612    /// ignore it.
613    pub fn on_secondary(mut self, message: M) -> Self {
614        if let Kind::Button { on_secondary, .. } | Kind::ListItem { on_secondary, .. } = &mut self.0
615        {
616            *on_secondary = Some(message);
617        }
618        self
619    }
620
621    /// For a [`button`]: the main action of its page, filled so it stands out. Other nodes ignore
622    /// it.
623    pub fn prominent(mut self) -> Self {
624        if let Kind::Button { prominent, .. } = &mut self.0 {
625            *prominent = true;
626        }
627        self
628    }
629
630    /// For a [`list_item`]: sent on a click on it. Other nodes ignore it.
631    pub fn on_press(mut self, message: M) -> Self {
632        if let Kind::ListItem { on_press, .. } = &mut self.0 {
633            *on_press = Some(message);
634        }
635        self
636    }
637
638    /// For a [`list_item`]: a second, dimmer line under the title. Other nodes ignore it.
639    pub fn subtitle(mut self, text: impl Into<String>) -> Self {
640        if let Kind::ListItem { subtitle, .. } = &mut self.0 {
641            *subtitle = Some(text.into());
642        }
643        self
644    }
645
646    /// For a [`list_item`]: an icon before the title. Other nodes ignore it.
647    pub fn leading(mut self, node: Node<M>) -> Self {
648        if let Kind::ListItem { leading, .. } = &mut self.0 {
649            *leading = Some(Box::new(node));
650        }
651        self
652    }
653
654    /// For a [`list_item`], [`section`], [`header`], [`text_input`] or [`text_area`]: a node at the
655    /// far end, like a lock or an info button. Other nodes ignore it.
656    pub fn trailing(mut self, node: Node<M>) -> Self {
657        if let Kind::ListItem { trailing, .. }
658        | Kind::Section { trailing, .. }
659        | Kind::Header { trailing, .. }
660        | Kind::TextInput { trailing, .. }
661        | Kind::TextArea { trailing, .. } = &mut self.0
662        {
663            *trailing = Some(Box::new(node));
664        }
665        self
666    }
667
668    /// For a [`list_item`]: marks it as the one in use. Other nodes ignore it.
669    pub fn selected(mut self, yes: bool) -> Self {
670        if let Kind::ListItem { selected, .. } = &mut self.0 {
671            *selected = yes;
672        }
673        self
674    }
675
676    /// For a [`header`]: shows a back button that sends `message`. Other nodes ignore it.
677    pub fn on_back(mut self, message: M) -> Self {
678        if let Kind::Header { on_back, .. } = &mut self.0 {
679            *on_back = Some(message);
680        }
681        self
682    }
683
684    /// For a [`slider`]: the step its value snaps to (1 by default). Other nodes ignore it.
685    pub fn step(mut self, by: f32) -> Self {
686        if let Kind::Slider { step, .. } = &mut self.0 {
687            *step = by;
688        }
689        self
690    }
691
692    /// For a [`slider`]: sent when it's let go. Other nodes ignore it.
693    pub fn on_release(mut self, message: M) -> Self {
694        if let Kind::Slider { on_release, .. } = &mut self.0 {
695            *on_release = Some(message);
696        }
697        self
698    }
699
700    /// For a [`text_input`] or [`text_area`]: sent on Enter. Other nodes ignore it.
701    pub fn on_submit(mut self, message: M) -> Self {
702        if let Kind::TextInput { on_submit, .. } | Kind::TextArea { on_submit, .. } = &mut self.0 {
703            *on_submit = Some(message);
704        }
705        self
706    }
707
708    /// For a [`text_input`]: hides what's typed, like a password. Other nodes ignore it.
709    pub fn secret(mut self, yes: bool) -> Self {
710        if let Kind::TextInput { secret, .. } = &mut self.0 {
711            *secret = yes;
712        }
713        self
714    }
715
716    /// For a [`sprite`]: plays for as long as it shows, not only when it shows up or changes.
717    /// Every frame redraws the bar, so keep it for what wants your eye, or for popovers. Other
718    /// nodes ignore it.
719    pub fn looping(mut self) -> Self {
720        if let Kind::Sprite { looping, .. } = &mut self.0 {
721            *looping = true;
722        }
723        self
724    }
725
726    /// For a [`cover`]: drawn big, as the main picture of a popover, not as an icon. Other nodes
727    /// ignore it.
728    pub fn large(mut self) -> Self {
729        if let Kind::Cover { large, .. } = &mut self.0 {
730            *large = true;
731        }
732        self
733    }
734
735    /// For a [`scroll`]: keeps its end in view as content arrives there, unless you've scrolled
736    /// up to read. Other nodes ignore it.
737    pub fn bottom(mut self) -> Self {
738        if let Kind::Scroll { bottom, .. } = &mut self.0 {
739            *bottom = true;
740        }
741        self
742    }
743
744    /// For a [`meter`]: marks a limit along it (0.0 to 1.0). Other nodes ignore it.
745    pub fn limit(mut self, at: f32) -> Self {
746        if let Kind::Meter { limit, .. } = &mut self.0 {
747            *limit = Some(at);
748        }
749        self
750    }
751
752    fn set_tone(&mut self, new: Tone) {
753        match &mut self.0 {
754            Kind::Label { tone, .. }
755            | Kind::Heading { tone, .. }
756            | Kind::Icon { tone, .. }
757            | Kind::AppIcon { tone, .. }
758            | Kind::Image { tone, .. }
759            | Kind::Sprite { tone, .. }
760            | Kind::Counter { tone, .. }
761            | Kind::Meter { tone, .. }
762            | Kind::TextInput { tone, .. }
763            | Kind::Markdown { tone, .. }
764            | Kind::Badge { tone, .. } => {
765                tone.get_or_insert(new);
766            }
767            Kind::Cover { tone, fallback, .. } => {
768                tone.get_or_insert(new);
769                fallback.set_tone(new);
770            }
771            Kind::Alternate {
772                options: children, ..
773            }
774            | Kind::Row(children)
775            | Kind::Column(children)
776            | Kind::Grid {
777                cells: children, ..
778            }
779            | Kind::List(children) => {
780                children.iter_mut().for_each(|child| child.set_tone(new));
781            }
782            Kind::Button { content, .. }
783            | Kind::Gulp(content)
784            | Kind::Center(content)
785            | Kind::Display(content)
786            | Kind::Mark(content)
787            | Kind::Scroll { content, .. }
788            | Kind::Detail { value: content, .. } => content.set_tone(new),
789            Kind::ListItem {
790                leading, trailing, ..
791            } => {
792                leading
793                    .iter_mut()
794                    .chain(trailing.iter_mut())
795                    .for_each(|child| child.set_tone(new));
796            }
797            Kind::Tabs(pages) => {
798                for (_, page) in pages {
799                    page.set_tone(new);
800                }
801            }
802            Kind::Switcher { options, .. } => {
803                options
804                    .iter_mut()
805                    .for_each(|(child, _)| child.set_tone(new));
806            }
807            Kind::Separator
808            | Kind::Empty
809            | Kind::Section { .. }
810            | Kind::Header { .. }
811            | Kind::Fill
812            | Kind::Slider { .. }
813            | Kind::TextArea { .. }
814            | Kind::Toggle { .. } => {}
815        }
816    }
817
818    /// Converts every message in this node. Sliders and text fields keep `f` to convert the
819    /// messages they make later, so it must be `Clone + 'static`, like a function or a closure
820    /// that captures nothing.
821    pub fn map<N: 'static>(self, f: &(impl Fn(M) -> N + Clone + 'static)) -> Node<N>
822    where
823        M: 'static,
824    {
825        let map_all = |nodes: Vec<Node<M>>| nodes.into_iter().map(|node| node.map(f)).collect();
826        let map_box = |node: Option<Box<Node<M>>>| node.map(|node| Box::new(node.map(f)));
827
828        Node(match self.0 {
829            Kind::Label { text, tone } => Kind::Label { text, tone },
830            Kind::Heading { text, tone } => Kind::Heading { text, tone },
831            Kind::Icon { icon, tone } => Kind::Icon { icon, tone },
832            Kind::AppIcon { app_id, tone } => Kind::AppIcon { app_id, tone },
833            Kind::Image { file, tint, tone } => Kind::Image { file, tint, tone },
834            Kind::Sprite {
835                frames,
836                order,
837                looping,
838                tone,
839            } => Kind::Sprite {
840                frames,
841                order,
842                looping,
843                tone,
844            },
845            Kind::Cover {
846                url,
847                fallback,
848                large,
849                tone,
850            } => Kind::Cover {
851                url,
852                fallback: Box::new(fallback.map(f)),
853                large,
854                tone,
855            },
856            Kind::Counter {
857                value,
858                range,
859                unit,
860                tone,
861            } => Kind::Counter {
862                value,
863                range,
864                unit,
865                tone,
866            },
867            Kind::Alternate {
868                options,
869                current,
870                hovered,
871                grows,
872            } => Kind::Alternate {
873                options: map_all(options),
874                current,
875                hovered,
876                grows,
877            },
878            Kind::Row(children) => Kind::Row(map_all(children)),
879            Kind::Column(children) => Kind::Column(map_all(children)),
880            Kind::Grid { columns, cells } => Kind::Grid {
881                columns,
882                cells: map_all(cells),
883            },
884            Kind::Button {
885                content,
886                on_press,
887                on_secondary,
888                prominent,
889            } => Kind::Button {
890                content: Box::new(content.map(f)),
891                on_press: f(on_press),
892                on_secondary: on_secondary.map(f),
893                prominent,
894            },
895            Kind::Switcher { options, current } => Kind::Switcher {
896                options: options
897                    .into_iter()
898                    .map(|(node, message)| (node.map(f), f(message)))
899                    .collect(),
900                current,
901            },
902            Kind::Gulp(content) => Kind::Gulp(Box::new(content.map(f))),
903            Kind::Center(content) => Kind::Center(Box::new(content.map(f))),
904            Kind::Separator => Kind::Separator,
905            Kind::Empty => Kind::Empty,
906            Kind::ListItem {
907                title,
908                subtitle,
909                leading,
910                trailing,
911                selected,
912                on_press,
913                on_secondary,
914            } => Kind::ListItem {
915                title,
916                subtitle,
917                leading: map_box(leading),
918                trailing: map_box(trailing),
919                selected,
920                on_press: on_press.map(f),
921                on_secondary: on_secondary.map(f),
922            },
923            Kind::List(rows) => Kind::List(map_all(rows)),
924            Kind::Section { title, trailing } => Kind::Section {
925                title,
926                trailing: map_box(trailing),
927            },
928            Kind::Header {
929                title,
930                on_back,
931                trailing,
932            } => Kind::Header {
933                title,
934                on_back: on_back.map(f),
935                trailing: map_box(trailing),
936            },
937            Kind::Detail { name, value } => Kind::Detail {
938                name,
939                value: Box::new(value.map(f)),
940            },
941            Kind::Fill => Kind::Fill,
942            Kind::Slider {
943                value,
944                range,
945                step,
946                on_change,
947                on_release,
948            } => {
949                let g = f.clone();
950                Kind::Slider {
951                    value,
952                    range,
953                    step,
954                    on_change: Box::new(move |value| g(on_change(value))),
955                    on_release: on_release.map(f),
956                }
957            }
958            Kind::Toggle { on, on_toggle } => Kind::Toggle {
959                on,
960                on_toggle: f(on_toggle),
961            },
962            Kind::Meter {
963                fraction,
964                limit,
965                tone,
966            } => Kind::Meter {
967                fraction,
968                limit,
969                tone,
970            },
971            Kind::TextInput {
972                placeholder,
973                value,
974                on_input,
975                on_submit,
976                secret,
977                trailing,
978                tone,
979            } => {
980                let g = f.clone();
981                Kind::TextInput {
982                    placeholder,
983                    value,
984                    on_input: Box::new(move |text| g(on_input(text))),
985                    on_submit: on_submit.map(f),
986                    secret,
987                    trailing: map_box(trailing),
988                    tone,
989                }
990            }
991            Kind::Display(content) => Kind::Display(Box::new(content.map(f))),
992            Kind::Mark(content) => Kind::Mark(Box::new(content.map(f))),
993            Kind::Badge { text, tone } => Kind::Badge { text, tone },
994            Kind::Tabs(pages) => Kind::Tabs(
995                pages
996                    .into_iter()
997                    .map(|(name, page)| (name, page.map(f)))
998                    .collect(),
999            ),
1000            Kind::Markdown { text, tone } => Kind::Markdown { text, tone },
1001            Kind::TextArea {
1002                placeholder,
1003                value,
1004                on_input,
1005                on_submit,
1006                trailing,
1007            } => {
1008                let g = f.clone();
1009                Kind::TextArea {
1010                    placeholder,
1011                    value,
1012                    on_input: Box::new(move |text| g(on_input(text))),
1013                    on_submit: on_submit.map(f),
1014                    trailing: map_box(trailing),
1015                }
1016            }
1017            Kind::Scroll { content, bottom } => Kind::Scroll {
1018                content: Box::new(content.map(f)),
1019                bottom,
1020            },
1021        })
1022    }
1023
1024    #[doc(hidden)]
1025    pub fn into_kind(self) -> Kind<M> {
1026        self.0
1027    }
1028
1029    #[doc(hidden)]
1030    pub fn kind(&self) -> &Kind<M> {
1031        &self.0
1032    }
1033}
1034
1035/// What a widget shows in the bar.
1036pub struct Item<M> {
1037    content: Option<Node<M>>,
1038    on_press: Option<M>,
1039    on_secondary: Option<M>,
1040    on_scroll: Option<(M, M)>,
1041}
1042
1043impl<M> Item<M> {
1044    pub fn new(content: Node<M>) -> Self {
1045        Self {
1046            content: Some(content),
1047            on_press: None,
1048            on_secondary: None,
1049            on_scroll: None,
1050        }
1051    }
1052
1053    /// Takes no space in the bar. For widgets with nothing to show, like a battery on a desktop.
1054    pub fn hidden() -> Self {
1055        Self {
1056            content: None,
1057            on_press: None,
1058            on_secondary: None,
1059            on_scroll: None,
1060        }
1061    }
1062
1063    /// Sent on a primary click. Replaces the default of toggling the popover.
1064    pub fn on_press(mut self, message: M) -> Self {
1065        self.on_press = Some(message);
1066        self
1067    }
1068
1069    /// Sent on a secondary (right) click.
1070    pub fn on_secondary(mut self, message: M) -> Self {
1071        self.on_secondary = Some(message);
1072        self
1073    }
1074
1075    /// Sent once per scroll step over the item, `up` or `down`. The host turns smooth touchpad
1076    /// scrolling into steps.
1077    pub fn on_scroll(mut self, up: M, down: M) -> Self {
1078        self.on_scroll = Some((up, down));
1079        self
1080    }
1081
1082    pub fn map<N: 'static>(self, f: &(impl Fn(M) -> N + Clone + 'static)) -> Item<N>
1083    where
1084        M: 'static,
1085    {
1086        Item {
1087            content: self.content.map(|content| content.map(f)),
1088            on_press: self.on_press.map(f),
1089            on_secondary: self.on_secondary.map(f),
1090            on_scroll: self.on_scroll.map(|(up, down)| (f(up), f(down))),
1091        }
1092    }
1093
1094    #[doc(hidden)]
1095    pub fn into_parts(self) -> ItemParts<M> {
1096        ItemParts {
1097            content: self.content,
1098            on_press: self.on_press,
1099            on_secondary: self.on_secondary,
1100            on_scroll: self.on_scroll,
1101        }
1102    }
1103}
1104
1105#[doc(hidden)]
1106pub struct ItemParts<M> {
1107    pub content: Option<Node<M>>,
1108    pub on_press: Option<M>,
1109    pub on_secondary: Option<M>,
1110    /// Up, then down.
1111    pub on_scroll: Option<(M, M)>,
1112}
1113
1114/// The contents of a popover. The host draws the surface, its background and its edges, and lays
1115/// every popover out at the same width, or at a wider one if it asks ([`Panel::wide`]).
1116pub struct Panel<M> {
1117    content: Node<M>,
1118    on_open: Option<M>,
1119    on_close: Option<M>,
1120    wide: bool,
1121}
1122
1123impl<M> Panel<M> {
1124    pub fn new(content: Node<M>) -> Self {
1125        Self {
1126            content,
1127            on_open: None,
1128            on_close: None,
1129            wide: false,
1130        }
1131    }
1132
1133    /// Laid out wider than other popovers, for a page that's mostly reading: a conversation, a
1134    /// document. The host decides how wide.
1135    pub fn wide(mut self) -> Self {
1136        self.wide = true;
1137        self
1138    }
1139
1140    #[doc(hidden)]
1141    pub fn is_wide(&self) -> bool {
1142        self.wide
1143    }
1144
1145    /// Sent each time the popover opens: start a scan, show the first page.
1146    pub fn on_open(mut self, message: M) -> Self {
1147        self.on_open = Some(message);
1148        self
1149    }
1150
1151    /// Sent each time the popover starts closing: stop listening, forget what was typed.
1152    pub fn on_close(mut self, message: M) -> Self {
1153        self.on_close = Some(message);
1154        self
1155    }
1156
1157    pub fn map<N: 'static>(self, f: &(impl Fn(M) -> N + Clone + 'static)) -> Panel<N>
1158    where
1159        M: 'static,
1160    {
1161        Panel {
1162            content: self.content.map(f),
1163            on_open: self.on_open.map(f),
1164            on_close: self.on_close.map(f),
1165            wide: self.wide,
1166        }
1167    }
1168
1169    #[doc(hidden)]
1170    pub fn into_node(self) -> Node<M> {
1171        self.content
1172    }
1173
1174    #[doc(hidden)]
1175    pub fn on_open_message(self) -> Option<M> {
1176        self.on_open
1177    }
1178
1179    #[doc(hidden)]
1180    pub fn on_close_message(self) -> Option<M> {
1181        self.on_close
1182    }
1183}
1184
1185/// How much a [`Notice`] matters. The host decides what that looks like.
1186#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1187pub enum Urgency {
1188    /// Background news: a download finished.
1189    Low,
1190    #[default]
1191    Normal,
1192    /// Needs attention now: a battery about to die.
1193    Critical,
1194}
1195
1196/// A notification to show on screen, from [`Widget::notices`](crate::Widget::notices).
1197///
1198/// It says what the notification is, not how it looks: the host lays out the notification, places it and
1199/// animates it in and out. Its `key` is how the host tells notices apart from one view to the
1200/// next: a notice that keeps its key and changes its text is updated in place, and one whose key
1201/// is gone from the list leaves the screen.
1202pub struct Notice<M> {
1203    key: u64,
1204    summary: String,
1205    app: Option<String>,
1206    app_icon: Option<String>,
1207    image: Option<String>,
1208    body: Option<String>,
1209    markdown: bool,
1210    urgency: Urgency,
1211    actions: Vec<(String, M)>,
1212    on_press: Option<M>,
1213    on_dismiss: Option<M>,
1214    expires: Option<(std::time::Duration, M)>,
1215    reply: Option<Reply<M>>,
1216}
1217
1218/// A field on a [`Notice`] to type an answer into, like a reply to a message: several lines,
1219/// Enter sends and Shift+Enter starts a new line.
1220///
1221/// It shows with the notice, but the host takes the keyboard only once it's clicked, and gives it
1222/// back on Esc, on sending, or on a click elsewhere. The notice doesn't expire while it has the
1223/// keyboard.
1224pub struct Reply<M> {
1225    placeholder: String,
1226    value: String,
1227    on_input: std::rc::Rc<dyn Fn(String) -> M>,
1228    on_submit: Option<M>,
1229    on_cancel: Option<M>,
1230}
1231
1232impl<M> Reply<M> {
1233    /// Shows `value`, and sends `on_input` with the new text on every change.
1234    pub fn new(
1235        placeholder: impl Into<String>,
1236        value: impl Into<String>,
1237        on_input: impl Fn(String) -> M + 'static,
1238    ) -> Self {
1239        Self {
1240            placeholder: placeholder.into(),
1241            value: value.into(),
1242            on_input: std::rc::Rc::new(on_input),
1243            on_submit: None,
1244            on_cancel: None,
1245        }
1246    }
1247
1248    /// Sent on Enter, or a click on the field's send button.
1249    pub fn on_submit(mut self, message: M) -> Self {
1250        self.on_submit = Some(message);
1251        self
1252    }
1253
1254    /// Sent on Esc.
1255    pub fn on_cancel(mut self, message: M) -> Self {
1256        self.on_cancel = Some(message);
1257        self
1258    }
1259
1260    pub fn map<N: 'static>(self, f: &(impl Fn(M) -> N + Clone + 'static)) -> Reply<N>
1261    where
1262        M: 'static,
1263    {
1264        let g = f.clone();
1265        let on_input = self.on_input;
1266        Reply {
1267            placeholder: self.placeholder,
1268            value: self.value,
1269            on_input: std::rc::Rc::new(move |text| g(on_input(text))),
1270            on_submit: self.on_submit.map(f),
1271            on_cancel: self.on_cancel.map(f),
1272        }
1273    }
1274
1275    #[doc(hidden)]
1276    pub fn into_parts(self) -> ReplyParts<M> {
1277        ReplyParts {
1278            placeholder: self.placeholder,
1279            value: self.value,
1280            on_input: self.on_input,
1281            on_submit: self.on_submit,
1282            on_cancel: self.on_cancel,
1283        }
1284    }
1285}
1286
1287#[doc(hidden)]
1288#[derive(Clone)]
1289pub struct ReplyParts<M> {
1290    pub placeholder: String,
1291    pub value: String,
1292    pub on_input: std::rc::Rc<dyn Fn(String) -> M>,
1293    pub on_submit: Option<M>,
1294    pub on_cancel: Option<M>,
1295}
1296
1297impl<M> Notice<M> {
1298    pub fn new(key: u64, summary: impl Into<String>) -> Self {
1299        Self {
1300            key,
1301            summary: summary.into(),
1302            app: None,
1303            app_icon: None,
1304            image: None,
1305            body: None,
1306            markdown: false,
1307            urgency: Urgency::Normal,
1308            actions: Vec::new(),
1309            on_press: None,
1310            on_dismiss: None,
1311            expires: None,
1312            reply: None,
1313        }
1314    }
1315
1316    /// The name of the app it comes from.
1317    pub fn app(mut self, name: impl Into<String>) -> Self {
1318        self.app = Some(name.into());
1319        self
1320    }
1321
1322    /// The app's icon, by app id, like [`app_icon`].
1323    pub fn app_icon(mut self, app_id: impl Into<String>) -> Self {
1324        self.app_icon = Some(app_id.into());
1325        self
1326    }
1327
1328    /// A picture to show instead of the app's icon, by URL, like a [`cover`]: an icon file the
1329    /// app gave as a path.
1330    pub fn image(mut self, url: impl Into<String>) -> Self {
1331        self.image = Some(url.into());
1332        self
1333    }
1334
1335    /// Plain text under the summary. The host wraps it and cuts it off after a few lines.
1336    pub fn body(mut self, text: impl Into<String>) -> Self {
1337        self.body = Some(text.into());
1338        self
1339    }
1340
1341    /// The body is Markdown, like a [`markdown`] node.
1342    pub fn markdown(mut self) -> Self {
1343        self.markdown = true;
1344        self
1345    }
1346
1347    pub fn urgency(mut self, urgency: Urgency) -> Self {
1348        self.urgency = urgency;
1349        self
1350    }
1351
1352    /// A button on the notification that sends `message`. The first one is the main action.
1353    pub fn action(mut self, label: impl Into<String>, message: M) -> Self {
1354        self.actions.push((label.into(), message));
1355        self
1356    }
1357
1358    /// Sent on a click on the notification itself, outside its buttons.
1359    pub fn on_press(mut self, message: M) -> Self {
1360        self.on_press = Some(message);
1361        self
1362    }
1363
1364    /// Sent when its close button is clicked. Without it, it has none.
1365    pub fn on_dismiss(mut self, message: M) -> Self {
1366        self.on_dismiss = Some(message);
1367        self
1368    }
1369
1370    /// Sent once it has been on screen for `after`. The host doesn't count while the pointer is
1371    /// on it or it's waiting to be shown. Without it, it stays
1372    /// until it leaves the list.
1373    pub fn expires(mut self, after: std::time::Duration, message: M) -> Self {
1374        self.expires = Some((after, message));
1375        self
1376    }
1377
1378    /// A field to type an answer into ([`Reply`]).
1379    pub fn reply(mut self, reply: Reply<M>) -> Self {
1380        self.reply = Some(reply);
1381        self
1382    }
1383
1384    pub fn map<N: 'static>(self, f: &(impl Fn(M) -> N + Clone + 'static)) -> Notice<N>
1385    where
1386        M: 'static,
1387    {
1388        Notice {
1389            key: self.key,
1390            summary: self.summary,
1391            app: self.app,
1392            app_icon: self.app_icon,
1393            image: self.image,
1394            body: self.body,
1395            markdown: self.markdown,
1396            urgency: self.urgency,
1397            actions: self
1398                .actions
1399                .into_iter()
1400                .map(|(label, message)| (label, f(message)))
1401                .collect(),
1402            on_press: self.on_press.map(f),
1403            on_dismiss: self.on_dismiss.map(f),
1404            expires: self.expires.map(|(after, message)| (after, f(message))),
1405            reply: self.reply.map(|reply| reply.map(f)),
1406        }
1407    }
1408
1409    pub fn key(&self) -> u64 {
1410        self.key
1411    }
1412
1413    #[doc(hidden)]
1414    pub fn into_parts(self) -> NoticeParts<M> {
1415        NoticeParts {
1416            key: self.key,
1417            summary: self.summary,
1418            app: self.app,
1419            app_icon: self.app_icon,
1420            image: self.image,
1421            body: self.body,
1422            markdown: self.markdown,
1423            urgency: self.urgency,
1424            actions: self.actions,
1425            on_press: self.on_press,
1426            on_dismiss: self.on_dismiss,
1427            expires: self.expires,
1428            reply: self.reply.map(Reply::into_parts),
1429        }
1430    }
1431}
1432
1433#[doc(hidden)]
1434#[derive(Clone)]
1435pub struct NoticeParts<M> {
1436    pub key: u64,
1437    pub summary: String,
1438    pub app: Option<String>,
1439    pub app_icon: Option<String>,
1440    pub image: Option<String>,
1441    pub body: Option<String>,
1442    pub markdown: bool,
1443    pub urgency: Urgency,
1444    pub actions: Vec<(String, M)>,
1445    pub on_press: Option<M>,
1446    pub on_dismiss: Option<M>,
1447    pub expires: Option<(std::time::Duration, M)>,
1448    pub reply: Option<ReplyParts<M>>,
1449}
1450
1451#[cfg(test)]
1452mod tests {
1453    use super::*;
1454
1455    #[test]
1456    fn map_converts_every_message() {
1457        let node = row([
1458            button(label("a"), 1).on_secondary(4),
1459            column([button(label("b"), 2), label("c")]),
1460            switcher([(label("d"), 3)], Some(0)),
1461        ]);
1462
1463        let Kind::Row(children) = node.map(&|n: i32| n * 10).into_kind() else {
1464            panic!("row became something else");
1465        };
1466        let mut messages = Vec::new();
1467        collect(children, &mut messages);
1468        assert_eq!(messages, [10, 40, 20, 30]);
1469    }
1470
1471    fn collect(nodes: Vec<Node<i32>>, out: &mut Vec<i32>) {
1472        for node in nodes {
1473            match node.into_kind() {
1474                Kind::Button {
1475                    on_press,
1476                    on_secondary,
1477                    ..
1478                } => {
1479                    out.push(on_press);
1480                    out.extend(on_secondary);
1481                }
1482                Kind::Switcher { options, .. } => {
1483                    out.extend(options.into_iter().map(|(_, message)| message))
1484                }
1485                Kind::Row(children) | Kind::Column(children) => collect(children, out),
1486                _ => {}
1487            }
1488        }
1489    }
1490
1491    #[test]
1492    fn hover_shows_the_second_option() {
1493        let node: Node<()> = hover(label("a"), label("b"));
1494        assert!(matches!(
1495            node.into_kind(),
1496            Kind::Alternate {
1497                current: 0,
1498                hovered: Some(1),
1499                ..
1500            }
1501        ));
1502        let node: Node<()> = label("a").on_hover(1);
1503        assert!(matches!(node.into_kind(), Kind::Label { .. }));
1504    }
1505
1506    #[test]
1507    fn tone_fills_only_unset_text() {
1508        let node: Node<()> =
1509            row([label("a").tone(Tone::Accent), label("b"), icon(Icon::Wired)]).tone(Tone::Dim);
1510        let Kind::Row(children) = node.into_kind() else {
1511            unreachable!()
1512        };
1513        let tones: Vec<_> = children
1514            .into_iter()
1515            .map(|child| match child.into_kind() {
1516                Kind::Label { tone, .. } | Kind::Icon { tone, .. } => tone,
1517                _ => None,
1518            })
1519            .collect();
1520        assert_eq!(
1521            tones,
1522            [Some(Tone::Accent), Some(Tone::Dim), Some(Tone::Dim)]
1523        );
1524    }
1525
1526    #[test]
1527    fn map_reaches_new_nodes_and_their_closures() {
1528        let node = column([
1529            header("h").on_back(1).trailing(toggle(true, 2)),
1530            list([list_item("a")
1531                .on_press(3)
1532                .on_secondary(4)
1533                .trailing(button(icon(Icon::Info), 5))]),
1534            slider(0.5, 0.0..=1.0, |v| (v * 10.0) as i32).on_release(6),
1535            text_input("p", "v", |text: String| text.len() as i32).on_submit(7),
1536            scroll(text_area("p", "v", |text: String| text.len() as i32 + 1).on_submit(8)).bottom(),
1537        ]);
1538        let Kind::Column(children) = node.map(&|n: i32| n * 10).into_kind() else {
1539            panic!("column became something else");
1540        };
1541        let mut children = children.into_iter().map(Node::into_kind);
1542        let Some(Kind::Header {
1543            on_back: Some(10),
1544            trailing: Some(trailing),
1545            ..
1546        }) = children.next()
1547        else {
1548            panic!("header lost its back message");
1549        };
1550        assert!(matches!(
1551            trailing.into_kind(),
1552            Kind::Toggle { on_toggle: 20, .. }
1553        ));
1554        let Some(Kind::List(rows)) = children.next() else {
1555            panic!("list lost");
1556        };
1557        let Some(Kind::ListItem {
1558            on_press: Some(30),
1559            on_secondary: Some(40),
1560            trailing: Some(trailing),
1561            ..
1562        }) = rows.into_iter().next().map(Node::into_kind)
1563        else {
1564            panic!("list item lost its messages");
1565        };
1566        assert!(matches!(
1567            trailing.into_kind(),
1568            Kind::Button { on_press: 50, .. }
1569        ));
1570        let Some(Kind::Slider {
1571            on_change,
1572            on_release: Some(60),
1573            ..
1574        }) = children.next()
1575        else {
1576            panic!("slider lost");
1577        };
1578        assert_eq!(on_change(0.3), 30);
1579        let Some(Kind::TextInput {
1580            on_input,
1581            on_submit: Some(70),
1582            ..
1583        }) = children.next()
1584        else {
1585            panic!("text input lost");
1586        };
1587        assert_eq!(on_input("abcd".into()), 40);
1588        let Some(Kind::Scroll {
1589            content,
1590            bottom: true,
1591        }) = children.next()
1592        else {
1593            panic!("scroll lost");
1594        };
1595        let Kind::TextArea {
1596            on_input,
1597            on_submit: Some(80),
1598            ..
1599        } = content.into_kind()
1600        else {
1601            panic!("text area lost");
1602        };
1603        assert_eq!(on_input("abcd".into()), 50);
1604    }
1605
1606    #[test]
1607    fn tabs_map_and_tone_every_page() {
1608        let node = tabs([("a", button(label("x"), 1)), ("b", button(label("y"), 2))])
1609            .tone(Tone::Dim)
1610            .map(&|n: i32| n * 10);
1611        let Kind::Tabs(pages) = node.into_kind() else {
1612            panic!("tabs became something else");
1613        };
1614        let names: Vec<_> = pages.iter().map(|(name, _)| name.as_str()).collect();
1615        assert_eq!(names, ["a", "b"]);
1616        let mut messages = Vec::new();
1617        for (_, page) in pages {
1618            let Kind::Button {
1619                content, on_press, ..
1620            } = page.into_kind()
1621            else {
1622                panic!("page lost its button");
1623            };
1624            assert!(matches!(
1625                content.into_kind(),
1626                Kind::Label {
1627                    tone: Some(Tone::Dim),
1628                    ..
1629                }
1630            ));
1631            messages.push(on_press);
1632        }
1633        assert_eq!(messages, [10, 20]);
1634    }
1635
1636    #[test]
1637    fn notice_map_reaches_every_message() {
1638        let notice = Notice::new(7, "s")
1639            .action("a", 1)
1640            .action("b", 2)
1641            .on_press(3)
1642            .on_dismiss(4)
1643            .expires(std::time::Duration::from_secs(5), 5)
1644            .reply(
1645                Reply::new("p", "v", |text: String| text.len() as i32)
1646                    .on_submit(6)
1647                    .on_cancel(7),
1648            )
1649            .map(&|n: i32| n * 10);
1650        assert_eq!(notice.key(), 7);
1651        let parts = notice.into_parts();
1652        let actions: Vec<_> = parts.actions.into_iter().map(|(_, m)| m).collect();
1653        assert_eq!(actions, [10, 20]);
1654        assert_eq!(parts.on_press, Some(30));
1655        assert_eq!(parts.on_dismiss, Some(40));
1656        assert_eq!(parts.expires.map(|(_, m)| m), Some(50));
1657        let reply = parts.reply.unwrap();
1658        assert_eq!((reply.on_input)("abc".into()), 30);
1659        assert_eq!((reply.on_submit, reply.on_cancel), (Some(60), Some(70)));
1660    }
1661
1662    #[test]
1663    fn modifiers_only_touch_their_nodes() {
1664        let node: Node<i32> = label("a").trailing(label("b")).on_press(1).selected(true);
1665        assert!(matches!(node.into_kind(), Kind::Label { .. }));
1666        let node: Node<i32> = button(label("a"), 1).on_press(2).prominent();
1667        assert!(matches!(
1668            node.into_kind(),
1669            Kind::Button {
1670                on_press: 1,
1671                prominent: true,
1672                ..
1673            }
1674        ));
1675    }
1676
1677    #[test]
1678    fn tone_reaches_details_and_row_icons() {
1679        let node: Node<()> = column([
1680            detail("n", label("v")),
1681            list_item("t").leading(icon(Icon::Lock)),
1682        ])
1683        .tone(Tone::Good);
1684        let Kind::Column(children) = node.into_kind() else {
1685            unreachable!()
1686        };
1687        let mut children = children.into_iter().map(Node::into_kind);
1688        let Some(Kind::Detail { value, .. }) = children.next() else {
1689            unreachable!()
1690        };
1691        assert!(matches!(
1692            value.into_kind(),
1693            Kind::Label {
1694                tone: Some(Tone::Good),
1695                ..
1696            }
1697        ));
1698        let Some(Kind::ListItem {
1699            leading: Some(leading),
1700            ..
1701        }) = children.next()
1702        else {
1703            unreachable!()
1704        };
1705        assert!(matches!(
1706            leading.into_kind(),
1707            Kind::Icon {
1708                tone: Some(Tone::Good),
1709                ..
1710            }
1711        ));
1712    }
1713}