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
114#[derive(Debug, Clone, Copy, PartialEq, Eq)]
116pub enum Key {
117 Char(char),
119 Enter,
120 Space,
121 Tab,
122 Up,
123 Down,
124 Left,
125 Right,
126}
127
128impl From<char> for Key {
129 fn from(c: char) -> Self {
130 Key::Char(c)
131 }
132}
133
134#[derive(Debug, Clone, Copy, PartialEq, Eq)]
136pub struct Shortcut {
137 pub key: Key,
138 pub ctrl: bool,
139 pub alt: bool,
140 pub shift: bool,
141 pub logo: bool,
143}
144
145#[doc(hidden)]
148#[derive(Debug, Clone, PartialEq, Eq)]
149pub struct Binding {
150 pub id: String,
151 pub key: Option<Shortcut>,
152}
153
154pub fn shortcut(key: impl Into<Key>) -> Shortcut {
157 Shortcut {
158 key: key.into(),
159 ctrl: false,
160 alt: false,
161 shift: false,
162 logo: false,
163 }
164}
165
166impl Shortcut {
167 pub fn ctrl(mut self) -> Self {
168 self.ctrl = true;
169 self
170 }
171
172 pub fn alt(mut self) -> Self {
173 self.alt = true;
174 self
175 }
176
177 pub fn shift(mut self) -> Self {
178 self.shift = true;
179 self
180 }
181
182 pub fn logo(mut self) -> Self {
184 self.logo = true;
185 self
186 }
187}
188
189pub struct Node<M>(Kind<M>);
191
192#[doc(hidden)]
193pub enum Kind<M> {
194 Label {
195 text: String,
196 tone: Option<Tone>,
197 },
198 Heading {
199 text: String,
200 tone: Option<Tone>,
201 },
202 Icon {
203 icon: Icon,
204 tone: Option<Tone>,
205 },
206 AppIcon {
207 app_id: String,
208 tone: Option<Tone>,
209 },
210 Image {
211 file: &'static [u8],
212 tint: bool,
213 tone: Option<Tone>,
214 },
215 Sprite {
216 frames: &'static [&'static [u8]],
217 order: &'static [u8],
219 looping: bool,
221 tone: Option<Tone>,
222 },
223 Cover {
224 url: String,
225 fallback: Box<Node<M>>,
227 large: bool,
229 tone: Option<Tone>,
230 },
231 Counter {
232 value: i64,
233 range: RangeInclusive<i64>,
234 unit: String,
235 tone: Option<Tone>,
236 },
237 Alternate {
238 options: Vec<Node<M>>,
239 current: usize,
240 hovered: Option<usize>,
242 grows: bool,
244 },
245 Row(Vec<Node<M>>),
246 Column(Vec<Node<M>>),
247 Grid {
248 columns: usize,
249 cells: Vec<Node<M>>,
250 },
251 Button {
252 content: Box<Node<M>>,
253 on_press: M,
254 on_secondary: Option<M>,
255 prominent: bool,
257 },
258 Switcher {
259 options: Vec<(Node<M>, M)>,
260 current: Option<usize>,
261 },
262 Gulp(Box<Node<M>>),
263 Center(Box<Node<M>>),
265 Separator,
266 Empty,
267 ListItem {
268 title: String,
269 subtitle: Option<String>,
270 leading: Option<Box<Node<M>>>,
271 trailing: Option<Box<Node<M>>>,
272 selected: bool,
273 on_press: Option<M>,
274 on_secondary: Option<M>,
275 shortcut: Option<Binding>,
276 },
277 List(Vec<Node<M>>),
278 Section {
279 title: String,
280 trailing: Option<Box<Node<M>>>,
281 },
282 Header {
283 title: String,
284 subtitle: Option<String>,
285 on_back: Option<M>,
286 leading: Option<Box<Node<M>>>,
287 trailing: Option<Box<Node<M>>>,
288 },
289 Detail {
290 name: String,
291 value: Box<Node<M>>,
292 },
293 Fill,
294 Slider {
295 value: f32,
296 range: RangeInclusive<f32>,
297 step: f32,
298 on_change: Box<dyn Fn(f32) -> M>,
299 on_release: Option<M>,
300 },
301 Toggle {
302 on: bool,
303 on_toggle: M,
304 },
305 Meter {
306 fraction: f32,
307 limit: Option<f32>,
309 tone: Option<Tone>,
310 },
311 TextInput {
312 placeholder: String,
313 value: String,
314 on_input: Box<dyn Fn(String) -> M>,
315 on_submit: Option<M>,
316 secret: bool,
317 trailing: Option<Box<Node<M>>>,
318 tone: Option<Tone>,
319 },
320 Display(Box<Node<M>>),
321 Mark(Box<Node<M>>),
322 Counted {
323 content: Box<Node<M>>,
324 count: u32,
325 },
326 Badge {
327 text: String,
328 tone: Option<Tone>,
329 },
330 Keys {
331 shortcut: Shortcut,
332 tone: Option<Tone>,
333 },
334 Tabs(Vec<(String, Node<M>)>),
336 Markdown {
337 text: String,
338 tone: Option<Tone>,
339 },
340 TextArea {
341 placeholder: String,
342 value: String,
343 on_input: Box<dyn Fn(String) -> M>,
344 on_submit: Option<M>,
345 trailing: Option<Box<Node<M>>>,
346 },
347 Scroll {
348 content: Box<Node<M>>,
349 bottom: bool,
351 },
352 Literal {
353 text: String,
354 tone: Option<Tone>,
355 },
356 Divider {
357 text: String,
358 },
359 Timeline(Vec<Node<M>>),
360 Step {
361 mark: Mark,
362 leading: Option<Box<Node<M>>>,
364 content: Box<Node<M>>,
365 },
366 Diff {
367 title: String,
368 lines: Vec<(Change, String)>,
369 },
370}
371
372pub fn label<M>(text: impl Into<String>) -> Node<M> {
373 Node(Kind::Label {
374 text: text.into(),
375 tone: None,
376 })
377}
378
379pub fn heading<M>(text: impl Into<String>) -> Node<M> {
381 Node(Kind::Heading {
382 text: text.into(),
383 tone: None,
384 })
385}
386
387pub fn icon<M>(icon: Icon) -> Node<M> {
389 Node(Kind::Icon { icon, tone: None })
390}
391
392pub fn app_icon<M>(app_id: impl Into<String>) -> Node<M> {
396 Node(Kind::AppIcon {
397 app_id: app_id.into(),
398 tone: None,
399 })
400}
401
402pub fn image<M>(file: &'static [u8]) -> Node<M> {
407 Node(Kind::Image {
408 file,
409 tint: false,
410 tone: None,
411 })
412}
413
414pub fn symbol<M>(file: &'static [u8]) -> Node<M> {
417 Node(Kind::Image {
418 file,
419 tint: true,
420 tone: None,
421 })
422}
423
424pub fn sprite<M>(frames: &'static [&'static [u8]], order: &'static [u8]) -> Node<M> {
432 Node(Kind::Sprite {
433 frames,
434 order,
435 looping: false,
436 tone: None,
437 })
438}
439
440pub fn cover<M>(url: impl Into<String>, fallback: Node<M>) -> Node<M> {
445 Node(Kind::Cover {
446 url: url.into(),
447 fallback: Box::new(fallback),
448 large: false,
449 tone: None,
450 })
451}
452
453pub fn counter<M>(value: i64, range: RangeInclusive<i64>, unit: impl Into<String>) -> Node<M> {
457 Node(Kind::Counter {
458 value,
459 range,
460 unit: unit.into(),
461 tone: None,
462 })
463}
464
465pub fn alternate<M>(options: impl IntoIterator<Item = Node<M>>, current: usize) -> Node<M> {
470 Node(Kind::Alternate {
471 options: options.into_iter().collect(),
472 current,
473 hovered: None,
474 grows: false,
475 })
476}
477
478pub fn hover<M>(normal: Node<M>, hovered: Node<M>) -> Node<M> {
481 alternate([normal, hovered], 0).on_hover(1)
482}
483
484pub fn expand<M>(collapsed: Node<M>, expanded: Node<M>) -> Node<M> {
489 Node(Kind::Alternate {
490 options: vec![collapsed, expanded],
491 current: 0,
492 hovered: Some(1),
493 grows: true,
494 })
495}
496
497pub fn row<M>(children: impl IntoIterator<Item = Node<M>>) -> Node<M> {
499 Node(Kind::Row(children.into_iter().collect()))
500}
501
502pub fn column<M>(children: impl IntoIterator<Item = Node<M>>) -> Node<M> {
504 Node(Kind::Column(children.into_iter().collect()))
505}
506
507pub fn center<M>(content: Node<M>) -> Node<M> {
511 Node(Kind::Center(Box::new(content)))
512}
513
514pub fn grid<M>(columns: usize, cells: impl IntoIterator<Item = Node<M>>) -> Node<M> {
516 Node(Kind::Grid {
517 columns: columns.max(1),
518 cells: cells.into_iter().collect(),
519 })
520}
521
522pub fn button<M>(content: Node<M>, on_press: M) -> Node<M> {
523 Node(Kind::Button {
524 content: Box::new(content),
525 on_press,
526 on_secondary: None,
527 prominent: false,
528 })
529}
530
531pub fn switcher<M>(
535 options: impl IntoIterator<Item = (Node<M>, M)>,
536 current: Option<usize>,
537) -> Node<M> {
538 Node(Kind::Switcher {
539 options: options.into_iter().collect(),
540 current,
541 })
542}
543
544pub fn gulp<M>(content: Node<M>) -> Node<M> {
547 Node(Kind::Gulp(Box::new(content)))
548}
549
550pub fn separator<M>() -> Node<M> {
552 Node(Kind::Separator)
553}
554
555pub fn empty<M>() -> Node<M> {
557 Node(Kind::Empty)
558}
559
560pub fn list_item<M>(title: impl Into<String>) -> Node<M> {
566 Node(Kind::ListItem {
567 title: title.into(),
568 subtitle: None,
569 leading: None,
570 trailing: None,
571 selected: false,
572 on_press: None,
573 on_secondary: None,
574 shortcut: None,
575 })
576}
577
578pub fn list<M>(rows: impl IntoIterator<Item = Node<M>>) -> Node<M> {
581 Node(Kind::List(rows.into_iter().collect()))
582}
583
584pub fn section<M>(title: impl Into<String>) -> Node<M> {
587 Node(Kind::Section {
588 title: title.into(),
589 trailing: None,
590 })
591}
592
593pub fn header<M>(title: impl Into<String>) -> Node<M> {
598 Node(Kind::Header {
599 title: title.into(),
600 subtitle: None,
601 on_back: None,
602 leading: None,
603 trailing: None,
604 })
605}
606
607pub fn detail<M>(name: impl Into<String>, value: Node<M>) -> Node<M> {
609 Node(Kind::Detail {
610 name: name.into(),
611 value: Box::new(value),
612 })
613}
614
615pub fn fill<M>() -> Node<M> {
617 Node(Kind::Fill)
618}
619
620pub fn slider<M: 'static>(
623 value: f32,
624 range: RangeInclusive<f32>,
625 on_change: impl Fn(f32) -> M + 'static,
626) -> Node<M> {
627 Node(Kind::Slider {
628 value,
629 range,
630 step: 1.0,
631 on_change: Box::new(on_change),
632 on_release: None,
633 })
634}
635
636pub fn toggle<M>(on: bool, on_toggle: M) -> Node<M> {
638 Node(Kind::Toggle { on, on_toggle })
639}
640
641pub fn meter<M>(fraction: f32) -> Node<M> {
644 Node(Kind::Meter {
645 fraction,
646 limit: None,
647 tone: None,
648 })
649}
650
651pub fn text_input<M: 'static>(
656 placeholder: impl Into<String>,
657 value: impl Into<String>,
658 on_input: impl Fn(String) -> M + 'static,
659) -> Node<M> {
660 Node(Kind::TextInput {
661 placeholder: placeholder.into(),
662 value: value.into(),
663 on_input: Box::new(on_input),
664 on_submit: None,
665 secret: false,
666 trailing: None,
667 tone: None,
668 })
669}
670
671pub fn text_area<M: 'static>(
676 placeholder: impl Into<String>,
677 value: impl Into<String>,
678 on_input: impl Fn(String) -> M + 'static,
679) -> Node<M> {
680 Node(Kind::TextArea {
681 placeholder: placeholder.into(),
682 value: value.into(),
683 on_input: Box::new(on_input),
684 on_submit: None,
685 trailing: None,
686 })
687}
688
689pub fn markdown<M>(text: impl Into<String>) -> Node<M> {
694 Node(Kind::Markdown {
695 text: text.into(),
696 tone: None,
697 })
698}
699
700pub fn scroll<M>(content: Node<M>) -> Node<M> {
704 Node(Kind::Scroll {
705 content: Box::new(content),
706 bottom: false,
707 })
708}
709
710pub fn literal<M>(text: impl Into<String>) -> Node<M> {
713 Node(Kind::Literal {
714 text: text.into(),
715 tone: None,
716 })
717}
718
719pub fn divider<M>(text: impl Into<String>) -> Node<M> {
722 Node(Kind::Divider { text: text.into() })
723}
724
725pub fn timeline<M>(steps: impl IntoIterator<Item = Node<M>>) -> Node<M> {
729 Node(Kind::Timeline(steps.into_iter().collect()))
730}
731
732pub fn step<M>(mark: Mark, content: Node<M>) -> Node<M> {
736 Node(Kind::Step {
737 mark,
738 leading: None,
739 content: Box::new(content),
740 })
741}
742
743pub fn diff<M>(
746 title: impl Into<String>,
747 lines: impl IntoIterator<Item = (Change, String)>,
748) -> Node<M> {
749 Node(Kind::Diff {
750 title: title.into(),
751 lines: lines.into_iter().collect(),
752 })
753}
754
755pub fn display<M>(content: Node<M>) -> Node<M> {
757 Node(Kind::Display(Box::new(content)))
758}
759
760pub fn mark<M>(content: Node<M>) -> Node<M> {
762 Node(Kind::Mark(Box::new(content)))
763}
764
765pub fn counted<M>(content: Node<M>, count: u32) -> Node<M> {
769 Node(Kind::Counted {
770 content: Box::new(content),
771 count,
772 })
773}
774
775pub fn badge<M>(text: impl Into<String>) -> Node<M> {
777 Node(Kind::Badge {
778 text: text.into(),
779 tone: None,
780 })
781}
782
783pub fn keys<M>(shortcut: Shortcut) -> Node<M> {
786 Node(Kind::Keys {
787 shortcut,
788 tone: None,
789 })
790}
791
792pub fn tabs<M>(pages: impl IntoIterator<Item = (impl Into<String>, Node<M>)>) -> Node<M> {
796 Node(Kind::Tabs(
797 pages
798 .into_iter()
799 .map(|(name, page)| (name.into(), page))
800 .collect(),
801 ))
802}
803
804impl<M> Node<M> {
805 pub fn tone(mut self, tone: Tone) -> Self {
807 self.set_tone(tone);
808 self
809 }
810
811 pub fn on_hover(mut self, index: usize) -> Self {
814 if let Kind::Alternate { hovered, .. } = &mut self.0 {
815 *hovered = Some(index);
816 }
817 self
818 }
819
820 pub fn on_secondary(mut self, message: M) -> Self {
823 if let Kind::Button { on_secondary, .. } | Kind::ListItem { on_secondary, .. } = &mut self.0
824 {
825 *on_secondary = Some(message);
826 }
827 self
828 }
829
830 pub fn prominent(mut self) -> Self {
833 if let Kind::Button { prominent, .. } = &mut self.0 {
834 *prominent = true;
835 }
836 self
837 }
838
839 pub fn on_press(mut self, message: M) -> Self {
841 if let Kind::ListItem { on_press, .. } = &mut self.0 {
842 *on_press = Some(message);
843 }
844 self
845 }
846
847 pub fn subtitle(mut self, text: impl Into<String>) -> Self {
850 if let Kind::ListItem { subtitle, .. } | Kind::Header { subtitle, .. } = &mut self.0 {
851 *subtitle = Some(text.into());
852 }
853 self
854 }
855
856 pub fn leading(mut self, node: Node<M>) -> Self {
859 if let Kind::ListItem { leading, .. }
860 | Kind::Header { leading, .. }
861 | Kind::Step { leading, .. } = &mut self.0
862 {
863 *leading = Some(Box::new(node));
864 }
865 self
866 }
867
868 pub fn trailing(mut self, node: Node<M>) -> Self {
871 if let Kind::ListItem { trailing, .. }
872 | Kind::Section { trailing, .. }
873 | Kind::Header { trailing, .. }
874 | Kind::TextInput { trailing, .. }
875 | Kind::TextArea { trailing, .. } = &mut self.0
876 {
877 *trailing = Some(Box::new(node));
878 }
879 self
880 }
881
882 pub fn shortcut(mut self, id: impl Into<String>, key: Shortcut) -> Self {
888 if let Kind::ListItem { shortcut, .. } = &mut self.0 {
889 *shortcut = Some(Binding {
890 id: id.into(),
891 key: Some(key),
892 });
893 }
894 self
895 }
896
897 pub fn selected(mut self, yes: bool) -> Self {
899 if let Kind::ListItem { selected, .. } = &mut self.0 {
900 *selected = yes;
901 }
902 self
903 }
904
905 pub fn on_back(mut self, message: M) -> Self {
907 if let Kind::Header { on_back, .. } = &mut self.0 {
908 *on_back = Some(message);
909 }
910 self
911 }
912
913 pub fn step(mut self, by: f32) -> Self {
915 if let Kind::Slider { step, .. } = &mut self.0 {
916 *step = by;
917 }
918 self
919 }
920
921 pub fn on_release(mut self, message: M) -> Self {
923 if let Kind::Slider { on_release, .. } = &mut self.0 {
924 *on_release = Some(message);
925 }
926 self
927 }
928
929 pub fn on_submit(mut self, message: M) -> Self {
931 if let Kind::TextInput { on_submit, .. } | Kind::TextArea { on_submit, .. } = &mut self.0 {
932 *on_submit = Some(message);
933 }
934 self
935 }
936
937 pub fn secret(mut self, yes: bool) -> Self {
939 if let Kind::TextInput { secret, .. } = &mut self.0 {
940 *secret = yes;
941 }
942 self
943 }
944
945 pub fn looping(mut self) -> Self {
949 if let Kind::Sprite { looping, .. } = &mut self.0 {
950 *looping = true;
951 }
952 self
953 }
954
955 pub fn large(mut self) -> Self {
958 if let Kind::Cover { large, .. } = &mut self.0 {
959 *large = true;
960 }
961 self
962 }
963
964 pub fn bottom(mut self) -> Self {
967 if let Kind::Scroll { bottom, .. } = &mut self.0 {
968 *bottom = true;
969 }
970 self
971 }
972
973 pub fn limit(mut self, at: f32) -> Self {
975 if let Kind::Meter { limit, .. } = &mut self.0 {
976 *limit = Some(at);
977 }
978 self
979 }
980
981 fn set_tone(&mut self, new: Tone) {
982 match &mut self.0 {
983 Kind::Label { tone, .. }
984 | Kind::Heading { tone, .. }
985 | Kind::Icon { tone, .. }
986 | Kind::AppIcon { tone, .. }
987 | Kind::Image { tone, .. }
988 | Kind::Sprite { tone, .. }
989 | Kind::Counter { tone, .. }
990 | Kind::Meter { tone, .. }
991 | Kind::TextInput { tone, .. }
992 | Kind::Markdown { tone, .. }
993 | Kind::Literal { tone, .. }
994 | Kind::Badge { tone, .. }
995 | Kind::Keys { tone, .. } => {
996 tone.get_or_insert(new);
997 }
998 Kind::Cover { tone, fallback, .. } => {
999 tone.get_or_insert(new);
1000 fallback.set_tone(new);
1001 }
1002 Kind::Alternate {
1003 options: children, ..
1004 }
1005 | Kind::Row(children)
1006 | Kind::Column(children)
1007 | Kind::Grid {
1008 cells: children, ..
1009 }
1010 | Kind::List(children)
1011 | Kind::Timeline(children) => {
1012 children.iter_mut().for_each(|child| child.set_tone(new));
1013 }
1014 Kind::Step {
1015 leading, content, ..
1016 } => {
1017 leading.iter_mut().for_each(|child| child.set_tone(new));
1018 content.set_tone(new);
1019 }
1020 Kind::Button { content, .. }
1021 | Kind::Gulp(content)
1022 | Kind::Center(content)
1023 | Kind::Display(content)
1024 | Kind::Mark(content)
1025 | Kind::Counted { content, .. }
1026 | Kind::Scroll { content, .. }
1027 | Kind::Detail { value: content, .. } => content.set_tone(new),
1028 Kind::ListItem {
1029 leading, trailing, ..
1030 }
1031 | Kind::Header {
1032 leading, trailing, ..
1033 } => {
1034 leading
1035 .iter_mut()
1036 .chain(trailing.iter_mut())
1037 .for_each(|child| child.set_tone(new));
1038 }
1039 Kind::Tabs(pages) => {
1040 for (_, page) in pages {
1041 page.set_tone(new);
1042 }
1043 }
1044 Kind::Switcher { options, .. } => {
1045 options
1046 .iter_mut()
1047 .for_each(|(child, _)| child.set_tone(new));
1048 }
1049 Kind::Separator
1050 | Kind::Divider { .. }
1051 | Kind::Diff { .. }
1052 | Kind::Empty
1053 | Kind::Section { .. }
1054 | Kind::Fill
1055 | Kind::Slider { .. }
1056 | Kind::TextArea { .. }
1057 | Kind::Toggle { .. } => {}
1058 }
1059 }
1060
1061 pub fn map<N: 'static>(self, f: &(impl Fn(M) -> N + Clone + 'static)) -> Node<N>
1065 where
1066 M: 'static,
1067 {
1068 let map_all = |nodes: Vec<Node<M>>| nodes.into_iter().map(|node| node.map(f)).collect();
1069 let map_box = |node: Option<Box<Node<M>>>| node.map(|node| Box::new(node.map(f)));
1070
1071 Node(match self.0 {
1072 Kind::Label { text, tone } => Kind::Label { text, tone },
1073 Kind::Heading { text, tone } => Kind::Heading { text, tone },
1074 Kind::Icon { icon, tone } => Kind::Icon { icon, tone },
1075 Kind::AppIcon { app_id, tone } => Kind::AppIcon { app_id, tone },
1076 Kind::Image { file, tint, tone } => Kind::Image { file, tint, tone },
1077 Kind::Sprite {
1078 frames,
1079 order,
1080 looping,
1081 tone,
1082 } => Kind::Sprite {
1083 frames,
1084 order,
1085 looping,
1086 tone,
1087 },
1088 Kind::Cover {
1089 url,
1090 fallback,
1091 large,
1092 tone,
1093 } => Kind::Cover {
1094 url,
1095 fallback: Box::new(fallback.map(f)),
1096 large,
1097 tone,
1098 },
1099 Kind::Counter {
1100 value,
1101 range,
1102 unit,
1103 tone,
1104 } => Kind::Counter {
1105 value,
1106 range,
1107 unit,
1108 tone,
1109 },
1110 Kind::Alternate {
1111 options,
1112 current,
1113 hovered,
1114 grows,
1115 } => Kind::Alternate {
1116 options: map_all(options),
1117 current,
1118 hovered,
1119 grows,
1120 },
1121 Kind::Row(children) => Kind::Row(map_all(children)),
1122 Kind::Column(children) => Kind::Column(map_all(children)),
1123 Kind::Grid { columns, cells } => Kind::Grid {
1124 columns,
1125 cells: map_all(cells),
1126 },
1127 Kind::Button {
1128 content,
1129 on_press,
1130 on_secondary,
1131 prominent,
1132 } => Kind::Button {
1133 content: Box::new(content.map(f)),
1134 on_press: f(on_press),
1135 on_secondary: on_secondary.map(f),
1136 prominent,
1137 },
1138 Kind::Switcher { options, current } => Kind::Switcher {
1139 options: options
1140 .into_iter()
1141 .map(|(node, message)| (node.map(f), f(message)))
1142 .collect(),
1143 current,
1144 },
1145 Kind::Gulp(content) => Kind::Gulp(Box::new(content.map(f))),
1146 Kind::Center(content) => Kind::Center(Box::new(content.map(f))),
1147 Kind::Separator => Kind::Separator,
1148 Kind::Empty => Kind::Empty,
1149 Kind::ListItem {
1150 title,
1151 subtitle,
1152 leading,
1153 trailing,
1154 selected,
1155 on_press,
1156 on_secondary,
1157 shortcut,
1158 } => Kind::ListItem {
1159 title,
1160 subtitle,
1161 leading: map_box(leading),
1162 trailing: map_box(trailing),
1163 selected,
1164 on_press: on_press.map(f),
1165 on_secondary: on_secondary.map(f),
1166 shortcut,
1167 },
1168 Kind::List(rows) => Kind::List(map_all(rows)),
1169 Kind::Section { title, trailing } => Kind::Section {
1170 title,
1171 trailing: map_box(trailing),
1172 },
1173 Kind::Header {
1174 title,
1175 subtitle,
1176 on_back,
1177 leading,
1178 trailing,
1179 } => Kind::Header {
1180 title,
1181 subtitle,
1182 on_back: on_back.map(f),
1183 leading: map_box(leading),
1184 trailing: map_box(trailing),
1185 },
1186 Kind::Detail { name, value } => Kind::Detail {
1187 name,
1188 value: Box::new(value.map(f)),
1189 },
1190 Kind::Fill => Kind::Fill,
1191 Kind::Slider {
1192 value,
1193 range,
1194 step,
1195 on_change,
1196 on_release,
1197 } => {
1198 let g = f.clone();
1199 Kind::Slider {
1200 value,
1201 range,
1202 step,
1203 on_change: Box::new(move |value| g(on_change(value))),
1204 on_release: on_release.map(f),
1205 }
1206 }
1207 Kind::Toggle { on, on_toggle } => Kind::Toggle {
1208 on,
1209 on_toggle: f(on_toggle),
1210 },
1211 Kind::Meter {
1212 fraction,
1213 limit,
1214 tone,
1215 } => Kind::Meter {
1216 fraction,
1217 limit,
1218 tone,
1219 },
1220 Kind::TextInput {
1221 placeholder,
1222 value,
1223 on_input,
1224 on_submit,
1225 secret,
1226 trailing,
1227 tone,
1228 } => {
1229 let g = f.clone();
1230 Kind::TextInput {
1231 placeholder,
1232 value,
1233 on_input: Box::new(move |text| g(on_input(text))),
1234 on_submit: on_submit.map(f),
1235 secret,
1236 trailing: map_box(trailing),
1237 tone,
1238 }
1239 }
1240 Kind::Display(content) => Kind::Display(Box::new(content.map(f))),
1241 Kind::Mark(content) => Kind::Mark(Box::new(content.map(f))),
1242 Kind::Counted { content, count } => Kind::Counted {
1243 content: Box::new(content.map(f)),
1244 count,
1245 },
1246 Kind::Badge { text, tone } => Kind::Badge { text, tone },
1247 Kind::Keys { shortcut, tone } => Kind::Keys { shortcut, tone },
1248 Kind::Tabs(pages) => Kind::Tabs(
1249 pages
1250 .into_iter()
1251 .map(|(name, page)| (name, page.map(f)))
1252 .collect(),
1253 ),
1254 Kind::Markdown { text, tone } => Kind::Markdown { text, tone },
1255 Kind::TextArea {
1256 placeholder,
1257 value,
1258 on_input,
1259 on_submit,
1260 trailing,
1261 } => {
1262 let g = f.clone();
1263 Kind::TextArea {
1264 placeholder,
1265 value,
1266 on_input: Box::new(move |text| g(on_input(text))),
1267 on_submit: on_submit.map(f),
1268 trailing: map_box(trailing),
1269 }
1270 }
1271 Kind::Scroll { content, bottom } => Kind::Scroll {
1272 content: Box::new(content.map(f)),
1273 bottom,
1274 },
1275 Kind::Literal { text, tone } => Kind::Literal { text, tone },
1276 Kind::Divider { text } => Kind::Divider { text },
1277 Kind::Timeline(steps) => Kind::Timeline(map_all(steps)),
1278 Kind::Step {
1279 mark,
1280 leading,
1281 content,
1282 } => Kind::Step {
1283 mark,
1284 leading: map_box(leading),
1285 content: Box::new(content.map(f)),
1286 },
1287 Kind::Diff { title, lines } => Kind::Diff { title, lines },
1288 })
1289 }
1290
1291 #[doc(hidden)]
1292 pub fn into_kind(self) -> Kind<M> {
1293 self.0
1294 }
1295
1296 #[doc(hidden)]
1297 pub fn kind(&self) -> &Kind<M> {
1298 &self.0
1299 }
1300
1301 #[doc(hidden)]
1302 pub fn kind_mut(&mut self) -> &mut Kind<M> {
1303 &mut self.0
1304 }
1305}
1306
1307pub struct Item<M> {
1309 content: Option<Node<M>>,
1310 on_press: Option<M>,
1311 on_secondary: Option<M>,
1312 on_scroll: Option<(M, M)>,
1313}
1314
1315impl<M> Item<M> {
1316 pub fn new(content: Node<M>) -> Self {
1317 Self {
1318 content: Some(content),
1319 on_press: None,
1320 on_secondary: None,
1321 on_scroll: None,
1322 }
1323 }
1324
1325 pub fn hidden() -> Self {
1327 Self {
1328 content: None,
1329 on_press: None,
1330 on_secondary: None,
1331 on_scroll: None,
1332 }
1333 }
1334
1335 pub fn on_press(mut self, message: M) -> Self {
1337 self.on_press = Some(message);
1338 self
1339 }
1340
1341 pub fn on_secondary(mut self, message: M) -> Self {
1343 self.on_secondary = Some(message);
1344 self
1345 }
1346
1347 pub fn on_scroll(mut self, up: M, down: M) -> Self {
1350 self.on_scroll = Some((up, down));
1351 self
1352 }
1353
1354 pub fn map<N: 'static>(self, f: &(impl Fn(M) -> N + Clone + 'static)) -> Item<N>
1355 where
1356 M: 'static,
1357 {
1358 Item {
1359 content: self.content.map(|content| content.map(f)),
1360 on_press: self.on_press.map(f),
1361 on_secondary: self.on_secondary.map(f),
1362 on_scroll: self.on_scroll.map(|(up, down)| (f(up), f(down))),
1363 }
1364 }
1365
1366 #[doc(hidden)]
1367 pub fn into_parts(self) -> ItemParts<M> {
1368 ItemParts {
1369 content: self.content,
1370 on_press: self.on_press,
1371 on_secondary: self.on_secondary,
1372 on_scroll: self.on_scroll,
1373 }
1374 }
1375}
1376
1377#[doc(hidden)]
1378pub struct ItemParts<M> {
1379 pub content: Option<Node<M>>,
1380 pub on_press: Option<M>,
1381 pub on_secondary: Option<M>,
1382 pub on_scroll: Option<(M, M)>,
1384}
1385
1386pub struct Panel<M> {
1389 content: Node<M>,
1390 on_open: Option<M>,
1391 on_close: Option<M>,
1392 wide: bool,
1393}
1394
1395impl<M> Panel<M> {
1396 pub fn new(content: Node<M>) -> Self {
1397 Self {
1398 content,
1399 on_open: None,
1400 on_close: None,
1401 wide: false,
1402 }
1403 }
1404
1405 pub fn wide(mut self) -> Self {
1408 self.wide = true;
1409 self
1410 }
1411
1412 #[doc(hidden)]
1413 pub fn is_wide(&self) -> bool {
1414 self.wide
1415 }
1416
1417 pub fn on_open(mut self, message: M) -> Self {
1419 self.on_open = Some(message);
1420 self
1421 }
1422
1423 pub fn on_close(mut self, message: M) -> Self {
1425 self.on_close = Some(message);
1426 self
1427 }
1428
1429 pub fn map<N: 'static>(self, f: &(impl Fn(M) -> N + Clone + 'static)) -> Panel<N>
1430 where
1431 M: 'static,
1432 {
1433 Panel {
1434 content: self.content.map(f),
1435 on_open: self.on_open.map(f),
1436 on_close: self.on_close.map(f),
1437 wide: self.wide,
1438 }
1439 }
1440
1441 #[doc(hidden)]
1442 pub fn into_node(self) -> Node<M> {
1443 self.content
1444 }
1445
1446 #[doc(hidden)]
1447 pub fn on_open_message(self) -> Option<M> {
1448 self.on_open
1449 }
1450
1451 #[doc(hidden)]
1452 pub fn on_close_message(self) -> Option<M> {
1453 self.on_close
1454 }
1455}
1456
1457#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1459pub enum Urgency {
1460 Low,
1462 #[default]
1463 Normal,
1464 Critical,
1466}
1467
1468pub struct Notice<M> {
1475 key: u64,
1476 summary: String,
1477 app: Option<String>,
1478 app_icon: Option<String>,
1479 icon: Option<Icon>,
1480 image: Option<String>,
1481 pixels: Option<Pixels>,
1482 body: Option<String>,
1483 markdown: bool,
1484 urgency: Urgency,
1485 time: Option<SystemTime>,
1486 thread: Option<String>,
1487 thread_name: Option<String>,
1488 progress: Option<f32>,
1489 actions: Vec<(String, M)>,
1490 choices: Option<ChoicesParts<M>>,
1491 step: Option<StepParts<M>>,
1492 on_press: Option<M>,
1493 on_dismiss: Option<M>,
1494 expires: Option<(std::time::Duration, M)>,
1495 reply: Option<Reply<M>>,
1496}
1497
1498#[derive(Clone)]
1504pub struct Pixels {
1505 width: u32,
1506 height: u32,
1507 rgba: Arc<[u8]>,
1508}
1509
1510impl Pixels {
1511 pub fn new(width: u32, height: u32, rgba: impl Into<Arc<[u8]>>) -> Option<Self> {
1513 let rgba = rgba.into();
1514 let expected = (width as usize)
1515 .checked_mul(height as usize)?
1516 .checked_mul(4)?;
1517 (width > 0 && height > 0 && rgba.len() == expected).then_some(Self {
1518 width,
1519 height,
1520 rgba,
1521 })
1522 }
1523
1524 pub fn width(&self) -> u32 {
1525 self.width
1526 }
1527
1528 pub fn height(&self) -> u32 {
1529 self.height
1530 }
1531
1532 pub fn rgba(&self) -> &Arc<[u8]> {
1533 &self.rgba
1534 }
1535}
1536
1537impl std::fmt::Debug for Pixels {
1538 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1539 write!(f, "Pixels({}×{})", self.width, self.height)
1540 }
1541}
1542
1543pub struct Choices<M> {
1546 parts: ChoicesParts<M>,
1547}
1548
1549impl<M> Choices<M> {
1550 pub fn one() -> Self {
1552 Self::new(false)
1553 }
1554
1555 pub fn many() -> Self {
1558 Self::new(true)
1559 }
1560
1561 fn new(multiple: bool) -> Self {
1562 Self {
1563 parts: ChoicesParts {
1564 multiple,
1565 options: Vec::new(),
1566 on_send: None,
1567 },
1568 }
1569 }
1570
1571 pub fn option(
1573 mut self,
1574 label: impl Into<String>,
1575 description: Option<String>,
1576 picked: bool,
1577 on_pick: M,
1578 ) -> Self {
1579 self.parts.options.push(ChoiceParts {
1580 label: label.into(),
1581 description: description.filter(|d| !d.is_empty()),
1582 recommended: false,
1583 picked,
1584 on_pick,
1585 });
1586 self
1587 }
1588
1589 pub fn recommended(mut self) -> Self {
1591 if let Some(option) = self.parts.options.last_mut() {
1592 option.recommended = true;
1593 }
1594 self
1595 }
1596
1597 pub fn on_send(mut self, message: M) -> Self {
1599 self.parts.on_send = Some(message);
1600 self
1601 }
1602
1603 fn map<N>(self, f: &impl Fn(M) -> N) -> Choices<N> {
1604 let parts = self.parts;
1605 Choices {
1606 parts: ChoicesParts {
1607 multiple: parts.multiple,
1608 options: parts
1609 .options
1610 .into_iter()
1611 .map(|option| ChoiceParts {
1612 label: option.label,
1613 description: option.description,
1614 recommended: option.recommended,
1615 picked: option.picked,
1616 on_pick: f(option.on_pick),
1617 })
1618 .collect(),
1619 on_send: parts.on_send.map(f),
1620 },
1621 }
1622 }
1623}
1624
1625#[doc(hidden)]
1626#[derive(Clone)]
1627pub struct ChoicesParts<M> {
1628 pub multiple: bool,
1629 pub options: Vec<ChoiceParts<M>>,
1630 pub on_send: Option<M>,
1631}
1632
1633#[doc(hidden)]
1634#[derive(Clone)]
1635pub struct ChoiceParts<M> {
1636 pub label: String,
1637 pub description: Option<String>,
1638 pub recommended: bool,
1639 pub picked: bool,
1640 pub on_pick: M,
1641}
1642
1643#[doc(hidden)]
1644#[derive(Clone)]
1645pub struct StepParts<M> {
1646 pub index: usize,
1648 pub count: usize,
1649 pub on_back: Option<M>,
1650}
1651
1652pub struct Reply<M> {
1659 placeholder: String,
1660 value: String,
1661 on_input: std::rc::Rc<dyn Fn(String) -> M>,
1662 on_submit: Option<M>,
1663 on_cancel: Option<M>,
1664 sent: bool,
1665}
1666
1667impl<M> Reply<M> {
1668 pub fn new(
1670 placeholder: impl Into<String>,
1671 value: impl Into<String>,
1672 on_input: impl Fn(String) -> M + 'static,
1673 ) -> Self {
1674 Self {
1675 placeholder: placeholder.into(),
1676 value: value.into(),
1677 on_input: std::rc::Rc::new(on_input),
1678 on_submit: None,
1679 on_cancel: None,
1680 sent: false,
1681 }
1682 }
1683
1684 pub fn on_submit(mut self, message: M) -> Self {
1686 self.on_submit = Some(message);
1687 self
1688 }
1689
1690 pub fn on_cancel(mut self, message: M) -> Self {
1692 self.on_cancel = Some(message);
1693 self
1694 }
1695
1696 pub fn sent(mut self, sent: bool) -> Self {
1699 self.sent = sent;
1700 self
1701 }
1702
1703 pub fn map<N: 'static>(self, f: &(impl Fn(M) -> N + Clone + 'static)) -> Reply<N>
1704 where
1705 M: 'static,
1706 {
1707 let g = f.clone();
1708 let on_input = self.on_input;
1709 Reply {
1710 placeholder: self.placeholder,
1711 value: self.value,
1712 on_input: std::rc::Rc::new(move |text| g(on_input(text))),
1713 on_submit: self.on_submit.map(f),
1714 on_cancel: self.on_cancel.map(f),
1715 sent: self.sent,
1716 }
1717 }
1718
1719 #[doc(hidden)]
1720 pub fn into_parts(self) -> ReplyParts<M> {
1721 ReplyParts {
1722 placeholder: self.placeholder,
1723 value: self.value,
1724 on_input: self.on_input,
1725 on_submit: self.on_submit,
1726 on_cancel: self.on_cancel,
1727 sent: self.sent,
1728 }
1729 }
1730}
1731
1732#[doc(hidden)]
1733#[derive(Clone)]
1734pub struct ReplyParts<M> {
1735 pub placeholder: String,
1736 pub value: String,
1737 pub on_input: std::rc::Rc<dyn Fn(String) -> M>,
1738 pub on_submit: Option<M>,
1739 pub on_cancel: Option<M>,
1740 pub sent: bool,
1741}
1742
1743impl<M> Notice<M> {
1744 pub fn new(key: u64, summary: impl Into<String>) -> Self {
1745 Self {
1746 key,
1747 summary: summary.into(),
1748 app: None,
1749 app_icon: None,
1750 icon: None,
1751 image: None,
1752 pixels: None,
1753 body: None,
1754 markdown: false,
1755 urgency: Urgency::Normal,
1756 time: None,
1757 thread: None,
1758 thread_name: None,
1759 progress: None,
1760 actions: Vec::new(),
1761 choices: None,
1762 step: None,
1763 on_press: None,
1764 on_dismiss: None,
1765 expires: None,
1766 reply: None,
1767 }
1768 }
1769
1770 pub fn app(mut self, name: impl Into<String>) -> Self {
1772 self.app = Some(name.into());
1773 self
1774 }
1775
1776 pub fn app_icon(mut self, app_id: impl Into<String>) -> Self {
1778 self.app_icon = Some(app_id.into());
1779 self
1780 }
1781
1782 pub fn icon(mut self, icon: Icon) -> Self {
1786 self.icon = Some(icon);
1787 self
1788 }
1789
1790 pub fn image(mut self, url: impl Into<String>) -> Self {
1793 self.image = Some(url.into());
1794 self
1795 }
1796
1797 pub fn pixels(mut self, pixels: Pixels) -> Self {
1800 self.pixels = Some(pixels);
1801 self
1802 }
1803
1804 pub fn time(mut self, time: SystemTime) -> Self {
1806 self.time = Some(time);
1807 self
1808 }
1809
1810 pub fn thread(mut self, key: impl Into<String>) -> Self {
1813 self.thread = Some(key.into());
1814 self
1815 }
1816
1817 pub fn thread_name(mut self, name: impl Into<String>) -> Self {
1819 self.thread_name = Some(name.into());
1820 self
1821 }
1822
1823 pub fn progress(mut self, fraction: f32) -> Self {
1825 self.progress = Some(fraction.clamp(0.0, 1.0));
1826 self
1827 }
1828
1829 pub fn choices(mut self, choices: Choices<M>) -> Self {
1831 self.choices = Some(choices.parts);
1832 self
1833 }
1834
1835 pub fn step(mut self, index: usize, count: usize, on_back: Option<M>) -> Self {
1839 self.step = Some(StepParts {
1840 index,
1841 count,
1842 on_back,
1843 });
1844 self
1845 }
1846
1847 pub fn body(mut self, text: impl Into<String>) -> Self {
1849 self.body = Some(text.into());
1850 self
1851 }
1852
1853 pub fn markdown(mut self) -> Self {
1855 self.markdown = true;
1856 self
1857 }
1858
1859 pub fn urgency(mut self, urgency: Urgency) -> Self {
1860 self.urgency = urgency;
1861 self
1862 }
1863
1864 pub fn action(mut self, label: impl Into<String>, message: M) -> Self {
1866 self.actions.push((label.into(), message));
1867 self
1868 }
1869
1870 pub fn on_press(mut self, message: M) -> Self {
1872 self.on_press = Some(message);
1873 self
1874 }
1875
1876 pub fn on_dismiss(mut self, message: M) -> Self {
1878 self.on_dismiss = Some(message);
1879 self
1880 }
1881
1882 pub fn expires(mut self, after: std::time::Duration, message: M) -> Self {
1886 self.expires = Some((after, message));
1887 self
1888 }
1889
1890 pub fn reply(mut self, reply: Reply<M>) -> Self {
1892 self.reply = Some(reply);
1893 self
1894 }
1895
1896 pub fn map<N: 'static>(self, f: &(impl Fn(M) -> N + Clone + 'static)) -> Notice<N>
1897 where
1898 M: 'static,
1899 {
1900 Notice {
1901 key: self.key,
1902 summary: self.summary,
1903 app: self.app,
1904 app_icon: self.app_icon,
1905 icon: self.icon,
1906 image: self.image,
1907 pixels: self.pixels,
1908 body: self.body,
1909 markdown: self.markdown,
1910 urgency: self.urgency,
1911 time: self.time,
1912 thread: self.thread,
1913 thread_name: self.thread_name,
1914 progress: self.progress,
1915 actions: self
1916 .actions
1917 .into_iter()
1918 .map(|(label, message)| (label, f(message)))
1919 .collect(),
1920 choices: self.choices.map(|parts| Choices { parts }.map(f).parts),
1921 step: self.step.map(|step| StepParts {
1922 index: step.index,
1923 count: step.count,
1924 on_back: step.on_back.map(f),
1925 }),
1926 on_press: self.on_press.map(f),
1927 on_dismiss: self.on_dismiss.map(f),
1928 expires: self.expires.map(|(after, message)| (after, f(message))),
1929 reply: self.reply.map(|reply| reply.map(f)),
1930 }
1931 }
1932
1933 pub fn key(&self) -> u64 {
1934 self.key
1935 }
1936
1937 #[doc(hidden)]
1938 pub fn into_parts(self) -> NoticeParts<M> {
1939 NoticeParts {
1940 key: self.key,
1941 summary: self.summary,
1942 app: self.app,
1943 app_icon: self.app_icon,
1944 icon: self.icon,
1945 image: self.image,
1946 pixels: self.pixels,
1947 body: self.body,
1948 markdown: self.markdown,
1949 urgency: self.urgency,
1950 time: self.time,
1951 thread: self.thread,
1952 thread_name: self.thread_name,
1953 progress: self.progress,
1954 actions: self.actions,
1955 choices: self.choices,
1956 step: self.step,
1957 on_press: self.on_press,
1958 on_dismiss: self.on_dismiss,
1959 expires: self.expires,
1960 reply: self.reply.map(Reply::into_parts),
1961 }
1962 }
1963}
1964
1965#[doc(hidden)]
1966#[derive(Clone)]
1967pub struct NoticeParts<M> {
1968 pub key: u64,
1969 pub summary: String,
1970 pub app: Option<String>,
1971 pub app_icon: Option<String>,
1972 pub icon: Option<Icon>,
1973 pub image: Option<String>,
1974 pub pixels: Option<Pixels>,
1975 pub body: Option<String>,
1976 pub markdown: bool,
1977 pub urgency: Urgency,
1978 pub time: Option<SystemTime>,
1979 pub thread: Option<String>,
1980 pub thread_name: Option<String>,
1981 pub progress: Option<f32>,
1982 pub actions: Vec<(String, M)>,
1983 pub choices: Option<ChoicesParts<M>>,
1984 pub step: Option<StepParts<M>>,
1985 pub on_press: Option<M>,
1986 pub on_dismiss: Option<M>,
1987 pub expires: Option<(std::time::Duration, M)>,
1988 pub reply: Option<ReplyParts<M>>,
1989}
1990
1991pub struct Action<M> {
1995 id: Option<String>,
1996 shortcut: Option<Shortcut>,
1997 title: String,
1998 subtitle: Option<String>,
1999 keywords: Vec<String>,
2000 leading: Option<Node<M>>,
2001 message: M,
2002}
2003
2004pub fn action<M>(title: impl Into<String>, message: M) -> Action<M> {
2006 Action {
2007 id: None,
2008 shortcut: None,
2009 title: title.into(),
2010 subtitle: None,
2011 keywords: Vec::new(),
2012 leading: None,
2013 message,
2014 }
2015}
2016
2017impl<M> Action<M> {
2018 pub fn id(mut self, id: impl Into<String>) -> Self {
2022 self.id = Some(id.into());
2023 self
2024 }
2025
2026 pub fn shortcut(mut self, key: Shortcut) -> Self {
2031 self.shortcut = Some(key);
2032 self
2033 }
2034
2035 pub fn subtitle(mut self, text: impl Into<String>) -> Self {
2037 self.subtitle = Some(text.into());
2038 self
2039 }
2040
2041 pub fn keywords(mut self, words: impl IntoIterator<Item = impl Into<String>>) -> Self {
2043 self.keywords.extend(words.into_iter().map(Into::into));
2044 self
2045 }
2046
2047 pub fn leading(mut self, node: Node<M>) -> Self {
2049 self.leading = Some(node);
2050 self
2051 }
2052
2053 pub fn map<N: 'static>(self, f: &(impl Fn(M) -> N + Clone + 'static)) -> Action<N>
2054 where
2055 M: 'static,
2056 {
2057 Action {
2058 id: self.id,
2059 shortcut: self.shortcut,
2060 title: self.title,
2061 subtitle: self.subtitle,
2062 keywords: self.keywords,
2063 leading: self.leading.map(|node| node.map(f)),
2064 message: f(self.message),
2065 }
2066 }
2067
2068 pub fn title(&self) -> &str {
2069 &self.title
2070 }
2071
2072 #[doc(hidden)]
2073 pub fn id_str(&self) -> Option<&str> {
2074 self.id.as_deref()
2075 }
2076
2077 #[doc(hidden)]
2078 pub fn into_parts(self) -> ActionParts<M> {
2079 ActionParts {
2080 id: self.id,
2081 shortcut: self.shortcut,
2082 title: self.title,
2083 subtitle: self.subtitle,
2084 keywords: self.keywords,
2085 leading: self.leading,
2086 message: self.message,
2087 }
2088 }
2089}
2090
2091#[doc(hidden)]
2092pub struct ActionParts<M> {
2093 pub id: Option<String>,
2094 pub shortcut: Option<Shortcut>,
2095 pub title: String,
2096 pub subtitle: Option<String>,
2097 pub keywords: Vec<String>,
2098 pub leading: Option<Node<M>>,
2099 pub message: M,
2100}
2101
2102#[cfg(test)]
2103mod tests {
2104 use super::*;
2105
2106 #[test]
2107 fn map_converts_every_message() {
2108 let node = row([
2109 button(label("a"), 1).on_secondary(4),
2110 column([button(label("b"), 2), label("c")]),
2111 switcher([(label("d"), 3)], Some(0)),
2112 ]);
2113
2114 let Kind::Row(children) = node.map(&|n: i32| n * 10).into_kind() else {
2115 panic!("row became something else");
2116 };
2117 let mut messages = Vec::new();
2118 collect(children, &mut messages);
2119 assert_eq!(messages, [10, 40, 20, 30]);
2120 }
2121
2122 fn collect(nodes: Vec<Node<i32>>, out: &mut Vec<i32>) {
2123 for node in nodes {
2124 match node.into_kind() {
2125 Kind::Button {
2126 on_press,
2127 on_secondary,
2128 ..
2129 } => {
2130 out.push(on_press);
2131 out.extend(on_secondary);
2132 }
2133 Kind::Switcher { options, .. } => {
2134 out.extend(options.into_iter().map(|(_, message)| message))
2135 }
2136 Kind::Row(children) | Kind::Column(children) => collect(children, out),
2137 _ => {}
2138 }
2139 }
2140 }
2141
2142 #[test]
2143 fn hover_shows_the_second_option() {
2144 let node: Node<()> = hover(label("a"), label("b"));
2145 assert!(matches!(
2146 node.into_kind(),
2147 Kind::Alternate {
2148 current: 0,
2149 hovered: Some(1),
2150 ..
2151 }
2152 ));
2153 let node: Node<()> = label("a").on_hover(1);
2154 assert!(matches!(node.into_kind(), Kind::Label { .. }));
2155 }
2156
2157 #[test]
2158 fn tone_fills_only_unset_text() {
2159 let node: Node<()> =
2160 row([label("a").tone(Tone::Accent), label("b"), icon(Icon::Wired)]).tone(Tone::Dim);
2161 let Kind::Row(children) = node.into_kind() else {
2162 unreachable!()
2163 };
2164 let tones: Vec<_> = children
2165 .into_iter()
2166 .map(|child| match child.into_kind() {
2167 Kind::Label { tone, .. } | Kind::Icon { tone, .. } => tone,
2168 _ => None,
2169 })
2170 .collect();
2171 assert_eq!(
2172 tones,
2173 [Some(Tone::Accent), Some(Tone::Dim), Some(Tone::Dim)]
2174 );
2175 }
2176
2177 #[test]
2178 fn map_reaches_new_nodes_and_their_closures() {
2179 let node = column([
2180 header("h").on_back(1).trailing(toggle(true, 2)),
2181 list([list_item("a")
2182 .on_press(3)
2183 .on_secondary(4)
2184 .trailing(button(icon(Icon::Info), 5))]),
2185 slider(0.5, 0.0..=1.0, |v| (v * 10.0) as i32).on_release(6),
2186 text_input("p", "v", |text: String| text.len() as i32).on_submit(7),
2187 scroll(text_area("p", "v", |text: String| text.len() as i32 + 1).on_submit(8)).bottom(),
2188 ]);
2189 let Kind::Column(children) = node.map(&|n: i32| n * 10).into_kind() else {
2190 panic!("column became something else");
2191 };
2192 let mut children = children.into_iter().map(Node::into_kind);
2193 let Some(Kind::Header {
2194 on_back: Some(10),
2195 trailing: Some(trailing),
2196 ..
2197 }) = children.next()
2198 else {
2199 panic!("header lost its back message");
2200 };
2201 assert!(matches!(
2202 trailing.into_kind(),
2203 Kind::Toggle { on_toggle: 20, .. }
2204 ));
2205 let Some(Kind::List(rows)) = children.next() else {
2206 panic!("list lost");
2207 };
2208 let Some(Kind::ListItem {
2209 on_press: Some(30),
2210 on_secondary: Some(40),
2211 trailing: Some(trailing),
2212 ..
2213 }) = rows.into_iter().next().map(Node::into_kind)
2214 else {
2215 panic!("list item lost its messages");
2216 };
2217 assert!(matches!(
2218 trailing.into_kind(),
2219 Kind::Button { on_press: 50, .. }
2220 ));
2221 let Some(Kind::Slider {
2222 on_change,
2223 on_release: Some(60),
2224 ..
2225 }) = children.next()
2226 else {
2227 panic!("slider lost");
2228 };
2229 assert_eq!(on_change(0.3), 30);
2230 let Some(Kind::TextInput {
2231 on_input,
2232 on_submit: Some(70),
2233 ..
2234 }) = children.next()
2235 else {
2236 panic!("text input lost");
2237 };
2238 assert_eq!(on_input("abcd".into()), 40);
2239 let Some(Kind::Scroll {
2240 content,
2241 bottom: true,
2242 }) = children.next()
2243 else {
2244 panic!("scroll lost");
2245 };
2246 let Kind::TextArea {
2247 on_input,
2248 on_submit: Some(80),
2249 ..
2250 } = content.into_kind()
2251 else {
2252 panic!("text area lost");
2253 };
2254 assert_eq!(on_input("abcd".into()), 50);
2255 }
2256
2257 #[test]
2258 fn tabs_map_and_tone_every_page() {
2259 let node = tabs([("a", button(label("x"), 1)), ("b", button(label("y"), 2))])
2260 .tone(Tone::Dim)
2261 .map(&|n: i32| n * 10);
2262 let Kind::Tabs(pages) = node.into_kind() else {
2263 panic!("tabs became something else");
2264 };
2265 let names: Vec<_> = pages.iter().map(|(name, _)| name.as_str()).collect();
2266 assert_eq!(names, ["a", "b"]);
2267 let mut messages = Vec::new();
2268 for (_, page) in pages {
2269 let Kind::Button {
2270 content, on_press, ..
2271 } = page.into_kind()
2272 else {
2273 panic!("page lost its button");
2274 };
2275 assert!(matches!(
2276 content.into_kind(),
2277 Kind::Label {
2278 tone: Some(Tone::Dim),
2279 ..
2280 }
2281 ));
2282 messages.push(on_press);
2283 }
2284 assert_eq!(messages, [10, 20]);
2285 }
2286
2287 #[test]
2288 fn notice_map_reaches_every_message() {
2289 let notice = Notice::new(7, "s")
2290 .action("a", 1)
2291 .action("b", 2)
2292 .on_press(3)
2293 .on_dismiss(4)
2294 .expires(std::time::Duration::from_secs(5), 5)
2295 .reply(
2296 Reply::new("p", "v", |text: String| text.len() as i32)
2297 .on_submit(6)
2298 .on_cancel(7),
2299 )
2300 .choices(
2301 Choices::many()
2302 .option("x", None, false, 8)
2303 .option("y", Some("why".into()), true, 9)
2304 .recommended()
2305 .on_send(10),
2306 )
2307 .step(1, 3, Some(11))
2308 .map(&|n: i32| n * 10);
2309 assert_eq!(notice.key(), 7);
2310 let parts = notice.into_parts();
2311 let actions: Vec<_> = parts.actions.into_iter().map(|(_, m)| m).collect();
2312 assert_eq!(actions, [10, 20]);
2313 assert_eq!(parts.on_press, Some(30));
2314 assert_eq!(parts.on_dismiss, Some(40));
2315 assert_eq!(parts.expires.map(|(_, m)| m), Some(50));
2316 let reply = parts.reply.unwrap();
2317 assert_eq!((reply.on_input)("abc".into()), 30);
2318 assert_eq!((reply.on_submit, reply.on_cancel), (Some(60), Some(70)));
2319 let choices = parts.choices.unwrap();
2320 let picks: Vec<_> = choices.options.iter().map(|o| o.on_pick).collect();
2321 assert_eq!(picks, [80, 90]);
2322 assert!(choices.multiple && choices.options[1].recommended && choices.options[1].picked);
2323 assert_eq!(choices.on_send, Some(100));
2324 let step = parts.step.unwrap();
2325 assert_eq!((step.index, step.count, step.on_back), (1, 3, Some(110)));
2326 }
2327
2328 #[test]
2329 fn pixels_must_be_whole() {
2330 assert!(Pixels::new(2, 2, vec![0; 16]).is_some());
2331 assert!(Pixels::new(2, 2, vec![0; 15]).is_none());
2332 assert!(Pixels::new(0, 0, Vec::new()).is_none());
2333 assert!(Pixels::new(u32::MAX, u32::MAX, Vec::new()).is_none());
2334 }
2335
2336 #[test]
2337 fn progress_stays_between_none_and_all() {
2338 let parts = Notice::<()>::new(1, "s").progress(1.5).into_parts();
2339 assert_eq!(parts.progress, Some(1.0));
2340 }
2341
2342 #[test]
2343 fn modifiers_only_touch_their_nodes() {
2344 let node: Node<i32> = label("a").trailing(label("b")).on_press(1).selected(true);
2345 assert!(matches!(node.into_kind(), Kind::Label { .. }));
2346 let node: Node<i32> = button(label("a"), 1).on_press(2).prominent();
2347 assert!(matches!(
2348 node.into_kind(),
2349 Kind::Button {
2350 on_press: 1,
2351 prominent: true,
2352 ..
2353 }
2354 ));
2355 }
2356
2357 #[test]
2358 fn a_header_takes_a_picture_and_a_subtitle() {
2359 let node = header("t")
2360 .subtitle("s")
2361 .leading(button(icon(Icon::Info), 1))
2362 .tone(Tone::Dim)
2363 .map(&|n: i32| n * 10);
2364 let Kind::Header {
2365 subtitle: Some(subtitle),
2366 leading: Some(leading),
2367 ..
2368 } = node.into_kind()
2369 else {
2370 panic!("header lost its picture or subtitle");
2371 };
2372 assert_eq!(subtitle, "s");
2373 let Kind::Button {
2374 content,
2375 on_press: 10,
2376 ..
2377 } = leading.into_kind()
2378 else {
2379 panic!("picture lost its message");
2380 };
2381 assert!(matches!(
2382 content.into_kind(),
2383 Kind::Icon {
2384 tone: Some(Tone::Dim),
2385 ..
2386 }
2387 ));
2388 }
2389
2390 #[test]
2391 fn timelines_map_and_tone_their_steps() {
2392 let node = timeline([
2393 step(Mark::You, label("a")),
2394 step(Mark::Failed, button(literal("b"), 1)).leading(icon(Icon::Check)),
2395 label("c"),
2396 ])
2397 .tone(Tone::Dim)
2398 .map(&|n: i32| n * 10);
2399 let Kind::Timeline(steps) = node.into_kind() else {
2400 panic!("timeline became something else");
2401 };
2402 let mut steps = steps.into_iter().map(Node::into_kind);
2403 let Some(Kind::Step {
2404 mark: Mark::You,
2405 leading: None,
2406 content,
2407 }) = steps.next()
2408 else {
2409 panic!("first step lost");
2410 };
2411 assert!(matches!(
2412 content.into_kind(),
2413 Kind::Label {
2414 tone: Some(Tone::Dim),
2415 ..
2416 }
2417 ));
2418 let Some(Kind::Step {
2419 mark: Mark::Failed,
2420 leading: Some(leading),
2421 content,
2422 }) = steps.next()
2423 else {
2424 panic!("second step lost its picture");
2425 };
2426 assert!(matches!(
2427 leading.into_kind(),
2428 Kind::Icon {
2429 tone: Some(Tone::Dim),
2430 ..
2431 }
2432 ));
2433 let Kind::Button {
2434 content,
2435 on_press: 10,
2436 ..
2437 } = content.into_kind()
2438 else {
2439 panic!("button lost its message");
2440 };
2441 assert!(matches!(
2442 content.into_kind(),
2443 Kind::Literal {
2444 tone: Some(Tone::Dim),
2445 ..
2446 }
2447 ));
2448 assert!(matches!(steps.next(), Some(Kind::Label { .. })));
2449 let node: Node<()> = diff("f", [(Change::Added, "x".to_string())]).leading(label("y"));
2450 assert!(matches!(node.into_kind(), Kind::Diff { lines, .. } if lines.len() == 1));
2451 }
2452
2453 #[test]
2454 fn tone_reaches_details_and_row_icons() {
2455 let node: Node<()> = column([
2456 detail("n", label("v")),
2457 list_item("t").leading(icon(Icon::Lock)),
2458 ])
2459 .tone(Tone::Good);
2460 let Kind::Column(children) = node.into_kind() else {
2461 unreachable!()
2462 };
2463 let mut children = children.into_iter().map(Node::into_kind);
2464 let Some(Kind::Detail { value, .. }) = children.next() else {
2465 unreachable!()
2466 };
2467 assert!(matches!(
2468 value.into_kind(),
2469 Kind::Label {
2470 tone: Some(Tone::Good),
2471 ..
2472 }
2473 ));
2474 let Some(Kind::ListItem {
2475 leading: Some(leading),
2476 ..
2477 }) = children.next()
2478 else {
2479 unreachable!()
2480 };
2481 assert!(matches!(
2482 leading.into_kind(),
2483 Kind::Icon {
2484 tone: Some(Tone::Good),
2485 ..
2486 }
2487 ));
2488 }
2489}