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