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 icon: Option<Icon>,
1365 image: Option<String>,
1366 pixels: Option<Pixels>,
1367 body: Option<String>,
1368 markdown: bool,
1369 urgency: Urgency,
1370 time: Option<SystemTime>,
1371 thread: Option<String>,
1372 thread_name: Option<String>,
1373 progress: Option<f32>,
1374 actions: Vec<(String, M)>,
1375 choices: Option<ChoicesParts<M>>,
1376 step: Option<StepParts<M>>,
1377 on_press: Option<M>,
1378 on_dismiss: Option<M>,
1379 expires: Option<(std::time::Duration, M)>,
1380 reply: Option<Reply<M>>,
1381}
1382
1383#[derive(Clone)]
1389pub struct Pixels {
1390 width: u32,
1391 height: u32,
1392 rgba: Arc<[u8]>,
1393}
1394
1395impl Pixels {
1396 pub fn new(width: u32, height: u32, rgba: impl Into<Arc<[u8]>>) -> Option<Self> {
1398 let rgba = rgba.into();
1399 let expected = (width as usize)
1400 .checked_mul(height as usize)?
1401 .checked_mul(4)?;
1402 (width > 0 && height > 0 && rgba.len() == expected).then_some(Self {
1403 width,
1404 height,
1405 rgba,
1406 })
1407 }
1408
1409 pub fn width(&self) -> u32 {
1410 self.width
1411 }
1412
1413 pub fn height(&self) -> u32 {
1414 self.height
1415 }
1416
1417 pub fn rgba(&self) -> &Arc<[u8]> {
1418 &self.rgba
1419 }
1420}
1421
1422impl std::fmt::Debug for Pixels {
1423 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1424 write!(f, "Pixels({}×{})", self.width, self.height)
1425 }
1426}
1427
1428pub struct Choices<M> {
1431 parts: ChoicesParts<M>,
1432}
1433
1434impl<M> Choices<M> {
1435 pub fn one() -> Self {
1437 Self::new(false)
1438 }
1439
1440 pub fn many() -> Self {
1443 Self::new(true)
1444 }
1445
1446 fn new(multiple: bool) -> Self {
1447 Self {
1448 parts: ChoicesParts {
1449 multiple,
1450 options: Vec::new(),
1451 on_send: None,
1452 },
1453 }
1454 }
1455
1456 pub fn option(
1458 mut self,
1459 label: impl Into<String>,
1460 description: Option<String>,
1461 picked: bool,
1462 on_pick: M,
1463 ) -> Self {
1464 self.parts.options.push(ChoiceParts {
1465 label: label.into(),
1466 description: description.filter(|d| !d.is_empty()),
1467 recommended: false,
1468 picked,
1469 on_pick,
1470 });
1471 self
1472 }
1473
1474 pub fn recommended(mut self) -> Self {
1476 if let Some(option) = self.parts.options.last_mut() {
1477 option.recommended = true;
1478 }
1479 self
1480 }
1481
1482 pub fn on_send(mut self, message: M) -> Self {
1484 self.parts.on_send = Some(message);
1485 self
1486 }
1487
1488 fn map<N>(self, f: &impl Fn(M) -> N) -> Choices<N> {
1489 let parts = self.parts;
1490 Choices {
1491 parts: ChoicesParts {
1492 multiple: parts.multiple,
1493 options: parts
1494 .options
1495 .into_iter()
1496 .map(|option| ChoiceParts {
1497 label: option.label,
1498 description: option.description,
1499 recommended: option.recommended,
1500 picked: option.picked,
1501 on_pick: f(option.on_pick),
1502 })
1503 .collect(),
1504 on_send: parts.on_send.map(f),
1505 },
1506 }
1507 }
1508}
1509
1510#[doc(hidden)]
1511#[derive(Clone)]
1512pub struct ChoicesParts<M> {
1513 pub multiple: bool,
1514 pub options: Vec<ChoiceParts<M>>,
1515 pub on_send: Option<M>,
1516}
1517
1518#[doc(hidden)]
1519#[derive(Clone)]
1520pub struct ChoiceParts<M> {
1521 pub label: String,
1522 pub description: Option<String>,
1523 pub recommended: bool,
1524 pub picked: bool,
1525 pub on_pick: M,
1526}
1527
1528#[doc(hidden)]
1529#[derive(Clone)]
1530pub struct StepParts<M> {
1531 pub index: usize,
1533 pub count: usize,
1534 pub on_back: Option<M>,
1535}
1536
1537pub struct Reply<M> {
1544 placeholder: String,
1545 value: String,
1546 on_input: std::rc::Rc<dyn Fn(String) -> M>,
1547 on_submit: Option<M>,
1548 on_cancel: Option<M>,
1549 sent: bool,
1550}
1551
1552impl<M> Reply<M> {
1553 pub fn new(
1555 placeholder: impl Into<String>,
1556 value: impl Into<String>,
1557 on_input: impl Fn(String) -> M + 'static,
1558 ) -> Self {
1559 Self {
1560 placeholder: placeholder.into(),
1561 value: value.into(),
1562 on_input: std::rc::Rc::new(on_input),
1563 on_submit: None,
1564 on_cancel: None,
1565 sent: false,
1566 }
1567 }
1568
1569 pub fn on_submit(mut self, message: M) -> Self {
1571 self.on_submit = Some(message);
1572 self
1573 }
1574
1575 pub fn on_cancel(mut self, message: M) -> Self {
1577 self.on_cancel = Some(message);
1578 self
1579 }
1580
1581 pub fn sent(mut self, sent: bool) -> Self {
1584 self.sent = sent;
1585 self
1586 }
1587
1588 pub fn map<N: 'static>(self, f: &(impl Fn(M) -> N + Clone + 'static)) -> Reply<N>
1589 where
1590 M: 'static,
1591 {
1592 let g = f.clone();
1593 let on_input = self.on_input;
1594 Reply {
1595 placeholder: self.placeholder,
1596 value: self.value,
1597 on_input: std::rc::Rc::new(move |text| g(on_input(text))),
1598 on_submit: self.on_submit.map(f),
1599 on_cancel: self.on_cancel.map(f),
1600 sent: self.sent,
1601 }
1602 }
1603
1604 #[doc(hidden)]
1605 pub fn into_parts(self) -> ReplyParts<M> {
1606 ReplyParts {
1607 placeholder: self.placeholder,
1608 value: self.value,
1609 on_input: self.on_input,
1610 on_submit: self.on_submit,
1611 on_cancel: self.on_cancel,
1612 sent: self.sent,
1613 }
1614 }
1615}
1616
1617#[doc(hidden)]
1618#[derive(Clone)]
1619pub struct ReplyParts<M> {
1620 pub placeholder: String,
1621 pub value: String,
1622 pub on_input: std::rc::Rc<dyn Fn(String) -> M>,
1623 pub on_submit: Option<M>,
1624 pub on_cancel: Option<M>,
1625 pub sent: bool,
1626}
1627
1628impl<M> Notice<M> {
1629 pub fn new(key: u64, summary: impl Into<String>) -> Self {
1630 Self {
1631 key,
1632 summary: summary.into(),
1633 app: None,
1634 app_icon: None,
1635 icon: None,
1636 image: None,
1637 pixels: None,
1638 body: None,
1639 markdown: false,
1640 urgency: Urgency::Normal,
1641 time: None,
1642 thread: None,
1643 thread_name: None,
1644 progress: None,
1645 actions: Vec::new(),
1646 choices: None,
1647 step: None,
1648 on_press: None,
1649 on_dismiss: None,
1650 expires: None,
1651 reply: None,
1652 }
1653 }
1654
1655 pub fn app(mut self, name: impl Into<String>) -> Self {
1657 self.app = Some(name.into());
1658 self
1659 }
1660
1661 pub fn app_icon(mut self, app_id: impl Into<String>) -> Self {
1663 self.app_icon = Some(app_id.into());
1664 self
1665 }
1666
1667 pub fn icon(mut self, icon: Icon) -> Self {
1671 self.icon = Some(icon);
1672 self
1673 }
1674
1675 pub fn image(mut self, url: impl Into<String>) -> Self {
1678 self.image = Some(url.into());
1679 self
1680 }
1681
1682 pub fn pixels(mut self, pixels: Pixels) -> Self {
1685 self.pixels = Some(pixels);
1686 self
1687 }
1688
1689 pub fn time(mut self, time: SystemTime) -> Self {
1691 self.time = Some(time);
1692 self
1693 }
1694
1695 pub fn thread(mut self, key: impl Into<String>) -> Self {
1698 self.thread = Some(key.into());
1699 self
1700 }
1701
1702 pub fn thread_name(mut self, name: impl Into<String>) -> Self {
1704 self.thread_name = Some(name.into());
1705 self
1706 }
1707
1708 pub fn progress(mut self, fraction: f32) -> Self {
1710 self.progress = Some(fraction.clamp(0.0, 1.0));
1711 self
1712 }
1713
1714 pub fn choices(mut self, choices: Choices<M>) -> Self {
1716 self.choices = Some(choices.parts);
1717 self
1718 }
1719
1720 pub fn step(mut self, index: usize, count: usize, on_back: Option<M>) -> Self {
1724 self.step = Some(StepParts {
1725 index,
1726 count,
1727 on_back,
1728 });
1729 self
1730 }
1731
1732 pub fn body(mut self, text: impl Into<String>) -> Self {
1734 self.body = Some(text.into());
1735 self
1736 }
1737
1738 pub fn markdown(mut self) -> Self {
1740 self.markdown = true;
1741 self
1742 }
1743
1744 pub fn urgency(mut self, urgency: Urgency) -> Self {
1745 self.urgency = urgency;
1746 self
1747 }
1748
1749 pub fn action(mut self, label: impl Into<String>, message: M) -> Self {
1751 self.actions.push((label.into(), message));
1752 self
1753 }
1754
1755 pub fn on_press(mut self, message: M) -> Self {
1757 self.on_press = Some(message);
1758 self
1759 }
1760
1761 pub fn on_dismiss(mut self, message: M) -> Self {
1763 self.on_dismiss = Some(message);
1764 self
1765 }
1766
1767 pub fn expires(mut self, after: std::time::Duration, message: M) -> Self {
1771 self.expires = Some((after, message));
1772 self
1773 }
1774
1775 pub fn reply(mut self, reply: Reply<M>) -> Self {
1777 self.reply = Some(reply);
1778 self
1779 }
1780
1781 pub fn map<N: 'static>(self, f: &(impl Fn(M) -> N + Clone + 'static)) -> Notice<N>
1782 where
1783 M: 'static,
1784 {
1785 Notice {
1786 key: self.key,
1787 summary: self.summary,
1788 app: self.app,
1789 app_icon: self.app_icon,
1790 icon: self.icon,
1791 image: self.image,
1792 pixels: self.pixels,
1793 body: self.body,
1794 markdown: self.markdown,
1795 urgency: self.urgency,
1796 time: self.time,
1797 thread: self.thread,
1798 thread_name: self.thread_name,
1799 progress: self.progress,
1800 actions: self
1801 .actions
1802 .into_iter()
1803 .map(|(label, message)| (label, f(message)))
1804 .collect(),
1805 choices: self.choices.map(|parts| Choices { parts }.map(f).parts),
1806 step: self.step.map(|step| StepParts {
1807 index: step.index,
1808 count: step.count,
1809 on_back: step.on_back.map(f),
1810 }),
1811 on_press: self.on_press.map(f),
1812 on_dismiss: self.on_dismiss.map(f),
1813 expires: self.expires.map(|(after, message)| (after, f(message))),
1814 reply: self.reply.map(|reply| reply.map(f)),
1815 }
1816 }
1817
1818 pub fn key(&self) -> u64 {
1819 self.key
1820 }
1821
1822 #[doc(hidden)]
1823 pub fn into_parts(self) -> NoticeParts<M> {
1824 NoticeParts {
1825 key: self.key,
1826 summary: self.summary,
1827 app: self.app,
1828 app_icon: self.app_icon,
1829 icon: self.icon,
1830 image: self.image,
1831 pixels: self.pixels,
1832 body: self.body,
1833 markdown: self.markdown,
1834 urgency: self.urgency,
1835 time: self.time,
1836 thread: self.thread,
1837 thread_name: self.thread_name,
1838 progress: self.progress,
1839 actions: self.actions,
1840 choices: self.choices,
1841 step: self.step,
1842 on_press: self.on_press,
1843 on_dismiss: self.on_dismiss,
1844 expires: self.expires,
1845 reply: self.reply.map(Reply::into_parts),
1846 }
1847 }
1848}
1849
1850#[doc(hidden)]
1851#[derive(Clone)]
1852pub struct NoticeParts<M> {
1853 pub key: u64,
1854 pub summary: String,
1855 pub app: Option<String>,
1856 pub app_icon: Option<String>,
1857 pub icon: Option<Icon>,
1858 pub image: Option<String>,
1859 pub pixels: Option<Pixels>,
1860 pub body: Option<String>,
1861 pub markdown: bool,
1862 pub urgency: Urgency,
1863 pub time: Option<SystemTime>,
1864 pub thread: Option<String>,
1865 pub thread_name: Option<String>,
1866 pub progress: Option<f32>,
1867 pub actions: Vec<(String, M)>,
1868 pub choices: Option<ChoicesParts<M>>,
1869 pub step: Option<StepParts<M>>,
1870 pub on_press: Option<M>,
1871 pub on_dismiss: Option<M>,
1872 pub expires: Option<(std::time::Duration, M)>,
1873 pub reply: Option<ReplyParts<M>>,
1874}
1875
1876pub struct Action<M> {
1880 title: String,
1881 subtitle: Option<String>,
1882 keywords: Vec<String>,
1883 leading: Option<Node<M>>,
1884 message: M,
1885}
1886
1887pub fn action<M>(title: impl Into<String>, message: M) -> Action<M> {
1889 Action {
1890 title: title.into(),
1891 subtitle: None,
1892 keywords: Vec::new(),
1893 leading: None,
1894 message,
1895 }
1896}
1897
1898impl<M> Action<M> {
1899 pub fn subtitle(mut self, text: impl Into<String>) -> Self {
1901 self.subtitle = Some(text.into());
1902 self
1903 }
1904
1905 pub fn keywords(mut self, words: impl IntoIterator<Item = impl Into<String>>) -> Self {
1907 self.keywords.extend(words.into_iter().map(Into::into));
1908 self
1909 }
1910
1911 pub fn leading(mut self, node: Node<M>) -> Self {
1913 self.leading = Some(node);
1914 self
1915 }
1916
1917 pub fn map<N: 'static>(self, f: &(impl Fn(M) -> N + Clone + 'static)) -> Action<N>
1918 where
1919 M: 'static,
1920 {
1921 Action {
1922 title: self.title,
1923 subtitle: self.subtitle,
1924 keywords: self.keywords,
1925 leading: self.leading.map(|node| node.map(f)),
1926 message: f(self.message),
1927 }
1928 }
1929
1930 pub fn title(&self) -> &str {
1931 &self.title
1932 }
1933
1934 #[doc(hidden)]
1935 pub fn into_parts(self) -> ActionParts<M> {
1936 ActionParts {
1937 title: self.title,
1938 subtitle: self.subtitle,
1939 keywords: self.keywords,
1940 leading: self.leading,
1941 message: self.message,
1942 }
1943 }
1944}
1945
1946#[doc(hidden)]
1947pub struct ActionParts<M> {
1948 pub title: String,
1949 pub subtitle: Option<String>,
1950 pub keywords: Vec<String>,
1951 pub leading: Option<Node<M>>,
1952 pub message: M,
1953}
1954
1955#[cfg(test)]
1956mod tests {
1957 use super::*;
1958
1959 #[test]
1960 fn map_converts_every_message() {
1961 let node = row([
1962 button(label("a"), 1).on_secondary(4),
1963 column([button(label("b"), 2), label("c")]),
1964 switcher([(label("d"), 3)], Some(0)),
1965 ]);
1966
1967 let Kind::Row(children) = node.map(&|n: i32| n * 10).into_kind() else {
1968 panic!("row became something else");
1969 };
1970 let mut messages = Vec::new();
1971 collect(children, &mut messages);
1972 assert_eq!(messages, [10, 40, 20, 30]);
1973 }
1974
1975 fn collect(nodes: Vec<Node<i32>>, out: &mut Vec<i32>) {
1976 for node in nodes {
1977 match node.into_kind() {
1978 Kind::Button {
1979 on_press,
1980 on_secondary,
1981 ..
1982 } => {
1983 out.push(on_press);
1984 out.extend(on_secondary);
1985 }
1986 Kind::Switcher { options, .. } => {
1987 out.extend(options.into_iter().map(|(_, message)| message))
1988 }
1989 Kind::Row(children) | Kind::Column(children) => collect(children, out),
1990 _ => {}
1991 }
1992 }
1993 }
1994
1995 #[test]
1996 fn hover_shows_the_second_option() {
1997 let node: Node<()> = hover(label("a"), label("b"));
1998 assert!(matches!(
1999 node.into_kind(),
2000 Kind::Alternate {
2001 current: 0,
2002 hovered: Some(1),
2003 ..
2004 }
2005 ));
2006 let node: Node<()> = label("a").on_hover(1);
2007 assert!(matches!(node.into_kind(), Kind::Label { .. }));
2008 }
2009
2010 #[test]
2011 fn tone_fills_only_unset_text() {
2012 let node: Node<()> =
2013 row([label("a").tone(Tone::Accent), label("b"), icon(Icon::Wired)]).tone(Tone::Dim);
2014 let Kind::Row(children) = node.into_kind() else {
2015 unreachable!()
2016 };
2017 let tones: Vec<_> = children
2018 .into_iter()
2019 .map(|child| match child.into_kind() {
2020 Kind::Label { tone, .. } | Kind::Icon { tone, .. } => tone,
2021 _ => None,
2022 })
2023 .collect();
2024 assert_eq!(
2025 tones,
2026 [Some(Tone::Accent), Some(Tone::Dim), Some(Tone::Dim)]
2027 );
2028 }
2029
2030 #[test]
2031 fn map_reaches_new_nodes_and_their_closures() {
2032 let node = column([
2033 header("h").on_back(1).trailing(toggle(true, 2)),
2034 list([list_item("a")
2035 .on_press(3)
2036 .on_secondary(4)
2037 .trailing(button(icon(Icon::Info), 5))]),
2038 slider(0.5, 0.0..=1.0, |v| (v * 10.0) as i32).on_release(6),
2039 text_input("p", "v", |text: String| text.len() as i32).on_submit(7),
2040 scroll(text_area("p", "v", |text: String| text.len() as i32 + 1).on_submit(8)).bottom(),
2041 ]);
2042 let Kind::Column(children) = node.map(&|n: i32| n * 10).into_kind() else {
2043 panic!("column became something else");
2044 };
2045 let mut children = children.into_iter().map(Node::into_kind);
2046 let Some(Kind::Header {
2047 on_back: Some(10),
2048 trailing: Some(trailing),
2049 ..
2050 }) = children.next()
2051 else {
2052 panic!("header lost its back message");
2053 };
2054 assert!(matches!(
2055 trailing.into_kind(),
2056 Kind::Toggle { on_toggle: 20, .. }
2057 ));
2058 let Some(Kind::List(rows)) = children.next() else {
2059 panic!("list lost");
2060 };
2061 let Some(Kind::ListItem {
2062 on_press: Some(30),
2063 on_secondary: Some(40),
2064 trailing: Some(trailing),
2065 ..
2066 }) = rows.into_iter().next().map(Node::into_kind)
2067 else {
2068 panic!("list item lost its messages");
2069 };
2070 assert!(matches!(
2071 trailing.into_kind(),
2072 Kind::Button { on_press: 50, .. }
2073 ));
2074 let Some(Kind::Slider {
2075 on_change,
2076 on_release: Some(60),
2077 ..
2078 }) = children.next()
2079 else {
2080 panic!("slider lost");
2081 };
2082 assert_eq!(on_change(0.3), 30);
2083 let Some(Kind::TextInput {
2084 on_input,
2085 on_submit: Some(70),
2086 ..
2087 }) = children.next()
2088 else {
2089 panic!("text input lost");
2090 };
2091 assert_eq!(on_input("abcd".into()), 40);
2092 let Some(Kind::Scroll {
2093 content,
2094 bottom: true,
2095 }) = children.next()
2096 else {
2097 panic!("scroll lost");
2098 };
2099 let Kind::TextArea {
2100 on_input,
2101 on_submit: Some(80),
2102 ..
2103 } = content.into_kind()
2104 else {
2105 panic!("text area lost");
2106 };
2107 assert_eq!(on_input("abcd".into()), 50);
2108 }
2109
2110 #[test]
2111 fn tabs_map_and_tone_every_page() {
2112 let node = tabs([("a", button(label("x"), 1)), ("b", button(label("y"), 2))])
2113 .tone(Tone::Dim)
2114 .map(&|n: i32| n * 10);
2115 let Kind::Tabs(pages) = node.into_kind() else {
2116 panic!("tabs became something else");
2117 };
2118 let names: Vec<_> = pages.iter().map(|(name, _)| name.as_str()).collect();
2119 assert_eq!(names, ["a", "b"]);
2120 let mut messages = Vec::new();
2121 for (_, page) in pages {
2122 let Kind::Button {
2123 content, on_press, ..
2124 } = page.into_kind()
2125 else {
2126 panic!("page lost its button");
2127 };
2128 assert!(matches!(
2129 content.into_kind(),
2130 Kind::Label {
2131 tone: Some(Tone::Dim),
2132 ..
2133 }
2134 ));
2135 messages.push(on_press);
2136 }
2137 assert_eq!(messages, [10, 20]);
2138 }
2139
2140 #[test]
2141 fn notice_map_reaches_every_message() {
2142 let notice = Notice::new(7, "s")
2143 .action("a", 1)
2144 .action("b", 2)
2145 .on_press(3)
2146 .on_dismiss(4)
2147 .expires(std::time::Duration::from_secs(5), 5)
2148 .reply(
2149 Reply::new("p", "v", |text: String| text.len() as i32)
2150 .on_submit(6)
2151 .on_cancel(7),
2152 )
2153 .choices(
2154 Choices::many()
2155 .option("x", None, false, 8)
2156 .option("y", Some("why".into()), true, 9)
2157 .recommended()
2158 .on_send(10),
2159 )
2160 .step(1, 3, Some(11))
2161 .map(&|n: i32| n * 10);
2162 assert_eq!(notice.key(), 7);
2163 let parts = notice.into_parts();
2164 let actions: Vec<_> = parts.actions.into_iter().map(|(_, m)| m).collect();
2165 assert_eq!(actions, [10, 20]);
2166 assert_eq!(parts.on_press, Some(30));
2167 assert_eq!(parts.on_dismiss, Some(40));
2168 assert_eq!(parts.expires.map(|(_, m)| m), Some(50));
2169 let reply = parts.reply.unwrap();
2170 assert_eq!((reply.on_input)("abc".into()), 30);
2171 assert_eq!((reply.on_submit, reply.on_cancel), (Some(60), Some(70)));
2172 let choices = parts.choices.unwrap();
2173 let picks: Vec<_> = choices.options.iter().map(|o| o.on_pick).collect();
2174 assert_eq!(picks, [80, 90]);
2175 assert!(choices.multiple && choices.options[1].recommended && choices.options[1].picked);
2176 assert_eq!(choices.on_send, Some(100));
2177 let step = parts.step.unwrap();
2178 assert_eq!((step.index, step.count, step.on_back), (1, 3, Some(110)));
2179 }
2180
2181 #[test]
2182 fn pixels_must_be_whole() {
2183 assert!(Pixels::new(2, 2, vec![0; 16]).is_some());
2184 assert!(Pixels::new(2, 2, vec![0; 15]).is_none());
2185 assert!(Pixels::new(0, 0, Vec::new()).is_none());
2186 assert!(Pixels::new(u32::MAX, u32::MAX, Vec::new()).is_none());
2187 }
2188
2189 #[test]
2190 fn progress_stays_between_none_and_all() {
2191 let parts = Notice::<()>::new(1, "s").progress(1.5).into_parts();
2192 assert_eq!(parts.progress, Some(1.0));
2193 }
2194
2195 #[test]
2196 fn modifiers_only_touch_their_nodes() {
2197 let node: Node<i32> = label("a").trailing(label("b")).on_press(1).selected(true);
2198 assert!(matches!(node.into_kind(), Kind::Label { .. }));
2199 let node: Node<i32> = button(label("a"), 1).on_press(2).prominent();
2200 assert!(matches!(
2201 node.into_kind(),
2202 Kind::Button {
2203 on_press: 1,
2204 prominent: true,
2205 ..
2206 }
2207 ));
2208 }
2209
2210 #[test]
2211 fn a_header_takes_a_picture_and_a_subtitle() {
2212 let node = header("t")
2213 .subtitle("s")
2214 .leading(button(icon(Icon::Info), 1))
2215 .tone(Tone::Dim)
2216 .map(&|n: i32| n * 10);
2217 let Kind::Header {
2218 subtitle: Some(subtitle),
2219 leading: Some(leading),
2220 ..
2221 } = node.into_kind()
2222 else {
2223 panic!("header lost its picture or subtitle");
2224 };
2225 assert_eq!(subtitle, "s");
2226 let Kind::Button {
2227 content,
2228 on_press: 10,
2229 ..
2230 } = leading.into_kind()
2231 else {
2232 panic!("picture lost its message");
2233 };
2234 assert!(matches!(
2235 content.into_kind(),
2236 Kind::Icon {
2237 tone: Some(Tone::Dim),
2238 ..
2239 }
2240 ));
2241 }
2242
2243 #[test]
2244 fn timelines_map_and_tone_their_steps() {
2245 let node = timeline([
2246 step(Mark::You, label("a")),
2247 step(Mark::Failed, button(literal("b"), 1)).leading(icon(Icon::Check)),
2248 label("c"),
2249 ])
2250 .tone(Tone::Dim)
2251 .map(&|n: i32| n * 10);
2252 let Kind::Timeline(steps) = node.into_kind() else {
2253 panic!("timeline became something else");
2254 };
2255 let mut steps = steps.into_iter().map(Node::into_kind);
2256 let Some(Kind::Step {
2257 mark: Mark::You,
2258 leading: None,
2259 content,
2260 }) = steps.next()
2261 else {
2262 panic!("first step lost");
2263 };
2264 assert!(matches!(
2265 content.into_kind(),
2266 Kind::Label {
2267 tone: Some(Tone::Dim),
2268 ..
2269 }
2270 ));
2271 let Some(Kind::Step {
2272 mark: Mark::Failed,
2273 leading: Some(leading),
2274 content,
2275 }) = steps.next()
2276 else {
2277 panic!("second step lost its picture");
2278 };
2279 assert!(matches!(
2280 leading.into_kind(),
2281 Kind::Icon {
2282 tone: Some(Tone::Dim),
2283 ..
2284 }
2285 ));
2286 let Kind::Button {
2287 content,
2288 on_press: 10,
2289 ..
2290 } = content.into_kind()
2291 else {
2292 panic!("button lost its message");
2293 };
2294 assert!(matches!(
2295 content.into_kind(),
2296 Kind::Literal {
2297 tone: Some(Tone::Dim),
2298 ..
2299 }
2300 ));
2301 assert!(matches!(steps.next(), Some(Kind::Label { .. })));
2302 let node: Node<()> = diff("f", [(Change::Added, "x".to_string())]).leading(label("y"));
2303 assert!(matches!(node.into_kind(), Kind::Diff { lines, .. } if lines.len() == 1));
2304 }
2305
2306 #[test]
2307 fn tone_reaches_details_and_row_icons() {
2308 let node: Node<()> = column([
2309 detail("n", label("v")),
2310 list_item("t").leading(icon(Icon::Lock)),
2311 ])
2312 .tone(Tone::Good);
2313 let Kind::Column(children) = node.into_kind() else {
2314 unreachable!()
2315 };
2316 let mut children = children.into_iter().map(Node::into_kind);
2317 let Some(Kind::Detail { value, .. }) = children.next() else {
2318 unreachable!()
2319 };
2320 assert!(matches!(
2321 value.into_kind(),
2322 Kind::Label {
2323 tone: Some(Tone::Good),
2324 ..
2325 }
2326 ));
2327 let Some(Kind::ListItem {
2328 leading: Some(leading),
2329 ..
2330 }) = children.next()
2331 else {
2332 unreachable!()
2333 };
2334 assert!(matches!(
2335 leading.into_kind(),
2336 Kind::Icon {
2337 tone: Some(Tone::Good),
2338 ..
2339 }
2340 ));
2341 }
2342}