1use std::ops::RangeInclusive;
8
9#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
11pub enum Tone {
12 #[default]
13 Normal,
14 Dim,
16 Accent,
18 Warning,
19 Critical,
20 Good,
22}
23
24#[derive(Debug, Clone, Copy, PartialEq)]
27pub enum Icon {
28 Volume { level: f32, muted: bool },
30 Wifi { strength: Option<f32> },
32 Wired,
34 Battery { charge: f32, charging: bool },
36 Lock,
38 Info,
40 Back,
42 Forward,
44 Check,
46 Refresh,
48 Close,
50 Reveal,
52 Bolt,
54 Plug,
56 Mic { muted: bool },
58 Speaker,
60 Headphones,
62 Display,
64 Bell { muted: bool },
66 Media { playing: bool },
69 Play,
71 Pause,
73 Previous,
75 Next,
77}
78
79#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
81pub enum Mark {
82 #[default]
84 Note,
85 You,
87 Running,
89 Done,
91 Failed,
93 Waiting,
95}
96
97#[derive(Debug, Clone, Copy, PartialEq, Eq)]
99pub enum Change {
100 Context,
102 Added,
103 Removed,
104 Hunk,
106}
107
108pub struct Node<M>(Kind<M>);
110
111#[doc(hidden)]
112pub enum Kind<M> {
113 Label {
114 text: String,
115 tone: Option<Tone>,
116 },
117 Heading {
118 text: String,
119 tone: Option<Tone>,
120 },
121 Icon {
122 icon: Icon,
123 tone: Option<Tone>,
124 },
125 AppIcon {
126 app_id: String,
127 tone: Option<Tone>,
128 },
129 Image {
130 file: &'static [u8],
131 tint: bool,
132 tone: Option<Tone>,
133 },
134 Sprite {
135 frames: &'static [&'static [u8]],
136 order: &'static [u8],
138 looping: bool,
140 tone: Option<Tone>,
141 },
142 Cover {
143 url: String,
144 fallback: Box<Node<M>>,
146 large: bool,
148 tone: Option<Tone>,
149 },
150 Counter {
151 value: i64,
152 range: RangeInclusive<i64>,
153 unit: String,
154 tone: Option<Tone>,
155 },
156 Alternate {
157 options: Vec<Node<M>>,
158 current: usize,
159 hovered: Option<usize>,
161 grows: bool,
163 },
164 Row(Vec<Node<M>>),
165 Column(Vec<Node<M>>),
166 Grid {
167 columns: usize,
168 cells: Vec<Node<M>>,
169 },
170 Button {
171 content: Box<Node<M>>,
172 on_press: M,
173 on_secondary: Option<M>,
174 prominent: bool,
176 },
177 Switcher {
178 options: Vec<(Node<M>, M)>,
179 current: Option<usize>,
180 },
181 Gulp(Box<Node<M>>),
182 Center(Box<Node<M>>),
184 Separator,
185 Empty,
186 ListItem {
187 title: String,
188 subtitle: Option<String>,
189 leading: Option<Box<Node<M>>>,
190 trailing: Option<Box<Node<M>>>,
191 selected: bool,
192 on_press: Option<M>,
193 on_secondary: Option<M>,
194 },
195 List(Vec<Node<M>>),
196 Section {
197 title: String,
198 trailing: Option<Box<Node<M>>>,
199 },
200 Header {
201 title: String,
202 subtitle: Option<String>,
203 on_back: Option<M>,
204 leading: Option<Box<Node<M>>>,
205 trailing: Option<Box<Node<M>>>,
206 },
207 Detail {
208 name: String,
209 value: Box<Node<M>>,
210 },
211 Fill,
212 Slider {
213 value: f32,
214 range: RangeInclusive<f32>,
215 step: f32,
216 on_change: Box<dyn Fn(f32) -> M>,
217 on_release: Option<M>,
218 },
219 Toggle {
220 on: bool,
221 on_toggle: M,
222 },
223 Meter {
224 fraction: f32,
225 limit: Option<f32>,
227 tone: Option<Tone>,
228 },
229 TextInput {
230 placeholder: String,
231 value: String,
232 on_input: Box<dyn Fn(String) -> M>,
233 on_submit: Option<M>,
234 secret: bool,
235 trailing: Option<Box<Node<M>>>,
236 tone: Option<Tone>,
237 },
238 Display(Box<Node<M>>),
239 Mark(Box<Node<M>>),
240 Badge {
241 text: String,
242 tone: Option<Tone>,
243 },
244 Tabs(Vec<(String, Node<M>)>),
246 Markdown {
247 text: String,
248 tone: Option<Tone>,
249 },
250 TextArea {
251 placeholder: String,
252 value: String,
253 on_input: Box<dyn Fn(String) -> M>,
254 on_submit: Option<M>,
255 trailing: Option<Box<Node<M>>>,
256 },
257 Scroll {
258 content: Box<Node<M>>,
259 bottom: bool,
261 },
262 Literal {
263 text: String,
264 tone: Option<Tone>,
265 },
266 Divider {
267 text: String,
268 },
269 Timeline(Vec<Node<M>>),
270 Step {
271 mark: Mark,
272 leading: Option<Box<Node<M>>>,
274 content: Box<Node<M>>,
275 },
276 Diff {
277 title: String,
278 lines: Vec<(Change, String)>,
279 },
280}
281
282pub fn label<M>(text: impl Into<String>) -> Node<M> {
283 Node(Kind::Label {
284 text: text.into(),
285 tone: None,
286 })
287}
288
289pub fn heading<M>(text: impl Into<String>) -> Node<M> {
291 Node(Kind::Heading {
292 text: text.into(),
293 tone: None,
294 })
295}
296
297pub fn icon<M>(icon: Icon) -> Node<M> {
299 Node(Kind::Icon { icon, tone: None })
300}
301
302pub fn app_icon<M>(app_id: impl Into<String>) -> Node<M> {
306 Node(Kind::AppIcon {
307 app_id: app_id.into(),
308 tone: None,
309 })
310}
311
312pub fn image<M>(file: &'static [u8]) -> Node<M> {
317 Node(Kind::Image {
318 file,
319 tint: false,
320 tone: None,
321 })
322}
323
324pub fn symbol<M>(file: &'static [u8]) -> Node<M> {
327 Node(Kind::Image {
328 file,
329 tint: true,
330 tone: None,
331 })
332}
333
334pub fn sprite<M>(frames: &'static [&'static [u8]], order: &'static [u8]) -> Node<M> {
342 Node(Kind::Sprite {
343 frames,
344 order,
345 looping: false,
346 tone: None,
347 })
348}
349
350pub fn cover<M>(url: impl Into<String>, fallback: Node<M>) -> Node<M> {
355 Node(Kind::Cover {
356 url: url.into(),
357 fallback: Box::new(fallback),
358 large: false,
359 tone: None,
360 })
361}
362
363pub fn counter<M>(value: i64, range: RangeInclusive<i64>, unit: impl Into<String>) -> Node<M> {
367 Node(Kind::Counter {
368 value,
369 range,
370 unit: unit.into(),
371 tone: None,
372 })
373}
374
375pub fn alternate<M>(options: impl IntoIterator<Item = Node<M>>, current: usize) -> Node<M> {
380 Node(Kind::Alternate {
381 options: options.into_iter().collect(),
382 current,
383 hovered: None,
384 grows: false,
385 })
386}
387
388pub fn hover<M>(normal: Node<M>, hovered: Node<M>) -> Node<M> {
391 alternate([normal, hovered], 0).on_hover(1)
392}
393
394pub fn expand<M>(collapsed: Node<M>, expanded: Node<M>) -> Node<M> {
399 Node(Kind::Alternate {
400 options: vec![collapsed, expanded],
401 current: 0,
402 hovered: Some(1),
403 grows: true,
404 })
405}
406
407pub fn row<M>(children: impl IntoIterator<Item = Node<M>>) -> Node<M> {
409 Node(Kind::Row(children.into_iter().collect()))
410}
411
412pub fn column<M>(children: impl IntoIterator<Item = Node<M>>) -> Node<M> {
414 Node(Kind::Column(children.into_iter().collect()))
415}
416
417pub fn center<M>(content: Node<M>) -> Node<M> {
421 Node(Kind::Center(Box::new(content)))
422}
423
424pub fn grid<M>(columns: usize, cells: impl IntoIterator<Item = Node<M>>) -> Node<M> {
426 Node(Kind::Grid {
427 columns: columns.max(1),
428 cells: cells.into_iter().collect(),
429 })
430}
431
432pub fn button<M>(content: Node<M>, on_press: M) -> Node<M> {
433 Node(Kind::Button {
434 content: Box::new(content),
435 on_press,
436 on_secondary: None,
437 prominent: false,
438 })
439}
440
441pub fn switcher<M>(
445 options: impl IntoIterator<Item = (Node<M>, M)>,
446 current: Option<usize>,
447) -> Node<M> {
448 Node(Kind::Switcher {
449 options: options.into_iter().collect(),
450 current,
451 })
452}
453
454pub fn gulp<M>(content: Node<M>) -> Node<M> {
457 Node(Kind::Gulp(Box::new(content)))
458}
459
460pub fn separator<M>() -> Node<M> {
462 Node(Kind::Separator)
463}
464
465pub fn empty<M>() -> Node<M> {
467 Node(Kind::Empty)
468}
469
470pub fn list_item<M>(title: impl Into<String>) -> Node<M> {
475 Node(Kind::ListItem {
476 title: title.into(),
477 subtitle: None,
478 leading: None,
479 trailing: None,
480 selected: false,
481 on_press: None,
482 on_secondary: None,
483 })
484}
485
486pub fn list<M>(rows: impl IntoIterator<Item = Node<M>>) -> Node<M> {
489 Node(Kind::List(rows.into_iter().collect()))
490}
491
492pub fn section<M>(title: impl Into<String>) -> Node<M> {
495 Node(Kind::Section {
496 title: title.into(),
497 trailing: None,
498 })
499}
500
501pub fn header<M>(title: impl Into<String>) -> Node<M> {
506 Node(Kind::Header {
507 title: title.into(),
508 subtitle: None,
509 on_back: None,
510 leading: None,
511 trailing: None,
512 })
513}
514
515pub fn detail<M>(name: impl Into<String>, value: Node<M>) -> Node<M> {
517 Node(Kind::Detail {
518 name: name.into(),
519 value: Box::new(value),
520 })
521}
522
523pub fn fill<M>() -> Node<M> {
525 Node(Kind::Fill)
526}
527
528pub fn slider<M: 'static>(
531 value: f32,
532 range: RangeInclusive<f32>,
533 on_change: impl Fn(f32) -> M + 'static,
534) -> Node<M> {
535 Node(Kind::Slider {
536 value,
537 range,
538 step: 1.0,
539 on_change: Box::new(on_change),
540 on_release: None,
541 })
542}
543
544pub fn toggle<M>(on: bool, on_toggle: M) -> Node<M> {
546 Node(Kind::Toggle { on, on_toggle })
547}
548
549pub fn meter<M>(fraction: f32) -> Node<M> {
552 Node(Kind::Meter {
553 fraction,
554 limit: None,
555 tone: None,
556 })
557}
558
559pub fn text_input<M: 'static>(
564 placeholder: impl Into<String>,
565 value: impl Into<String>,
566 on_input: impl Fn(String) -> M + 'static,
567) -> Node<M> {
568 Node(Kind::TextInput {
569 placeholder: placeholder.into(),
570 value: value.into(),
571 on_input: Box::new(on_input),
572 on_submit: None,
573 secret: false,
574 trailing: None,
575 tone: None,
576 })
577}
578
579pub fn text_area<M: 'static>(
584 placeholder: impl Into<String>,
585 value: impl Into<String>,
586 on_input: impl Fn(String) -> M + 'static,
587) -> Node<M> {
588 Node(Kind::TextArea {
589 placeholder: placeholder.into(),
590 value: value.into(),
591 on_input: Box::new(on_input),
592 on_submit: None,
593 trailing: None,
594 })
595}
596
597pub fn markdown<M>(text: impl Into<String>) -> Node<M> {
602 Node(Kind::Markdown {
603 text: text.into(),
604 tone: None,
605 })
606}
607
608pub fn scroll<M>(content: Node<M>) -> Node<M> {
612 Node(Kind::Scroll {
613 content: Box::new(content),
614 bottom: false,
615 })
616}
617
618pub fn literal<M>(text: impl Into<String>) -> Node<M> {
621 Node(Kind::Literal {
622 text: text.into(),
623 tone: None,
624 })
625}
626
627pub fn divider<M>(text: impl Into<String>) -> Node<M> {
630 Node(Kind::Divider { text: text.into() })
631}
632
633pub fn timeline<M>(steps: impl IntoIterator<Item = Node<M>>) -> Node<M> {
637 Node(Kind::Timeline(steps.into_iter().collect()))
638}
639
640pub fn step<M>(mark: Mark, content: Node<M>) -> Node<M> {
644 Node(Kind::Step {
645 mark,
646 leading: None,
647 content: Box::new(content),
648 })
649}
650
651pub fn diff<M>(
654 title: impl Into<String>,
655 lines: impl IntoIterator<Item = (Change, String)>,
656) -> Node<M> {
657 Node(Kind::Diff {
658 title: title.into(),
659 lines: lines.into_iter().collect(),
660 })
661}
662
663pub fn display<M>(content: Node<M>) -> Node<M> {
665 Node(Kind::Display(Box::new(content)))
666}
667
668pub fn mark<M>(content: Node<M>) -> Node<M> {
670 Node(Kind::Mark(Box::new(content)))
671}
672
673pub fn badge<M>(text: impl Into<String>) -> Node<M> {
675 Node(Kind::Badge {
676 text: text.into(),
677 tone: None,
678 })
679}
680
681pub fn tabs<M>(pages: impl IntoIterator<Item = (impl Into<String>, Node<M>)>) -> Node<M> {
685 Node(Kind::Tabs(
686 pages
687 .into_iter()
688 .map(|(name, page)| (name.into(), page))
689 .collect(),
690 ))
691}
692
693impl<M> Node<M> {
694 pub fn tone(mut self, tone: Tone) -> Self {
696 self.set_tone(tone);
697 self
698 }
699
700 pub fn on_hover(mut self, index: usize) -> Self {
703 if let Kind::Alternate { hovered, .. } = &mut self.0 {
704 *hovered = Some(index);
705 }
706 self
707 }
708
709 pub fn on_secondary(mut self, message: M) -> Self {
712 if let Kind::Button { on_secondary, .. } | Kind::ListItem { on_secondary, .. } = &mut self.0
713 {
714 *on_secondary = Some(message);
715 }
716 self
717 }
718
719 pub fn prominent(mut self) -> Self {
722 if let Kind::Button { prominent, .. } = &mut self.0 {
723 *prominent = true;
724 }
725 self
726 }
727
728 pub fn on_press(mut self, message: M) -> Self {
730 if let Kind::ListItem { on_press, .. } = &mut self.0 {
731 *on_press = Some(message);
732 }
733 self
734 }
735
736 pub fn subtitle(mut self, text: impl Into<String>) -> Self {
739 if let Kind::ListItem { subtitle, .. } | Kind::Header { subtitle, .. } = &mut self.0 {
740 *subtitle = Some(text.into());
741 }
742 self
743 }
744
745 pub fn leading(mut self, node: Node<M>) -> Self {
748 if let Kind::ListItem { leading, .. }
749 | Kind::Header { leading, .. }
750 | Kind::Step { leading, .. } = &mut self.0
751 {
752 *leading = Some(Box::new(node));
753 }
754 self
755 }
756
757 pub fn trailing(mut self, node: Node<M>) -> Self {
760 if let Kind::ListItem { trailing, .. }
761 | Kind::Section { trailing, .. }
762 | Kind::Header { trailing, .. }
763 | Kind::TextInput { trailing, .. }
764 | Kind::TextArea { trailing, .. } = &mut self.0
765 {
766 *trailing = Some(Box::new(node));
767 }
768 self
769 }
770
771 pub fn selected(mut self, yes: bool) -> Self {
773 if let Kind::ListItem { selected, .. } = &mut self.0 {
774 *selected = yes;
775 }
776 self
777 }
778
779 pub fn on_back(mut self, message: M) -> Self {
781 if let Kind::Header { on_back, .. } = &mut self.0 {
782 *on_back = Some(message);
783 }
784 self
785 }
786
787 pub fn step(mut self, by: f32) -> Self {
789 if let Kind::Slider { step, .. } = &mut self.0 {
790 *step = by;
791 }
792 self
793 }
794
795 pub fn on_release(mut self, message: M) -> Self {
797 if let Kind::Slider { on_release, .. } = &mut self.0 {
798 *on_release = Some(message);
799 }
800 self
801 }
802
803 pub fn on_submit(mut self, message: M) -> Self {
805 if let Kind::TextInput { on_submit, .. } | Kind::TextArea { on_submit, .. } = &mut self.0 {
806 *on_submit = Some(message);
807 }
808 self
809 }
810
811 pub fn secret(mut self, yes: bool) -> Self {
813 if let Kind::TextInput { secret, .. } = &mut self.0 {
814 *secret = yes;
815 }
816 self
817 }
818
819 pub fn looping(mut self) -> Self {
823 if let Kind::Sprite { looping, .. } = &mut self.0 {
824 *looping = true;
825 }
826 self
827 }
828
829 pub fn large(mut self) -> Self {
832 if let Kind::Cover { large, .. } = &mut self.0 {
833 *large = true;
834 }
835 self
836 }
837
838 pub fn bottom(mut self) -> Self {
841 if let Kind::Scroll { bottom, .. } = &mut self.0 {
842 *bottom = true;
843 }
844 self
845 }
846
847 pub fn limit(mut self, at: f32) -> Self {
849 if let Kind::Meter { limit, .. } = &mut self.0 {
850 *limit = Some(at);
851 }
852 self
853 }
854
855 fn set_tone(&mut self, new: Tone) {
856 match &mut self.0 {
857 Kind::Label { tone, .. }
858 | Kind::Heading { tone, .. }
859 | Kind::Icon { tone, .. }
860 | Kind::AppIcon { tone, .. }
861 | Kind::Image { tone, .. }
862 | Kind::Sprite { tone, .. }
863 | Kind::Counter { tone, .. }
864 | Kind::Meter { tone, .. }
865 | Kind::TextInput { tone, .. }
866 | Kind::Markdown { tone, .. }
867 | Kind::Literal { tone, .. }
868 | Kind::Badge { tone, .. } => {
869 tone.get_or_insert(new);
870 }
871 Kind::Cover { tone, fallback, .. } => {
872 tone.get_or_insert(new);
873 fallback.set_tone(new);
874 }
875 Kind::Alternate {
876 options: children, ..
877 }
878 | Kind::Row(children)
879 | Kind::Column(children)
880 | Kind::Grid {
881 cells: children, ..
882 }
883 | Kind::List(children)
884 | Kind::Timeline(children) => {
885 children.iter_mut().for_each(|child| child.set_tone(new));
886 }
887 Kind::Step {
888 leading, content, ..
889 } => {
890 leading.iter_mut().for_each(|child| child.set_tone(new));
891 content.set_tone(new);
892 }
893 Kind::Button { content, .. }
894 | Kind::Gulp(content)
895 | Kind::Center(content)
896 | Kind::Display(content)
897 | Kind::Mark(content)
898 | Kind::Scroll { content, .. }
899 | Kind::Detail { value: content, .. } => content.set_tone(new),
900 Kind::ListItem {
901 leading, trailing, ..
902 }
903 | Kind::Header {
904 leading, trailing, ..
905 } => {
906 leading
907 .iter_mut()
908 .chain(trailing.iter_mut())
909 .for_each(|child| child.set_tone(new));
910 }
911 Kind::Tabs(pages) => {
912 for (_, page) in pages {
913 page.set_tone(new);
914 }
915 }
916 Kind::Switcher { options, .. } => {
917 options
918 .iter_mut()
919 .for_each(|(child, _)| child.set_tone(new));
920 }
921 Kind::Separator
922 | Kind::Divider { .. }
923 | Kind::Diff { .. }
924 | Kind::Empty
925 | Kind::Section { .. }
926 | Kind::Fill
927 | Kind::Slider { .. }
928 | Kind::TextArea { .. }
929 | Kind::Toggle { .. } => {}
930 }
931 }
932
933 pub fn map<N: 'static>(self, f: &(impl Fn(M) -> N + Clone + 'static)) -> Node<N>
937 where
938 M: 'static,
939 {
940 let map_all = |nodes: Vec<Node<M>>| nodes.into_iter().map(|node| node.map(f)).collect();
941 let map_box = |node: Option<Box<Node<M>>>| node.map(|node| Box::new(node.map(f)));
942
943 Node(match self.0 {
944 Kind::Label { text, tone } => Kind::Label { text, tone },
945 Kind::Heading { text, tone } => Kind::Heading { text, tone },
946 Kind::Icon { icon, tone } => Kind::Icon { icon, tone },
947 Kind::AppIcon { app_id, tone } => Kind::AppIcon { app_id, tone },
948 Kind::Image { file, tint, tone } => Kind::Image { file, tint, tone },
949 Kind::Sprite {
950 frames,
951 order,
952 looping,
953 tone,
954 } => Kind::Sprite {
955 frames,
956 order,
957 looping,
958 tone,
959 },
960 Kind::Cover {
961 url,
962 fallback,
963 large,
964 tone,
965 } => Kind::Cover {
966 url,
967 fallback: Box::new(fallback.map(f)),
968 large,
969 tone,
970 },
971 Kind::Counter {
972 value,
973 range,
974 unit,
975 tone,
976 } => Kind::Counter {
977 value,
978 range,
979 unit,
980 tone,
981 },
982 Kind::Alternate {
983 options,
984 current,
985 hovered,
986 grows,
987 } => Kind::Alternate {
988 options: map_all(options),
989 current,
990 hovered,
991 grows,
992 },
993 Kind::Row(children) => Kind::Row(map_all(children)),
994 Kind::Column(children) => Kind::Column(map_all(children)),
995 Kind::Grid { columns, cells } => Kind::Grid {
996 columns,
997 cells: map_all(cells),
998 },
999 Kind::Button {
1000 content,
1001 on_press,
1002 on_secondary,
1003 prominent,
1004 } => Kind::Button {
1005 content: Box::new(content.map(f)),
1006 on_press: f(on_press),
1007 on_secondary: on_secondary.map(f),
1008 prominent,
1009 },
1010 Kind::Switcher { options, current } => Kind::Switcher {
1011 options: options
1012 .into_iter()
1013 .map(|(node, message)| (node.map(f), f(message)))
1014 .collect(),
1015 current,
1016 },
1017 Kind::Gulp(content) => Kind::Gulp(Box::new(content.map(f))),
1018 Kind::Center(content) => Kind::Center(Box::new(content.map(f))),
1019 Kind::Separator => Kind::Separator,
1020 Kind::Empty => Kind::Empty,
1021 Kind::ListItem {
1022 title,
1023 subtitle,
1024 leading,
1025 trailing,
1026 selected,
1027 on_press,
1028 on_secondary,
1029 } => Kind::ListItem {
1030 title,
1031 subtitle,
1032 leading: map_box(leading),
1033 trailing: map_box(trailing),
1034 selected,
1035 on_press: on_press.map(f),
1036 on_secondary: on_secondary.map(f),
1037 },
1038 Kind::List(rows) => Kind::List(map_all(rows)),
1039 Kind::Section { title, trailing } => Kind::Section {
1040 title,
1041 trailing: map_box(trailing),
1042 },
1043 Kind::Header {
1044 title,
1045 subtitle,
1046 on_back,
1047 leading,
1048 trailing,
1049 } => Kind::Header {
1050 title,
1051 subtitle,
1052 on_back: on_back.map(f),
1053 leading: map_box(leading),
1054 trailing: map_box(trailing),
1055 },
1056 Kind::Detail { name, value } => Kind::Detail {
1057 name,
1058 value: Box::new(value.map(f)),
1059 },
1060 Kind::Fill => Kind::Fill,
1061 Kind::Slider {
1062 value,
1063 range,
1064 step,
1065 on_change,
1066 on_release,
1067 } => {
1068 let g = f.clone();
1069 Kind::Slider {
1070 value,
1071 range,
1072 step,
1073 on_change: Box::new(move |value| g(on_change(value))),
1074 on_release: on_release.map(f),
1075 }
1076 }
1077 Kind::Toggle { on, on_toggle } => Kind::Toggle {
1078 on,
1079 on_toggle: f(on_toggle),
1080 },
1081 Kind::Meter {
1082 fraction,
1083 limit,
1084 tone,
1085 } => Kind::Meter {
1086 fraction,
1087 limit,
1088 tone,
1089 },
1090 Kind::TextInput {
1091 placeholder,
1092 value,
1093 on_input,
1094 on_submit,
1095 secret,
1096 trailing,
1097 tone,
1098 } => {
1099 let g = f.clone();
1100 Kind::TextInput {
1101 placeholder,
1102 value,
1103 on_input: Box::new(move |text| g(on_input(text))),
1104 on_submit: on_submit.map(f),
1105 secret,
1106 trailing: map_box(trailing),
1107 tone,
1108 }
1109 }
1110 Kind::Display(content) => Kind::Display(Box::new(content.map(f))),
1111 Kind::Mark(content) => Kind::Mark(Box::new(content.map(f))),
1112 Kind::Badge { text, tone } => Kind::Badge { text, tone },
1113 Kind::Tabs(pages) => Kind::Tabs(
1114 pages
1115 .into_iter()
1116 .map(|(name, page)| (name, page.map(f)))
1117 .collect(),
1118 ),
1119 Kind::Markdown { text, tone } => Kind::Markdown { text, tone },
1120 Kind::TextArea {
1121 placeholder,
1122 value,
1123 on_input,
1124 on_submit,
1125 trailing,
1126 } => {
1127 let g = f.clone();
1128 Kind::TextArea {
1129 placeholder,
1130 value,
1131 on_input: Box::new(move |text| g(on_input(text))),
1132 on_submit: on_submit.map(f),
1133 trailing: map_box(trailing),
1134 }
1135 }
1136 Kind::Scroll { content, bottom } => Kind::Scroll {
1137 content: Box::new(content.map(f)),
1138 bottom,
1139 },
1140 Kind::Literal { text, tone } => Kind::Literal { text, tone },
1141 Kind::Divider { text } => Kind::Divider { text },
1142 Kind::Timeline(steps) => Kind::Timeline(map_all(steps)),
1143 Kind::Step {
1144 mark,
1145 leading,
1146 content,
1147 } => Kind::Step {
1148 mark,
1149 leading: map_box(leading),
1150 content: Box::new(content.map(f)),
1151 },
1152 Kind::Diff { title, lines } => Kind::Diff { title, lines },
1153 })
1154 }
1155
1156 #[doc(hidden)]
1157 pub fn into_kind(self) -> Kind<M> {
1158 self.0
1159 }
1160
1161 #[doc(hidden)]
1162 pub fn kind(&self) -> &Kind<M> {
1163 &self.0
1164 }
1165}
1166
1167pub struct Item<M> {
1169 content: Option<Node<M>>,
1170 on_press: Option<M>,
1171 on_secondary: Option<M>,
1172 on_scroll: Option<(M, M)>,
1173}
1174
1175impl<M> Item<M> {
1176 pub fn new(content: Node<M>) -> Self {
1177 Self {
1178 content: Some(content),
1179 on_press: None,
1180 on_secondary: None,
1181 on_scroll: None,
1182 }
1183 }
1184
1185 pub fn hidden() -> Self {
1187 Self {
1188 content: None,
1189 on_press: None,
1190 on_secondary: None,
1191 on_scroll: None,
1192 }
1193 }
1194
1195 pub fn on_press(mut self, message: M) -> Self {
1197 self.on_press = Some(message);
1198 self
1199 }
1200
1201 pub fn on_secondary(mut self, message: M) -> Self {
1203 self.on_secondary = Some(message);
1204 self
1205 }
1206
1207 pub fn on_scroll(mut self, up: M, down: M) -> Self {
1210 self.on_scroll = Some((up, down));
1211 self
1212 }
1213
1214 pub fn map<N: 'static>(self, f: &(impl Fn(M) -> N + Clone + 'static)) -> Item<N>
1215 where
1216 M: 'static,
1217 {
1218 Item {
1219 content: self.content.map(|content| content.map(f)),
1220 on_press: self.on_press.map(f),
1221 on_secondary: self.on_secondary.map(f),
1222 on_scroll: self.on_scroll.map(|(up, down)| (f(up), f(down))),
1223 }
1224 }
1225
1226 #[doc(hidden)]
1227 pub fn into_parts(self) -> ItemParts<M> {
1228 ItemParts {
1229 content: self.content,
1230 on_press: self.on_press,
1231 on_secondary: self.on_secondary,
1232 on_scroll: self.on_scroll,
1233 }
1234 }
1235}
1236
1237#[doc(hidden)]
1238pub struct ItemParts<M> {
1239 pub content: Option<Node<M>>,
1240 pub on_press: Option<M>,
1241 pub on_secondary: Option<M>,
1242 pub on_scroll: Option<(M, M)>,
1244}
1245
1246pub struct Panel<M> {
1249 content: Node<M>,
1250 on_open: Option<M>,
1251 on_close: Option<M>,
1252 wide: bool,
1253}
1254
1255impl<M> Panel<M> {
1256 pub fn new(content: Node<M>) -> Self {
1257 Self {
1258 content,
1259 on_open: None,
1260 on_close: None,
1261 wide: false,
1262 }
1263 }
1264
1265 pub fn wide(mut self) -> Self {
1268 self.wide = true;
1269 self
1270 }
1271
1272 #[doc(hidden)]
1273 pub fn is_wide(&self) -> bool {
1274 self.wide
1275 }
1276
1277 pub fn on_open(mut self, message: M) -> Self {
1279 self.on_open = Some(message);
1280 self
1281 }
1282
1283 pub fn on_close(mut self, message: M) -> Self {
1285 self.on_close = Some(message);
1286 self
1287 }
1288
1289 pub fn map<N: 'static>(self, f: &(impl Fn(M) -> N + Clone + 'static)) -> Panel<N>
1290 where
1291 M: 'static,
1292 {
1293 Panel {
1294 content: self.content.map(f),
1295 on_open: self.on_open.map(f),
1296 on_close: self.on_close.map(f),
1297 wide: self.wide,
1298 }
1299 }
1300
1301 #[doc(hidden)]
1302 pub fn into_node(self) -> Node<M> {
1303 self.content
1304 }
1305
1306 #[doc(hidden)]
1307 pub fn on_open_message(self) -> Option<M> {
1308 self.on_open
1309 }
1310
1311 #[doc(hidden)]
1312 pub fn on_close_message(self) -> Option<M> {
1313 self.on_close
1314 }
1315}
1316
1317#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1319pub enum Urgency {
1320 Low,
1322 #[default]
1323 Normal,
1324 Critical,
1326}
1327
1328pub struct Notice<M> {
1335 key: u64,
1336 summary: String,
1337 app: Option<String>,
1338 app_icon: Option<String>,
1339 image: Option<String>,
1340 body: Option<String>,
1341 markdown: bool,
1342 urgency: Urgency,
1343 actions: Vec<(String, M)>,
1344 on_press: Option<M>,
1345 on_dismiss: Option<M>,
1346 expires: Option<(std::time::Duration, M)>,
1347 reply: Option<Reply<M>>,
1348}
1349
1350pub struct Reply<M> {
1357 placeholder: String,
1358 value: String,
1359 on_input: std::rc::Rc<dyn Fn(String) -> M>,
1360 on_submit: Option<M>,
1361 on_cancel: Option<M>,
1362}
1363
1364impl<M> Reply<M> {
1365 pub fn new(
1367 placeholder: impl Into<String>,
1368 value: impl Into<String>,
1369 on_input: impl Fn(String) -> M + 'static,
1370 ) -> Self {
1371 Self {
1372 placeholder: placeholder.into(),
1373 value: value.into(),
1374 on_input: std::rc::Rc::new(on_input),
1375 on_submit: None,
1376 on_cancel: None,
1377 }
1378 }
1379
1380 pub fn on_submit(mut self, message: M) -> Self {
1382 self.on_submit = Some(message);
1383 self
1384 }
1385
1386 pub fn on_cancel(mut self, message: M) -> Self {
1388 self.on_cancel = Some(message);
1389 self
1390 }
1391
1392 pub fn map<N: 'static>(self, f: &(impl Fn(M) -> N + Clone + 'static)) -> Reply<N>
1393 where
1394 M: 'static,
1395 {
1396 let g = f.clone();
1397 let on_input = self.on_input;
1398 Reply {
1399 placeholder: self.placeholder,
1400 value: self.value,
1401 on_input: std::rc::Rc::new(move |text| g(on_input(text))),
1402 on_submit: self.on_submit.map(f),
1403 on_cancel: self.on_cancel.map(f),
1404 }
1405 }
1406
1407 #[doc(hidden)]
1408 pub fn into_parts(self) -> ReplyParts<M> {
1409 ReplyParts {
1410 placeholder: self.placeholder,
1411 value: self.value,
1412 on_input: self.on_input,
1413 on_submit: self.on_submit,
1414 on_cancel: self.on_cancel,
1415 }
1416 }
1417}
1418
1419#[doc(hidden)]
1420#[derive(Clone)]
1421pub struct ReplyParts<M> {
1422 pub placeholder: String,
1423 pub value: String,
1424 pub on_input: std::rc::Rc<dyn Fn(String) -> M>,
1425 pub on_submit: Option<M>,
1426 pub on_cancel: Option<M>,
1427}
1428
1429impl<M> Notice<M> {
1430 pub fn new(key: u64, summary: impl Into<String>) -> Self {
1431 Self {
1432 key,
1433 summary: summary.into(),
1434 app: None,
1435 app_icon: None,
1436 image: None,
1437 body: None,
1438 markdown: false,
1439 urgency: Urgency::Normal,
1440 actions: Vec::new(),
1441 on_press: None,
1442 on_dismiss: None,
1443 expires: None,
1444 reply: None,
1445 }
1446 }
1447
1448 pub fn app(mut self, name: impl Into<String>) -> Self {
1450 self.app = Some(name.into());
1451 self
1452 }
1453
1454 pub fn app_icon(mut self, app_id: impl Into<String>) -> Self {
1456 self.app_icon = Some(app_id.into());
1457 self
1458 }
1459
1460 pub fn image(mut self, url: impl Into<String>) -> Self {
1463 self.image = Some(url.into());
1464 self
1465 }
1466
1467 pub fn body(mut self, text: impl Into<String>) -> Self {
1469 self.body = Some(text.into());
1470 self
1471 }
1472
1473 pub fn markdown(mut self) -> Self {
1475 self.markdown = true;
1476 self
1477 }
1478
1479 pub fn urgency(mut self, urgency: Urgency) -> Self {
1480 self.urgency = urgency;
1481 self
1482 }
1483
1484 pub fn action(mut self, label: impl Into<String>, message: M) -> Self {
1486 self.actions.push((label.into(), message));
1487 self
1488 }
1489
1490 pub fn on_press(mut self, message: M) -> Self {
1492 self.on_press = Some(message);
1493 self
1494 }
1495
1496 pub fn on_dismiss(mut self, message: M) -> Self {
1498 self.on_dismiss = Some(message);
1499 self
1500 }
1501
1502 pub fn expires(mut self, after: std::time::Duration, message: M) -> Self {
1506 self.expires = Some((after, message));
1507 self
1508 }
1509
1510 pub fn reply(mut self, reply: Reply<M>) -> Self {
1512 self.reply = Some(reply);
1513 self
1514 }
1515
1516 pub fn map<N: 'static>(self, f: &(impl Fn(M) -> N + Clone + 'static)) -> Notice<N>
1517 where
1518 M: 'static,
1519 {
1520 Notice {
1521 key: self.key,
1522 summary: self.summary,
1523 app: self.app,
1524 app_icon: self.app_icon,
1525 image: self.image,
1526 body: self.body,
1527 markdown: self.markdown,
1528 urgency: self.urgency,
1529 actions: self
1530 .actions
1531 .into_iter()
1532 .map(|(label, message)| (label, f(message)))
1533 .collect(),
1534 on_press: self.on_press.map(f),
1535 on_dismiss: self.on_dismiss.map(f),
1536 expires: self.expires.map(|(after, message)| (after, f(message))),
1537 reply: self.reply.map(|reply| reply.map(f)),
1538 }
1539 }
1540
1541 pub fn key(&self) -> u64 {
1542 self.key
1543 }
1544
1545 #[doc(hidden)]
1546 pub fn into_parts(self) -> NoticeParts<M> {
1547 NoticeParts {
1548 key: self.key,
1549 summary: self.summary,
1550 app: self.app,
1551 app_icon: self.app_icon,
1552 image: self.image,
1553 body: self.body,
1554 markdown: self.markdown,
1555 urgency: self.urgency,
1556 actions: self.actions,
1557 on_press: self.on_press,
1558 on_dismiss: self.on_dismiss,
1559 expires: self.expires,
1560 reply: self.reply.map(Reply::into_parts),
1561 }
1562 }
1563}
1564
1565#[doc(hidden)]
1566#[derive(Clone)]
1567pub struct NoticeParts<M> {
1568 pub key: u64,
1569 pub summary: String,
1570 pub app: Option<String>,
1571 pub app_icon: Option<String>,
1572 pub image: Option<String>,
1573 pub body: Option<String>,
1574 pub markdown: bool,
1575 pub urgency: Urgency,
1576 pub actions: Vec<(String, M)>,
1577 pub on_press: Option<M>,
1578 pub on_dismiss: Option<M>,
1579 pub expires: Option<(std::time::Duration, M)>,
1580 pub reply: Option<ReplyParts<M>>,
1581}
1582
1583#[cfg(test)]
1584mod tests {
1585 use super::*;
1586
1587 #[test]
1588 fn map_converts_every_message() {
1589 let node = row([
1590 button(label("a"), 1).on_secondary(4),
1591 column([button(label("b"), 2), label("c")]),
1592 switcher([(label("d"), 3)], Some(0)),
1593 ]);
1594
1595 let Kind::Row(children) = node.map(&|n: i32| n * 10).into_kind() else {
1596 panic!("row became something else");
1597 };
1598 let mut messages = Vec::new();
1599 collect(children, &mut messages);
1600 assert_eq!(messages, [10, 40, 20, 30]);
1601 }
1602
1603 fn collect(nodes: Vec<Node<i32>>, out: &mut Vec<i32>) {
1604 for node in nodes {
1605 match node.into_kind() {
1606 Kind::Button {
1607 on_press,
1608 on_secondary,
1609 ..
1610 } => {
1611 out.push(on_press);
1612 out.extend(on_secondary);
1613 }
1614 Kind::Switcher { options, .. } => {
1615 out.extend(options.into_iter().map(|(_, message)| message))
1616 }
1617 Kind::Row(children) | Kind::Column(children) => collect(children, out),
1618 _ => {}
1619 }
1620 }
1621 }
1622
1623 #[test]
1624 fn hover_shows_the_second_option() {
1625 let node: Node<()> = hover(label("a"), label("b"));
1626 assert!(matches!(
1627 node.into_kind(),
1628 Kind::Alternate {
1629 current: 0,
1630 hovered: Some(1),
1631 ..
1632 }
1633 ));
1634 let node: Node<()> = label("a").on_hover(1);
1635 assert!(matches!(node.into_kind(), Kind::Label { .. }));
1636 }
1637
1638 #[test]
1639 fn tone_fills_only_unset_text() {
1640 let node: Node<()> =
1641 row([label("a").tone(Tone::Accent), label("b"), icon(Icon::Wired)]).tone(Tone::Dim);
1642 let Kind::Row(children) = node.into_kind() else {
1643 unreachable!()
1644 };
1645 let tones: Vec<_> = children
1646 .into_iter()
1647 .map(|child| match child.into_kind() {
1648 Kind::Label { tone, .. } | Kind::Icon { tone, .. } => tone,
1649 _ => None,
1650 })
1651 .collect();
1652 assert_eq!(
1653 tones,
1654 [Some(Tone::Accent), Some(Tone::Dim), Some(Tone::Dim)]
1655 );
1656 }
1657
1658 #[test]
1659 fn map_reaches_new_nodes_and_their_closures() {
1660 let node = column([
1661 header("h").on_back(1).trailing(toggle(true, 2)),
1662 list([list_item("a")
1663 .on_press(3)
1664 .on_secondary(4)
1665 .trailing(button(icon(Icon::Info), 5))]),
1666 slider(0.5, 0.0..=1.0, |v| (v * 10.0) as i32).on_release(6),
1667 text_input("p", "v", |text: String| text.len() as i32).on_submit(7),
1668 scroll(text_area("p", "v", |text: String| text.len() as i32 + 1).on_submit(8)).bottom(),
1669 ]);
1670 let Kind::Column(children) = node.map(&|n: i32| n * 10).into_kind() else {
1671 panic!("column became something else");
1672 };
1673 let mut children = children.into_iter().map(Node::into_kind);
1674 let Some(Kind::Header {
1675 on_back: Some(10),
1676 trailing: Some(trailing),
1677 ..
1678 }) = children.next()
1679 else {
1680 panic!("header lost its back message");
1681 };
1682 assert!(matches!(
1683 trailing.into_kind(),
1684 Kind::Toggle { on_toggle: 20, .. }
1685 ));
1686 let Some(Kind::List(rows)) = children.next() else {
1687 panic!("list lost");
1688 };
1689 let Some(Kind::ListItem {
1690 on_press: Some(30),
1691 on_secondary: Some(40),
1692 trailing: Some(trailing),
1693 ..
1694 }) = rows.into_iter().next().map(Node::into_kind)
1695 else {
1696 panic!("list item lost its messages");
1697 };
1698 assert!(matches!(
1699 trailing.into_kind(),
1700 Kind::Button { on_press: 50, .. }
1701 ));
1702 let Some(Kind::Slider {
1703 on_change,
1704 on_release: Some(60),
1705 ..
1706 }) = children.next()
1707 else {
1708 panic!("slider lost");
1709 };
1710 assert_eq!(on_change(0.3), 30);
1711 let Some(Kind::TextInput {
1712 on_input,
1713 on_submit: Some(70),
1714 ..
1715 }) = children.next()
1716 else {
1717 panic!("text input lost");
1718 };
1719 assert_eq!(on_input("abcd".into()), 40);
1720 let Some(Kind::Scroll {
1721 content,
1722 bottom: true,
1723 }) = children.next()
1724 else {
1725 panic!("scroll lost");
1726 };
1727 let Kind::TextArea {
1728 on_input,
1729 on_submit: Some(80),
1730 ..
1731 } = content.into_kind()
1732 else {
1733 panic!("text area lost");
1734 };
1735 assert_eq!(on_input("abcd".into()), 50);
1736 }
1737
1738 #[test]
1739 fn tabs_map_and_tone_every_page() {
1740 let node = tabs([("a", button(label("x"), 1)), ("b", button(label("y"), 2))])
1741 .tone(Tone::Dim)
1742 .map(&|n: i32| n * 10);
1743 let Kind::Tabs(pages) = node.into_kind() else {
1744 panic!("tabs became something else");
1745 };
1746 let names: Vec<_> = pages.iter().map(|(name, _)| name.as_str()).collect();
1747 assert_eq!(names, ["a", "b"]);
1748 let mut messages = Vec::new();
1749 for (_, page) in pages {
1750 let Kind::Button {
1751 content, on_press, ..
1752 } = page.into_kind()
1753 else {
1754 panic!("page lost its button");
1755 };
1756 assert!(matches!(
1757 content.into_kind(),
1758 Kind::Label {
1759 tone: Some(Tone::Dim),
1760 ..
1761 }
1762 ));
1763 messages.push(on_press);
1764 }
1765 assert_eq!(messages, [10, 20]);
1766 }
1767
1768 #[test]
1769 fn notice_map_reaches_every_message() {
1770 let notice = Notice::new(7, "s")
1771 .action("a", 1)
1772 .action("b", 2)
1773 .on_press(3)
1774 .on_dismiss(4)
1775 .expires(std::time::Duration::from_secs(5), 5)
1776 .reply(
1777 Reply::new("p", "v", |text: String| text.len() as i32)
1778 .on_submit(6)
1779 .on_cancel(7),
1780 )
1781 .map(&|n: i32| n * 10);
1782 assert_eq!(notice.key(), 7);
1783 let parts = notice.into_parts();
1784 let actions: Vec<_> = parts.actions.into_iter().map(|(_, m)| m).collect();
1785 assert_eq!(actions, [10, 20]);
1786 assert_eq!(parts.on_press, Some(30));
1787 assert_eq!(parts.on_dismiss, Some(40));
1788 assert_eq!(parts.expires.map(|(_, m)| m), Some(50));
1789 let reply = parts.reply.unwrap();
1790 assert_eq!((reply.on_input)("abc".into()), 30);
1791 assert_eq!((reply.on_submit, reply.on_cancel), (Some(60), Some(70)));
1792 }
1793
1794 #[test]
1795 fn modifiers_only_touch_their_nodes() {
1796 let node: Node<i32> = label("a").trailing(label("b")).on_press(1).selected(true);
1797 assert!(matches!(node.into_kind(), Kind::Label { .. }));
1798 let node: Node<i32> = button(label("a"), 1).on_press(2).prominent();
1799 assert!(matches!(
1800 node.into_kind(),
1801 Kind::Button {
1802 on_press: 1,
1803 prominent: true,
1804 ..
1805 }
1806 ));
1807 }
1808
1809 #[test]
1810 fn a_header_takes_a_picture_and_a_subtitle() {
1811 let node = header("t")
1812 .subtitle("s")
1813 .leading(button(icon(Icon::Info), 1))
1814 .tone(Tone::Dim)
1815 .map(&|n: i32| n * 10);
1816 let Kind::Header {
1817 subtitle: Some(subtitle),
1818 leading: Some(leading),
1819 ..
1820 } = node.into_kind()
1821 else {
1822 panic!("header lost its picture or subtitle");
1823 };
1824 assert_eq!(subtitle, "s");
1825 let Kind::Button {
1826 content,
1827 on_press: 10,
1828 ..
1829 } = leading.into_kind()
1830 else {
1831 panic!("picture lost its message");
1832 };
1833 assert!(matches!(
1834 content.into_kind(),
1835 Kind::Icon {
1836 tone: Some(Tone::Dim),
1837 ..
1838 }
1839 ));
1840 }
1841
1842 #[test]
1843 fn timelines_map_and_tone_their_steps() {
1844 let node = timeline([
1845 step(Mark::You, label("a")),
1846 step(Mark::Failed, button(literal("b"), 1)).leading(icon(Icon::Check)),
1847 label("c"),
1848 ])
1849 .tone(Tone::Dim)
1850 .map(&|n: i32| n * 10);
1851 let Kind::Timeline(steps) = node.into_kind() else {
1852 panic!("timeline became something else");
1853 };
1854 let mut steps = steps.into_iter().map(Node::into_kind);
1855 let Some(Kind::Step {
1856 mark: Mark::You,
1857 leading: None,
1858 content,
1859 }) = steps.next()
1860 else {
1861 panic!("first step lost");
1862 };
1863 assert!(matches!(
1864 content.into_kind(),
1865 Kind::Label {
1866 tone: Some(Tone::Dim),
1867 ..
1868 }
1869 ));
1870 let Some(Kind::Step {
1871 mark: Mark::Failed,
1872 leading: Some(leading),
1873 content,
1874 }) = steps.next()
1875 else {
1876 panic!("second step lost its picture");
1877 };
1878 assert!(matches!(
1879 leading.into_kind(),
1880 Kind::Icon {
1881 tone: Some(Tone::Dim),
1882 ..
1883 }
1884 ));
1885 let Kind::Button {
1886 content,
1887 on_press: 10,
1888 ..
1889 } = content.into_kind()
1890 else {
1891 panic!("button lost its message");
1892 };
1893 assert!(matches!(
1894 content.into_kind(),
1895 Kind::Literal {
1896 tone: Some(Tone::Dim),
1897 ..
1898 }
1899 ));
1900 assert!(matches!(steps.next(), Some(Kind::Label { .. })));
1901 let node: Node<()> = diff("f", [(Change::Added, "x".to_string())]).leading(label("y"));
1902 assert!(matches!(node.into_kind(), Kind::Diff { lines, .. } if lines.len() == 1));
1903 }
1904
1905 #[test]
1906 fn tone_reaches_details_and_row_icons() {
1907 let node: Node<()> = column([
1908 detail("n", label("v")),
1909 list_item("t").leading(icon(Icon::Lock)),
1910 ])
1911 .tone(Tone::Good);
1912 let Kind::Column(children) = node.into_kind() else {
1913 unreachable!()
1914 };
1915 let mut children = children.into_iter().map(Node::into_kind);
1916 let Some(Kind::Detail { value, .. }) = children.next() else {
1917 unreachable!()
1918 };
1919 assert!(matches!(
1920 value.into_kind(),
1921 Kind::Label {
1922 tone: Some(Tone::Good),
1923 ..
1924 }
1925 ));
1926 let Some(Kind::ListItem {
1927 leading: Some(leading),
1928 ..
1929 }) = children.next()
1930 else {
1931 unreachable!()
1932 };
1933 assert!(matches!(
1934 leading.into_kind(),
1935 Kind::Icon {
1936 tone: Some(Tone::Good),
1937 ..
1938 }
1939 ));
1940 }
1941}