1use alloc::string::String;
4use alloc::vec::Vec;
5
6use denise::Pen;
7use denise::{ElementState, InputEvent, KeyCode, Point, Radius, Rect, Role, Theme};
8use denise_text::{TextEngine, TextStyle};
9
10use crate::widget::{
11 Event, EventCtx, Handled, MeasureCtx, Measured, Offer, PaintCtx, VisualState, Widget,
12};
13use crate::widgets::describe::{
14 Describe, DynDescribe, Group, Mismatch, Payload, Property, PropertyKind, ROLES, Value,
15};
16use crate::widgets::style::{
17 Align, ClickPair, Intent, RowKind, columns, draw_aligned, focus_ring, hovered_row, row_colors,
18};
19
20#[derive(Clone, Debug, PartialEq, Eq)]
30pub struct TreeItem {
31 text: String,
32 leading: String,
33 trailing: String,
34 depth: u16,
35 open: bool,
36 enabled: bool,
37}
38
39impl TreeItem {
40 pub fn new(text: impl Into<String>) -> Self {
45 Self {
46 text: text.into(),
47 leading: String::new(),
48 trailing: String::new(),
49 depth: 0,
50 open: true,
51 enabled: true,
52 }
53 }
54
55 pub fn at_depth(mut self, depth: u16) -> Self {
61 self.depth = depth;
62 self
63 }
64
65 pub fn shut(mut self) -> Self {
67 self.open = false;
68 self
69 }
70
71 pub fn with_leading(mut self, leading: impl Into<String>) -> Self {
73 self.leading = leading.into();
74 self
75 }
76
77 pub fn with_trailing(mut self, trailing: impl Into<String>) -> Self {
79 self.trailing = trailing.into();
80 self
81 }
82
83 pub fn disabled(mut self) -> Self {
88 self.enabled = false;
89 self
90 }
91
92 pub fn text(&self) -> &str {
94 &self.text
95 }
96
97 pub fn leading(&self) -> &str {
99 &self.leading
100 }
101
102 pub fn trailing(&self) -> &str {
104 &self.trailing
105 }
106
107 #[inline]
109 pub const fn depth(&self) -> u16 {
110 self.depth
111 }
112
113 #[inline]
115 pub const fn is_open(&self) -> bool {
116 self.open
117 }
118
119 #[inline]
121 pub const fn is_enabled(&self) -> bool {
122 self.enabled
123 }
124
125 pub fn set_text(&mut self, text: impl Into<String>) {
127 self.text = text.into();
128 }
129
130 pub fn set_open(&mut self, open: bool) {
132 self.open = open;
133 }
134
135 pub fn set_enabled(&mut self, enabled: bool) {
137 self.enabled = enabled;
138 }
139
140 fn trailing_width(&self, engine: &mut TextEngine, style: TextStyle) -> i32 {
141 if self.trailing.is_empty() {
142 0
143 } else {
144 engine.measure_line(style, &self.trailing)
145 }
146 }
147}
148
149impl From<&str> for TreeItem {
150 fn from(text: &str) -> Self {
151 Self::new(text)
152 }
153}
154
155impl From<String> for TreeItem {
156 fn from(text: String) -> Self {
157 Self::new(text)
158 }
159}
160
161#[derive(Clone, Debug)]
239pub struct Tree<M> {
240 items: Vec<TreeItem>,
241 selected: Option<usize>,
242 hovered: Option<usize>,
243 row_height: Option<i32>,
244 indent: i32,
245 selection: Option<fn(usize) -> M>,
246 activation: Option<fn(usize) -> M>,
247 toggle: Option<fn(usize) -> M>,
248 single_click: bool,
249 clicks: ClickPair,
250 role: Role,
251 style: TextStyle,
252}
253
254const INDENT: i32 = 14;
256
257impl<M> Tree<M> {
258 pub fn new(
260 items: impl IntoIterator<Item = impl Into<TreeItem>>,
261 message: fn(usize) -> M,
262 ) -> Self {
263 Self {
264 items: items.into_iter().map(Into::into).collect(),
265 selection: Some(message),
266 ..Self::bare()
267 }
268 }
269
270 pub fn inert(items: impl IntoIterator<Item = impl Into<TreeItem>>) -> Self {
273 Self {
274 items: items.into_iter().map(Into::into).collect(),
275 ..Self::bare()
276 }
277 }
278
279 fn bare() -> Self {
280 Self {
281 items: Vec::new(),
282 selected: None,
283 hovered: None,
284 row_height: None,
285 indent: INDENT,
286 selection: None,
287 activation: None,
288 toggle: None,
289 single_click: false,
290 clicks: ClickPair::default(),
291 role: Role::Primary,
292 style: TextStyle::built_in(16),
293 }
294 }
295
296 pub fn on_activate(mut self, message: fn(usize) -> M) -> Self {
298 self.activation = Some(message);
299 self
300 }
301
302 pub fn on_toggle(mut self, message: fn(usize) -> M) -> Self {
308 self.toggle = Some(message);
309 self
310 }
311
312 pub fn activate_on_click(mut self) -> Self {
316 self.single_click = true;
317 self
318 }
319
320 pub fn with_selected(mut self, index: Option<usize>) -> Self {
322 self.set_selected(index);
323 self
324 }
325
326 pub fn with_row_height(mut self, height: i32) -> Self {
328 self.row_height = Some(height.max(1));
329 self
330 }
331
332 pub fn with_indent(mut self, indent: i32) -> Self {
334 self.indent = indent.max(0);
335 self
336 }
337
338 pub fn with_role(mut self, role: Role) -> Self {
340 self.role = role;
341 self
342 }
343
344 pub fn with_style(mut self, style: TextStyle) -> Self {
346 self.style = style;
347 self
348 }
349
350 #[inline]
352 pub const fn selected(&self) -> Option<usize> {
353 self.selected
354 }
355
356 pub fn selected_item(&self) -> Option<&TreeItem> {
358 self.items.get(self.selected?)
359 }
360
361 pub fn set_selected(&mut self, index: Option<usize>) {
368 self.selected = index.filter(|index| {
369 self.items.get(*index).is_some_and(TreeItem::is_enabled) && self.is_shown(*index)
370 });
371 }
372
373 pub fn items(&self) -> &[TreeItem] {
375 &self.items
376 }
377
378 pub fn set_items(&mut self, items: impl IntoIterator<Item = impl Into<TreeItem>>) {
380 self.items = items.into_iter().map(Into::into).collect();
381 let selected = self.selected;
382 self.set_selected(selected);
383 self.hovered = None;
384 self.clicks.forget();
386 }
387
388 pub fn set_open(&mut self, index: usize, open: bool) {
393 if let Some(item) = self.items.get_mut(index) {
394 item.open = open;
395 }
396 let selected = self.selected;
397 self.set_selected(selected);
398 }
399
400 pub fn set_all_open(&mut self, open: bool) {
402 for item in &mut self.items {
403 item.open = open;
404 }
405 let selected = self.selected;
406 self.set_selected(selected);
407 }
408
409 pub fn set_row_enabled(&mut self, index: usize, enabled: bool) {
411 if let Some(item) = self.items.get_mut(index) {
412 item.set_enabled(enabled);
413 }
414 let selected = self.selected;
415 self.set_selected(selected);
416 }
417
418 pub fn set_role(&mut self, role: Role) {
420 self.role = role;
421 }
422
423 pub fn set_style(&mut self, style: TextStyle) {
425 self.style = style;
426 }
427
428 pub fn has_children(&self, index: usize) -> bool {
432 has_children(&self.items, index)
433 }
434
435 pub fn is_shown(&self, index: usize) -> bool {
437 Shown::new(&self.items).any(|(shown, _)| shown == index)
438 }
439
440 pub fn shown_rows(&self) -> usize {
442 Shown::new(&self.items).count()
443 }
444
445 pub fn row_height(&self, theme: &Theme) -> i32 {
447 self.row_height.unwrap_or(theme.metrics.size_field).max(1)
448 }
449
450 pub fn visible_rows(&self, theme: &Theme, height: i32) -> usize {
452 if height <= 0 {
453 return 0;
454 }
455 (height / self.row_height(theme)) as usize
456 }
457
458 pub fn preferred_height(&self, theme: &Theme) -> i32 {
463 let rows = self.shown_rows().max(1) as i64;
464 (i64::from(self.row_height(theme)) * rows).min(i64::from(i32::MAX)) as i32
465 }
466
467 pub fn preferred_width(&self, engine: &mut TextEngine) -> i32 {
469 let pad = padding(self.style.size_px);
470 let gutter = self.gutter();
471 let mut widest = 0;
472 let mut trailing = 0;
473 for (index, item) in Shown::new(&self.items) {
474 let _ = index;
475 let text = engine.measure_line(self.style, &item.text);
476 let leading = if item.leading.is_empty() {
477 0
478 } else {
479 engine.measure_line(self.style, &item.leading) + pad
480 };
481 widest = widest.max(self.indent_of(item) + gutter + leading + text);
482 trailing = trailing.max(item.trailing_width(engine, self.style));
483 }
484 let gap = if trailing > 0 { pad } else { 0 };
485 pad * 2 + widest + trailing + gap
486 }
487
488 fn gutter(&self) -> i32 {
493 (i32::from(self.style.size_px) * 3 / 4).max(8)
494 }
495
496 fn indent_of(&self, item: &TreeItem) -> i32 {
498 self.indent.saturating_mul(i32::from(item.depth))
499 }
500
501 fn parts(&self, row: Rect, item: &TreeItem) -> (Rect, Rect) {
503 let pad = padding(self.style.size_px);
504 let start = row.x + pad;
505 let right = (row.right() - pad).max(start);
506 let left = start
511 .saturating_add(self.indent_of(item))
512 .clamp(start, right);
513 let triangle = Rect::from_edges(
514 left,
515 row.y,
516 left.saturating_add(self.gutter()).min(right),
517 row.bottom(),
518 );
519 let content = Rect::from_edges(triangle.right(), row.y, right, row.bottom());
520 (triangle, content)
521 }
522
523 fn hit(&self, bounds: Rect, row_height: i32, point: Point) -> Option<(usize, bool)> {
529 if !bounds.contains(point) {
530 return None;
531 }
532 let nth = (i64::from(point.y - bounds.y) / i64::from(row_height.max(1))) as usize;
533 let (index, item) = Shown::new(&self.items).nth(nth)?;
534 let row = row_rect(bounds, row_height, nth);
535 let (triangle, _) = self.parts(row, item);
536 let on_triangle = has_children(&self.items, index) && triangle.contains(point);
537 Some((index, on_triangle))
538 }
539
540 fn select(&mut self, target: Option<usize>, ctx: &mut EventCtx<'_, M>) -> Handled {
542 let Some(target) = target else {
543 return Handled::Yes;
544 };
545 if self.selected == Some(target) {
546 return Handled::Yes;
547 }
548 self.selected = Some(target);
549 if let Some(nth) = Shown::new(&self.items).position(|(index, _)| index == target) {
550 ctx.reveal(row_rect(ctx.bounds, self.row_height(ctx.theme), nth));
551 }
552 if let Some(message) = self.selection {
553 ctx.emit(message(target));
554 }
555 Handled::Yes
556 }
557
558 fn toggle(&mut self, index: usize, ctx: &mut EventCtx<'_, M>) -> Handled {
560 if !has_children(&self.items, index) {
561 return Handled::No;
562 }
563 let Some(item) = self.items.get_mut(index) else {
564 return Handled::No;
565 };
566 item.open = !item.open;
567 let selected = self.selected;
570 self.set_selected(selected);
571 if self.selected.is_none() && selected.is_some() {
572 self.selected = Some(index);
573 }
574 if let Some(message) = self.toggle {
575 ctx.emit(message(index));
576 }
577 Handled::Yes
578 }
579
580 fn activate(&mut self, row: usize, ctx: &mut EventCtx<'_, M>) -> Handled {
582 if let Some(message) = self.activation {
583 ctx.emit(message(row));
584 }
585 Handled::Yes
586 }
587
588 fn go_in(&mut self, ctx: &mut EventCtx<'_, M>) -> Handled {
590 let Some(index) = self.selected else {
591 let target = step(&self.items, None, true);
592 return self.select(target, ctx);
593 };
594 if !has_children(&self.items, index) {
595 return Handled::Yes;
596 }
597 if self.items.get(index).is_some_and(TreeItem::is_open) {
598 let child = step(&self.items, Some(index), true);
599 return self.select(child, ctx);
600 }
601 self.toggle(index, ctx)
602 }
603
604 fn go_out(&mut self, ctx: &mut EventCtx<'_, M>) -> Handled {
606 let Some(index) = self.selected else {
607 let target = step(&self.items, None, false);
608 return self.select(target, ctx);
609 };
610 let open = self.items.get(index).is_some_and(TreeItem::is_open);
611 if has_children(&self.items, index) && open {
612 return self.toggle(index, ctx);
613 }
614 match parent_of(&self.items, index) {
615 Some(parent) if self.items[parent].enabled => self.select(Some(parent), ctx),
618 _ => Handled::Yes,
619 }
620 }
621}
622
623struct Shown<'a> {
629 items: &'a [TreeItem],
630 at: usize,
631 shut_at: Option<u16>,
633}
634
635impl<'a> Shown<'a> {
636 fn new(items: &'a [TreeItem]) -> Self {
637 Self {
638 items,
639 at: 0,
640 shut_at: None,
641 }
642 }
643}
644
645impl<'a> Iterator for Shown<'a> {
646 type Item = (usize, &'a TreeItem);
647
648 fn next(&mut self) -> Option<Self::Item> {
649 loop {
650 let index = self.at;
651 let item = self.items.get(index)?;
652 self.at += 1;
653 if let Some(depth) = self.shut_at {
654 if item.depth > depth {
655 continue;
656 }
657 self.shut_at = None;
658 }
659 if !item.open && has_children(self.items, index) {
660 self.shut_at = Some(item.depth);
661 }
662 return Some((index, item));
663 }
664 }
665}
666
667fn has_children(items: &[TreeItem], index: usize) -> bool {
669 let Some(item) = items.get(index) else {
670 return false;
671 };
672 items
673 .get(index + 1)
674 .is_some_and(|next| next.depth > item.depth)
675}
676
677fn parent_of(items: &[TreeItem], index: usize) -> Option<usize> {
679 let depth = items.get(index)?.depth;
680 if depth == 0 {
681 return None;
682 }
683 items[..index].iter().rposition(|item| item.depth < depth)
684}
685
686fn first_enabled(items: &[TreeItem]) -> Option<usize> {
688 Shown::new(items)
689 .find(|(_, item)| item.enabled)
690 .map(|(index, _)| index)
691}
692
693fn last_enabled(items: &[TreeItem]) -> Option<usize> {
695 Shown::new(items)
696 .filter(|(_, item)| item.enabled)
697 .map(|(index, _)| index)
698 .last()
699}
700
701fn step(items: &[TreeItem], from: Option<usize>, forward: bool) -> Option<usize> {
706 let shown: Option<usize> = match from {
707 None => {
708 return if forward {
709 first_enabled(items)
710 } else {
711 last_enabled(items)
712 };
713 }
714 Some(from) => Shown::new(items).position(|(index, _)| index == from),
715 };
716 let shown = shown?;
717 let mut walk = Shown::new(items)
718 .enumerate()
719 .filter(|(_, (_, item))| item.enabled)
720 .map(|(nth, (index, _))| (nth, index));
721 if forward {
722 walk.find(|(nth, _)| *nth > shown).map(|(_, index)| index)
723 } else {
724 walk.take_while(|(nth, _)| *nth < shown)
725 .map(|(_, index)| index)
726 .last()
727 }
728}
729
730#[inline]
732const fn padding(size_px: u16) -> i32 {
733 let half = size_px as i32 / 2;
734 if half < 4 { 4 } else { half }
735}
736
737fn row_rect(bounds: Rect, row_height: i32, nth: usize) -> Rect {
739 let height = row_height.max(1);
740 let nth = nth.min(i32::MAX as usize) as i64;
741 let ceiling = i64::from(i32::MAX - height);
742 let y = (i64::from(bounds.y) + i64::from(height) * nth).min(ceiling) as i32;
743 Rect::new(bounds.x, y, bounds.width, height)
744}
745
746fn disclosure(canvas: &mut Pen<'_>, box_of: Rect, open: bool, color: denise::Color) {
752 let size = (box_of.height / 3).clamp(3, 9) | 1;
754 let cx = box_of.x + box_of.width / 2;
755 let cy = box_of.y + box_of.height / 2;
756 if open {
757 for step in 0..=size {
759 let half = size - step;
760 canvas.fill_rect(
761 Rect::new(cx - half, cy - size / 2 + step, half * 2 + 1, 1),
762 color,
763 );
764 }
765 } else {
766 for step in 0..=size {
768 let half = size - step;
769 canvas.fill_rect(
770 Rect::new(cx - size / 2 + step, cy - half, 1, half * 2 + 1),
771 color,
772 );
773 }
774 }
775}
776
777impl<M: 'static> Widget<M> for Tree<M> {
778 fn describe(&self) -> Option<&dyn DynDescribe> {
779 Some(self)
780 }
781
782 fn describe_mut(&mut self) -> Option<&mut dyn DynDescribe> {
783 Some(self)
784 }
785
786 fn measure(&self, ctx: &mut MeasureCtx<'_>, _offered: Offer) -> Measured {
787 Measured::both(
790 self.preferred_width(ctx.text),
791 self.preferred_height(ctx.theme),
792 )
793 }
794
795 fn paint(&self, ctx: &mut PaintCtx<'_>, canvas: &mut Pen<'_>) {
796 let bounds = ctx.bounds;
797 if bounds.is_empty() || self.items.is_empty() {
798 return;
799 }
800 let (backdrop, _) = row_colors(ctx.theme, ctx.state, self.role, RowKind::Resting, true);
801 canvas.fill_rect(bounds, backdrop);
802
803 let row_height = self.row_height(ctx.theme);
804 let pad = padding(self.style.size_px);
805 let radius = ctx.theme.radius(Radius::Field);
806 let hovered = hovered_row(ctx.state, self.hovered);
807
808 for (nth, (index, item)) in Shown::new(&self.items).enumerate() {
809 let row = row_rect(bounds, row_height, nth);
810 if row.y >= bounds.bottom() {
811 break;
813 }
814 let kind = if self.selected == Some(index) {
815 RowKind::Selected
816 } else if hovered == Some(index) {
817 RowKind::Hovered
818 } else {
819 RowKind::Resting
820 };
821 let (fill, content) = row_colors(ctx.theme, ctx.state, self.role, kind, item.enabled);
822 if kind != RowKind::Resting {
823 canvas.fill_rounded_rect(row, radius, fill);
824 }
825 if kind == RowKind::Selected && ctx.state.contains(VisualState::FOCUSED) {
826 focus_ring(ctx.theme, row, radius, canvas);
827 }
828
829 let (triangle, rest) = self.parts(row, item);
830 if has_children(&self.items, index) && !triangle.is_empty() {
831 disclosure(canvas, triangle, item.open, content);
832 }
833
834 let trailing_width = item.trailing_width(ctx.text, self.style);
835 let leading_width = if item.leading.is_empty() {
836 0
837 } else {
838 ctx.text.measure_line(self.style, &item.leading)
839 };
840 let (leading, label, trailing) = columns(rest, pad, leading_width, trailing_width);
841 for (box_of, text, align) in [
842 (leading, &item.leading, Align::Start),
843 (label, &item.text, Align::Start),
844 (trailing, &item.trailing, Align::End),
845 ] {
846 if text.is_empty() || box_of.is_empty() {
847 continue;
848 }
849 let mut column = canvas.with_clip(box_of);
850 draw_aligned(
851 &mut column,
852 ctx.text,
853 self.style,
854 box_of,
855 (align, Align::Center),
856 text,
857 content,
858 );
859 }
860 }
861
862 if ctx.state.contains(VisualState::FOCUSED) && self.selected.is_none() {
863 focus_ring(ctx.theme, bounds, radius, canvas);
864 }
865 }
866
867 fn on_event(&mut self, event: &Event<'_>, ctx: &mut EventCtx<'_, M>) -> Handled {
868 if self.items.is_empty() {
869 return Handled::No;
870 }
871 let row_height = self.row_height(ctx.theme);
872
873 match event {
874 Event::Input(InputEvent::PointerMoved { position }) => {
875 let row = self
876 .hit(ctx.bounds, row_height, *position)
877 .map(|(index, _)| index)
878 .filter(|index| self.items[*index].enabled);
879 if row == self.hovered {
880 return Handled::No;
881 }
882 self.hovered = row;
883 Handled::Yes
884 }
885 Event::Input(InputEvent::PointerButton {
886 state: ElementState::Up,
887 position,
888 ..
889 })
890 | Event::Input(InputEvent::TouchUp {
891 position,
892 cancelled: false,
893 ..
894 }) => {
895 let Some((index, on_triangle)) = self.hit(ctx.bounds, row_height, *position) else {
896 return Handled::No;
897 };
898 if on_triangle {
902 return self.toggle(index, ctx);
903 }
904 if !self.items[index].enabled {
905 return Handled::No;
906 }
907 let intent = self.clicks.classify(index, ctx.now_ms, self.single_click);
908 let handled = self.select(Some(index), ctx);
909 if intent == Intent::Activate {
910 self.activate(index, ctx);
911 }
912 handled
913 }
914 Event::Input(InputEvent::Key {
915 code,
916 state: ElementState::Down,
917 ..
918 }) if ctx.state.contains(VisualState::FOCUSED) => match code {
919 KeyCode::ArrowDown => {
920 let target = step(&self.items, self.selected, true);
921 self.select(target, ctx)
922 }
923 KeyCode::ArrowUp => {
924 let target = step(&self.items, self.selected, false);
925 self.select(target, ctx)
926 }
927 KeyCode::ArrowRight => self.go_in(ctx),
928 KeyCode::ArrowLeft => self.go_out(ctx),
929 KeyCode::Home => {
930 let target = first_enabled(&self.items);
931 self.select(target, ctx)
932 }
933 KeyCode::End => {
934 let target = last_enabled(&self.items);
935 self.select(target, ctx)
936 }
937 KeyCode::Enter | KeyCode::NumpadEnter => match self.selected {
938 Some(row) if self.items.get(row).is_some_and(TreeItem::is_enabled) => {
939 self.activate(row, ctx)
940 }
941 _ => Handled::No,
942 },
943 _ => Handled::No,
944 },
945 _ => Handled::No,
946 }
947 }
948
949 fn accepts_pointer(&self) -> bool {
950 true
951 }
952
953 fn focusable(&self) -> bool {
955 self.items.iter().any(TreeItem::is_enabled)
956 }
957}
958
959impl<M> Describe for Tree<M> {
960 const KIND: &'static str = "tree";
961 const DOC: &'static str = "A hierarchy of rows that open and shut, indented by depth.";
962 const GROUP: Group = Group::Data;
963 const ICON: &'static denise::icon::Icon = &super::icons::TREE;
964
965 const PROPERTIES: &'static [Property] = &[
966 Property::new(
967 "item",
968 PropertyKind::List,
969 "The rows, as `item` child nodes, each at its own `depth`. Real data, like a list's.",
970 ),
971 Property::new(
972 "selected",
973 PropertyKind::Int { min: 0, max: 9999 },
974 "Which row is selected, by its position in the file.",
975 ),
976 Property::new(
977 "on-select",
978 PropertyKind::Message(Payload::Index),
979 "Sent with the row when the selection moves.",
980 ),
981 Property::new(
982 "on-activate",
983 PropertyKind::Message(Payload::Index),
984 "Sent with the row on Enter or a double-click.",
985 ),
986 Property::new(
987 "on-toggle",
988 PropertyKind::Message(Payload::Index),
989 "Sent with the row when a branch is opened or shut.",
990 ),
991 Property::new(
992 "activate-on-click",
993 PropertyKind::Bool,
994 "Whether one tap both selects and activates. For a touch panel.",
995 ),
996 Property::new(
997 "row-height",
998 PropertyKind::Int { min: 16, max: 200 },
999 "Height of every row in logical pixels, overriding the theme's field height.",
1000 )
1001 .in_pixels(),
1002 Property::new(
1003 "indent",
1004 PropertyKind::Int { min: 0, max: 100 },
1005 "How far one level is indented from the one above, in logical pixels.",
1006 )
1007 .in_pixels(),
1008 Property::new(
1009 "role",
1010 PropertyKind::Enum(ROLES),
1011 "The colour of the selected row.",
1012 ),
1013 Property::new(
1014 "size",
1015 PropertyKind::Int { min: 6, max: 96 },
1016 "Text size in logical pixels.",
1017 )
1018 .in_pixels(),
1019 ];
1020
1021 fn get(&self, name: &str) -> Option<Value> {
1022 Some(match name {
1023 "selected" => Value::Int(i32::try_from(self.selected?).ok()?),
1024 "activate-on-click" => Value::Bool(self.single_click),
1025 "row-height" => Value::Int(self.row_height?),
1026 "indent" => Value::Int(self.indent),
1027 "role" => Value::role(self.role),
1028 "size" => Value::Int(i32::from(self.style.size_px)),
1029 _ => return None,
1030 })
1031 }
1032
1033 fn apply(&mut self, name: &str, value: Value) -> Result<(), Mismatch> {
1034 match name {
1035 "selected" => self.set_selected(Some(value.as_index()?)),
1038 "on-select" | "on-activate" | "on-toggle" | "item" => return Err(Mismatch::Supplied),
1041 "activate-on-click" => self.single_click = value.as_bool()?,
1042 "row-height" => self.row_height = Some(value.as_int()?.max(1)),
1043 "indent" => self.indent = value.as_int()?.max(0),
1044 "role" => self.role = value.as_role()?,
1045 "size" => self.style.size_px = value.as_size()?,
1046 _ => return Err(Mismatch::Unknown),
1047 }
1048 Ok(())
1049 }
1050}
1051
1052#[cfg(test)]
1053mod tests {
1054 use super::*;
1055
1056 fn items() -> Vec<TreeItem> {
1063 alloc::vec![
1064 TreeItem::new("Nettverk"),
1065 TreeItem::new("Wi-Fi").at_depth(1),
1066 TreeItem::new("Hjemme").at_depth(2),
1067 TreeItem::new("Ethernet").at_depth(1),
1068 TreeItem::new("Skjerm"),
1069 TreeItem::new("Lysstyrke").at_depth(1),
1070 ]
1071 }
1072
1073 fn tree() -> Tree<usize> {
1074 Tree::new(items(), |row| row)
1075 }
1076
1077 fn shown(tree: &Tree<usize>) -> Vec<usize> {
1079 Shown::new(&tree.items).map(|(index, _)| index).collect()
1080 }
1081
1082 #[test]
1083 fn the_hierarchy_is_the_depths_and_nothing_else() {
1084 let items = items();
1085 assert!(has_children(&items, 0), "Nettverk holds Wi-Fi");
1088 assert!(has_children(&items, 1), "Wi-Fi holds Hjemme");
1089 assert!(!has_children(&items, 2), "Hjemme holds nothing");
1090 assert!(!has_children(&items, 3), "Ethernet holds nothing");
1091 assert!(has_children(&items, 4), "Skjerm holds Lysstyrke");
1092 assert!(!has_children(&items, 5), "the last row holds nothing");
1093
1094 assert_eq!(parent_of(&items, 0), None);
1096 assert_eq!(parent_of(&items, 1), Some(0));
1097 assert_eq!(parent_of(&items, 2), Some(1));
1098 assert_eq!(parent_of(&items, 3), Some(0), "past its deeper sibling");
1099 assert_eq!(parent_of(&items, 5), Some(4));
1100 }
1101
1102 #[test]
1103 fn shutting_a_branch_hides_everything_under_it_however_deep() {
1104 let mut tree = tree();
1105 assert_eq!(shown(&tree), alloc::vec![0, 1, 2, 3, 4, 5]);
1106
1107 tree.set_open(1, false);
1109 assert_eq!(shown(&tree), alloc::vec![0, 1, 3, 4, 5]);
1110
1111 tree.set_open(0, false);
1114 assert_eq!(shown(&tree), alloc::vec![0, 4, 5]);
1115
1116 tree.set_open(0, true);
1119 assert_eq!(shown(&tree), alloc::vec![0, 1, 3, 4, 5]);
1120 }
1121
1122 #[test]
1123 fn a_row_with_no_children_is_never_shut_around() {
1124 let mut items = items();
1126 items[2].open = false;
1127 let tree = Tree::new(items, |row| row);
1128 assert_eq!(shown(&tree), alloc::vec![0, 1, 2, 3, 4, 5]);
1129 }
1130
1131 #[test]
1132 fn the_keyboard_walks_the_rows_that_are_shown() {
1133 let mut tree = tree();
1134 tree.set_open(1, false);
1135
1136 assert_eq!(step(&tree.items, Some(1), true), Some(3));
1138 assert_eq!(step(&tree.items, Some(3), false), Some(1));
1140
1141 assert_eq!(step(&tree.items, Some(5), true), None);
1143 assert_eq!(step(&tree.items, Some(0), false), None);
1144
1145 assert_eq!(step(&tree.items, None, true), Some(0));
1147 assert_eq!(step(&tree.items, None, false), Some(5));
1148 }
1149
1150 #[test]
1151 fn disabled_rows_are_stepped_over_and_hidden_ones_are_not_reachable() {
1152 let mut items = items();
1153 items[1].enabled = false;
1154 items[3].enabled = false;
1155 let tree = Tree::new(items, |row| row);
1156
1157 assert_eq!(step(&tree.items, Some(0), true), Some(2));
1160 assert_eq!(step(&tree.items, Some(2), true), Some(4));
1161
1162 assert_eq!(first_enabled(&tree.items), Some(0));
1163 assert_eq!(last_enabled(&tree.items), Some(5));
1164 }
1165
1166 #[test]
1167 fn a_selection_under_a_branch_that_shuts_moves_to_the_branch() {
1168 let mut tree = tree();
1169 tree.set_selected(Some(2));
1170 assert_eq!(tree.selected(), Some(2));
1171
1172 tree.set_open(1, false);
1175 assert_eq!(tree.selected(), None, "a hidden row stayed selected");
1176 }
1177
1178 #[test]
1179 fn a_hidden_or_disabled_row_cannot_be_selected() {
1180 let mut tree = tree();
1181 tree.set_open(0, false);
1182
1183 tree.set_selected(Some(2));
1184 assert_eq!(
1185 tree.selected(),
1186 None,
1187 "selected something under a shut branch"
1188 );
1189
1190 tree.set_selected(Some(9));
1191 assert_eq!(tree.selected(), None, "selected a row that is not there");
1192
1193 tree.set_row_enabled(4, false);
1194 tree.set_selected(Some(4));
1195 assert_eq!(tree.selected(), None, "selected a disabled row");
1196 }
1197
1198 #[test]
1199 fn rows_are_a_fixed_height_stacked_from_the_top() {
1200 let bounds = Rect::new(10, 20, 300, 400);
1201 let mut previous = bounds.y;
1202 for nth in 0..6 {
1203 let row = row_rect(bounds, 36, nth);
1204 assert_eq!(row.y, previous);
1205 assert_eq!(row.height, 36);
1206 assert_eq!(row.x, bounds.x);
1207 assert_eq!(row.right(), bounds.right());
1208 previous = row.bottom();
1209 }
1210 }
1211
1212 #[test]
1213 fn an_absurdly_deep_tree_neither_overflows_nor_panics() {
1214 let bounds = Rect::new(0, 0, 300, 400);
1217 let row = row_rect(bounds, 36, usize::MAX / 2);
1218 assert!(row.height > 0);
1219 assert!(row.bottom() >= row.y, "the rectangle inverted");
1220
1221 let deep = Tree::<usize>::inert(alloc::vec![
1222 TreeItem::new("a").at_depth(0),
1223 TreeItem::new("b").at_depth(u16::MAX),
1224 ]);
1225 assert!(
1226 deep.indent_of(&deep.items[1]) > 0,
1227 "the indent saturated wrong"
1228 );
1229 let row = row_rect(bounds, 36, 1);
1230 let (triangle, content) = deep.parts(row, &deep.items[1]);
1231 assert!(
1232 triangle.width >= 0 && content.width >= 0,
1233 "a column inverted"
1234 );
1235 assert!(content.right() <= row.right(), "a column left the row");
1236 }
1237
1238 #[test]
1239 fn the_triangle_is_its_own_target_and_the_rest_of_the_row_is_not() {
1240 let tree = tree();
1241 let bounds = Rect::new(0, 0, 300, 240);
1242 let row = row_rect(bounds, 40, 0);
1243 let (triangle, _) = tree.parts(row, &tree.items[0]);
1244
1245 let on_triangle = Point::new(triangle.x + triangle.width / 2, row.y + row.height / 2);
1246 assert_eq!(tree.hit(bounds, 40, on_triangle), Some((0, true)));
1247
1248 let on_label = Point::new(row.right() - 10, row.y + row.height / 2);
1249 assert_eq!(tree.hit(bounds, 40, on_label), Some((0, false)));
1250
1251 let leaf_row = row_rect(bounds, 40, 2);
1253 let (leaf_triangle, _) = tree.parts(leaf_row, &tree.items[2]);
1254 let on_nothing = Point::new(
1255 leaf_triangle.x + leaf_triangle.width / 2,
1256 leaf_row.y + leaf_row.height / 2,
1257 );
1258 assert_eq!(tree.hit(bounds, 40, on_nothing), Some((2, false)));
1259 }
1260
1261 #[test]
1262 fn a_deeper_rows_triangle_is_indented_with_it() {
1263 let tree = tree();
1264 let bounds = Rect::new(0, 0, 300, 240);
1265 let (top, _) = tree.parts(row_rect(bounds, 40, 0), &tree.items[0]);
1266 let (nested, _) = tree.parts(row_rect(bounds, 40, 1), &tree.items[1]);
1267 assert_eq!(nested.x - top.x, INDENT, "one level is one indent");
1268
1269 let (deeper, _) = tree.parts(row_rect(bounds, 40, 2), &tree.items[2]);
1270 assert_eq!(deeper.x - top.x, INDENT * 2);
1271 }
1272
1273 #[test]
1274 fn a_point_below_the_last_shown_row_is_not_the_last_row() {
1275 let mut tree = tree();
1276 tree.set_open(0, false);
1277 tree.set_open(4, false);
1278 let bounds = Rect::new(0, 0, 300, 400);
1279 assert_eq!(tree.shown_rows(), 2);
1281 let below = Point::new(50, bounds.y + 40 * 2 + 5);
1282 assert_eq!(tree.hit(bounds, 40, below), None);
1283 }
1284
1285 #[test]
1286 fn the_height_it_asks_for_follows_what_is_open() {
1287 let theme = &denise::theme::DARK;
1288 let mut tree = tree();
1289 tree = tree.with_row_height(20);
1290 assert_eq!(tree.preferred_height(theme), 120, "six rows");
1291
1292 tree.set_open(0, false);
1293 assert_eq!(tree.preferred_height(theme), 60, "three rows");
1294
1295 tree.set_all_open(false);
1298 assert!(tree.preferred_height(theme) > 0);
1299 }
1300
1301 #[test]
1302 fn an_empty_tree_is_inert_rather_than_broken() {
1303 let tree = Tree::<usize>::inert(Vec::<TreeItem>::new());
1304 assert_eq!(tree.shown_rows(), 0);
1305 assert_eq!(tree.selected(), None);
1306 assert!(
1307 !tree.focusable(),
1308 "an empty tree is a tab stop with nothing in it"
1309 );
1310 assert_eq!(
1311 tree.hit(Rect::new(0, 0, 100, 100), 20, Point::new(5, 5)),
1312 None
1313 );
1314 assert!(tree.preferred_height(&denise::theme::DARK) > 0);
1315 }
1316
1317 #[test]
1318 fn replacing_the_rows_keeps_a_selection_that_still_makes_sense() {
1319 let mut tree = tree();
1320 tree.set_selected(Some(3));
1321
1322 tree.set_items(items());
1323 assert_eq!(tree.selected(), Some(3), "the same row is still there");
1324
1325 tree.set_items(alloc::vec![TreeItem::new("only")]);
1327 assert_eq!(tree.selected(), None);
1328 }
1329}