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