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