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;
8
9/// The role of a piece of text. The theme decides the color.
10#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
11pub enum Tone {
12    #[default]
13    Normal,
14    /// Secondary information.
15    Dim,
16    /// The thing to notice: today in a calendar, the active workspace.
17    Accent,
18    Warning,
19    Critical,
20    /// Healthy, or gaining: a charging battery.
21    Good,
22}
23
24/// A status symbol. It names what to show and how full it is; the host draws it in the text's
25/// tone, at the text's size. Fractions are 0.0 to 1.0.
26#[derive(Debug, Clone, Copy, PartialEq)]
27pub enum Icon {
28    /// A speaker with sound waves for the level. Above 1.0 is amplified.
29    Volume { level: f32, muted: bool },
30    /// A Wi-Fi fan with as many bars as the signal. `None` when there's no link.
31    Wifi { strength: Option<f32> },
32    /// A wired connection.
33    Wired,
34    /// A battery filled to its charge.
35    Battery { charge: f32, charging: bool },
36    /// A padlock: a secured network.
37    Lock,
38    /// An `i` in a circle: more about this.
39    Info,
40    /// A chevron pointing left: back, or earlier.
41    Back,
42    /// A chevron pointing right: on, or later.
43    Forward,
44    /// A check mark: the one in use.
45    Check,
46    /// A circling arrow: look again.
47    Refresh,
48    /// A cross: close this.
49    Close,
50    /// An eye: show what's hidden.
51    Reveal,
52    /// A lightning bolt: charging.
53    Bolt,
54    /// A power plug: on mains power.
55    Plug,
56    /// A microphone, struck through when muted.
57    Mic { muted: bool },
58    /// A loudspeaker box: built-in speakers.
59    Speaker,
60    /// Headphones, or a headset.
61    Headphones,
62    /// A screen: a monitor or TV that plays sound.
63    Display,
64    /// A bell: notifications, struck through when they're silenced.
65    Bell { muted: bool },
66    /// Three level bars, like an equalizer: media. Uneven while `playing`, low and level when
67    /// not.
68    Media { playing: bool },
69    /// A triangle pointing right: play.
70    Play,
71    /// Two upright bars: pause.
72    Pause,
73    /// A triangle pointing left against a bar: back to the previous track.
74    Previous,
75    /// A triangle pointing right against a bar: on to the next track.
76    Next,
77}
78
79/// Where a [`step`] of a [`timeline`] stands. The host draws it as the step's mark on the line.
80#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
81pub enum Mark {
82    /// Something said or noted along the way.
83    #[default]
84    Note,
85    /// What you did or said: where a stretch of steps starts.
86    You,
87    /// Under way.
88    Running,
89    /// Finished.
90    Done,
91    /// Finished, and it went wrong.
92    Failed,
93    /// Held up until you answer.
94    Waiting,
95}
96
97/// What a line of a [`diff`] is.
98#[derive(Debug, Clone, Copy, PartialEq, Eq)]
99pub enum Change {
100    /// Unchanged, shown around the changes.
101    Context,
102    Added,
103    Removed,
104    /// Where the next stretch of changes is, like `@@ -88,7 +88,9 @@`.
105    Hunk,
106}
107
108/// A piece of UI. Build it with the functions in this module.
109pub struct Node<M>(Kind<M>);
110
111#[doc(hidden)]
112pub enum Kind<M> {
113    Label {
114        text: String,
115        tone: Option<Tone>,
116    },
117    Heading {
118        text: String,
119        tone: Option<Tone>,
120    },
121    Icon {
122        icon: Icon,
123        tone: Option<Tone>,
124    },
125    AppIcon {
126        app_id: String,
127        tone: Option<Tone>,
128    },
129    Image {
130        file: &'static [u8],
131        tint: bool,
132        tone: Option<Tone>,
133    },
134    Sprite {
135        frames: &'static [&'static [u8]],
136        /// Indices into `frames`, in the order they play.
137        order: &'static [u8],
138        /// Plays for as long as it shows, not only when it shows up or changes.
139        looping: bool,
140        tone: Option<Tone>,
141    },
142    Cover {
143        url: String,
144        /// Shown until the picture is loaded, and instead of it if it can't be.
145        fallback: Box<Node<M>>,
146        /// The main picture of a popover, not an icon.
147        large: bool,
148        tone: Option<Tone>,
149    },
150    Counter {
151        value: i64,
152        range: RangeInclusive<i64>,
153        unit: String,
154        tone: Option<Tone>,
155    },
156    Alternate {
157        options: Vec<Node<M>>,
158        current: usize,
159        /// Shown instead of `current` while the pointer is over the item.
160        hovered: Option<usize>,
161        /// As wide as the option on screen, hovered by its own width, after a rest ([`expand`]).
162        grows: bool,
163    },
164    Row(Vec<Node<M>>),
165    Column(Vec<Node<M>>),
166    Grid {
167        columns: usize,
168        cells: Vec<Node<M>>,
169    },
170    Button {
171        content: Box<Node<M>>,
172        on_press: M,
173        on_secondary: Option<M>,
174        /// The main action of a popover page: filled, so it stands out.
175        prominent: bool,
176    },
177    Switcher {
178        options: Vec<(Node<M>, M)>,
179        current: Option<usize>,
180    },
181    Gulp(Box<Node<M>>),
182    /// Centered across the panel's width.
183    Center(Box<Node<M>>),
184    Separator,
185    Empty,
186    ListItem {
187        title: String,
188        subtitle: Option<String>,
189        leading: Option<Box<Node<M>>>,
190        trailing: Option<Box<Node<M>>>,
191        selected: bool,
192        on_press: Option<M>,
193        on_secondary: Option<M>,
194    },
195    List(Vec<Node<M>>),
196    Section {
197        title: String,
198        trailing: Option<Box<Node<M>>>,
199    },
200    Header {
201        title: String,
202        subtitle: Option<String>,
203        on_back: Option<M>,
204        leading: Option<Box<Node<M>>>,
205        trailing: Option<Box<Node<M>>>,
206    },
207    Detail {
208        name: String,
209        value: Box<Node<M>>,
210    },
211    Fill,
212    Slider {
213        value: f32,
214        range: RangeInclusive<f32>,
215        step: f32,
216        on_change: Box<dyn Fn(f32) -> M>,
217        on_release: Option<M>,
218    },
219    Toggle {
220        on: bool,
221        on_toggle: M,
222    },
223    Meter {
224        fraction: f32,
225        /// Where a limit sits along it, like a battery's charge limit.
226        limit: Option<f32>,
227        tone: Option<Tone>,
228    },
229    TextInput {
230        placeholder: String,
231        value: String,
232        on_input: Box<dyn Fn(String) -> M>,
233        on_submit: Option<M>,
234        secret: bool,
235        trailing: Option<Box<Node<M>>>,
236        tone: Option<Tone>,
237    },
238    Display(Box<Node<M>>),
239    Mark(Box<Node<M>>),
240    Badge {
241        text: String,
242        tone: Option<Tone>,
243    },
244    /// Pages and their names. The host shows one at a time and owns which.
245    Tabs(Vec<(String, Node<M>)>),
246    Markdown {
247        text: String,
248        tone: Option<Tone>,
249    },
250    TextArea {
251        placeholder: String,
252        value: String,
253        on_input: Box<dyn Fn(String) -> M>,
254        on_submit: Option<M>,
255        trailing: Option<Box<Node<M>>>,
256    },
257    Scroll {
258        content: Box<Node<M>>,
259        /// Keeps the end in view as content arrives there, like a chat.
260        bottom: bool,
261    },
262    Literal {
263        text: String,
264        tone: Option<Tone>,
265    },
266    Divider {
267        text: String,
268    },
269    Timeline(Vec<Node<M>>),
270    Step {
271        mark: Mark,
272        /// Drawn on the line in place of the mark.
273        leading: Option<Box<Node<M>>>,
274        content: Box<Node<M>>,
275    },
276    Diff {
277        title: String,
278        lines: Vec<(Change, String)>,
279    },
280}
281
282pub fn label<M>(text: impl Into<String>) -> Node<M> {
283    Node(Kind::Label {
284        text: text.into(),
285        tone: None,
286    })
287}
288
289/// A title, larger than a label. Meant for popovers.
290pub fn heading<M>(text: impl Into<String>) -> Node<M> {
291    Node(Kind::Heading {
292        text: text.into(),
293        tone: None,
294    })
295}
296
297/// A symbol, sized like a label.
298pub fn icon<M>(icon: Icon) -> Node<M> {
299    Node(Kind::Icon { icon, tone: None })
300}
301
302/// An application's icon, found by its app id or window class (`firefox`, `org.gnome.Nautilus`)
303/// in the user's icon theme. Sized like an [`icon`], in the app's own colors: a tone only dims it.
304/// Until the host has found it, or if there is none, it shows the id's first letter.
305pub fn app_icon<M>(app_id: impl Into<String>) -> Node<M> {
306    Node(Kind::AppIcon {
307        app_id: app_id.into(),
308        tone: None,
309    })
310}
311
312/// An icon the plugin ships: an SVG or PNG file compiled into it with `include_bytes!`, told
313/// apart by its first bytes. Sized like an [`icon`], in the file's own colors: a tone only dims
314/// it, as with [`app_icon`]. The host draws it once, off the UI thread, and shows nothing in its
315/// place until then.
316pub fn image<M>(file: &'static [u8]) -> Node<M> {
317    Node(Kind::Image {
318        file,
319        tint: false,
320        tone: None,
321    })
322}
323
324/// Like [`image`], but only the file's shape counts: it's drawn in the text's tone, like an
325/// [`icon`]. For a monochrome symbol the vocabulary doesn't have.
326pub fn symbol<M>(file: &'static [u8]) -> Node<M> {
327    Node(Kind::Image {
328        file,
329        tint: true,
330        tone: None,
331    })
332}
333
334/// A little animation the plugin ships, like a pixel-art mascot: `frames` are PNG or SVG files
335/// compiled in with `include_bytes!`, all the same size, and `order` lists which frame shows when,
336/// by index into `frames`. The host keeps the frames' shape and picks the height (a little under
337/// a label's in the bar, taller in a popover), scaling pixels up whole, never blurred: ship pixel
338/// art at its own size. The host shows a frame for a quarter of a second. It plays the loop a
339/// couple of times when it shows up or changes, then rests on the first frame of `order`;
340/// [`Node::looping`] keeps it going. A tone only dims it.
341pub fn sprite<M>(frames: &'static [&'static [u8]], order: &'static [u8]) -> Node<M> {
342    Node(Kind::Sprite {
343        frames,
344        order,
345        looping: false,
346        tone: None,
347    })
348}
349
350/// A picture a program hands over at runtime, by URL: a track's cover art. `file://` and
351/// `https://` URLs load; the host fetches, crops it square and rounds it, off the UI thread, and
352/// keeps the few latest. Drawn at icon size, or big with [`Node::large`]. Until it's there, or if
353/// it can't be loaded, `fallback` shows in its place. A tone only dims it.
354pub fn cover<M>(url: impl Into<String>, fallback: Node<M>) -> Node<M> {
355    Node(Kind::Cover {
356        url: url.into(),
357        fallback: Box::new(fallback),
358        large: false,
359        tone: None,
360    })
361}
362
363/// A number from `range`, with a unit after it (`"%"`, `"°"`, or `""`), shown like a label. It
364/// keeps room for the widest number in `range`, so it doesn't change width as it changes. When
365/// `value` changes, the host counts to it through every number in between.
366pub fn counter<M>(value: i64, range: RangeInclusive<i64>, unit: impl Into<String>) -> Node<M> {
367    Node(Kind::Counter {
368        value,
369        range,
370        unit: unit.into(),
371        tone: None,
372    })
373}
374
375/// One of `options` at a time: the one at `current`, or the last one if it's past the end. It's as
376/// big as the biggest option, so it doesn't change size when `current` does. The host fades from
377/// one to the other. Keep the options the same from one
378/// view to the next, so the host can tell which one is coming in.
379pub fn alternate<M>(options: impl IntoIterator<Item = Node<M>>, current: usize) -> Node<M> {
380    Node(Kind::Alternate {
381        options: options.into_iter().collect(),
382        current,
383        hovered: None,
384        grows: false,
385    })
386}
387
388/// `normal`, or `hovered` while the pointer is over the item, fading between them like an
389/// [`alternate`]: an icon that shows its percent, a time that shows the date.
390pub fn hover<M>(normal: Node<M>, hovered: Node<M>) -> Node<M> {
391    alternate([normal, hovered], 0).on_hover(1)
392}
393
394/// `collapsed`, or `expanded` once the pointer rests on this node, fading between them like a
395/// [`hover`]. Unlike `hover`, it's only as big as what it shows, so the item grows to make room
396/// and the island springs along, and it reacts to the pointer over it, not the whole item: a
397/// workspace that shows every window when you point at it.
398pub fn expand<M>(collapsed: Node<M>, expanded: Node<M>) -> Node<M> {
399    Node(Kind::Alternate {
400        options: vec![collapsed, expanded],
401        current: 0,
402        hovered: Some(1),
403        grows: true,
404    })
405}
406
407/// Children side by side, vertically centered.
408pub fn row<M>(children: impl IntoIterator<Item = Node<M>>) -> Node<M> {
409    Node(Kind::Row(children.into_iter().collect()))
410}
411
412/// Children stacked top to bottom.
413pub fn column<M>(children: impl IntoIterator<Item = Node<M>>) -> Node<M> {
414    Node(Kind::Column(children.into_iter().collect()))
415}
416
417/// `content` centered across the panel's width: a cover, a title, a row of controls. Text in it
418/// stays on one line, cut and faded at the edges of its room if it's too long. In the bar, it's
419/// just `content`.
420pub fn center<M>(content: Node<M>) -> Node<M> {
421    Node(Kind::Center(Box::new(content)))
422}
423
424/// Cells laid out left to right in rows of `columns`, all the same width. A calendar, a keypad.
425pub fn grid<M>(columns: usize, cells: impl IntoIterator<Item = Node<M>>) -> Node<M> {
426    Node(Kind::Grid {
427        columns: columns.max(1),
428        cells: cells.into_iter().collect(),
429    })
430}
431
432pub fn button<M>(content: Node<M>, on_press: M) -> Node<M> {
433    Node(Kind::Button {
434        content: Box::new(content),
435        on_press,
436        on_secondary: None,
437        prominent: false,
438    })
439}
440
441/// A row of choices, one of them current: workspaces, output devices. Each option sends its
442/// message when clicked. The host marks the current option and moves the mark smoothly when
443/// `current` changes.
444pub fn switcher<M>(
445    options: impl IntoIterator<Item = (Node<M>, M)>,
446    current: Option<usize>,
447) -> Node<M> {
448    Node(Kind::Switcher {
449        options: options.into_iter().collect(),
450        current,
451    })
452}
453
454/// Keeps gulping, like a bottle being poured: the host swells `content` gently and steadily for as
455/// long as it's on screen. For something filling up, like a charging battery's percent.
456pub fn gulp<M>(content: Node<M>) -> Node<M> {
457    Node(Kind::Gulp(Box::new(content)))
458}
459
460/// A thin horizontal line between groups.
461pub fn separator<M>() -> Node<M> {
462    Node(Kind::Separator)
463}
464
465/// Takes up a grid cell, or nothing elsewhere.
466pub fn empty<M>() -> Node<M> {
467    Node(Kind::Empty)
468}
469
470/// A row in a popover list: a network, a device, a window. Give it a [`subtitle`](Node::subtitle),
471/// a [`leading`](Node::leading) icon, a [`trailing`](Node::trailing) node at the far end (a lock,
472/// an info button), mark the one in use with [`selected`](Node::selected), and make it clickable
473/// with [`on_press`](Node::on_press). A long title fades out at the end rather than wrapping.
474pub fn list_item<M>(title: impl Into<String>) -> Node<M> {
475    Node(Kind::ListItem {
476        title: title.into(),
477        subtitle: None,
478        leading: None,
479        trailing: None,
480        selected: false,
481        on_press: None,
482        on_secondary: None,
483    })
484}
485
486/// Rows stacked closely, usually [`list_item`]s. In a popover, a long list scrolls instead of
487/// growing past a few rows.
488pub fn list<M>(rows: impl IntoIterator<Item = Node<M>>) -> Node<M> {
489    Node(Kind::List(rows.into_iter().collect()))
490}
491
492/// A small caption over a group in a popover ("Output", "Health"), with an optional
493/// [`trailing`](Node::trailing) node at the far end.
494pub fn section<M>(title: impl Into<String>) -> Node<M> {
495    Node(Kind::Section {
496        title: title.into(),
497        trailing: None,
498    })
499}
500
501/// The title of a popover page. Sub-pages get a back button with [`on_back`](Node::on_back); a
502/// [`leading`](Node::leading) picture goes before the title, a [`subtitle`](Node::subtitle)
503/// under it (where it is, what it's about), and a [`trailing`](Node::trailing) node sits at the
504/// far end (a switch, a count).
505pub fn header<M>(title: impl Into<String>) -> Node<M> {
506    Node(Kind::Header {
507        title: title.into(),
508        subtitle: None,
509        on_back: None,
510        leading: None,
511        trailing: None,
512    })
513}
514
515/// A named value on one line: the name dimmed at the start, the value at the end.
516pub fn detail<M>(name: impl Into<String>, value: Node<M>) -> Node<M> {
517    Node(Kind::Detail {
518        name: name.into(),
519        value: Box::new(value),
520    })
521}
522
523/// Takes the free space in a popover row, pushing what follows to the end. Nothing in the bar.
524pub fn fill<M>() -> Node<M> {
525    Node(Kind::Fill)
526}
527
528/// A value dragged along `range`: a volume. Sends `on_change` while dragging; add
529/// [`on_release`](Node::on_release) to act once it's let go, and [`step`](Node::step) to snap.
530pub fn slider<M: 'static>(
531    value: f32,
532    range: RangeInclusive<f32>,
533    on_change: impl Fn(f32) -> M + 'static,
534) -> Node<M> {
535    Node(Kind::Slider {
536        value,
537        range,
538        step: 1.0,
539        on_change: Box::new(on_change),
540        on_release: None,
541    })
542}
543
544/// An on/off switch. Sends `on_toggle` when clicked; the host slides it over.
545pub fn toggle<M>(on: bool, on_toggle: M) -> Node<M> {
546    Node(Kind::Toggle { on, on_toggle })
547}
548
549/// A bar filled to `fraction` (0.0 to 1.0), in its tone: a charge, a signal. The host eases it to
550/// a new value.
551pub fn meter<M>(fraction: f32) -> Node<M> {
552    Node(Kind::Meter {
553        fraction,
554        limit: None,
555        tone: None,
556    })
557}
558
559/// A field to type into, showing `value`. Sends `on_input` with the new text on every change.
560/// It takes the focus when it appears, and while it has it, keys stay in the popover; Esc still
561/// closes it. [`on_submit`](Node::on_submit) is sent on Enter. One per popover page. A
562/// [`Tone::Critical`] tone marks it as wrong.
563pub fn text_input<M: 'static>(
564    placeholder: impl Into<String>,
565    value: impl Into<String>,
566    on_input: impl Fn(String) -> M + 'static,
567) -> Node<M> {
568    Node(Kind::TextInput {
569        placeholder: placeholder.into(),
570        value: value.into(),
571        on_input: Box::new(on_input),
572        on_submit: None,
573        secret: false,
574        trailing: None,
575        tone: None,
576    })
577}
578
579/// A field to type several lines into, showing `value`: a message. It grows with its text up
580/// to a few lines, then scrolls. Sends `on_input` with the new text on every change, and
581/// [`on_submit`](Node::on_submit) on Enter; Shift+Enter starts a new line. In a popover it takes
582/// the focus when it appears, like a [`text_input`]. One per popover page.
583pub fn text_area<M: 'static>(
584    placeholder: impl Into<String>,
585    value: impl Into<String>,
586    on_input: impl Fn(String) -> M + 'static,
587) -> Node<M> {
588    Node(Kind::TextArea {
589        placeholder: placeholder.into(),
590        value: value.into(),
591        on_input: Box::new(on_input),
592        on_submit: None,
593        trailing: None,
594    })
595}
596
597/// Text written in Markdown, as people and programs write messages: paragraphs, emphasis,
598/// `code`, lists, quotes, headings, code blocks and tables. The host sets it in its own type and
599/// colors, wrapped to the width it has. Links show as links but don't open: the bar starts no
600/// programs.
601pub fn markdown<M>(text: impl Into<String>) -> Node<M> {
602    Node(Kind::Markdown {
603        text: text.into(),
604        tone: None,
605    })
606}
607
608/// Its content, scrolling in a popover once it's taller than the host allows a page to grow: a
609/// conversation, a long document. With [`Node::bottom`] it keeps its end in view. In the bar it's
610/// just its content.
611pub fn scroll<M>(content: Node<M>) -> Node<M> {
612    Node(Kind::Scroll {
613        content: Box::new(content),
614        bottom: false,
615    })
616}
617
618/// Text exactly as typed, not prose: a path, a command, a name in code. It doesn't wrap: when it
619/// doesn't fit, its end fades out.
620pub fn literal<M>(text: impl Into<String>) -> Node<M> {
621    Node(Kind::Literal {
622        text: text.into(),
623        tone: None,
624    })
625}
626
627/// A [`separator`] with a short caption in the middle of it, like the time between two turns of
628/// a conversation.
629pub fn divider<M>(text: impl Into<String>) -> Node<M> {
630    Node(Kind::Divider { text: text.into() })
631}
632
633/// Steps one after another, strung on a line: what someone did, in order. Each child is a
634/// [`step`], whose mark says where it stands; other nodes count as steps marked
635/// [`Mark::Note`].
636pub fn timeline<M>(steps: impl IntoIterator<Item = Node<M>>) -> Node<M> {
637    Node(Kind::Timeline(steps.into_iter().collect()))
638}
639
640/// One step of a [`timeline`]: `content`, marked on the line as `mark`. A
641/// [`leading`](Node::leading) node, like a small picture, stands on the line in place of the
642/// mark. Outside a timeline it's just its content.
643pub fn step<M>(mark: Mark, content: Node<M>) -> Node<M> {
644    Node(Kind::Step {
645        mark,
646        leading: None,
647        content: Box::new(content),
648    })
649}
650
651/// A change to a file, titled with its name: lines added, removed, and some unchanged ones around
652/// them. The host counts what was added and removed.
653pub fn diff<M>(
654    title: impl Into<String>,
655    lines: impl IntoIterator<Item = (Change, String)>,
656) -> Node<M> {
657    Node(Kind::Diff {
658        title: title.into(),
659        lines: lines.into_iter().collect(),
660    })
661}
662
663/// Its text, large: the one number a popover is about, like a battery's charge.
664pub fn display<M>(content: Node<M>) -> Node<M> {
665    Node(Kind::Display(Box::new(content)))
666}
667
668/// Its content marked with a ring: today in a calendar.
669pub fn mark<M>(content: Node<M>) -> Node<M> {
670    Node(Kind::Mark(Box::new(content)))
671}
672
673/// A short code in a small outlined box: a keyboard layout's `US`.
674pub fn badge<M>(text: impl Into<String>) -> Node<M> {
675    Node(Kind::Badge {
676        text: text.into(),
677        tone: None,
678    })
679}
680
681/// Pages behind a row of tabs, one page showing at a time: a popover's Output, Input and Apps.
682/// The host draws the tabs, switches pages when one is clicked and starts on the first each time
683/// the popover opens, so the plugin keeps no state for it. In the bar, only the first page shows.
684pub fn tabs<M>(pages: impl IntoIterator<Item = (impl Into<String>, Node<M>)>) -> Node<M> {
685    Node(Kind::Tabs(
686        pages
687            .into_iter()
688            .map(|(name, page)| (name.into(), page))
689            .collect(),
690    ))
691}
692
693impl<M> Node<M> {
694    /// Sets the tone of this text, or of every text inside this node that has none yet.
695    pub fn tone(mut self, tone: Tone) -> Self {
696        self.set_tone(tone);
697        self
698    }
699
700    /// For an [`alternate`]: shows the option at `index` while the pointer is over the item,
701    /// whatever `current` is. Other nodes have nothing to show on hover and ignore it.
702    pub fn on_hover(mut self, index: usize) -> Self {
703        if let Kind::Alternate { hovered, .. } = &mut self.0 {
704            *hovered = Some(index);
705        }
706        self
707    }
708
709    /// For a [`button`] or [`list_item`]: sent on a secondary (right) click on it. Other nodes
710    /// ignore it.
711    pub fn on_secondary(mut self, message: M) -> Self {
712        if let Kind::Button { on_secondary, .. } | Kind::ListItem { on_secondary, .. } = &mut self.0
713        {
714            *on_secondary = Some(message);
715        }
716        self
717    }
718
719    /// For a [`button`]: the main action of its page, filled so it stands out. Other nodes ignore
720    /// it.
721    pub fn prominent(mut self) -> Self {
722        if let Kind::Button { prominent, .. } = &mut self.0 {
723            *prominent = true;
724        }
725        self
726    }
727
728    /// For a [`list_item`]: sent on a click on it. Other nodes ignore it.
729    pub fn on_press(mut self, message: M) -> Self {
730        if let Kind::ListItem { on_press, .. } = &mut self.0 {
731            *on_press = Some(message);
732        }
733        self
734    }
735
736    /// For a [`list_item`] or [`header`]: a second, dimmer line under the title. Other nodes
737    /// ignore it.
738    pub fn subtitle(mut self, text: impl Into<String>) -> Self {
739        if let Kind::ListItem { subtitle, .. } | Kind::Header { subtitle, .. } = &mut self.0 {
740            *subtitle = Some(text.into());
741        }
742        self
743    }
744
745    /// For a [`list_item`] or [`header`]: an icon or small picture before the title. For a
746    /// [`step`]: a small picture on the line, in place of its mark. Other nodes ignore it.
747    pub fn leading(mut self, node: Node<M>) -> Self {
748        if let Kind::ListItem { leading, .. }
749        | Kind::Header { leading, .. }
750        | Kind::Step { leading, .. } = &mut self.0
751        {
752            *leading = Some(Box::new(node));
753        }
754        self
755    }
756
757    /// For a [`list_item`], [`section`], [`header`], [`text_input`] or [`text_area`]: a node at the
758    /// far end, like a lock or an info button. Other nodes ignore it.
759    pub fn trailing(mut self, node: Node<M>) -> Self {
760        if let Kind::ListItem { trailing, .. }
761        | Kind::Section { trailing, .. }
762        | Kind::Header { trailing, .. }
763        | Kind::TextInput { trailing, .. }
764        | Kind::TextArea { trailing, .. } = &mut self.0
765        {
766            *trailing = Some(Box::new(node));
767        }
768        self
769    }
770
771    /// For a [`list_item`]: marks it as the one in use. Other nodes ignore it.
772    pub fn selected(mut self, yes: bool) -> Self {
773        if let Kind::ListItem { selected, .. } = &mut self.0 {
774            *selected = yes;
775        }
776        self
777    }
778
779    /// For a [`header`]: shows a back button that sends `message`. Other nodes ignore it.
780    pub fn on_back(mut self, message: M) -> Self {
781        if let Kind::Header { on_back, .. } = &mut self.0 {
782            *on_back = Some(message);
783        }
784        self
785    }
786
787    /// For a [`slider`]: the step its value snaps to (1 by default). Other nodes ignore it.
788    pub fn step(mut self, by: f32) -> Self {
789        if let Kind::Slider { step, .. } = &mut self.0 {
790            *step = by;
791        }
792        self
793    }
794
795    /// For a [`slider`]: sent when it's let go. Other nodes ignore it.
796    pub fn on_release(mut self, message: M) -> Self {
797        if let Kind::Slider { on_release, .. } = &mut self.0 {
798            *on_release = Some(message);
799        }
800        self
801    }
802
803    /// For a [`text_input`] or [`text_area`]: sent on Enter. Other nodes ignore it.
804    pub fn on_submit(mut self, message: M) -> Self {
805        if let Kind::TextInput { on_submit, .. } | Kind::TextArea { on_submit, .. } = &mut self.0 {
806            *on_submit = Some(message);
807        }
808        self
809    }
810
811    /// For a [`text_input`]: hides what's typed, like a password. Other nodes ignore it.
812    pub fn secret(mut self, yes: bool) -> Self {
813        if let Kind::TextInput { secret, .. } = &mut self.0 {
814            *secret = yes;
815        }
816        self
817    }
818
819    /// For a [`sprite`]: plays for as long as it shows, not only when it shows up or changes.
820    /// Every frame redraws the bar, so keep it for what wants your eye, or for popovers. Other
821    /// nodes ignore it.
822    pub fn looping(mut self) -> Self {
823        if let Kind::Sprite { looping, .. } = &mut self.0 {
824            *looping = true;
825        }
826        self
827    }
828
829    /// For a [`cover`]: drawn big, as the main picture of a popover, not as an icon. Other nodes
830    /// ignore it.
831    pub fn large(mut self) -> Self {
832        if let Kind::Cover { large, .. } = &mut self.0 {
833            *large = true;
834        }
835        self
836    }
837
838    /// For a [`scroll`]: keeps its end in view as content arrives there, unless you've scrolled
839    /// up to read. Other nodes ignore it.
840    pub fn bottom(mut self) -> Self {
841        if let Kind::Scroll { bottom, .. } = &mut self.0 {
842            *bottom = true;
843        }
844        self
845    }
846
847    /// For a [`meter`]: marks a limit along it (0.0 to 1.0). Other nodes ignore it.
848    pub fn limit(mut self, at: f32) -> Self {
849        if let Kind::Meter { limit, .. } = &mut self.0 {
850            *limit = Some(at);
851        }
852        self
853    }
854
855    fn set_tone(&mut self, new: Tone) {
856        match &mut self.0 {
857            Kind::Label { tone, .. }
858            | Kind::Heading { tone, .. }
859            | Kind::Icon { tone, .. }
860            | Kind::AppIcon { tone, .. }
861            | Kind::Image { tone, .. }
862            | Kind::Sprite { tone, .. }
863            | Kind::Counter { tone, .. }
864            | Kind::Meter { tone, .. }
865            | Kind::TextInput { tone, .. }
866            | Kind::Markdown { tone, .. }
867            | Kind::Literal { tone, .. }
868            | Kind::Badge { tone, .. } => {
869                tone.get_or_insert(new);
870            }
871            Kind::Cover { tone, fallback, .. } => {
872                tone.get_or_insert(new);
873                fallback.set_tone(new);
874            }
875            Kind::Alternate {
876                options: children, ..
877            }
878            | Kind::Row(children)
879            | Kind::Column(children)
880            | Kind::Grid {
881                cells: children, ..
882            }
883            | Kind::List(children)
884            | Kind::Timeline(children) => {
885                children.iter_mut().for_each(|child| child.set_tone(new));
886            }
887            Kind::Step {
888                leading, content, ..
889            } => {
890                leading.iter_mut().for_each(|child| child.set_tone(new));
891                content.set_tone(new);
892            }
893            Kind::Button { content, .. }
894            | Kind::Gulp(content)
895            | Kind::Center(content)
896            | Kind::Display(content)
897            | Kind::Mark(content)
898            | Kind::Scroll { content, .. }
899            | Kind::Detail { value: content, .. } => content.set_tone(new),
900            Kind::ListItem {
901                leading, trailing, ..
902            }
903            | Kind::Header {
904                leading, trailing, ..
905            } => {
906                leading
907                    .iter_mut()
908                    .chain(trailing.iter_mut())
909                    .for_each(|child| child.set_tone(new));
910            }
911            Kind::Tabs(pages) => {
912                for (_, page) in pages {
913                    page.set_tone(new);
914                }
915            }
916            Kind::Switcher { options, .. } => {
917                options
918                    .iter_mut()
919                    .for_each(|(child, _)| child.set_tone(new));
920            }
921            Kind::Separator
922            | Kind::Divider { .. }
923            | Kind::Diff { .. }
924            | Kind::Empty
925            | Kind::Section { .. }
926            | Kind::Fill
927            | Kind::Slider { .. }
928            | Kind::TextArea { .. }
929            | Kind::Toggle { .. } => {}
930        }
931    }
932
933    /// Converts every message in this node. Sliders and text fields keep `f` to convert the
934    /// messages they make later, so it must be `Clone + 'static`, like a function or a closure
935    /// that captures nothing.
936    pub fn map<N: 'static>(self, f: &(impl Fn(M) -> N + Clone + 'static)) -> Node<N>
937    where
938        M: 'static,
939    {
940        let map_all = |nodes: Vec<Node<M>>| nodes.into_iter().map(|node| node.map(f)).collect();
941        let map_box = |node: Option<Box<Node<M>>>| node.map(|node| Box::new(node.map(f)));
942
943        Node(match self.0 {
944            Kind::Label { text, tone } => Kind::Label { text, tone },
945            Kind::Heading { text, tone } => Kind::Heading { text, tone },
946            Kind::Icon { icon, tone } => Kind::Icon { icon, tone },
947            Kind::AppIcon { app_id, tone } => Kind::AppIcon { app_id, tone },
948            Kind::Image { file, tint, tone } => Kind::Image { file, tint, tone },
949            Kind::Sprite {
950                frames,
951                order,
952                looping,
953                tone,
954            } => Kind::Sprite {
955                frames,
956                order,
957                looping,
958                tone,
959            },
960            Kind::Cover {
961                url,
962                fallback,
963                large,
964                tone,
965            } => Kind::Cover {
966                url,
967                fallback: Box::new(fallback.map(f)),
968                large,
969                tone,
970            },
971            Kind::Counter {
972                value,
973                range,
974                unit,
975                tone,
976            } => Kind::Counter {
977                value,
978                range,
979                unit,
980                tone,
981            },
982            Kind::Alternate {
983                options,
984                current,
985                hovered,
986                grows,
987            } => Kind::Alternate {
988                options: map_all(options),
989                current,
990                hovered,
991                grows,
992            },
993            Kind::Row(children) => Kind::Row(map_all(children)),
994            Kind::Column(children) => Kind::Column(map_all(children)),
995            Kind::Grid { columns, cells } => Kind::Grid {
996                columns,
997                cells: map_all(cells),
998            },
999            Kind::Button {
1000                content,
1001                on_press,
1002                on_secondary,
1003                prominent,
1004            } => Kind::Button {
1005                content: Box::new(content.map(f)),
1006                on_press: f(on_press),
1007                on_secondary: on_secondary.map(f),
1008                prominent,
1009            },
1010            Kind::Switcher { options, current } => Kind::Switcher {
1011                options: options
1012                    .into_iter()
1013                    .map(|(node, message)| (node.map(f), f(message)))
1014                    .collect(),
1015                current,
1016            },
1017            Kind::Gulp(content) => Kind::Gulp(Box::new(content.map(f))),
1018            Kind::Center(content) => Kind::Center(Box::new(content.map(f))),
1019            Kind::Separator => Kind::Separator,
1020            Kind::Empty => Kind::Empty,
1021            Kind::ListItem {
1022                title,
1023                subtitle,
1024                leading,
1025                trailing,
1026                selected,
1027                on_press,
1028                on_secondary,
1029            } => Kind::ListItem {
1030                title,
1031                subtitle,
1032                leading: map_box(leading),
1033                trailing: map_box(trailing),
1034                selected,
1035                on_press: on_press.map(f),
1036                on_secondary: on_secondary.map(f),
1037            },
1038            Kind::List(rows) => Kind::List(map_all(rows)),
1039            Kind::Section { title, trailing } => Kind::Section {
1040                title,
1041                trailing: map_box(trailing),
1042            },
1043            Kind::Header {
1044                title,
1045                subtitle,
1046                on_back,
1047                leading,
1048                trailing,
1049            } => Kind::Header {
1050                title,
1051                subtitle,
1052                on_back: on_back.map(f),
1053                leading: map_box(leading),
1054                trailing: map_box(trailing),
1055            },
1056            Kind::Detail { name, value } => Kind::Detail {
1057                name,
1058                value: Box::new(value.map(f)),
1059            },
1060            Kind::Fill => Kind::Fill,
1061            Kind::Slider {
1062                value,
1063                range,
1064                step,
1065                on_change,
1066                on_release,
1067            } => {
1068                let g = f.clone();
1069                Kind::Slider {
1070                    value,
1071                    range,
1072                    step,
1073                    on_change: Box::new(move |value| g(on_change(value))),
1074                    on_release: on_release.map(f),
1075                }
1076            }
1077            Kind::Toggle { on, on_toggle } => Kind::Toggle {
1078                on,
1079                on_toggle: f(on_toggle),
1080            },
1081            Kind::Meter {
1082                fraction,
1083                limit,
1084                tone,
1085            } => Kind::Meter {
1086                fraction,
1087                limit,
1088                tone,
1089            },
1090            Kind::TextInput {
1091                placeholder,
1092                value,
1093                on_input,
1094                on_submit,
1095                secret,
1096                trailing,
1097                tone,
1098            } => {
1099                let g = f.clone();
1100                Kind::TextInput {
1101                    placeholder,
1102                    value,
1103                    on_input: Box::new(move |text| g(on_input(text))),
1104                    on_submit: on_submit.map(f),
1105                    secret,
1106                    trailing: map_box(trailing),
1107                    tone,
1108                }
1109            }
1110            Kind::Display(content) => Kind::Display(Box::new(content.map(f))),
1111            Kind::Mark(content) => Kind::Mark(Box::new(content.map(f))),
1112            Kind::Badge { text, tone } => Kind::Badge { text, tone },
1113            Kind::Tabs(pages) => Kind::Tabs(
1114                pages
1115                    .into_iter()
1116                    .map(|(name, page)| (name, page.map(f)))
1117                    .collect(),
1118            ),
1119            Kind::Markdown { text, tone } => Kind::Markdown { text, tone },
1120            Kind::TextArea {
1121                placeholder,
1122                value,
1123                on_input,
1124                on_submit,
1125                trailing,
1126            } => {
1127                let g = f.clone();
1128                Kind::TextArea {
1129                    placeholder,
1130                    value,
1131                    on_input: Box::new(move |text| g(on_input(text))),
1132                    on_submit: on_submit.map(f),
1133                    trailing: map_box(trailing),
1134                }
1135            }
1136            Kind::Scroll { content, bottom } => Kind::Scroll {
1137                content: Box::new(content.map(f)),
1138                bottom,
1139            },
1140            Kind::Literal { text, tone } => Kind::Literal { text, tone },
1141            Kind::Divider { text } => Kind::Divider { text },
1142            Kind::Timeline(steps) => Kind::Timeline(map_all(steps)),
1143            Kind::Step {
1144                mark,
1145                leading,
1146                content,
1147            } => Kind::Step {
1148                mark,
1149                leading: map_box(leading),
1150                content: Box::new(content.map(f)),
1151            },
1152            Kind::Diff { title, lines } => Kind::Diff { title, lines },
1153        })
1154    }
1155
1156    #[doc(hidden)]
1157    pub fn into_kind(self) -> Kind<M> {
1158        self.0
1159    }
1160
1161    #[doc(hidden)]
1162    pub fn kind(&self) -> &Kind<M> {
1163        &self.0
1164    }
1165}
1166
1167/// What a widget shows in the bar.
1168pub struct Item<M> {
1169    content: Option<Node<M>>,
1170    on_press: Option<M>,
1171    on_secondary: Option<M>,
1172    on_scroll: Option<(M, M)>,
1173}
1174
1175impl<M> Item<M> {
1176    pub fn new(content: Node<M>) -> Self {
1177        Self {
1178            content: Some(content),
1179            on_press: None,
1180            on_secondary: None,
1181            on_scroll: None,
1182        }
1183    }
1184
1185    /// Takes no space in the bar. For widgets with nothing to show, like a battery on a desktop.
1186    pub fn hidden() -> Self {
1187        Self {
1188            content: None,
1189            on_press: None,
1190            on_secondary: None,
1191            on_scroll: None,
1192        }
1193    }
1194
1195    /// Sent on a primary click. Replaces the default of toggling the popover.
1196    pub fn on_press(mut self, message: M) -> Self {
1197        self.on_press = Some(message);
1198        self
1199    }
1200
1201    /// Sent on a secondary (right) click.
1202    pub fn on_secondary(mut self, message: M) -> Self {
1203        self.on_secondary = Some(message);
1204        self
1205    }
1206
1207    /// Sent once per scroll step over the item, `up` or `down`. The host turns smooth touchpad
1208    /// scrolling into steps.
1209    pub fn on_scroll(mut self, up: M, down: M) -> Self {
1210        self.on_scroll = Some((up, down));
1211        self
1212    }
1213
1214    pub fn map<N: 'static>(self, f: &(impl Fn(M) -> N + Clone + 'static)) -> Item<N>
1215    where
1216        M: 'static,
1217    {
1218        Item {
1219            content: self.content.map(|content| content.map(f)),
1220            on_press: self.on_press.map(f),
1221            on_secondary: self.on_secondary.map(f),
1222            on_scroll: self.on_scroll.map(|(up, down)| (f(up), f(down))),
1223        }
1224    }
1225
1226    #[doc(hidden)]
1227    pub fn into_parts(self) -> ItemParts<M> {
1228        ItemParts {
1229            content: self.content,
1230            on_press: self.on_press,
1231            on_secondary: self.on_secondary,
1232            on_scroll: self.on_scroll,
1233        }
1234    }
1235}
1236
1237#[doc(hidden)]
1238pub struct ItemParts<M> {
1239    pub content: Option<Node<M>>,
1240    pub on_press: Option<M>,
1241    pub on_secondary: Option<M>,
1242    /// Up, then down.
1243    pub on_scroll: Option<(M, M)>,
1244}
1245
1246/// The contents of a popover. The host draws the surface, its background and its edges, and lays
1247/// every popover out at the same width, or at a wider one if it asks ([`Panel::wide`]).
1248pub struct Panel<M> {
1249    content: Node<M>,
1250    on_open: Option<M>,
1251    on_close: Option<M>,
1252    wide: bool,
1253}
1254
1255impl<M> Panel<M> {
1256    pub fn new(content: Node<M>) -> Self {
1257        Self {
1258            content,
1259            on_open: None,
1260            on_close: None,
1261            wide: false,
1262        }
1263    }
1264
1265    /// Laid out wider than other popovers, for a page that's mostly reading: a conversation, a
1266    /// document. The host decides how wide.
1267    pub fn wide(mut self) -> Self {
1268        self.wide = true;
1269        self
1270    }
1271
1272    #[doc(hidden)]
1273    pub fn is_wide(&self) -> bool {
1274        self.wide
1275    }
1276
1277    /// Sent each time the popover opens: start a scan, show the first page.
1278    pub fn on_open(mut self, message: M) -> Self {
1279        self.on_open = Some(message);
1280        self
1281    }
1282
1283    /// Sent each time the popover starts closing: stop listening, forget what was typed.
1284    pub fn on_close(mut self, message: M) -> Self {
1285        self.on_close = Some(message);
1286        self
1287    }
1288
1289    pub fn map<N: 'static>(self, f: &(impl Fn(M) -> N + Clone + 'static)) -> Panel<N>
1290    where
1291        M: 'static,
1292    {
1293        Panel {
1294            content: self.content.map(f),
1295            on_open: self.on_open.map(f),
1296            on_close: self.on_close.map(f),
1297            wide: self.wide,
1298        }
1299    }
1300
1301    #[doc(hidden)]
1302    pub fn into_node(self) -> Node<M> {
1303        self.content
1304    }
1305
1306    #[doc(hidden)]
1307    pub fn on_open_message(self) -> Option<M> {
1308        self.on_open
1309    }
1310
1311    #[doc(hidden)]
1312    pub fn on_close_message(self) -> Option<M> {
1313        self.on_close
1314    }
1315}
1316
1317/// How much a [`Notice`] matters. The host decides what that looks like.
1318#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1319pub enum Urgency {
1320    /// Background news: a download finished.
1321    Low,
1322    #[default]
1323    Normal,
1324    /// Needs attention now: a battery about to die.
1325    Critical,
1326}
1327
1328/// A notification to show on screen, from [`Widget::notices`](crate::Widget::notices).
1329///
1330/// It says what the notification is, not how it looks: the host lays out the notification, places it and
1331/// animates it in and out. Its `key` is how the host tells notices apart from one view to the
1332/// next: a notice that keeps its key and changes its text is updated in place, and one whose key
1333/// is gone from the list leaves the screen.
1334pub struct Notice<M> {
1335    key: u64,
1336    summary: String,
1337    app: Option<String>,
1338    app_icon: Option<String>,
1339    image: Option<String>,
1340    body: Option<String>,
1341    markdown: bool,
1342    urgency: Urgency,
1343    actions: Vec<(String, M)>,
1344    on_press: Option<M>,
1345    on_dismiss: Option<M>,
1346    expires: Option<(std::time::Duration, M)>,
1347    reply: Option<Reply<M>>,
1348}
1349
1350/// A field on a [`Notice`] to type an answer into, like a reply to a message: several lines,
1351/// Enter sends and Shift+Enter starts a new line.
1352///
1353/// It shows with the notice, but the host takes the keyboard only once it's clicked, and gives it
1354/// back on Esc, on sending, or on a click elsewhere. The notice doesn't expire while it has the
1355/// keyboard.
1356pub struct Reply<M> {
1357    placeholder: String,
1358    value: String,
1359    on_input: std::rc::Rc<dyn Fn(String) -> M>,
1360    on_submit: Option<M>,
1361    on_cancel: Option<M>,
1362}
1363
1364impl<M> Reply<M> {
1365    /// Shows `value`, and sends `on_input` with the new text on every change.
1366    pub fn new(
1367        placeholder: impl Into<String>,
1368        value: impl Into<String>,
1369        on_input: impl Fn(String) -> M + 'static,
1370    ) -> Self {
1371        Self {
1372            placeholder: placeholder.into(),
1373            value: value.into(),
1374            on_input: std::rc::Rc::new(on_input),
1375            on_submit: None,
1376            on_cancel: None,
1377        }
1378    }
1379
1380    /// Sent on Enter, or a click on the field's send button.
1381    pub fn on_submit(mut self, message: M) -> Self {
1382        self.on_submit = Some(message);
1383        self
1384    }
1385
1386    /// Sent on Esc.
1387    pub fn on_cancel(mut self, message: M) -> Self {
1388        self.on_cancel = Some(message);
1389        self
1390    }
1391
1392    pub fn map<N: 'static>(self, f: &(impl Fn(M) -> N + Clone + 'static)) -> Reply<N>
1393    where
1394        M: 'static,
1395    {
1396        let g = f.clone();
1397        let on_input = self.on_input;
1398        Reply {
1399            placeholder: self.placeholder,
1400            value: self.value,
1401            on_input: std::rc::Rc::new(move |text| g(on_input(text))),
1402            on_submit: self.on_submit.map(f),
1403            on_cancel: self.on_cancel.map(f),
1404        }
1405    }
1406
1407    #[doc(hidden)]
1408    pub fn into_parts(self) -> ReplyParts<M> {
1409        ReplyParts {
1410            placeholder: self.placeholder,
1411            value: self.value,
1412            on_input: self.on_input,
1413            on_submit: self.on_submit,
1414            on_cancel: self.on_cancel,
1415        }
1416    }
1417}
1418
1419#[doc(hidden)]
1420#[derive(Clone)]
1421pub struct ReplyParts<M> {
1422    pub placeholder: String,
1423    pub value: String,
1424    pub on_input: std::rc::Rc<dyn Fn(String) -> M>,
1425    pub on_submit: Option<M>,
1426    pub on_cancel: Option<M>,
1427}
1428
1429impl<M> Notice<M> {
1430    pub fn new(key: u64, summary: impl Into<String>) -> Self {
1431        Self {
1432            key,
1433            summary: summary.into(),
1434            app: None,
1435            app_icon: None,
1436            image: None,
1437            body: None,
1438            markdown: false,
1439            urgency: Urgency::Normal,
1440            actions: Vec::new(),
1441            on_press: None,
1442            on_dismiss: None,
1443            expires: None,
1444            reply: None,
1445        }
1446    }
1447
1448    /// The name of the app it comes from.
1449    pub fn app(mut self, name: impl Into<String>) -> Self {
1450        self.app = Some(name.into());
1451        self
1452    }
1453
1454    /// The app's icon, by app id, like [`app_icon`].
1455    pub fn app_icon(mut self, app_id: impl Into<String>) -> Self {
1456        self.app_icon = Some(app_id.into());
1457        self
1458    }
1459
1460    /// A picture to show instead of the app's icon, by URL, like a [`cover`]: an icon file the
1461    /// app gave as a path.
1462    pub fn image(mut self, url: impl Into<String>) -> Self {
1463        self.image = Some(url.into());
1464        self
1465    }
1466
1467    /// Plain text under the summary. The host wraps it and cuts it off after a few lines.
1468    pub fn body(mut self, text: impl Into<String>) -> Self {
1469        self.body = Some(text.into());
1470        self
1471    }
1472
1473    /// The body is Markdown, like a [`markdown`] node.
1474    pub fn markdown(mut self) -> Self {
1475        self.markdown = true;
1476        self
1477    }
1478
1479    pub fn urgency(mut self, urgency: Urgency) -> Self {
1480        self.urgency = urgency;
1481        self
1482    }
1483
1484    /// A button on the notification that sends `message`. The first one is the main action.
1485    pub fn action(mut self, label: impl Into<String>, message: M) -> Self {
1486        self.actions.push((label.into(), message));
1487        self
1488    }
1489
1490    /// Sent on a click on the notification itself, outside its buttons.
1491    pub fn on_press(mut self, message: M) -> Self {
1492        self.on_press = Some(message);
1493        self
1494    }
1495
1496    /// Sent when its close button is clicked. Without it, it has none.
1497    pub fn on_dismiss(mut self, message: M) -> Self {
1498        self.on_dismiss = Some(message);
1499        self
1500    }
1501
1502    /// Sent once it has been on screen for `after`. The host doesn't count while the pointer is
1503    /// on it or it's waiting to be shown. Without it, it stays
1504    /// until it leaves the list.
1505    pub fn expires(mut self, after: std::time::Duration, message: M) -> Self {
1506        self.expires = Some((after, message));
1507        self
1508    }
1509
1510    /// A field to type an answer into ([`Reply`]).
1511    pub fn reply(mut self, reply: Reply<M>) -> Self {
1512        self.reply = Some(reply);
1513        self
1514    }
1515
1516    pub fn map<N: 'static>(self, f: &(impl Fn(M) -> N + Clone + 'static)) -> Notice<N>
1517    where
1518        M: 'static,
1519    {
1520        Notice {
1521            key: self.key,
1522            summary: self.summary,
1523            app: self.app,
1524            app_icon: self.app_icon,
1525            image: self.image,
1526            body: self.body,
1527            markdown: self.markdown,
1528            urgency: self.urgency,
1529            actions: self
1530                .actions
1531                .into_iter()
1532                .map(|(label, message)| (label, f(message)))
1533                .collect(),
1534            on_press: self.on_press.map(f),
1535            on_dismiss: self.on_dismiss.map(f),
1536            expires: self.expires.map(|(after, message)| (after, f(message))),
1537            reply: self.reply.map(|reply| reply.map(f)),
1538        }
1539    }
1540
1541    pub fn key(&self) -> u64 {
1542        self.key
1543    }
1544
1545    #[doc(hidden)]
1546    pub fn into_parts(self) -> NoticeParts<M> {
1547        NoticeParts {
1548            key: self.key,
1549            summary: self.summary,
1550            app: self.app,
1551            app_icon: self.app_icon,
1552            image: self.image,
1553            body: self.body,
1554            markdown: self.markdown,
1555            urgency: self.urgency,
1556            actions: self.actions,
1557            on_press: self.on_press,
1558            on_dismiss: self.on_dismiss,
1559            expires: self.expires,
1560            reply: self.reply.map(Reply::into_parts),
1561        }
1562    }
1563}
1564
1565#[doc(hidden)]
1566#[derive(Clone)]
1567pub struct NoticeParts<M> {
1568    pub key: u64,
1569    pub summary: String,
1570    pub app: Option<String>,
1571    pub app_icon: Option<String>,
1572    pub image: Option<String>,
1573    pub body: Option<String>,
1574    pub markdown: bool,
1575    pub urgency: Urgency,
1576    pub actions: Vec<(String, M)>,
1577    pub on_press: Option<M>,
1578    pub on_dismiss: Option<M>,
1579    pub expires: Option<(std::time::Duration, M)>,
1580    pub reply: Option<ReplyParts<M>>,
1581}
1582
1583#[cfg(test)]
1584mod tests {
1585    use super::*;
1586
1587    #[test]
1588    fn map_converts_every_message() {
1589        let node = row([
1590            button(label("a"), 1).on_secondary(4),
1591            column([button(label("b"), 2), label("c")]),
1592            switcher([(label("d"), 3)], Some(0)),
1593        ]);
1594
1595        let Kind::Row(children) = node.map(&|n: i32| n * 10).into_kind() else {
1596            panic!("row became something else");
1597        };
1598        let mut messages = Vec::new();
1599        collect(children, &mut messages);
1600        assert_eq!(messages, [10, 40, 20, 30]);
1601    }
1602
1603    fn collect(nodes: Vec<Node<i32>>, out: &mut Vec<i32>) {
1604        for node in nodes {
1605            match node.into_kind() {
1606                Kind::Button {
1607                    on_press,
1608                    on_secondary,
1609                    ..
1610                } => {
1611                    out.push(on_press);
1612                    out.extend(on_secondary);
1613                }
1614                Kind::Switcher { options, .. } => {
1615                    out.extend(options.into_iter().map(|(_, message)| message))
1616                }
1617                Kind::Row(children) | Kind::Column(children) => collect(children, out),
1618                _ => {}
1619            }
1620        }
1621    }
1622
1623    #[test]
1624    fn hover_shows_the_second_option() {
1625        let node: Node<()> = hover(label("a"), label("b"));
1626        assert!(matches!(
1627            node.into_kind(),
1628            Kind::Alternate {
1629                current: 0,
1630                hovered: Some(1),
1631                ..
1632            }
1633        ));
1634        let node: Node<()> = label("a").on_hover(1);
1635        assert!(matches!(node.into_kind(), Kind::Label { .. }));
1636    }
1637
1638    #[test]
1639    fn tone_fills_only_unset_text() {
1640        let node: Node<()> =
1641            row([label("a").tone(Tone::Accent), label("b"), icon(Icon::Wired)]).tone(Tone::Dim);
1642        let Kind::Row(children) = node.into_kind() else {
1643            unreachable!()
1644        };
1645        let tones: Vec<_> = children
1646            .into_iter()
1647            .map(|child| match child.into_kind() {
1648                Kind::Label { tone, .. } | Kind::Icon { tone, .. } => tone,
1649                _ => None,
1650            })
1651            .collect();
1652        assert_eq!(
1653            tones,
1654            [Some(Tone::Accent), Some(Tone::Dim), Some(Tone::Dim)]
1655        );
1656    }
1657
1658    #[test]
1659    fn map_reaches_new_nodes_and_their_closures() {
1660        let node = column([
1661            header("h").on_back(1).trailing(toggle(true, 2)),
1662            list([list_item("a")
1663                .on_press(3)
1664                .on_secondary(4)
1665                .trailing(button(icon(Icon::Info), 5))]),
1666            slider(0.5, 0.0..=1.0, |v| (v * 10.0) as i32).on_release(6),
1667            text_input("p", "v", |text: String| text.len() as i32).on_submit(7),
1668            scroll(text_area("p", "v", |text: String| text.len() as i32 + 1).on_submit(8)).bottom(),
1669        ]);
1670        let Kind::Column(children) = node.map(&|n: i32| n * 10).into_kind() else {
1671            panic!("column became something else");
1672        };
1673        let mut children = children.into_iter().map(Node::into_kind);
1674        let Some(Kind::Header {
1675            on_back: Some(10),
1676            trailing: Some(trailing),
1677            ..
1678        }) = children.next()
1679        else {
1680            panic!("header lost its back message");
1681        };
1682        assert!(matches!(
1683            trailing.into_kind(),
1684            Kind::Toggle { on_toggle: 20, .. }
1685        ));
1686        let Some(Kind::List(rows)) = children.next() else {
1687            panic!("list lost");
1688        };
1689        let Some(Kind::ListItem {
1690            on_press: Some(30),
1691            on_secondary: Some(40),
1692            trailing: Some(trailing),
1693            ..
1694        }) = rows.into_iter().next().map(Node::into_kind)
1695        else {
1696            panic!("list item lost its messages");
1697        };
1698        assert!(matches!(
1699            trailing.into_kind(),
1700            Kind::Button { on_press: 50, .. }
1701        ));
1702        let Some(Kind::Slider {
1703            on_change,
1704            on_release: Some(60),
1705            ..
1706        }) = children.next()
1707        else {
1708            panic!("slider lost");
1709        };
1710        assert_eq!(on_change(0.3), 30);
1711        let Some(Kind::TextInput {
1712            on_input,
1713            on_submit: Some(70),
1714            ..
1715        }) = children.next()
1716        else {
1717            panic!("text input lost");
1718        };
1719        assert_eq!(on_input("abcd".into()), 40);
1720        let Some(Kind::Scroll {
1721            content,
1722            bottom: true,
1723        }) = children.next()
1724        else {
1725            panic!("scroll lost");
1726        };
1727        let Kind::TextArea {
1728            on_input,
1729            on_submit: Some(80),
1730            ..
1731        } = content.into_kind()
1732        else {
1733            panic!("text area lost");
1734        };
1735        assert_eq!(on_input("abcd".into()), 50);
1736    }
1737
1738    #[test]
1739    fn tabs_map_and_tone_every_page() {
1740        let node = tabs([("a", button(label("x"), 1)), ("b", button(label("y"), 2))])
1741            .tone(Tone::Dim)
1742            .map(&|n: i32| n * 10);
1743        let Kind::Tabs(pages) = node.into_kind() else {
1744            panic!("tabs became something else");
1745        };
1746        let names: Vec<_> = pages.iter().map(|(name, _)| name.as_str()).collect();
1747        assert_eq!(names, ["a", "b"]);
1748        let mut messages = Vec::new();
1749        for (_, page) in pages {
1750            let Kind::Button {
1751                content, on_press, ..
1752            } = page.into_kind()
1753            else {
1754                panic!("page lost its button");
1755            };
1756            assert!(matches!(
1757                content.into_kind(),
1758                Kind::Label {
1759                    tone: Some(Tone::Dim),
1760                    ..
1761                }
1762            ));
1763            messages.push(on_press);
1764        }
1765        assert_eq!(messages, [10, 20]);
1766    }
1767
1768    #[test]
1769    fn notice_map_reaches_every_message() {
1770        let notice = Notice::new(7, "s")
1771            .action("a", 1)
1772            .action("b", 2)
1773            .on_press(3)
1774            .on_dismiss(4)
1775            .expires(std::time::Duration::from_secs(5), 5)
1776            .reply(
1777                Reply::new("p", "v", |text: String| text.len() as i32)
1778                    .on_submit(6)
1779                    .on_cancel(7),
1780            )
1781            .map(&|n: i32| n * 10);
1782        assert_eq!(notice.key(), 7);
1783        let parts = notice.into_parts();
1784        let actions: Vec<_> = parts.actions.into_iter().map(|(_, m)| m).collect();
1785        assert_eq!(actions, [10, 20]);
1786        assert_eq!(parts.on_press, Some(30));
1787        assert_eq!(parts.on_dismiss, Some(40));
1788        assert_eq!(parts.expires.map(|(_, m)| m), Some(50));
1789        let reply = parts.reply.unwrap();
1790        assert_eq!((reply.on_input)("abc".into()), 30);
1791        assert_eq!((reply.on_submit, reply.on_cancel), (Some(60), Some(70)));
1792    }
1793
1794    #[test]
1795    fn modifiers_only_touch_their_nodes() {
1796        let node: Node<i32> = label("a").trailing(label("b")).on_press(1).selected(true);
1797        assert!(matches!(node.into_kind(), Kind::Label { .. }));
1798        let node: Node<i32> = button(label("a"), 1).on_press(2).prominent();
1799        assert!(matches!(
1800            node.into_kind(),
1801            Kind::Button {
1802                on_press: 1,
1803                prominent: true,
1804                ..
1805            }
1806        ));
1807    }
1808
1809    #[test]
1810    fn a_header_takes_a_picture_and_a_subtitle() {
1811        let node = header("t")
1812            .subtitle("s")
1813            .leading(button(icon(Icon::Info), 1))
1814            .tone(Tone::Dim)
1815            .map(&|n: i32| n * 10);
1816        let Kind::Header {
1817            subtitle: Some(subtitle),
1818            leading: Some(leading),
1819            ..
1820        } = node.into_kind()
1821        else {
1822            panic!("header lost its picture or subtitle");
1823        };
1824        assert_eq!(subtitle, "s");
1825        let Kind::Button {
1826            content,
1827            on_press: 10,
1828            ..
1829        } = leading.into_kind()
1830        else {
1831            panic!("picture lost its message");
1832        };
1833        assert!(matches!(
1834            content.into_kind(),
1835            Kind::Icon {
1836                tone: Some(Tone::Dim),
1837                ..
1838            }
1839        ));
1840    }
1841
1842    #[test]
1843    fn timelines_map_and_tone_their_steps() {
1844        let node = timeline([
1845            step(Mark::You, label("a")),
1846            step(Mark::Failed, button(literal("b"), 1)).leading(icon(Icon::Check)),
1847            label("c"),
1848        ])
1849        .tone(Tone::Dim)
1850        .map(&|n: i32| n * 10);
1851        let Kind::Timeline(steps) = node.into_kind() else {
1852            panic!("timeline became something else");
1853        };
1854        let mut steps = steps.into_iter().map(Node::into_kind);
1855        let Some(Kind::Step {
1856            mark: Mark::You,
1857            leading: None,
1858            content,
1859        }) = steps.next()
1860        else {
1861            panic!("first step lost");
1862        };
1863        assert!(matches!(
1864            content.into_kind(),
1865            Kind::Label {
1866                tone: Some(Tone::Dim),
1867                ..
1868            }
1869        ));
1870        let Some(Kind::Step {
1871            mark: Mark::Failed,
1872            leading: Some(leading),
1873            content,
1874        }) = steps.next()
1875        else {
1876            panic!("second step lost its picture");
1877        };
1878        assert!(matches!(
1879            leading.into_kind(),
1880            Kind::Icon {
1881                tone: Some(Tone::Dim),
1882                ..
1883            }
1884        ));
1885        let Kind::Button {
1886            content,
1887            on_press: 10,
1888            ..
1889        } = content.into_kind()
1890        else {
1891            panic!("button lost its message");
1892        };
1893        assert!(matches!(
1894            content.into_kind(),
1895            Kind::Literal {
1896                tone: Some(Tone::Dim),
1897                ..
1898            }
1899        ));
1900        assert!(matches!(steps.next(), Some(Kind::Label { .. })));
1901        let node: Node<()> = diff("f", [(Change::Added, "x".to_string())]).leading(label("y"));
1902        assert!(matches!(node.into_kind(), Kind::Diff { lines, .. } if lines.len() == 1));
1903    }
1904
1905    #[test]
1906    fn tone_reaches_details_and_row_icons() {
1907        let node: Node<()> = column([
1908            detail("n", label("v")),
1909            list_item("t").leading(icon(Icon::Lock)),
1910        ])
1911        .tone(Tone::Good);
1912        let Kind::Column(children) = node.into_kind() else {
1913            unreachable!()
1914        };
1915        let mut children = children.into_iter().map(Node::into_kind);
1916        let Some(Kind::Detail { value, .. }) = children.next() else {
1917            unreachable!()
1918        };
1919        assert!(matches!(
1920            value.into_kind(),
1921            Kind::Label {
1922                tone: Some(Tone::Good),
1923                ..
1924            }
1925        ));
1926        let Some(Kind::ListItem {
1927            leading: Some(leading),
1928            ..
1929        }) = children.next()
1930        else {
1931            unreachable!()
1932        };
1933        assert!(matches!(
1934            leading.into_kind(),
1935            Kind::Icon {
1936                tone: Some(Tone::Good),
1937                ..
1938            }
1939        ));
1940    }
1941}