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 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 hovered: Option<usize>,
105 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 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 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 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
195pub fn heading<M>(text: impl Into<String>) -> Node<M> {
197 Node(Kind::Heading {
198 text: text.into(),
199 tone: None,
200 })
201}
202
203pub fn icon<M>(icon: Icon) -> Node<M> {
205 Node(Kind::Icon { icon, tone: None })
206}
207
208pub 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
218pub fn image<M>(file: &'static [u8]) -> Node<M> {
223 Node(Kind::Image {
224 file,
225 tint: false,
226 tone: None,
227 })
228}
229
230pub fn symbol<M>(file: &'static [u8]) -> Node<M> {
233 Node(Kind::Image {
234 file,
235 tint: true,
236 tone: None,
237 })
238}
239
240pub 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
252pub 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
265pub fn hover<M>(normal: Node<M>, hovered: Node<M>) -> Node<M> {
268 alternate([normal, hovered], 0).on_hover(1)
269}
270
271pub 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
284pub fn row<M>(children: impl IntoIterator<Item = Node<M>>) -> Node<M> {
286 Node(Kind::Row(children.into_iter().collect()))
287}
288
289pub fn column<M>(children: impl IntoIterator<Item = Node<M>>) -> Node<M> {
291 Node(Kind::Column(children.into_iter().collect()))
292}
293
294pub 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
311pub 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
324pub fn gulp<M>(content: Node<M>) -> Node<M> {
327 Node(Kind::Gulp(Box::new(content)))
328}
329
330pub fn separator<M>() -> Node<M> {
332 Node(Kind::Separator)
333}
334
335pub fn empty<M>() -> Node<M> {
337 Node(Kind::Empty)
338}
339
340pub 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
356pub fn list<M>(rows: impl IntoIterator<Item = Node<M>>) -> Node<M> {
359 Node(Kind::List(rows.into_iter().collect()))
360}
361
362pub fn section<M>(title: impl Into<String>) -> Node<M> {
365 Node(Kind::Section {
366 title: title.into(),
367 trailing: None,
368 })
369}
370
371pub 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
381pub 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
389pub fn fill<M>() -> Node<M> {
391 Node(Kind::Fill)
392}
393
394pub 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
410pub fn toggle<M>(on: bool, on_toggle: M) -> Node<M> {
412 Node(Kind::Toggle { on, on_toggle })
413}
414
415pub fn meter<M>(fraction: f32) -> Node<M> {
418 Node(Kind::Meter {
419 fraction,
420 limit: None,
421 tone: None,
422 })
423}
424
425pub 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
445pub fn display<M>(content: Node<M>) -> Node<M> {
447 Node(Kind::Display(Box::new(content)))
448}
449
450pub fn mark<M>(content: Node<M>) -> Node<M> {
452 Node(Kind::Mark(Box::new(content)))
453}
454
455pub fn badge<M>(text: impl Into<String>) -> Node<M> {
457 Node(Kind::Badge {
458 text: text.into(),
459 tone: None,
460 })
461}
462
463pub 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 pub fn tone(mut self, tone: Tone) -> Self {
478 self.set_tone(tone);
479 self
480 }
481
482 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 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 pub fn prominent(mut self) -> Self {
504 if let Kind::Button { prominent, .. } = &mut self.0 {
505 *prominent = true;
506 }
507 self
508 }
509
510 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 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 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 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 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 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 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 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 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 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 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 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
833pub 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 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 pub fn on_press(mut self, message: M) -> Self {
863 self.on_press = Some(message);
864 self
865 }
866
867 pub fn on_secondary(mut self, message: M) -> Self {
869 self.on_secondary = Some(message);
870 self
871 }
872
873 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 pub on_scroll: Option<(M, M)>,
910}
911
912pub 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 pub fn on_open(mut self, message: M) -> Self {
931 self.on_open = Some(message);
932 self
933 }
934
935 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#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
970pub enum Urgency {
971 Low,
973 #[default]
974 Normal,
975 Critical,
977}
978
979pub 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
999pub 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 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 pub fn on_submit(mut self, message: M) -> Self {
1030 self.on_submit = Some(message);
1031 self
1032 }
1033
1034 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 pub fn app(mut self, name: impl Into<String>) -> Self {
1096 self.app = Some(name.into());
1097 self
1098 }
1099
1100 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 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 pub fn action(mut self, label: impl Into<String>, message: M) -> Self {
1119 self.actions.push((label.into(), message));
1120 self
1121 }
1122
1123 pub fn on_press(mut self, message: M) -> Self {
1125 self.on_press = Some(message);
1126 self
1127 }
1128
1129 pub fn on_dismiss(mut self, message: M) -> Self {
1131 self.on_dismiss = Some(message);
1132 self
1133 }
1134
1135 pub fn expires(mut self, after: std::time::Duration, message: M) -> Self {
1139 self.expires = Some((after, message));
1140 self
1141 }
1142
1143 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}