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