1use std::ops::RangeInclusive;
8
9#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
11pub enum Tone {
12 #[default]
13 Normal,
14 Dim,
16 Accent,
18 Warning,
19 Critical,
20 Good,
22}
23
24#[derive(Debug, Clone, Copy, PartialEq)]
27pub enum Icon {
28 Volume { level: f32, muted: bool },
30 Wifi { strength: Option<f32> },
32 Wired,
34 Battery { charge: f32, charging: bool },
36 Lock,
38 Info,
40 Back,
42 Forward,
44 Check,
46 Refresh,
48 Close,
50 Reveal,
52 Bolt,
54 Plug,
56 Mic { muted: bool },
58 Speaker,
60 Headphones,
62 Display,
64 Bell { muted: bool },
66 Media { playing: bool },
69 Play,
71 Pause,
73 Previous,
75 Next,
77}
78
79pub struct Node<M>(Kind<M>);
81
82#[doc(hidden)]
83pub enum Kind<M> {
84 Label {
85 text: String,
86 tone: Option<Tone>,
87 },
88 Heading {
89 text: String,
90 tone: Option<Tone>,
91 },
92 Icon {
93 icon: Icon,
94 tone: Option<Tone>,
95 },
96 AppIcon {
97 app_id: String,
98 tone: Option<Tone>,
99 },
100 Image {
101 file: &'static [u8],
102 tint: bool,
103 tone: Option<Tone>,
104 },
105 Cover {
106 url: String,
107 fallback: Box<Node<M>>,
109 large: bool,
111 tone: Option<Tone>,
112 },
113 Counter {
114 value: i64,
115 range: RangeInclusive<i64>,
116 unit: String,
117 tone: Option<Tone>,
118 },
119 Alternate {
120 options: Vec<Node<M>>,
121 current: usize,
122 hovered: Option<usize>,
124 grows: bool,
126 },
127 Row(Vec<Node<M>>),
128 Column(Vec<Node<M>>),
129 Grid {
130 columns: usize,
131 cells: Vec<Node<M>>,
132 },
133 Button {
134 content: Box<Node<M>>,
135 on_press: M,
136 on_secondary: Option<M>,
137 prominent: bool,
139 },
140 Switcher {
141 options: Vec<(Node<M>, M)>,
142 current: Option<usize>,
143 },
144 Gulp(Box<Node<M>>),
145 Center(Box<Node<M>>),
147 Separator,
148 Empty,
149 ListItem {
150 title: String,
151 subtitle: Option<String>,
152 leading: Option<Box<Node<M>>>,
153 trailing: Option<Box<Node<M>>>,
154 selected: bool,
155 on_press: Option<M>,
156 on_secondary: Option<M>,
157 },
158 List(Vec<Node<M>>),
159 Section {
160 title: String,
161 trailing: Option<Box<Node<M>>>,
162 },
163 Header {
164 title: String,
165 on_back: Option<M>,
166 trailing: Option<Box<Node<M>>>,
167 },
168 Detail {
169 name: String,
170 value: Box<Node<M>>,
171 },
172 Fill,
173 Slider {
174 value: f32,
175 range: RangeInclusive<f32>,
176 step: f32,
177 on_change: Box<dyn Fn(f32) -> M>,
178 on_release: Option<M>,
179 },
180 Toggle {
181 on: bool,
182 on_toggle: M,
183 },
184 Meter {
185 fraction: f32,
186 limit: Option<f32>,
188 tone: Option<Tone>,
189 },
190 TextInput {
191 placeholder: String,
192 value: String,
193 on_input: Box<dyn Fn(String) -> M>,
194 on_submit: Option<M>,
195 secret: bool,
196 trailing: Option<Box<Node<M>>>,
197 tone: Option<Tone>,
198 },
199 Display(Box<Node<M>>),
200 Mark(Box<Node<M>>),
201 Badge {
202 text: String,
203 tone: Option<Tone>,
204 },
205 Tabs(Vec<(String, Node<M>)>),
207 Markdown {
208 text: String,
209 tone: Option<Tone>,
210 },
211 TextArea {
212 placeholder: String,
213 value: String,
214 on_input: Box<dyn Fn(String) -> M>,
215 on_submit: Option<M>,
216 trailing: Option<Box<Node<M>>>,
217 },
218 Scroll {
219 content: Box<Node<M>>,
220 bottom: bool,
222 },
223}
224
225pub fn label<M>(text: impl Into<String>) -> Node<M> {
226 Node(Kind::Label {
227 text: text.into(),
228 tone: None,
229 })
230}
231
232pub fn heading<M>(text: impl Into<String>) -> Node<M> {
234 Node(Kind::Heading {
235 text: text.into(),
236 tone: None,
237 })
238}
239
240pub fn icon<M>(icon: Icon) -> Node<M> {
242 Node(Kind::Icon { icon, tone: None })
243}
244
245pub fn app_icon<M>(app_id: impl Into<String>) -> Node<M> {
249 Node(Kind::AppIcon {
250 app_id: app_id.into(),
251 tone: None,
252 })
253}
254
255pub fn image<M>(file: &'static [u8]) -> Node<M> {
260 Node(Kind::Image {
261 file,
262 tint: false,
263 tone: None,
264 })
265}
266
267pub fn symbol<M>(file: &'static [u8]) -> Node<M> {
270 Node(Kind::Image {
271 file,
272 tint: true,
273 tone: None,
274 })
275}
276
277pub fn cover<M>(url: impl Into<String>, fallback: Node<M>) -> Node<M> {
282 Node(Kind::Cover {
283 url: url.into(),
284 fallback: Box::new(fallback),
285 large: false,
286 tone: None,
287 })
288}
289
290pub fn counter<M>(value: i64, range: RangeInclusive<i64>, unit: impl Into<String>) -> Node<M> {
294 Node(Kind::Counter {
295 value,
296 range,
297 unit: unit.into(),
298 tone: None,
299 })
300}
301
302pub fn alternate<M>(options: impl IntoIterator<Item = Node<M>>, current: usize) -> Node<M> {
307 Node(Kind::Alternate {
308 options: options.into_iter().collect(),
309 current,
310 hovered: None,
311 grows: false,
312 })
313}
314
315pub fn hover<M>(normal: Node<M>, hovered: Node<M>) -> Node<M> {
318 alternate([normal, hovered], 0).on_hover(1)
319}
320
321pub fn expand<M>(collapsed: Node<M>, expanded: Node<M>) -> Node<M> {
326 Node(Kind::Alternate {
327 options: vec![collapsed, expanded],
328 current: 0,
329 hovered: Some(1),
330 grows: true,
331 })
332}
333
334pub fn row<M>(children: impl IntoIterator<Item = Node<M>>) -> Node<M> {
336 Node(Kind::Row(children.into_iter().collect()))
337}
338
339pub fn column<M>(children: impl IntoIterator<Item = Node<M>>) -> Node<M> {
341 Node(Kind::Column(children.into_iter().collect()))
342}
343
344pub fn center<M>(content: Node<M>) -> Node<M> {
348 Node(Kind::Center(Box::new(content)))
349}
350
351pub fn grid<M>(columns: usize, cells: impl IntoIterator<Item = Node<M>>) -> Node<M> {
353 Node(Kind::Grid {
354 columns: columns.max(1),
355 cells: cells.into_iter().collect(),
356 })
357}
358
359pub fn button<M>(content: Node<M>, on_press: M) -> Node<M> {
360 Node(Kind::Button {
361 content: Box::new(content),
362 on_press,
363 on_secondary: None,
364 prominent: false,
365 })
366}
367
368pub fn switcher<M>(
372 options: impl IntoIterator<Item = (Node<M>, M)>,
373 current: Option<usize>,
374) -> Node<M> {
375 Node(Kind::Switcher {
376 options: options.into_iter().collect(),
377 current,
378 })
379}
380
381pub fn gulp<M>(content: Node<M>) -> Node<M> {
384 Node(Kind::Gulp(Box::new(content)))
385}
386
387pub fn separator<M>() -> Node<M> {
389 Node(Kind::Separator)
390}
391
392pub fn empty<M>() -> Node<M> {
394 Node(Kind::Empty)
395}
396
397pub fn list_item<M>(title: impl Into<String>) -> Node<M> {
402 Node(Kind::ListItem {
403 title: title.into(),
404 subtitle: None,
405 leading: None,
406 trailing: None,
407 selected: false,
408 on_press: None,
409 on_secondary: None,
410 })
411}
412
413pub fn list<M>(rows: impl IntoIterator<Item = Node<M>>) -> Node<M> {
416 Node(Kind::List(rows.into_iter().collect()))
417}
418
419pub fn section<M>(title: impl Into<String>) -> Node<M> {
422 Node(Kind::Section {
423 title: title.into(),
424 trailing: None,
425 })
426}
427
428pub fn header<M>(title: impl Into<String>) -> Node<M> {
431 Node(Kind::Header {
432 title: title.into(),
433 on_back: None,
434 trailing: None,
435 })
436}
437
438pub fn detail<M>(name: impl Into<String>, value: Node<M>) -> Node<M> {
440 Node(Kind::Detail {
441 name: name.into(),
442 value: Box::new(value),
443 })
444}
445
446pub fn fill<M>() -> Node<M> {
448 Node(Kind::Fill)
449}
450
451pub fn slider<M: 'static>(
454 value: f32,
455 range: RangeInclusive<f32>,
456 on_change: impl Fn(f32) -> M + 'static,
457) -> Node<M> {
458 Node(Kind::Slider {
459 value,
460 range,
461 step: 1.0,
462 on_change: Box::new(on_change),
463 on_release: None,
464 })
465}
466
467pub fn toggle<M>(on: bool, on_toggle: M) -> Node<M> {
469 Node(Kind::Toggle { on, on_toggle })
470}
471
472pub fn meter<M>(fraction: f32) -> Node<M> {
475 Node(Kind::Meter {
476 fraction,
477 limit: None,
478 tone: None,
479 })
480}
481
482pub fn text_input<M: 'static>(
487 placeholder: impl Into<String>,
488 value: impl Into<String>,
489 on_input: impl Fn(String) -> M + 'static,
490) -> Node<M> {
491 Node(Kind::TextInput {
492 placeholder: placeholder.into(),
493 value: value.into(),
494 on_input: Box::new(on_input),
495 on_submit: None,
496 secret: false,
497 trailing: None,
498 tone: None,
499 })
500}
501
502pub fn text_area<M: 'static>(
507 placeholder: impl Into<String>,
508 value: impl Into<String>,
509 on_input: impl Fn(String) -> M + 'static,
510) -> Node<M> {
511 Node(Kind::TextArea {
512 placeholder: placeholder.into(),
513 value: value.into(),
514 on_input: Box::new(on_input),
515 on_submit: None,
516 trailing: None,
517 })
518}
519
520pub fn markdown<M>(text: impl Into<String>) -> Node<M> {
525 Node(Kind::Markdown {
526 text: text.into(),
527 tone: None,
528 })
529}
530
531pub fn scroll<M>(content: Node<M>) -> Node<M> {
535 Node(Kind::Scroll {
536 content: Box::new(content),
537 bottom: false,
538 })
539}
540
541pub fn display<M>(content: Node<M>) -> Node<M> {
543 Node(Kind::Display(Box::new(content)))
544}
545
546pub fn mark<M>(content: Node<M>) -> Node<M> {
548 Node(Kind::Mark(Box::new(content)))
549}
550
551pub fn badge<M>(text: impl Into<String>) -> Node<M> {
553 Node(Kind::Badge {
554 text: text.into(),
555 tone: None,
556 })
557}
558
559pub fn tabs<M>(pages: impl IntoIterator<Item = (impl Into<String>, Node<M>)>) -> Node<M> {
563 Node(Kind::Tabs(
564 pages
565 .into_iter()
566 .map(|(name, page)| (name.into(), page))
567 .collect(),
568 ))
569}
570
571impl<M> Node<M> {
572 pub fn tone(mut self, tone: Tone) -> Self {
574 self.set_tone(tone);
575 self
576 }
577
578 pub fn on_hover(mut self, index: usize) -> Self {
581 if let Kind::Alternate { hovered, .. } = &mut self.0 {
582 *hovered = Some(index);
583 }
584 self
585 }
586
587 pub fn on_secondary(mut self, message: M) -> Self {
590 if let Kind::Button { on_secondary, .. } | Kind::ListItem { on_secondary, .. } = &mut self.0
591 {
592 *on_secondary = Some(message);
593 }
594 self
595 }
596
597 pub fn prominent(mut self) -> Self {
600 if let Kind::Button { prominent, .. } = &mut self.0 {
601 *prominent = true;
602 }
603 self
604 }
605
606 pub fn on_press(mut self, message: M) -> Self {
608 if let Kind::ListItem { on_press, .. } = &mut self.0 {
609 *on_press = Some(message);
610 }
611 self
612 }
613
614 pub fn subtitle(mut self, text: impl Into<String>) -> Self {
616 if let Kind::ListItem { subtitle, .. } = &mut self.0 {
617 *subtitle = Some(text.into());
618 }
619 self
620 }
621
622 pub fn leading(mut self, node: Node<M>) -> Self {
624 if let Kind::ListItem { leading, .. } = &mut self.0 {
625 *leading = Some(Box::new(node));
626 }
627 self
628 }
629
630 pub fn trailing(mut self, node: Node<M>) -> Self {
633 if let Kind::ListItem { trailing, .. }
634 | Kind::Section { trailing, .. }
635 | Kind::Header { trailing, .. }
636 | Kind::TextInput { trailing, .. }
637 | Kind::TextArea { trailing, .. } = &mut self.0
638 {
639 *trailing = Some(Box::new(node));
640 }
641 self
642 }
643
644 pub fn selected(mut self, yes: bool) -> Self {
646 if let Kind::ListItem { selected, .. } = &mut self.0 {
647 *selected = yes;
648 }
649 self
650 }
651
652 pub fn on_back(mut self, message: M) -> Self {
654 if let Kind::Header { on_back, .. } = &mut self.0 {
655 *on_back = Some(message);
656 }
657 self
658 }
659
660 pub fn step(mut self, by: f32) -> Self {
662 if let Kind::Slider { step, .. } = &mut self.0 {
663 *step = by;
664 }
665 self
666 }
667
668 pub fn on_release(mut self, message: M) -> Self {
670 if let Kind::Slider { on_release, .. } = &mut self.0 {
671 *on_release = Some(message);
672 }
673 self
674 }
675
676 pub fn on_submit(mut self, message: M) -> Self {
678 if let Kind::TextInput { on_submit, .. } | Kind::TextArea { on_submit, .. } = &mut self.0 {
679 *on_submit = Some(message);
680 }
681 self
682 }
683
684 pub fn secret(mut self, yes: bool) -> Self {
686 if let Kind::TextInput { secret, .. } = &mut self.0 {
687 *secret = yes;
688 }
689 self
690 }
691
692 pub fn large(mut self) -> Self {
695 if let Kind::Cover { large, .. } = &mut self.0 {
696 *large = true;
697 }
698 self
699 }
700
701 pub fn bottom(mut self) -> Self {
704 if let Kind::Scroll { bottom, .. } = &mut self.0 {
705 *bottom = true;
706 }
707 self
708 }
709
710 pub fn limit(mut self, at: f32) -> Self {
712 if let Kind::Meter { limit, .. } = &mut self.0 {
713 *limit = Some(at);
714 }
715 self
716 }
717
718 fn set_tone(&mut self, new: Tone) {
719 match &mut self.0 {
720 Kind::Label { tone, .. }
721 | Kind::Heading { tone, .. }
722 | Kind::Icon { tone, .. }
723 | Kind::AppIcon { tone, .. }
724 | Kind::Image { tone, .. }
725 | Kind::Counter { tone, .. }
726 | Kind::Meter { tone, .. }
727 | Kind::TextInput { tone, .. }
728 | Kind::Markdown { tone, .. }
729 | Kind::Badge { tone, .. } => {
730 tone.get_or_insert(new);
731 }
732 Kind::Cover { tone, fallback, .. } => {
733 tone.get_or_insert(new);
734 fallback.set_tone(new);
735 }
736 Kind::Alternate {
737 options: children, ..
738 }
739 | Kind::Row(children)
740 | Kind::Column(children)
741 | Kind::Grid {
742 cells: children, ..
743 }
744 | Kind::List(children) => {
745 children.iter_mut().for_each(|child| child.set_tone(new));
746 }
747 Kind::Button { content, .. }
748 | Kind::Gulp(content)
749 | Kind::Center(content)
750 | Kind::Display(content)
751 | Kind::Mark(content)
752 | Kind::Scroll { content, .. }
753 | Kind::Detail { value: content, .. } => content.set_tone(new),
754 Kind::ListItem {
755 leading, trailing, ..
756 } => {
757 leading
758 .iter_mut()
759 .chain(trailing.iter_mut())
760 .for_each(|child| child.set_tone(new));
761 }
762 Kind::Tabs(pages) => {
763 for (_, page) in pages {
764 page.set_tone(new);
765 }
766 }
767 Kind::Switcher { options, .. } => {
768 options
769 .iter_mut()
770 .for_each(|(child, _)| child.set_tone(new));
771 }
772 Kind::Separator
773 | Kind::Empty
774 | Kind::Section { .. }
775 | Kind::Header { .. }
776 | Kind::Fill
777 | Kind::Slider { .. }
778 | Kind::TextArea { .. }
779 | Kind::Toggle { .. } => {}
780 }
781 }
782
783 pub fn map<N: 'static>(self, f: &(impl Fn(M) -> N + Clone + 'static)) -> Node<N>
787 where
788 M: 'static,
789 {
790 let map_all = |nodes: Vec<Node<M>>| nodes.into_iter().map(|node| node.map(f)).collect();
791 let map_box = |node: Option<Box<Node<M>>>| node.map(|node| Box::new(node.map(f)));
792
793 Node(match self.0 {
794 Kind::Label { text, tone } => Kind::Label { text, tone },
795 Kind::Heading { text, tone } => Kind::Heading { text, tone },
796 Kind::Icon { icon, tone } => Kind::Icon { icon, tone },
797 Kind::AppIcon { app_id, tone } => Kind::AppIcon { app_id, tone },
798 Kind::Image { file, tint, tone } => Kind::Image { file, tint, tone },
799 Kind::Cover {
800 url,
801 fallback,
802 large,
803 tone,
804 } => Kind::Cover {
805 url,
806 fallback: Box::new(fallback.map(f)),
807 large,
808 tone,
809 },
810 Kind::Counter {
811 value,
812 range,
813 unit,
814 tone,
815 } => Kind::Counter {
816 value,
817 range,
818 unit,
819 tone,
820 },
821 Kind::Alternate {
822 options,
823 current,
824 hovered,
825 grows,
826 } => Kind::Alternate {
827 options: map_all(options),
828 current,
829 hovered,
830 grows,
831 },
832 Kind::Row(children) => Kind::Row(map_all(children)),
833 Kind::Column(children) => Kind::Column(map_all(children)),
834 Kind::Grid { columns, cells } => Kind::Grid {
835 columns,
836 cells: map_all(cells),
837 },
838 Kind::Button {
839 content,
840 on_press,
841 on_secondary,
842 prominent,
843 } => Kind::Button {
844 content: Box::new(content.map(f)),
845 on_press: f(on_press),
846 on_secondary: on_secondary.map(f),
847 prominent,
848 },
849 Kind::Switcher { options, current } => Kind::Switcher {
850 options: options
851 .into_iter()
852 .map(|(node, message)| (node.map(f), f(message)))
853 .collect(),
854 current,
855 },
856 Kind::Gulp(content) => Kind::Gulp(Box::new(content.map(f))),
857 Kind::Center(content) => Kind::Center(Box::new(content.map(f))),
858 Kind::Separator => Kind::Separator,
859 Kind::Empty => Kind::Empty,
860 Kind::ListItem {
861 title,
862 subtitle,
863 leading,
864 trailing,
865 selected,
866 on_press,
867 on_secondary,
868 } => Kind::ListItem {
869 title,
870 subtitle,
871 leading: map_box(leading),
872 trailing: map_box(trailing),
873 selected,
874 on_press: on_press.map(f),
875 on_secondary: on_secondary.map(f),
876 },
877 Kind::List(rows) => Kind::List(map_all(rows)),
878 Kind::Section { title, trailing } => Kind::Section {
879 title,
880 trailing: map_box(trailing),
881 },
882 Kind::Header {
883 title,
884 on_back,
885 trailing,
886 } => Kind::Header {
887 title,
888 on_back: on_back.map(f),
889 trailing: map_box(trailing),
890 },
891 Kind::Detail { name, value } => Kind::Detail {
892 name,
893 value: Box::new(value.map(f)),
894 },
895 Kind::Fill => Kind::Fill,
896 Kind::Slider {
897 value,
898 range,
899 step,
900 on_change,
901 on_release,
902 } => {
903 let g = f.clone();
904 Kind::Slider {
905 value,
906 range,
907 step,
908 on_change: Box::new(move |value| g(on_change(value))),
909 on_release: on_release.map(f),
910 }
911 }
912 Kind::Toggle { on, on_toggle } => Kind::Toggle {
913 on,
914 on_toggle: f(on_toggle),
915 },
916 Kind::Meter {
917 fraction,
918 limit,
919 tone,
920 } => Kind::Meter {
921 fraction,
922 limit,
923 tone,
924 },
925 Kind::TextInput {
926 placeholder,
927 value,
928 on_input,
929 on_submit,
930 secret,
931 trailing,
932 tone,
933 } => {
934 let g = f.clone();
935 Kind::TextInput {
936 placeholder,
937 value,
938 on_input: Box::new(move |text| g(on_input(text))),
939 on_submit: on_submit.map(f),
940 secret,
941 trailing: map_box(trailing),
942 tone,
943 }
944 }
945 Kind::Display(content) => Kind::Display(Box::new(content.map(f))),
946 Kind::Mark(content) => Kind::Mark(Box::new(content.map(f))),
947 Kind::Badge { text, tone } => Kind::Badge { text, tone },
948 Kind::Tabs(pages) => Kind::Tabs(
949 pages
950 .into_iter()
951 .map(|(name, page)| (name, page.map(f)))
952 .collect(),
953 ),
954 Kind::Markdown { text, tone } => Kind::Markdown { text, tone },
955 Kind::TextArea {
956 placeholder,
957 value,
958 on_input,
959 on_submit,
960 trailing,
961 } => {
962 let g = f.clone();
963 Kind::TextArea {
964 placeholder,
965 value,
966 on_input: Box::new(move |text| g(on_input(text))),
967 on_submit: on_submit.map(f),
968 trailing: map_box(trailing),
969 }
970 }
971 Kind::Scroll { content, bottom } => Kind::Scroll {
972 content: Box::new(content.map(f)),
973 bottom,
974 },
975 })
976 }
977
978 #[doc(hidden)]
979 pub fn into_kind(self) -> Kind<M> {
980 self.0
981 }
982
983 #[doc(hidden)]
984 pub fn kind(&self) -> &Kind<M> {
985 &self.0
986 }
987}
988
989pub struct Item<M> {
991 content: Option<Node<M>>,
992 on_press: Option<M>,
993 on_secondary: Option<M>,
994 on_scroll: Option<(M, M)>,
995}
996
997impl<M> Item<M> {
998 pub fn new(content: Node<M>) -> Self {
999 Self {
1000 content: Some(content),
1001 on_press: None,
1002 on_secondary: None,
1003 on_scroll: None,
1004 }
1005 }
1006
1007 pub fn hidden() -> Self {
1009 Self {
1010 content: None,
1011 on_press: None,
1012 on_secondary: None,
1013 on_scroll: None,
1014 }
1015 }
1016
1017 pub fn on_press(mut self, message: M) -> Self {
1019 self.on_press = Some(message);
1020 self
1021 }
1022
1023 pub fn on_secondary(mut self, message: M) -> Self {
1025 self.on_secondary = Some(message);
1026 self
1027 }
1028
1029 pub fn on_scroll(mut self, up: M, down: M) -> Self {
1032 self.on_scroll = Some((up, down));
1033 self
1034 }
1035
1036 pub fn map<N: 'static>(self, f: &(impl Fn(M) -> N + Clone + 'static)) -> Item<N>
1037 where
1038 M: 'static,
1039 {
1040 Item {
1041 content: self.content.map(|content| content.map(f)),
1042 on_press: self.on_press.map(f),
1043 on_secondary: self.on_secondary.map(f),
1044 on_scroll: self.on_scroll.map(|(up, down)| (f(up), f(down))),
1045 }
1046 }
1047
1048 #[doc(hidden)]
1049 pub fn into_parts(self) -> ItemParts<M> {
1050 ItemParts {
1051 content: self.content,
1052 on_press: self.on_press,
1053 on_secondary: self.on_secondary,
1054 on_scroll: self.on_scroll,
1055 }
1056 }
1057}
1058
1059#[doc(hidden)]
1060pub struct ItemParts<M> {
1061 pub content: Option<Node<M>>,
1062 pub on_press: Option<M>,
1063 pub on_secondary: Option<M>,
1064 pub on_scroll: Option<(M, M)>,
1066}
1067
1068pub struct Panel<M> {
1071 content: Node<M>,
1072 on_open: Option<M>,
1073 on_close: Option<M>,
1074 wide: bool,
1075}
1076
1077impl<M> Panel<M> {
1078 pub fn new(content: Node<M>) -> Self {
1079 Self {
1080 content,
1081 on_open: None,
1082 on_close: None,
1083 wide: false,
1084 }
1085 }
1086
1087 pub fn wide(mut self) -> Self {
1090 self.wide = true;
1091 self
1092 }
1093
1094 #[doc(hidden)]
1095 pub fn is_wide(&self) -> bool {
1096 self.wide
1097 }
1098
1099 pub fn on_open(mut self, message: M) -> Self {
1101 self.on_open = Some(message);
1102 self
1103 }
1104
1105 pub fn on_close(mut self, message: M) -> Self {
1107 self.on_close = Some(message);
1108 self
1109 }
1110
1111 pub fn map<N: 'static>(self, f: &(impl Fn(M) -> N + Clone + 'static)) -> Panel<N>
1112 where
1113 M: 'static,
1114 {
1115 Panel {
1116 content: self.content.map(f),
1117 on_open: self.on_open.map(f),
1118 on_close: self.on_close.map(f),
1119 wide: self.wide,
1120 }
1121 }
1122
1123 #[doc(hidden)]
1124 pub fn into_node(self) -> Node<M> {
1125 self.content
1126 }
1127
1128 #[doc(hidden)]
1129 pub fn on_open_message(self) -> Option<M> {
1130 self.on_open
1131 }
1132
1133 #[doc(hidden)]
1134 pub fn on_close_message(self) -> Option<M> {
1135 self.on_close
1136 }
1137}
1138
1139#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1141pub enum Urgency {
1142 Low,
1144 #[default]
1145 Normal,
1146 Critical,
1148}
1149
1150pub struct Notice<M> {
1157 key: u64,
1158 summary: String,
1159 app: Option<String>,
1160 app_icon: Option<String>,
1161 image: Option<String>,
1162 body: Option<String>,
1163 markdown: bool,
1164 urgency: Urgency,
1165 actions: Vec<(String, M)>,
1166 on_press: Option<M>,
1167 on_dismiss: Option<M>,
1168 expires: Option<(std::time::Duration, M)>,
1169 reply: Option<Reply<M>>,
1170}
1171
1172pub struct Reply<M> {
1179 placeholder: String,
1180 value: String,
1181 on_input: std::rc::Rc<dyn Fn(String) -> M>,
1182 on_submit: Option<M>,
1183 on_cancel: Option<M>,
1184}
1185
1186impl<M> Reply<M> {
1187 pub fn new(
1189 placeholder: impl Into<String>,
1190 value: impl Into<String>,
1191 on_input: impl Fn(String) -> M + 'static,
1192 ) -> Self {
1193 Self {
1194 placeholder: placeholder.into(),
1195 value: value.into(),
1196 on_input: std::rc::Rc::new(on_input),
1197 on_submit: None,
1198 on_cancel: None,
1199 }
1200 }
1201
1202 pub fn on_submit(mut self, message: M) -> Self {
1204 self.on_submit = Some(message);
1205 self
1206 }
1207
1208 pub fn on_cancel(mut self, message: M) -> Self {
1210 self.on_cancel = Some(message);
1211 self
1212 }
1213
1214 pub fn map<N: 'static>(self, f: &(impl Fn(M) -> N + Clone + 'static)) -> Reply<N>
1215 where
1216 M: 'static,
1217 {
1218 let g = f.clone();
1219 let on_input = self.on_input;
1220 Reply {
1221 placeholder: self.placeholder,
1222 value: self.value,
1223 on_input: std::rc::Rc::new(move |text| g(on_input(text))),
1224 on_submit: self.on_submit.map(f),
1225 on_cancel: self.on_cancel.map(f),
1226 }
1227 }
1228
1229 #[doc(hidden)]
1230 pub fn into_parts(self) -> ReplyParts<M> {
1231 ReplyParts {
1232 placeholder: self.placeholder,
1233 value: self.value,
1234 on_input: self.on_input,
1235 on_submit: self.on_submit,
1236 on_cancel: self.on_cancel,
1237 }
1238 }
1239}
1240
1241#[doc(hidden)]
1242#[derive(Clone)]
1243pub struct ReplyParts<M> {
1244 pub placeholder: String,
1245 pub value: String,
1246 pub on_input: std::rc::Rc<dyn Fn(String) -> M>,
1247 pub on_submit: Option<M>,
1248 pub on_cancel: Option<M>,
1249}
1250
1251impl<M> Notice<M> {
1252 pub fn new(key: u64, summary: impl Into<String>) -> Self {
1253 Self {
1254 key,
1255 summary: summary.into(),
1256 app: None,
1257 app_icon: None,
1258 image: None,
1259 body: None,
1260 markdown: false,
1261 urgency: Urgency::Normal,
1262 actions: Vec::new(),
1263 on_press: None,
1264 on_dismiss: None,
1265 expires: None,
1266 reply: None,
1267 }
1268 }
1269
1270 pub fn app(mut self, name: impl Into<String>) -> Self {
1272 self.app = Some(name.into());
1273 self
1274 }
1275
1276 pub fn app_icon(mut self, app_id: impl Into<String>) -> Self {
1278 self.app_icon = Some(app_id.into());
1279 self
1280 }
1281
1282 pub fn image(mut self, url: impl Into<String>) -> Self {
1285 self.image = Some(url.into());
1286 self
1287 }
1288
1289 pub fn body(mut self, text: impl Into<String>) -> Self {
1291 self.body = Some(text.into());
1292 self
1293 }
1294
1295 pub fn markdown(mut self) -> Self {
1297 self.markdown = true;
1298 self
1299 }
1300
1301 pub fn urgency(mut self, urgency: Urgency) -> Self {
1302 self.urgency = urgency;
1303 self
1304 }
1305
1306 pub fn action(mut self, label: impl Into<String>, message: M) -> Self {
1308 self.actions.push((label.into(), message));
1309 self
1310 }
1311
1312 pub fn on_press(mut self, message: M) -> Self {
1314 self.on_press = Some(message);
1315 self
1316 }
1317
1318 pub fn on_dismiss(mut self, message: M) -> Self {
1320 self.on_dismiss = Some(message);
1321 self
1322 }
1323
1324 pub fn expires(mut self, after: std::time::Duration, message: M) -> Self {
1328 self.expires = Some((after, message));
1329 self
1330 }
1331
1332 pub fn reply(mut self, reply: Reply<M>) -> Self {
1334 self.reply = Some(reply);
1335 self
1336 }
1337
1338 pub fn map<N: 'static>(self, f: &(impl Fn(M) -> N + Clone + 'static)) -> Notice<N>
1339 where
1340 M: 'static,
1341 {
1342 Notice {
1343 key: self.key,
1344 summary: self.summary,
1345 app: self.app,
1346 app_icon: self.app_icon,
1347 image: self.image,
1348 body: self.body,
1349 markdown: self.markdown,
1350 urgency: self.urgency,
1351 actions: self
1352 .actions
1353 .into_iter()
1354 .map(|(label, message)| (label, f(message)))
1355 .collect(),
1356 on_press: self.on_press.map(f),
1357 on_dismiss: self.on_dismiss.map(f),
1358 expires: self.expires.map(|(after, message)| (after, f(message))),
1359 reply: self.reply.map(|reply| reply.map(f)),
1360 }
1361 }
1362
1363 pub fn key(&self) -> u64 {
1364 self.key
1365 }
1366
1367 #[doc(hidden)]
1368 pub fn into_parts(self) -> NoticeParts<M> {
1369 NoticeParts {
1370 key: self.key,
1371 summary: self.summary,
1372 app: self.app,
1373 app_icon: self.app_icon,
1374 image: self.image,
1375 body: self.body,
1376 markdown: self.markdown,
1377 urgency: self.urgency,
1378 actions: self.actions,
1379 on_press: self.on_press,
1380 on_dismiss: self.on_dismiss,
1381 expires: self.expires,
1382 reply: self.reply.map(Reply::into_parts),
1383 }
1384 }
1385}
1386
1387#[doc(hidden)]
1388#[derive(Clone)]
1389pub struct NoticeParts<M> {
1390 pub key: u64,
1391 pub summary: String,
1392 pub app: Option<String>,
1393 pub app_icon: Option<String>,
1394 pub image: Option<String>,
1395 pub body: Option<String>,
1396 pub markdown: bool,
1397 pub urgency: Urgency,
1398 pub actions: Vec<(String, M)>,
1399 pub on_press: Option<M>,
1400 pub on_dismiss: Option<M>,
1401 pub expires: Option<(std::time::Duration, M)>,
1402 pub reply: Option<ReplyParts<M>>,
1403}
1404
1405#[cfg(test)]
1406mod tests {
1407 use super::*;
1408
1409 #[test]
1410 fn map_converts_every_message() {
1411 let node = row([
1412 button(label("a"), 1).on_secondary(4),
1413 column([button(label("b"), 2), label("c")]),
1414 switcher([(label("d"), 3)], Some(0)),
1415 ]);
1416
1417 let Kind::Row(children) = node.map(&|n: i32| n * 10).into_kind() else {
1418 panic!("row became something else");
1419 };
1420 let mut messages = Vec::new();
1421 collect(children, &mut messages);
1422 assert_eq!(messages, [10, 40, 20, 30]);
1423 }
1424
1425 fn collect(nodes: Vec<Node<i32>>, out: &mut Vec<i32>) {
1426 for node in nodes {
1427 match node.into_kind() {
1428 Kind::Button {
1429 on_press,
1430 on_secondary,
1431 ..
1432 } => {
1433 out.push(on_press);
1434 out.extend(on_secondary);
1435 }
1436 Kind::Switcher { options, .. } => {
1437 out.extend(options.into_iter().map(|(_, message)| message))
1438 }
1439 Kind::Row(children) | Kind::Column(children) => collect(children, out),
1440 _ => {}
1441 }
1442 }
1443 }
1444
1445 #[test]
1446 fn hover_shows_the_second_option() {
1447 let node: Node<()> = hover(label("a"), label("b"));
1448 assert!(matches!(
1449 node.into_kind(),
1450 Kind::Alternate {
1451 current: 0,
1452 hovered: Some(1),
1453 ..
1454 }
1455 ));
1456 let node: Node<()> = label("a").on_hover(1);
1457 assert!(matches!(node.into_kind(), Kind::Label { .. }));
1458 }
1459
1460 #[test]
1461 fn tone_fills_only_unset_text() {
1462 let node: Node<()> =
1463 row([label("a").tone(Tone::Accent), label("b"), icon(Icon::Wired)]).tone(Tone::Dim);
1464 let Kind::Row(children) = node.into_kind() else {
1465 unreachable!()
1466 };
1467 let tones: Vec<_> = children
1468 .into_iter()
1469 .map(|child| match child.into_kind() {
1470 Kind::Label { tone, .. } | Kind::Icon { tone, .. } => tone,
1471 _ => None,
1472 })
1473 .collect();
1474 assert_eq!(
1475 tones,
1476 [Some(Tone::Accent), Some(Tone::Dim), Some(Tone::Dim)]
1477 );
1478 }
1479
1480 #[test]
1481 fn map_reaches_new_nodes_and_their_closures() {
1482 let node = column([
1483 header("h").on_back(1).trailing(toggle(true, 2)),
1484 list([list_item("a")
1485 .on_press(3)
1486 .on_secondary(4)
1487 .trailing(button(icon(Icon::Info), 5))]),
1488 slider(0.5, 0.0..=1.0, |v| (v * 10.0) as i32).on_release(6),
1489 text_input("p", "v", |text: String| text.len() as i32).on_submit(7),
1490 scroll(text_area("p", "v", |text: String| text.len() as i32 + 1).on_submit(8)).bottom(),
1491 ]);
1492 let Kind::Column(children) = node.map(&|n: i32| n * 10).into_kind() else {
1493 panic!("column became something else");
1494 };
1495 let mut children = children.into_iter().map(Node::into_kind);
1496 let Some(Kind::Header {
1497 on_back: Some(10),
1498 trailing: Some(trailing),
1499 ..
1500 }) = children.next()
1501 else {
1502 panic!("header lost its back message");
1503 };
1504 assert!(matches!(
1505 trailing.into_kind(),
1506 Kind::Toggle { on_toggle: 20, .. }
1507 ));
1508 let Some(Kind::List(rows)) = children.next() else {
1509 panic!("list lost");
1510 };
1511 let Some(Kind::ListItem {
1512 on_press: Some(30),
1513 on_secondary: Some(40),
1514 trailing: Some(trailing),
1515 ..
1516 }) = rows.into_iter().next().map(Node::into_kind)
1517 else {
1518 panic!("list item lost its messages");
1519 };
1520 assert!(matches!(
1521 trailing.into_kind(),
1522 Kind::Button { on_press: 50, .. }
1523 ));
1524 let Some(Kind::Slider {
1525 on_change,
1526 on_release: Some(60),
1527 ..
1528 }) = children.next()
1529 else {
1530 panic!("slider lost");
1531 };
1532 assert_eq!(on_change(0.3), 30);
1533 let Some(Kind::TextInput {
1534 on_input,
1535 on_submit: Some(70),
1536 ..
1537 }) = children.next()
1538 else {
1539 panic!("text input lost");
1540 };
1541 assert_eq!(on_input("abcd".into()), 40);
1542 let Some(Kind::Scroll {
1543 content,
1544 bottom: true,
1545 }) = children.next()
1546 else {
1547 panic!("scroll lost");
1548 };
1549 let Kind::TextArea {
1550 on_input,
1551 on_submit: Some(80),
1552 ..
1553 } = content.into_kind()
1554 else {
1555 panic!("text area lost");
1556 };
1557 assert_eq!(on_input("abcd".into()), 50);
1558 }
1559
1560 #[test]
1561 fn tabs_map_and_tone_every_page() {
1562 let node = tabs([("a", button(label("x"), 1)), ("b", button(label("y"), 2))])
1563 .tone(Tone::Dim)
1564 .map(&|n: i32| n * 10);
1565 let Kind::Tabs(pages) = node.into_kind() else {
1566 panic!("tabs became something else");
1567 };
1568 let names: Vec<_> = pages.iter().map(|(name, _)| name.as_str()).collect();
1569 assert_eq!(names, ["a", "b"]);
1570 let mut messages = Vec::new();
1571 for (_, page) in pages {
1572 let Kind::Button {
1573 content, on_press, ..
1574 } = page.into_kind()
1575 else {
1576 panic!("page lost its button");
1577 };
1578 assert!(matches!(
1579 content.into_kind(),
1580 Kind::Label {
1581 tone: Some(Tone::Dim),
1582 ..
1583 }
1584 ));
1585 messages.push(on_press);
1586 }
1587 assert_eq!(messages, [10, 20]);
1588 }
1589
1590 #[test]
1591 fn notice_map_reaches_every_message() {
1592 let notice = Notice::new(7, "s")
1593 .action("a", 1)
1594 .action("b", 2)
1595 .on_press(3)
1596 .on_dismiss(4)
1597 .expires(std::time::Duration::from_secs(5), 5)
1598 .reply(
1599 Reply::new("p", "v", |text: String| text.len() as i32)
1600 .on_submit(6)
1601 .on_cancel(7),
1602 )
1603 .map(&|n: i32| n * 10);
1604 assert_eq!(notice.key(), 7);
1605 let parts = notice.into_parts();
1606 let actions: Vec<_> = parts.actions.into_iter().map(|(_, m)| m).collect();
1607 assert_eq!(actions, [10, 20]);
1608 assert_eq!(parts.on_press, Some(30));
1609 assert_eq!(parts.on_dismiss, Some(40));
1610 assert_eq!(parts.expires.map(|(_, m)| m), Some(50));
1611 let reply = parts.reply.unwrap();
1612 assert_eq!((reply.on_input)("abc".into()), 30);
1613 assert_eq!((reply.on_submit, reply.on_cancel), (Some(60), Some(70)));
1614 }
1615
1616 #[test]
1617 fn modifiers_only_touch_their_nodes() {
1618 let node: Node<i32> = label("a").trailing(label("b")).on_press(1).selected(true);
1619 assert!(matches!(node.into_kind(), Kind::Label { .. }));
1620 let node: Node<i32> = button(label("a"), 1).on_press(2).prominent();
1621 assert!(matches!(
1622 node.into_kind(),
1623 Kind::Button {
1624 on_press: 1,
1625 prominent: true,
1626 ..
1627 }
1628 ));
1629 }
1630
1631 #[test]
1632 fn tone_reaches_details_and_row_icons() {
1633 let node: Node<()> = column([
1634 detail("n", label("v")),
1635 list_item("t").leading(icon(Icon::Lock)),
1636 ])
1637 .tone(Tone::Good);
1638 let Kind::Column(children) = node.into_kind() else {
1639 unreachable!()
1640 };
1641 let mut children = children.into_iter().map(Node::into_kind);
1642 let Some(Kind::Detail { value, .. }) = children.next() else {
1643 unreachable!()
1644 };
1645 assert!(matches!(
1646 value.into_kind(),
1647 Kind::Label {
1648 tone: Some(Tone::Good),
1649 ..
1650 }
1651 ));
1652 let Some(Kind::ListItem {
1653 leading: Some(leading),
1654 ..
1655 }) = children.next()
1656 else {
1657 unreachable!()
1658 };
1659 assert!(matches!(
1660 leading.into_kind(),
1661 Kind::Icon {
1662 tone: Some(Tone::Good),
1663 ..
1664 }
1665 ));
1666 }
1667}