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