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}
67
68/// A piece of UI. Build it with the functions in this module.
69pub struct Node<M>(Kind<M>);
70
71#[doc(hidden)]
72pub enum Kind<M> {
73    Label {
74        text: String,
75        tone: Option<Tone>,
76    },
77    Heading {
78        text: String,
79        tone: Option<Tone>,
80    },
81    Icon {
82        icon: Icon,
83        tone: Option<Tone>,
84    },
85    AppIcon {
86        app_id: String,
87        tone: Option<Tone>,
88    },
89    Image {
90        file: &'static [u8],
91        tint: bool,
92        tone: Option<Tone>,
93    },
94    Counter {
95        value: i64,
96        range: RangeInclusive<i64>,
97        unit: String,
98        tone: Option<Tone>,
99    },
100    Alternate {
101        options: Vec<Node<M>>,
102        current: usize,
103        /// Shown instead of `current` while the pointer is over the item.
104        hovered: Option<usize>,
105        /// As wide as the option on screen, hovered by its own width, after a rest ([`expand`]).
106        grows: bool,
107    },
108    Row(Vec<Node<M>>),
109    Column(Vec<Node<M>>),
110    Grid {
111        columns: usize,
112        cells: Vec<Node<M>>,
113    },
114    Button {
115        content: Box<Node<M>>,
116        on_press: M,
117        on_secondary: Option<M>,
118        /// The main action of a popover page: filled with the accent.
119        prominent: bool,
120    },
121    Switcher {
122        options: Vec<(Node<M>, M)>,
123        current: Option<usize>,
124    },
125    Gulp(Box<Node<M>>),
126    Separator,
127    Empty,
128    ListItem {
129        title: String,
130        subtitle: Option<String>,
131        leading: Option<Box<Node<M>>>,
132        trailing: Option<Box<Node<M>>>,
133        selected: bool,
134        on_press: Option<M>,
135        on_secondary: Option<M>,
136    },
137    List(Vec<Node<M>>),
138    Section {
139        title: String,
140        trailing: Option<Box<Node<M>>>,
141    },
142    Header {
143        title: String,
144        on_back: Option<M>,
145        trailing: Option<Box<Node<M>>>,
146    },
147    Detail {
148        name: String,
149        value: Box<Node<M>>,
150    },
151    Fill,
152    Slider {
153        value: f32,
154        range: RangeInclusive<f32>,
155        step: f32,
156        on_change: Box<dyn Fn(f32) -> M>,
157        on_release: Option<M>,
158    },
159    Toggle {
160        on: bool,
161        on_toggle: M,
162    },
163    Meter {
164        fraction: f32,
165        /// Where a limit sits along it, like a battery's charge limit.
166        limit: Option<f32>,
167        tone: Option<Tone>,
168    },
169    TextInput {
170        placeholder: String,
171        value: String,
172        on_input: Box<dyn Fn(String) -> M>,
173        on_submit: Option<M>,
174        secret: bool,
175        trailing: Option<Box<Node<M>>>,
176        tone: Option<Tone>,
177    },
178    Display(Box<Node<M>>),
179    Mark(Box<Node<M>>),
180    Badge {
181        text: String,
182        tone: Option<Tone>,
183    },
184    /// Pages and their names. The host shows one at a time and owns which.
185    Tabs(Vec<(String, Node<M>)>),
186}
187
188pub fn label<M>(text: impl Into<String>) -> Node<M> {
189    Node(Kind::Label {
190        text: text.into(),
191        tone: None,
192    })
193}
194
195/// A title, larger than a label. Meant for popovers.
196pub fn heading<M>(text: impl Into<String>) -> Node<M> {
197    Node(Kind::Heading {
198        text: text.into(),
199        tone: None,
200    })
201}
202
203/// A symbol, sized like a label.
204pub fn icon<M>(icon: Icon) -> Node<M> {
205    Node(Kind::Icon { icon, tone: None })
206}
207
208/// An application's icon, found by its app id or window class (`firefox`, `org.gnome.Nautilus`)
209/// in the user's icon theme. Sized like an [`icon`], in the app's own colors: a tone only dims it.
210/// Until the host has found it, or if there is none, it shows the id's first letter.
211pub fn app_icon<M>(app_id: impl Into<String>) -> Node<M> {
212    Node(Kind::AppIcon {
213        app_id: app_id.into(),
214        tone: None,
215    })
216}
217
218/// An icon the plugin ships: an SVG or PNG file compiled into it with `include_bytes!`, told
219/// apart by its first bytes. Sized like an [`icon`], in the file's own colors: a tone only dims
220/// it, as with [`app_icon`]. The host draws it once, off the UI thread, and shows nothing in its
221/// place until then.
222pub fn image<M>(file: &'static [u8]) -> Node<M> {
223    Node(Kind::Image {
224        file,
225        tint: false,
226        tone: None,
227    })
228}
229
230/// Like [`image`], but only the file's shape counts: it's drawn in the text's tone, like an
231/// [`icon`]. For a monochrome symbol the vocabulary doesn't have.
232pub fn symbol<M>(file: &'static [u8]) -> Node<M> {
233    Node(Kind::Image {
234        file,
235        tint: true,
236        tone: None,
237    })
238}
239
240/// A number from `range`, with a unit after it (`"%"`, `"°"`, or `""`), shown like a label. It
241/// keeps room for the widest number in `range`, so it doesn't change width as it changes. When
242/// `value` changes, the host counts to it through every number in between.
243pub fn counter<M>(value: i64, range: RangeInclusive<i64>, unit: impl Into<String>) -> Node<M> {
244    Node(Kind::Counter {
245        value,
246        range,
247        unit: unit.into(),
248        tone: None,
249    })
250}
251
252/// One of `options` at a time: the one at `current`, or the last one if it's past the end. It's as
253/// big as the biggest option, so it doesn't change size when `current` does. The host fades from
254/// one to the other. Keep the options the same from one
255/// view to the next, so the host can tell which one is coming in.
256pub fn alternate<M>(options: impl IntoIterator<Item = Node<M>>, current: usize) -> Node<M> {
257    Node(Kind::Alternate {
258        options: options.into_iter().collect(),
259        current,
260        hovered: None,
261        grows: false,
262    })
263}
264
265/// `normal`, or `hovered` while the pointer is over the item, fading between them like an
266/// [`alternate`]: an icon that shows its percent, a time that shows the date.
267pub fn hover<M>(normal: Node<M>, hovered: Node<M>) -> Node<M> {
268    alternate([normal, hovered], 0).on_hover(1)
269}
270
271/// `collapsed`, or `expanded` once the pointer rests on this node, fading between them like a
272/// [`hover`]. Unlike `hover`, it's only as big as what it shows, so the item grows to make room
273/// and the island springs along, and it reacts to the pointer over it, not the whole item: a
274/// workspace that shows every window when you point at it.
275pub fn expand<M>(collapsed: Node<M>, expanded: Node<M>) -> Node<M> {
276    Node(Kind::Alternate {
277        options: vec![collapsed, expanded],
278        current: 0,
279        hovered: Some(1),
280        grows: true,
281    })
282}
283
284/// Children side by side, vertically centered.
285pub fn row<M>(children: impl IntoIterator<Item = Node<M>>) -> Node<M> {
286    Node(Kind::Row(children.into_iter().collect()))
287}
288
289/// Children stacked top to bottom.
290pub fn column<M>(children: impl IntoIterator<Item = Node<M>>) -> Node<M> {
291    Node(Kind::Column(children.into_iter().collect()))
292}
293
294/// Cells laid out left to right in rows of `columns`, all the same width. A calendar, a keypad.
295pub fn grid<M>(columns: usize, cells: impl IntoIterator<Item = Node<M>>) -> Node<M> {
296    Node(Kind::Grid {
297        columns: columns.max(1),
298        cells: cells.into_iter().collect(),
299    })
300}
301
302pub fn button<M>(content: Node<M>, on_press: M) -> Node<M> {
303    Node(Kind::Button {
304        content: Box::new(content),
305        on_press,
306        on_secondary: None,
307        prominent: false,
308    })
309}
310
311/// A row of choices, one of them current: workspaces, output devices. Each option sends its
312/// message when clicked. The host marks the current option and moves the mark smoothly when
313/// `current` changes.
314pub fn switcher<M>(
315    options: impl IntoIterator<Item = (Node<M>, M)>,
316    current: Option<usize>,
317) -> Node<M> {
318    Node(Kind::Switcher {
319        options: options.into_iter().collect(),
320        current,
321    })
322}
323
324/// Keeps gulping, like a bottle being poured: the host swells `content` gently and steadily for as
325/// long as it's on screen. For something filling up, like a charging battery's percent.
326pub fn gulp<M>(content: Node<M>) -> Node<M> {
327    Node(Kind::Gulp(Box::new(content)))
328}
329
330/// A thin horizontal line between groups.
331pub fn separator<M>() -> Node<M> {
332    Node(Kind::Separator)
333}
334
335/// Takes up a grid cell, or nothing elsewhere.
336pub fn empty<M>() -> Node<M> {
337    Node(Kind::Empty)
338}
339
340/// A row in a popover list: a network, a device, a window. Give it a [`subtitle`](Node::subtitle),
341/// a [`leading`](Node::leading) icon, a [`trailing`](Node::trailing) node at the far end (a lock,
342/// an info button), mark the one in use with [`selected`](Node::selected), and make it clickable
343/// with [`on_press`](Node::on_press). A long title fades out at the end rather than wrapping.
344pub fn list_item<M>(title: impl Into<String>) -> Node<M> {
345    Node(Kind::ListItem {
346        title: title.into(),
347        subtitle: None,
348        leading: None,
349        trailing: None,
350        selected: false,
351        on_press: None,
352        on_secondary: None,
353    })
354}
355
356/// Rows stacked closely, usually [`list_item`]s. In a popover, a long list scrolls instead of
357/// growing past a few rows.
358pub fn list<M>(rows: impl IntoIterator<Item = Node<M>>) -> Node<M> {
359    Node(Kind::List(rows.into_iter().collect()))
360}
361
362/// A small caption over a group in a popover ("Output", "Health"), with an optional
363/// [`trailing`](Node::trailing) node at the far end.
364pub fn section<M>(title: impl Into<String>) -> Node<M> {
365    Node(Kind::Section {
366        title: title.into(),
367        trailing: None,
368    })
369}
370
371/// The title of a popover page. Sub-pages get a back button with [`on_back`](Node::on_back); a
372/// [`trailing`](Node::trailing) node sits at the far end (a switch, a count).
373pub fn header<M>(title: impl Into<String>) -> Node<M> {
374    Node(Kind::Header {
375        title: title.into(),
376        on_back: None,
377        trailing: None,
378    })
379}
380
381/// A named value on one line: the name dimmed at the start, the value at the end.
382pub fn detail<M>(name: impl Into<String>, value: Node<M>) -> Node<M> {
383    Node(Kind::Detail {
384        name: name.into(),
385        value: Box::new(value),
386    })
387}
388
389/// Takes the free space in a popover row, pushing what follows to the end. Nothing in the bar.
390pub fn fill<M>() -> Node<M> {
391    Node(Kind::Fill)
392}
393
394/// A value dragged along `range`: a volume. Sends `on_change` while dragging; add
395/// [`on_release`](Node::on_release) to act once it's let go, and [`step`](Node::step) to snap.
396pub fn slider<M: 'static>(
397    value: f32,
398    range: RangeInclusive<f32>,
399    on_change: impl Fn(f32) -> M + 'static,
400) -> Node<M> {
401    Node(Kind::Slider {
402        value,
403        range,
404        step: 1.0,
405        on_change: Box::new(on_change),
406        on_release: None,
407    })
408}
409
410/// An on/off switch. Sends `on_toggle` when clicked; the host slides it over.
411pub fn toggle<M>(on: bool, on_toggle: M) -> Node<M> {
412    Node(Kind::Toggle { on, on_toggle })
413}
414
415/// A bar filled to `fraction` (0.0 to 1.0), in its tone: a charge, a signal. The host eases it to
416/// a new value.
417pub fn meter<M>(fraction: f32) -> Node<M> {
418    Node(Kind::Meter {
419        fraction,
420        limit: None,
421        tone: None,
422    })
423}
424
425/// A field to type into, showing `value`. Sends `on_input` with the new text on every change.
426/// It takes the focus when it appears, and while it has it, keys stay in the popover; Esc still
427/// closes it. [`on_submit`](Node::on_submit) is sent on Enter. One per popover page. A
428/// [`Tone::Critical`] tone marks it as wrong.
429pub fn text_input<M: 'static>(
430    placeholder: impl Into<String>,
431    value: impl Into<String>,
432    on_input: impl Fn(String) -> M + 'static,
433) -> Node<M> {
434    Node(Kind::TextInput {
435        placeholder: placeholder.into(),
436        value: value.into(),
437        on_input: Box::new(on_input),
438        on_submit: None,
439        secret: false,
440        trailing: None,
441        tone: None,
442    })
443}
444
445/// Its text, large: the one number a popover is about, like a battery's charge.
446pub fn display<M>(content: Node<M>) -> Node<M> {
447    Node(Kind::Display(Box::new(content)))
448}
449
450/// Its content on a filled accent circle: today in a calendar.
451pub fn mark<M>(content: Node<M>) -> Node<M> {
452    Node(Kind::Mark(Box::new(content)))
453}
454
455/// A short code in a small outlined box: a keyboard layout's `US`.
456pub fn badge<M>(text: impl Into<String>) -> Node<M> {
457    Node(Kind::Badge {
458        text: text.into(),
459        tone: None,
460    })
461}
462
463/// Pages behind a row of tabs, one page showing at a time: a popover's Output, Input and Apps.
464/// The host draws the tabs, switches pages when one is clicked and starts on the first each time
465/// the popover opens, so the plugin keeps no state for it. In the bar, only the first page shows.
466pub fn tabs<M>(pages: impl IntoIterator<Item = (impl Into<String>, Node<M>)>) -> Node<M> {
467    Node(Kind::Tabs(
468        pages
469            .into_iter()
470            .map(|(name, page)| (name.into(), page))
471            .collect(),
472    ))
473}
474
475impl<M> Node<M> {
476    /// Sets the tone of this text, or of every text inside this node that has none yet.
477    pub fn tone(mut self, tone: Tone) -> Self {
478        self.set_tone(tone);
479        self
480    }
481
482    /// For an [`alternate`]: shows the option at `index` while the pointer is over the item,
483    /// whatever `current` is. Other nodes have nothing to show on hover and ignore it.
484    pub fn on_hover(mut self, index: usize) -> Self {
485        if let Kind::Alternate { hovered, .. } = &mut self.0 {
486            *hovered = Some(index);
487        }
488        self
489    }
490
491    /// For a [`button`] or [`list_item`]: sent on a secondary (right) click on it. Other nodes
492    /// ignore it.
493    pub fn on_secondary(mut self, message: M) -> Self {
494        if let Kind::Button { on_secondary, .. } | Kind::ListItem { on_secondary, .. } = &mut self.0
495        {
496            *on_secondary = Some(message);
497        }
498        self
499    }
500
501    /// For a [`button`]: the main action of its page, filled with the accent. Other nodes ignore
502    /// it.
503    pub fn prominent(mut self) -> Self {
504        if let Kind::Button { prominent, .. } = &mut self.0 {
505            *prominent = true;
506        }
507        self
508    }
509
510    /// For a [`list_item`]: sent on a click on it. Other nodes ignore it.
511    pub fn on_press(mut self, message: M) -> Self {
512        if let Kind::ListItem { on_press, .. } = &mut self.0 {
513            *on_press = Some(message);
514        }
515        self
516    }
517
518    /// For a [`list_item`]: a second, dimmer line under the title. Other nodes ignore it.
519    pub fn subtitle(mut self, text: impl Into<String>) -> Self {
520        if let Kind::ListItem { subtitle, .. } = &mut self.0 {
521            *subtitle = Some(text.into());
522        }
523        self
524    }
525
526    /// For a [`list_item`]: an icon before the title. Other nodes ignore it.
527    pub fn leading(mut self, node: Node<M>) -> Self {
528        if let Kind::ListItem { leading, .. } = &mut self.0 {
529            *leading = Some(Box::new(node));
530        }
531        self
532    }
533
534    /// For a [`list_item`], [`section`], [`header`] or [`text_input`]: a node at the far end, like
535    /// a lock or an info button. Other nodes ignore it.
536    pub fn trailing(mut self, node: Node<M>) -> Self {
537        if let Kind::ListItem { trailing, .. }
538        | Kind::Section { trailing, .. }
539        | Kind::Header { trailing, .. }
540        | Kind::TextInput { trailing, .. } = &mut self.0
541        {
542            *trailing = Some(Box::new(node));
543        }
544        self
545    }
546
547    /// For a [`list_item`]: marks it as the one in use. Other nodes ignore it.
548    pub fn selected(mut self, yes: bool) -> Self {
549        if let Kind::ListItem { selected, .. } = &mut self.0 {
550            *selected = yes;
551        }
552        self
553    }
554
555    /// For a [`header`]: shows a back button that sends `message`. Other nodes ignore it.
556    pub fn on_back(mut self, message: M) -> Self {
557        if let Kind::Header { on_back, .. } = &mut self.0 {
558            *on_back = Some(message);
559        }
560        self
561    }
562
563    /// For a [`slider`]: the step its value snaps to (1 by default). Other nodes ignore it.
564    pub fn step(mut self, by: f32) -> Self {
565        if let Kind::Slider { step, .. } = &mut self.0 {
566            *step = by;
567        }
568        self
569    }
570
571    /// For a [`slider`]: sent when it's let go. Other nodes ignore it.
572    pub fn on_release(mut self, message: M) -> Self {
573        if let Kind::Slider { on_release, .. } = &mut self.0 {
574            *on_release = Some(message);
575        }
576        self
577    }
578
579    /// For a [`text_input`]: sent on Enter. Other nodes ignore it.
580    pub fn on_submit(mut self, message: M) -> Self {
581        if let Kind::TextInput { on_submit, .. } = &mut self.0 {
582            *on_submit = Some(message);
583        }
584        self
585    }
586
587    /// For a [`text_input`]: hides what's typed, like a password. Other nodes ignore it.
588    pub fn secret(mut self, yes: bool) -> Self {
589        if let Kind::TextInput { secret, .. } = &mut self.0 {
590            *secret = yes;
591        }
592        self
593    }
594
595    /// For a [`meter`]: marks a limit along it (0.0 to 1.0). Other nodes ignore it.
596    pub fn limit(mut self, at: f32) -> Self {
597        if let Kind::Meter { limit, .. } = &mut self.0 {
598            *limit = Some(at);
599        }
600        self
601    }
602
603    fn set_tone(&mut self, new: Tone) {
604        match &mut self.0 {
605            Kind::Label { tone, .. }
606            | Kind::Heading { tone, .. }
607            | Kind::Icon { tone, .. }
608            | Kind::AppIcon { tone, .. }
609            | Kind::Image { tone, .. }
610            | Kind::Counter { tone, .. }
611            | Kind::Meter { tone, .. }
612            | Kind::TextInput { tone, .. }
613            | Kind::Badge { tone, .. } => {
614                tone.get_or_insert(new);
615            }
616            Kind::Alternate {
617                options: children, ..
618            }
619            | Kind::Row(children)
620            | Kind::Column(children)
621            | Kind::Grid {
622                cells: children, ..
623            }
624            | Kind::List(children) => {
625                children.iter_mut().for_each(|child| child.set_tone(new));
626            }
627            Kind::Button { content, .. }
628            | Kind::Gulp(content)
629            | Kind::Display(content)
630            | Kind::Mark(content)
631            | Kind::Detail { value: content, .. } => content.set_tone(new),
632            Kind::ListItem {
633                leading, trailing, ..
634            } => {
635                leading
636                    .iter_mut()
637                    .chain(trailing.iter_mut())
638                    .for_each(|child| child.set_tone(new));
639            }
640            Kind::Tabs(pages) => {
641                for (_, page) in pages {
642                    page.set_tone(new);
643                }
644            }
645            Kind::Switcher { options, .. } => {
646                options
647                    .iter_mut()
648                    .for_each(|(child, _)| child.set_tone(new));
649            }
650            Kind::Separator
651            | Kind::Empty
652            | Kind::Section { .. }
653            | Kind::Header { .. }
654            | Kind::Fill
655            | Kind::Slider { .. }
656            | Kind::Toggle { .. } => {}
657        }
658    }
659
660    /// Converts every message in this node. Sliders and text fields keep `f` to convert the
661    /// messages they make later, so it must be `Clone + 'static`, like a function or a closure
662    /// that captures nothing.
663    pub fn map<N: 'static>(self, f: &(impl Fn(M) -> N + Clone + 'static)) -> Node<N>
664    where
665        M: 'static,
666    {
667        let map_all = |nodes: Vec<Node<M>>| nodes.into_iter().map(|node| node.map(f)).collect();
668        let map_box = |node: Option<Box<Node<M>>>| node.map(|node| Box::new(node.map(f)));
669
670        Node(match self.0 {
671            Kind::Label { text, tone } => Kind::Label { text, tone },
672            Kind::Heading { text, tone } => Kind::Heading { text, tone },
673            Kind::Icon { icon, tone } => Kind::Icon { icon, tone },
674            Kind::AppIcon { app_id, tone } => Kind::AppIcon { app_id, tone },
675            Kind::Image { file, tint, tone } => Kind::Image { file, tint, tone },
676            Kind::Counter {
677                value,
678                range,
679                unit,
680                tone,
681            } => Kind::Counter {
682                value,
683                range,
684                unit,
685                tone,
686            },
687            Kind::Alternate {
688                options,
689                current,
690                hovered,
691                grows,
692            } => Kind::Alternate {
693                options: map_all(options),
694                current,
695                hovered,
696                grows,
697            },
698            Kind::Row(children) => Kind::Row(map_all(children)),
699            Kind::Column(children) => Kind::Column(map_all(children)),
700            Kind::Grid { columns, cells } => Kind::Grid {
701                columns,
702                cells: map_all(cells),
703            },
704            Kind::Button {
705                content,
706                on_press,
707                on_secondary,
708                prominent,
709            } => Kind::Button {
710                content: Box::new(content.map(f)),
711                on_press: f(on_press),
712                on_secondary: on_secondary.map(f),
713                prominent,
714            },
715            Kind::Switcher { options, current } => Kind::Switcher {
716                options: options
717                    .into_iter()
718                    .map(|(node, message)| (node.map(f), f(message)))
719                    .collect(),
720                current,
721            },
722            Kind::Gulp(content) => Kind::Gulp(Box::new(content.map(f))),
723            Kind::Separator => Kind::Separator,
724            Kind::Empty => Kind::Empty,
725            Kind::ListItem {
726                title,
727                subtitle,
728                leading,
729                trailing,
730                selected,
731                on_press,
732                on_secondary,
733            } => Kind::ListItem {
734                title,
735                subtitle,
736                leading: map_box(leading),
737                trailing: map_box(trailing),
738                selected,
739                on_press: on_press.map(f),
740                on_secondary: on_secondary.map(f),
741            },
742            Kind::List(rows) => Kind::List(map_all(rows)),
743            Kind::Section { title, trailing } => Kind::Section {
744                title,
745                trailing: map_box(trailing),
746            },
747            Kind::Header {
748                title,
749                on_back,
750                trailing,
751            } => Kind::Header {
752                title,
753                on_back: on_back.map(f),
754                trailing: map_box(trailing),
755            },
756            Kind::Detail { name, value } => Kind::Detail {
757                name,
758                value: Box::new(value.map(f)),
759            },
760            Kind::Fill => Kind::Fill,
761            Kind::Slider {
762                value,
763                range,
764                step,
765                on_change,
766                on_release,
767            } => {
768                let g = f.clone();
769                Kind::Slider {
770                    value,
771                    range,
772                    step,
773                    on_change: Box::new(move |value| g(on_change(value))),
774                    on_release: on_release.map(f),
775                }
776            }
777            Kind::Toggle { on, on_toggle } => Kind::Toggle {
778                on,
779                on_toggle: f(on_toggle),
780            },
781            Kind::Meter {
782                fraction,
783                limit,
784                tone,
785            } => Kind::Meter {
786                fraction,
787                limit,
788                tone,
789            },
790            Kind::TextInput {
791                placeholder,
792                value,
793                on_input,
794                on_submit,
795                secret,
796                trailing,
797                tone,
798            } => {
799                let g = f.clone();
800                Kind::TextInput {
801                    placeholder,
802                    value,
803                    on_input: Box::new(move |text| g(on_input(text))),
804                    on_submit: on_submit.map(f),
805                    secret,
806                    trailing: map_box(trailing),
807                    tone,
808                }
809            }
810            Kind::Display(content) => Kind::Display(Box::new(content.map(f))),
811            Kind::Mark(content) => Kind::Mark(Box::new(content.map(f))),
812            Kind::Badge { text, tone } => Kind::Badge { text, tone },
813            Kind::Tabs(pages) => Kind::Tabs(
814                pages
815                    .into_iter()
816                    .map(|(name, page)| (name, page.map(f)))
817                    .collect(),
818            ),
819        })
820    }
821
822    #[doc(hidden)]
823    pub fn into_kind(self) -> Kind<M> {
824        self.0
825    }
826
827    #[doc(hidden)]
828    pub fn kind(&self) -> &Kind<M> {
829        &self.0
830    }
831}
832
833/// What a widget shows in the bar.
834pub struct Item<M> {
835    content: Option<Node<M>>,
836    on_press: Option<M>,
837    on_secondary: Option<M>,
838    on_scroll: Option<(M, M)>,
839}
840
841impl<M> Item<M> {
842    pub fn new(content: Node<M>) -> Self {
843        Self {
844            content: Some(content),
845            on_press: None,
846            on_secondary: None,
847            on_scroll: None,
848        }
849    }
850
851    /// Takes no space in the bar. For widgets with nothing to show, like a battery on a desktop.
852    pub fn hidden() -> Self {
853        Self {
854            content: None,
855            on_press: None,
856            on_secondary: None,
857            on_scroll: None,
858        }
859    }
860
861    /// Sent on a primary click. Replaces the default of toggling the popover.
862    pub fn on_press(mut self, message: M) -> Self {
863        self.on_press = Some(message);
864        self
865    }
866
867    /// Sent on a secondary (right) click.
868    pub fn on_secondary(mut self, message: M) -> Self {
869        self.on_secondary = Some(message);
870        self
871    }
872
873    /// Sent once per scroll step over the item, `up` or `down`. The host turns smooth touchpad
874    /// scrolling into steps.
875    pub fn on_scroll(mut self, up: M, down: M) -> Self {
876        self.on_scroll = Some((up, down));
877        self
878    }
879
880    pub fn map<N: 'static>(self, f: &(impl Fn(M) -> N + Clone + 'static)) -> Item<N>
881    where
882        M: 'static,
883    {
884        Item {
885            content: self.content.map(|content| content.map(f)),
886            on_press: self.on_press.map(f),
887            on_secondary: self.on_secondary.map(f),
888            on_scroll: self.on_scroll.map(|(up, down)| (f(up), f(down))),
889        }
890    }
891
892    #[doc(hidden)]
893    pub fn into_parts(self) -> ItemParts<M> {
894        ItemParts {
895            content: self.content,
896            on_press: self.on_press,
897            on_secondary: self.on_secondary,
898            on_scroll: self.on_scroll,
899        }
900    }
901}
902
903#[doc(hidden)]
904pub struct ItemParts<M> {
905    pub content: Option<Node<M>>,
906    pub on_press: Option<M>,
907    pub on_secondary: Option<M>,
908    /// Up, then down.
909    pub on_scroll: Option<(M, M)>,
910}
911
912/// The contents of a popover. The host draws the surface, its background and its edges, and lays
913/// every popover out at the same width.
914pub struct Panel<M> {
915    content: Node<M>,
916    on_open: Option<M>,
917    on_close: Option<M>,
918}
919
920impl<M> Panel<M> {
921    pub fn new(content: Node<M>) -> Self {
922        Self {
923            content,
924            on_open: None,
925            on_close: None,
926        }
927    }
928
929    /// Sent each time the popover opens: start a scan, show the first page.
930    pub fn on_open(mut self, message: M) -> Self {
931        self.on_open = Some(message);
932        self
933    }
934
935    /// Sent each time the popover starts closing: stop listening, forget what was typed.
936    pub fn on_close(mut self, message: M) -> Self {
937        self.on_close = Some(message);
938        self
939    }
940
941    pub fn map<N: 'static>(self, f: &(impl Fn(M) -> N + Clone + 'static)) -> Panel<N>
942    where
943        M: 'static,
944    {
945        Panel {
946            content: self.content.map(f),
947            on_open: self.on_open.map(f),
948            on_close: self.on_close.map(f),
949        }
950    }
951
952    #[doc(hidden)]
953    pub fn into_node(self) -> Node<M> {
954        self.content
955    }
956
957    #[doc(hidden)]
958    pub fn on_open_message(self) -> Option<M> {
959        self.on_open
960    }
961
962    #[doc(hidden)]
963    pub fn on_close_message(self) -> Option<M> {
964        self.on_close
965    }
966}
967
968/// How much a [`Notice`] matters. The host decides what that looks like.
969#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
970pub enum Urgency {
971    /// Background news: a download finished.
972    Low,
973    #[default]
974    Normal,
975    /// Needs attention now: a battery about to die.
976    Critical,
977}
978
979/// A notification to show on screen, from [`Widget::notices`](crate::Widget::notices).
980///
981/// It says what the notification is, not how it looks: the host lays out the notification, places it and
982/// animates it in and out. Its `key` is how the host tells notices apart from one view to the
983/// next: a notice that keeps its key and changes its text is updated in place, and one whose key
984/// is gone from the list leaves the screen.
985pub struct Notice<M> {
986    key: u64,
987    summary: String,
988    app: Option<String>,
989    app_icon: Option<String>,
990    body: Option<String>,
991    urgency: Urgency,
992    actions: Vec<(String, M)>,
993    on_press: Option<M>,
994    on_dismiss: Option<M>,
995    expires: Option<(std::time::Duration, M)>,
996    reply: Option<Reply<M>>,
997}
998
999/// A field on a [`Notice`] to type an answer into, like a reply to a message.
1000///
1001/// While a notice shows one, the host takes the keyboard, so show it only once asked to (after
1002/// a click on a Reply button, say), and drop it once the answer is sent or cancelled. The
1003/// notice doesn't expire meanwhile.
1004pub struct Reply<M> {
1005    placeholder: String,
1006    value: String,
1007    on_input: std::rc::Rc<dyn Fn(String) -> M>,
1008    on_submit: Option<M>,
1009    on_cancel: Option<M>,
1010}
1011
1012impl<M> Reply<M> {
1013    /// Shows `value`, and sends `on_input` with the new text on every change.
1014    pub fn new(
1015        placeholder: impl Into<String>,
1016        value: impl Into<String>,
1017        on_input: impl Fn(String) -> M + 'static,
1018    ) -> Self {
1019        Self {
1020            placeholder: placeholder.into(),
1021            value: value.into(),
1022            on_input: std::rc::Rc::new(on_input),
1023            on_submit: None,
1024            on_cancel: None,
1025        }
1026    }
1027
1028    /// Sent on Enter, or a click on the field's send button.
1029    pub fn on_submit(mut self, message: M) -> Self {
1030        self.on_submit = Some(message);
1031        self
1032    }
1033
1034    /// Sent on Esc.
1035    pub fn on_cancel(mut self, message: M) -> Self {
1036        self.on_cancel = Some(message);
1037        self
1038    }
1039
1040    pub fn map<N: 'static>(self, f: &(impl Fn(M) -> N + Clone + 'static)) -> Reply<N>
1041    where
1042        M: 'static,
1043    {
1044        let g = f.clone();
1045        let on_input = self.on_input;
1046        Reply {
1047            placeholder: self.placeholder,
1048            value: self.value,
1049            on_input: std::rc::Rc::new(move |text| g(on_input(text))),
1050            on_submit: self.on_submit.map(f),
1051            on_cancel: self.on_cancel.map(f),
1052        }
1053    }
1054
1055    #[doc(hidden)]
1056    pub fn into_parts(self) -> ReplyParts<M> {
1057        ReplyParts {
1058            placeholder: self.placeholder,
1059            value: self.value,
1060            on_input: self.on_input,
1061            on_submit: self.on_submit,
1062            on_cancel: self.on_cancel,
1063        }
1064    }
1065}
1066
1067#[doc(hidden)]
1068#[derive(Clone)]
1069pub struct ReplyParts<M> {
1070    pub placeholder: String,
1071    pub value: String,
1072    pub on_input: std::rc::Rc<dyn Fn(String) -> M>,
1073    pub on_submit: Option<M>,
1074    pub on_cancel: Option<M>,
1075}
1076
1077impl<M> Notice<M> {
1078    pub fn new(key: u64, summary: impl Into<String>) -> Self {
1079        Self {
1080            key,
1081            summary: summary.into(),
1082            app: None,
1083            app_icon: None,
1084            body: None,
1085            urgency: Urgency::Normal,
1086            actions: Vec::new(),
1087            on_press: None,
1088            on_dismiss: None,
1089            expires: None,
1090            reply: None,
1091        }
1092    }
1093
1094    /// The name of the app it comes from.
1095    pub fn app(mut self, name: impl Into<String>) -> Self {
1096        self.app = Some(name.into());
1097        self
1098    }
1099
1100    /// The app's icon, by app id, like [`app_icon`].
1101    pub fn app_icon(mut self, app_id: impl Into<String>) -> Self {
1102        self.app_icon = Some(app_id.into());
1103        self
1104    }
1105
1106    /// Plain text under the summary. The host wraps it and cuts it off after a few lines.
1107    pub fn body(mut self, text: impl Into<String>) -> Self {
1108        self.body = Some(text.into());
1109        self
1110    }
1111
1112    pub fn urgency(mut self, urgency: Urgency) -> Self {
1113        self.urgency = urgency;
1114        self
1115    }
1116
1117    /// A button on the notification that sends `message`. The first one is the main action.
1118    pub fn action(mut self, label: impl Into<String>, message: M) -> Self {
1119        self.actions.push((label.into(), message));
1120        self
1121    }
1122
1123    /// Sent on a click on the notification itself, outside its buttons.
1124    pub fn on_press(mut self, message: M) -> Self {
1125        self.on_press = Some(message);
1126        self
1127    }
1128
1129    /// Sent when its close button is clicked. Without it, it has none.
1130    pub fn on_dismiss(mut self, message: M) -> Self {
1131        self.on_dismiss = Some(message);
1132        self
1133    }
1134
1135    /// Sent once it has been on screen for `after`. The host doesn't count while the pointer is
1136    /// on it or it's waiting to be shown. Without it, it stays
1137    /// until it leaves the list.
1138    pub fn expires(mut self, after: std::time::Duration, message: M) -> Self {
1139        self.expires = Some((after, message));
1140        self
1141    }
1142
1143    /// A field to type an answer into ([`Reply`]).
1144    pub fn reply(mut self, reply: Reply<M>) -> Self {
1145        self.reply = Some(reply);
1146        self
1147    }
1148
1149    pub fn map<N: 'static>(self, f: &(impl Fn(M) -> N + Clone + 'static)) -> Notice<N>
1150    where
1151        M: 'static,
1152    {
1153        Notice {
1154            key: self.key,
1155            summary: self.summary,
1156            app: self.app,
1157            app_icon: self.app_icon,
1158            body: self.body,
1159            urgency: self.urgency,
1160            actions: self
1161                .actions
1162                .into_iter()
1163                .map(|(label, message)| (label, f(message)))
1164                .collect(),
1165            on_press: self.on_press.map(f),
1166            on_dismiss: self.on_dismiss.map(f),
1167            expires: self.expires.map(|(after, message)| (after, f(message))),
1168            reply: self.reply.map(|reply| reply.map(f)),
1169        }
1170    }
1171
1172    pub fn key(&self) -> u64 {
1173        self.key
1174    }
1175
1176    #[doc(hidden)]
1177    pub fn into_parts(self) -> NoticeParts<M> {
1178        NoticeParts {
1179            key: self.key,
1180            summary: self.summary,
1181            app: self.app,
1182            app_icon: self.app_icon,
1183            body: self.body,
1184            urgency: self.urgency,
1185            actions: self.actions,
1186            on_press: self.on_press,
1187            on_dismiss: self.on_dismiss,
1188            expires: self.expires,
1189            reply: self.reply.map(Reply::into_parts),
1190        }
1191    }
1192}
1193
1194#[doc(hidden)]
1195#[derive(Clone)]
1196pub struct NoticeParts<M> {
1197    pub key: u64,
1198    pub summary: String,
1199    pub app: Option<String>,
1200    pub app_icon: Option<String>,
1201    pub body: Option<String>,
1202    pub urgency: Urgency,
1203    pub actions: Vec<(String, M)>,
1204    pub on_press: Option<M>,
1205    pub on_dismiss: Option<M>,
1206    pub expires: Option<(std::time::Duration, M)>,
1207    pub reply: Option<ReplyParts<M>>,
1208}
1209
1210#[cfg(test)]
1211mod tests {
1212    use super::*;
1213
1214    #[test]
1215    fn map_converts_every_message() {
1216        let node = row([
1217            button(label("a"), 1).on_secondary(4),
1218            column([button(label("b"), 2), label("c")]),
1219            switcher([(label("d"), 3)], Some(0)),
1220        ]);
1221
1222        let Kind::Row(children) = node.map(&|n: i32| n * 10).into_kind() else {
1223            panic!("row became something else");
1224        };
1225        let mut messages = Vec::new();
1226        collect(children, &mut messages);
1227        assert_eq!(messages, [10, 40, 20, 30]);
1228    }
1229
1230    fn collect(nodes: Vec<Node<i32>>, out: &mut Vec<i32>) {
1231        for node in nodes {
1232            match node.into_kind() {
1233                Kind::Button {
1234                    on_press,
1235                    on_secondary,
1236                    ..
1237                } => {
1238                    out.push(on_press);
1239                    out.extend(on_secondary);
1240                }
1241                Kind::Switcher { options, .. } => {
1242                    out.extend(options.into_iter().map(|(_, message)| message))
1243                }
1244                Kind::Row(children) | Kind::Column(children) => collect(children, out),
1245                _ => {}
1246            }
1247        }
1248    }
1249
1250    #[test]
1251    fn hover_shows_the_second_option() {
1252        let node: Node<()> = hover(label("a"), label("b"));
1253        assert!(matches!(
1254            node.into_kind(),
1255            Kind::Alternate {
1256                current: 0,
1257                hovered: Some(1),
1258                ..
1259            }
1260        ));
1261        let node: Node<()> = label("a").on_hover(1);
1262        assert!(matches!(node.into_kind(), Kind::Label { .. }));
1263    }
1264
1265    #[test]
1266    fn tone_fills_only_unset_text() {
1267        let node: Node<()> =
1268            row([label("a").tone(Tone::Accent), label("b"), icon(Icon::Wired)]).tone(Tone::Dim);
1269        let Kind::Row(children) = node.into_kind() else {
1270            unreachable!()
1271        };
1272        let tones: Vec<_> = children
1273            .into_iter()
1274            .map(|child| match child.into_kind() {
1275                Kind::Label { tone, .. } | Kind::Icon { tone, .. } => tone,
1276                _ => None,
1277            })
1278            .collect();
1279        assert_eq!(
1280            tones,
1281            [Some(Tone::Accent), Some(Tone::Dim), Some(Tone::Dim)]
1282        );
1283    }
1284
1285    #[test]
1286    fn map_reaches_new_nodes_and_their_closures() {
1287        let node = column([
1288            header("h").on_back(1).trailing(toggle(true, 2)),
1289            list([list_item("a")
1290                .on_press(3)
1291                .on_secondary(4)
1292                .trailing(button(icon(Icon::Info), 5))]),
1293            slider(0.5, 0.0..=1.0, |v| (v * 10.0) as i32).on_release(6),
1294            text_input("p", "v", |text: String| text.len() as i32).on_submit(7),
1295        ]);
1296        let Kind::Column(children) = node.map(&|n: i32| n * 10).into_kind() else {
1297            panic!("column became something else");
1298        };
1299        let mut children = children.into_iter().map(Node::into_kind);
1300        let Some(Kind::Header {
1301            on_back: Some(10),
1302            trailing: Some(trailing),
1303            ..
1304        }) = children.next()
1305        else {
1306            panic!("header lost its back message");
1307        };
1308        assert!(matches!(
1309            trailing.into_kind(),
1310            Kind::Toggle { on_toggle: 20, .. }
1311        ));
1312        let Some(Kind::List(rows)) = children.next() else {
1313            panic!("list lost");
1314        };
1315        let Some(Kind::ListItem {
1316            on_press: Some(30),
1317            on_secondary: Some(40),
1318            trailing: Some(trailing),
1319            ..
1320        }) = rows.into_iter().next().map(Node::into_kind)
1321        else {
1322            panic!("list item lost its messages");
1323        };
1324        assert!(matches!(
1325            trailing.into_kind(),
1326            Kind::Button { on_press: 50, .. }
1327        ));
1328        let Some(Kind::Slider {
1329            on_change,
1330            on_release: Some(60),
1331            ..
1332        }) = children.next()
1333        else {
1334            panic!("slider lost");
1335        };
1336        assert_eq!(on_change(0.3), 30);
1337        let Some(Kind::TextInput {
1338            on_input,
1339            on_submit: Some(70),
1340            ..
1341        }) = children.next()
1342        else {
1343            panic!("text input lost");
1344        };
1345        assert_eq!(on_input("abcd".into()), 40);
1346    }
1347
1348    #[test]
1349    fn tabs_map_and_tone_every_page() {
1350        let node = tabs([("a", button(label("x"), 1)), ("b", button(label("y"), 2))])
1351            .tone(Tone::Dim)
1352            .map(&|n: i32| n * 10);
1353        let Kind::Tabs(pages) = node.into_kind() else {
1354            panic!("tabs became something else");
1355        };
1356        let names: Vec<_> = pages.iter().map(|(name, _)| name.as_str()).collect();
1357        assert_eq!(names, ["a", "b"]);
1358        let mut messages = Vec::new();
1359        for (_, page) in pages {
1360            let Kind::Button {
1361                content, on_press, ..
1362            } = page.into_kind()
1363            else {
1364                panic!("page lost its button");
1365            };
1366            assert!(matches!(
1367                content.into_kind(),
1368                Kind::Label {
1369                    tone: Some(Tone::Dim),
1370                    ..
1371                }
1372            ));
1373            messages.push(on_press);
1374        }
1375        assert_eq!(messages, [10, 20]);
1376    }
1377
1378    #[test]
1379    fn notice_map_reaches_every_message() {
1380        let notice = Notice::new(7, "s")
1381            .action("a", 1)
1382            .action("b", 2)
1383            .on_press(3)
1384            .on_dismiss(4)
1385            .expires(std::time::Duration::from_secs(5), 5)
1386            .reply(
1387                Reply::new("p", "v", |text: String| text.len() as i32)
1388                    .on_submit(6)
1389                    .on_cancel(7),
1390            )
1391            .map(&|n: i32| n * 10);
1392        assert_eq!(notice.key(), 7);
1393        let parts = notice.into_parts();
1394        let actions: Vec<_> = parts.actions.into_iter().map(|(_, m)| m).collect();
1395        assert_eq!(actions, [10, 20]);
1396        assert_eq!(parts.on_press, Some(30));
1397        assert_eq!(parts.on_dismiss, Some(40));
1398        assert_eq!(parts.expires.map(|(_, m)| m), Some(50));
1399        let reply = parts.reply.unwrap();
1400        assert_eq!((reply.on_input)("abc".into()), 30);
1401        assert_eq!((reply.on_submit, reply.on_cancel), (Some(60), Some(70)));
1402    }
1403
1404    #[test]
1405    fn modifiers_only_touch_their_nodes() {
1406        let node: Node<i32> = label("a").trailing(label("b")).on_press(1).selected(true);
1407        assert!(matches!(node.into_kind(), Kind::Label { .. }));
1408        let node: Node<i32> = button(label("a"), 1).on_press(2).prominent();
1409        assert!(matches!(
1410            node.into_kind(),
1411            Kind::Button {
1412                on_press: 1,
1413                prominent: true,
1414                ..
1415            }
1416        ));
1417    }
1418
1419    #[test]
1420    fn tone_reaches_details_and_row_icons() {
1421        let node: Node<()> = column([
1422            detail("n", label("v")),
1423            list_item("t").leading(icon(Icon::Lock)),
1424        ])
1425        .tone(Tone::Good);
1426        let Kind::Column(children) = node.into_kind() else {
1427            unreachable!()
1428        };
1429        let mut children = children.into_iter().map(Node::into_kind);
1430        let Some(Kind::Detail { value, .. }) = children.next() else {
1431            unreachable!()
1432        };
1433        assert!(matches!(
1434            value.into_kind(),
1435            Kind::Label {
1436                tone: Some(Tone::Good),
1437                ..
1438            }
1439        ));
1440        let Some(Kind::ListItem {
1441            leading: Some(leading),
1442            ..
1443        }) = children.next()
1444        else {
1445            unreachable!()
1446        };
1447        assert!(matches!(
1448            leading.into_kind(),
1449            Kind::Icon {
1450                tone: Some(Tone::Good),
1451                ..
1452            }
1453        ));
1454    }
1455}