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 Page {
274 order: i64,
275 content: Box<Node<M>>,
276 },
277 Separator,
278 Empty,
279 ListItem {
280 title: String,
281 subtitle: Option<String>,
282 leading: Option<Box<Node<M>>>,
283 trailing: Option<Box<Node<M>>>,
284 selected: bool,
285 on_press: Option<M>,
286 on_secondary: Option<M>,
287 shortcut: Option<Binding>,
288 },
289 List(Vec<Node<M>>),
290 Section {
291 title: String,
292 trailing: Option<Box<Node<M>>>,
293 },
294 Header {
295 title: String,
296 subtitle: Option<String>,
297 on_back: Option<M>,
298 leading: Option<Box<Node<M>>>,
299 trailing: Option<Box<Node<M>>>,
300 },
301 Detail {
302 name: String,
303 value: Box<Node<M>>,
304 },
305 Fill,
306 Slider {
307 value: f32,
308 range: RangeInclusive<f32>,
309 step: f32,
310 on_change: Box<dyn Fn(f32) -> M>,
311 on_release: Option<M>,
312 },
313 Toggle {
314 on: bool,
315 on_toggle: M,
316 },
317 Meter {
318 fraction: f32,
319 limit: Option<f32>,
321 tone: Option<Tone>,
322 },
323 TextInput {
324 placeholder: String,
325 value: String,
326 on_input: Box<dyn Fn(String) -> M>,
327 on_submit: Option<M>,
328 secret: bool,
329 trailing: Option<Box<Node<M>>>,
330 tone: Option<Tone>,
331 },
332 Display(Box<Node<M>>),
333 Mark(Box<Node<M>>),
334 Counted {
335 content: Box<Node<M>>,
336 count: u32,
337 },
338 Badge {
339 text: String,
340 tone: Option<Tone>,
341 },
342 Keys {
343 shortcut: Shortcut,
344 tone: Option<Tone>,
345 },
346 Tabs(Vec<(String, Node<M>)>),
348 Markdown {
349 text: String,
350 tone: Option<Tone>,
351 },
352 TextArea {
353 placeholder: String,
354 value: String,
355 on_input: Box<dyn Fn(String) -> M>,
356 on_submit: Option<M>,
357 trailing: Option<Box<Node<M>>>,
358 },
359 Scroll {
360 content: Box<Node<M>>,
361 bottom: bool,
363 },
364 Literal {
365 text: String,
366 tone: Option<Tone>,
367 },
368 Divider {
369 text: String,
370 },
371 Timeline(Vec<Node<M>>),
372 Step {
373 mark: Mark,
374 leading: Option<Box<Node<M>>>,
376 content: Box<Node<M>>,
377 },
378 Diff {
379 title: String,
380 lines: Vec<(Change, String)>,
381 },
382 Bay {
384 name: String,
385 subject: String,
386 },
387}
388
389pub fn label<M>(text: impl Into<String>) -> Node<M> {
390 Node(Kind::Label {
391 text: text.into(),
392 tone: None,
393 })
394}
395
396pub fn heading<M>(text: impl Into<String>) -> Node<M> {
398 Node(Kind::Heading {
399 text: text.into(),
400 tone: None,
401 })
402}
403
404pub fn icon<M>(icon: Icon) -> Node<M> {
406 Node(Kind::Icon { icon, tone: None })
407}
408
409pub fn app_icon<M>(app_id: impl Into<String>) -> Node<M> {
413 Node(Kind::AppIcon {
414 app_id: app_id.into(),
415 tone: None,
416 })
417}
418
419pub fn image<M>(file: &'static [u8]) -> Node<M> {
424 Node(Kind::Image {
425 file,
426 tint: false,
427 tone: None,
428 })
429}
430
431pub fn symbol<M>(file: &'static [u8]) -> Node<M> {
434 Node(Kind::Image {
435 file,
436 tint: true,
437 tone: None,
438 })
439}
440
441pub fn sprite<M>(frames: &'static [&'static [u8]], order: &'static [u8]) -> Node<M> {
449 Node(Kind::Sprite {
450 frames,
451 order,
452 looping: false,
453 tone: None,
454 })
455}
456
457pub fn cover<M>(url: impl Into<String>, fallback: Node<M>) -> Node<M> {
462 Node(Kind::Cover {
463 url: url.into(),
464 fallback: Box::new(fallback),
465 large: false,
466 tone: None,
467 })
468}
469
470pub fn counter<M>(value: i64, range: RangeInclusive<i64>, unit: impl Into<String>) -> Node<M> {
474 Node(Kind::Counter {
475 value,
476 range,
477 unit: unit.into(),
478 tone: None,
479 })
480}
481
482pub fn alternate<M>(options: impl IntoIterator<Item = Node<M>>, current: usize) -> Node<M> {
487 Node(Kind::Alternate {
488 options: options.into_iter().collect(),
489 current,
490 hovered: None,
491 grows: false,
492 })
493}
494
495pub fn hover<M>(normal: Node<M>, hovered: Node<M>) -> Node<M> {
498 alternate([normal, hovered], 0).on_hover(1)
499}
500
501pub fn expand<M>(collapsed: Node<M>, expanded: Node<M>) -> Node<M> {
506 Node(Kind::Alternate {
507 options: vec![collapsed, expanded],
508 current: 0,
509 hovered: Some(1),
510 grows: true,
511 })
512}
513
514pub fn row<M>(children: impl IntoIterator<Item = Node<M>>) -> Node<M> {
516 Node(Kind::Row(children.into_iter().collect()))
517}
518
519pub fn column<M>(children: impl IntoIterator<Item = Node<M>>) -> Node<M> {
521 Node(Kind::Column(children.into_iter().collect()))
522}
523
524pub fn center<M>(content: Node<M>) -> Node<M> {
528 Node(Kind::Center(Box::new(content)))
529}
530
531pub fn grid<M>(columns: usize, cells: impl IntoIterator<Item = Node<M>>) -> Node<M> {
533 Node(Kind::Grid {
534 columns: columns.max(1),
535 cells: cells.into_iter().collect(),
536 })
537}
538
539pub fn button<M>(content: Node<M>, on_press: M) -> Node<M> {
540 Node(Kind::Button {
541 content: Box::new(content),
542 on_press,
543 on_secondary: None,
544 prominent: false,
545 shortcut: None,
546 })
547}
548
549pub fn switcher<M>(
553 options: impl IntoIterator<Item = (Node<M>, M)>,
554 current: Option<usize>,
555) -> Node<M> {
556 Node(Kind::Switcher {
557 options: options.into_iter().collect(),
558 current,
559 })
560}
561
562pub fn gulp<M>(content: Node<M>) -> Node<M> {
565 Node(Kind::Gulp(Box::new(content)))
566}
567
568pub fn separator<M>() -> Node<M> {
570 Node(Kind::Separator)
571}
572
573pub fn empty<M>() -> Node<M> {
575 Node(Kind::Empty)
576}
577
578pub fn list_item<M>(title: impl Into<String>) -> Node<M> {
584 Node(Kind::ListItem {
585 title: title.into(),
586 subtitle: None,
587 leading: None,
588 trailing: None,
589 selected: false,
590 on_press: None,
591 on_secondary: None,
592 shortcut: None,
593 })
594}
595
596pub fn list<M>(rows: impl IntoIterator<Item = Node<M>>) -> Node<M> {
599 Node(Kind::List(rows.into_iter().collect()))
600}
601
602pub fn section<M>(title: impl Into<String>) -> Node<M> {
605 Node(Kind::Section {
606 title: title.into(),
607 trailing: None,
608 })
609}
610
611pub fn header<M>(title: impl Into<String>) -> Node<M> {
616 Node(Kind::Header {
617 title: title.into(),
618 subtitle: None,
619 on_back: None,
620 leading: None,
621 trailing: None,
622 })
623}
624
625pub fn detail<M>(name: impl Into<String>, value: Node<M>) -> Node<M> {
627 Node(Kind::Detail {
628 name: name.into(),
629 value: Box::new(value),
630 })
631}
632
633pub fn fill<M>() -> Node<M> {
635 Node(Kind::Fill)
636}
637
638pub fn slider<M: 'static>(
641 value: f32,
642 range: RangeInclusive<f32>,
643 on_change: impl Fn(f32) -> M + 'static,
644) -> Node<M> {
645 Node(Kind::Slider {
646 value,
647 range,
648 step: 1.0,
649 on_change: Box::new(on_change),
650 on_release: None,
651 })
652}
653
654pub fn toggle<M>(on: bool, on_toggle: M) -> Node<M> {
656 Node(Kind::Toggle { on, on_toggle })
657}
658
659pub fn meter<M>(fraction: f32) -> Node<M> {
662 Node(Kind::Meter {
663 fraction,
664 limit: None,
665 tone: None,
666 })
667}
668
669pub fn text_input<M: 'static>(
674 placeholder: impl Into<String>,
675 value: impl Into<String>,
676 on_input: impl Fn(String) -> M + 'static,
677) -> Node<M> {
678 Node(Kind::TextInput {
679 placeholder: placeholder.into(),
680 value: value.into(),
681 on_input: Box::new(on_input),
682 on_submit: None,
683 secret: false,
684 trailing: None,
685 tone: None,
686 })
687}
688
689pub fn text_area<M: 'static>(
694 placeholder: impl Into<String>,
695 value: impl Into<String>,
696 on_input: impl Fn(String) -> M + 'static,
697) -> Node<M> {
698 Node(Kind::TextArea {
699 placeholder: placeholder.into(),
700 value: value.into(),
701 on_input: Box::new(on_input),
702 on_submit: None,
703 trailing: None,
704 })
705}
706
707pub fn markdown<M>(text: impl Into<String>) -> Node<M> {
712 Node(Kind::Markdown {
713 text: text.into(),
714 tone: None,
715 })
716}
717
718pub fn scroll<M>(content: Node<M>) -> Node<M> {
722 Node(Kind::Scroll {
723 content: Box::new(content),
724 bottom: false,
725 })
726}
727
728pub fn literal<M>(text: impl Into<String>) -> Node<M> {
731 Node(Kind::Literal {
732 text: text.into(),
733 tone: None,
734 })
735}
736
737pub fn divider<M>(text: impl Into<String>) -> Node<M> {
740 Node(Kind::Divider { text: text.into() })
741}
742
743pub fn timeline<M>(steps: impl IntoIterator<Item = Node<M>>) -> Node<M> {
747 Node(Kind::Timeline(steps.into_iter().collect()))
748}
749
750pub fn step<M>(mark: Mark, content: Node<M>) -> Node<M> {
754 Node(Kind::Step {
755 mark,
756 leading: None,
757 content: Box::new(content),
758 })
759}
760
761pub fn diff<M>(
764 title: impl Into<String>,
765 lines: impl IntoIterator<Item = (Change, String)>,
766) -> Node<M> {
767 Node(Kind::Diff {
768 title: title.into(),
769 lines: lines.into_iter().collect(),
770 })
771}
772
773pub fn display<M>(content: Node<M>) -> Node<M> {
775 Node(Kind::Display(Box::new(content)))
776}
777
778pub fn mark<M>(content: Node<M>) -> Node<M> {
780 Node(Kind::Mark(Box::new(content)))
781}
782
783pub fn counted<M>(content: Node<M>, count: u32) -> Node<M> {
787 Node(Kind::Counted {
788 content: Box::new(content),
789 count,
790 })
791}
792
793pub fn badge<M>(text: impl Into<String>) -> Node<M> {
795 Node(Kind::Badge {
796 text: text.into(),
797 tone: None,
798 })
799}
800
801pub fn keys<M>(shortcut: Shortcut) -> Node<M> {
804 Node(Kind::Keys {
805 shortcut,
806 tone: None,
807 })
808}
809
810pub fn page<M>(order: i64, content: Node<M>) -> Node<M> {
817 Node(Kind::Page {
818 order,
819 content: Box::new(content),
820 })
821}
822
823pub fn tabs<M>(pages: impl IntoIterator<Item = (impl Into<String>, Node<M>)>) -> Node<M> {
827 Node(Kind::Tabs(
828 pages
829 .into_iter()
830 .map(|(name, page)| (name.into(), page))
831 .collect(),
832 ))
833}
834
835pub fn bay<M>(name: impl Into<String>, subject: impl Into<String>) -> Node<M> {
842 Node(Kind::Bay {
843 name: name.into(),
844 subject: subject.into(),
845 })
846}
847
848impl<M> Node<M> {
849 pub fn tone(mut self, tone: Tone) -> Self {
851 self.set_tone(tone);
852 self
853 }
854
855 pub fn on_hover(mut self, index: usize) -> Self {
858 if let Kind::Alternate { hovered, .. } = &mut self.0 {
859 *hovered = Some(index);
860 }
861 self
862 }
863
864 pub fn on_secondary(mut self, message: M) -> Self {
867 if let Kind::Button { on_secondary, .. } | Kind::ListItem { on_secondary, .. } = &mut self.0
868 {
869 *on_secondary = Some(message);
870 }
871 self
872 }
873
874 pub fn prominent(mut self) -> Self {
877 if let Kind::Button { prominent, .. } = &mut self.0 {
878 *prominent = true;
879 }
880 self
881 }
882
883 pub fn on_press(mut self, message: M) -> Self {
885 if let Kind::ListItem { on_press, .. } = &mut self.0 {
886 *on_press = Some(message);
887 }
888 self
889 }
890
891 pub fn subtitle(mut self, text: impl Into<String>) -> Self {
894 if let Kind::ListItem { subtitle, .. } | Kind::Header { subtitle, .. } = &mut self.0 {
895 *subtitle = Some(text.into());
896 }
897 self
898 }
899
900 pub fn leading(mut self, node: Node<M>) -> Self {
903 if let Kind::ListItem { leading, .. }
904 | Kind::Header { leading, .. }
905 | Kind::Step { leading, .. } = &mut self.0
906 {
907 *leading = Some(Box::new(node));
908 }
909 self
910 }
911
912 pub fn trailing(mut self, node: Node<M>) -> Self {
915 if let Kind::ListItem { trailing, .. }
916 | Kind::Section { trailing, .. }
917 | Kind::Header { trailing, .. }
918 | Kind::TextInput { trailing, .. }
919 | Kind::TextArea { trailing, .. } = &mut self.0
920 {
921 *trailing = Some(Box::new(node));
922 }
923 self
924 }
925
926 pub fn shortcut(mut self, id: impl Into<String>, key: Shortcut) -> Self {
933 if let Kind::ListItem { shortcut, .. } | Kind::Button { shortcut, .. } = &mut self.0 {
934 *shortcut = Some(Binding {
935 id: id.into(),
936 key: Some(key),
937 });
938 }
939 self
940 }
941
942 pub fn selected(mut self, yes: bool) -> Self {
944 if let Kind::ListItem { selected, .. } = &mut self.0 {
945 *selected = yes;
946 }
947 self
948 }
949
950 pub fn on_back(mut self, message: M) -> Self {
952 if let Kind::Header { on_back, .. } = &mut self.0 {
953 *on_back = Some(message);
954 }
955 self
956 }
957
958 pub fn step(mut self, by: f32) -> Self {
960 if let Kind::Slider { step, .. } = &mut self.0 {
961 *step = by;
962 }
963 self
964 }
965
966 pub fn on_release(mut self, message: M) -> Self {
968 if let Kind::Slider { on_release, .. } = &mut self.0 {
969 *on_release = Some(message);
970 }
971 self
972 }
973
974 pub fn on_submit(mut self, message: M) -> Self {
976 if let Kind::TextInput { on_submit, .. } | Kind::TextArea { on_submit, .. } = &mut self.0 {
977 *on_submit = Some(message);
978 }
979 self
980 }
981
982 pub fn secret(mut self, yes: bool) -> Self {
984 if let Kind::TextInput { secret, .. } = &mut self.0 {
985 *secret = yes;
986 }
987 self
988 }
989
990 pub fn looping(mut self) -> Self {
994 if let Kind::Sprite { looping, .. } = &mut self.0 {
995 *looping = true;
996 }
997 self
998 }
999
1000 pub fn large(mut self) -> Self {
1003 if let Kind::Cover { large, .. } = &mut self.0 {
1004 *large = true;
1005 }
1006 self
1007 }
1008
1009 pub fn bottom(mut self) -> Self {
1012 if let Kind::Scroll { bottom, .. } = &mut self.0 {
1013 *bottom = true;
1014 }
1015 self
1016 }
1017
1018 pub fn limit(mut self, at: f32) -> Self {
1020 if let Kind::Meter { limit, .. } = &mut self.0 {
1021 *limit = Some(at);
1022 }
1023 self
1024 }
1025
1026 fn set_tone(&mut self, new: Tone) {
1027 match &mut self.0 {
1028 Kind::Label { tone, .. }
1029 | Kind::Heading { tone, .. }
1030 | Kind::Icon { tone, .. }
1031 | Kind::AppIcon { tone, .. }
1032 | Kind::Image { tone, .. }
1033 | Kind::Sprite { tone, .. }
1034 | Kind::Counter { tone, .. }
1035 | Kind::Meter { tone, .. }
1036 | Kind::TextInput { tone, .. }
1037 | Kind::Markdown { tone, .. }
1038 | Kind::Literal { tone, .. }
1039 | Kind::Badge { tone, .. }
1040 | Kind::Keys { tone, .. } => {
1041 tone.get_or_insert(new);
1042 }
1043 Kind::Cover { tone, fallback, .. } => {
1044 tone.get_or_insert(new);
1045 fallback.set_tone(new);
1046 }
1047 Kind::Alternate {
1048 options: children, ..
1049 }
1050 | Kind::Row(children)
1051 | Kind::Column(children)
1052 | Kind::Grid {
1053 cells: children, ..
1054 }
1055 | Kind::List(children)
1056 | Kind::Timeline(children) => {
1057 children.iter_mut().for_each(|child| child.set_tone(new));
1058 }
1059 Kind::Step {
1060 leading, content, ..
1061 } => {
1062 leading.iter_mut().for_each(|child| child.set_tone(new));
1063 content.set_tone(new);
1064 }
1065 Kind::Button { content, .. }
1066 | Kind::Gulp(content)
1067 | Kind::Center(content)
1068 | Kind::Page { content, .. }
1069 | Kind::Display(content)
1070 | Kind::Mark(content)
1071 | Kind::Counted { content, .. }
1072 | Kind::Scroll { content, .. }
1073 | Kind::Detail { value: content, .. } => content.set_tone(new),
1074 Kind::ListItem {
1075 leading, trailing, ..
1076 }
1077 | Kind::Header {
1078 leading, trailing, ..
1079 } => {
1080 leading
1081 .iter_mut()
1082 .chain(trailing.iter_mut())
1083 .for_each(|child| child.set_tone(new));
1084 }
1085 Kind::Tabs(pages) => {
1086 for (_, page) in pages {
1087 page.set_tone(new);
1088 }
1089 }
1090 Kind::Switcher { options, .. } => {
1091 options
1092 .iter_mut()
1093 .for_each(|(child, _)| child.set_tone(new));
1094 }
1095 Kind::Separator
1096 | Kind::Divider { .. }
1097 | Kind::Diff { .. }
1098 | Kind::Bay { .. }
1099 | Kind::Empty
1100 | Kind::Section { .. }
1101 | Kind::Fill
1102 | Kind::Slider { .. }
1103 | Kind::TextArea { .. }
1104 | Kind::Toggle { .. } => {}
1105 }
1106 }
1107
1108 pub fn map<N: 'static>(self, f: &(impl Fn(M) -> N + Clone + 'static)) -> Node<N>
1112 where
1113 M: 'static,
1114 {
1115 let map_all = |nodes: Vec<Node<M>>| nodes.into_iter().map(|node| node.map(f)).collect();
1116 let map_box = |node: Option<Box<Node<M>>>| node.map(|node| Box::new(node.map(f)));
1117
1118 Node(match self.0 {
1119 Kind::Label { text, tone } => Kind::Label { text, tone },
1120 Kind::Heading { text, tone } => Kind::Heading { text, tone },
1121 Kind::Icon { icon, tone } => Kind::Icon { icon, tone },
1122 Kind::AppIcon { app_id, tone } => Kind::AppIcon { app_id, tone },
1123 Kind::Image { file, tint, tone } => Kind::Image { file, tint, tone },
1124 Kind::Sprite {
1125 frames,
1126 order,
1127 looping,
1128 tone,
1129 } => Kind::Sprite {
1130 frames,
1131 order,
1132 looping,
1133 tone,
1134 },
1135 Kind::Cover {
1136 url,
1137 fallback,
1138 large,
1139 tone,
1140 } => Kind::Cover {
1141 url,
1142 fallback: Box::new(fallback.map(f)),
1143 large,
1144 tone,
1145 },
1146 Kind::Counter {
1147 value,
1148 range,
1149 unit,
1150 tone,
1151 } => Kind::Counter {
1152 value,
1153 range,
1154 unit,
1155 tone,
1156 },
1157 Kind::Alternate {
1158 options,
1159 current,
1160 hovered,
1161 grows,
1162 } => Kind::Alternate {
1163 options: map_all(options),
1164 current,
1165 hovered,
1166 grows,
1167 },
1168 Kind::Row(children) => Kind::Row(map_all(children)),
1169 Kind::Column(children) => Kind::Column(map_all(children)),
1170 Kind::Grid { columns, cells } => Kind::Grid {
1171 columns,
1172 cells: map_all(cells),
1173 },
1174 Kind::Button {
1175 content,
1176 on_press,
1177 on_secondary,
1178 prominent,
1179 shortcut,
1180 } => Kind::Button {
1181 content: Box::new(content.map(f)),
1182 on_press: f(on_press),
1183 on_secondary: on_secondary.map(f),
1184 prominent,
1185 shortcut,
1186 },
1187 Kind::Switcher { options, current } => Kind::Switcher {
1188 options: options
1189 .into_iter()
1190 .map(|(node, message)| (node.map(f), f(message)))
1191 .collect(),
1192 current,
1193 },
1194 Kind::Gulp(content) => Kind::Gulp(Box::new(content.map(f))),
1195 Kind::Center(content) => Kind::Center(Box::new(content.map(f))),
1196 Kind::Page { order, content } => Kind::Page {
1197 order,
1198 content: Box::new(content.map(f)),
1199 },
1200 Kind::Separator => Kind::Separator,
1201 Kind::Empty => Kind::Empty,
1202 Kind::ListItem {
1203 title,
1204 subtitle,
1205 leading,
1206 trailing,
1207 selected,
1208 on_press,
1209 on_secondary,
1210 shortcut,
1211 } => Kind::ListItem {
1212 title,
1213 subtitle,
1214 leading: map_box(leading),
1215 trailing: map_box(trailing),
1216 selected,
1217 on_press: on_press.map(f),
1218 on_secondary: on_secondary.map(f),
1219 shortcut,
1220 },
1221 Kind::List(rows) => Kind::List(map_all(rows)),
1222 Kind::Section { title, trailing } => Kind::Section {
1223 title,
1224 trailing: map_box(trailing),
1225 },
1226 Kind::Header {
1227 title,
1228 subtitle,
1229 on_back,
1230 leading,
1231 trailing,
1232 } => Kind::Header {
1233 title,
1234 subtitle,
1235 on_back: on_back.map(f),
1236 leading: map_box(leading),
1237 trailing: map_box(trailing),
1238 },
1239 Kind::Detail { name, value } => Kind::Detail {
1240 name,
1241 value: Box::new(value.map(f)),
1242 },
1243 Kind::Fill => Kind::Fill,
1244 Kind::Slider {
1245 value,
1246 range,
1247 step,
1248 on_change,
1249 on_release,
1250 } => {
1251 let g = f.clone();
1252 Kind::Slider {
1253 value,
1254 range,
1255 step,
1256 on_change: Box::new(move |value| g(on_change(value))),
1257 on_release: on_release.map(f),
1258 }
1259 }
1260 Kind::Toggle { on, on_toggle } => Kind::Toggle {
1261 on,
1262 on_toggle: f(on_toggle),
1263 },
1264 Kind::Meter {
1265 fraction,
1266 limit,
1267 tone,
1268 } => Kind::Meter {
1269 fraction,
1270 limit,
1271 tone,
1272 },
1273 Kind::TextInput {
1274 placeholder,
1275 value,
1276 on_input,
1277 on_submit,
1278 secret,
1279 trailing,
1280 tone,
1281 } => {
1282 let g = f.clone();
1283 Kind::TextInput {
1284 placeholder,
1285 value,
1286 on_input: Box::new(move |text| g(on_input(text))),
1287 on_submit: on_submit.map(f),
1288 secret,
1289 trailing: map_box(trailing),
1290 tone,
1291 }
1292 }
1293 Kind::Display(content) => Kind::Display(Box::new(content.map(f))),
1294 Kind::Mark(content) => Kind::Mark(Box::new(content.map(f))),
1295 Kind::Counted { content, count } => Kind::Counted {
1296 content: Box::new(content.map(f)),
1297 count,
1298 },
1299 Kind::Badge { text, tone } => Kind::Badge { text, tone },
1300 Kind::Keys { shortcut, tone } => Kind::Keys { shortcut, tone },
1301 Kind::Tabs(pages) => Kind::Tabs(
1302 pages
1303 .into_iter()
1304 .map(|(name, page)| (name, page.map(f)))
1305 .collect(),
1306 ),
1307 Kind::Markdown { text, tone } => Kind::Markdown { text, tone },
1308 Kind::TextArea {
1309 placeholder,
1310 value,
1311 on_input,
1312 on_submit,
1313 trailing,
1314 } => {
1315 let g = f.clone();
1316 Kind::TextArea {
1317 placeholder,
1318 value,
1319 on_input: Box::new(move |text| g(on_input(text))),
1320 on_submit: on_submit.map(f),
1321 trailing: map_box(trailing),
1322 }
1323 }
1324 Kind::Scroll { content, bottom } => Kind::Scroll {
1325 content: Box::new(content.map(f)),
1326 bottom,
1327 },
1328 Kind::Literal { text, tone } => Kind::Literal { text, tone },
1329 Kind::Divider { text } => Kind::Divider { text },
1330 Kind::Timeline(steps) => Kind::Timeline(map_all(steps)),
1331 Kind::Step {
1332 mark,
1333 leading,
1334 content,
1335 } => Kind::Step {
1336 mark,
1337 leading: map_box(leading),
1338 content: Box::new(content.map(f)),
1339 },
1340 Kind::Diff { title, lines } => Kind::Diff { title, lines },
1341 Kind::Bay { name, subject } => Kind::Bay { name, subject },
1342 })
1343 }
1344
1345 #[doc(hidden)]
1349 pub fn fill_bays(&mut self, fill: &mut dyn FnMut(&str, &str) -> Vec<Node<M>>) {
1350 if let Kind::Bay { name, subject } = &self.0 {
1351 let mut nodes = fill(name, subject);
1352 *self = match nodes.len() {
1353 0 => empty(),
1354 1 => nodes.remove(0),
1355 _ => column(nodes),
1356 };
1357 return;
1358 }
1359 let one = |node: &mut Node<M>, fill: &mut dyn FnMut(&str, &str) -> Vec<Node<M>>| {
1360 node.fill_bays(fill)
1361 };
1362 let all = |nodes: &mut Vec<Node<M>>, fill: &mut dyn FnMut(&str, &str) -> Vec<Node<M>>| {
1363 for mut node in std::mem::take(nodes) {
1364 match &node.0 {
1365 Kind::Bay { name, subject } => nodes.extend(fill(name, subject)),
1366 _ => {
1367 node.fill_bays(fill);
1368 nodes.push(node);
1369 }
1370 }
1371 }
1372 };
1373 match &mut self.0 {
1374 Kind::Alternate {
1375 options: children, ..
1376 }
1377 | Kind::Row(children)
1378 | Kind::Column(children)
1379 | Kind::Grid {
1380 cells: children, ..
1381 }
1382 | Kind::List(children)
1383 | Kind::Timeline(children) => all(children, fill),
1384 Kind::Step {
1385 leading, content, ..
1386 } => {
1387 leading.iter_mut().for_each(|child| one(child, fill));
1388 one(content, fill);
1389 }
1390 Kind::Cover {
1391 fallback: content, ..
1392 }
1393 | Kind::Button { content, .. }
1394 | Kind::Gulp(content)
1395 | Kind::Center(content)
1396 | Kind::Page { content, .. }
1397 | Kind::Display(content)
1398 | Kind::Mark(content)
1399 | Kind::Counted { content, .. }
1400 | Kind::Scroll { content, .. }
1401 | Kind::Detail { value: content, .. } => one(content, fill),
1402 Kind::ListItem {
1403 leading, trailing, ..
1404 }
1405 | Kind::Header {
1406 leading, trailing, ..
1407 } => leading
1408 .iter_mut()
1409 .chain(trailing.iter_mut())
1410 .for_each(|child| one(child, fill)),
1411 Kind::Section { trailing, .. }
1412 | Kind::TextInput { trailing, .. }
1413 | Kind::TextArea { trailing, .. } => {
1414 trailing.iter_mut().for_each(|child| one(child, fill))
1415 }
1416 Kind::Tabs(pages) => pages.iter_mut().for_each(|(_, page)| one(page, fill)),
1417 Kind::Switcher { options, .. } => {
1418 options.iter_mut().for_each(|(child, _)| one(child, fill))
1419 }
1420 Kind::Label { .. }
1421 | Kind::Heading { .. }
1422 | Kind::Icon { .. }
1423 | Kind::AppIcon { .. }
1424 | Kind::Image { .. }
1425 | Kind::Sprite { .. }
1426 | Kind::Counter { .. }
1427 | Kind::Separator
1428 | Kind::Empty
1429 | Kind::Fill
1430 | Kind::Slider { .. }
1431 | Kind::Toggle { .. }
1432 | Kind::Meter { .. }
1433 | Kind::Badge { .. }
1434 | Kind::Keys { .. }
1435 | Kind::Markdown { .. }
1436 | Kind::Literal { .. }
1437 | Kind::Divider { .. }
1438 | Kind::Diff { .. }
1439 | Kind::Bay { .. } => {}
1440 }
1441 }
1442
1443 #[doc(hidden)]
1444 pub fn into_kind(self) -> Kind<M> {
1445 self.0
1446 }
1447
1448 #[doc(hidden)]
1449 pub fn kind(&self) -> &Kind<M> {
1450 &self.0
1451 }
1452
1453 #[doc(hidden)]
1454 pub fn kind_mut(&mut self) -> &mut Kind<M> {
1455 &mut self.0
1456 }
1457}
1458
1459pub struct Item<M> {
1461 content: Option<Node<M>>,
1462 on_press: Option<M>,
1463 on_secondary: Option<M>,
1464 on_scroll: Option<(M, M)>,
1465}
1466
1467impl<M> Item<M> {
1468 pub fn new(content: Node<M>) -> Self {
1469 Self {
1470 content: Some(content),
1471 on_press: None,
1472 on_secondary: None,
1473 on_scroll: None,
1474 }
1475 }
1476
1477 pub fn hidden() -> Self {
1479 Self {
1480 content: None,
1481 on_press: None,
1482 on_secondary: None,
1483 on_scroll: None,
1484 }
1485 }
1486
1487 pub fn on_press(mut self, message: M) -> Self {
1489 self.on_press = Some(message);
1490 self
1491 }
1492
1493 pub fn on_secondary(mut self, message: M) -> Self {
1495 self.on_secondary = Some(message);
1496 self
1497 }
1498
1499 pub fn on_scroll(mut self, up: M, down: M) -> Self {
1502 self.on_scroll = Some((up, down));
1503 self
1504 }
1505
1506 pub fn map<N: 'static>(self, f: &(impl Fn(M) -> N + Clone + 'static)) -> Item<N>
1507 where
1508 M: 'static,
1509 {
1510 Item {
1511 content: self.content.map(|content| content.map(f)),
1512 on_press: self.on_press.map(f),
1513 on_secondary: self.on_secondary.map(f),
1514 on_scroll: self.on_scroll.map(|(up, down)| (f(up), f(down))),
1515 }
1516 }
1517
1518 #[doc(hidden)]
1519 pub fn into_parts(self) -> ItemParts<M> {
1520 ItemParts {
1521 content: self.content,
1522 on_press: self.on_press,
1523 on_secondary: self.on_secondary,
1524 on_scroll: self.on_scroll,
1525 }
1526 }
1527}
1528
1529#[doc(hidden)]
1530pub struct ItemParts<M> {
1531 pub content: Option<Node<M>>,
1532 pub on_press: Option<M>,
1533 pub on_secondary: Option<M>,
1534 pub on_scroll: Option<(M, M)>,
1536}
1537
1538pub struct Panel<M> {
1541 content: Node<M>,
1542 on_open: Option<M>,
1543 on_close: Option<M>,
1544 wide: bool,
1545}
1546
1547impl<M> Panel<M> {
1548 pub fn new(content: Node<M>) -> Self {
1549 Self {
1550 content,
1551 on_open: None,
1552 on_close: None,
1553 wide: false,
1554 }
1555 }
1556
1557 pub fn wide(mut self) -> Self {
1560 self.wide = true;
1561 self
1562 }
1563
1564 #[doc(hidden)]
1565 pub fn is_wide(&self) -> bool {
1566 self.wide
1567 }
1568
1569 pub fn on_open(mut self, message: M) -> Self {
1571 self.on_open = Some(message);
1572 self
1573 }
1574
1575 pub fn on_close(mut self, message: M) -> Self {
1577 self.on_close = Some(message);
1578 self
1579 }
1580
1581 pub fn map<N: 'static>(self, f: &(impl Fn(M) -> N + Clone + 'static)) -> Panel<N>
1582 where
1583 M: 'static,
1584 {
1585 Panel {
1586 content: self.content.map(f),
1587 on_open: self.on_open.map(f),
1588 on_close: self.on_close.map(f),
1589 wide: self.wide,
1590 }
1591 }
1592
1593 #[doc(hidden)]
1594 pub fn into_node(self) -> Node<M> {
1595 self.content
1596 }
1597
1598 #[doc(hidden)]
1599 pub fn on_open_message(self) -> Option<M> {
1600 self.on_open
1601 }
1602
1603 #[doc(hidden)]
1604 pub fn on_close_message(self) -> Option<M> {
1605 self.on_close
1606 }
1607}
1608
1609#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1611pub struct Bay<'a> {
1612 name: &'a str,
1613 subject: &'a str,
1614}
1615
1616impl<'a> Bay<'a> {
1617 #[doc(hidden)]
1618 pub fn new(name: &'a str, subject: &'a str) -> Self {
1619 Self { name, subject }
1620 }
1621
1622 pub fn name(&self) -> &'a str {
1624 self.name
1625 }
1626
1627 pub fn subject(&self) -> &'a str {
1629 self.subject
1630 }
1631}
1632
1633pub struct Part<M> {
1636 content: Node<M>,
1637 on_open: Option<M>,
1638 on_close: Option<M>,
1639}
1640
1641impl<M> Part<M> {
1642 pub fn new(content: Node<M>) -> Self {
1643 Self {
1644 content,
1645 on_open: None,
1646 on_close: None,
1647 }
1648 }
1649
1650 pub fn on_open(mut self, message: M) -> Self {
1652 self.on_open = Some(message);
1653 self
1654 }
1655
1656 pub fn on_close(mut self, message: M) -> Self {
1658 self.on_close = Some(message);
1659 self
1660 }
1661
1662 pub fn map<N: 'static>(self, f: &(impl Fn(M) -> N + Clone + 'static)) -> Part<N>
1663 where
1664 M: 'static,
1665 {
1666 Part {
1667 content: self.content.map(f),
1668 on_open: self.on_open.map(f),
1669 on_close: self.on_close.map(f),
1670 }
1671 }
1672
1673 #[doc(hidden)]
1674 pub fn into_parts(self) -> (Node<M>, Option<M>, Option<M>) {
1675 (self.content, self.on_open, self.on_close)
1676 }
1677}
1678
1679impl<M> From<Panel<M>> for Part<M> {
1682 fn from(panel: Panel<M>) -> Self {
1683 Self {
1684 content: panel.content,
1685 on_open: panel.on_open,
1686 on_close: panel.on_close,
1687 }
1688 }
1689}
1690
1691#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1693pub enum Urgency {
1694 Low,
1696 #[default]
1697 Normal,
1698 Critical,
1700}
1701
1702pub struct Notice<M> {
1709 key: u64,
1710 summary: String,
1711 app: Option<String>,
1712 app_icon: Option<String>,
1713 icon: Option<Icon>,
1714 image: Option<String>,
1715 pixels: Option<Pixels>,
1716 body: Option<String>,
1717 markdown: bool,
1718 urgency: Urgency,
1719 time: Option<SystemTime>,
1720 thread: Option<String>,
1721 thread_name: Option<String>,
1722 progress: Option<f32>,
1723 actions: Vec<(String, M)>,
1724 choices: Option<ChoicesParts<M>>,
1725 step: Option<StepParts<M>>,
1726 on_press: Option<M>,
1727 on_dismiss: Option<M>,
1728 expires: Option<(std::time::Duration, M)>,
1729 reply: Option<Reply<M>>,
1730}
1731
1732#[derive(Clone)]
1738pub struct Pixels {
1739 width: u32,
1740 height: u32,
1741 rgba: Arc<[u8]>,
1742}
1743
1744impl Pixels {
1745 pub fn new(width: u32, height: u32, rgba: impl Into<Arc<[u8]>>) -> Option<Self> {
1747 let rgba = rgba.into();
1748 let expected = (width as usize)
1749 .checked_mul(height as usize)?
1750 .checked_mul(4)?;
1751 (width > 0 && height > 0 && rgba.len() == expected).then_some(Self {
1752 width,
1753 height,
1754 rgba,
1755 })
1756 }
1757
1758 pub fn width(&self) -> u32 {
1759 self.width
1760 }
1761
1762 pub fn height(&self) -> u32 {
1763 self.height
1764 }
1765
1766 pub fn rgba(&self) -> &Arc<[u8]> {
1767 &self.rgba
1768 }
1769}
1770
1771impl std::fmt::Debug for Pixels {
1772 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1773 write!(f, "Pixels({}×{})", self.width, self.height)
1774 }
1775}
1776
1777pub struct Choices<M> {
1780 parts: ChoicesParts<M>,
1781}
1782
1783impl<M> Choices<M> {
1784 pub fn one() -> Self {
1786 Self::new(false)
1787 }
1788
1789 pub fn many() -> Self {
1792 Self::new(true)
1793 }
1794
1795 fn new(multiple: bool) -> Self {
1796 Self {
1797 parts: ChoicesParts {
1798 multiple,
1799 options: Vec::new(),
1800 on_send: None,
1801 },
1802 }
1803 }
1804
1805 pub fn option(
1807 mut self,
1808 label: impl Into<String>,
1809 description: Option<String>,
1810 picked: bool,
1811 on_pick: M,
1812 ) -> Self {
1813 self.parts.options.push(ChoiceParts {
1814 label: label.into(),
1815 description: description.filter(|d| !d.is_empty()),
1816 recommended: false,
1817 picked,
1818 on_pick,
1819 });
1820 self
1821 }
1822
1823 pub fn recommended(mut self) -> Self {
1825 if let Some(option) = self.parts.options.last_mut() {
1826 option.recommended = true;
1827 }
1828 self
1829 }
1830
1831 pub fn on_send(mut self, message: M) -> Self {
1833 self.parts.on_send = Some(message);
1834 self
1835 }
1836
1837 fn map<N>(self, f: &impl Fn(M) -> N) -> Choices<N> {
1838 let parts = self.parts;
1839 Choices {
1840 parts: ChoicesParts {
1841 multiple: parts.multiple,
1842 options: parts
1843 .options
1844 .into_iter()
1845 .map(|option| ChoiceParts {
1846 label: option.label,
1847 description: option.description,
1848 recommended: option.recommended,
1849 picked: option.picked,
1850 on_pick: f(option.on_pick),
1851 })
1852 .collect(),
1853 on_send: parts.on_send.map(f),
1854 },
1855 }
1856 }
1857}
1858
1859#[doc(hidden)]
1860#[derive(Clone)]
1861pub struct ChoicesParts<M> {
1862 pub multiple: bool,
1863 pub options: Vec<ChoiceParts<M>>,
1864 pub on_send: Option<M>,
1865}
1866
1867#[doc(hidden)]
1868#[derive(Clone)]
1869pub struct ChoiceParts<M> {
1870 pub label: String,
1871 pub description: Option<String>,
1872 pub recommended: bool,
1873 pub picked: bool,
1874 pub on_pick: M,
1875}
1876
1877#[doc(hidden)]
1878#[derive(Clone)]
1879pub struct StepParts<M> {
1880 pub index: usize,
1882 pub count: usize,
1883 pub on_back: Option<M>,
1884}
1885
1886pub struct Reply<M> {
1893 placeholder: String,
1894 value: String,
1895 on_input: std::rc::Rc<dyn Fn(String) -> M>,
1896 on_submit: Option<M>,
1897 on_cancel: Option<M>,
1898 sent: bool,
1899}
1900
1901impl<M> Reply<M> {
1902 pub fn new(
1904 placeholder: impl Into<String>,
1905 value: impl Into<String>,
1906 on_input: impl Fn(String) -> M + 'static,
1907 ) -> Self {
1908 Self {
1909 placeholder: placeholder.into(),
1910 value: value.into(),
1911 on_input: std::rc::Rc::new(on_input),
1912 on_submit: None,
1913 on_cancel: None,
1914 sent: false,
1915 }
1916 }
1917
1918 pub fn on_submit(mut self, message: M) -> Self {
1920 self.on_submit = Some(message);
1921 self
1922 }
1923
1924 pub fn on_cancel(mut self, message: M) -> Self {
1926 self.on_cancel = Some(message);
1927 self
1928 }
1929
1930 pub fn sent(mut self, sent: bool) -> Self {
1933 self.sent = sent;
1934 self
1935 }
1936
1937 pub fn map<N: 'static>(self, f: &(impl Fn(M) -> N + Clone + 'static)) -> Reply<N>
1938 where
1939 M: 'static,
1940 {
1941 let g = f.clone();
1942 let on_input = self.on_input;
1943 Reply {
1944 placeholder: self.placeholder,
1945 value: self.value,
1946 on_input: std::rc::Rc::new(move |text| g(on_input(text))),
1947 on_submit: self.on_submit.map(f),
1948 on_cancel: self.on_cancel.map(f),
1949 sent: self.sent,
1950 }
1951 }
1952
1953 #[doc(hidden)]
1954 pub fn into_parts(self) -> ReplyParts<M> {
1955 ReplyParts {
1956 placeholder: self.placeholder,
1957 value: self.value,
1958 on_input: self.on_input,
1959 on_submit: self.on_submit,
1960 on_cancel: self.on_cancel,
1961 sent: self.sent,
1962 }
1963 }
1964}
1965
1966#[doc(hidden)]
1967#[derive(Clone)]
1968pub struct ReplyParts<M> {
1969 pub placeholder: String,
1970 pub value: String,
1971 pub on_input: std::rc::Rc<dyn Fn(String) -> M>,
1972 pub on_submit: Option<M>,
1973 pub on_cancel: Option<M>,
1974 pub sent: bool,
1975}
1976
1977impl<M> Notice<M> {
1978 pub fn new(key: u64, summary: impl Into<String>) -> Self {
1979 Self {
1980 key,
1981 summary: summary.into(),
1982 app: None,
1983 app_icon: None,
1984 icon: None,
1985 image: None,
1986 pixels: None,
1987 body: None,
1988 markdown: false,
1989 urgency: Urgency::Normal,
1990 time: None,
1991 thread: None,
1992 thread_name: None,
1993 progress: None,
1994 actions: Vec::new(),
1995 choices: None,
1996 step: None,
1997 on_press: None,
1998 on_dismiss: None,
1999 expires: None,
2000 reply: None,
2001 }
2002 }
2003
2004 pub fn app(mut self, name: impl Into<String>) -> Self {
2006 self.app = Some(name.into());
2007 self
2008 }
2009
2010 pub fn app_icon(mut self, app_id: impl Into<String>) -> Self {
2012 self.app_icon = Some(app_id.into());
2013 self
2014 }
2015
2016 pub fn icon(mut self, icon: Icon) -> Self {
2020 self.icon = Some(icon);
2021 self
2022 }
2023
2024 pub fn image(mut self, url: impl Into<String>) -> Self {
2027 self.image = Some(url.into());
2028 self
2029 }
2030
2031 pub fn pixels(mut self, pixels: Pixels) -> Self {
2034 self.pixels = Some(pixels);
2035 self
2036 }
2037
2038 pub fn time(mut self, time: SystemTime) -> Self {
2040 self.time = Some(time);
2041 self
2042 }
2043
2044 pub fn thread(mut self, key: impl Into<String>) -> Self {
2047 self.thread = Some(key.into());
2048 self
2049 }
2050
2051 pub fn thread_name(mut self, name: impl Into<String>) -> Self {
2053 self.thread_name = Some(name.into());
2054 self
2055 }
2056
2057 pub fn progress(mut self, fraction: f32) -> Self {
2059 self.progress = Some(fraction.clamp(0.0, 1.0));
2060 self
2061 }
2062
2063 pub fn choices(mut self, choices: Choices<M>) -> Self {
2065 self.choices = Some(choices.parts);
2066 self
2067 }
2068
2069 pub fn step(mut self, index: usize, count: usize, on_back: Option<M>) -> Self {
2073 self.step = Some(StepParts {
2074 index,
2075 count,
2076 on_back,
2077 });
2078 self
2079 }
2080
2081 pub fn body(mut self, text: impl Into<String>) -> Self {
2083 self.body = Some(text.into());
2084 self
2085 }
2086
2087 pub fn markdown(mut self) -> Self {
2089 self.markdown = true;
2090 self
2091 }
2092
2093 pub fn urgency(mut self, urgency: Urgency) -> Self {
2094 self.urgency = urgency;
2095 self
2096 }
2097
2098 pub fn action(mut self, label: impl Into<String>, message: M) -> Self {
2100 self.actions.push((label.into(), message));
2101 self
2102 }
2103
2104 pub fn on_press(mut self, message: M) -> Self {
2106 self.on_press = Some(message);
2107 self
2108 }
2109
2110 pub fn on_dismiss(mut self, message: M) -> Self {
2112 self.on_dismiss = Some(message);
2113 self
2114 }
2115
2116 pub fn expires(mut self, after: std::time::Duration, message: M) -> Self {
2120 self.expires = Some((after, message));
2121 self
2122 }
2123
2124 pub fn reply(mut self, reply: Reply<M>) -> Self {
2126 self.reply = Some(reply);
2127 self
2128 }
2129
2130 pub fn map<N: 'static>(self, f: &(impl Fn(M) -> N + Clone + 'static)) -> Notice<N>
2131 where
2132 M: 'static,
2133 {
2134 Notice {
2135 key: self.key,
2136 summary: self.summary,
2137 app: self.app,
2138 app_icon: self.app_icon,
2139 icon: self.icon,
2140 image: self.image,
2141 pixels: self.pixels,
2142 body: self.body,
2143 markdown: self.markdown,
2144 urgency: self.urgency,
2145 time: self.time,
2146 thread: self.thread,
2147 thread_name: self.thread_name,
2148 progress: self.progress,
2149 actions: self
2150 .actions
2151 .into_iter()
2152 .map(|(label, message)| (label, f(message)))
2153 .collect(),
2154 choices: self.choices.map(|parts| Choices { parts }.map(f).parts),
2155 step: self.step.map(|step| StepParts {
2156 index: step.index,
2157 count: step.count,
2158 on_back: step.on_back.map(f),
2159 }),
2160 on_press: self.on_press.map(f),
2161 on_dismiss: self.on_dismiss.map(f),
2162 expires: self.expires.map(|(after, message)| (after, f(message))),
2163 reply: self.reply.map(|reply| reply.map(f)),
2164 }
2165 }
2166
2167 pub fn key(&self) -> u64 {
2168 self.key
2169 }
2170
2171 #[doc(hidden)]
2172 pub fn into_parts(self) -> NoticeParts<M> {
2173 NoticeParts {
2174 key: self.key,
2175 summary: self.summary,
2176 app: self.app,
2177 app_icon: self.app_icon,
2178 icon: self.icon,
2179 image: self.image,
2180 pixels: self.pixels,
2181 body: self.body,
2182 markdown: self.markdown,
2183 urgency: self.urgency,
2184 time: self.time,
2185 thread: self.thread,
2186 thread_name: self.thread_name,
2187 progress: self.progress,
2188 actions: self.actions,
2189 choices: self.choices,
2190 step: self.step,
2191 on_press: self.on_press,
2192 on_dismiss: self.on_dismiss,
2193 expires: self.expires,
2194 reply: self.reply.map(Reply::into_parts),
2195 }
2196 }
2197}
2198
2199#[doc(hidden)]
2200#[derive(Clone)]
2201pub struct NoticeParts<M> {
2202 pub key: u64,
2203 pub summary: String,
2204 pub app: Option<String>,
2205 pub app_icon: Option<String>,
2206 pub icon: Option<Icon>,
2207 pub image: Option<String>,
2208 pub pixels: Option<Pixels>,
2209 pub body: Option<String>,
2210 pub markdown: bool,
2211 pub urgency: Urgency,
2212 pub time: Option<SystemTime>,
2213 pub thread: Option<String>,
2214 pub thread_name: Option<String>,
2215 pub progress: Option<f32>,
2216 pub actions: Vec<(String, M)>,
2217 pub choices: Option<ChoicesParts<M>>,
2218 pub step: Option<StepParts<M>>,
2219 pub on_press: Option<M>,
2220 pub on_dismiss: Option<M>,
2221 pub expires: Option<(std::time::Duration, M)>,
2222 pub reply: Option<ReplyParts<M>>,
2223}
2224
2225pub struct Action<M> {
2229 id: Option<String>,
2230 shortcut: Option<Shortcut>,
2231 title: String,
2232 subtitle: Option<String>,
2233 keywords: Vec<String>,
2234 leading: Option<Node<M>>,
2235 message: M,
2236}
2237
2238pub fn action<M>(title: impl Into<String>, message: M) -> Action<M> {
2240 Action {
2241 id: None,
2242 shortcut: None,
2243 title: title.into(),
2244 subtitle: None,
2245 keywords: Vec::new(),
2246 leading: None,
2247 message,
2248 }
2249}
2250
2251impl<M> Action<M> {
2252 pub fn id(mut self, id: impl Into<String>) -> Self {
2256 self.id = Some(id.into());
2257 self
2258 }
2259
2260 pub fn shortcut(mut self, key: Shortcut) -> Self {
2265 self.shortcut = Some(key);
2266 self
2267 }
2268
2269 pub fn subtitle(mut self, text: impl Into<String>) -> Self {
2271 self.subtitle = Some(text.into());
2272 self
2273 }
2274
2275 pub fn keywords(mut self, words: impl IntoIterator<Item = impl Into<String>>) -> Self {
2277 self.keywords.extend(words.into_iter().map(Into::into));
2278 self
2279 }
2280
2281 pub fn leading(mut self, node: Node<M>) -> Self {
2283 self.leading = Some(node);
2284 self
2285 }
2286
2287 pub fn map<N: 'static>(self, f: &(impl Fn(M) -> N + Clone + 'static)) -> Action<N>
2288 where
2289 M: 'static,
2290 {
2291 Action {
2292 id: self.id,
2293 shortcut: self.shortcut,
2294 title: self.title,
2295 subtitle: self.subtitle,
2296 keywords: self.keywords,
2297 leading: self.leading.map(|node| node.map(f)),
2298 message: f(self.message),
2299 }
2300 }
2301
2302 pub fn title(&self) -> &str {
2303 &self.title
2304 }
2305
2306 #[doc(hidden)]
2307 pub fn id_str(&self) -> Option<&str> {
2308 self.id.as_deref()
2309 }
2310
2311 #[doc(hidden)]
2312 pub fn into_parts(self) -> ActionParts<M> {
2313 ActionParts {
2314 id: self.id,
2315 shortcut: self.shortcut,
2316 title: self.title,
2317 subtitle: self.subtitle,
2318 keywords: self.keywords,
2319 leading: self.leading,
2320 message: self.message,
2321 }
2322 }
2323}
2324
2325#[doc(hidden)]
2326pub struct ActionParts<M> {
2327 pub id: Option<String>,
2328 pub shortcut: Option<Shortcut>,
2329 pub title: String,
2330 pub subtitle: Option<String>,
2331 pub keywords: Vec<String>,
2332 pub leading: Option<Node<M>>,
2333 pub message: M,
2334}
2335
2336#[cfg(test)]
2337mod tests {
2338 use super::*;
2339
2340 #[test]
2341 fn map_converts_every_message() {
2342 let node = row([
2343 button(label("a"), 1).on_secondary(4),
2344 column([button(label("b"), 2), label("c")]),
2345 switcher([(label("d"), 3)], Some(0)),
2346 ]);
2347
2348 let Kind::Row(children) = node.map(&|n: i32| n * 10).into_kind() else {
2349 panic!("row became something else");
2350 };
2351 let mut messages = Vec::new();
2352 collect(children, &mut messages);
2353 assert_eq!(messages, [10, 40, 20, 30]);
2354 }
2355
2356 fn collect(nodes: Vec<Node<i32>>, out: &mut Vec<i32>) {
2357 for node in nodes {
2358 match node.into_kind() {
2359 Kind::Button {
2360 on_press,
2361 on_secondary,
2362 ..
2363 } => {
2364 out.push(on_press);
2365 out.extend(on_secondary);
2366 }
2367 Kind::Switcher { options, .. } => {
2368 out.extend(options.into_iter().map(|(_, message)| message))
2369 }
2370 Kind::Row(children) | Kind::Column(children) => collect(children, out),
2371 _ => {}
2372 }
2373 }
2374 }
2375
2376 #[test]
2377 fn hover_shows_the_second_option() {
2378 let node: Node<()> = hover(label("a"), label("b"));
2379 assert!(matches!(
2380 node.into_kind(),
2381 Kind::Alternate {
2382 current: 0,
2383 hovered: Some(1),
2384 ..
2385 }
2386 ));
2387 let node: Node<()> = label("a").on_hover(1);
2388 assert!(matches!(node.into_kind(), Kind::Label { .. }));
2389 }
2390
2391 #[test]
2392 fn tone_fills_only_unset_text() {
2393 let node: Node<()> =
2394 row([label("a").tone(Tone::Accent), label("b"), icon(Icon::Wired)]).tone(Tone::Dim);
2395 let Kind::Row(children) = node.into_kind() else {
2396 unreachable!()
2397 };
2398 let tones: Vec<_> = children
2399 .into_iter()
2400 .map(|child| match child.into_kind() {
2401 Kind::Label { tone, .. } | Kind::Icon { tone, .. } => tone,
2402 _ => None,
2403 })
2404 .collect();
2405 assert_eq!(
2406 tones,
2407 [Some(Tone::Accent), Some(Tone::Dim), Some(Tone::Dim)]
2408 );
2409 }
2410
2411 #[test]
2412 fn map_reaches_new_nodes_and_their_closures() {
2413 let node = column([
2414 header("h").on_back(1).trailing(toggle(true, 2)),
2415 list([list_item("a")
2416 .on_press(3)
2417 .on_secondary(4)
2418 .trailing(button(icon(Icon::Info), 5))]),
2419 slider(0.5, 0.0..=1.0, |v| (v * 10.0) as i32).on_release(6),
2420 text_input("p", "v", |text: String| text.len() as i32).on_submit(7),
2421 scroll(text_area("p", "v", |text: String| text.len() as i32 + 1).on_submit(8)).bottom(),
2422 ]);
2423 let Kind::Column(children) = node.map(&|n: i32| n * 10).into_kind() else {
2424 panic!("column became something else");
2425 };
2426 let mut children = children.into_iter().map(Node::into_kind);
2427 let Some(Kind::Header {
2428 on_back: Some(10),
2429 trailing: Some(trailing),
2430 ..
2431 }) = children.next()
2432 else {
2433 panic!("header lost its back message");
2434 };
2435 assert!(matches!(
2436 trailing.into_kind(),
2437 Kind::Toggle { on_toggle: 20, .. }
2438 ));
2439 let Some(Kind::List(rows)) = children.next() else {
2440 panic!("list lost");
2441 };
2442 let Some(Kind::ListItem {
2443 on_press: Some(30),
2444 on_secondary: Some(40),
2445 trailing: Some(trailing),
2446 ..
2447 }) = rows.into_iter().next().map(Node::into_kind)
2448 else {
2449 panic!("list item lost its messages");
2450 };
2451 assert!(matches!(
2452 trailing.into_kind(),
2453 Kind::Button { on_press: 50, .. }
2454 ));
2455 let Some(Kind::Slider {
2456 on_change,
2457 on_release: Some(60),
2458 ..
2459 }) = children.next()
2460 else {
2461 panic!("slider lost");
2462 };
2463 assert_eq!(on_change(0.3), 30);
2464 let Some(Kind::TextInput {
2465 on_input,
2466 on_submit: Some(70),
2467 ..
2468 }) = children.next()
2469 else {
2470 panic!("text input lost");
2471 };
2472 assert_eq!(on_input("abcd".into()), 40);
2473 let Some(Kind::Scroll {
2474 content,
2475 bottom: true,
2476 }) = children.next()
2477 else {
2478 panic!("scroll lost");
2479 };
2480 let Kind::TextArea {
2481 on_input,
2482 on_submit: Some(80),
2483 ..
2484 } = content.into_kind()
2485 else {
2486 panic!("text area lost");
2487 };
2488 assert_eq!(on_input("abcd".into()), 50);
2489 }
2490
2491 #[test]
2492 fn tabs_map_and_tone_every_page() {
2493 let node = tabs([("a", button(label("x"), 1)), ("b", button(label("y"), 2))])
2494 .tone(Tone::Dim)
2495 .map(&|n: i32| n * 10);
2496 let Kind::Tabs(pages) = node.into_kind() else {
2497 panic!("tabs became something else");
2498 };
2499 let names: Vec<_> = pages.iter().map(|(name, _)| name.as_str()).collect();
2500 assert_eq!(names, ["a", "b"]);
2501 let mut messages = Vec::new();
2502 for (_, page) in pages {
2503 let Kind::Button {
2504 content, on_press, ..
2505 } = page.into_kind()
2506 else {
2507 panic!("page lost its button");
2508 };
2509 assert!(matches!(
2510 content.into_kind(),
2511 Kind::Label {
2512 tone: Some(Tone::Dim),
2513 ..
2514 }
2515 ));
2516 messages.push(on_press);
2517 }
2518 assert_eq!(messages, [10, 20]);
2519 }
2520
2521 #[test]
2522 fn notice_map_reaches_every_message() {
2523 let notice = Notice::new(7, "s")
2524 .action("a", 1)
2525 .action("b", 2)
2526 .on_press(3)
2527 .on_dismiss(4)
2528 .expires(std::time::Duration::from_secs(5), 5)
2529 .reply(
2530 Reply::new("p", "v", |text: String| text.len() as i32)
2531 .on_submit(6)
2532 .on_cancel(7),
2533 )
2534 .choices(
2535 Choices::many()
2536 .option("x", None, false, 8)
2537 .option("y", Some("why".into()), true, 9)
2538 .recommended()
2539 .on_send(10),
2540 )
2541 .step(1, 3, Some(11))
2542 .map(&|n: i32| n * 10);
2543 assert_eq!(notice.key(), 7);
2544 let parts = notice.into_parts();
2545 let actions: Vec<_> = parts.actions.into_iter().map(|(_, m)| m).collect();
2546 assert_eq!(actions, [10, 20]);
2547 assert_eq!(parts.on_press, Some(30));
2548 assert_eq!(parts.on_dismiss, Some(40));
2549 assert_eq!(parts.expires.map(|(_, m)| m), Some(50));
2550 let reply = parts.reply.unwrap();
2551 assert_eq!((reply.on_input)("abc".into()), 30);
2552 assert_eq!((reply.on_submit, reply.on_cancel), (Some(60), Some(70)));
2553 let choices = parts.choices.unwrap();
2554 let picks: Vec<_> = choices.options.iter().map(|o| o.on_pick).collect();
2555 assert_eq!(picks, [80, 90]);
2556 assert!(choices.multiple && choices.options[1].recommended && choices.options[1].picked);
2557 assert_eq!(choices.on_send, Some(100));
2558 let step = parts.step.unwrap();
2559 assert_eq!((step.index, step.count, step.on_back), (1, 3, Some(110)));
2560 }
2561
2562 #[test]
2563 fn pixels_must_be_whole() {
2564 assert!(Pixels::new(2, 2, vec![0; 16]).is_some());
2565 assert!(Pixels::new(2, 2, vec![0; 15]).is_none());
2566 assert!(Pixels::new(0, 0, Vec::new()).is_none());
2567 assert!(Pixels::new(u32::MAX, u32::MAX, Vec::new()).is_none());
2568 }
2569
2570 #[test]
2571 fn progress_stays_between_none_and_all() {
2572 let parts = Notice::<()>::new(1, "s").progress(1.5).into_parts();
2573 assert_eq!(parts.progress, Some(1.0));
2574 }
2575
2576 #[test]
2577 fn modifiers_only_touch_their_nodes() {
2578 let node: Node<i32> = label("a").trailing(label("b")).on_press(1).selected(true);
2579 assert!(matches!(node.into_kind(), Kind::Label { .. }));
2580 let node: Node<i32> = button(label("a"), 1).on_press(2).prominent();
2581 assert!(matches!(
2582 node.into_kind(),
2583 Kind::Button {
2584 on_press: 1,
2585 prominent: true,
2586 ..
2587 }
2588 ));
2589 }
2590
2591 #[test]
2592 fn a_header_takes_a_picture_and_a_subtitle() {
2593 let node = header("t")
2594 .subtitle("s")
2595 .leading(button(icon(Icon::Info), 1))
2596 .tone(Tone::Dim)
2597 .map(&|n: i32| n * 10);
2598 let Kind::Header {
2599 subtitle: Some(subtitle),
2600 leading: Some(leading),
2601 ..
2602 } = node.into_kind()
2603 else {
2604 panic!("header lost its picture or subtitle");
2605 };
2606 assert_eq!(subtitle, "s");
2607 let Kind::Button {
2608 content,
2609 on_press: 10,
2610 ..
2611 } = leading.into_kind()
2612 else {
2613 panic!("picture lost its message");
2614 };
2615 assert!(matches!(
2616 content.into_kind(),
2617 Kind::Icon {
2618 tone: Some(Tone::Dim),
2619 ..
2620 }
2621 ));
2622 }
2623
2624 #[test]
2625 fn timelines_map_and_tone_their_steps() {
2626 let node = timeline([
2627 step(Mark::You, label("a")),
2628 step(Mark::Failed, button(literal("b"), 1)).leading(icon(Icon::Check)),
2629 label("c"),
2630 ])
2631 .tone(Tone::Dim)
2632 .map(&|n: i32| n * 10);
2633 let Kind::Timeline(steps) = node.into_kind() else {
2634 panic!("timeline became something else");
2635 };
2636 let mut steps = steps.into_iter().map(Node::into_kind);
2637 let Some(Kind::Step {
2638 mark: Mark::You,
2639 leading: None,
2640 content,
2641 }) = steps.next()
2642 else {
2643 panic!("first step lost");
2644 };
2645 assert!(matches!(
2646 content.into_kind(),
2647 Kind::Label {
2648 tone: Some(Tone::Dim),
2649 ..
2650 }
2651 ));
2652 let Some(Kind::Step {
2653 mark: Mark::Failed,
2654 leading: Some(leading),
2655 content,
2656 }) = steps.next()
2657 else {
2658 panic!("second step lost its picture");
2659 };
2660 assert!(matches!(
2661 leading.into_kind(),
2662 Kind::Icon {
2663 tone: Some(Tone::Dim),
2664 ..
2665 }
2666 ));
2667 let Kind::Button {
2668 content,
2669 on_press: 10,
2670 ..
2671 } = content.into_kind()
2672 else {
2673 panic!("button lost its message");
2674 };
2675 assert!(matches!(
2676 content.into_kind(),
2677 Kind::Literal {
2678 tone: Some(Tone::Dim),
2679 ..
2680 }
2681 ));
2682 assert!(matches!(steps.next(), Some(Kind::Label { .. })));
2683 let node: Node<()> = diff("f", [(Change::Added, "x".to_string())]).leading(label("y"));
2684 assert!(matches!(node.into_kind(), Kind::Diff { lines, .. } if lines.len() == 1));
2685 }
2686
2687 #[test]
2688 fn tone_reaches_details_and_row_icons() {
2689 let node: Node<()> = column([
2690 detail("n", label("v")),
2691 list_item("t").leading(icon(Icon::Lock)),
2692 ])
2693 .tone(Tone::Good);
2694 let Kind::Column(children) = node.into_kind() else {
2695 unreachable!()
2696 };
2697 let mut children = children.into_iter().map(Node::into_kind);
2698 let Some(Kind::Detail { value, .. }) = children.next() else {
2699 unreachable!()
2700 };
2701 assert!(matches!(
2702 value.into_kind(),
2703 Kind::Label {
2704 tone: Some(Tone::Good),
2705 ..
2706 }
2707 ));
2708 let Some(Kind::ListItem {
2709 leading: Some(leading),
2710 ..
2711 }) = children.next()
2712 else {
2713 unreachable!()
2714 };
2715 assert!(matches!(
2716 leading.into_kind(),
2717 Kind::Icon {
2718 tone: Some(Tone::Good),
2719 ..
2720 }
2721 ));
2722 }
2723
2724 #[test]
2725 fn bays_take_what_fills_them_or_leave_no_gap() {
2726 let mut node: Node<u8> = column([
2727 label("a"),
2728 bay("player", "spotify"),
2729 bay("other", "x"),
2730 center(bay("player", "y")),
2731 ]);
2732 let mut asked = Vec::new();
2733 node.fill_bays(&mut |name, subject| {
2734 asked.push(format!("{name}:{subject}"));
2735 match subject {
2736 "spotify" => vec![label("b"), label("c")],
2737 "y" => vec![label("d"), label("e")],
2738 _ => Vec::new(),
2739 }
2740 });
2741 assert_eq!(asked, ["player:spotify", "other:x", "player:y"]);
2742 let Kind::Column(children) = node.into_kind() else {
2743 panic!("still a column");
2744 };
2745 assert_eq!(children.len(), 4);
2747 assert!(matches!(children[2].kind(), Kind::Label { text, .. } if text == "c"));
2748 assert!(
2749 matches!(children[3].kind(), Kind::Center(inner) if matches!(inner.kind(), Kind::Column(two) if two.len() == 2))
2750 );
2751 }
2752}