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;
8use std::sync::Arc;
9use std::time::SystemTime;
10
11/// The role of a piece of text. The theme decides the color.
12#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
13pub enum Tone {
14    #[default]
15    Normal,
16    /// Secondary information.
17    Dim,
18    /// The thing to notice: today in a calendar, the active workspace.
19    Accent,
20    Warning,
21    Critical,
22    /// Healthy, or gaining: a charging battery.
23    Good,
24}
25
26/// A status symbol. It names what to show and how full it is; the host draws it in the text's
27/// tone, at the text's size. Fractions are 0.0 to 1.0.
28#[derive(Debug, Clone, Copy, PartialEq)]
29pub enum Icon {
30    /// A speaker with sound waves for the level. Above 1.0 is amplified.
31    Volume { level: f32, muted: bool },
32    /// A Wi-Fi fan with as many bars as the signal. `None` when there's no link.
33    Wifi { strength: Option<f32> },
34    /// A wired connection.
35    Wired,
36    /// A battery filled to its charge.
37    Battery { charge: f32, charging: bool },
38    /// A padlock: a secured network.
39    Lock,
40    /// An `i` in a circle: more about this.
41    Info,
42    /// A chevron pointing left: back, or earlier.
43    Back,
44    /// A chevron pointing right: on, or later.
45    Forward,
46    /// A check mark: the one in use.
47    Check,
48    /// A circling arrow: look again.
49    Refresh,
50    /// A cross: close this.
51    Close,
52    /// An eye: show what's hidden.
53    Reveal,
54    /// A lightning bolt: charging.
55    Bolt,
56    /// A power plug: on mains power.
57    Plug,
58    /// A microphone, struck through when muted.
59    Mic { muted: bool },
60    /// A loudspeaker box: built-in speakers.
61    Speaker,
62    /// Headphones, or a headset.
63    Headphones,
64    /// A screen: a monitor or TV that plays sound.
65    Display,
66    /// A bell: notifications, struck through when they're silenced.
67    Bell { muted: bool },
68    /// A sun whose rays light up with the level: a screen's brightness.
69    Brightness { level: f32 },
70    /// A keyboard: its backlight.
71    KeyboardLight,
72    /// Three level bars, like an equalizer: media. Uneven while `playing`, low and level when
73    /// not.
74    Media { playing: bool },
75    /// A triangle pointing right: play.
76    Play,
77    /// Two upright bars: pause.
78    Pause,
79    /// A triangle pointing left against a bar: back to the previous track.
80    Previous,
81    /// A triangle pointing right against a bar: on to the next track.
82    Next,
83}
84
85/// Where a [`step`] of a [`timeline`] stands. The host draws it as the step's mark on the line.
86#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
87pub enum Mark {
88    /// Something said or noted along the way.
89    #[default]
90    Note,
91    /// What you did or said: where a stretch of steps starts.
92    You,
93    /// Under way.
94    Running,
95    /// Finished.
96    Done,
97    /// Finished, and it went wrong.
98    Failed,
99    /// Held up until you answer.
100    Waiting,
101}
102
103/// What a line of a [`diff`] is.
104#[derive(Debug, Clone, Copy, PartialEq, Eq)]
105pub enum Change {
106    /// Unchanged, shown around the changes.
107    Context,
108    Added,
109    Removed,
110    /// Where the next stretch of changes is, like `@@ -88,7 +88,9 @@`.
111    Hunk,
112}
113
114/// A piece of UI. Build it with the functions in this module.
115pub struct Node<M>(Kind<M>);
116
117#[doc(hidden)]
118pub enum Kind<M> {
119    Label {
120        text: String,
121        tone: Option<Tone>,
122    },
123    Heading {
124        text: String,
125        tone: Option<Tone>,
126    },
127    Icon {
128        icon: Icon,
129        tone: Option<Tone>,
130    },
131    AppIcon {
132        app_id: String,
133        tone: Option<Tone>,
134    },
135    Image {
136        file: &'static [u8],
137        tint: bool,
138        tone: Option<Tone>,
139    },
140    Sprite {
141        frames: &'static [&'static [u8]],
142        /// Indices into `frames`, in the order they play.
143        order: &'static [u8],
144        /// Plays for as long as it shows, not only when it shows up or changes.
145        looping: bool,
146        tone: Option<Tone>,
147    },
148    Cover {
149        url: String,
150        /// Shown until the picture is loaded, and instead of it if it can't be.
151        fallback: Box<Node<M>>,
152        /// The main picture of a popover, not an icon.
153        large: bool,
154        tone: Option<Tone>,
155    },
156    Counter {
157        value: i64,
158        range: RangeInclusive<i64>,
159        unit: String,
160        tone: Option<Tone>,
161    },
162    Alternate {
163        options: Vec<Node<M>>,
164        current: usize,
165        /// Shown instead of `current` while the pointer is over the item.
166        hovered: Option<usize>,
167        /// As wide as the option on screen, hovered by its own width, after a rest ([`expand`]).
168        grows: bool,
169    },
170    Row(Vec<Node<M>>),
171    Column(Vec<Node<M>>),
172    Grid {
173        columns: usize,
174        cells: Vec<Node<M>>,
175    },
176    Button {
177        content: Box<Node<M>>,
178        on_press: M,
179        on_secondary: Option<M>,
180        /// The main action of a popover page: filled, so it stands out.
181        prominent: bool,
182    },
183    Switcher {
184        options: Vec<(Node<M>, M)>,
185        current: Option<usize>,
186    },
187    Gulp(Box<Node<M>>),
188    /// Centered across the panel's width.
189    Center(Box<Node<M>>),
190    Separator,
191    Empty,
192    ListItem {
193        title: String,
194        subtitle: Option<String>,
195        leading: Option<Box<Node<M>>>,
196        trailing: Option<Box<Node<M>>>,
197        selected: bool,
198        on_press: Option<M>,
199        on_secondary: Option<M>,
200    },
201    List(Vec<Node<M>>),
202    Section {
203        title: String,
204        trailing: Option<Box<Node<M>>>,
205    },
206    Header {
207        title: String,
208        subtitle: Option<String>,
209        on_back: Option<M>,
210        leading: Option<Box<Node<M>>>,
211        trailing: Option<Box<Node<M>>>,
212    },
213    Detail {
214        name: String,
215        value: Box<Node<M>>,
216    },
217    Fill,
218    Slider {
219        value: f32,
220        range: RangeInclusive<f32>,
221        step: f32,
222        on_change: Box<dyn Fn(f32) -> M>,
223        on_release: Option<M>,
224    },
225    Toggle {
226        on: bool,
227        on_toggle: M,
228    },
229    Meter {
230        fraction: f32,
231        /// Where a limit sits along it, like a battery's charge limit.
232        limit: Option<f32>,
233        tone: Option<Tone>,
234    },
235    TextInput {
236        placeholder: String,
237        value: String,
238        on_input: Box<dyn Fn(String) -> M>,
239        on_submit: Option<M>,
240        secret: bool,
241        trailing: Option<Box<Node<M>>>,
242        tone: Option<Tone>,
243    },
244    Display(Box<Node<M>>),
245    Mark(Box<Node<M>>),
246    Counted {
247        content: Box<Node<M>>,
248        count: u32,
249    },
250    Badge {
251        text: String,
252        tone: Option<Tone>,
253    },
254    /// Pages and their names. The host shows one at a time and owns which.
255    Tabs(Vec<(String, Node<M>)>),
256    Markdown {
257        text: String,
258        tone: Option<Tone>,
259    },
260    TextArea {
261        placeholder: String,
262        value: String,
263        on_input: Box<dyn Fn(String) -> M>,
264        on_submit: Option<M>,
265        trailing: Option<Box<Node<M>>>,
266    },
267    Scroll {
268        content: Box<Node<M>>,
269        /// Keeps the end in view as content arrives there, like a chat.
270        bottom: bool,
271    },
272    Literal {
273        text: String,
274        tone: Option<Tone>,
275    },
276    Divider {
277        text: String,
278    },
279    Timeline(Vec<Node<M>>),
280    Step {
281        mark: Mark,
282        /// Drawn on the line in place of the mark.
283        leading: Option<Box<Node<M>>>,
284        content: Box<Node<M>>,
285    },
286    Diff {
287        title: String,
288        lines: Vec<(Change, String)>,
289    },
290}
291
292pub fn label<M>(text: impl Into<String>) -> Node<M> {
293    Node(Kind::Label {
294        text: text.into(),
295        tone: None,
296    })
297}
298
299/// A title, larger than a label. Meant for popovers.
300pub fn heading<M>(text: impl Into<String>) -> Node<M> {
301    Node(Kind::Heading {
302        text: text.into(),
303        tone: None,
304    })
305}
306
307/// A symbol, sized like a label.
308pub fn icon<M>(icon: Icon) -> Node<M> {
309    Node(Kind::Icon { icon, tone: None })
310}
311
312/// An application's icon, found by its app id or window class (`firefox`, `org.gnome.Nautilus`)
313/// in the user's icon theme. Sized like an [`icon`], in the app's own colors: a tone only dims it.
314/// Until the host has found it, or if there is none, it shows the id's first letter.
315pub fn app_icon<M>(app_id: impl Into<String>) -> Node<M> {
316    Node(Kind::AppIcon {
317        app_id: app_id.into(),
318        tone: None,
319    })
320}
321
322/// An icon the plugin ships: an SVG or PNG file compiled into it with `include_bytes!`, told
323/// apart by its first bytes. Sized like an [`icon`], in the file's own colors: a tone only dims
324/// it, as with [`app_icon`]. The host draws it once, off the UI thread, and shows nothing in its
325/// place until then.
326pub fn image<M>(file: &'static [u8]) -> Node<M> {
327    Node(Kind::Image {
328        file,
329        tint: false,
330        tone: None,
331    })
332}
333
334/// Like [`image`], but only the file's shape counts: it's drawn in the text's tone, like an
335/// [`icon`]. For a monochrome symbol the vocabulary doesn't have.
336pub fn symbol<M>(file: &'static [u8]) -> Node<M> {
337    Node(Kind::Image {
338        file,
339        tint: true,
340        tone: None,
341    })
342}
343
344/// A little animation the plugin ships, like a pixel-art mascot: `frames` are PNG or SVG files
345/// compiled in with `include_bytes!`, all the same size, and `order` lists which frame shows when,
346/// by index into `frames`. The host keeps the frames' shape and picks the height (a little under
347/// a label's in the bar, taller in a popover), scaling pixels up whole, never blurred: ship pixel
348/// art at its own size. The host shows a frame for a quarter of a second. It plays the loop a
349/// couple of times when it shows up or changes, then rests on the first frame of `order`;
350/// [`Node::looping`] keeps it going. A tone only dims it.
351pub fn sprite<M>(frames: &'static [&'static [u8]], order: &'static [u8]) -> Node<M> {
352    Node(Kind::Sprite {
353        frames,
354        order,
355        looping: false,
356        tone: None,
357    })
358}
359
360/// A picture a program hands over at runtime, by URL: a track's cover art. `file://` and
361/// `https://` URLs load; the host fetches, crops it square and rounds it, off the UI thread, and
362/// keeps the few latest. Drawn at icon size, or big with [`Node::large`]. Until it's there, or if
363/// it can't be loaded, `fallback` shows in its place. A tone only dims it.
364pub fn cover<M>(url: impl Into<String>, fallback: Node<M>) -> Node<M> {
365    Node(Kind::Cover {
366        url: url.into(),
367        fallback: Box::new(fallback),
368        large: false,
369        tone: None,
370    })
371}
372
373/// A number from `range`, with a unit after it (`"%"`, `"°"`, or `""`), shown like a label. It
374/// keeps room for the widest number in `range`, so it doesn't change width as it changes. When
375/// `value` changes, the host counts to it through every number in between.
376pub fn counter<M>(value: i64, range: RangeInclusive<i64>, unit: impl Into<String>) -> Node<M> {
377    Node(Kind::Counter {
378        value,
379        range,
380        unit: unit.into(),
381        tone: None,
382    })
383}
384
385/// One of `options` at a time: the one at `current`, or the last one if it's past the end. It's as
386/// big as the biggest option, so it doesn't change size when `current` does. The host fades from
387/// one to the other. Keep the options the same from one
388/// view to the next, so the host can tell which one is coming in.
389pub fn alternate<M>(options: impl IntoIterator<Item = Node<M>>, current: usize) -> Node<M> {
390    Node(Kind::Alternate {
391        options: options.into_iter().collect(),
392        current,
393        hovered: None,
394        grows: false,
395    })
396}
397
398/// `normal`, or `hovered` while the pointer is over the item, fading between them like an
399/// [`alternate`]: an icon that shows its percent, a time that shows the date.
400pub fn hover<M>(normal: Node<M>, hovered: Node<M>) -> Node<M> {
401    alternate([normal, hovered], 0).on_hover(1)
402}
403
404/// `collapsed`, or `expanded` once the pointer rests on this node, fading between them like a
405/// [`hover`]. Unlike `hover`, it's only as big as what it shows, so the item grows to make room
406/// and the island springs along, and it reacts to the pointer over it, not the whole item: a
407/// workspace that shows every window when you point at it.
408pub fn expand<M>(collapsed: Node<M>, expanded: Node<M>) -> Node<M> {
409    Node(Kind::Alternate {
410        options: vec![collapsed, expanded],
411        current: 0,
412        hovered: Some(1),
413        grows: true,
414    })
415}
416
417/// Children side by side, vertically centered.
418pub fn row<M>(children: impl IntoIterator<Item = Node<M>>) -> Node<M> {
419    Node(Kind::Row(children.into_iter().collect()))
420}
421
422/// Children stacked top to bottom.
423pub fn column<M>(children: impl IntoIterator<Item = Node<M>>) -> Node<M> {
424    Node(Kind::Column(children.into_iter().collect()))
425}
426
427/// `content` centered across the panel's width: a cover, a title, a row of controls. Text in it
428/// stays on one line, cut and faded at the edges of its room if it's too long. In the bar, it's
429/// just `content`.
430pub fn center<M>(content: Node<M>) -> Node<M> {
431    Node(Kind::Center(Box::new(content)))
432}
433
434/// Cells laid out left to right in rows of `columns`, all the same width. A calendar, a keypad.
435pub fn grid<M>(columns: usize, cells: impl IntoIterator<Item = Node<M>>) -> Node<M> {
436    Node(Kind::Grid {
437        columns: columns.max(1),
438        cells: cells.into_iter().collect(),
439    })
440}
441
442pub fn button<M>(content: Node<M>, on_press: M) -> Node<M> {
443    Node(Kind::Button {
444        content: Box::new(content),
445        on_press,
446        on_secondary: None,
447        prominent: false,
448    })
449}
450
451/// A row of choices, one of them current: workspaces, output devices. Each option sends its
452/// message when clicked. The host marks the current option and moves the mark smoothly when
453/// `current` changes.
454pub fn switcher<M>(
455    options: impl IntoIterator<Item = (Node<M>, M)>,
456    current: Option<usize>,
457) -> Node<M> {
458    Node(Kind::Switcher {
459        options: options.into_iter().collect(),
460        current,
461    })
462}
463
464/// Keeps gulping, like a bottle being poured: the host swells `content` gently and steadily for as
465/// long as it's on screen. For something filling up, like a charging battery's percent.
466pub fn gulp<M>(content: Node<M>) -> Node<M> {
467    Node(Kind::Gulp(Box::new(content)))
468}
469
470/// A thin horizontal line between groups.
471pub fn separator<M>() -> Node<M> {
472    Node(Kind::Separator)
473}
474
475/// Takes up a grid cell, or nothing elsewhere.
476pub fn empty<M>() -> Node<M> {
477    Node(Kind::Empty)
478}
479
480/// A row in a popover list: a network, a device, a window. Give it a [`subtitle`](Node::subtitle),
481/// a [`leading`](Node::leading) icon, a [`trailing`](Node::trailing) node at the far end (a lock,
482/// an info button), mark the one in use with [`selected`](Node::selected), and make it clickable
483/// with [`on_press`](Node::on_press). A long title fades out at the end rather than wrapping.
484pub fn list_item<M>(title: impl Into<String>) -> Node<M> {
485    Node(Kind::ListItem {
486        title: title.into(),
487        subtitle: None,
488        leading: None,
489        trailing: None,
490        selected: false,
491        on_press: None,
492        on_secondary: None,
493    })
494}
495
496/// Rows stacked closely, usually [`list_item`]s. In a popover, a long list scrolls instead of
497/// growing past a few rows.
498pub fn list<M>(rows: impl IntoIterator<Item = Node<M>>) -> Node<M> {
499    Node(Kind::List(rows.into_iter().collect()))
500}
501
502/// A small caption over a group in a popover ("Output", "Health"), with an optional
503/// [`trailing`](Node::trailing) node at the far end.
504pub fn section<M>(title: impl Into<String>) -> Node<M> {
505    Node(Kind::Section {
506        title: title.into(),
507        trailing: None,
508    })
509}
510
511/// The title of a popover page. Sub-pages get a back button with [`on_back`](Node::on_back); a
512/// [`leading`](Node::leading) picture goes before the title, a [`subtitle`](Node::subtitle)
513/// under it (where it is, what it's about), and a [`trailing`](Node::trailing) node sits at the
514/// far end (a switch, a count).
515pub fn header<M>(title: impl Into<String>) -> Node<M> {
516    Node(Kind::Header {
517        title: title.into(),
518        subtitle: None,
519        on_back: None,
520        leading: None,
521        trailing: None,
522    })
523}
524
525/// A named value on one line: the name dimmed at the start, the value at the end.
526pub fn detail<M>(name: impl Into<String>, value: Node<M>) -> Node<M> {
527    Node(Kind::Detail {
528        name: name.into(),
529        value: Box::new(value),
530    })
531}
532
533/// Takes the free space in a popover row, pushing what follows to the end. Nothing in the bar.
534pub fn fill<M>() -> Node<M> {
535    Node(Kind::Fill)
536}
537
538/// A value dragged along `range`: a volume. Sends `on_change` while dragging; add
539/// [`on_release`](Node::on_release) to act once it's let go, and [`step`](Node::step) to snap.
540pub fn slider<M: 'static>(
541    value: f32,
542    range: RangeInclusive<f32>,
543    on_change: impl Fn(f32) -> M + 'static,
544) -> Node<M> {
545    Node(Kind::Slider {
546        value,
547        range,
548        step: 1.0,
549        on_change: Box::new(on_change),
550        on_release: None,
551    })
552}
553
554/// An on/off switch. Sends `on_toggle` when clicked; the host slides it over.
555pub fn toggle<M>(on: bool, on_toggle: M) -> Node<M> {
556    Node(Kind::Toggle { on, on_toggle })
557}
558
559/// A bar filled to `fraction` (0.0 to 1.0), in its tone: a charge, a signal. The host eases it to
560/// a new value.
561pub fn meter<M>(fraction: f32) -> Node<M> {
562    Node(Kind::Meter {
563        fraction,
564        limit: None,
565        tone: None,
566    })
567}
568
569/// A field to type into, showing `value`. Sends `on_input` with the new text on every change.
570/// It takes the focus when it appears, and while it has it, keys stay in the popover; Esc still
571/// closes it. [`on_submit`](Node::on_submit) is sent on Enter. One per popover page. A
572/// [`Tone::Critical`] tone marks it as wrong.
573pub fn text_input<M: 'static>(
574    placeholder: impl Into<String>,
575    value: impl Into<String>,
576    on_input: impl Fn(String) -> M + 'static,
577) -> Node<M> {
578    Node(Kind::TextInput {
579        placeholder: placeholder.into(),
580        value: value.into(),
581        on_input: Box::new(on_input),
582        on_submit: None,
583        secret: false,
584        trailing: None,
585        tone: None,
586    })
587}
588
589/// A field to type several lines into, showing `value`: a message. It grows with its text up
590/// to a few lines, then scrolls. Sends `on_input` with the new text on every change, and
591/// [`on_submit`](Node::on_submit) on Enter; Shift+Enter starts a new line. In a popover it takes
592/// the focus when it appears, like a [`text_input`]. One per popover page.
593pub fn text_area<M: 'static>(
594    placeholder: impl Into<String>,
595    value: impl Into<String>,
596    on_input: impl Fn(String) -> M + 'static,
597) -> Node<M> {
598    Node(Kind::TextArea {
599        placeholder: placeholder.into(),
600        value: value.into(),
601        on_input: Box::new(on_input),
602        on_submit: None,
603        trailing: None,
604    })
605}
606
607/// Text written in Markdown, as people and programs write messages: paragraphs, emphasis,
608/// `code`, lists, quotes, headings, code blocks and tables. The host sets it in its own type and
609/// colors, wrapped to the width it has. Links show as links but don't open: the bar starts no
610/// programs.
611pub fn markdown<M>(text: impl Into<String>) -> Node<M> {
612    Node(Kind::Markdown {
613        text: text.into(),
614        tone: None,
615    })
616}
617
618/// Its content, scrolling in a popover once it's taller than the host allows a page to grow: a
619/// conversation, a long document. With [`Node::bottom`] it keeps its end in view. In the bar it's
620/// just its content.
621pub fn scroll<M>(content: Node<M>) -> Node<M> {
622    Node(Kind::Scroll {
623        content: Box::new(content),
624        bottom: false,
625    })
626}
627
628/// Text exactly as typed, not prose: a path, a command, a name in code. It doesn't wrap: when it
629/// doesn't fit, its end fades out.
630pub fn literal<M>(text: impl Into<String>) -> Node<M> {
631    Node(Kind::Literal {
632        text: text.into(),
633        tone: None,
634    })
635}
636
637/// A [`separator`] with a short caption in the middle of it, like the time between two turns of
638/// a conversation.
639pub fn divider<M>(text: impl Into<String>) -> Node<M> {
640    Node(Kind::Divider { text: text.into() })
641}
642
643/// Steps one after another, strung on a line: what someone did, in order. Each child is a
644/// [`step`], whose mark says where it stands; other nodes count as steps marked
645/// [`Mark::Note`].
646pub fn timeline<M>(steps: impl IntoIterator<Item = Node<M>>) -> Node<M> {
647    Node(Kind::Timeline(steps.into_iter().collect()))
648}
649
650/// One step of a [`timeline`]: `content`, marked on the line as `mark`. A
651/// [`leading`](Node::leading) node, like a small picture, stands on the line in place of the
652/// mark. Outside a timeline it's just its content.
653pub fn step<M>(mark: Mark, content: Node<M>) -> Node<M> {
654    Node(Kind::Step {
655        mark,
656        leading: None,
657        content: Box::new(content),
658    })
659}
660
661/// A change to a file, titled with its name: lines added, removed, and some unchanged ones around
662/// them. The host counts what was added and removed.
663pub fn diff<M>(
664    title: impl Into<String>,
665    lines: impl IntoIterator<Item = (Change, String)>,
666) -> Node<M> {
667    Node(Kind::Diff {
668        title: title.into(),
669        lines: lines.into_iter().collect(),
670    })
671}
672
673/// Its text, large: the one number a popover is about, like a battery's charge.
674pub fn display<M>(content: Node<M>) -> Node<M> {
675    Node(Kind::Display(Box::new(content)))
676}
677
678/// Its content marked with a ring: today in a calendar.
679pub fn mark<M>(content: Node<M>) -> Node<M> {
680    Node(Kind::Mark(Box::new(content)))
681}
682
683/// Its content with `count` in a red badge at its top right, overlapping it a little: unread
684/// notifications on a bell. At 0 there's no badge. The host grows the badge in and out and counts
685/// through the numbers in between; past 99 it shows 99.
686pub fn counted<M>(content: Node<M>, count: u32) -> Node<M> {
687    Node(Kind::Counted {
688        content: Box::new(content),
689        count,
690    })
691}
692
693/// A short code in a small outlined box: a keyboard layout's `US`.
694pub fn badge<M>(text: impl Into<String>) -> Node<M> {
695    Node(Kind::Badge {
696        text: text.into(),
697        tone: None,
698    })
699}
700
701/// Pages behind a row of tabs, one page showing at a time: a popover's Output, Input and Apps.
702/// The host draws the tabs, switches pages when one is clicked and starts on the first each time
703/// the popover opens, so the plugin keeps no state for it. In the bar, only the first page shows.
704pub fn tabs<M>(pages: impl IntoIterator<Item = (impl Into<String>, Node<M>)>) -> Node<M> {
705    Node(Kind::Tabs(
706        pages
707            .into_iter()
708            .map(|(name, page)| (name.into(), page))
709            .collect(),
710    ))
711}
712
713impl<M> Node<M> {
714    /// Sets the tone of this text, or of every text inside this node that has none yet.
715    pub fn tone(mut self, tone: Tone) -> Self {
716        self.set_tone(tone);
717        self
718    }
719
720    /// For an [`alternate`]: shows the option at `index` while the pointer is over the item,
721    /// whatever `current` is. Other nodes have nothing to show on hover and ignore it.
722    pub fn on_hover(mut self, index: usize) -> Self {
723        if let Kind::Alternate { hovered, .. } = &mut self.0 {
724            *hovered = Some(index);
725        }
726        self
727    }
728
729    /// For a [`button`] or [`list_item`]: sent on a secondary (right) click on it. Other nodes
730    /// ignore it.
731    pub fn on_secondary(mut self, message: M) -> Self {
732        if let Kind::Button { on_secondary, .. } | Kind::ListItem { on_secondary, .. } = &mut self.0
733        {
734            *on_secondary = Some(message);
735        }
736        self
737    }
738
739    /// For a [`button`]: the main action of its page, filled so it stands out. Other nodes ignore
740    /// it.
741    pub fn prominent(mut self) -> Self {
742        if let Kind::Button { prominent, .. } = &mut self.0 {
743            *prominent = true;
744        }
745        self
746    }
747
748    /// For a [`list_item`]: sent on a click on it. Other nodes ignore it.
749    pub fn on_press(mut self, message: M) -> Self {
750        if let Kind::ListItem { on_press, .. } = &mut self.0 {
751            *on_press = Some(message);
752        }
753        self
754    }
755
756    /// For a [`list_item`] or [`header`]: a second, dimmer line under the title. Other nodes
757    /// ignore it.
758    pub fn subtitle(mut self, text: impl Into<String>) -> Self {
759        if let Kind::ListItem { subtitle, .. } | Kind::Header { subtitle, .. } = &mut self.0 {
760            *subtitle = Some(text.into());
761        }
762        self
763    }
764
765    /// For a [`list_item`] or [`header`]: an icon or small picture before the title. For a
766    /// [`step`]: a small picture on the line, in place of its mark. Other nodes ignore it.
767    pub fn leading(mut self, node: Node<M>) -> Self {
768        if let Kind::ListItem { leading, .. }
769        | Kind::Header { leading, .. }
770        | Kind::Step { leading, .. } = &mut self.0
771        {
772            *leading = Some(Box::new(node));
773        }
774        self
775    }
776
777    /// For a [`list_item`], [`section`], [`header`], [`text_input`] or [`text_area`]: a node at the
778    /// far end, like a lock or an info button. Other nodes ignore it.
779    pub fn trailing(mut self, node: Node<M>) -> Self {
780        if let Kind::ListItem { trailing, .. }
781        | Kind::Section { trailing, .. }
782        | Kind::Header { trailing, .. }
783        | Kind::TextInput { trailing, .. }
784        | Kind::TextArea { trailing, .. } = &mut self.0
785        {
786            *trailing = Some(Box::new(node));
787        }
788        self
789    }
790
791    /// For a [`list_item`]: marks it as the one in use. Other nodes ignore it.
792    pub fn selected(mut self, yes: bool) -> Self {
793        if let Kind::ListItem { selected, .. } = &mut self.0 {
794            *selected = yes;
795        }
796        self
797    }
798
799    /// For a [`header`]: shows a back button that sends `message`. Other nodes ignore it.
800    pub fn on_back(mut self, message: M) -> Self {
801        if let Kind::Header { on_back, .. } = &mut self.0 {
802            *on_back = Some(message);
803        }
804        self
805    }
806
807    /// For a [`slider`]: the step its value snaps to (1 by default). Other nodes ignore it.
808    pub fn step(mut self, by: f32) -> Self {
809        if let Kind::Slider { step, .. } = &mut self.0 {
810            *step = by;
811        }
812        self
813    }
814
815    /// For a [`slider`]: sent when it's let go. Other nodes ignore it.
816    pub fn on_release(mut self, message: M) -> Self {
817        if let Kind::Slider { on_release, .. } = &mut self.0 {
818            *on_release = Some(message);
819        }
820        self
821    }
822
823    /// For a [`text_input`] or [`text_area`]: sent on Enter. Other nodes ignore it.
824    pub fn on_submit(mut self, message: M) -> Self {
825        if let Kind::TextInput { on_submit, .. } | Kind::TextArea { on_submit, .. } = &mut self.0 {
826            *on_submit = Some(message);
827        }
828        self
829    }
830
831    /// For a [`text_input`]: hides what's typed, like a password. Other nodes ignore it.
832    pub fn secret(mut self, yes: bool) -> Self {
833        if let Kind::TextInput { secret, .. } = &mut self.0 {
834            *secret = yes;
835        }
836        self
837    }
838
839    /// For a [`sprite`]: plays for as long as it shows, not only when it shows up or changes.
840    /// Every frame redraws the bar, so keep it for what wants your eye, or for popovers. Other
841    /// nodes ignore it.
842    pub fn looping(mut self) -> Self {
843        if let Kind::Sprite { looping, .. } = &mut self.0 {
844            *looping = true;
845        }
846        self
847    }
848
849    /// For a [`cover`]: drawn big, as the main picture of a popover, not as an icon. Other nodes
850    /// ignore it.
851    pub fn large(mut self) -> Self {
852        if let Kind::Cover { large, .. } = &mut self.0 {
853            *large = true;
854        }
855        self
856    }
857
858    /// For a [`scroll`]: keeps its end in view as content arrives there, unless you've scrolled
859    /// up to read. Other nodes ignore it.
860    pub fn bottom(mut self) -> Self {
861        if let Kind::Scroll { bottom, .. } = &mut self.0 {
862            *bottom = true;
863        }
864        self
865    }
866
867    /// For a [`meter`]: marks a limit along it (0.0 to 1.0). Other nodes ignore it.
868    pub fn limit(mut self, at: f32) -> Self {
869        if let Kind::Meter { limit, .. } = &mut self.0 {
870            *limit = Some(at);
871        }
872        self
873    }
874
875    fn set_tone(&mut self, new: Tone) {
876        match &mut self.0 {
877            Kind::Label { tone, .. }
878            | Kind::Heading { tone, .. }
879            | Kind::Icon { tone, .. }
880            | Kind::AppIcon { tone, .. }
881            | Kind::Image { tone, .. }
882            | Kind::Sprite { tone, .. }
883            | Kind::Counter { tone, .. }
884            | Kind::Meter { tone, .. }
885            | Kind::TextInput { tone, .. }
886            | Kind::Markdown { tone, .. }
887            | Kind::Literal { tone, .. }
888            | Kind::Badge { tone, .. } => {
889                tone.get_or_insert(new);
890            }
891            Kind::Cover { tone, fallback, .. } => {
892                tone.get_or_insert(new);
893                fallback.set_tone(new);
894            }
895            Kind::Alternate {
896                options: children, ..
897            }
898            | Kind::Row(children)
899            | Kind::Column(children)
900            | Kind::Grid {
901                cells: children, ..
902            }
903            | Kind::List(children)
904            | Kind::Timeline(children) => {
905                children.iter_mut().for_each(|child| child.set_tone(new));
906            }
907            Kind::Step {
908                leading, content, ..
909            } => {
910                leading.iter_mut().for_each(|child| child.set_tone(new));
911                content.set_tone(new);
912            }
913            Kind::Button { content, .. }
914            | Kind::Gulp(content)
915            | Kind::Center(content)
916            | Kind::Display(content)
917            | Kind::Mark(content)
918            | Kind::Counted { content, .. }
919            | Kind::Scroll { content, .. }
920            | Kind::Detail { value: content, .. } => content.set_tone(new),
921            Kind::ListItem {
922                leading, trailing, ..
923            }
924            | Kind::Header {
925                leading, trailing, ..
926            } => {
927                leading
928                    .iter_mut()
929                    .chain(trailing.iter_mut())
930                    .for_each(|child| child.set_tone(new));
931            }
932            Kind::Tabs(pages) => {
933                for (_, page) in pages {
934                    page.set_tone(new);
935                }
936            }
937            Kind::Switcher { options, .. } => {
938                options
939                    .iter_mut()
940                    .for_each(|(child, _)| child.set_tone(new));
941            }
942            Kind::Separator
943            | Kind::Divider { .. }
944            | Kind::Diff { .. }
945            | Kind::Empty
946            | Kind::Section { .. }
947            | Kind::Fill
948            | Kind::Slider { .. }
949            | Kind::TextArea { .. }
950            | Kind::Toggle { .. } => {}
951        }
952    }
953
954    /// Converts every message in this node. Sliders and text fields keep `f` to convert the
955    /// messages they make later, so it must be `Clone + 'static`, like a function or a closure
956    /// that captures nothing.
957    pub fn map<N: 'static>(self, f: &(impl Fn(M) -> N + Clone + 'static)) -> Node<N>
958    where
959        M: 'static,
960    {
961        let map_all = |nodes: Vec<Node<M>>| nodes.into_iter().map(|node| node.map(f)).collect();
962        let map_box = |node: Option<Box<Node<M>>>| node.map(|node| Box::new(node.map(f)));
963
964        Node(match self.0 {
965            Kind::Label { text, tone } => Kind::Label { text, tone },
966            Kind::Heading { text, tone } => Kind::Heading { text, tone },
967            Kind::Icon { icon, tone } => Kind::Icon { icon, tone },
968            Kind::AppIcon { app_id, tone } => Kind::AppIcon { app_id, tone },
969            Kind::Image { file, tint, tone } => Kind::Image { file, tint, tone },
970            Kind::Sprite {
971                frames,
972                order,
973                looping,
974                tone,
975            } => Kind::Sprite {
976                frames,
977                order,
978                looping,
979                tone,
980            },
981            Kind::Cover {
982                url,
983                fallback,
984                large,
985                tone,
986            } => Kind::Cover {
987                url,
988                fallback: Box::new(fallback.map(f)),
989                large,
990                tone,
991            },
992            Kind::Counter {
993                value,
994                range,
995                unit,
996                tone,
997            } => Kind::Counter {
998                value,
999                range,
1000                unit,
1001                tone,
1002            },
1003            Kind::Alternate {
1004                options,
1005                current,
1006                hovered,
1007                grows,
1008            } => Kind::Alternate {
1009                options: map_all(options),
1010                current,
1011                hovered,
1012                grows,
1013            },
1014            Kind::Row(children) => Kind::Row(map_all(children)),
1015            Kind::Column(children) => Kind::Column(map_all(children)),
1016            Kind::Grid { columns, cells } => Kind::Grid {
1017                columns,
1018                cells: map_all(cells),
1019            },
1020            Kind::Button {
1021                content,
1022                on_press,
1023                on_secondary,
1024                prominent,
1025            } => Kind::Button {
1026                content: Box::new(content.map(f)),
1027                on_press: f(on_press),
1028                on_secondary: on_secondary.map(f),
1029                prominent,
1030            },
1031            Kind::Switcher { options, current } => Kind::Switcher {
1032                options: options
1033                    .into_iter()
1034                    .map(|(node, message)| (node.map(f), f(message)))
1035                    .collect(),
1036                current,
1037            },
1038            Kind::Gulp(content) => Kind::Gulp(Box::new(content.map(f))),
1039            Kind::Center(content) => Kind::Center(Box::new(content.map(f))),
1040            Kind::Separator => Kind::Separator,
1041            Kind::Empty => Kind::Empty,
1042            Kind::ListItem {
1043                title,
1044                subtitle,
1045                leading,
1046                trailing,
1047                selected,
1048                on_press,
1049                on_secondary,
1050            } => Kind::ListItem {
1051                title,
1052                subtitle,
1053                leading: map_box(leading),
1054                trailing: map_box(trailing),
1055                selected,
1056                on_press: on_press.map(f),
1057                on_secondary: on_secondary.map(f),
1058            },
1059            Kind::List(rows) => Kind::List(map_all(rows)),
1060            Kind::Section { title, trailing } => Kind::Section {
1061                title,
1062                trailing: map_box(trailing),
1063            },
1064            Kind::Header {
1065                title,
1066                subtitle,
1067                on_back,
1068                leading,
1069                trailing,
1070            } => Kind::Header {
1071                title,
1072                subtitle,
1073                on_back: on_back.map(f),
1074                leading: map_box(leading),
1075                trailing: map_box(trailing),
1076            },
1077            Kind::Detail { name, value } => Kind::Detail {
1078                name,
1079                value: Box::new(value.map(f)),
1080            },
1081            Kind::Fill => Kind::Fill,
1082            Kind::Slider {
1083                value,
1084                range,
1085                step,
1086                on_change,
1087                on_release,
1088            } => {
1089                let g = f.clone();
1090                Kind::Slider {
1091                    value,
1092                    range,
1093                    step,
1094                    on_change: Box::new(move |value| g(on_change(value))),
1095                    on_release: on_release.map(f),
1096                }
1097            }
1098            Kind::Toggle { on, on_toggle } => Kind::Toggle {
1099                on,
1100                on_toggle: f(on_toggle),
1101            },
1102            Kind::Meter {
1103                fraction,
1104                limit,
1105                tone,
1106            } => Kind::Meter {
1107                fraction,
1108                limit,
1109                tone,
1110            },
1111            Kind::TextInput {
1112                placeholder,
1113                value,
1114                on_input,
1115                on_submit,
1116                secret,
1117                trailing,
1118                tone,
1119            } => {
1120                let g = f.clone();
1121                Kind::TextInput {
1122                    placeholder,
1123                    value,
1124                    on_input: Box::new(move |text| g(on_input(text))),
1125                    on_submit: on_submit.map(f),
1126                    secret,
1127                    trailing: map_box(trailing),
1128                    tone,
1129                }
1130            }
1131            Kind::Display(content) => Kind::Display(Box::new(content.map(f))),
1132            Kind::Mark(content) => Kind::Mark(Box::new(content.map(f))),
1133            Kind::Counted { content, count } => Kind::Counted {
1134                content: Box::new(content.map(f)),
1135                count,
1136            },
1137            Kind::Badge { text, tone } => Kind::Badge { text, tone },
1138            Kind::Tabs(pages) => Kind::Tabs(
1139                pages
1140                    .into_iter()
1141                    .map(|(name, page)| (name, page.map(f)))
1142                    .collect(),
1143            ),
1144            Kind::Markdown { text, tone } => Kind::Markdown { text, tone },
1145            Kind::TextArea {
1146                placeholder,
1147                value,
1148                on_input,
1149                on_submit,
1150                trailing,
1151            } => {
1152                let g = f.clone();
1153                Kind::TextArea {
1154                    placeholder,
1155                    value,
1156                    on_input: Box::new(move |text| g(on_input(text))),
1157                    on_submit: on_submit.map(f),
1158                    trailing: map_box(trailing),
1159                }
1160            }
1161            Kind::Scroll { content, bottom } => Kind::Scroll {
1162                content: Box::new(content.map(f)),
1163                bottom,
1164            },
1165            Kind::Literal { text, tone } => Kind::Literal { text, tone },
1166            Kind::Divider { text } => Kind::Divider { text },
1167            Kind::Timeline(steps) => Kind::Timeline(map_all(steps)),
1168            Kind::Step {
1169                mark,
1170                leading,
1171                content,
1172            } => Kind::Step {
1173                mark,
1174                leading: map_box(leading),
1175                content: Box::new(content.map(f)),
1176            },
1177            Kind::Diff { title, lines } => Kind::Diff { title, lines },
1178        })
1179    }
1180
1181    #[doc(hidden)]
1182    pub fn into_kind(self) -> Kind<M> {
1183        self.0
1184    }
1185
1186    #[doc(hidden)]
1187    pub fn kind(&self) -> &Kind<M> {
1188        &self.0
1189    }
1190}
1191
1192/// What a widget shows in the bar.
1193pub struct Item<M> {
1194    content: Option<Node<M>>,
1195    on_press: Option<M>,
1196    on_secondary: Option<M>,
1197    on_scroll: Option<(M, M)>,
1198}
1199
1200impl<M> Item<M> {
1201    pub fn new(content: Node<M>) -> Self {
1202        Self {
1203            content: Some(content),
1204            on_press: None,
1205            on_secondary: None,
1206            on_scroll: None,
1207        }
1208    }
1209
1210    /// Takes no space in the bar. For widgets with nothing to show, like a battery on a desktop.
1211    pub fn hidden() -> Self {
1212        Self {
1213            content: None,
1214            on_press: None,
1215            on_secondary: None,
1216            on_scroll: None,
1217        }
1218    }
1219
1220    /// Sent on a primary click. Replaces the default of toggling the popover.
1221    pub fn on_press(mut self, message: M) -> Self {
1222        self.on_press = Some(message);
1223        self
1224    }
1225
1226    /// Sent on a secondary (right) click.
1227    pub fn on_secondary(mut self, message: M) -> Self {
1228        self.on_secondary = Some(message);
1229        self
1230    }
1231
1232    /// Sent once per scroll step over the item, `up` or `down`. The host turns smooth touchpad
1233    /// scrolling into steps.
1234    pub fn on_scroll(mut self, up: M, down: M) -> Self {
1235        self.on_scroll = Some((up, down));
1236        self
1237    }
1238
1239    pub fn map<N: 'static>(self, f: &(impl Fn(M) -> N + Clone + 'static)) -> Item<N>
1240    where
1241        M: 'static,
1242    {
1243        Item {
1244            content: self.content.map(|content| content.map(f)),
1245            on_press: self.on_press.map(f),
1246            on_secondary: self.on_secondary.map(f),
1247            on_scroll: self.on_scroll.map(|(up, down)| (f(up), f(down))),
1248        }
1249    }
1250
1251    #[doc(hidden)]
1252    pub fn into_parts(self) -> ItemParts<M> {
1253        ItemParts {
1254            content: self.content,
1255            on_press: self.on_press,
1256            on_secondary: self.on_secondary,
1257            on_scroll: self.on_scroll,
1258        }
1259    }
1260}
1261
1262#[doc(hidden)]
1263pub struct ItemParts<M> {
1264    pub content: Option<Node<M>>,
1265    pub on_press: Option<M>,
1266    pub on_secondary: Option<M>,
1267    /// Up, then down.
1268    pub on_scroll: Option<(M, M)>,
1269}
1270
1271/// The contents of a popover. The host draws the surface, its background and its edges, and lays
1272/// every popover out at the same width, or at a wider one if it asks ([`Panel::wide`]).
1273pub struct Panel<M> {
1274    content: Node<M>,
1275    on_open: Option<M>,
1276    on_close: Option<M>,
1277    wide: bool,
1278}
1279
1280impl<M> Panel<M> {
1281    pub fn new(content: Node<M>) -> Self {
1282        Self {
1283            content,
1284            on_open: None,
1285            on_close: None,
1286            wide: false,
1287        }
1288    }
1289
1290    /// Laid out wider than other popovers, for a page that's mostly reading: a conversation, a
1291    /// document. The host decides how wide.
1292    pub fn wide(mut self) -> Self {
1293        self.wide = true;
1294        self
1295    }
1296
1297    #[doc(hidden)]
1298    pub fn is_wide(&self) -> bool {
1299        self.wide
1300    }
1301
1302    /// Sent each time the popover opens: start a scan, show the first page.
1303    pub fn on_open(mut self, message: M) -> Self {
1304        self.on_open = Some(message);
1305        self
1306    }
1307
1308    /// Sent each time the popover starts closing: stop listening, forget what was typed.
1309    pub fn on_close(mut self, message: M) -> Self {
1310        self.on_close = Some(message);
1311        self
1312    }
1313
1314    pub fn map<N: 'static>(self, f: &(impl Fn(M) -> N + Clone + 'static)) -> Panel<N>
1315    where
1316        M: 'static,
1317    {
1318        Panel {
1319            content: self.content.map(f),
1320            on_open: self.on_open.map(f),
1321            on_close: self.on_close.map(f),
1322            wide: self.wide,
1323        }
1324    }
1325
1326    #[doc(hidden)]
1327    pub fn into_node(self) -> Node<M> {
1328        self.content
1329    }
1330
1331    #[doc(hidden)]
1332    pub fn on_open_message(self) -> Option<M> {
1333        self.on_open
1334    }
1335
1336    #[doc(hidden)]
1337    pub fn on_close_message(self) -> Option<M> {
1338        self.on_close
1339    }
1340}
1341
1342/// How much a [`Notice`] matters. The host decides what that looks like.
1343#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1344pub enum Urgency {
1345    /// Background news: a download finished.
1346    Low,
1347    #[default]
1348    Normal,
1349    /// Needs attention now: a battery about to die.
1350    Critical,
1351}
1352
1353/// A notification to show on screen, from [`Widget::notices`](crate::Widget::notices).
1354///
1355/// It says what the notification is, not how it looks: the host lays out the notification, places it and
1356/// animates it in and out. Its `key` is how the host tells notices apart from one view to the
1357/// next: a notice that keeps its key and changes its text is updated in place, and one whose key
1358/// is gone from the list leaves the screen.
1359pub struct Notice<M> {
1360    key: u64,
1361    summary: String,
1362    app: Option<String>,
1363    app_icon: Option<String>,
1364    icon: Option<Icon>,
1365    image: Option<String>,
1366    pixels: Option<Pixels>,
1367    body: Option<String>,
1368    markdown: bool,
1369    urgency: Urgency,
1370    time: Option<SystemTime>,
1371    thread: Option<String>,
1372    thread_name: Option<String>,
1373    progress: Option<f32>,
1374    actions: Vec<(String, M)>,
1375    choices: Option<ChoicesParts<M>>,
1376    step: Option<StepParts<M>>,
1377    on_press: Option<M>,
1378    on_dismiss: Option<M>,
1379    expires: Option<(std::time::Duration, M)>,
1380    reply: Option<Reply<M>>,
1381}
1382
1383/// A picture as its pixels, for a [`Notice`] whose picture came as bytes rather than a file: RGBA,
1384/// 8 bits a channel, row after row with nothing between them.
1385///
1386/// Cheap to clone. Keep the same `Pixels` for as long as the notice shows: the host draws it once
1387/// and knows it again by its bytes' address.
1388#[derive(Clone)]
1389pub struct Pixels {
1390    width: u32,
1391    height: u32,
1392    rgba: Arc<[u8]>,
1393}
1394
1395impl Pixels {
1396    /// `None` if `rgba` isn't `width × height` pixels of 4 bytes, or the picture is empty.
1397    pub fn new(width: u32, height: u32, rgba: impl Into<Arc<[u8]>>) -> Option<Self> {
1398        let rgba = rgba.into();
1399        let expected = (width as usize)
1400            .checked_mul(height as usize)?
1401            .checked_mul(4)?;
1402        (width > 0 && height > 0 && rgba.len() == expected).then_some(Self {
1403            width,
1404            height,
1405            rgba,
1406        })
1407    }
1408
1409    pub fn width(&self) -> u32 {
1410        self.width
1411    }
1412
1413    pub fn height(&self) -> u32 {
1414        self.height
1415    }
1416
1417    pub fn rgba(&self) -> &Arc<[u8]> {
1418        &self.rgba
1419    }
1420}
1421
1422impl std::fmt::Debug for Pixels {
1423    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1424        write!(f, "Pixels({}×{})", self.width, self.height)
1425    }
1426}
1427
1428/// Options to pick from on a [`Notice`], like the answers to a question: one, which a click sends,
1429/// or several, checked and then sent together.
1430pub struct Choices<M> {
1431    parts: ChoicesParts<M>,
1432}
1433
1434impl<M> Choices<M> {
1435    /// A click on an option sends its message: that's the answer.
1436    pub fn one() -> Self {
1437        Self::new(false)
1438    }
1439
1440    /// A click on an option sends its message, to check or uncheck it, and
1441    /// [`on_send`](Self::on_send) sends what's checked.
1442    pub fn many() -> Self {
1443        Self::new(true)
1444    }
1445
1446    fn new(multiple: bool) -> Self {
1447        Self {
1448            parts: ChoicesParts {
1449                multiple,
1450                options: Vec::new(),
1451                on_send: None,
1452            },
1453        }
1454    }
1455
1456    /// An option, with a line saying what it means, and whether it's checked.
1457    pub fn option(
1458        mut self,
1459        label: impl Into<String>,
1460        description: Option<String>,
1461        picked: bool,
1462        on_pick: M,
1463    ) -> Self {
1464        self.parts.options.push(ChoiceParts {
1465            label: label.into(),
1466            description: description.filter(|d| !d.is_empty()),
1467            recommended: false,
1468            picked,
1469            on_pick,
1470        });
1471        self
1472    }
1473
1474    /// The option just added is the one to suggest.
1475    pub fn recommended(mut self) -> Self {
1476        if let Some(option) = self.parts.options.last_mut() {
1477            option.recommended = true;
1478        }
1479        self
1480    }
1481
1482    /// Sent by the button that sends what's checked, for [`many`](Self::many).
1483    pub fn on_send(mut self, message: M) -> Self {
1484        self.parts.on_send = Some(message);
1485        self
1486    }
1487
1488    fn map<N>(self, f: &impl Fn(M) -> N) -> Choices<N> {
1489        let parts = self.parts;
1490        Choices {
1491            parts: ChoicesParts {
1492                multiple: parts.multiple,
1493                options: parts
1494                    .options
1495                    .into_iter()
1496                    .map(|option| ChoiceParts {
1497                        label: option.label,
1498                        description: option.description,
1499                        recommended: option.recommended,
1500                        picked: option.picked,
1501                        on_pick: f(option.on_pick),
1502                    })
1503                    .collect(),
1504                on_send: parts.on_send.map(f),
1505            },
1506        }
1507    }
1508}
1509
1510#[doc(hidden)]
1511#[derive(Clone)]
1512pub struct ChoicesParts<M> {
1513    pub multiple: bool,
1514    pub options: Vec<ChoiceParts<M>>,
1515    pub on_send: Option<M>,
1516}
1517
1518#[doc(hidden)]
1519#[derive(Clone)]
1520pub struct ChoiceParts<M> {
1521    pub label: String,
1522    pub description: Option<String>,
1523    pub recommended: bool,
1524    pub picked: bool,
1525    pub on_pick: M,
1526}
1527
1528#[doc(hidden)]
1529#[derive(Clone)]
1530pub struct StepParts<M> {
1531    /// From 0.
1532    pub index: usize,
1533    pub count: usize,
1534    pub on_back: Option<M>,
1535}
1536
1537/// A field on a [`Notice`] to type an answer into, like a reply to a message: several lines,
1538/// Enter sends and Shift+Enter starts a new line.
1539///
1540/// It shows with the notice, but the host takes the keyboard only once it's clicked, and gives it
1541/// back on Esc, on sending, or on a click elsewhere. The notice doesn't expire while it has the
1542/// keyboard.
1543pub struct Reply<M> {
1544    placeholder: String,
1545    value: String,
1546    on_input: std::rc::Rc<dyn Fn(String) -> M>,
1547    on_submit: Option<M>,
1548    on_cancel: Option<M>,
1549    sent: bool,
1550}
1551
1552impl<M> Reply<M> {
1553    /// Shows `value`, and sends `on_input` with the new text on every change.
1554    pub fn new(
1555        placeholder: impl Into<String>,
1556        value: impl Into<String>,
1557        on_input: impl Fn(String) -> M + 'static,
1558    ) -> Self {
1559        Self {
1560            placeholder: placeholder.into(),
1561            value: value.into(),
1562            on_input: std::rc::Rc::new(on_input),
1563            on_submit: None,
1564            on_cancel: None,
1565            sent: false,
1566        }
1567    }
1568
1569    /// Sent on Enter, or a click on the field's send button.
1570    pub fn on_submit(mut self, message: M) -> Self {
1571        self.on_submit = Some(message);
1572        self
1573    }
1574
1575    /// Sent on Esc.
1576    pub fn on_cancel(mut self, message: M) -> Self {
1577        self.on_cancel = Some(message);
1578        self
1579    }
1580
1581    /// The answer went: the host says so in the field's place. Keep the notice a moment after
1582    /// sending, so it can be read.
1583    pub fn sent(mut self, sent: bool) -> Self {
1584        self.sent = sent;
1585        self
1586    }
1587
1588    pub fn map<N: 'static>(self, f: &(impl Fn(M) -> N + Clone + 'static)) -> Reply<N>
1589    where
1590        M: 'static,
1591    {
1592        let g = f.clone();
1593        let on_input = self.on_input;
1594        Reply {
1595            placeholder: self.placeholder,
1596            value: self.value,
1597            on_input: std::rc::Rc::new(move |text| g(on_input(text))),
1598            on_submit: self.on_submit.map(f),
1599            on_cancel: self.on_cancel.map(f),
1600            sent: self.sent,
1601        }
1602    }
1603
1604    #[doc(hidden)]
1605    pub fn into_parts(self) -> ReplyParts<M> {
1606        ReplyParts {
1607            placeholder: self.placeholder,
1608            value: self.value,
1609            on_input: self.on_input,
1610            on_submit: self.on_submit,
1611            on_cancel: self.on_cancel,
1612            sent: self.sent,
1613        }
1614    }
1615}
1616
1617#[doc(hidden)]
1618#[derive(Clone)]
1619pub struct ReplyParts<M> {
1620    pub placeholder: String,
1621    pub value: String,
1622    pub on_input: std::rc::Rc<dyn Fn(String) -> M>,
1623    pub on_submit: Option<M>,
1624    pub on_cancel: Option<M>,
1625    pub sent: bool,
1626}
1627
1628impl<M> Notice<M> {
1629    pub fn new(key: u64, summary: impl Into<String>) -> Self {
1630        Self {
1631            key,
1632            summary: summary.into(),
1633            app: None,
1634            app_icon: None,
1635            icon: None,
1636            image: None,
1637            pixels: None,
1638            body: None,
1639            markdown: false,
1640            urgency: Urgency::Normal,
1641            time: None,
1642            thread: None,
1643            thread_name: None,
1644            progress: None,
1645            actions: Vec::new(),
1646            choices: None,
1647            step: None,
1648            on_press: None,
1649            on_dismiss: None,
1650            expires: None,
1651            reply: None,
1652        }
1653    }
1654
1655    /// The name of the app it comes from.
1656    pub fn app(mut self, name: impl Into<String>) -> Self {
1657        self.app = Some(name.into());
1658        self
1659    }
1660
1661    /// The app's icon, by app id, like [`app_icon`], or a `file://` URL to a picture of it.
1662    pub fn app_icon(mut self, app_id: impl Into<String>) -> Self {
1663        self.app_icon = Some(app_id.into());
1664        self
1665    }
1666
1667    /// A symbol as its icon, drawn in the text's tone, for what's about the device rather than
1668    /// an app: a battery running low, a network that dropped. It wins over
1669    /// [`app_icon`](Self::app_icon).
1670    pub fn icon(mut self, icon: Icon) -> Self {
1671        self.icon = Some(icon);
1672        self
1673    }
1674
1675    /// A picture of its own, by URL, like a [`cover`]: an avatar, a screenshot. The host shows it
1676    /// beside the text, or under it, full width, if it's wide.
1677    pub fn image(mut self, url: impl Into<String>) -> Self {
1678        self.image = Some(url.into());
1679        self
1680    }
1681
1682    /// A picture of its own, as its pixels: what an app sent as bytes rather than a file. It wins
1683    /// over [`image`](Self::image).
1684    pub fn pixels(mut self, pixels: Pixels) -> Self {
1685        self.pixels = Some(pixels);
1686        self
1687    }
1688
1689    /// When it arrived. The host says how long ago, beside where it's from.
1690    pub fn time(mut self, time: SystemTime) -> Self {
1691        self.time = Some(time);
1692        self
1693    }
1694
1695    /// The conversation it's part of: a chat, a session. The host stacks notices of the same app
1696    /// and thread that come one after another into one, the newest in full.
1697    pub fn thread(mut self, key: impl Into<String>) -> Self {
1698        self.thread = Some(key.into());
1699        self
1700    }
1701
1702    /// What to call its [`thread`](Self::thread), beside the app's name: the chat, the session.
1703    pub fn thread_name(mut self, name: impl Into<String>) -> Self {
1704        self.thread_name = Some(name.into());
1705        self
1706    }
1707
1708    /// How far along what it's about is, from 0.0 to 1.0: a download, a copy.
1709    pub fn progress(mut self, fraction: f32) -> Self {
1710        self.progress = Some(fraction.clamp(0.0, 1.0));
1711        self
1712    }
1713
1714    /// Options to pick from ([`Choices`]), above the reply field and the actions.
1715    pub fn choices(mut self, choices: Choices<M>) -> Self {
1716        self.choices = Some(choices.parts);
1717        self
1718    }
1719
1720    /// It's question `index` (from 0) of `count`, asked one at a time on the same notice. The host
1721    /// shows where it is, and slides the content along when `index` changes. `on_back` goes to
1722    /// the one before.
1723    pub fn step(mut self, index: usize, count: usize, on_back: Option<M>) -> Self {
1724        self.step = Some(StepParts {
1725            index,
1726            count,
1727            on_back,
1728        });
1729        self
1730    }
1731
1732    /// Plain text under the summary. The host wraps it and cuts it off after a few lines.
1733    pub fn body(mut self, text: impl Into<String>) -> Self {
1734        self.body = Some(text.into());
1735        self
1736    }
1737
1738    /// The body is Markdown, like a [`markdown`] node.
1739    pub fn markdown(mut self) -> Self {
1740        self.markdown = true;
1741        self
1742    }
1743
1744    pub fn urgency(mut self, urgency: Urgency) -> Self {
1745        self.urgency = urgency;
1746        self
1747    }
1748
1749    /// A button on the notification that sends `message`. The first one is the main action.
1750    pub fn action(mut self, label: impl Into<String>, message: M) -> Self {
1751        self.actions.push((label.into(), message));
1752        self
1753    }
1754
1755    /// Sent on a click on the notification itself, outside its buttons.
1756    pub fn on_press(mut self, message: M) -> Self {
1757        self.on_press = Some(message);
1758        self
1759    }
1760
1761    /// Sent when its close button is clicked. Without it, it has none.
1762    pub fn on_dismiss(mut self, message: M) -> Self {
1763        self.on_dismiss = Some(message);
1764        self
1765    }
1766
1767    /// Sent once it has been on screen for `after`. The host doesn't count while the pointer is
1768    /// on it or it's waiting to be shown. Without it, it stays
1769    /// until it leaves the list.
1770    pub fn expires(mut self, after: std::time::Duration, message: M) -> Self {
1771        self.expires = Some((after, message));
1772        self
1773    }
1774
1775    /// A field to type an answer into ([`Reply`]).
1776    pub fn reply(mut self, reply: Reply<M>) -> Self {
1777        self.reply = Some(reply);
1778        self
1779    }
1780
1781    pub fn map<N: 'static>(self, f: &(impl Fn(M) -> N + Clone + 'static)) -> Notice<N>
1782    where
1783        M: 'static,
1784    {
1785        Notice {
1786            key: self.key,
1787            summary: self.summary,
1788            app: self.app,
1789            app_icon: self.app_icon,
1790            icon: self.icon,
1791            image: self.image,
1792            pixels: self.pixels,
1793            body: self.body,
1794            markdown: self.markdown,
1795            urgency: self.urgency,
1796            time: self.time,
1797            thread: self.thread,
1798            thread_name: self.thread_name,
1799            progress: self.progress,
1800            actions: self
1801                .actions
1802                .into_iter()
1803                .map(|(label, message)| (label, f(message)))
1804                .collect(),
1805            choices: self.choices.map(|parts| Choices { parts }.map(f).parts),
1806            step: self.step.map(|step| StepParts {
1807                index: step.index,
1808                count: step.count,
1809                on_back: step.on_back.map(f),
1810            }),
1811            on_press: self.on_press.map(f),
1812            on_dismiss: self.on_dismiss.map(f),
1813            expires: self.expires.map(|(after, message)| (after, f(message))),
1814            reply: self.reply.map(|reply| reply.map(f)),
1815        }
1816    }
1817
1818    pub fn key(&self) -> u64 {
1819        self.key
1820    }
1821
1822    #[doc(hidden)]
1823    pub fn into_parts(self) -> NoticeParts<M> {
1824        NoticeParts {
1825            key: self.key,
1826            summary: self.summary,
1827            app: self.app,
1828            app_icon: self.app_icon,
1829            icon: self.icon,
1830            image: self.image,
1831            pixels: self.pixels,
1832            body: self.body,
1833            markdown: self.markdown,
1834            urgency: self.urgency,
1835            time: self.time,
1836            thread: self.thread,
1837            thread_name: self.thread_name,
1838            progress: self.progress,
1839            actions: self.actions,
1840            choices: self.choices,
1841            step: self.step,
1842            on_press: self.on_press,
1843            on_dismiss: self.on_dismiss,
1844            expires: self.expires,
1845            reply: self.reply.map(Reply::into_parts),
1846        }
1847    }
1848}
1849
1850#[doc(hidden)]
1851#[derive(Clone)]
1852pub struct NoticeParts<M> {
1853    pub key: u64,
1854    pub summary: String,
1855    pub app: Option<String>,
1856    pub app_icon: Option<String>,
1857    pub icon: Option<Icon>,
1858    pub image: Option<String>,
1859    pub pixels: Option<Pixels>,
1860    pub body: Option<String>,
1861    pub markdown: bool,
1862    pub urgency: Urgency,
1863    pub time: Option<SystemTime>,
1864    pub thread: Option<String>,
1865    pub thread_name: Option<String>,
1866    pub progress: Option<f32>,
1867    pub actions: Vec<(String, M)>,
1868    pub choices: Option<ChoicesParts<M>>,
1869    pub step: Option<StepParts<M>>,
1870    pub on_press: Option<M>,
1871    pub on_dismiss: Option<M>,
1872    pub expires: Option<(std::time::Duration, M)>,
1873    pub reply: Option<ReplyParts<M>>,
1874}
1875
1876/// Something the widget can do, from [`Widget::actions`](crate::Widget::actions), for the dormer:
1877/// the host's search dialog. Typing finds it by its title, subtitle and keywords, and picking it
1878/// sends `message` to the widget.
1879pub struct Action<M> {
1880    title: String,
1881    subtitle: Option<String>,
1882    keywords: Vec<String>,
1883    leading: Option<Node<M>>,
1884    message: M,
1885}
1886
1887/// An action titled `title` that sends `message` when picked.
1888pub fn action<M>(title: impl Into<String>, message: M) -> Action<M> {
1889    Action {
1890        title: title.into(),
1891        subtitle: None,
1892        keywords: Vec::new(),
1893        leading: None,
1894        message,
1895    }
1896}
1897
1898impl<M> Action<M> {
1899    /// A line under the title: what it acts on, or what kind of thing it is.
1900    pub fn subtitle(mut self, text: impl Into<String>) -> Self {
1901        self.subtitle = Some(text.into());
1902        self
1903    }
1904
1905    /// Other words it should be found by, not shown.
1906    pub fn keywords(mut self, words: impl IntoIterator<Item = impl Into<String>>) -> Self {
1907        self.keywords.extend(words.into_iter().map(Into::into));
1908        self
1909    }
1910
1911    /// A small picture before the title: an [`icon`], [`app_icon`], [`image`] or [`symbol`].
1912    pub fn leading(mut self, node: Node<M>) -> Self {
1913        self.leading = Some(node);
1914        self
1915    }
1916
1917    pub fn map<N: 'static>(self, f: &(impl Fn(M) -> N + Clone + 'static)) -> Action<N>
1918    where
1919        M: 'static,
1920    {
1921        Action {
1922            title: self.title,
1923            subtitle: self.subtitle,
1924            keywords: self.keywords,
1925            leading: self.leading.map(|node| node.map(f)),
1926            message: f(self.message),
1927        }
1928    }
1929
1930    pub fn title(&self) -> &str {
1931        &self.title
1932    }
1933
1934    #[doc(hidden)]
1935    pub fn into_parts(self) -> ActionParts<M> {
1936        ActionParts {
1937            title: self.title,
1938            subtitle: self.subtitle,
1939            keywords: self.keywords,
1940            leading: self.leading,
1941            message: self.message,
1942        }
1943    }
1944}
1945
1946#[doc(hidden)]
1947pub struct ActionParts<M> {
1948    pub title: String,
1949    pub subtitle: Option<String>,
1950    pub keywords: Vec<String>,
1951    pub leading: Option<Node<M>>,
1952    pub message: M,
1953}
1954
1955#[cfg(test)]
1956mod tests {
1957    use super::*;
1958
1959    #[test]
1960    fn map_converts_every_message() {
1961        let node = row([
1962            button(label("a"), 1).on_secondary(4),
1963            column([button(label("b"), 2), label("c")]),
1964            switcher([(label("d"), 3)], Some(0)),
1965        ]);
1966
1967        let Kind::Row(children) = node.map(&|n: i32| n * 10).into_kind() else {
1968            panic!("row became something else");
1969        };
1970        let mut messages = Vec::new();
1971        collect(children, &mut messages);
1972        assert_eq!(messages, [10, 40, 20, 30]);
1973    }
1974
1975    fn collect(nodes: Vec<Node<i32>>, out: &mut Vec<i32>) {
1976        for node in nodes {
1977            match node.into_kind() {
1978                Kind::Button {
1979                    on_press,
1980                    on_secondary,
1981                    ..
1982                } => {
1983                    out.push(on_press);
1984                    out.extend(on_secondary);
1985                }
1986                Kind::Switcher { options, .. } => {
1987                    out.extend(options.into_iter().map(|(_, message)| message))
1988                }
1989                Kind::Row(children) | Kind::Column(children) => collect(children, out),
1990                _ => {}
1991            }
1992        }
1993    }
1994
1995    #[test]
1996    fn hover_shows_the_second_option() {
1997        let node: Node<()> = hover(label("a"), label("b"));
1998        assert!(matches!(
1999            node.into_kind(),
2000            Kind::Alternate {
2001                current: 0,
2002                hovered: Some(1),
2003                ..
2004            }
2005        ));
2006        let node: Node<()> = label("a").on_hover(1);
2007        assert!(matches!(node.into_kind(), Kind::Label { .. }));
2008    }
2009
2010    #[test]
2011    fn tone_fills_only_unset_text() {
2012        let node: Node<()> =
2013            row([label("a").tone(Tone::Accent), label("b"), icon(Icon::Wired)]).tone(Tone::Dim);
2014        let Kind::Row(children) = node.into_kind() else {
2015            unreachable!()
2016        };
2017        let tones: Vec<_> = children
2018            .into_iter()
2019            .map(|child| match child.into_kind() {
2020                Kind::Label { tone, .. } | Kind::Icon { tone, .. } => tone,
2021                _ => None,
2022            })
2023            .collect();
2024        assert_eq!(
2025            tones,
2026            [Some(Tone::Accent), Some(Tone::Dim), Some(Tone::Dim)]
2027        );
2028    }
2029
2030    #[test]
2031    fn map_reaches_new_nodes_and_their_closures() {
2032        let node = column([
2033            header("h").on_back(1).trailing(toggle(true, 2)),
2034            list([list_item("a")
2035                .on_press(3)
2036                .on_secondary(4)
2037                .trailing(button(icon(Icon::Info), 5))]),
2038            slider(0.5, 0.0..=1.0, |v| (v * 10.0) as i32).on_release(6),
2039            text_input("p", "v", |text: String| text.len() as i32).on_submit(7),
2040            scroll(text_area("p", "v", |text: String| text.len() as i32 + 1).on_submit(8)).bottom(),
2041        ]);
2042        let Kind::Column(children) = node.map(&|n: i32| n * 10).into_kind() else {
2043            panic!("column became something else");
2044        };
2045        let mut children = children.into_iter().map(Node::into_kind);
2046        let Some(Kind::Header {
2047            on_back: Some(10),
2048            trailing: Some(trailing),
2049            ..
2050        }) = children.next()
2051        else {
2052            panic!("header lost its back message");
2053        };
2054        assert!(matches!(
2055            trailing.into_kind(),
2056            Kind::Toggle { on_toggle: 20, .. }
2057        ));
2058        let Some(Kind::List(rows)) = children.next() else {
2059            panic!("list lost");
2060        };
2061        let Some(Kind::ListItem {
2062            on_press: Some(30),
2063            on_secondary: Some(40),
2064            trailing: Some(trailing),
2065            ..
2066        }) = rows.into_iter().next().map(Node::into_kind)
2067        else {
2068            panic!("list item lost its messages");
2069        };
2070        assert!(matches!(
2071            trailing.into_kind(),
2072            Kind::Button { on_press: 50, .. }
2073        ));
2074        let Some(Kind::Slider {
2075            on_change,
2076            on_release: Some(60),
2077            ..
2078        }) = children.next()
2079        else {
2080            panic!("slider lost");
2081        };
2082        assert_eq!(on_change(0.3), 30);
2083        let Some(Kind::TextInput {
2084            on_input,
2085            on_submit: Some(70),
2086            ..
2087        }) = children.next()
2088        else {
2089            panic!("text input lost");
2090        };
2091        assert_eq!(on_input("abcd".into()), 40);
2092        let Some(Kind::Scroll {
2093            content,
2094            bottom: true,
2095        }) = children.next()
2096        else {
2097            panic!("scroll lost");
2098        };
2099        let Kind::TextArea {
2100            on_input,
2101            on_submit: Some(80),
2102            ..
2103        } = content.into_kind()
2104        else {
2105            panic!("text area lost");
2106        };
2107        assert_eq!(on_input("abcd".into()), 50);
2108    }
2109
2110    #[test]
2111    fn tabs_map_and_tone_every_page() {
2112        let node = tabs([("a", button(label("x"), 1)), ("b", button(label("y"), 2))])
2113            .tone(Tone::Dim)
2114            .map(&|n: i32| n * 10);
2115        let Kind::Tabs(pages) = node.into_kind() else {
2116            panic!("tabs became something else");
2117        };
2118        let names: Vec<_> = pages.iter().map(|(name, _)| name.as_str()).collect();
2119        assert_eq!(names, ["a", "b"]);
2120        let mut messages = Vec::new();
2121        for (_, page) in pages {
2122            let Kind::Button {
2123                content, on_press, ..
2124            } = page.into_kind()
2125            else {
2126                panic!("page lost its button");
2127            };
2128            assert!(matches!(
2129                content.into_kind(),
2130                Kind::Label {
2131                    tone: Some(Tone::Dim),
2132                    ..
2133                }
2134            ));
2135            messages.push(on_press);
2136        }
2137        assert_eq!(messages, [10, 20]);
2138    }
2139
2140    #[test]
2141    fn notice_map_reaches_every_message() {
2142        let notice = Notice::new(7, "s")
2143            .action("a", 1)
2144            .action("b", 2)
2145            .on_press(3)
2146            .on_dismiss(4)
2147            .expires(std::time::Duration::from_secs(5), 5)
2148            .reply(
2149                Reply::new("p", "v", |text: String| text.len() as i32)
2150                    .on_submit(6)
2151                    .on_cancel(7),
2152            )
2153            .choices(
2154                Choices::many()
2155                    .option("x", None, false, 8)
2156                    .option("y", Some("why".into()), true, 9)
2157                    .recommended()
2158                    .on_send(10),
2159            )
2160            .step(1, 3, Some(11))
2161            .map(&|n: i32| n * 10);
2162        assert_eq!(notice.key(), 7);
2163        let parts = notice.into_parts();
2164        let actions: Vec<_> = parts.actions.into_iter().map(|(_, m)| m).collect();
2165        assert_eq!(actions, [10, 20]);
2166        assert_eq!(parts.on_press, Some(30));
2167        assert_eq!(parts.on_dismiss, Some(40));
2168        assert_eq!(parts.expires.map(|(_, m)| m), Some(50));
2169        let reply = parts.reply.unwrap();
2170        assert_eq!((reply.on_input)("abc".into()), 30);
2171        assert_eq!((reply.on_submit, reply.on_cancel), (Some(60), Some(70)));
2172        let choices = parts.choices.unwrap();
2173        let picks: Vec<_> = choices.options.iter().map(|o| o.on_pick).collect();
2174        assert_eq!(picks, [80, 90]);
2175        assert!(choices.multiple && choices.options[1].recommended && choices.options[1].picked);
2176        assert_eq!(choices.on_send, Some(100));
2177        let step = parts.step.unwrap();
2178        assert_eq!((step.index, step.count, step.on_back), (1, 3, Some(110)));
2179    }
2180
2181    #[test]
2182    fn pixels_must_be_whole() {
2183        assert!(Pixels::new(2, 2, vec![0; 16]).is_some());
2184        assert!(Pixels::new(2, 2, vec![0; 15]).is_none());
2185        assert!(Pixels::new(0, 0, Vec::new()).is_none());
2186        assert!(Pixels::new(u32::MAX, u32::MAX, Vec::new()).is_none());
2187    }
2188
2189    #[test]
2190    fn progress_stays_between_none_and_all() {
2191        let parts = Notice::<()>::new(1, "s").progress(1.5).into_parts();
2192        assert_eq!(parts.progress, Some(1.0));
2193    }
2194
2195    #[test]
2196    fn modifiers_only_touch_their_nodes() {
2197        let node: Node<i32> = label("a").trailing(label("b")).on_press(1).selected(true);
2198        assert!(matches!(node.into_kind(), Kind::Label { .. }));
2199        let node: Node<i32> = button(label("a"), 1).on_press(2).prominent();
2200        assert!(matches!(
2201            node.into_kind(),
2202            Kind::Button {
2203                on_press: 1,
2204                prominent: true,
2205                ..
2206            }
2207        ));
2208    }
2209
2210    #[test]
2211    fn a_header_takes_a_picture_and_a_subtitle() {
2212        let node = header("t")
2213            .subtitle("s")
2214            .leading(button(icon(Icon::Info), 1))
2215            .tone(Tone::Dim)
2216            .map(&|n: i32| n * 10);
2217        let Kind::Header {
2218            subtitle: Some(subtitle),
2219            leading: Some(leading),
2220            ..
2221        } = node.into_kind()
2222        else {
2223            panic!("header lost its picture or subtitle");
2224        };
2225        assert_eq!(subtitle, "s");
2226        let Kind::Button {
2227            content,
2228            on_press: 10,
2229            ..
2230        } = leading.into_kind()
2231        else {
2232            panic!("picture lost its message");
2233        };
2234        assert!(matches!(
2235            content.into_kind(),
2236            Kind::Icon {
2237                tone: Some(Tone::Dim),
2238                ..
2239            }
2240        ));
2241    }
2242
2243    #[test]
2244    fn timelines_map_and_tone_their_steps() {
2245        let node = timeline([
2246            step(Mark::You, label("a")),
2247            step(Mark::Failed, button(literal("b"), 1)).leading(icon(Icon::Check)),
2248            label("c"),
2249        ])
2250        .tone(Tone::Dim)
2251        .map(&|n: i32| n * 10);
2252        let Kind::Timeline(steps) = node.into_kind() else {
2253            panic!("timeline became something else");
2254        };
2255        let mut steps = steps.into_iter().map(Node::into_kind);
2256        let Some(Kind::Step {
2257            mark: Mark::You,
2258            leading: None,
2259            content,
2260        }) = steps.next()
2261        else {
2262            panic!("first step lost");
2263        };
2264        assert!(matches!(
2265            content.into_kind(),
2266            Kind::Label {
2267                tone: Some(Tone::Dim),
2268                ..
2269            }
2270        ));
2271        let Some(Kind::Step {
2272            mark: Mark::Failed,
2273            leading: Some(leading),
2274            content,
2275        }) = steps.next()
2276        else {
2277            panic!("second step lost its picture");
2278        };
2279        assert!(matches!(
2280            leading.into_kind(),
2281            Kind::Icon {
2282                tone: Some(Tone::Dim),
2283                ..
2284            }
2285        ));
2286        let Kind::Button {
2287            content,
2288            on_press: 10,
2289            ..
2290        } = content.into_kind()
2291        else {
2292            panic!("button lost its message");
2293        };
2294        assert!(matches!(
2295            content.into_kind(),
2296            Kind::Literal {
2297                tone: Some(Tone::Dim),
2298                ..
2299            }
2300        ));
2301        assert!(matches!(steps.next(), Some(Kind::Label { .. })));
2302        let node: Node<()> = diff("f", [(Change::Added, "x".to_string())]).leading(label("y"));
2303        assert!(matches!(node.into_kind(), Kind::Diff { lines, .. } if lines.len() == 1));
2304    }
2305
2306    #[test]
2307    fn tone_reaches_details_and_row_icons() {
2308        let node: Node<()> = column([
2309            detail("n", label("v")),
2310            list_item("t").leading(icon(Icon::Lock)),
2311        ])
2312        .tone(Tone::Good);
2313        let Kind::Column(children) = node.into_kind() else {
2314            unreachable!()
2315        };
2316        let mut children = children.into_iter().map(Node::into_kind);
2317        let Some(Kind::Detail { value, .. }) = children.next() else {
2318            unreachable!()
2319        };
2320        assert!(matches!(
2321            value.into_kind(),
2322            Kind::Label {
2323                tone: Some(Tone::Good),
2324                ..
2325            }
2326        ));
2327        let Some(Kind::ListItem {
2328            leading: Some(leading),
2329            ..
2330        }) = children.next()
2331        else {
2332            unreachable!()
2333        };
2334        assert!(matches!(
2335            leading.into_kind(),
2336            Kind::Icon {
2337                tone: Some(Tone::Good),
2338                ..
2339            }
2340        ));
2341    }
2342}