Skip to main content

eave_api/
ui.rs

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