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 Brightness { level: f32 },
68 KeyboardLight,
70 Media { playing: bool },
73 Play,
75 Pause,
77 Previous,
79 Next,
81}
82
83#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
85pub enum Mark {
86 #[default]
88 Note,
89 You,
91 Running,
93 Done,
95 Failed,
97 Waiting,
99}
100
101#[derive(Debug, Clone, Copy, PartialEq, Eq)]
103pub enum Change {
104 Context,
106 Added,
107 Removed,
108 Hunk,
110}
111
112pub struct Node<M>(Kind<M>);
114
115#[doc(hidden)]
116pub enum Kind<M> {
117 Label {
118 text: String,
119 tone: Option<Tone>,
120 },
121 Heading {
122 text: String,
123 tone: Option<Tone>,
124 },
125 Icon {
126 icon: Icon,
127 tone: Option<Tone>,
128 },
129 AppIcon {
130 app_id: String,
131 tone: Option<Tone>,
132 },
133 Image {
134 file: &'static [u8],
135 tint: bool,
136 tone: Option<Tone>,
137 },
138 Sprite {
139 frames: &'static [&'static [u8]],
140 order: &'static [u8],
142 looping: bool,
144 tone: Option<Tone>,
145 },
146 Cover {
147 url: String,
148 fallback: Box<Node<M>>,
150 large: bool,
152 tone: Option<Tone>,
153 },
154 Counter {
155 value: i64,
156 range: RangeInclusive<i64>,
157 unit: String,
158 tone: Option<Tone>,
159 },
160 Alternate {
161 options: Vec<Node<M>>,
162 current: usize,
163 hovered: Option<usize>,
165 grows: bool,
167 },
168 Row(Vec<Node<M>>),
169 Column(Vec<Node<M>>),
170 Grid {
171 columns: usize,
172 cells: Vec<Node<M>>,
173 },
174 Button {
175 content: Box<Node<M>>,
176 on_press: M,
177 on_secondary: Option<M>,
178 prominent: bool,
180 },
181 Switcher {
182 options: Vec<(Node<M>, M)>,
183 current: Option<usize>,
184 },
185 Gulp(Box<Node<M>>),
186 Center(Box<Node<M>>),
188 Separator,
189 Empty,
190 ListItem {
191 title: String,
192 subtitle: Option<String>,
193 leading: Option<Box<Node<M>>>,
194 trailing: Option<Box<Node<M>>>,
195 selected: bool,
196 on_press: Option<M>,
197 on_secondary: Option<M>,
198 },
199 List(Vec<Node<M>>),
200 Section {
201 title: String,
202 trailing: Option<Box<Node<M>>>,
203 },
204 Header {
205 title: String,
206 subtitle: Option<String>,
207 on_back: Option<M>,
208 leading: Option<Box<Node<M>>>,
209 trailing: Option<Box<Node<M>>>,
210 },
211 Detail {
212 name: String,
213 value: Box<Node<M>>,
214 },
215 Fill,
216 Slider {
217 value: f32,
218 range: RangeInclusive<f32>,
219 step: f32,
220 on_change: Box<dyn Fn(f32) -> M>,
221 on_release: Option<M>,
222 },
223 Toggle {
224 on: bool,
225 on_toggle: M,
226 },
227 Meter {
228 fraction: f32,
229 limit: Option<f32>,
231 tone: Option<Tone>,
232 },
233 TextInput {
234 placeholder: String,
235 value: String,
236 on_input: Box<dyn Fn(String) -> M>,
237 on_submit: Option<M>,
238 secret: bool,
239 trailing: Option<Box<Node<M>>>,
240 tone: Option<Tone>,
241 },
242 Display(Box<Node<M>>),
243 Mark(Box<Node<M>>),
244 Counted {
245 content: Box<Node<M>>,
246 count: u32,
247 },
248 Badge {
249 text: String,
250 tone: Option<Tone>,
251 },
252 Tabs(Vec<(String, Node<M>)>),
254 Markdown {
255 text: String,
256 tone: Option<Tone>,
257 },
258 TextArea {
259 placeholder: String,
260 value: String,
261 on_input: Box<dyn Fn(String) -> M>,
262 on_submit: Option<M>,
263 trailing: Option<Box<Node<M>>>,
264 },
265 Scroll {
266 content: Box<Node<M>>,
267 bottom: bool,
269 },
270 Literal {
271 text: String,
272 tone: Option<Tone>,
273 },
274 Divider {
275 text: String,
276 },
277 Timeline(Vec<Node<M>>),
278 Step {
279 mark: Mark,
280 leading: Option<Box<Node<M>>>,
282 content: Box<Node<M>>,
283 },
284 Diff {
285 title: String,
286 lines: Vec<(Change, String)>,
287 },
288}
289
290pub fn label<M>(text: impl Into<String>) -> Node<M> {
291 Node(Kind::Label {
292 text: text.into(),
293 tone: None,
294 })
295}
296
297pub fn heading<M>(text: impl Into<String>) -> Node<M> {
299 Node(Kind::Heading {
300 text: text.into(),
301 tone: None,
302 })
303}
304
305pub fn icon<M>(icon: Icon) -> Node<M> {
307 Node(Kind::Icon { icon, tone: None })
308}
309
310pub fn app_icon<M>(app_id: impl Into<String>) -> Node<M> {
314 Node(Kind::AppIcon {
315 app_id: app_id.into(),
316 tone: None,
317 })
318}
319
320pub fn image<M>(file: &'static [u8]) -> Node<M> {
325 Node(Kind::Image {
326 file,
327 tint: false,
328 tone: None,
329 })
330}
331
332pub fn symbol<M>(file: &'static [u8]) -> Node<M> {
335 Node(Kind::Image {
336 file,
337 tint: true,
338 tone: None,
339 })
340}
341
342pub fn sprite<M>(frames: &'static [&'static [u8]], order: &'static [u8]) -> Node<M> {
350 Node(Kind::Sprite {
351 frames,
352 order,
353 looping: false,
354 tone: None,
355 })
356}
357
358pub fn cover<M>(url: impl Into<String>, fallback: Node<M>) -> Node<M> {
363 Node(Kind::Cover {
364 url: url.into(),
365 fallback: Box::new(fallback),
366 large: false,
367 tone: None,
368 })
369}
370
371pub fn counter<M>(value: i64, range: RangeInclusive<i64>, unit: impl Into<String>) -> Node<M> {
375 Node(Kind::Counter {
376 value,
377 range,
378 unit: unit.into(),
379 tone: None,
380 })
381}
382
383pub fn alternate<M>(options: impl IntoIterator<Item = Node<M>>, current: usize) -> Node<M> {
388 Node(Kind::Alternate {
389 options: options.into_iter().collect(),
390 current,
391 hovered: None,
392 grows: false,
393 })
394}
395
396pub fn hover<M>(normal: Node<M>, hovered: Node<M>) -> Node<M> {
399 alternate([normal, hovered], 0).on_hover(1)
400}
401
402pub fn expand<M>(collapsed: Node<M>, expanded: Node<M>) -> Node<M> {
407 Node(Kind::Alternate {
408 options: vec![collapsed, expanded],
409 current: 0,
410 hovered: Some(1),
411 grows: true,
412 })
413}
414
415pub fn row<M>(children: impl IntoIterator<Item = Node<M>>) -> Node<M> {
417 Node(Kind::Row(children.into_iter().collect()))
418}
419
420pub fn column<M>(children: impl IntoIterator<Item = Node<M>>) -> Node<M> {
422 Node(Kind::Column(children.into_iter().collect()))
423}
424
425pub fn center<M>(content: Node<M>) -> Node<M> {
429 Node(Kind::Center(Box::new(content)))
430}
431
432pub fn grid<M>(columns: usize, cells: impl IntoIterator<Item = Node<M>>) -> Node<M> {
434 Node(Kind::Grid {
435 columns: columns.max(1),
436 cells: cells.into_iter().collect(),
437 })
438}
439
440pub fn button<M>(content: Node<M>, on_press: M) -> Node<M> {
441 Node(Kind::Button {
442 content: Box::new(content),
443 on_press,
444 on_secondary: None,
445 prominent: false,
446 })
447}
448
449pub fn switcher<M>(
453 options: impl IntoIterator<Item = (Node<M>, M)>,
454 current: Option<usize>,
455) -> Node<M> {
456 Node(Kind::Switcher {
457 options: options.into_iter().collect(),
458 current,
459 })
460}
461
462pub fn gulp<M>(content: Node<M>) -> Node<M> {
465 Node(Kind::Gulp(Box::new(content)))
466}
467
468pub fn separator<M>() -> Node<M> {
470 Node(Kind::Separator)
471}
472
473pub fn empty<M>() -> Node<M> {
475 Node(Kind::Empty)
476}
477
478pub fn list_item<M>(title: impl Into<String>) -> Node<M> {
483 Node(Kind::ListItem {
484 title: title.into(),
485 subtitle: None,
486 leading: None,
487 trailing: None,
488 selected: false,
489 on_press: None,
490 on_secondary: None,
491 })
492}
493
494pub fn list<M>(rows: impl IntoIterator<Item = Node<M>>) -> Node<M> {
497 Node(Kind::List(rows.into_iter().collect()))
498}
499
500pub fn section<M>(title: impl Into<String>) -> Node<M> {
503 Node(Kind::Section {
504 title: title.into(),
505 trailing: None,
506 })
507}
508
509pub fn header<M>(title: impl Into<String>) -> Node<M> {
514 Node(Kind::Header {
515 title: title.into(),
516 subtitle: None,
517 on_back: None,
518 leading: None,
519 trailing: None,
520 })
521}
522
523pub fn detail<M>(name: impl Into<String>, value: Node<M>) -> Node<M> {
525 Node(Kind::Detail {
526 name: name.into(),
527 value: Box::new(value),
528 })
529}
530
531pub fn fill<M>() -> Node<M> {
533 Node(Kind::Fill)
534}
535
536pub fn slider<M: 'static>(
539 value: f32,
540 range: RangeInclusive<f32>,
541 on_change: impl Fn(f32) -> M + 'static,
542) -> Node<M> {
543 Node(Kind::Slider {
544 value,
545 range,
546 step: 1.0,
547 on_change: Box::new(on_change),
548 on_release: None,
549 })
550}
551
552pub fn toggle<M>(on: bool, on_toggle: M) -> Node<M> {
554 Node(Kind::Toggle { on, on_toggle })
555}
556
557pub fn meter<M>(fraction: f32) -> Node<M> {
560 Node(Kind::Meter {
561 fraction,
562 limit: None,
563 tone: None,
564 })
565}
566
567pub fn text_input<M: 'static>(
572 placeholder: impl Into<String>,
573 value: impl Into<String>,
574 on_input: impl Fn(String) -> M + 'static,
575) -> Node<M> {
576 Node(Kind::TextInput {
577 placeholder: placeholder.into(),
578 value: value.into(),
579 on_input: Box::new(on_input),
580 on_submit: None,
581 secret: false,
582 trailing: None,
583 tone: None,
584 })
585}
586
587pub fn text_area<M: 'static>(
592 placeholder: impl Into<String>,
593 value: impl Into<String>,
594 on_input: impl Fn(String) -> M + 'static,
595) -> Node<M> {
596 Node(Kind::TextArea {
597 placeholder: placeholder.into(),
598 value: value.into(),
599 on_input: Box::new(on_input),
600 on_submit: None,
601 trailing: None,
602 })
603}
604
605pub fn markdown<M>(text: impl Into<String>) -> Node<M> {
610 Node(Kind::Markdown {
611 text: text.into(),
612 tone: None,
613 })
614}
615
616pub fn scroll<M>(content: Node<M>) -> Node<M> {
620 Node(Kind::Scroll {
621 content: Box::new(content),
622 bottom: false,
623 })
624}
625
626pub fn literal<M>(text: impl Into<String>) -> Node<M> {
629 Node(Kind::Literal {
630 text: text.into(),
631 tone: None,
632 })
633}
634
635pub fn divider<M>(text: impl Into<String>) -> Node<M> {
638 Node(Kind::Divider { text: text.into() })
639}
640
641pub fn timeline<M>(steps: impl IntoIterator<Item = Node<M>>) -> Node<M> {
645 Node(Kind::Timeline(steps.into_iter().collect()))
646}
647
648pub fn step<M>(mark: Mark, content: Node<M>) -> Node<M> {
652 Node(Kind::Step {
653 mark,
654 leading: None,
655 content: Box::new(content),
656 })
657}
658
659pub fn diff<M>(
662 title: impl Into<String>,
663 lines: impl IntoIterator<Item = (Change, String)>,
664) -> Node<M> {
665 Node(Kind::Diff {
666 title: title.into(),
667 lines: lines.into_iter().collect(),
668 })
669}
670
671pub fn display<M>(content: Node<M>) -> Node<M> {
673 Node(Kind::Display(Box::new(content)))
674}
675
676pub fn mark<M>(content: Node<M>) -> Node<M> {
678 Node(Kind::Mark(Box::new(content)))
679}
680
681pub fn counted<M>(content: Node<M>, count: u32) -> Node<M> {
685 Node(Kind::Counted {
686 content: Box::new(content),
687 count,
688 })
689}
690
691pub fn badge<M>(text: impl Into<String>) -> Node<M> {
693 Node(Kind::Badge {
694 text: text.into(),
695 tone: None,
696 })
697}
698
699pub fn tabs<M>(pages: impl IntoIterator<Item = (impl Into<String>, Node<M>)>) -> Node<M> {
703 Node(Kind::Tabs(
704 pages
705 .into_iter()
706 .map(|(name, page)| (name.into(), page))
707 .collect(),
708 ))
709}
710
711impl<M> Node<M> {
712 pub fn tone(mut self, tone: Tone) -> Self {
714 self.set_tone(tone);
715 self
716 }
717
718 pub fn on_hover(mut self, index: usize) -> Self {
721 if let Kind::Alternate { hovered, .. } = &mut self.0 {
722 *hovered = Some(index);
723 }
724 self
725 }
726
727 pub fn on_secondary(mut self, message: M) -> Self {
730 if let Kind::Button { on_secondary, .. } | Kind::ListItem { on_secondary, .. } = &mut self.0
731 {
732 *on_secondary = Some(message);
733 }
734 self
735 }
736
737 pub fn prominent(mut self) -> Self {
740 if let Kind::Button { prominent, .. } = &mut self.0 {
741 *prominent = true;
742 }
743 self
744 }
745
746 pub fn on_press(mut self, message: M) -> Self {
748 if let Kind::ListItem { on_press, .. } = &mut self.0 {
749 *on_press = Some(message);
750 }
751 self
752 }
753
754 pub fn subtitle(mut self, text: impl Into<String>) -> Self {
757 if let Kind::ListItem { subtitle, .. } | Kind::Header { subtitle, .. } = &mut self.0 {
758 *subtitle = Some(text.into());
759 }
760 self
761 }
762
763 pub fn leading(mut self, node: Node<M>) -> Self {
766 if let Kind::ListItem { leading, .. }
767 | Kind::Header { leading, .. }
768 | Kind::Step { leading, .. } = &mut self.0
769 {
770 *leading = Some(Box::new(node));
771 }
772 self
773 }
774
775 pub fn trailing(mut self, node: Node<M>) -> Self {
778 if let Kind::ListItem { trailing, .. }
779 | Kind::Section { trailing, .. }
780 | Kind::Header { trailing, .. }
781 | Kind::TextInput { trailing, .. }
782 | Kind::TextArea { trailing, .. } = &mut self.0
783 {
784 *trailing = Some(Box::new(node));
785 }
786 self
787 }
788
789 pub fn selected(mut self, yes: bool) -> Self {
791 if let Kind::ListItem { selected, .. } = &mut self.0 {
792 *selected = yes;
793 }
794 self
795 }
796
797 pub fn on_back(mut self, message: M) -> Self {
799 if let Kind::Header { on_back, .. } = &mut self.0 {
800 *on_back = Some(message);
801 }
802 self
803 }
804
805 pub fn step(mut self, by: f32) -> Self {
807 if let Kind::Slider { step, .. } = &mut self.0 {
808 *step = by;
809 }
810 self
811 }
812
813 pub fn on_release(mut self, message: M) -> Self {
815 if let Kind::Slider { on_release, .. } = &mut self.0 {
816 *on_release = Some(message);
817 }
818 self
819 }
820
821 pub fn on_submit(mut self, message: M) -> Self {
823 if let Kind::TextInput { on_submit, .. } | Kind::TextArea { on_submit, .. } = &mut self.0 {
824 *on_submit = Some(message);
825 }
826 self
827 }
828
829 pub fn secret(mut self, yes: bool) -> Self {
831 if let Kind::TextInput { secret, .. } = &mut self.0 {
832 *secret = yes;
833 }
834 self
835 }
836
837 pub fn looping(mut self) -> Self {
841 if let Kind::Sprite { looping, .. } = &mut self.0 {
842 *looping = true;
843 }
844 self
845 }
846
847 pub fn large(mut self) -> Self {
850 if let Kind::Cover { large, .. } = &mut self.0 {
851 *large = true;
852 }
853 self
854 }
855
856 pub fn bottom(mut self) -> Self {
859 if let Kind::Scroll { bottom, .. } = &mut self.0 {
860 *bottom = true;
861 }
862 self
863 }
864
865 pub fn limit(mut self, at: f32) -> Self {
867 if let Kind::Meter { limit, .. } = &mut self.0 {
868 *limit = Some(at);
869 }
870 self
871 }
872
873 fn set_tone(&mut self, new: Tone) {
874 match &mut self.0 {
875 Kind::Label { tone, .. }
876 | Kind::Heading { tone, .. }
877 | Kind::Icon { tone, .. }
878 | Kind::AppIcon { tone, .. }
879 | Kind::Image { tone, .. }
880 | Kind::Sprite { tone, .. }
881 | Kind::Counter { tone, .. }
882 | Kind::Meter { tone, .. }
883 | Kind::TextInput { tone, .. }
884 | Kind::Markdown { tone, .. }
885 | Kind::Literal { tone, .. }
886 | Kind::Badge { tone, .. } => {
887 tone.get_or_insert(new);
888 }
889 Kind::Cover { tone, fallback, .. } => {
890 tone.get_or_insert(new);
891 fallback.set_tone(new);
892 }
893 Kind::Alternate {
894 options: children, ..
895 }
896 | Kind::Row(children)
897 | Kind::Column(children)
898 | Kind::Grid {
899 cells: children, ..
900 }
901 | Kind::List(children)
902 | Kind::Timeline(children) => {
903 children.iter_mut().for_each(|child| child.set_tone(new));
904 }
905 Kind::Step {
906 leading, content, ..
907 } => {
908 leading.iter_mut().for_each(|child| child.set_tone(new));
909 content.set_tone(new);
910 }
911 Kind::Button { content, .. }
912 | Kind::Gulp(content)
913 | Kind::Center(content)
914 | Kind::Display(content)
915 | Kind::Mark(content)
916 | Kind::Counted { content, .. }
917 | Kind::Scroll { content, .. }
918 | Kind::Detail { value: content, .. } => content.set_tone(new),
919 Kind::ListItem {
920 leading, trailing, ..
921 }
922 | Kind::Header {
923 leading, trailing, ..
924 } => {
925 leading
926 .iter_mut()
927 .chain(trailing.iter_mut())
928 .for_each(|child| child.set_tone(new));
929 }
930 Kind::Tabs(pages) => {
931 for (_, page) in pages {
932 page.set_tone(new);
933 }
934 }
935 Kind::Switcher { options, .. } => {
936 options
937 .iter_mut()
938 .for_each(|(child, _)| child.set_tone(new));
939 }
940 Kind::Separator
941 | Kind::Divider { .. }
942 | Kind::Diff { .. }
943 | Kind::Empty
944 | Kind::Section { .. }
945 | Kind::Fill
946 | Kind::Slider { .. }
947 | Kind::TextArea { .. }
948 | Kind::Toggle { .. } => {}
949 }
950 }
951
952 pub fn map<N: 'static>(self, f: &(impl Fn(M) -> N + Clone + 'static)) -> Node<N>
956 where
957 M: 'static,
958 {
959 let map_all = |nodes: Vec<Node<M>>| nodes.into_iter().map(|node| node.map(f)).collect();
960 let map_box = |node: Option<Box<Node<M>>>| node.map(|node| Box::new(node.map(f)));
961
962 Node(match self.0 {
963 Kind::Label { text, tone } => Kind::Label { text, tone },
964 Kind::Heading { text, tone } => Kind::Heading { text, tone },
965 Kind::Icon { icon, tone } => Kind::Icon { icon, tone },
966 Kind::AppIcon { app_id, tone } => Kind::AppIcon { app_id, tone },
967 Kind::Image { file, tint, tone } => Kind::Image { file, tint, tone },
968 Kind::Sprite {
969 frames,
970 order,
971 looping,
972 tone,
973 } => Kind::Sprite {
974 frames,
975 order,
976 looping,
977 tone,
978 },
979 Kind::Cover {
980 url,
981 fallback,
982 large,
983 tone,
984 } => Kind::Cover {
985 url,
986 fallback: Box::new(fallback.map(f)),
987 large,
988 tone,
989 },
990 Kind::Counter {
991 value,
992 range,
993 unit,
994 tone,
995 } => Kind::Counter {
996 value,
997 range,
998 unit,
999 tone,
1000 },
1001 Kind::Alternate {
1002 options,
1003 current,
1004 hovered,
1005 grows,
1006 } => Kind::Alternate {
1007 options: map_all(options),
1008 current,
1009 hovered,
1010 grows,
1011 },
1012 Kind::Row(children) => Kind::Row(map_all(children)),
1013 Kind::Column(children) => Kind::Column(map_all(children)),
1014 Kind::Grid { columns, cells } => Kind::Grid {
1015 columns,
1016 cells: map_all(cells),
1017 },
1018 Kind::Button {
1019 content,
1020 on_press,
1021 on_secondary,
1022 prominent,
1023 } => Kind::Button {
1024 content: Box::new(content.map(f)),
1025 on_press: f(on_press),
1026 on_secondary: on_secondary.map(f),
1027 prominent,
1028 },
1029 Kind::Switcher { options, current } => Kind::Switcher {
1030 options: options
1031 .into_iter()
1032 .map(|(node, message)| (node.map(f), f(message)))
1033 .collect(),
1034 current,
1035 },
1036 Kind::Gulp(content) => Kind::Gulp(Box::new(content.map(f))),
1037 Kind::Center(content) => Kind::Center(Box::new(content.map(f))),
1038 Kind::Separator => Kind::Separator,
1039 Kind::Empty => Kind::Empty,
1040 Kind::ListItem {
1041 title,
1042 subtitle,
1043 leading,
1044 trailing,
1045 selected,
1046 on_press,
1047 on_secondary,
1048 } => Kind::ListItem {
1049 title,
1050 subtitle,
1051 leading: map_box(leading),
1052 trailing: map_box(trailing),
1053 selected,
1054 on_press: on_press.map(f),
1055 on_secondary: on_secondary.map(f),
1056 },
1057 Kind::List(rows) => Kind::List(map_all(rows)),
1058 Kind::Section { title, trailing } => Kind::Section {
1059 title,
1060 trailing: map_box(trailing),
1061 },
1062 Kind::Header {
1063 title,
1064 subtitle,
1065 on_back,
1066 leading,
1067 trailing,
1068 } => Kind::Header {
1069 title,
1070 subtitle,
1071 on_back: on_back.map(f),
1072 leading: map_box(leading),
1073 trailing: map_box(trailing),
1074 },
1075 Kind::Detail { name, value } => Kind::Detail {
1076 name,
1077 value: Box::new(value.map(f)),
1078 },
1079 Kind::Fill => Kind::Fill,
1080 Kind::Slider {
1081 value,
1082 range,
1083 step,
1084 on_change,
1085 on_release,
1086 } => {
1087 let g = f.clone();
1088 Kind::Slider {
1089 value,
1090 range,
1091 step,
1092 on_change: Box::new(move |value| g(on_change(value))),
1093 on_release: on_release.map(f),
1094 }
1095 }
1096 Kind::Toggle { on, on_toggle } => Kind::Toggle {
1097 on,
1098 on_toggle: f(on_toggle),
1099 },
1100 Kind::Meter {
1101 fraction,
1102 limit,
1103 tone,
1104 } => Kind::Meter {
1105 fraction,
1106 limit,
1107 tone,
1108 },
1109 Kind::TextInput {
1110 placeholder,
1111 value,
1112 on_input,
1113 on_submit,
1114 secret,
1115 trailing,
1116 tone,
1117 } => {
1118 let g = f.clone();
1119 Kind::TextInput {
1120 placeholder,
1121 value,
1122 on_input: Box::new(move |text| g(on_input(text))),
1123 on_submit: on_submit.map(f),
1124 secret,
1125 trailing: map_box(trailing),
1126 tone,
1127 }
1128 }
1129 Kind::Display(content) => Kind::Display(Box::new(content.map(f))),
1130 Kind::Mark(content) => Kind::Mark(Box::new(content.map(f))),
1131 Kind::Counted { content, count } => Kind::Counted {
1132 content: Box::new(content.map(f)),
1133 count,
1134 },
1135 Kind::Badge { text, tone } => Kind::Badge { text, tone },
1136 Kind::Tabs(pages) => Kind::Tabs(
1137 pages
1138 .into_iter()
1139 .map(|(name, page)| (name, page.map(f)))
1140 .collect(),
1141 ),
1142 Kind::Markdown { text, tone } => Kind::Markdown { text, tone },
1143 Kind::TextArea {
1144 placeholder,
1145 value,
1146 on_input,
1147 on_submit,
1148 trailing,
1149 } => {
1150 let g = f.clone();
1151 Kind::TextArea {
1152 placeholder,
1153 value,
1154 on_input: Box::new(move |text| g(on_input(text))),
1155 on_submit: on_submit.map(f),
1156 trailing: map_box(trailing),
1157 }
1158 }
1159 Kind::Scroll { content, bottom } => Kind::Scroll {
1160 content: Box::new(content.map(f)),
1161 bottom,
1162 },
1163 Kind::Literal { text, tone } => Kind::Literal { text, tone },
1164 Kind::Divider { text } => Kind::Divider { text },
1165 Kind::Timeline(steps) => Kind::Timeline(map_all(steps)),
1166 Kind::Step {
1167 mark,
1168 leading,
1169 content,
1170 } => Kind::Step {
1171 mark,
1172 leading: map_box(leading),
1173 content: Box::new(content.map(f)),
1174 },
1175 Kind::Diff { title, lines } => Kind::Diff { title, lines },
1176 })
1177 }
1178
1179 #[doc(hidden)]
1180 pub fn into_kind(self) -> Kind<M> {
1181 self.0
1182 }
1183
1184 #[doc(hidden)]
1185 pub fn kind(&self) -> &Kind<M> {
1186 &self.0
1187 }
1188}
1189
1190pub struct Item<M> {
1192 content: Option<Node<M>>,
1193 on_press: Option<M>,
1194 on_secondary: Option<M>,
1195 on_scroll: Option<(M, M)>,
1196}
1197
1198impl<M> Item<M> {
1199 pub fn new(content: Node<M>) -> Self {
1200 Self {
1201 content: Some(content),
1202 on_press: None,
1203 on_secondary: None,
1204 on_scroll: None,
1205 }
1206 }
1207
1208 pub fn hidden() -> Self {
1210 Self {
1211 content: None,
1212 on_press: None,
1213 on_secondary: None,
1214 on_scroll: None,
1215 }
1216 }
1217
1218 pub fn on_press(mut self, message: M) -> Self {
1220 self.on_press = Some(message);
1221 self
1222 }
1223
1224 pub fn on_secondary(mut self, message: M) -> Self {
1226 self.on_secondary = Some(message);
1227 self
1228 }
1229
1230 pub fn on_scroll(mut self, up: M, down: M) -> Self {
1233 self.on_scroll = Some((up, down));
1234 self
1235 }
1236
1237 pub fn map<N: 'static>(self, f: &(impl Fn(M) -> N + Clone + 'static)) -> Item<N>
1238 where
1239 M: 'static,
1240 {
1241 Item {
1242 content: self.content.map(|content| content.map(f)),
1243 on_press: self.on_press.map(f),
1244 on_secondary: self.on_secondary.map(f),
1245 on_scroll: self.on_scroll.map(|(up, down)| (f(up), f(down))),
1246 }
1247 }
1248
1249 #[doc(hidden)]
1250 pub fn into_parts(self) -> ItemParts<M> {
1251 ItemParts {
1252 content: self.content,
1253 on_press: self.on_press,
1254 on_secondary: self.on_secondary,
1255 on_scroll: self.on_scroll,
1256 }
1257 }
1258}
1259
1260#[doc(hidden)]
1261pub struct ItemParts<M> {
1262 pub content: Option<Node<M>>,
1263 pub on_press: Option<M>,
1264 pub on_secondary: Option<M>,
1265 pub on_scroll: Option<(M, M)>,
1267}
1268
1269pub struct Panel<M> {
1272 content: Node<M>,
1273 on_open: Option<M>,
1274 on_close: Option<M>,
1275 wide: bool,
1276}
1277
1278impl<M> Panel<M> {
1279 pub fn new(content: Node<M>) -> Self {
1280 Self {
1281 content,
1282 on_open: None,
1283 on_close: None,
1284 wide: false,
1285 }
1286 }
1287
1288 pub fn wide(mut self) -> Self {
1291 self.wide = true;
1292 self
1293 }
1294
1295 #[doc(hidden)]
1296 pub fn is_wide(&self) -> bool {
1297 self.wide
1298 }
1299
1300 pub fn on_open(mut self, message: M) -> Self {
1302 self.on_open = Some(message);
1303 self
1304 }
1305
1306 pub fn on_close(mut self, message: M) -> Self {
1308 self.on_close = Some(message);
1309 self
1310 }
1311
1312 pub fn map<N: 'static>(self, f: &(impl Fn(M) -> N + Clone + 'static)) -> Panel<N>
1313 where
1314 M: 'static,
1315 {
1316 Panel {
1317 content: self.content.map(f),
1318 on_open: self.on_open.map(f),
1319 on_close: self.on_close.map(f),
1320 wide: self.wide,
1321 }
1322 }
1323
1324 #[doc(hidden)]
1325 pub fn into_node(self) -> Node<M> {
1326 self.content
1327 }
1328
1329 #[doc(hidden)]
1330 pub fn on_open_message(self) -> Option<M> {
1331 self.on_open
1332 }
1333
1334 #[doc(hidden)]
1335 pub fn on_close_message(self) -> Option<M> {
1336 self.on_close
1337 }
1338}
1339
1340#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1342pub enum Urgency {
1343 Low,
1345 #[default]
1346 Normal,
1347 Critical,
1349}
1350
1351pub struct Notice<M> {
1358 key: u64,
1359 summary: String,
1360 app: Option<String>,
1361 app_icon: Option<String>,
1362 image: Option<String>,
1363 body: Option<String>,
1364 markdown: bool,
1365 urgency: Urgency,
1366 actions: Vec<(String, M)>,
1367 on_press: Option<M>,
1368 on_dismiss: Option<M>,
1369 expires: Option<(std::time::Duration, M)>,
1370 reply: Option<Reply<M>>,
1371}
1372
1373pub struct Reply<M> {
1380 placeholder: String,
1381 value: String,
1382 on_input: std::rc::Rc<dyn Fn(String) -> M>,
1383 on_submit: Option<M>,
1384 on_cancel: Option<M>,
1385}
1386
1387impl<M> Reply<M> {
1388 pub fn new(
1390 placeholder: impl Into<String>,
1391 value: impl Into<String>,
1392 on_input: impl Fn(String) -> M + 'static,
1393 ) -> Self {
1394 Self {
1395 placeholder: placeholder.into(),
1396 value: value.into(),
1397 on_input: std::rc::Rc::new(on_input),
1398 on_submit: None,
1399 on_cancel: None,
1400 }
1401 }
1402
1403 pub fn on_submit(mut self, message: M) -> Self {
1405 self.on_submit = Some(message);
1406 self
1407 }
1408
1409 pub fn on_cancel(mut self, message: M) -> Self {
1411 self.on_cancel = Some(message);
1412 self
1413 }
1414
1415 pub fn map<N: 'static>(self, f: &(impl Fn(M) -> N + Clone + 'static)) -> Reply<N>
1416 where
1417 M: 'static,
1418 {
1419 let g = f.clone();
1420 let on_input = self.on_input;
1421 Reply {
1422 placeholder: self.placeholder,
1423 value: self.value,
1424 on_input: std::rc::Rc::new(move |text| g(on_input(text))),
1425 on_submit: self.on_submit.map(f),
1426 on_cancel: self.on_cancel.map(f),
1427 }
1428 }
1429
1430 #[doc(hidden)]
1431 pub fn into_parts(self) -> ReplyParts<M> {
1432 ReplyParts {
1433 placeholder: self.placeholder,
1434 value: self.value,
1435 on_input: self.on_input,
1436 on_submit: self.on_submit,
1437 on_cancel: self.on_cancel,
1438 }
1439 }
1440}
1441
1442#[doc(hidden)]
1443#[derive(Clone)]
1444pub struct ReplyParts<M> {
1445 pub placeholder: String,
1446 pub value: String,
1447 pub on_input: std::rc::Rc<dyn Fn(String) -> M>,
1448 pub on_submit: Option<M>,
1449 pub on_cancel: Option<M>,
1450}
1451
1452impl<M> Notice<M> {
1453 pub fn new(key: u64, summary: impl Into<String>) -> Self {
1454 Self {
1455 key,
1456 summary: summary.into(),
1457 app: None,
1458 app_icon: None,
1459 image: None,
1460 body: None,
1461 markdown: false,
1462 urgency: Urgency::Normal,
1463 actions: Vec::new(),
1464 on_press: None,
1465 on_dismiss: None,
1466 expires: None,
1467 reply: None,
1468 }
1469 }
1470
1471 pub fn app(mut self, name: impl Into<String>) -> Self {
1473 self.app = Some(name.into());
1474 self
1475 }
1476
1477 pub fn app_icon(mut self, app_id: impl Into<String>) -> Self {
1479 self.app_icon = Some(app_id.into());
1480 self
1481 }
1482
1483 pub fn image(mut self, url: impl Into<String>) -> Self {
1486 self.image = Some(url.into());
1487 self
1488 }
1489
1490 pub fn body(mut self, text: impl Into<String>) -> Self {
1492 self.body = Some(text.into());
1493 self
1494 }
1495
1496 pub fn markdown(mut self) -> Self {
1498 self.markdown = true;
1499 self
1500 }
1501
1502 pub fn urgency(mut self, urgency: Urgency) -> Self {
1503 self.urgency = urgency;
1504 self
1505 }
1506
1507 pub fn action(mut self, label: impl Into<String>, message: M) -> Self {
1509 self.actions.push((label.into(), message));
1510 self
1511 }
1512
1513 pub fn on_press(mut self, message: M) -> Self {
1515 self.on_press = Some(message);
1516 self
1517 }
1518
1519 pub fn on_dismiss(mut self, message: M) -> Self {
1521 self.on_dismiss = Some(message);
1522 self
1523 }
1524
1525 pub fn expires(mut self, after: std::time::Duration, message: M) -> Self {
1529 self.expires = Some((after, message));
1530 self
1531 }
1532
1533 pub fn reply(mut self, reply: Reply<M>) -> Self {
1535 self.reply = Some(reply);
1536 self
1537 }
1538
1539 pub fn map<N: 'static>(self, f: &(impl Fn(M) -> N + Clone + 'static)) -> Notice<N>
1540 where
1541 M: 'static,
1542 {
1543 Notice {
1544 key: self.key,
1545 summary: self.summary,
1546 app: self.app,
1547 app_icon: self.app_icon,
1548 image: self.image,
1549 body: self.body,
1550 markdown: self.markdown,
1551 urgency: self.urgency,
1552 actions: self
1553 .actions
1554 .into_iter()
1555 .map(|(label, message)| (label, f(message)))
1556 .collect(),
1557 on_press: self.on_press.map(f),
1558 on_dismiss: self.on_dismiss.map(f),
1559 expires: self.expires.map(|(after, message)| (after, f(message))),
1560 reply: self.reply.map(|reply| reply.map(f)),
1561 }
1562 }
1563
1564 pub fn key(&self) -> u64 {
1565 self.key
1566 }
1567
1568 #[doc(hidden)]
1569 pub fn into_parts(self) -> NoticeParts<M> {
1570 NoticeParts {
1571 key: self.key,
1572 summary: self.summary,
1573 app: self.app,
1574 app_icon: self.app_icon,
1575 image: self.image,
1576 body: self.body,
1577 markdown: self.markdown,
1578 urgency: self.urgency,
1579 actions: self.actions,
1580 on_press: self.on_press,
1581 on_dismiss: self.on_dismiss,
1582 expires: self.expires,
1583 reply: self.reply.map(Reply::into_parts),
1584 }
1585 }
1586}
1587
1588#[doc(hidden)]
1589#[derive(Clone)]
1590pub struct NoticeParts<M> {
1591 pub key: u64,
1592 pub summary: String,
1593 pub app: Option<String>,
1594 pub app_icon: Option<String>,
1595 pub image: Option<String>,
1596 pub body: Option<String>,
1597 pub markdown: bool,
1598 pub urgency: Urgency,
1599 pub actions: Vec<(String, M)>,
1600 pub on_press: Option<M>,
1601 pub on_dismiss: Option<M>,
1602 pub expires: Option<(std::time::Duration, M)>,
1603 pub reply: Option<ReplyParts<M>>,
1604}
1605
1606#[cfg(test)]
1607mod tests {
1608 use super::*;
1609
1610 #[test]
1611 fn map_converts_every_message() {
1612 let node = row([
1613 button(label("a"), 1).on_secondary(4),
1614 column([button(label("b"), 2), label("c")]),
1615 switcher([(label("d"), 3)], Some(0)),
1616 ]);
1617
1618 let Kind::Row(children) = node.map(&|n: i32| n * 10).into_kind() else {
1619 panic!("row became something else");
1620 };
1621 let mut messages = Vec::new();
1622 collect(children, &mut messages);
1623 assert_eq!(messages, [10, 40, 20, 30]);
1624 }
1625
1626 fn collect(nodes: Vec<Node<i32>>, out: &mut Vec<i32>) {
1627 for node in nodes {
1628 match node.into_kind() {
1629 Kind::Button {
1630 on_press,
1631 on_secondary,
1632 ..
1633 } => {
1634 out.push(on_press);
1635 out.extend(on_secondary);
1636 }
1637 Kind::Switcher { options, .. } => {
1638 out.extend(options.into_iter().map(|(_, message)| message))
1639 }
1640 Kind::Row(children) | Kind::Column(children) => collect(children, out),
1641 _ => {}
1642 }
1643 }
1644 }
1645
1646 #[test]
1647 fn hover_shows_the_second_option() {
1648 let node: Node<()> = hover(label("a"), label("b"));
1649 assert!(matches!(
1650 node.into_kind(),
1651 Kind::Alternate {
1652 current: 0,
1653 hovered: Some(1),
1654 ..
1655 }
1656 ));
1657 let node: Node<()> = label("a").on_hover(1);
1658 assert!(matches!(node.into_kind(), Kind::Label { .. }));
1659 }
1660
1661 #[test]
1662 fn tone_fills_only_unset_text() {
1663 let node: Node<()> =
1664 row([label("a").tone(Tone::Accent), label("b"), icon(Icon::Wired)]).tone(Tone::Dim);
1665 let Kind::Row(children) = node.into_kind() else {
1666 unreachable!()
1667 };
1668 let tones: Vec<_> = children
1669 .into_iter()
1670 .map(|child| match child.into_kind() {
1671 Kind::Label { tone, .. } | Kind::Icon { tone, .. } => tone,
1672 _ => None,
1673 })
1674 .collect();
1675 assert_eq!(
1676 tones,
1677 [Some(Tone::Accent), Some(Tone::Dim), Some(Tone::Dim)]
1678 );
1679 }
1680
1681 #[test]
1682 fn map_reaches_new_nodes_and_their_closures() {
1683 let node = column([
1684 header("h").on_back(1).trailing(toggle(true, 2)),
1685 list([list_item("a")
1686 .on_press(3)
1687 .on_secondary(4)
1688 .trailing(button(icon(Icon::Info), 5))]),
1689 slider(0.5, 0.0..=1.0, |v| (v * 10.0) as i32).on_release(6),
1690 text_input("p", "v", |text: String| text.len() as i32).on_submit(7),
1691 scroll(text_area("p", "v", |text: String| text.len() as i32 + 1).on_submit(8)).bottom(),
1692 ]);
1693 let Kind::Column(children) = node.map(&|n: i32| n * 10).into_kind() else {
1694 panic!("column became something else");
1695 };
1696 let mut children = children.into_iter().map(Node::into_kind);
1697 let Some(Kind::Header {
1698 on_back: Some(10),
1699 trailing: Some(trailing),
1700 ..
1701 }) = children.next()
1702 else {
1703 panic!("header lost its back message");
1704 };
1705 assert!(matches!(
1706 trailing.into_kind(),
1707 Kind::Toggle { on_toggle: 20, .. }
1708 ));
1709 let Some(Kind::List(rows)) = children.next() else {
1710 panic!("list lost");
1711 };
1712 let Some(Kind::ListItem {
1713 on_press: Some(30),
1714 on_secondary: Some(40),
1715 trailing: Some(trailing),
1716 ..
1717 }) = rows.into_iter().next().map(Node::into_kind)
1718 else {
1719 panic!("list item lost its messages");
1720 };
1721 assert!(matches!(
1722 trailing.into_kind(),
1723 Kind::Button { on_press: 50, .. }
1724 ));
1725 let Some(Kind::Slider {
1726 on_change,
1727 on_release: Some(60),
1728 ..
1729 }) = children.next()
1730 else {
1731 panic!("slider lost");
1732 };
1733 assert_eq!(on_change(0.3), 30);
1734 let Some(Kind::TextInput {
1735 on_input,
1736 on_submit: Some(70),
1737 ..
1738 }) = children.next()
1739 else {
1740 panic!("text input lost");
1741 };
1742 assert_eq!(on_input("abcd".into()), 40);
1743 let Some(Kind::Scroll {
1744 content,
1745 bottom: true,
1746 }) = children.next()
1747 else {
1748 panic!("scroll lost");
1749 };
1750 let Kind::TextArea {
1751 on_input,
1752 on_submit: Some(80),
1753 ..
1754 } = content.into_kind()
1755 else {
1756 panic!("text area lost");
1757 };
1758 assert_eq!(on_input("abcd".into()), 50);
1759 }
1760
1761 #[test]
1762 fn tabs_map_and_tone_every_page() {
1763 let node = tabs([("a", button(label("x"), 1)), ("b", button(label("y"), 2))])
1764 .tone(Tone::Dim)
1765 .map(&|n: i32| n * 10);
1766 let Kind::Tabs(pages) = node.into_kind() else {
1767 panic!("tabs became something else");
1768 };
1769 let names: Vec<_> = pages.iter().map(|(name, _)| name.as_str()).collect();
1770 assert_eq!(names, ["a", "b"]);
1771 let mut messages = Vec::new();
1772 for (_, page) in pages {
1773 let Kind::Button {
1774 content, on_press, ..
1775 } = page.into_kind()
1776 else {
1777 panic!("page lost its button");
1778 };
1779 assert!(matches!(
1780 content.into_kind(),
1781 Kind::Label {
1782 tone: Some(Tone::Dim),
1783 ..
1784 }
1785 ));
1786 messages.push(on_press);
1787 }
1788 assert_eq!(messages, [10, 20]);
1789 }
1790
1791 #[test]
1792 fn notice_map_reaches_every_message() {
1793 let notice = Notice::new(7, "s")
1794 .action("a", 1)
1795 .action("b", 2)
1796 .on_press(3)
1797 .on_dismiss(4)
1798 .expires(std::time::Duration::from_secs(5), 5)
1799 .reply(
1800 Reply::new("p", "v", |text: String| text.len() as i32)
1801 .on_submit(6)
1802 .on_cancel(7),
1803 )
1804 .map(&|n: i32| n * 10);
1805 assert_eq!(notice.key(), 7);
1806 let parts = notice.into_parts();
1807 let actions: Vec<_> = parts.actions.into_iter().map(|(_, m)| m).collect();
1808 assert_eq!(actions, [10, 20]);
1809 assert_eq!(parts.on_press, Some(30));
1810 assert_eq!(parts.on_dismiss, Some(40));
1811 assert_eq!(parts.expires.map(|(_, m)| m), Some(50));
1812 let reply = parts.reply.unwrap();
1813 assert_eq!((reply.on_input)("abc".into()), 30);
1814 assert_eq!((reply.on_submit, reply.on_cancel), (Some(60), Some(70)));
1815 }
1816
1817 #[test]
1818 fn modifiers_only_touch_their_nodes() {
1819 let node: Node<i32> = label("a").trailing(label("b")).on_press(1).selected(true);
1820 assert!(matches!(node.into_kind(), Kind::Label { .. }));
1821 let node: Node<i32> = button(label("a"), 1).on_press(2).prominent();
1822 assert!(matches!(
1823 node.into_kind(),
1824 Kind::Button {
1825 on_press: 1,
1826 prominent: true,
1827 ..
1828 }
1829 ));
1830 }
1831
1832 #[test]
1833 fn a_header_takes_a_picture_and_a_subtitle() {
1834 let node = header("t")
1835 .subtitle("s")
1836 .leading(button(icon(Icon::Info), 1))
1837 .tone(Tone::Dim)
1838 .map(&|n: i32| n * 10);
1839 let Kind::Header {
1840 subtitle: Some(subtitle),
1841 leading: Some(leading),
1842 ..
1843 } = node.into_kind()
1844 else {
1845 panic!("header lost its picture or subtitle");
1846 };
1847 assert_eq!(subtitle, "s");
1848 let Kind::Button {
1849 content,
1850 on_press: 10,
1851 ..
1852 } = leading.into_kind()
1853 else {
1854 panic!("picture lost its message");
1855 };
1856 assert!(matches!(
1857 content.into_kind(),
1858 Kind::Icon {
1859 tone: Some(Tone::Dim),
1860 ..
1861 }
1862 ));
1863 }
1864
1865 #[test]
1866 fn timelines_map_and_tone_their_steps() {
1867 let node = timeline([
1868 step(Mark::You, label("a")),
1869 step(Mark::Failed, button(literal("b"), 1)).leading(icon(Icon::Check)),
1870 label("c"),
1871 ])
1872 .tone(Tone::Dim)
1873 .map(&|n: i32| n * 10);
1874 let Kind::Timeline(steps) = node.into_kind() else {
1875 panic!("timeline became something else");
1876 };
1877 let mut steps = steps.into_iter().map(Node::into_kind);
1878 let Some(Kind::Step {
1879 mark: Mark::You,
1880 leading: None,
1881 content,
1882 }) = steps.next()
1883 else {
1884 panic!("first step lost");
1885 };
1886 assert!(matches!(
1887 content.into_kind(),
1888 Kind::Label {
1889 tone: Some(Tone::Dim),
1890 ..
1891 }
1892 ));
1893 let Some(Kind::Step {
1894 mark: Mark::Failed,
1895 leading: Some(leading),
1896 content,
1897 }) = steps.next()
1898 else {
1899 panic!("second step lost its picture");
1900 };
1901 assert!(matches!(
1902 leading.into_kind(),
1903 Kind::Icon {
1904 tone: Some(Tone::Dim),
1905 ..
1906 }
1907 ));
1908 let Kind::Button {
1909 content,
1910 on_press: 10,
1911 ..
1912 } = content.into_kind()
1913 else {
1914 panic!("button lost its message");
1915 };
1916 assert!(matches!(
1917 content.into_kind(),
1918 Kind::Literal {
1919 tone: Some(Tone::Dim),
1920 ..
1921 }
1922 ));
1923 assert!(matches!(steps.next(), Some(Kind::Label { .. })));
1924 let node: Node<()> = diff("f", [(Change::Added, "x".to_string())]).leading(label("y"));
1925 assert!(matches!(node.into_kind(), Kind::Diff { lines, .. } if lines.len() == 1));
1926 }
1927
1928 #[test]
1929 fn tone_reaches_details_and_row_icons() {
1930 let node: Node<()> = column([
1931 detail("n", label("v")),
1932 list_item("t").leading(icon(Icon::Lock)),
1933 ])
1934 .tone(Tone::Good);
1935 let Kind::Column(children) = node.into_kind() else {
1936 unreachable!()
1937 };
1938 let mut children = children.into_iter().map(Node::into_kind);
1939 let Some(Kind::Detail { value, .. }) = children.next() else {
1940 unreachable!()
1941 };
1942 assert!(matches!(
1943 value.into_kind(),
1944 Kind::Label {
1945 tone: Some(Tone::Good),
1946 ..
1947 }
1948 ));
1949 let Some(Kind::ListItem {
1950 leading: Some(leading),
1951 ..
1952 }) = children.next()
1953 else {
1954 unreachable!()
1955 };
1956 assert!(matches!(
1957 leading.into_kind(),
1958 Kind::Icon {
1959 tone: Some(Tone::Good),
1960 ..
1961 }
1962 ));
1963 }
1964}