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