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 Media { playing: bool },
69 Play,
71 Pause,
73 Previous,
75 Next,
77}
78
79pub struct Node<M>(Kind<M>);
81
82#[doc(hidden)]
83pub enum Kind<M> {
84 Label {
85 text: String,
86 tone: Option<Tone>,
87 },
88 Heading {
89 text: String,
90 tone: Option<Tone>,
91 },
92 Icon {
93 icon: Icon,
94 tone: Option<Tone>,
95 },
96 AppIcon {
97 app_id: String,
98 tone: Option<Tone>,
99 },
100 Image {
101 file: &'static [u8],
102 tint: bool,
103 tone: Option<Tone>,
104 },
105 Sprite {
106 frames: &'static [&'static [u8]],
107 order: &'static [u8],
109 looping: bool,
111 tone: Option<Tone>,
112 },
113 Cover {
114 url: String,
115 fallback: Box<Node<M>>,
117 large: bool,
119 tone: Option<Tone>,
120 },
121 Counter {
122 value: i64,
123 range: RangeInclusive<i64>,
124 unit: String,
125 tone: Option<Tone>,
126 },
127 Alternate {
128 options: Vec<Node<M>>,
129 current: usize,
130 hovered: Option<usize>,
132 grows: bool,
134 },
135 Row(Vec<Node<M>>),
136 Column(Vec<Node<M>>),
137 Grid {
138 columns: usize,
139 cells: Vec<Node<M>>,
140 },
141 Button {
142 content: Box<Node<M>>,
143 on_press: M,
144 on_secondary: Option<M>,
145 prominent: bool,
147 },
148 Switcher {
149 options: Vec<(Node<M>, M)>,
150 current: Option<usize>,
151 },
152 Gulp(Box<Node<M>>),
153 Center(Box<Node<M>>),
155 Separator,
156 Empty,
157 ListItem {
158 title: String,
159 subtitle: Option<String>,
160 leading: Option<Box<Node<M>>>,
161 trailing: Option<Box<Node<M>>>,
162 selected: bool,
163 on_press: Option<M>,
164 on_secondary: Option<M>,
165 },
166 List(Vec<Node<M>>),
167 Section {
168 title: String,
169 trailing: Option<Box<Node<M>>>,
170 },
171 Header {
172 title: String,
173 on_back: Option<M>,
174 trailing: Option<Box<Node<M>>>,
175 },
176 Detail {
177 name: String,
178 value: Box<Node<M>>,
179 },
180 Fill,
181 Slider {
182 value: f32,
183 range: RangeInclusive<f32>,
184 step: f32,
185 on_change: Box<dyn Fn(f32) -> M>,
186 on_release: Option<M>,
187 },
188 Toggle {
189 on: bool,
190 on_toggle: M,
191 },
192 Meter {
193 fraction: f32,
194 limit: Option<f32>,
196 tone: Option<Tone>,
197 },
198 TextInput {
199 placeholder: String,
200 value: String,
201 on_input: Box<dyn Fn(String) -> M>,
202 on_submit: Option<M>,
203 secret: bool,
204 trailing: Option<Box<Node<M>>>,
205 tone: Option<Tone>,
206 },
207 Display(Box<Node<M>>),
208 Mark(Box<Node<M>>),
209 Badge {
210 text: String,
211 tone: Option<Tone>,
212 },
213 Tabs(Vec<(String, Node<M>)>),
215 Markdown {
216 text: String,
217 tone: Option<Tone>,
218 },
219 TextArea {
220 placeholder: String,
221 value: String,
222 on_input: Box<dyn Fn(String) -> M>,
223 on_submit: Option<M>,
224 trailing: Option<Box<Node<M>>>,
225 },
226 Scroll {
227 content: Box<Node<M>>,
228 bottom: bool,
230 },
231}
232
233pub fn label<M>(text: impl Into<String>) -> Node<M> {
234 Node(Kind::Label {
235 text: text.into(),
236 tone: None,
237 })
238}
239
240pub fn heading<M>(text: impl Into<String>) -> Node<M> {
242 Node(Kind::Heading {
243 text: text.into(),
244 tone: None,
245 })
246}
247
248pub fn icon<M>(icon: Icon) -> Node<M> {
250 Node(Kind::Icon { icon, tone: None })
251}
252
253pub fn app_icon<M>(app_id: impl Into<String>) -> Node<M> {
257 Node(Kind::AppIcon {
258 app_id: app_id.into(),
259 tone: None,
260 })
261}
262
263pub fn image<M>(file: &'static [u8]) -> Node<M> {
268 Node(Kind::Image {
269 file,
270 tint: false,
271 tone: None,
272 })
273}
274
275pub fn symbol<M>(file: &'static [u8]) -> Node<M> {
278 Node(Kind::Image {
279 file,
280 tint: true,
281 tone: None,
282 })
283}
284
285pub fn sprite<M>(frames: &'static [&'static [u8]], order: &'static [u8]) -> Node<M> {
293 Node(Kind::Sprite {
294 frames,
295 order,
296 looping: false,
297 tone: None,
298 })
299}
300
301pub fn cover<M>(url: impl Into<String>, fallback: Node<M>) -> Node<M> {
306 Node(Kind::Cover {
307 url: url.into(),
308 fallback: Box::new(fallback),
309 large: false,
310 tone: None,
311 })
312}
313
314pub fn counter<M>(value: i64, range: RangeInclusive<i64>, unit: impl Into<String>) -> Node<M> {
318 Node(Kind::Counter {
319 value,
320 range,
321 unit: unit.into(),
322 tone: None,
323 })
324}
325
326pub fn alternate<M>(options: impl IntoIterator<Item = Node<M>>, current: usize) -> Node<M> {
331 Node(Kind::Alternate {
332 options: options.into_iter().collect(),
333 current,
334 hovered: None,
335 grows: false,
336 })
337}
338
339pub fn hover<M>(normal: Node<M>, hovered: Node<M>) -> Node<M> {
342 alternate([normal, hovered], 0).on_hover(1)
343}
344
345pub fn expand<M>(collapsed: Node<M>, expanded: Node<M>) -> Node<M> {
350 Node(Kind::Alternate {
351 options: vec![collapsed, expanded],
352 current: 0,
353 hovered: Some(1),
354 grows: true,
355 })
356}
357
358pub fn row<M>(children: impl IntoIterator<Item = Node<M>>) -> Node<M> {
360 Node(Kind::Row(children.into_iter().collect()))
361}
362
363pub fn column<M>(children: impl IntoIterator<Item = Node<M>>) -> Node<M> {
365 Node(Kind::Column(children.into_iter().collect()))
366}
367
368pub fn center<M>(content: Node<M>) -> Node<M> {
372 Node(Kind::Center(Box::new(content)))
373}
374
375pub fn grid<M>(columns: usize, cells: impl IntoIterator<Item = Node<M>>) -> Node<M> {
377 Node(Kind::Grid {
378 columns: columns.max(1),
379 cells: cells.into_iter().collect(),
380 })
381}
382
383pub fn button<M>(content: Node<M>, on_press: M) -> Node<M> {
384 Node(Kind::Button {
385 content: Box::new(content),
386 on_press,
387 on_secondary: None,
388 prominent: false,
389 })
390}
391
392pub fn switcher<M>(
396 options: impl IntoIterator<Item = (Node<M>, M)>,
397 current: Option<usize>,
398) -> Node<M> {
399 Node(Kind::Switcher {
400 options: options.into_iter().collect(),
401 current,
402 })
403}
404
405pub fn gulp<M>(content: Node<M>) -> Node<M> {
408 Node(Kind::Gulp(Box::new(content)))
409}
410
411pub fn separator<M>() -> Node<M> {
413 Node(Kind::Separator)
414}
415
416pub fn empty<M>() -> Node<M> {
418 Node(Kind::Empty)
419}
420
421pub fn list_item<M>(title: impl Into<String>) -> Node<M> {
426 Node(Kind::ListItem {
427 title: title.into(),
428 subtitle: None,
429 leading: None,
430 trailing: None,
431 selected: false,
432 on_press: None,
433 on_secondary: None,
434 })
435}
436
437pub fn list<M>(rows: impl IntoIterator<Item = Node<M>>) -> Node<M> {
440 Node(Kind::List(rows.into_iter().collect()))
441}
442
443pub fn section<M>(title: impl Into<String>) -> Node<M> {
446 Node(Kind::Section {
447 title: title.into(),
448 trailing: None,
449 })
450}
451
452pub fn header<M>(title: impl Into<String>) -> Node<M> {
455 Node(Kind::Header {
456 title: title.into(),
457 on_back: None,
458 trailing: None,
459 })
460}
461
462pub fn detail<M>(name: impl Into<String>, value: Node<M>) -> Node<M> {
464 Node(Kind::Detail {
465 name: name.into(),
466 value: Box::new(value),
467 })
468}
469
470pub fn fill<M>() -> Node<M> {
472 Node(Kind::Fill)
473}
474
475pub fn slider<M: 'static>(
478 value: f32,
479 range: RangeInclusive<f32>,
480 on_change: impl Fn(f32) -> M + 'static,
481) -> Node<M> {
482 Node(Kind::Slider {
483 value,
484 range,
485 step: 1.0,
486 on_change: Box::new(on_change),
487 on_release: None,
488 })
489}
490
491pub fn toggle<M>(on: bool, on_toggle: M) -> Node<M> {
493 Node(Kind::Toggle { on, on_toggle })
494}
495
496pub fn meter<M>(fraction: f32) -> Node<M> {
499 Node(Kind::Meter {
500 fraction,
501 limit: None,
502 tone: None,
503 })
504}
505
506pub fn text_input<M: 'static>(
511 placeholder: impl Into<String>,
512 value: impl Into<String>,
513 on_input: impl Fn(String) -> M + 'static,
514) -> Node<M> {
515 Node(Kind::TextInput {
516 placeholder: placeholder.into(),
517 value: value.into(),
518 on_input: Box::new(on_input),
519 on_submit: None,
520 secret: false,
521 trailing: None,
522 tone: None,
523 })
524}
525
526pub fn text_area<M: 'static>(
531 placeholder: impl Into<String>,
532 value: impl Into<String>,
533 on_input: impl Fn(String) -> M + 'static,
534) -> Node<M> {
535 Node(Kind::TextArea {
536 placeholder: placeholder.into(),
537 value: value.into(),
538 on_input: Box::new(on_input),
539 on_submit: None,
540 trailing: None,
541 })
542}
543
544pub fn markdown<M>(text: impl Into<String>) -> Node<M> {
549 Node(Kind::Markdown {
550 text: text.into(),
551 tone: None,
552 })
553}
554
555pub fn scroll<M>(content: Node<M>) -> Node<M> {
559 Node(Kind::Scroll {
560 content: Box::new(content),
561 bottom: false,
562 })
563}
564
565pub fn display<M>(content: Node<M>) -> Node<M> {
567 Node(Kind::Display(Box::new(content)))
568}
569
570pub fn mark<M>(content: Node<M>) -> Node<M> {
572 Node(Kind::Mark(Box::new(content)))
573}
574
575pub fn badge<M>(text: impl Into<String>) -> Node<M> {
577 Node(Kind::Badge {
578 text: text.into(),
579 tone: None,
580 })
581}
582
583pub fn tabs<M>(pages: impl IntoIterator<Item = (impl Into<String>, Node<M>)>) -> Node<M> {
587 Node(Kind::Tabs(
588 pages
589 .into_iter()
590 .map(|(name, page)| (name.into(), page))
591 .collect(),
592 ))
593}
594
595impl<M> Node<M> {
596 pub fn tone(mut self, tone: Tone) -> Self {
598 self.set_tone(tone);
599 self
600 }
601
602 pub fn on_hover(mut self, index: usize) -> Self {
605 if let Kind::Alternate { hovered, .. } = &mut self.0 {
606 *hovered = Some(index);
607 }
608 self
609 }
610
611 pub fn on_secondary(mut self, message: M) -> Self {
614 if let Kind::Button { on_secondary, .. } | Kind::ListItem { on_secondary, .. } = &mut self.0
615 {
616 *on_secondary = Some(message);
617 }
618 self
619 }
620
621 pub fn prominent(mut self) -> Self {
624 if let Kind::Button { prominent, .. } = &mut self.0 {
625 *prominent = true;
626 }
627 self
628 }
629
630 pub fn on_press(mut self, message: M) -> Self {
632 if let Kind::ListItem { on_press, .. } = &mut self.0 {
633 *on_press = Some(message);
634 }
635 self
636 }
637
638 pub fn subtitle(mut self, text: impl Into<String>) -> Self {
640 if let Kind::ListItem { subtitle, .. } = &mut self.0 {
641 *subtitle = Some(text.into());
642 }
643 self
644 }
645
646 pub fn leading(mut self, node: Node<M>) -> Self {
648 if let Kind::ListItem { leading, .. } = &mut self.0 {
649 *leading = Some(Box::new(node));
650 }
651 self
652 }
653
654 pub fn trailing(mut self, node: Node<M>) -> Self {
657 if let Kind::ListItem { trailing, .. }
658 | Kind::Section { trailing, .. }
659 | Kind::Header { trailing, .. }
660 | Kind::TextInput { trailing, .. }
661 | Kind::TextArea { trailing, .. } = &mut self.0
662 {
663 *trailing = Some(Box::new(node));
664 }
665 self
666 }
667
668 pub fn selected(mut self, yes: bool) -> Self {
670 if let Kind::ListItem { selected, .. } = &mut self.0 {
671 *selected = yes;
672 }
673 self
674 }
675
676 pub fn on_back(mut self, message: M) -> Self {
678 if let Kind::Header { on_back, .. } = &mut self.0 {
679 *on_back = Some(message);
680 }
681 self
682 }
683
684 pub fn step(mut self, by: f32) -> Self {
686 if let Kind::Slider { step, .. } = &mut self.0 {
687 *step = by;
688 }
689 self
690 }
691
692 pub fn on_release(mut self, message: M) -> Self {
694 if let Kind::Slider { on_release, .. } = &mut self.0 {
695 *on_release = Some(message);
696 }
697 self
698 }
699
700 pub fn on_submit(mut self, message: M) -> Self {
702 if let Kind::TextInput { on_submit, .. } | Kind::TextArea { on_submit, .. } = &mut self.0 {
703 *on_submit = Some(message);
704 }
705 self
706 }
707
708 pub fn secret(mut self, yes: bool) -> Self {
710 if let Kind::TextInput { secret, .. } = &mut self.0 {
711 *secret = yes;
712 }
713 self
714 }
715
716 pub fn looping(mut self) -> Self {
720 if let Kind::Sprite { looping, .. } = &mut self.0 {
721 *looping = true;
722 }
723 self
724 }
725
726 pub fn large(mut self) -> Self {
729 if let Kind::Cover { large, .. } = &mut self.0 {
730 *large = true;
731 }
732 self
733 }
734
735 pub fn bottom(mut self) -> Self {
738 if let Kind::Scroll { bottom, .. } = &mut self.0 {
739 *bottom = true;
740 }
741 self
742 }
743
744 pub fn limit(mut self, at: f32) -> Self {
746 if let Kind::Meter { limit, .. } = &mut self.0 {
747 *limit = Some(at);
748 }
749 self
750 }
751
752 fn set_tone(&mut self, new: Tone) {
753 match &mut self.0 {
754 Kind::Label { tone, .. }
755 | Kind::Heading { tone, .. }
756 | Kind::Icon { tone, .. }
757 | Kind::AppIcon { tone, .. }
758 | Kind::Image { tone, .. }
759 | Kind::Sprite { tone, .. }
760 | Kind::Counter { tone, .. }
761 | Kind::Meter { tone, .. }
762 | Kind::TextInput { tone, .. }
763 | Kind::Markdown { tone, .. }
764 | Kind::Badge { tone, .. } => {
765 tone.get_or_insert(new);
766 }
767 Kind::Cover { tone, fallback, .. } => {
768 tone.get_or_insert(new);
769 fallback.set_tone(new);
770 }
771 Kind::Alternate {
772 options: children, ..
773 }
774 | Kind::Row(children)
775 | Kind::Column(children)
776 | Kind::Grid {
777 cells: children, ..
778 }
779 | Kind::List(children) => {
780 children.iter_mut().for_each(|child| child.set_tone(new));
781 }
782 Kind::Button { content, .. }
783 | Kind::Gulp(content)
784 | Kind::Center(content)
785 | Kind::Display(content)
786 | Kind::Mark(content)
787 | Kind::Scroll { content, .. }
788 | Kind::Detail { value: content, .. } => content.set_tone(new),
789 Kind::ListItem {
790 leading, trailing, ..
791 } => {
792 leading
793 .iter_mut()
794 .chain(trailing.iter_mut())
795 .for_each(|child| child.set_tone(new));
796 }
797 Kind::Tabs(pages) => {
798 for (_, page) in pages {
799 page.set_tone(new);
800 }
801 }
802 Kind::Switcher { options, .. } => {
803 options
804 .iter_mut()
805 .for_each(|(child, _)| child.set_tone(new));
806 }
807 Kind::Separator
808 | Kind::Empty
809 | Kind::Section { .. }
810 | Kind::Header { .. }
811 | Kind::Fill
812 | Kind::Slider { .. }
813 | Kind::TextArea { .. }
814 | Kind::Toggle { .. } => {}
815 }
816 }
817
818 pub fn map<N: 'static>(self, f: &(impl Fn(M) -> N + Clone + 'static)) -> Node<N>
822 where
823 M: 'static,
824 {
825 let map_all = |nodes: Vec<Node<M>>| nodes.into_iter().map(|node| node.map(f)).collect();
826 let map_box = |node: Option<Box<Node<M>>>| node.map(|node| Box::new(node.map(f)));
827
828 Node(match self.0 {
829 Kind::Label { text, tone } => Kind::Label { text, tone },
830 Kind::Heading { text, tone } => Kind::Heading { text, tone },
831 Kind::Icon { icon, tone } => Kind::Icon { icon, tone },
832 Kind::AppIcon { app_id, tone } => Kind::AppIcon { app_id, tone },
833 Kind::Image { file, tint, tone } => Kind::Image { file, tint, tone },
834 Kind::Sprite {
835 frames,
836 order,
837 looping,
838 tone,
839 } => Kind::Sprite {
840 frames,
841 order,
842 looping,
843 tone,
844 },
845 Kind::Cover {
846 url,
847 fallback,
848 large,
849 tone,
850 } => Kind::Cover {
851 url,
852 fallback: Box::new(fallback.map(f)),
853 large,
854 tone,
855 },
856 Kind::Counter {
857 value,
858 range,
859 unit,
860 tone,
861 } => Kind::Counter {
862 value,
863 range,
864 unit,
865 tone,
866 },
867 Kind::Alternate {
868 options,
869 current,
870 hovered,
871 grows,
872 } => Kind::Alternate {
873 options: map_all(options),
874 current,
875 hovered,
876 grows,
877 },
878 Kind::Row(children) => Kind::Row(map_all(children)),
879 Kind::Column(children) => Kind::Column(map_all(children)),
880 Kind::Grid { columns, cells } => Kind::Grid {
881 columns,
882 cells: map_all(cells),
883 },
884 Kind::Button {
885 content,
886 on_press,
887 on_secondary,
888 prominent,
889 } => Kind::Button {
890 content: Box::new(content.map(f)),
891 on_press: f(on_press),
892 on_secondary: on_secondary.map(f),
893 prominent,
894 },
895 Kind::Switcher { options, current } => Kind::Switcher {
896 options: options
897 .into_iter()
898 .map(|(node, message)| (node.map(f), f(message)))
899 .collect(),
900 current,
901 },
902 Kind::Gulp(content) => Kind::Gulp(Box::new(content.map(f))),
903 Kind::Center(content) => Kind::Center(Box::new(content.map(f))),
904 Kind::Separator => Kind::Separator,
905 Kind::Empty => Kind::Empty,
906 Kind::ListItem {
907 title,
908 subtitle,
909 leading,
910 trailing,
911 selected,
912 on_press,
913 on_secondary,
914 } => Kind::ListItem {
915 title,
916 subtitle,
917 leading: map_box(leading),
918 trailing: map_box(trailing),
919 selected,
920 on_press: on_press.map(f),
921 on_secondary: on_secondary.map(f),
922 },
923 Kind::List(rows) => Kind::List(map_all(rows)),
924 Kind::Section { title, trailing } => Kind::Section {
925 title,
926 trailing: map_box(trailing),
927 },
928 Kind::Header {
929 title,
930 on_back,
931 trailing,
932 } => Kind::Header {
933 title,
934 on_back: on_back.map(f),
935 trailing: map_box(trailing),
936 },
937 Kind::Detail { name, value } => Kind::Detail {
938 name,
939 value: Box::new(value.map(f)),
940 },
941 Kind::Fill => Kind::Fill,
942 Kind::Slider {
943 value,
944 range,
945 step,
946 on_change,
947 on_release,
948 } => {
949 let g = f.clone();
950 Kind::Slider {
951 value,
952 range,
953 step,
954 on_change: Box::new(move |value| g(on_change(value))),
955 on_release: on_release.map(f),
956 }
957 }
958 Kind::Toggle { on, on_toggle } => Kind::Toggle {
959 on,
960 on_toggle: f(on_toggle),
961 },
962 Kind::Meter {
963 fraction,
964 limit,
965 tone,
966 } => Kind::Meter {
967 fraction,
968 limit,
969 tone,
970 },
971 Kind::TextInput {
972 placeholder,
973 value,
974 on_input,
975 on_submit,
976 secret,
977 trailing,
978 tone,
979 } => {
980 let g = f.clone();
981 Kind::TextInput {
982 placeholder,
983 value,
984 on_input: Box::new(move |text| g(on_input(text))),
985 on_submit: on_submit.map(f),
986 secret,
987 trailing: map_box(trailing),
988 tone,
989 }
990 }
991 Kind::Display(content) => Kind::Display(Box::new(content.map(f))),
992 Kind::Mark(content) => Kind::Mark(Box::new(content.map(f))),
993 Kind::Badge { text, tone } => Kind::Badge { text, tone },
994 Kind::Tabs(pages) => Kind::Tabs(
995 pages
996 .into_iter()
997 .map(|(name, page)| (name, page.map(f)))
998 .collect(),
999 ),
1000 Kind::Markdown { text, tone } => Kind::Markdown { text, tone },
1001 Kind::TextArea {
1002 placeholder,
1003 value,
1004 on_input,
1005 on_submit,
1006 trailing,
1007 } => {
1008 let g = f.clone();
1009 Kind::TextArea {
1010 placeholder,
1011 value,
1012 on_input: Box::new(move |text| g(on_input(text))),
1013 on_submit: on_submit.map(f),
1014 trailing: map_box(trailing),
1015 }
1016 }
1017 Kind::Scroll { content, bottom } => Kind::Scroll {
1018 content: Box::new(content.map(f)),
1019 bottom,
1020 },
1021 })
1022 }
1023
1024 #[doc(hidden)]
1025 pub fn into_kind(self) -> Kind<M> {
1026 self.0
1027 }
1028
1029 #[doc(hidden)]
1030 pub fn kind(&self) -> &Kind<M> {
1031 &self.0
1032 }
1033}
1034
1035pub struct Item<M> {
1037 content: Option<Node<M>>,
1038 on_press: Option<M>,
1039 on_secondary: Option<M>,
1040 on_scroll: Option<(M, M)>,
1041}
1042
1043impl<M> Item<M> {
1044 pub fn new(content: Node<M>) -> Self {
1045 Self {
1046 content: Some(content),
1047 on_press: None,
1048 on_secondary: None,
1049 on_scroll: None,
1050 }
1051 }
1052
1053 pub fn hidden() -> Self {
1055 Self {
1056 content: None,
1057 on_press: None,
1058 on_secondary: None,
1059 on_scroll: None,
1060 }
1061 }
1062
1063 pub fn on_press(mut self, message: M) -> Self {
1065 self.on_press = Some(message);
1066 self
1067 }
1068
1069 pub fn on_secondary(mut self, message: M) -> Self {
1071 self.on_secondary = Some(message);
1072 self
1073 }
1074
1075 pub fn on_scroll(mut self, up: M, down: M) -> Self {
1078 self.on_scroll = Some((up, down));
1079 self
1080 }
1081
1082 pub fn map<N: 'static>(self, f: &(impl Fn(M) -> N + Clone + 'static)) -> Item<N>
1083 where
1084 M: 'static,
1085 {
1086 Item {
1087 content: self.content.map(|content| content.map(f)),
1088 on_press: self.on_press.map(f),
1089 on_secondary: self.on_secondary.map(f),
1090 on_scroll: self.on_scroll.map(|(up, down)| (f(up), f(down))),
1091 }
1092 }
1093
1094 #[doc(hidden)]
1095 pub fn into_parts(self) -> ItemParts<M> {
1096 ItemParts {
1097 content: self.content,
1098 on_press: self.on_press,
1099 on_secondary: self.on_secondary,
1100 on_scroll: self.on_scroll,
1101 }
1102 }
1103}
1104
1105#[doc(hidden)]
1106pub struct ItemParts<M> {
1107 pub content: Option<Node<M>>,
1108 pub on_press: Option<M>,
1109 pub on_secondary: Option<M>,
1110 pub on_scroll: Option<(M, M)>,
1112}
1113
1114pub struct Panel<M> {
1117 content: Node<M>,
1118 on_open: Option<M>,
1119 on_close: Option<M>,
1120 wide: bool,
1121}
1122
1123impl<M> Panel<M> {
1124 pub fn new(content: Node<M>) -> Self {
1125 Self {
1126 content,
1127 on_open: None,
1128 on_close: None,
1129 wide: false,
1130 }
1131 }
1132
1133 pub fn wide(mut self) -> Self {
1136 self.wide = true;
1137 self
1138 }
1139
1140 #[doc(hidden)]
1141 pub fn is_wide(&self) -> bool {
1142 self.wide
1143 }
1144
1145 pub fn on_open(mut self, message: M) -> Self {
1147 self.on_open = Some(message);
1148 self
1149 }
1150
1151 pub fn on_close(mut self, message: M) -> Self {
1153 self.on_close = Some(message);
1154 self
1155 }
1156
1157 pub fn map<N: 'static>(self, f: &(impl Fn(M) -> N + Clone + 'static)) -> Panel<N>
1158 where
1159 M: 'static,
1160 {
1161 Panel {
1162 content: self.content.map(f),
1163 on_open: self.on_open.map(f),
1164 on_close: self.on_close.map(f),
1165 wide: self.wide,
1166 }
1167 }
1168
1169 #[doc(hidden)]
1170 pub fn into_node(self) -> Node<M> {
1171 self.content
1172 }
1173
1174 #[doc(hidden)]
1175 pub fn on_open_message(self) -> Option<M> {
1176 self.on_open
1177 }
1178
1179 #[doc(hidden)]
1180 pub fn on_close_message(self) -> Option<M> {
1181 self.on_close
1182 }
1183}
1184
1185#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1187pub enum Urgency {
1188 Low,
1190 #[default]
1191 Normal,
1192 Critical,
1194}
1195
1196pub struct Notice<M> {
1203 key: u64,
1204 summary: String,
1205 app: Option<String>,
1206 app_icon: Option<String>,
1207 image: Option<String>,
1208 body: Option<String>,
1209 markdown: bool,
1210 urgency: Urgency,
1211 actions: Vec<(String, M)>,
1212 on_press: Option<M>,
1213 on_dismiss: Option<M>,
1214 expires: Option<(std::time::Duration, M)>,
1215 reply: Option<Reply<M>>,
1216}
1217
1218pub struct Reply<M> {
1225 placeholder: String,
1226 value: String,
1227 on_input: std::rc::Rc<dyn Fn(String) -> M>,
1228 on_submit: Option<M>,
1229 on_cancel: Option<M>,
1230}
1231
1232impl<M> Reply<M> {
1233 pub fn new(
1235 placeholder: impl Into<String>,
1236 value: impl Into<String>,
1237 on_input: impl Fn(String) -> M + 'static,
1238 ) -> Self {
1239 Self {
1240 placeholder: placeholder.into(),
1241 value: value.into(),
1242 on_input: std::rc::Rc::new(on_input),
1243 on_submit: None,
1244 on_cancel: None,
1245 }
1246 }
1247
1248 pub fn on_submit(mut self, message: M) -> Self {
1250 self.on_submit = Some(message);
1251 self
1252 }
1253
1254 pub fn on_cancel(mut self, message: M) -> Self {
1256 self.on_cancel = Some(message);
1257 self
1258 }
1259
1260 pub fn map<N: 'static>(self, f: &(impl Fn(M) -> N + Clone + 'static)) -> Reply<N>
1261 where
1262 M: 'static,
1263 {
1264 let g = f.clone();
1265 let on_input = self.on_input;
1266 Reply {
1267 placeholder: self.placeholder,
1268 value: self.value,
1269 on_input: std::rc::Rc::new(move |text| g(on_input(text))),
1270 on_submit: self.on_submit.map(f),
1271 on_cancel: self.on_cancel.map(f),
1272 }
1273 }
1274
1275 #[doc(hidden)]
1276 pub fn into_parts(self) -> ReplyParts<M> {
1277 ReplyParts {
1278 placeholder: self.placeholder,
1279 value: self.value,
1280 on_input: self.on_input,
1281 on_submit: self.on_submit,
1282 on_cancel: self.on_cancel,
1283 }
1284 }
1285}
1286
1287#[doc(hidden)]
1288#[derive(Clone)]
1289pub struct ReplyParts<M> {
1290 pub placeholder: String,
1291 pub value: String,
1292 pub on_input: std::rc::Rc<dyn Fn(String) -> M>,
1293 pub on_submit: Option<M>,
1294 pub on_cancel: Option<M>,
1295}
1296
1297impl<M> Notice<M> {
1298 pub fn new(key: u64, summary: impl Into<String>) -> Self {
1299 Self {
1300 key,
1301 summary: summary.into(),
1302 app: None,
1303 app_icon: None,
1304 image: None,
1305 body: None,
1306 markdown: false,
1307 urgency: Urgency::Normal,
1308 actions: Vec::new(),
1309 on_press: None,
1310 on_dismiss: None,
1311 expires: None,
1312 reply: None,
1313 }
1314 }
1315
1316 pub fn app(mut self, name: impl Into<String>) -> Self {
1318 self.app = Some(name.into());
1319 self
1320 }
1321
1322 pub fn app_icon(mut self, app_id: impl Into<String>) -> Self {
1324 self.app_icon = Some(app_id.into());
1325 self
1326 }
1327
1328 pub fn image(mut self, url: impl Into<String>) -> Self {
1331 self.image = Some(url.into());
1332 self
1333 }
1334
1335 pub fn body(mut self, text: impl Into<String>) -> Self {
1337 self.body = Some(text.into());
1338 self
1339 }
1340
1341 pub fn markdown(mut self) -> Self {
1343 self.markdown = true;
1344 self
1345 }
1346
1347 pub fn urgency(mut self, urgency: Urgency) -> Self {
1348 self.urgency = urgency;
1349 self
1350 }
1351
1352 pub fn action(mut self, label: impl Into<String>, message: M) -> Self {
1354 self.actions.push((label.into(), message));
1355 self
1356 }
1357
1358 pub fn on_press(mut self, message: M) -> Self {
1360 self.on_press = Some(message);
1361 self
1362 }
1363
1364 pub fn on_dismiss(mut self, message: M) -> Self {
1366 self.on_dismiss = Some(message);
1367 self
1368 }
1369
1370 pub fn expires(mut self, after: std::time::Duration, message: M) -> Self {
1374 self.expires = Some((after, message));
1375 self
1376 }
1377
1378 pub fn reply(mut self, reply: Reply<M>) -> Self {
1380 self.reply = Some(reply);
1381 self
1382 }
1383
1384 pub fn map<N: 'static>(self, f: &(impl Fn(M) -> N + Clone + 'static)) -> Notice<N>
1385 where
1386 M: 'static,
1387 {
1388 Notice {
1389 key: self.key,
1390 summary: self.summary,
1391 app: self.app,
1392 app_icon: self.app_icon,
1393 image: self.image,
1394 body: self.body,
1395 markdown: self.markdown,
1396 urgency: self.urgency,
1397 actions: self
1398 .actions
1399 .into_iter()
1400 .map(|(label, message)| (label, f(message)))
1401 .collect(),
1402 on_press: self.on_press.map(f),
1403 on_dismiss: self.on_dismiss.map(f),
1404 expires: self.expires.map(|(after, message)| (after, f(message))),
1405 reply: self.reply.map(|reply| reply.map(f)),
1406 }
1407 }
1408
1409 pub fn key(&self) -> u64 {
1410 self.key
1411 }
1412
1413 #[doc(hidden)]
1414 pub fn into_parts(self) -> NoticeParts<M> {
1415 NoticeParts {
1416 key: self.key,
1417 summary: self.summary,
1418 app: self.app,
1419 app_icon: self.app_icon,
1420 image: self.image,
1421 body: self.body,
1422 markdown: self.markdown,
1423 urgency: self.urgency,
1424 actions: self.actions,
1425 on_press: self.on_press,
1426 on_dismiss: self.on_dismiss,
1427 expires: self.expires,
1428 reply: self.reply.map(Reply::into_parts),
1429 }
1430 }
1431}
1432
1433#[doc(hidden)]
1434#[derive(Clone)]
1435pub struct NoticeParts<M> {
1436 pub key: u64,
1437 pub summary: String,
1438 pub app: Option<String>,
1439 pub app_icon: Option<String>,
1440 pub image: Option<String>,
1441 pub body: Option<String>,
1442 pub markdown: bool,
1443 pub urgency: Urgency,
1444 pub actions: Vec<(String, M)>,
1445 pub on_press: Option<M>,
1446 pub on_dismiss: Option<M>,
1447 pub expires: Option<(std::time::Duration, M)>,
1448 pub reply: Option<ReplyParts<M>>,
1449}
1450
1451#[cfg(test)]
1452mod tests {
1453 use super::*;
1454
1455 #[test]
1456 fn map_converts_every_message() {
1457 let node = row([
1458 button(label("a"), 1).on_secondary(4),
1459 column([button(label("b"), 2), label("c")]),
1460 switcher([(label("d"), 3)], Some(0)),
1461 ]);
1462
1463 let Kind::Row(children) = node.map(&|n: i32| n * 10).into_kind() else {
1464 panic!("row became something else");
1465 };
1466 let mut messages = Vec::new();
1467 collect(children, &mut messages);
1468 assert_eq!(messages, [10, 40, 20, 30]);
1469 }
1470
1471 fn collect(nodes: Vec<Node<i32>>, out: &mut Vec<i32>) {
1472 for node in nodes {
1473 match node.into_kind() {
1474 Kind::Button {
1475 on_press,
1476 on_secondary,
1477 ..
1478 } => {
1479 out.push(on_press);
1480 out.extend(on_secondary);
1481 }
1482 Kind::Switcher { options, .. } => {
1483 out.extend(options.into_iter().map(|(_, message)| message))
1484 }
1485 Kind::Row(children) | Kind::Column(children) => collect(children, out),
1486 _ => {}
1487 }
1488 }
1489 }
1490
1491 #[test]
1492 fn hover_shows_the_second_option() {
1493 let node: Node<()> = hover(label("a"), label("b"));
1494 assert!(matches!(
1495 node.into_kind(),
1496 Kind::Alternate {
1497 current: 0,
1498 hovered: Some(1),
1499 ..
1500 }
1501 ));
1502 let node: Node<()> = label("a").on_hover(1);
1503 assert!(matches!(node.into_kind(), Kind::Label { .. }));
1504 }
1505
1506 #[test]
1507 fn tone_fills_only_unset_text() {
1508 let node: Node<()> =
1509 row([label("a").tone(Tone::Accent), label("b"), icon(Icon::Wired)]).tone(Tone::Dim);
1510 let Kind::Row(children) = node.into_kind() else {
1511 unreachable!()
1512 };
1513 let tones: Vec<_> = children
1514 .into_iter()
1515 .map(|child| match child.into_kind() {
1516 Kind::Label { tone, .. } | Kind::Icon { tone, .. } => tone,
1517 _ => None,
1518 })
1519 .collect();
1520 assert_eq!(
1521 tones,
1522 [Some(Tone::Accent), Some(Tone::Dim), Some(Tone::Dim)]
1523 );
1524 }
1525
1526 #[test]
1527 fn map_reaches_new_nodes_and_their_closures() {
1528 let node = column([
1529 header("h").on_back(1).trailing(toggle(true, 2)),
1530 list([list_item("a")
1531 .on_press(3)
1532 .on_secondary(4)
1533 .trailing(button(icon(Icon::Info), 5))]),
1534 slider(0.5, 0.0..=1.0, |v| (v * 10.0) as i32).on_release(6),
1535 text_input("p", "v", |text: String| text.len() as i32).on_submit(7),
1536 scroll(text_area("p", "v", |text: String| text.len() as i32 + 1).on_submit(8)).bottom(),
1537 ]);
1538 let Kind::Column(children) = node.map(&|n: i32| n * 10).into_kind() else {
1539 panic!("column became something else");
1540 };
1541 let mut children = children.into_iter().map(Node::into_kind);
1542 let Some(Kind::Header {
1543 on_back: Some(10),
1544 trailing: Some(trailing),
1545 ..
1546 }) = children.next()
1547 else {
1548 panic!("header lost its back message");
1549 };
1550 assert!(matches!(
1551 trailing.into_kind(),
1552 Kind::Toggle { on_toggle: 20, .. }
1553 ));
1554 let Some(Kind::List(rows)) = children.next() else {
1555 panic!("list lost");
1556 };
1557 let Some(Kind::ListItem {
1558 on_press: Some(30),
1559 on_secondary: Some(40),
1560 trailing: Some(trailing),
1561 ..
1562 }) = rows.into_iter().next().map(Node::into_kind)
1563 else {
1564 panic!("list item lost its messages");
1565 };
1566 assert!(matches!(
1567 trailing.into_kind(),
1568 Kind::Button { on_press: 50, .. }
1569 ));
1570 let Some(Kind::Slider {
1571 on_change,
1572 on_release: Some(60),
1573 ..
1574 }) = children.next()
1575 else {
1576 panic!("slider lost");
1577 };
1578 assert_eq!(on_change(0.3), 30);
1579 let Some(Kind::TextInput {
1580 on_input,
1581 on_submit: Some(70),
1582 ..
1583 }) = children.next()
1584 else {
1585 panic!("text input lost");
1586 };
1587 assert_eq!(on_input("abcd".into()), 40);
1588 let Some(Kind::Scroll {
1589 content,
1590 bottom: true,
1591 }) = children.next()
1592 else {
1593 panic!("scroll lost");
1594 };
1595 let Kind::TextArea {
1596 on_input,
1597 on_submit: Some(80),
1598 ..
1599 } = content.into_kind()
1600 else {
1601 panic!("text area lost");
1602 };
1603 assert_eq!(on_input("abcd".into()), 50);
1604 }
1605
1606 #[test]
1607 fn tabs_map_and_tone_every_page() {
1608 let node = tabs([("a", button(label("x"), 1)), ("b", button(label("y"), 2))])
1609 .tone(Tone::Dim)
1610 .map(&|n: i32| n * 10);
1611 let Kind::Tabs(pages) = node.into_kind() else {
1612 panic!("tabs became something else");
1613 };
1614 let names: Vec<_> = pages.iter().map(|(name, _)| name.as_str()).collect();
1615 assert_eq!(names, ["a", "b"]);
1616 let mut messages = Vec::new();
1617 for (_, page) in pages {
1618 let Kind::Button {
1619 content, on_press, ..
1620 } = page.into_kind()
1621 else {
1622 panic!("page lost its button");
1623 };
1624 assert!(matches!(
1625 content.into_kind(),
1626 Kind::Label {
1627 tone: Some(Tone::Dim),
1628 ..
1629 }
1630 ));
1631 messages.push(on_press);
1632 }
1633 assert_eq!(messages, [10, 20]);
1634 }
1635
1636 #[test]
1637 fn notice_map_reaches_every_message() {
1638 let notice = Notice::new(7, "s")
1639 .action("a", 1)
1640 .action("b", 2)
1641 .on_press(3)
1642 .on_dismiss(4)
1643 .expires(std::time::Duration::from_secs(5), 5)
1644 .reply(
1645 Reply::new("p", "v", |text: String| text.len() as i32)
1646 .on_submit(6)
1647 .on_cancel(7),
1648 )
1649 .map(&|n: i32| n * 10);
1650 assert_eq!(notice.key(), 7);
1651 let parts = notice.into_parts();
1652 let actions: Vec<_> = parts.actions.into_iter().map(|(_, m)| m).collect();
1653 assert_eq!(actions, [10, 20]);
1654 assert_eq!(parts.on_press, Some(30));
1655 assert_eq!(parts.on_dismiss, Some(40));
1656 assert_eq!(parts.expires.map(|(_, m)| m), Some(50));
1657 let reply = parts.reply.unwrap();
1658 assert_eq!((reply.on_input)("abc".into()), 30);
1659 assert_eq!((reply.on_submit, reply.on_cancel), (Some(60), Some(70)));
1660 }
1661
1662 #[test]
1663 fn modifiers_only_touch_their_nodes() {
1664 let node: Node<i32> = label("a").trailing(label("b")).on_press(1).selected(true);
1665 assert!(matches!(node.into_kind(), Kind::Label { .. }));
1666 let node: Node<i32> = button(label("a"), 1).on_press(2).prominent();
1667 assert!(matches!(
1668 node.into_kind(),
1669 Kind::Button {
1670 on_press: 1,
1671 prominent: true,
1672 ..
1673 }
1674 ));
1675 }
1676
1677 #[test]
1678 fn tone_reaches_details_and_row_icons() {
1679 let node: Node<()> = column([
1680 detail("n", label("v")),
1681 list_item("t").leading(icon(Icon::Lock)),
1682 ])
1683 .tone(Tone::Good);
1684 let Kind::Column(children) = node.into_kind() else {
1685 unreachable!()
1686 };
1687 let mut children = children.into_iter().map(Node::into_kind);
1688 let Some(Kind::Detail { value, .. }) = children.next() else {
1689 unreachable!()
1690 };
1691 assert!(matches!(
1692 value.into_kind(),
1693 Kind::Label {
1694 tone: Some(Tone::Good),
1695 ..
1696 }
1697 ));
1698 let Some(Kind::ListItem {
1699 leading: Some(leading),
1700 ..
1701 }) = children.next()
1702 else {
1703 unreachable!()
1704 };
1705 assert!(matches!(
1706 leading.into_kind(),
1707 Kind::Icon {
1708 tone: Some(Tone::Good),
1709 ..
1710 }
1711 ));
1712 }
1713}