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    image: Option<String>,
1365    pixels: Option<Pixels>,
1366    body: Option<String>,
1367    markdown: bool,
1368    urgency: Urgency,
1369    time: Option<SystemTime>,
1370    thread: Option<String>,
1371    thread_name: Option<String>,
1372    progress: Option<f32>,
1373    actions: Vec<(String, M)>,
1374    choices: Option<ChoicesParts<M>>,
1375    step: Option<StepParts<M>>,
1376    on_press: Option<M>,
1377    on_dismiss: Option<M>,
1378    expires: Option<(std::time::Duration, M)>,
1379    reply: Option<Reply<M>>,
1380}
1381
1382/// A picture as its pixels, for a [`Notice`] whose picture came as bytes rather than a file: RGBA,
1383/// 8 bits a channel, row after row with nothing between them.
1384///
1385/// Cheap to clone. Keep the same `Pixels` for as long as the notice shows: the host draws it once
1386/// and knows it again by its bytes' address.
1387#[derive(Clone)]
1388pub struct Pixels {
1389    width: u32,
1390    height: u32,
1391    rgba: Arc<[u8]>,
1392}
1393
1394impl Pixels {
1395    /// `None` if `rgba` isn't `width × height` pixels of 4 bytes, or the picture is empty.
1396    pub fn new(width: u32, height: u32, rgba: impl Into<Arc<[u8]>>) -> Option<Self> {
1397        let rgba = rgba.into();
1398        let expected = (width as usize)
1399            .checked_mul(height as usize)?
1400            .checked_mul(4)?;
1401        (width > 0 && height > 0 && rgba.len() == expected).then_some(Self {
1402            width,
1403            height,
1404            rgba,
1405        })
1406    }
1407
1408    pub fn width(&self) -> u32 {
1409        self.width
1410    }
1411
1412    pub fn height(&self) -> u32 {
1413        self.height
1414    }
1415
1416    pub fn rgba(&self) -> &Arc<[u8]> {
1417        &self.rgba
1418    }
1419}
1420
1421impl std::fmt::Debug for Pixels {
1422    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1423        write!(f, "Pixels({}×{})", self.width, self.height)
1424    }
1425}
1426
1427/// Options to pick from on a [`Notice`], like the answers to a question: one, which a click sends,
1428/// or several, checked and then sent together.
1429pub struct Choices<M> {
1430    parts: ChoicesParts<M>,
1431}
1432
1433impl<M> Choices<M> {
1434    /// A click on an option sends its message: that's the answer.
1435    pub fn one() -> Self {
1436        Self::new(false)
1437    }
1438
1439    /// A click on an option sends its message, to check or uncheck it, and
1440    /// [`on_send`](Self::on_send) sends what's checked.
1441    pub fn many() -> Self {
1442        Self::new(true)
1443    }
1444
1445    fn new(multiple: bool) -> Self {
1446        Self {
1447            parts: ChoicesParts {
1448                multiple,
1449                options: Vec::new(),
1450                on_send: None,
1451            },
1452        }
1453    }
1454
1455    /// An option, with a line saying what it means, and whether it's checked.
1456    pub fn option(
1457        mut self,
1458        label: impl Into<String>,
1459        description: Option<String>,
1460        picked: bool,
1461        on_pick: M,
1462    ) -> Self {
1463        self.parts.options.push(ChoiceParts {
1464            label: label.into(),
1465            description: description.filter(|d| !d.is_empty()),
1466            recommended: false,
1467            picked,
1468            on_pick,
1469        });
1470        self
1471    }
1472
1473    /// The option just added is the one to suggest.
1474    pub fn recommended(mut self) -> Self {
1475        if let Some(option) = self.parts.options.last_mut() {
1476            option.recommended = true;
1477        }
1478        self
1479    }
1480
1481    /// Sent by the button that sends what's checked, for [`many`](Self::many).
1482    pub fn on_send(mut self, message: M) -> Self {
1483        self.parts.on_send = Some(message);
1484        self
1485    }
1486
1487    fn map<N>(self, f: &impl Fn(M) -> N) -> Choices<N> {
1488        let parts = self.parts;
1489        Choices {
1490            parts: ChoicesParts {
1491                multiple: parts.multiple,
1492                options: parts
1493                    .options
1494                    .into_iter()
1495                    .map(|option| ChoiceParts {
1496                        label: option.label,
1497                        description: option.description,
1498                        recommended: option.recommended,
1499                        picked: option.picked,
1500                        on_pick: f(option.on_pick),
1501                    })
1502                    .collect(),
1503                on_send: parts.on_send.map(f),
1504            },
1505        }
1506    }
1507}
1508
1509#[doc(hidden)]
1510#[derive(Clone)]
1511pub struct ChoicesParts<M> {
1512    pub multiple: bool,
1513    pub options: Vec<ChoiceParts<M>>,
1514    pub on_send: Option<M>,
1515}
1516
1517#[doc(hidden)]
1518#[derive(Clone)]
1519pub struct ChoiceParts<M> {
1520    pub label: String,
1521    pub description: Option<String>,
1522    pub recommended: bool,
1523    pub picked: bool,
1524    pub on_pick: M,
1525}
1526
1527#[doc(hidden)]
1528#[derive(Clone)]
1529pub struct StepParts<M> {
1530    /// From 0.
1531    pub index: usize,
1532    pub count: usize,
1533    pub on_back: Option<M>,
1534}
1535
1536/// A field on a [`Notice`] to type an answer into, like a reply to a message: several lines,
1537/// Enter sends and Shift+Enter starts a new line.
1538///
1539/// It shows with the notice, but the host takes the keyboard only once it's clicked, and gives it
1540/// back on Esc, on sending, or on a click elsewhere. The notice doesn't expire while it has the
1541/// keyboard.
1542pub struct Reply<M> {
1543    placeholder: String,
1544    value: String,
1545    on_input: std::rc::Rc<dyn Fn(String) -> M>,
1546    on_submit: Option<M>,
1547    on_cancel: Option<M>,
1548    sent: bool,
1549}
1550
1551impl<M> Reply<M> {
1552    /// Shows `value`, and sends `on_input` with the new text on every change.
1553    pub fn new(
1554        placeholder: impl Into<String>,
1555        value: impl Into<String>,
1556        on_input: impl Fn(String) -> M + 'static,
1557    ) -> Self {
1558        Self {
1559            placeholder: placeholder.into(),
1560            value: value.into(),
1561            on_input: std::rc::Rc::new(on_input),
1562            on_submit: None,
1563            on_cancel: None,
1564            sent: false,
1565        }
1566    }
1567
1568    /// Sent on Enter, or a click on the field's send button.
1569    pub fn on_submit(mut self, message: M) -> Self {
1570        self.on_submit = Some(message);
1571        self
1572    }
1573
1574    /// Sent on Esc.
1575    pub fn on_cancel(mut self, message: M) -> Self {
1576        self.on_cancel = Some(message);
1577        self
1578    }
1579
1580    /// The answer went: the host says so in the field's place. Keep the notice a moment after
1581    /// sending, so it can be read.
1582    pub fn sent(mut self, sent: bool) -> Self {
1583        self.sent = sent;
1584        self
1585    }
1586
1587    pub fn map<N: 'static>(self, f: &(impl Fn(M) -> N + Clone + 'static)) -> Reply<N>
1588    where
1589        M: 'static,
1590    {
1591        let g = f.clone();
1592        let on_input = self.on_input;
1593        Reply {
1594            placeholder: self.placeholder,
1595            value: self.value,
1596            on_input: std::rc::Rc::new(move |text| g(on_input(text))),
1597            on_submit: self.on_submit.map(f),
1598            on_cancel: self.on_cancel.map(f),
1599            sent: self.sent,
1600        }
1601    }
1602
1603    #[doc(hidden)]
1604    pub fn into_parts(self) -> ReplyParts<M> {
1605        ReplyParts {
1606            placeholder: self.placeholder,
1607            value: self.value,
1608            on_input: self.on_input,
1609            on_submit: self.on_submit,
1610            on_cancel: self.on_cancel,
1611            sent: self.sent,
1612        }
1613    }
1614}
1615
1616#[doc(hidden)]
1617#[derive(Clone)]
1618pub struct ReplyParts<M> {
1619    pub placeholder: String,
1620    pub value: String,
1621    pub on_input: std::rc::Rc<dyn Fn(String) -> M>,
1622    pub on_submit: Option<M>,
1623    pub on_cancel: Option<M>,
1624    pub sent: bool,
1625}
1626
1627impl<M> Notice<M> {
1628    pub fn new(key: u64, summary: impl Into<String>) -> Self {
1629        Self {
1630            key,
1631            summary: summary.into(),
1632            app: None,
1633            app_icon: None,
1634            image: None,
1635            pixels: None,
1636            body: None,
1637            markdown: false,
1638            urgency: Urgency::Normal,
1639            time: None,
1640            thread: None,
1641            thread_name: None,
1642            progress: None,
1643            actions: Vec::new(),
1644            choices: None,
1645            step: None,
1646            on_press: None,
1647            on_dismiss: None,
1648            expires: None,
1649            reply: None,
1650        }
1651    }
1652
1653    /// The name of the app it comes from.
1654    pub fn app(mut self, name: impl Into<String>) -> Self {
1655        self.app = Some(name.into());
1656        self
1657    }
1658
1659    /// The app's icon, by app id, like [`app_icon`], or a `file://` URL to a picture of it.
1660    pub fn app_icon(mut self, app_id: impl Into<String>) -> Self {
1661        self.app_icon = Some(app_id.into());
1662        self
1663    }
1664
1665    /// A picture of its own, by URL, like a [`cover`]: an avatar, a screenshot. The host shows it
1666    /// beside the text, or under it, full width, if it's wide.
1667    pub fn image(mut self, url: impl Into<String>) -> Self {
1668        self.image = Some(url.into());
1669        self
1670    }
1671
1672    /// A picture of its own, as its pixels: what an app sent as bytes rather than a file. It wins
1673    /// over [`image`](Self::image).
1674    pub fn pixels(mut self, pixels: Pixels) -> Self {
1675        self.pixels = Some(pixels);
1676        self
1677    }
1678
1679    /// When it arrived. The host says how long ago, beside where it's from.
1680    pub fn time(mut self, time: SystemTime) -> Self {
1681        self.time = Some(time);
1682        self
1683    }
1684
1685    /// The conversation it's part of: a chat, a session. The host stacks notices of the same app
1686    /// and thread that come one after another into one, the newest in full.
1687    pub fn thread(mut self, key: impl Into<String>) -> Self {
1688        self.thread = Some(key.into());
1689        self
1690    }
1691
1692    /// What to call its [`thread`](Self::thread), beside the app's name: the chat, the session.
1693    pub fn thread_name(mut self, name: impl Into<String>) -> Self {
1694        self.thread_name = Some(name.into());
1695        self
1696    }
1697
1698    /// How far along what it's about is, from 0.0 to 1.0: a download, a copy.
1699    pub fn progress(mut self, fraction: f32) -> Self {
1700        self.progress = Some(fraction.clamp(0.0, 1.0));
1701        self
1702    }
1703
1704    /// Options to pick from ([`Choices`]), above the reply field and the actions.
1705    pub fn choices(mut self, choices: Choices<M>) -> Self {
1706        self.choices = Some(choices.parts);
1707        self
1708    }
1709
1710    /// It's question `index` (from 0) of `count`, asked one at a time on the same notice. The host
1711    /// shows where it is, and slides the content along when `index` changes. `on_back` goes to
1712    /// the one before.
1713    pub fn step(mut self, index: usize, count: usize, on_back: Option<M>) -> Self {
1714        self.step = Some(StepParts {
1715            index,
1716            count,
1717            on_back,
1718        });
1719        self
1720    }
1721
1722    /// Plain text under the summary. The host wraps it and cuts it off after a few lines.
1723    pub fn body(mut self, text: impl Into<String>) -> Self {
1724        self.body = Some(text.into());
1725        self
1726    }
1727
1728    /// The body is Markdown, like a [`markdown`] node.
1729    pub fn markdown(mut self) -> Self {
1730        self.markdown = true;
1731        self
1732    }
1733
1734    pub fn urgency(mut self, urgency: Urgency) -> Self {
1735        self.urgency = urgency;
1736        self
1737    }
1738
1739    /// A button on the notification that sends `message`. The first one is the main action.
1740    pub fn action(mut self, label: impl Into<String>, message: M) -> Self {
1741        self.actions.push((label.into(), message));
1742        self
1743    }
1744
1745    /// Sent on a click on the notification itself, outside its buttons.
1746    pub fn on_press(mut self, message: M) -> Self {
1747        self.on_press = Some(message);
1748        self
1749    }
1750
1751    /// Sent when its close button is clicked. Without it, it has none.
1752    pub fn on_dismiss(mut self, message: M) -> Self {
1753        self.on_dismiss = Some(message);
1754        self
1755    }
1756
1757    /// Sent once it has been on screen for `after`. The host doesn't count while the pointer is
1758    /// on it or it's waiting to be shown. Without it, it stays
1759    /// until it leaves the list.
1760    pub fn expires(mut self, after: std::time::Duration, message: M) -> Self {
1761        self.expires = Some((after, message));
1762        self
1763    }
1764
1765    /// A field to type an answer into ([`Reply`]).
1766    pub fn reply(mut self, reply: Reply<M>) -> Self {
1767        self.reply = Some(reply);
1768        self
1769    }
1770
1771    pub fn map<N: 'static>(self, f: &(impl Fn(M) -> N + Clone + 'static)) -> Notice<N>
1772    where
1773        M: 'static,
1774    {
1775        Notice {
1776            key: self.key,
1777            summary: self.summary,
1778            app: self.app,
1779            app_icon: self.app_icon,
1780            image: self.image,
1781            pixels: self.pixels,
1782            body: self.body,
1783            markdown: self.markdown,
1784            urgency: self.urgency,
1785            time: self.time,
1786            thread: self.thread,
1787            thread_name: self.thread_name,
1788            progress: self.progress,
1789            actions: self
1790                .actions
1791                .into_iter()
1792                .map(|(label, message)| (label, f(message)))
1793                .collect(),
1794            choices: self.choices.map(|parts| Choices { parts }.map(f).parts),
1795            step: self.step.map(|step| StepParts {
1796                index: step.index,
1797                count: step.count,
1798                on_back: step.on_back.map(f),
1799            }),
1800            on_press: self.on_press.map(f),
1801            on_dismiss: self.on_dismiss.map(f),
1802            expires: self.expires.map(|(after, message)| (after, f(message))),
1803            reply: self.reply.map(|reply| reply.map(f)),
1804        }
1805    }
1806
1807    pub fn key(&self) -> u64 {
1808        self.key
1809    }
1810
1811    #[doc(hidden)]
1812    pub fn into_parts(self) -> NoticeParts<M> {
1813        NoticeParts {
1814            key: self.key,
1815            summary: self.summary,
1816            app: self.app,
1817            app_icon: self.app_icon,
1818            image: self.image,
1819            pixels: self.pixels,
1820            body: self.body,
1821            markdown: self.markdown,
1822            urgency: self.urgency,
1823            time: self.time,
1824            thread: self.thread,
1825            thread_name: self.thread_name,
1826            progress: self.progress,
1827            actions: self.actions,
1828            choices: self.choices,
1829            step: self.step,
1830            on_press: self.on_press,
1831            on_dismiss: self.on_dismiss,
1832            expires: self.expires,
1833            reply: self.reply.map(Reply::into_parts),
1834        }
1835    }
1836}
1837
1838#[doc(hidden)]
1839#[derive(Clone)]
1840pub struct NoticeParts<M> {
1841    pub key: u64,
1842    pub summary: String,
1843    pub app: Option<String>,
1844    pub app_icon: Option<String>,
1845    pub image: Option<String>,
1846    pub pixels: Option<Pixels>,
1847    pub body: Option<String>,
1848    pub markdown: bool,
1849    pub urgency: Urgency,
1850    pub time: Option<SystemTime>,
1851    pub thread: Option<String>,
1852    pub thread_name: Option<String>,
1853    pub progress: Option<f32>,
1854    pub actions: Vec<(String, M)>,
1855    pub choices: Option<ChoicesParts<M>>,
1856    pub step: Option<StepParts<M>>,
1857    pub on_press: Option<M>,
1858    pub on_dismiss: Option<M>,
1859    pub expires: Option<(std::time::Duration, M)>,
1860    pub reply: Option<ReplyParts<M>>,
1861}
1862
1863/// Something the widget can do, from [`Widget::actions`](crate::Widget::actions), for the dormer:
1864/// the host's search dialog. Typing finds it by its title, subtitle and keywords, and picking it
1865/// sends `message` to the widget.
1866pub struct Action<M> {
1867    title: String,
1868    subtitle: Option<String>,
1869    keywords: Vec<String>,
1870    leading: Option<Node<M>>,
1871    message: M,
1872}
1873
1874/// An action titled `title` that sends `message` when picked.
1875pub fn action<M>(title: impl Into<String>, message: M) -> Action<M> {
1876    Action {
1877        title: title.into(),
1878        subtitle: None,
1879        keywords: Vec::new(),
1880        leading: None,
1881        message,
1882    }
1883}
1884
1885impl<M> Action<M> {
1886    /// A line under the title: what it acts on, or what kind of thing it is.
1887    pub fn subtitle(mut self, text: impl Into<String>) -> Self {
1888        self.subtitle = Some(text.into());
1889        self
1890    }
1891
1892    /// Other words it should be found by, not shown.
1893    pub fn keywords(mut self, words: impl IntoIterator<Item = impl Into<String>>) -> Self {
1894        self.keywords.extend(words.into_iter().map(Into::into));
1895        self
1896    }
1897
1898    /// A small picture before the title: an [`icon`], [`app_icon`], [`image`] or [`symbol`].
1899    pub fn leading(mut self, node: Node<M>) -> Self {
1900        self.leading = Some(node);
1901        self
1902    }
1903
1904    pub fn map<N: 'static>(self, f: &(impl Fn(M) -> N + Clone + 'static)) -> Action<N>
1905    where
1906        M: 'static,
1907    {
1908        Action {
1909            title: self.title,
1910            subtitle: self.subtitle,
1911            keywords: self.keywords,
1912            leading: self.leading.map(|node| node.map(f)),
1913            message: f(self.message),
1914        }
1915    }
1916
1917    pub fn title(&self) -> &str {
1918        &self.title
1919    }
1920
1921    #[doc(hidden)]
1922    pub fn into_parts(self) -> ActionParts<M> {
1923        ActionParts {
1924            title: self.title,
1925            subtitle: self.subtitle,
1926            keywords: self.keywords,
1927            leading: self.leading,
1928            message: self.message,
1929        }
1930    }
1931}
1932
1933#[doc(hidden)]
1934pub struct ActionParts<M> {
1935    pub title: String,
1936    pub subtitle: Option<String>,
1937    pub keywords: Vec<String>,
1938    pub leading: Option<Node<M>>,
1939    pub message: M,
1940}
1941
1942#[cfg(test)]
1943mod tests {
1944    use super::*;
1945
1946    #[test]
1947    fn map_converts_every_message() {
1948        let node = row([
1949            button(label("a"), 1).on_secondary(4),
1950            column([button(label("b"), 2), label("c")]),
1951            switcher([(label("d"), 3)], Some(0)),
1952        ]);
1953
1954        let Kind::Row(children) = node.map(&|n: i32| n * 10).into_kind() else {
1955            panic!("row became something else");
1956        };
1957        let mut messages = Vec::new();
1958        collect(children, &mut messages);
1959        assert_eq!(messages, [10, 40, 20, 30]);
1960    }
1961
1962    fn collect(nodes: Vec<Node<i32>>, out: &mut Vec<i32>) {
1963        for node in nodes {
1964            match node.into_kind() {
1965                Kind::Button {
1966                    on_press,
1967                    on_secondary,
1968                    ..
1969                } => {
1970                    out.push(on_press);
1971                    out.extend(on_secondary);
1972                }
1973                Kind::Switcher { options, .. } => {
1974                    out.extend(options.into_iter().map(|(_, message)| message))
1975                }
1976                Kind::Row(children) | Kind::Column(children) => collect(children, out),
1977                _ => {}
1978            }
1979        }
1980    }
1981
1982    #[test]
1983    fn hover_shows_the_second_option() {
1984        let node: Node<()> = hover(label("a"), label("b"));
1985        assert!(matches!(
1986            node.into_kind(),
1987            Kind::Alternate {
1988                current: 0,
1989                hovered: Some(1),
1990                ..
1991            }
1992        ));
1993        let node: Node<()> = label("a").on_hover(1);
1994        assert!(matches!(node.into_kind(), Kind::Label { .. }));
1995    }
1996
1997    #[test]
1998    fn tone_fills_only_unset_text() {
1999        let node: Node<()> =
2000            row([label("a").tone(Tone::Accent), label("b"), icon(Icon::Wired)]).tone(Tone::Dim);
2001        let Kind::Row(children) = node.into_kind() else {
2002            unreachable!()
2003        };
2004        let tones: Vec<_> = children
2005            .into_iter()
2006            .map(|child| match child.into_kind() {
2007                Kind::Label { tone, .. } | Kind::Icon { tone, .. } => tone,
2008                _ => None,
2009            })
2010            .collect();
2011        assert_eq!(
2012            tones,
2013            [Some(Tone::Accent), Some(Tone::Dim), Some(Tone::Dim)]
2014        );
2015    }
2016
2017    #[test]
2018    fn map_reaches_new_nodes_and_their_closures() {
2019        let node = column([
2020            header("h").on_back(1).trailing(toggle(true, 2)),
2021            list([list_item("a")
2022                .on_press(3)
2023                .on_secondary(4)
2024                .trailing(button(icon(Icon::Info), 5))]),
2025            slider(0.5, 0.0..=1.0, |v| (v * 10.0) as i32).on_release(6),
2026            text_input("p", "v", |text: String| text.len() as i32).on_submit(7),
2027            scroll(text_area("p", "v", |text: String| text.len() as i32 + 1).on_submit(8)).bottom(),
2028        ]);
2029        let Kind::Column(children) = node.map(&|n: i32| n * 10).into_kind() else {
2030            panic!("column became something else");
2031        };
2032        let mut children = children.into_iter().map(Node::into_kind);
2033        let Some(Kind::Header {
2034            on_back: Some(10),
2035            trailing: Some(trailing),
2036            ..
2037        }) = children.next()
2038        else {
2039            panic!("header lost its back message");
2040        };
2041        assert!(matches!(
2042            trailing.into_kind(),
2043            Kind::Toggle { on_toggle: 20, .. }
2044        ));
2045        let Some(Kind::List(rows)) = children.next() else {
2046            panic!("list lost");
2047        };
2048        let Some(Kind::ListItem {
2049            on_press: Some(30),
2050            on_secondary: Some(40),
2051            trailing: Some(trailing),
2052            ..
2053        }) = rows.into_iter().next().map(Node::into_kind)
2054        else {
2055            panic!("list item lost its messages");
2056        };
2057        assert!(matches!(
2058            trailing.into_kind(),
2059            Kind::Button { on_press: 50, .. }
2060        ));
2061        let Some(Kind::Slider {
2062            on_change,
2063            on_release: Some(60),
2064            ..
2065        }) = children.next()
2066        else {
2067            panic!("slider lost");
2068        };
2069        assert_eq!(on_change(0.3), 30);
2070        let Some(Kind::TextInput {
2071            on_input,
2072            on_submit: Some(70),
2073            ..
2074        }) = children.next()
2075        else {
2076            panic!("text input lost");
2077        };
2078        assert_eq!(on_input("abcd".into()), 40);
2079        let Some(Kind::Scroll {
2080            content,
2081            bottom: true,
2082        }) = children.next()
2083        else {
2084            panic!("scroll lost");
2085        };
2086        let Kind::TextArea {
2087            on_input,
2088            on_submit: Some(80),
2089            ..
2090        } = content.into_kind()
2091        else {
2092            panic!("text area lost");
2093        };
2094        assert_eq!(on_input("abcd".into()), 50);
2095    }
2096
2097    #[test]
2098    fn tabs_map_and_tone_every_page() {
2099        let node = tabs([("a", button(label("x"), 1)), ("b", button(label("y"), 2))])
2100            .tone(Tone::Dim)
2101            .map(&|n: i32| n * 10);
2102        let Kind::Tabs(pages) = node.into_kind() else {
2103            panic!("tabs became something else");
2104        };
2105        let names: Vec<_> = pages.iter().map(|(name, _)| name.as_str()).collect();
2106        assert_eq!(names, ["a", "b"]);
2107        let mut messages = Vec::new();
2108        for (_, page) in pages {
2109            let Kind::Button {
2110                content, on_press, ..
2111            } = page.into_kind()
2112            else {
2113                panic!("page lost its button");
2114            };
2115            assert!(matches!(
2116                content.into_kind(),
2117                Kind::Label {
2118                    tone: Some(Tone::Dim),
2119                    ..
2120                }
2121            ));
2122            messages.push(on_press);
2123        }
2124        assert_eq!(messages, [10, 20]);
2125    }
2126
2127    #[test]
2128    fn notice_map_reaches_every_message() {
2129        let notice = Notice::new(7, "s")
2130            .action("a", 1)
2131            .action("b", 2)
2132            .on_press(3)
2133            .on_dismiss(4)
2134            .expires(std::time::Duration::from_secs(5), 5)
2135            .reply(
2136                Reply::new("p", "v", |text: String| text.len() as i32)
2137                    .on_submit(6)
2138                    .on_cancel(7),
2139            )
2140            .choices(
2141                Choices::many()
2142                    .option("x", None, false, 8)
2143                    .option("y", Some("why".into()), true, 9)
2144                    .recommended()
2145                    .on_send(10),
2146            )
2147            .step(1, 3, Some(11))
2148            .map(&|n: i32| n * 10);
2149        assert_eq!(notice.key(), 7);
2150        let parts = notice.into_parts();
2151        let actions: Vec<_> = parts.actions.into_iter().map(|(_, m)| m).collect();
2152        assert_eq!(actions, [10, 20]);
2153        assert_eq!(parts.on_press, Some(30));
2154        assert_eq!(parts.on_dismiss, Some(40));
2155        assert_eq!(parts.expires.map(|(_, m)| m), Some(50));
2156        let reply = parts.reply.unwrap();
2157        assert_eq!((reply.on_input)("abc".into()), 30);
2158        assert_eq!((reply.on_submit, reply.on_cancel), (Some(60), Some(70)));
2159        let choices = parts.choices.unwrap();
2160        let picks: Vec<_> = choices.options.iter().map(|o| o.on_pick).collect();
2161        assert_eq!(picks, [80, 90]);
2162        assert!(choices.multiple && choices.options[1].recommended && choices.options[1].picked);
2163        assert_eq!(choices.on_send, Some(100));
2164        let step = parts.step.unwrap();
2165        assert_eq!((step.index, step.count, step.on_back), (1, 3, Some(110)));
2166    }
2167
2168    #[test]
2169    fn pixels_must_be_whole() {
2170        assert!(Pixels::new(2, 2, vec![0; 16]).is_some());
2171        assert!(Pixels::new(2, 2, vec![0; 15]).is_none());
2172        assert!(Pixels::new(0, 0, Vec::new()).is_none());
2173        assert!(Pixels::new(u32::MAX, u32::MAX, Vec::new()).is_none());
2174    }
2175
2176    #[test]
2177    fn progress_stays_between_none_and_all() {
2178        let parts = Notice::<()>::new(1, "s").progress(1.5).into_parts();
2179        assert_eq!(parts.progress, Some(1.0));
2180    }
2181
2182    #[test]
2183    fn modifiers_only_touch_their_nodes() {
2184        let node: Node<i32> = label("a").trailing(label("b")).on_press(1).selected(true);
2185        assert!(matches!(node.into_kind(), Kind::Label { .. }));
2186        let node: Node<i32> = button(label("a"), 1).on_press(2).prominent();
2187        assert!(matches!(
2188            node.into_kind(),
2189            Kind::Button {
2190                on_press: 1,
2191                prominent: true,
2192                ..
2193            }
2194        ));
2195    }
2196
2197    #[test]
2198    fn a_header_takes_a_picture_and_a_subtitle() {
2199        let node = header("t")
2200            .subtitle("s")
2201            .leading(button(icon(Icon::Info), 1))
2202            .tone(Tone::Dim)
2203            .map(&|n: i32| n * 10);
2204        let Kind::Header {
2205            subtitle: Some(subtitle),
2206            leading: Some(leading),
2207            ..
2208        } = node.into_kind()
2209        else {
2210            panic!("header lost its picture or subtitle");
2211        };
2212        assert_eq!(subtitle, "s");
2213        let Kind::Button {
2214            content,
2215            on_press: 10,
2216            ..
2217        } = leading.into_kind()
2218        else {
2219            panic!("picture lost its message");
2220        };
2221        assert!(matches!(
2222            content.into_kind(),
2223            Kind::Icon {
2224                tone: Some(Tone::Dim),
2225                ..
2226            }
2227        ));
2228    }
2229
2230    #[test]
2231    fn timelines_map_and_tone_their_steps() {
2232        let node = timeline([
2233            step(Mark::You, label("a")),
2234            step(Mark::Failed, button(literal("b"), 1)).leading(icon(Icon::Check)),
2235            label("c"),
2236        ])
2237        .tone(Tone::Dim)
2238        .map(&|n: i32| n * 10);
2239        let Kind::Timeline(steps) = node.into_kind() else {
2240            panic!("timeline became something else");
2241        };
2242        let mut steps = steps.into_iter().map(Node::into_kind);
2243        let Some(Kind::Step {
2244            mark: Mark::You,
2245            leading: None,
2246            content,
2247        }) = steps.next()
2248        else {
2249            panic!("first step lost");
2250        };
2251        assert!(matches!(
2252            content.into_kind(),
2253            Kind::Label {
2254                tone: Some(Tone::Dim),
2255                ..
2256            }
2257        ));
2258        let Some(Kind::Step {
2259            mark: Mark::Failed,
2260            leading: Some(leading),
2261            content,
2262        }) = steps.next()
2263        else {
2264            panic!("second step lost its picture");
2265        };
2266        assert!(matches!(
2267            leading.into_kind(),
2268            Kind::Icon {
2269                tone: Some(Tone::Dim),
2270                ..
2271            }
2272        ));
2273        let Kind::Button {
2274            content,
2275            on_press: 10,
2276            ..
2277        } = content.into_kind()
2278        else {
2279            panic!("button lost its message");
2280        };
2281        assert!(matches!(
2282            content.into_kind(),
2283            Kind::Literal {
2284                tone: Some(Tone::Dim),
2285                ..
2286            }
2287        ));
2288        assert!(matches!(steps.next(), Some(Kind::Label { .. })));
2289        let node: Node<()> = diff("f", [(Change::Added, "x".to_string())]).leading(label("y"));
2290        assert!(matches!(node.into_kind(), Kind::Diff { lines, .. } if lines.len() == 1));
2291    }
2292
2293    #[test]
2294    fn tone_reaches_details_and_row_icons() {
2295        let node: Node<()> = column([
2296            detail("n", label("v")),
2297            list_item("t").leading(icon(Icon::Lock)),
2298        ])
2299        .tone(Tone::Good);
2300        let Kind::Column(children) = node.into_kind() else {
2301            unreachable!()
2302        };
2303        let mut children = children.into_iter().map(Node::into_kind);
2304        let Some(Kind::Detail { value, .. }) = children.next() else {
2305            unreachable!()
2306        };
2307        assert!(matches!(
2308            value.into_kind(),
2309            Kind::Label {
2310                tone: Some(Tone::Good),
2311                ..
2312            }
2313        ));
2314        let Some(Kind::ListItem {
2315            leading: Some(leading),
2316            ..
2317        }) = children.next()
2318        else {
2319            unreachable!()
2320        };
2321        assert!(matches!(
2322            leading.into_kind(),
2323            Kind::Icon {
2324                tone: Some(Tone::Good),
2325                ..
2326            }
2327        ));
2328    }
2329}