1use std::ops::RangeInclusive;
8use std::sync::Arc;
9use std::time::SystemTime;
10
11#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
13pub enum Tone {
14 #[default]
15 Normal,
16 Dim,
18 Accent,
20 Warning,
21 Critical,
22 Good,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq)]
29pub enum Icon {
30 Volume { level: f32, muted: bool },
32 Wifi { strength: Option<f32> },
34 Wired,
36 Battery { charge: f32, charging: bool },
38 Lock,
40 Info,
42 Back,
44 Forward,
46 Check,
48 Refresh,
50 Close,
52 Reveal,
54 Bolt,
56 Plug,
58 Mic { muted: bool },
60 Speaker,
62 Headphones,
64 Display,
66 Bell { muted: bool },
68 Brightness { level: f32 },
70 KeyboardLight,
72 Media { playing: bool },
75 Play,
77 Pause,
79 Previous,
81 Next,
83}
84
85#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
87pub enum Mark {
88 #[default]
90 Note,
91 You,
93 Running,
95 Done,
97 Failed,
99 Waiting,
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq)]
105pub enum Change {
106 Context,
108 Added,
109 Removed,
110 Hunk,
112}
113
114pub struct Node<M>(Kind<M>);
116
117#[doc(hidden)]
118pub enum Kind<M> {
119 Label {
120 text: String,
121 tone: Option<Tone>,
122 },
123 Heading {
124 text: String,
125 tone: Option<Tone>,
126 },
127 Icon {
128 icon: Icon,
129 tone: Option<Tone>,
130 },
131 AppIcon {
132 app_id: String,
133 tone: Option<Tone>,
134 },
135 Image {
136 file: &'static [u8],
137 tint: bool,
138 tone: Option<Tone>,
139 },
140 Sprite {
141 frames: &'static [&'static [u8]],
142 order: &'static [u8],
144 looping: bool,
146 tone: Option<Tone>,
147 },
148 Cover {
149 url: String,
150 fallback: Box<Node<M>>,
152 large: bool,
154 tone: Option<Tone>,
155 },
156 Counter {
157 value: i64,
158 range: RangeInclusive<i64>,
159 unit: String,
160 tone: Option<Tone>,
161 },
162 Alternate {
163 options: Vec<Node<M>>,
164 current: usize,
165 hovered: Option<usize>,
167 grows: bool,
169 },
170 Row(Vec<Node<M>>),
171 Column(Vec<Node<M>>),
172 Grid {
173 columns: usize,
174 cells: Vec<Node<M>>,
175 },
176 Button {
177 content: Box<Node<M>>,
178 on_press: M,
179 on_secondary: Option<M>,
180 prominent: bool,
182 },
183 Switcher {
184 options: Vec<(Node<M>, M)>,
185 current: Option<usize>,
186 },
187 Gulp(Box<Node<M>>),
188 Center(Box<Node<M>>),
190 Separator,
191 Empty,
192 ListItem {
193 title: String,
194 subtitle: Option<String>,
195 leading: Option<Box<Node<M>>>,
196 trailing: Option<Box<Node<M>>>,
197 selected: bool,
198 on_press: Option<M>,
199 on_secondary: Option<M>,
200 },
201 List(Vec<Node<M>>),
202 Section {
203 title: String,
204 trailing: Option<Box<Node<M>>>,
205 },
206 Header {
207 title: String,
208 subtitle: Option<String>,
209 on_back: Option<M>,
210 leading: Option<Box<Node<M>>>,
211 trailing: Option<Box<Node<M>>>,
212 },
213 Detail {
214 name: String,
215 value: Box<Node<M>>,
216 },
217 Fill,
218 Slider {
219 value: f32,
220 range: RangeInclusive<f32>,
221 step: f32,
222 on_change: Box<dyn Fn(f32) -> M>,
223 on_release: Option<M>,
224 },
225 Toggle {
226 on: bool,
227 on_toggle: M,
228 },
229 Meter {
230 fraction: f32,
231 limit: Option<f32>,
233 tone: Option<Tone>,
234 },
235 TextInput {
236 placeholder: String,
237 value: String,
238 on_input: Box<dyn Fn(String) -> M>,
239 on_submit: Option<M>,
240 secret: bool,
241 trailing: Option<Box<Node<M>>>,
242 tone: Option<Tone>,
243 },
244 Display(Box<Node<M>>),
245 Mark(Box<Node<M>>),
246 Counted {
247 content: Box<Node<M>>,
248 count: u32,
249 },
250 Badge {
251 text: String,
252 tone: Option<Tone>,
253 },
254 Tabs(Vec<(String, Node<M>)>),
256 Markdown {
257 text: String,
258 tone: Option<Tone>,
259 },
260 TextArea {
261 placeholder: String,
262 value: String,
263 on_input: Box<dyn Fn(String) -> M>,
264 on_submit: Option<M>,
265 trailing: Option<Box<Node<M>>>,
266 },
267 Scroll {
268 content: Box<Node<M>>,
269 bottom: bool,
271 },
272 Literal {
273 text: String,
274 tone: Option<Tone>,
275 },
276 Divider {
277 text: String,
278 },
279 Timeline(Vec<Node<M>>),
280 Step {
281 mark: Mark,
282 leading: Option<Box<Node<M>>>,
284 content: Box<Node<M>>,
285 },
286 Diff {
287 title: String,
288 lines: Vec<(Change, String)>,
289 },
290}
291
292pub fn label<M>(text: impl Into<String>) -> Node<M> {
293 Node(Kind::Label {
294 text: text.into(),
295 tone: None,
296 })
297}
298
299pub fn heading<M>(text: impl Into<String>) -> Node<M> {
301 Node(Kind::Heading {
302 text: text.into(),
303 tone: None,
304 })
305}
306
307pub fn icon<M>(icon: Icon) -> Node<M> {
309 Node(Kind::Icon { icon, tone: None })
310}
311
312pub fn app_icon<M>(app_id: impl Into<String>) -> Node<M> {
316 Node(Kind::AppIcon {
317 app_id: app_id.into(),
318 tone: None,
319 })
320}
321
322pub fn image<M>(file: &'static [u8]) -> Node<M> {
327 Node(Kind::Image {
328 file,
329 tint: false,
330 tone: None,
331 })
332}
333
334pub fn symbol<M>(file: &'static [u8]) -> Node<M> {
337 Node(Kind::Image {
338 file,
339 tint: true,
340 tone: None,
341 })
342}
343
344pub fn sprite<M>(frames: &'static [&'static [u8]], order: &'static [u8]) -> Node<M> {
352 Node(Kind::Sprite {
353 frames,
354 order,
355 looping: false,
356 tone: None,
357 })
358}
359
360pub fn cover<M>(url: impl Into<String>, fallback: Node<M>) -> Node<M> {
365 Node(Kind::Cover {
366 url: url.into(),
367 fallback: Box::new(fallback),
368 large: false,
369 tone: None,
370 })
371}
372
373pub fn counter<M>(value: i64, range: RangeInclusive<i64>, unit: impl Into<String>) -> Node<M> {
377 Node(Kind::Counter {
378 value,
379 range,
380 unit: unit.into(),
381 tone: None,
382 })
383}
384
385pub fn alternate<M>(options: impl IntoIterator<Item = Node<M>>, current: usize) -> Node<M> {
390 Node(Kind::Alternate {
391 options: options.into_iter().collect(),
392 current,
393 hovered: None,
394 grows: false,
395 })
396}
397
398pub fn hover<M>(normal: Node<M>, hovered: Node<M>) -> Node<M> {
401 alternate([normal, hovered], 0).on_hover(1)
402}
403
404pub fn expand<M>(collapsed: Node<M>, expanded: Node<M>) -> Node<M> {
409 Node(Kind::Alternate {
410 options: vec![collapsed, expanded],
411 current: 0,
412 hovered: Some(1),
413 grows: true,
414 })
415}
416
417pub fn row<M>(children: impl IntoIterator<Item = Node<M>>) -> Node<M> {
419 Node(Kind::Row(children.into_iter().collect()))
420}
421
422pub fn column<M>(children: impl IntoIterator<Item = Node<M>>) -> Node<M> {
424 Node(Kind::Column(children.into_iter().collect()))
425}
426
427pub fn center<M>(content: Node<M>) -> Node<M> {
431 Node(Kind::Center(Box::new(content)))
432}
433
434pub fn grid<M>(columns: usize, cells: impl IntoIterator<Item = Node<M>>) -> Node<M> {
436 Node(Kind::Grid {
437 columns: columns.max(1),
438 cells: cells.into_iter().collect(),
439 })
440}
441
442pub fn button<M>(content: Node<M>, on_press: M) -> Node<M> {
443 Node(Kind::Button {
444 content: Box::new(content),
445 on_press,
446 on_secondary: None,
447 prominent: false,
448 })
449}
450
451pub fn switcher<M>(
455 options: impl IntoIterator<Item = (Node<M>, M)>,
456 current: Option<usize>,
457) -> Node<M> {
458 Node(Kind::Switcher {
459 options: options.into_iter().collect(),
460 current,
461 })
462}
463
464pub fn gulp<M>(content: Node<M>) -> Node<M> {
467 Node(Kind::Gulp(Box::new(content)))
468}
469
470pub fn separator<M>() -> Node<M> {
472 Node(Kind::Separator)
473}
474
475pub fn empty<M>() -> Node<M> {
477 Node(Kind::Empty)
478}
479
480pub fn list_item<M>(title: impl Into<String>) -> Node<M> {
485 Node(Kind::ListItem {
486 title: title.into(),
487 subtitle: None,
488 leading: None,
489 trailing: None,
490 selected: false,
491 on_press: None,
492 on_secondary: None,
493 })
494}
495
496pub fn list<M>(rows: impl IntoIterator<Item = Node<M>>) -> Node<M> {
499 Node(Kind::List(rows.into_iter().collect()))
500}
501
502pub fn section<M>(title: impl Into<String>) -> Node<M> {
505 Node(Kind::Section {
506 title: title.into(),
507 trailing: None,
508 })
509}
510
511pub fn header<M>(title: impl Into<String>) -> Node<M> {
516 Node(Kind::Header {
517 title: title.into(),
518 subtitle: None,
519 on_back: None,
520 leading: None,
521 trailing: None,
522 })
523}
524
525pub fn detail<M>(name: impl Into<String>, value: Node<M>) -> Node<M> {
527 Node(Kind::Detail {
528 name: name.into(),
529 value: Box::new(value),
530 })
531}
532
533pub fn fill<M>() -> Node<M> {
535 Node(Kind::Fill)
536}
537
538pub fn slider<M: 'static>(
541 value: f32,
542 range: RangeInclusive<f32>,
543 on_change: impl Fn(f32) -> M + 'static,
544) -> Node<M> {
545 Node(Kind::Slider {
546 value,
547 range,
548 step: 1.0,
549 on_change: Box::new(on_change),
550 on_release: None,
551 })
552}
553
554pub fn toggle<M>(on: bool, on_toggle: M) -> Node<M> {
556 Node(Kind::Toggle { on, on_toggle })
557}
558
559pub fn meter<M>(fraction: f32) -> Node<M> {
562 Node(Kind::Meter {
563 fraction,
564 limit: None,
565 tone: None,
566 })
567}
568
569pub fn text_input<M: 'static>(
574 placeholder: impl Into<String>,
575 value: impl Into<String>,
576 on_input: impl Fn(String) -> M + 'static,
577) -> Node<M> {
578 Node(Kind::TextInput {
579 placeholder: placeholder.into(),
580 value: value.into(),
581 on_input: Box::new(on_input),
582 on_submit: None,
583 secret: false,
584 trailing: None,
585 tone: None,
586 })
587}
588
589pub fn text_area<M: 'static>(
594 placeholder: impl Into<String>,
595 value: impl Into<String>,
596 on_input: impl Fn(String) -> M + 'static,
597) -> Node<M> {
598 Node(Kind::TextArea {
599 placeholder: placeholder.into(),
600 value: value.into(),
601 on_input: Box::new(on_input),
602 on_submit: None,
603 trailing: None,
604 })
605}
606
607pub fn markdown<M>(text: impl Into<String>) -> Node<M> {
612 Node(Kind::Markdown {
613 text: text.into(),
614 tone: None,
615 })
616}
617
618pub fn scroll<M>(content: Node<M>) -> Node<M> {
622 Node(Kind::Scroll {
623 content: Box::new(content),
624 bottom: false,
625 })
626}
627
628pub fn literal<M>(text: impl Into<String>) -> Node<M> {
631 Node(Kind::Literal {
632 text: text.into(),
633 tone: None,
634 })
635}
636
637pub fn divider<M>(text: impl Into<String>) -> Node<M> {
640 Node(Kind::Divider { text: text.into() })
641}
642
643pub fn timeline<M>(steps: impl IntoIterator<Item = Node<M>>) -> Node<M> {
647 Node(Kind::Timeline(steps.into_iter().collect()))
648}
649
650pub fn step<M>(mark: Mark, content: Node<M>) -> Node<M> {
654 Node(Kind::Step {
655 mark,
656 leading: None,
657 content: Box::new(content),
658 })
659}
660
661pub fn diff<M>(
664 title: impl Into<String>,
665 lines: impl IntoIterator<Item = (Change, String)>,
666) -> Node<M> {
667 Node(Kind::Diff {
668 title: title.into(),
669 lines: lines.into_iter().collect(),
670 })
671}
672
673pub fn display<M>(content: Node<M>) -> Node<M> {
675 Node(Kind::Display(Box::new(content)))
676}
677
678pub fn mark<M>(content: Node<M>) -> Node<M> {
680 Node(Kind::Mark(Box::new(content)))
681}
682
683pub fn counted<M>(content: Node<M>, count: u32) -> Node<M> {
687 Node(Kind::Counted {
688 content: Box::new(content),
689 count,
690 })
691}
692
693pub fn badge<M>(text: impl Into<String>) -> Node<M> {
695 Node(Kind::Badge {
696 text: text.into(),
697 tone: None,
698 })
699}
700
701pub fn tabs<M>(pages: impl IntoIterator<Item = (impl Into<String>, Node<M>)>) -> Node<M> {
705 Node(Kind::Tabs(
706 pages
707 .into_iter()
708 .map(|(name, page)| (name.into(), page))
709 .collect(),
710 ))
711}
712
713impl<M> Node<M> {
714 pub fn tone(mut self, tone: Tone) -> Self {
716 self.set_tone(tone);
717 self
718 }
719
720 pub fn on_hover(mut self, index: usize) -> Self {
723 if let Kind::Alternate { hovered, .. } = &mut self.0 {
724 *hovered = Some(index);
725 }
726 self
727 }
728
729 pub fn on_secondary(mut self, message: M) -> Self {
732 if let Kind::Button { on_secondary, .. } | Kind::ListItem { on_secondary, .. } = &mut self.0
733 {
734 *on_secondary = Some(message);
735 }
736 self
737 }
738
739 pub fn prominent(mut self) -> Self {
742 if let Kind::Button { prominent, .. } = &mut self.0 {
743 *prominent = true;
744 }
745 self
746 }
747
748 pub fn on_press(mut self, message: M) -> Self {
750 if let Kind::ListItem { on_press, .. } = &mut self.0 {
751 *on_press = Some(message);
752 }
753 self
754 }
755
756 pub fn subtitle(mut self, text: impl Into<String>) -> Self {
759 if let Kind::ListItem { subtitle, .. } | Kind::Header { subtitle, .. } = &mut self.0 {
760 *subtitle = Some(text.into());
761 }
762 self
763 }
764
765 pub fn leading(mut self, node: Node<M>) -> Self {
768 if let Kind::ListItem { leading, .. }
769 | Kind::Header { leading, .. }
770 | Kind::Step { leading, .. } = &mut self.0
771 {
772 *leading = Some(Box::new(node));
773 }
774 self
775 }
776
777 pub fn trailing(mut self, node: Node<M>) -> Self {
780 if let Kind::ListItem { trailing, .. }
781 | Kind::Section { trailing, .. }
782 | Kind::Header { trailing, .. }
783 | Kind::TextInput { trailing, .. }
784 | Kind::TextArea { trailing, .. } = &mut self.0
785 {
786 *trailing = Some(Box::new(node));
787 }
788 self
789 }
790
791 pub fn selected(mut self, yes: bool) -> Self {
793 if let Kind::ListItem { selected, .. } = &mut self.0 {
794 *selected = yes;
795 }
796 self
797 }
798
799 pub fn on_back(mut self, message: M) -> Self {
801 if let Kind::Header { on_back, .. } = &mut self.0 {
802 *on_back = Some(message);
803 }
804 self
805 }
806
807 pub fn step(mut self, by: f32) -> Self {
809 if let Kind::Slider { step, .. } = &mut self.0 {
810 *step = by;
811 }
812 self
813 }
814
815 pub fn on_release(mut self, message: M) -> Self {
817 if let Kind::Slider { on_release, .. } = &mut self.0 {
818 *on_release = Some(message);
819 }
820 self
821 }
822
823 pub fn on_submit(mut self, message: M) -> Self {
825 if let Kind::TextInput { on_submit, .. } | Kind::TextArea { on_submit, .. } = &mut self.0 {
826 *on_submit = Some(message);
827 }
828 self
829 }
830
831 pub fn secret(mut self, yes: bool) -> Self {
833 if let Kind::TextInput { secret, .. } = &mut self.0 {
834 *secret = yes;
835 }
836 self
837 }
838
839 pub fn looping(mut self) -> Self {
843 if let Kind::Sprite { looping, .. } = &mut self.0 {
844 *looping = true;
845 }
846 self
847 }
848
849 pub fn large(mut self) -> Self {
852 if let Kind::Cover { large, .. } = &mut self.0 {
853 *large = true;
854 }
855 self
856 }
857
858 pub fn bottom(mut self) -> Self {
861 if let Kind::Scroll { bottom, .. } = &mut self.0 {
862 *bottom = true;
863 }
864 self
865 }
866
867 pub fn limit(mut self, at: f32) -> Self {
869 if let Kind::Meter { limit, .. } = &mut self.0 {
870 *limit = Some(at);
871 }
872 self
873 }
874
875 fn set_tone(&mut self, new: Tone) {
876 match &mut self.0 {
877 Kind::Label { tone, .. }
878 | Kind::Heading { tone, .. }
879 | Kind::Icon { tone, .. }
880 | Kind::AppIcon { tone, .. }
881 | Kind::Image { tone, .. }
882 | Kind::Sprite { tone, .. }
883 | Kind::Counter { tone, .. }
884 | Kind::Meter { tone, .. }
885 | Kind::TextInput { tone, .. }
886 | Kind::Markdown { tone, .. }
887 | Kind::Literal { tone, .. }
888 | Kind::Badge { tone, .. } => {
889 tone.get_or_insert(new);
890 }
891 Kind::Cover { tone, fallback, .. } => {
892 tone.get_or_insert(new);
893 fallback.set_tone(new);
894 }
895 Kind::Alternate {
896 options: children, ..
897 }
898 | Kind::Row(children)
899 | Kind::Column(children)
900 | Kind::Grid {
901 cells: children, ..
902 }
903 | Kind::List(children)
904 | Kind::Timeline(children) => {
905 children.iter_mut().for_each(|child| child.set_tone(new));
906 }
907 Kind::Step {
908 leading, content, ..
909 } => {
910 leading.iter_mut().for_each(|child| child.set_tone(new));
911 content.set_tone(new);
912 }
913 Kind::Button { content, .. }
914 | Kind::Gulp(content)
915 | Kind::Center(content)
916 | Kind::Display(content)
917 | Kind::Mark(content)
918 | Kind::Counted { content, .. }
919 | Kind::Scroll { content, .. }
920 | Kind::Detail { value: content, .. } => content.set_tone(new),
921 Kind::ListItem {
922 leading, trailing, ..
923 }
924 | Kind::Header {
925 leading, trailing, ..
926 } => {
927 leading
928 .iter_mut()
929 .chain(trailing.iter_mut())
930 .for_each(|child| child.set_tone(new));
931 }
932 Kind::Tabs(pages) => {
933 for (_, page) in pages {
934 page.set_tone(new);
935 }
936 }
937 Kind::Switcher { options, .. } => {
938 options
939 .iter_mut()
940 .for_each(|(child, _)| child.set_tone(new));
941 }
942 Kind::Separator
943 | Kind::Divider { .. }
944 | Kind::Diff { .. }
945 | Kind::Empty
946 | Kind::Section { .. }
947 | Kind::Fill
948 | Kind::Slider { .. }
949 | Kind::TextArea { .. }
950 | Kind::Toggle { .. } => {}
951 }
952 }
953
954 pub fn map<N: 'static>(self, f: &(impl Fn(M) -> N + Clone + 'static)) -> Node<N>
958 where
959 M: 'static,
960 {
961 let map_all = |nodes: Vec<Node<M>>| nodes.into_iter().map(|node| node.map(f)).collect();
962 let map_box = |node: Option<Box<Node<M>>>| node.map(|node| Box::new(node.map(f)));
963
964 Node(match self.0 {
965 Kind::Label { text, tone } => Kind::Label { text, tone },
966 Kind::Heading { text, tone } => Kind::Heading { text, tone },
967 Kind::Icon { icon, tone } => Kind::Icon { icon, tone },
968 Kind::AppIcon { app_id, tone } => Kind::AppIcon { app_id, tone },
969 Kind::Image { file, tint, tone } => Kind::Image { file, tint, tone },
970 Kind::Sprite {
971 frames,
972 order,
973 looping,
974 tone,
975 } => Kind::Sprite {
976 frames,
977 order,
978 looping,
979 tone,
980 },
981 Kind::Cover {
982 url,
983 fallback,
984 large,
985 tone,
986 } => Kind::Cover {
987 url,
988 fallback: Box::new(fallback.map(f)),
989 large,
990 tone,
991 },
992 Kind::Counter {
993 value,
994 range,
995 unit,
996 tone,
997 } => Kind::Counter {
998 value,
999 range,
1000 unit,
1001 tone,
1002 },
1003 Kind::Alternate {
1004 options,
1005 current,
1006 hovered,
1007 grows,
1008 } => Kind::Alternate {
1009 options: map_all(options),
1010 current,
1011 hovered,
1012 grows,
1013 },
1014 Kind::Row(children) => Kind::Row(map_all(children)),
1015 Kind::Column(children) => Kind::Column(map_all(children)),
1016 Kind::Grid { columns, cells } => Kind::Grid {
1017 columns,
1018 cells: map_all(cells),
1019 },
1020 Kind::Button {
1021 content,
1022 on_press,
1023 on_secondary,
1024 prominent,
1025 } => Kind::Button {
1026 content: Box::new(content.map(f)),
1027 on_press: f(on_press),
1028 on_secondary: on_secondary.map(f),
1029 prominent,
1030 },
1031 Kind::Switcher { options, current } => Kind::Switcher {
1032 options: options
1033 .into_iter()
1034 .map(|(node, message)| (node.map(f), f(message)))
1035 .collect(),
1036 current,
1037 },
1038 Kind::Gulp(content) => Kind::Gulp(Box::new(content.map(f))),
1039 Kind::Center(content) => Kind::Center(Box::new(content.map(f))),
1040 Kind::Separator => Kind::Separator,
1041 Kind::Empty => Kind::Empty,
1042 Kind::ListItem {
1043 title,
1044 subtitle,
1045 leading,
1046 trailing,
1047 selected,
1048 on_press,
1049 on_secondary,
1050 } => Kind::ListItem {
1051 title,
1052 subtitle,
1053 leading: map_box(leading),
1054 trailing: map_box(trailing),
1055 selected,
1056 on_press: on_press.map(f),
1057 on_secondary: on_secondary.map(f),
1058 },
1059 Kind::List(rows) => Kind::List(map_all(rows)),
1060 Kind::Section { title, trailing } => Kind::Section {
1061 title,
1062 trailing: map_box(trailing),
1063 },
1064 Kind::Header {
1065 title,
1066 subtitle,
1067 on_back,
1068 leading,
1069 trailing,
1070 } => Kind::Header {
1071 title,
1072 subtitle,
1073 on_back: on_back.map(f),
1074 leading: map_box(leading),
1075 trailing: map_box(trailing),
1076 },
1077 Kind::Detail { name, value } => Kind::Detail {
1078 name,
1079 value: Box::new(value.map(f)),
1080 },
1081 Kind::Fill => Kind::Fill,
1082 Kind::Slider {
1083 value,
1084 range,
1085 step,
1086 on_change,
1087 on_release,
1088 } => {
1089 let g = f.clone();
1090 Kind::Slider {
1091 value,
1092 range,
1093 step,
1094 on_change: Box::new(move |value| g(on_change(value))),
1095 on_release: on_release.map(f),
1096 }
1097 }
1098 Kind::Toggle { on, on_toggle } => Kind::Toggle {
1099 on,
1100 on_toggle: f(on_toggle),
1101 },
1102 Kind::Meter {
1103 fraction,
1104 limit,
1105 tone,
1106 } => Kind::Meter {
1107 fraction,
1108 limit,
1109 tone,
1110 },
1111 Kind::TextInput {
1112 placeholder,
1113 value,
1114 on_input,
1115 on_submit,
1116 secret,
1117 trailing,
1118 tone,
1119 } => {
1120 let g = f.clone();
1121 Kind::TextInput {
1122 placeholder,
1123 value,
1124 on_input: Box::new(move |text| g(on_input(text))),
1125 on_submit: on_submit.map(f),
1126 secret,
1127 trailing: map_box(trailing),
1128 tone,
1129 }
1130 }
1131 Kind::Display(content) => Kind::Display(Box::new(content.map(f))),
1132 Kind::Mark(content) => Kind::Mark(Box::new(content.map(f))),
1133 Kind::Counted { content, count } => Kind::Counted {
1134 content: Box::new(content.map(f)),
1135 count,
1136 },
1137 Kind::Badge { text, tone } => Kind::Badge { text, tone },
1138 Kind::Tabs(pages) => Kind::Tabs(
1139 pages
1140 .into_iter()
1141 .map(|(name, page)| (name, page.map(f)))
1142 .collect(),
1143 ),
1144 Kind::Markdown { text, tone } => Kind::Markdown { text, tone },
1145 Kind::TextArea {
1146 placeholder,
1147 value,
1148 on_input,
1149 on_submit,
1150 trailing,
1151 } => {
1152 let g = f.clone();
1153 Kind::TextArea {
1154 placeholder,
1155 value,
1156 on_input: Box::new(move |text| g(on_input(text))),
1157 on_submit: on_submit.map(f),
1158 trailing: map_box(trailing),
1159 }
1160 }
1161 Kind::Scroll { content, bottom } => Kind::Scroll {
1162 content: Box::new(content.map(f)),
1163 bottom,
1164 },
1165 Kind::Literal { text, tone } => Kind::Literal { text, tone },
1166 Kind::Divider { text } => Kind::Divider { text },
1167 Kind::Timeline(steps) => Kind::Timeline(map_all(steps)),
1168 Kind::Step {
1169 mark,
1170 leading,
1171 content,
1172 } => Kind::Step {
1173 mark,
1174 leading: map_box(leading),
1175 content: Box::new(content.map(f)),
1176 },
1177 Kind::Diff { title, lines } => Kind::Diff { title, lines },
1178 })
1179 }
1180
1181 #[doc(hidden)]
1182 pub fn into_kind(self) -> Kind<M> {
1183 self.0
1184 }
1185
1186 #[doc(hidden)]
1187 pub fn kind(&self) -> &Kind<M> {
1188 &self.0
1189 }
1190}
1191
1192pub struct Item<M> {
1194 content: Option<Node<M>>,
1195 on_press: Option<M>,
1196 on_secondary: Option<M>,
1197 on_scroll: Option<(M, M)>,
1198}
1199
1200impl<M> Item<M> {
1201 pub fn new(content: Node<M>) -> Self {
1202 Self {
1203 content: Some(content),
1204 on_press: None,
1205 on_secondary: None,
1206 on_scroll: None,
1207 }
1208 }
1209
1210 pub fn hidden() -> Self {
1212 Self {
1213 content: None,
1214 on_press: None,
1215 on_secondary: None,
1216 on_scroll: None,
1217 }
1218 }
1219
1220 pub fn on_press(mut self, message: M) -> Self {
1222 self.on_press = Some(message);
1223 self
1224 }
1225
1226 pub fn on_secondary(mut self, message: M) -> Self {
1228 self.on_secondary = Some(message);
1229 self
1230 }
1231
1232 pub fn on_scroll(mut self, up: M, down: M) -> Self {
1235 self.on_scroll = Some((up, down));
1236 self
1237 }
1238
1239 pub fn map<N: 'static>(self, f: &(impl Fn(M) -> N + Clone + 'static)) -> Item<N>
1240 where
1241 M: 'static,
1242 {
1243 Item {
1244 content: self.content.map(|content| content.map(f)),
1245 on_press: self.on_press.map(f),
1246 on_secondary: self.on_secondary.map(f),
1247 on_scroll: self.on_scroll.map(|(up, down)| (f(up), f(down))),
1248 }
1249 }
1250
1251 #[doc(hidden)]
1252 pub fn into_parts(self) -> ItemParts<M> {
1253 ItemParts {
1254 content: self.content,
1255 on_press: self.on_press,
1256 on_secondary: self.on_secondary,
1257 on_scroll: self.on_scroll,
1258 }
1259 }
1260}
1261
1262#[doc(hidden)]
1263pub struct ItemParts<M> {
1264 pub content: Option<Node<M>>,
1265 pub on_press: Option<M>,
1266 pub on_secondary: Option<M>,
1267 pub on_scroll: Option<(M, M)>,
1269}
1270
1271pub struct Panel<M> {
1274 content: Node<M>,
1275 on_open: Option<M>,
1276 on_close: Option<M>,
1277 wide: bool,
1278}
1279
1280impl<M> Panel<M> {
1281 pub fn new(content: Node<M>) -> Self {
1282 Self {
1283 content,
1284 on_open: None,
1285 on_close: None,
1286 wide: false,
1287 }
1288 }
1289
1290 pub fn wide(mut self) -> Self {
1293 self.wide = true;
1294 self
1295 }
1296
1297 #[doc(hidden)]
1298 pub fn is_wide(&self) -> bool {
1299 self.wide
1300 }
1301
1302 pub fn on_open(mut self, message: M) -> Self {
1304 self.on_open = Some(message);
1305 self
1306 }
1307
1308 pub fn on_close(mut self, message: M) -> Self {
1310 self.on_close = Some(message);
1311 self
1312 }
1313
1314 pub fn map<N: 'static>(self, f: &(impl Fn(M) -> N + Clone + 'static)) -> Panel<N>
1315 where
1316 M: 'static,
1317 {
1318 Panel {
1319 content: self.content.map(f),
1320 on_open: self.on_open.map(f),
1321 on_close: self.on_close.map(f),
1322 wide: self.wide,
1323 }
1324 }
1325
1326 #[doc(hidden)]
1327 pub fn into_node(self) -> Node<M> {
1328 self.content
1329 }
1330
1331 #[doc(hidden)]
1332 pub fn on_open_message(self) -> Option<M> {
1333 self.on_open
1334 }
1335
1336 #[doc(hidden)]
1337 pub fn on_close_message(self) -> Option<M> {
1338 self.on_close
1339 }
1340}
1341
1342#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1344pub enum Urgency {
1345 Low,
1347 #[default]
1348 Normal,
1349 Critical,
1351}
1352
1353pub struct Notice<M> {
1360 key: u64,
1361 summary: String,
1362 app: Option<String>,
1363 app_icon: Option<String>,
1364 image: Option<String>,
1365 pixels: Option<Pixels>,
1366 body: Option<String>,
1367 markdown: bool,
1368 urgency: Urgency,
1369 time: Option<SystemTime>,
1370 thread: Option<String>,
1371 thread_name: Option<String>,
1372 progress: Option<f32>,
1373 actions: Vec<(String, M)>,
1374 choices: Option<ChoicesParts<M>>,
1375 step: Option<StepParts<M>>,
1376 on_press: Option<M>,
1377 on_dismiss: Option<M>,
1378 expires: Option<(std::time::Duration, M)>,
1379 reply: Option<Reply<M>>,
1380}
1381
1382#[derive(Clone)]
1388pub struct Pixels {
1389 width: u32,
1390 height: u32,
1391 rgba: Arc<[u8]>,
1392}
1393
1394impl Pixels {
1395 pub fn new(width: u32, height: u32, rgba: impl Into<Arc<[u8]>>) -> Option<Self> {
1397 let rgba = rgba.into();
1398 let expected = (width as usize)
1399 .checked_mul(height as usize)?
1400 .checked_mul(4)?;
1401 (width > 0 && height > 0 && rgba.len() == expected).then_some(Self {
1402 width,
1403 height,
1404 rgba,
1405 })
1406 }
1407
1408 pub fn width(&self) -> u32 {
1409 self.width
1410 }
1411
1412 pub fn height(&self) -> u32 {
1413 self.height
1414 }
1415
1416 pub fn rgba(&self) -> &Arc<[u8]> {
1417 &self.rgba
1418 }
1419}
1420
1421impl std::fmt::Debug for Pixels {
1422 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1423 write!(f, "Pixels({}×{})", self.width, self.height)
1424 }
1425}
1426
1427pub struct Choices<M> {
1430 parts: ChoicesParts<M>,
1431}
1432
1433impl<M> Choices<M> {
1434 pub fn one() -> Self {
1436 Self::new(false)
1437 }
1438
1439 pub fn many() -> Self {
1442 Self::new(true)
1443 }
1444
1445 fn new(multiple: bool) -> Self {
1446 Self {
1447 parts: ChoicesParts {
1448 multiple,
1449 options: Vec::new(),
1450 on_send: None,
1451 },
1452 }
1453 }
1454
1455 pub fn option(
1457 mut self,
1458 label: impl Into<String>,
1459 description: Option<String>,
1460 picked: bool,
1461 on_pick: M,
1462 ) -> Self {
1463 self.parts.options.push(ChoiceParts {
1464 label: label.into(),
1465 description: description.filter(|d| !d.is_empty()),
1466 recommended: false,
1467 picked,
1468 on_pick,
1469 });
1470 self
1471 }
1472
1473 pub fn recommended(mut self) -> Self {
1475 if let Some(option) = self.parts.options.last_mut() {
1476 option.recommended = true;
1477 }
1478 self
1479 }
1480
1481 pub fn on_send(mut self, message: M) -> Self {
1483 self.parts.on_send = Some(message);
1484 self
1485 }
1486
1487 fn map<N>(self, f: &impl Fn(M) -> N) -> Choices<N> {
1488 let parts = self.parts;
1489 Choices {
1490 parts: ChoicesParts {
1491 multiple: parts.multiple,
1492 options: parts
1493 .options
1494 .into_iter()
1495 .map(|option| ChoiceParts {
1496 label: option.label,
1497 description: option.description,
1498 recommended: option.recommended,
1499 picked: option.picked,
1500 on_pick: f(option.on_pick),
1501 })
1502 .collect(),
1503 on_send: parts.on_send.map(f),
1504 },
1505 }
1506 }
1507}
1508
1509#[doc(hidden)]
1510#[derive(Clone)]
1511pub struct ChoicesParts<M> {
1512 pub multiple: bool,
1513 pub options: Vec<ChoiceParts<M>>,
1514 pub on_send: Option<M>,
1515}
1516
1517#[doc(hidden)]
1518#[derive(Clone)]
1519pub struct ChoiceParts<M> {
1520 pub label: String,
1521 pub description: Option<String>,
1522 pub recommended: bool,
1523 pub picked: bool,
1524 pub on_pick: M,
1525}
1526
1527#[doc(hidden)]
1528#[derive(Clone)]
1529pub struct StepParts<M> {
1530 pub index: usize,
1532 pub count: usize,
1533 pub on_back: Option<M>,
1534}
1535
1536pub struct Reply<M> {
1543 placeholder: String,
1544 value: String,
1545 on_input: std::rc::Rc<dyn Fn(String) -> M>,
1546 on_submit: Option<M>,
1547 on_cancel: Option<M>,
1548 sent: bool,
1549}
1550
1551impl<M> Reply<M> {
1552 pub fn new(
1554 placeholder: impl Into<String>,
1555 value: impl Into<String>,
1556 on_input: impl Fn(String) -> M + 'static,
1557 ) -> Self {
1558 Self {
1559 placeholder: placeholder.into(),
1560 value: value.into(),
1561 on_input: std::rc::Rc::new(on_input),
1562 on_submit: None,
1563 on_cancel: None,
1564 sent: false,
1565 }
1566 }
1567
1568 pub fn on_submit(mut self, message: M) -> Self {
1570 self.on_submit = Some(message);
1571 self
1572 }
1573
1574 pub fn on_cancel(mut self, message: M) -> Self {
1576 self.on_cancel = Some(message);
1577 self
1578 }
1579
1580 pub fn sent(mut self, sent: bool) -> Self {
1583 self.sent = sent;
1584 self
1585 }
1586
1587 pub fn map<N: 'static>(self, f: &(impl Fn(M) -> N + Clone + 'static)) -> Reply<N>
1588 where
1589 M: 'static,
1590 {
1591 let g = f.clone();
1592 let on_input = self.on_input;
1593 Reply {
1594 placeholder: self.placeholder,
1595 value: self.value,
1596 on_input: std::rc::Rc::new(move |text| g(on_input(text))),
1597 on_submit: self.on_submit.map(f),
1598 on_cancel: self.on_cancel.map(f),
1599 sent: self.sent,
1600 }
1601 }
1602
1603 #[doc(hidden)]
1604 pub fn into_parts(self) -> ReplyParts<M> {
1605 ReplyParts {
1606 placeholder: self.placeholder,
1607 value: self.value,
1608 on_input: self.on_input,
1609 on_submit: self.on_submit,
1610 on_cancel: self.on_cancel,
1611 sent: self.sent,
1612 }
1613 }
1614}
1615
1616#[doc(hidden)]
1617#[derive(Clone)]
1618pub struct ReplyParts<M> {
1619 pub placeholder: String,
1620 pub value: String,
1621 pub on_input: std::rc::Rc<dyn Fn(String) -> M>,
1622 pub on_submit: Option<M>,
1623 pub on_cancel: Option<M>,
1624 pub sent: bool,
1625}
1626
1627impl<M> Notice<M> {
1628 pub fn new(key: u64, summary: impl Into<String>) -> Self {
1629 Self {
1630 key,
1631 summary: summary.into(),
1632 app: None,
1633 app_icon: None,
1634 image: None,
1635 pixels: None,
1636 body: None,
1637 markdown: false,
1638 urgency: Urgency::Normal,
1639 time: None,
1640 thread: None,
1641 thread_name: None,
1642 progress: None,
1643 actions: Vec::new(),
1644 choices: None,
1645 step: None,
1646 on_press: None,
1647 on_dismiss: None,
1648 expires: None,
1649 reply: None,
1650 }
1651 }
1652
1653 pub fn app(mut self, name: impl Into<String>) -> Self {
1655 self.app = Some(name.into());
1656 self
1657 }
1658
1659 pub fn app_icon(mut self, app_id: impl Into<String>) -> Self {
1661 self.app_icon = Some(app_id.into());
1662 self
1663 }
1664
1665 pub fn image(mut self, url: impl Into<String>) -> Self {
1668 self.image = Some(url.into());
1669 self
1670 }
1671
1672 pub fn pixels(mut self, pixels: Pixels) -> Self {
1675 self.pixels = Some(pixels);
1676 self
1677 }
1678
1679 pub fn time(mut self, time: SystemTime) -> Self {
1681 self.time = Some(time);
1682 self
1683 }
1684
1685 pub fn thread(mut self, key: impl Into<String>) -> Self {
1688 self.thread = Some(key.into());
1689 self
1690 }
1691
1692 pub fn thread_name(mut self, name: impl Into<String>) -> Self {
1694 self.thread_name = Some(name.into());
1695 self
1696 }
1697
1698 pub fn progress(mut self, fraction: f32) -> Self {
1700 self.progress = Some(fraction.clamp(0.0, 1.0));
1701 self
1702 }
1703
1704 pub fn choices(mut self, choices: Choices<M>) -> Self {
1706 self.choices = Some(choices.parts);
1707 self
1708 }
1709
1710 pub fn step(mut self, index: usize, count: usize, on_back: Option<M>) -> Self {
1714 self.step = Some(StepParts {
1715 index,
1716 count,
1717 on_back,
1718 });
1719 self
1720 }
1721
1722 pub fn body(mut self, text: impl Into<String>) -> Self {
1724 self.body = Some(text.into());
1725 self
1726 }
1727
1728 pub fn markdown(mut self) -> Self {
1730 self.markdown = true;
1731 self
1732 }
1733
1734 pub fn urgency(mut self, urgency: Urgency) -> Self {
1735 self.urgency = urgency;
1736 self
1737 }
1738
1739 pub fn action(mut self, label: impl Into<String>, message: M) -> Self {
1741 self.actions.push((label.into(), message));
1742 self
1743 }
1744
1745 pub fn on_press(mut self, message: M) -> Self {
1747 self.on_press = Some(message);
1748 self
1749 }
1750
1751 pub fn on_dismiss(mut self, message: M) -> Self {
1753 self.on_dismiss = Some(message);
1754 self
1755 }
1756
1757 pub fn expires(mut self, after: std::time::Duration, message: M) -> Self {
1761 self.expires = Some((after, message));
1762 self
1763 }
1764
1765 pub fn reply(mut self, reply: Reply<M>) -> Self {
1767 self.reply = Some(reply);
1768 self
1769 }
1770
1771 pub fn map<N: 'static>(self, f: &(impl Fn(M) -> N + Clone + 'static)) -> Notice<N>
1772 where
1773 M: 'static,
1774 {
1775 Notice {
1776 key: self.key,
1777 summary: self.summary,
1778 app: self.app,
1779 app_icon: self.app_icon,
1780 image: self.image,
1781 pixels: self.pixels,
1782 body: self.body,
1783 markdown: self.markdown,
1784 urgency: self.urgency,
1785 time: self.time,
1786 thread: self.thread,
1787 thread_name: self.thread_name,
1788 progress: self.progress,
1789 actions: self
1790 .actions
1791 .into_iter()
1792 .map(|(label, message)| (label, f(message)))
1793 .collect(),
1794 choices: self.choices.map(|parts| Choices { parts }.map(f).parts),
1795 step: self.step.map(|step| StepParts {
1796 index: step.index,
1797 count: step.count,
1798 on_back: step.on_back.map(f),
1799 }),
1800 on_press: self.on_press.map(f),
1801 on_dismiss: self.on_dismiss.map(f),
1802 expires: self.expires.map(|(after, message)| (after, f(message))),
1803 reply: self.reply.map(|reply| reply.map(f)),
1804 }
1805 }
1806
1807 pub fn key(&self) -> u64 {
1808 self.key
1809 }
1810
1811 #[doc(hidden)]
1812 pub fn into_parts(self) -> NoticeParts<M> {
1813 NoticeParts {
1814 key: self.key,
1815 summary: self.summary,
1816 app: self.app,
1817 app_icon: self.app_icon,
1818 image: self.image,
1819 pixels: self.pixels,
1820 body: self.body,
1821 markdown: self.markdown,
1822 urgency: self.urgency,
1823 time: self.time,
1824 thread: self.thread,
1825 thread_name: self.thread_name,
1826 progress: self.progress,
1827 actions: self.actions,
1828 choices: self.choices,
1829 step: self.step,
1830 on_press: self.on_press,
1831 on_dismiss: self.on_dismiss,
1832 expires: self.expires,
1833 reply: self.reply.map(Reply::into_parts),
1834 }
1835 }
1836}
1837
1838#[doc(hidden)]
1839#[derive(Clone)]
1840pub struct NoticeParts<M> {
1841 pub key: u64,
1842 pub summary: String,
1843 pub app: Option<String>,
1844 pub app_icon: Option<String>,
1845 pub image: Option<String>,
1846 pub pixels: Option<Pixels>,
1847 pub body: Option<String>,
1848 pub markdown: bool,
1849 pub urgency: Urgency,
1850 pub time: Option<SystemTime>,
1851 pub thread: Option<String>,
1852 pub thread_name: Option<String>,
1853 pub progress: Option<f32>,
1854 pub actions: Vec<(String, M)>,
1855 pub choices: Option<ChoicesParts<M>>,
1856 pub step: Option<StepParts<M>>,
1857 pub on_press: Option<M>,
1858 pub on_dismiss: Option<M>,
1859 pub expires: Option<(std::time::Duration, M)>,
1860 pub reply: Option<ReplyParts<M>>,
1861}
1862
1863pub struct Action<M> {
1867 title: String,
1868 subtitle: Option<String>,
1869 keywords: Vec<String>,
1870 leading: Option<Node<M>>,
1871 message: M,
1872}
1873
1874pub fn action<M>(title: impl Into<String>, message: M) -> Action<M> {
1876 Action {
1877 title: title.into(),
1878 subtitle: None,
1879 keywords: Vec::new(),
1880 leading: None,
1881 message,
1882 }
1883}
1884
1885impl<M> Action<M> {
1886 pub fn subtitle(mut self, text: impl Into<String>) -> Self {
1888 self.subtitle = Some(text.into());
1889 self
1890 }
1891
1892 pub fn keywords(mut self, words: impl IntoIterator<Item = impl Into<String>>) -> Self {
1894 self.keywords.extend(words.into_iter().map(Into::into));
1895 self
1896 }
1897
1898 pub fn leading(mut self, node: Node<M>) -> Self {
1900 self.leading = Some(node);
1901 self
1902 }
1903
1904 pub fn map<N: 'static>(self, f: &(impl Fn(M) -> N + Clone + 'static)) -> Action<N>
1905 where
1906 M: 'static,
1907 {
1908 Action {
1909 title: self.title,
1910 subtitle: self.subtitle,
1911 keywords: self.keywords,
1912 leading: self.leading.map(|node| node.map(f)),
1913 message: f(self.message),
1914 }
1915 }
1916
1917 pub fn title(&self) -> &str {
1918 &self.title
1919 }
1920
1921 #[doc(hidden)]
1922 pub fn into_parts(self) -> ActionParts<M> {
1923 ActionParts {
1924 title: self.title,
1925 subtitle: self.subtitle,
1926 keywords: self.keywords,
1927 leading: self.leading,
1928 message: self.message,
1929 }
1930 }
1931}
1932
1933#[doc(hidden)]
1934pub struct ActionParts<M> {
1935 pub title: String,
1936 pub subtitle: Option<String>,
1937 pub keywords: Vec<String>,
1938 pub leading: Option<Node<M>>,
1939 pub message: M,
1940}
1941
1942#[cfg(test)]
1943mod tests {
1944 use super::*;
1945
1946 #[test]
1947 fn map_converts_every_message() {
1948 let node = row([
1949 button(label("a"), 1).on_secondary(4),
1950 column([button(label("b"), 2), label("c")]),
1951 switcher([(label("d"), 3)], Some(0)),
1952 ]);
1953
1954 let Kind::Row(children) = node.map(&|n: i32| n * 10).into_kind() else {
1955 panic!("row became something else");
1956 };
1957 let mut messages = Vec::new();
1958 collect(children, &mut messages);
1959 assert_eq!(messages, [10, 40, 20, 30]);
1960 }
1961
1962 fn collect(nodes: Vec<Node<i32>>, out: &mut Vec<i32>) {
1963 for node in nodes {
1964 match node.into_kind() {
1965 Kind::Button {
1966 on_press,
1967 on_secondary,
1968 ..
1969 } => {
1970 out.push(on_press);
1971 out.extend(on_secondary);
1972 }
1973 Kind::Switcher { options, .. } => {
1974 out.extend(options.into_iter().map(|(_, message)| message))
1975 }
1976 Kind::Row(children) | Kind::Column(children) => collect(children, out),
1977 _ => {}
1978 }
1979 }
1980 }
1981
1982 #[test]
1983 fn hover_shows_the_second_option() {
1984 let node: Node<()> = hover(label("a"), label("b"));
1985 assert!(matches!(
1986 node.into_kind(),
1987 Kind::Alternate {
1988 current: 0,
1989 hovered: Some(1),
1990 ..
1991 }
1992 ));
1993 let node: Node<()> = label("a").on_hover(1);
1994 assert!(matches!(node.into_kind(), Kind::Label { .. }));
1995 }
1996
1997 #[test]
1998 fn tone_fills_only_unset_text() {
1999 let node: Node<()> =
2000 row([label("a").tone(Tone::Accent), label("b"), icon(Icon::Wired)]).tone(Tone::Dim);
2001 let Kind::Row(children) = node.into_kind() else {
2002 unreachable!()
2003 };
2004 let tones: Vec<_> = children
2005 .into_iter()
2006 .map(|child| match child.into_kind() {
2007 Kind::Label { tone, .. } | Kind::Icon { tone, .. } => tone,
2008 _ => None,
2009 })
2010 .collect();
2011 assert_eq!(
2012 tones,
2013 [Some(Tone::Accent), Some(Tone::Dim), Some(Tone::Dim)]
2014 );
2015 }
2016
2017 #[test]
2018 fn map_reaches_new_nodes_and_their_closures() {
2019 let node = column([
2020 header("h").on_back(1).trailing(toggle(true, 2)),
2021 list([list_item("a")
2022 .on_press(3)
2023 .on_secondary(4)
2024 .trailing(button(icon(Icon::Info), 5))]),
2025 slider(0.5, 0.0..=1.0, |v| (v * 10.0) as i32).on_release(6),
2026 text_input("p", "v", |text: String| text.len() as i32).on_submit(7),
2027 scroll(text_area("p", "v", |text: String| text.len() as i32 + 1).on_submit(8)).bottom(),
2028 ]);
2029 let Kind::Column(children) = node.map(&|n: i32| n * 10).into_kind() else {
2030 panic!("column became something else");
2031 };
2032 let mut children = children.into_iter().map(Node::into_kind);
2033 let Some(Kind::Header {
2034 on_back: Some(10),
2035 trailing: Some(trailing),
2036 ..
2037 }) = children.next()
2038 else {
2039 panic!("header lost its back message");
2040 };
2041 assert!(matches!(
2042 trailing.into_kind(),
2043 Kind::Toggle { on_toggle: 20, .. }
2044 ));
2045 let Some(Kind::List(rows)) = children.next() else {
2046 panic!("list lost");
2047 };
2048 let Some(Kind::ListItem {
2049 on_press: Some(30),
2050 on_secondary: Some(40),
2051 trailing: Some(trailing),
2052 ..
2053 }) = rows.into_iter().next().map(Node::into_kind)
2054 else {
2055 panic!("list item lost its messages");
2056 };
2057 assert!(matches!(
2058 trailing.into_kind(),
2059 Kind::Button { on_press: 50, .. }
2060 ));
2061 let Some(Kind::Slider {
2062 on_change,
2063 on_release: Some(60),
2064 ..
2065 }) = children.next()
2066 else {
2067 panic!("slider lost");
2068 };
2069 assert_eq!(on_change(0.3), 30);
2070 let Some(Kind::TextInput {
2071 on_input,
2072 on_submit: Some(70),
2073 ..
2074 }) = children.next()
2075 else {
2076 panic!("text input lost");
2077 };
2078 assert_eq!(on_input("abcd".into()), 40);
2079 let Some(Kind::Scroll {
2080 content,
2081 bottom: true,
2082 }) = children.next()
2083 else {
2084 panic!("scroll lost");
2085 };
2086 let Kind::TextArea {
2087 on_input,
2088 on_submit: Some(80),
2089 ..
2090 } = content.into_kind()
2091 else {
2092 panic!("text area lost");
2093 };
2094 assert_eq!(on_input("abcd".into()), 50);
2095 }
2096
2097 #[test]
2098 fn tabs_map_and_tone_every_page() {
2099 let node = tabs([("a", button(label("x"), 1)), ("b", button(label("y"), 2))])
2100 .tone(Tone::Dim)
2101 .map(&|n: i32| n * 10);
2102 let Kind::Tabs(pages) = node.into_kind() else {
2103 panic!("tabs became something else");
2104 };
2105 let names: Vec<_> = pages.iter().map(|(name, _)| name.as_str()).collect();
2106 assert_eq!(names, ["a", "b"]);
2107 let mut messages = Vec::new();
2108 for (_, page) in pages {
2109 let Kind::Button {
2110 content, on_press, ..
2111 } = page.into_kind()
2112 else {
2113 panic!("page lost its button");
2114 };
2115 assert!(matches!(
2116 content.into_kind(),
2117 Kind::Label {
2118 tone: Some(Tone::Dim),
2119 ..
2120 }
2121 ));
2122 messages.push(on_press);
2123 }
2124 assert_eq!(messages, [10, 20]);
2125 }
2126
2127 #[test]
2128 fn notice_map_reaches_every_message() {
2129 let notice = Notice::new(7, "s")
2130 .action("a", 1)
2131 .action("b", 2)
2132 .on_press(3)
2133 .on_dismiss(4)
2134 .expires(std::time::Duration::from_secs(5), 5)
2135 .reply(
2136 Reply::new("p", "v", |text: String| text.len() as i32)
2137 .on_submit(6)
2138 .on_cancel(7),
2139 )
2140 .choices(
2141 Choices::many()
2142 .option("x", None, false, 8)
2143 .option("y", Some("why".into()), true, 9)
2144 .recommended()
2145 .on_send(10),
2146 )
2147 .step(1, 3, Some(11))
2148 .map(&|n: i32| n * 10);
2149 assert_eq!(notice.key(), 7);
2150 let parts = notice.into_parts();
2151 let actions: Vec<_> = parts.actions.into_iter().map(|(_, m)| m).collect();
2152 assert_eq!(actions, [10, 20]);
2153 assert_eq!(parts.on_press, Some(30));
2154 assert_eq!(parts.on_dismiss, Some(40));
2155 assert_eq!(parts.expires.map(|(_, m)| m), Some(50));
2156 let reply = parts.reply.unwrap();
2157 assert_eq!((reply.on_input)("abc".into()), 30);
2158 assert_eq!((reply.on_submit, reply.on_cancel), (Some(60), Some(70)));
2159 let choices = parts.choices.unwrap();
2160 let picks: Vec<_> = choices.options.iter().map(|o| o.on_pick).collect();
2161 assert_eq!(picks, [80, 90]);
2162 assert!(choices.multiple && choices.options[1].recommended && choices.options[1].picked);
2163 assert_eq!(choices.on_send, Some(100));
2164 let step = parts.step.unwrap();
2165 assert_eq!((step.index, step.count, step.on_back), (1, 3, Some(110)));
2166 }
2167
2168 #[test]
2169 fn pixels_must_be_whole() {
2170 assert!(Pixels::new(2, 2, vec![0; 16]).is_some());
2171 assert!(Pixels::new(2, 2, vec![0; 15]).is_none());
2172 assert!(Pixels::new(0, 0, Vec::new()).is_none());
2173 assert!(Pixels::new(u32::MAX, u32::MAX, Vec::new()).is_none());
2174 }
2175
2176 #[test]
2177 fn progress_stays_between_none_and_all() {
2178 let parts = Notice::<()>::new(1, "s").progress(1.5).into_parts();
2179 assert_eq!(parts.progress, Some(1.0));
2180 }
2181
2182 #[test]
2183 fn modifiers_only_touch_their_nodes() {
2184 let node: Node<i32> = label("a").trailing(label("b")).on_press(1).selected(true);
2185 assert!(matches!(node.into_kind(), Kind::Label { .. }));
2186 let node: Node<i32> = button(label("a"), 1).on_press(2).prominent();
2187 assert!(matches!(
2188 node.into_kind(),
2189 Kind::Button {
2190 on_press: 1,
2191 prominent: true,
2192 ..
2193 }
2194 ));
2195 }
2196
2197 #[test]
2198 fn a_header_takes_a_picture_and_a_subtitle() {
2199 let node = header("t")
2200 .subtitle("s")
2201 .leading(button(icon(Icon::Info), 1))
2202 .tone(Tone::Dim)
2203 .map(&|n: i32| n * 10);
2204 let Kind::Header {
2205 subtitle: Some(subtitle),
2206 leading: Some(leading),
2207 ..
2208 } = node.into_kind()
2209 else {
2210 panic!("header lost its picture or subtitle");
2211 };
2212 assert_eq!(subtitle, "s");
2213 let Kind::Button {
2214 content,
2215 on_press: 10,
2216 ..
2217 } = leading.into_kind()
2218 else {
2219 panic!("picture lost its message");
2220 };
2221 assert!(matches!(
2222 content.into_kind(),
2223 Kind::Icon {
2224 tone: Some(Tone::Dim),
2225 ..
2226 }
2227 ));
2228 }
2229
2230 #[test]
2231 fn timelines_map_and_tone_their_steps() {
2232 let node = timeline([
2233 step(Mark::You, label("a")),
2234 step(Mark::Failed, button(literal("b"), 1)).leading(icon(Icon::Check)),
2235 label("c"),
2236 ])
2237 .tone(Tone::Dim)
2238 .map(&|n: i32| n * 10);
2239 let Kind::Timeline(steps) = node.into_kind() else {
2240 panic!("timeline became something else");
2241 };
2242 let mut steps = steps.into_iter().map(Node::into_kind);
2243 let Some(Kind::Step {
2244 mark: Mark::You,
2245 leading: None,
2246 content,
2247 }) = steps.next()
2248 else {
2249 panic!("first step lost");
2250 };
2251 assert!(matches!(
2252 content.into_kind(),
2253 Kind::Label {
2254 tone: Some(Tone::Dim),
2255 ..
2256 }
2257 ));
2258 let Some(Kind::Step {
2259 mark: Mark::Failed,
2260 leading: Some(leading),
2261 content,
2262 }) = steps.next()
2263 else {
2264 panic!("second step lost its picture");
2265 };
2266 assert!(matches!(
2267 leading.into_kind(),
2268 Kind::Icon {
2269 tone: Some(Tone::Dim),
2270 ..
2271 }
2272 ));
2273 let Kind::Button {
2274 content,
2275 on_press: 10,
2276 ..
2277 } = content.into_kind()
2278 else {
2279 panic!("button lost its message");
2280 };
2281 assert!(matches!(
2282 content.into_kind(),
2283 Kind::Literal {
2284 tone: Some(Tone::Dim),
2285 ..
2286 }
2287 ));
2288 assert!(matches!(steps.next(), Some(Kind::Label { .. })));
2289 let node: Node<()> = diff("f", [(Change::Added, "x".to_string())]).leading(label("y"));
2290 assert!(matches!(node.into_kind(), Kind::Diff { lines, .. } if lines.len() == 1));
2291 }
2292
2293 #[test]
2294 fn tone_reaches_details_and_row_icons() {
2295 let node: Node<()> = column([
2296 detail("n", label("v")),
2297 list_item("t").leading(icon(Icon::Lock)),
2298 ])
2299 .tone(Tone::Good);
2300 let Kind::Column(children) = node.into_kind() else {
2301 unreachable!()
2302 };
2303 let mut children = children.into_iter().map(Node::into_kind);
2304 let Some(Kind::Detail { value, .. }) = children.next() else {
2305 unreachable!()
2306 };
2307 assert!(matches!(
2308 value.into_kind(),
2309 Kind::Label {
2310 tone: Some(Tone::Good),
2311 ..
2312 }
2313 ));
2314 let Some(Kind::ListItem {
2315 leading: Some(leading),
2316 ..
2317 }) = children.next()
2318 else {
2319 unreachable!()
2320 };
2321 assert!(matches!(
2322 leading.into_kind(),
2323 Kind::Icon {
2324 tone: Some(Tone::Good),
2325 ..
2326 }
2327 ));
2328 }
2329}