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