1use std::ops::RangeInclusive;
8
9#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
11pub enum Tone {
12 #[default]
13 Normal,
14 Dim,
16 Accent,
18 Warning,
19 Critical,
20 Good,
22}
23
24#[derive(Debug, Clone, Copy, PartialEq)]
27pub enum Icon {
28 Volume { level: f32, muted: bool },
30 Wifi { strength: Option<f32> },
32 Wired,
34 Battery { charge: f32, charging: bool },
36 Lock,
38 Info,
40 Back,
42 Forward,
44 Check,
46 Refresh,
48 Close,
50 Reveal,
52 Bolt,
54 Plug,
56 Mic { muted: bool },
58 Speaker,
60 Headphones,
62 Display,
64 Bell { muted: bool },
66}
67
68pub 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 hovered: Option<usize>,
100 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 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 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 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
190pub fn heading<M>(text: impl Into<String>) -> Node<M> {
192 Node(Kind::Heading {
193 text: text.into(),
194 tone: None,
195 })
196}
197
198pub fn icon<M>(icon: Icon) -> Node<M> {
200 Node(Kind::Icon { icon, tone: None })
201}
202
203pub 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
213pub 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
225pub 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
238pub fn hover<M>(normal: Node<M>, hovered: Node<M>) -> Node<M> {
241 alternate([normal, hovered], 0).on_hover(1)
242}
243
244pub 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
257pub fn row<M>(children: impl IntoIterator<Item = Node<M>>) -> Node<M> {
259 Node(Kind::Row(children.into_iter().collect()))
260}
261
262pub fn column<M>(children: impl IntoIterator<Item = Node<M>>) -> Node<M> {
264 Node(Kind::Column(children.into_iter().collect()))
265}
266
267pub 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
284pub 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
297pub fn gulp<M>(content: Node<M>) -> Node<M> {
300 Node(Kind::Gulp(Box::new(content)))
301}
302
303pub fn separator<M>() -> Node<M> {
305 Node(Kind::Separator)
306}
307
308pub fn empty<M>() -> Node<M> {
310 Node(Kind::Empty)
311}
312
313pub 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
329pub fn list<M>(rows: impl IntoIterator<Item = Node<M>>) -> Node<M> {
332 Node(Kind::List(rows.into_iter().collect()))
333}
334
335pub fn section<M>(title: impl Into<String>) -> Node<M> {
338 Node(Kind::Section {
339 title: title.into(),
340 trailing: None,
341 })
342}
343
344pub 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
354pub 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
362pub fn fill<M>() -> Node<M> {
364 Node(Kind::Fill)
365}
366
367pub 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
383pub fn toggle<M>(on: bool, on_toggle: M) -> Node<M> {
385 Node(Kind::Toggle { on, on_toggle })
386}
387
388pub fn meter<M>(fraction: f32) -> Node<M> {
391 Node(Kind::Meter {
392 fraction,
393 limit: None,
394 tone: None,
395 })
396}
397
398pub 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
418pub fn display<M>(content: Node<M>) -> Node<M> {
420 Node(Kind::Display(Box::new(content)))
421}
422
423pub fn mark<M>(content: Node<M>) -> Node<M> {
425 Node(Kind::Mark(Box::new(content)))
426}
427
428pub fn badge<M>(text: impl Into<String>) -> Node<M> {
430 Node(Kind::Badge {
431 text: text.into(),
432 tone: None,
433 })
434}
435
436pub 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 pub fn tone(mut self, tone: Tone) -> Self {
451 self.set_tone(tone);
452 self
453 }
454
455 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 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 pub fn prominent(mut self) -> Self {
477 if let Kind::Button { prominent, .. } = &mut self.0 {
478 *prominent = true;
479 }
480 self
481 }
482
483 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 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 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 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 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 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 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 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 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 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 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 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
804pub 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 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 pub fn on_press(mut self, message: M) -> Self {
834 self.on_press = Some(message);
835 self
836 }
837
838 pub fn on_secondary(mut self, message: M) -> Self {
840 self.on_secondary = Some(message);
841 self
842 }
843
844 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 pub on_scroll: Option<(M, M)>,
881}
882
883pub 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 pub fn on_open(mut self, message: M) -> Self {
902 self.on_open = Some(message);
903 self
904 }
905
906 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#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
941pub enum Urgency {
942 Low,
944 #[default]
945 Normal,
946 Critical,
948}
949
950pub 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
970pub 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 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 pub fn on_submit(mut self, message: M) -> Self {
1001 self.on_submit = Some(message);
1002 self
1003 }
1004
1005 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 pub fn app(mut self, name: impl Into<String>) -> Self {
1067 self.app = Some(name.into());
1068 self
1069 }
1070
1071 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 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 pub fn action(mut self, label: impl Into<String>, message: M) -> Self {
1090 self.actions.push((label.into(), message));
1091 self
1092 }
1093
1094 pub fn on_press(mut self, message: M) -> Self {
1096 self.on_press = Some(message);
1097 self
1098 }
1099
1100 pub fn on_dismiss(mut self, message: M) -> Self {
1102 self.on_dismiss = Some(message);
1103 self
1104 }
1105
1106 pub fn expires(mut self, after: std::time::Duration, message: M) -> Self {
1110 self.expires = Some((after, message));
1111 self
1112 }
1113
1114 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}