1mod drag;
10mod node;
11pub mod row;
12
13use drag::{
14 apply_drop, compute_drop_target, paint_drop_child_highlight, paint_drop_line, paint_ghost,
15};
16use node::{flatten_visible, DragState, DropPosition, FlatRow};
17pub use node::{NodeIcon, TreeNode};
18use row::{icon_color, EXPAND_W};
19pub use row::{ExpandToggle, NodeIconWidget, TreeRow};
20
21use std::sync::Arc;
22
23use crate::draw_ctx::DrawCtx;
24use crate::event::{Event, EventResult, Key, Modifiers, MouseButton};
25use crate::geometry::{Point, Rect, Size};
26use crate::layout_props::{HAnchor, Insets, VAnchor, WidgetBase};
27use crate::text::Font;
28use crate::widget::Widget;
29
30const SCROLLBAR_W: f64 = 10.0;
31const DRAG_THRESHOLD: f64 = 4.0;
32
33struct RowMeta {
39 node_idx: usize,
41 toggle_rect: Option<Rect>,
44}
45
46pub struct TreeView {
51 bounds: Rect,
52 row_widgets: Vec<Box<dyn Widget>>,
54 base: WidgetBase,
55 row_metas: Vec<RowMeta>,
57
58 pub nodes: Vec<TreeNode>,
59
60 scroll_offset: f64,
62 content_height: f64,
63
64 pub row_height: f64,
66 pub indent_width: f64,
67 pub font: Arc<Font>,
68 pub font_size: f64,
69
70 pub drag_enabled: bool,
72 pub toggle_on_row_click: bool,
80 hover_repaint: bool,
81 focused: bool,
82 hovered_row: Option<usize>,
84 cursor_node: Option<usize>,
86 drag: Option<DragState>,
88 drop_target: Option<DropPosition>,
90
91 hovered_scrollbar: bool,
93 dragging_scrollbar: bool,
94 sb_drag_start_y: f64,
95 sb_drag_start_offset: f64,
96}
97
98impl TreeView {
103 pub fn new(font: Arc<Font>) -> Self {
104 Self {
105 bounds: Rect::default(),
106 row_widgets: Vec::new(),
107 base: WidgetBase::new(),
108 row_metas: Vec::new(),
109 nodes: Vec::new(),
110 scroll_offset: 0.0,
111 content_height: 0.0,
112 row_height: 24.0,
113 indent_width: 16.0,
114 font,
115 font_size: 13.0,
116 drag_enabled: false,
117 toggle_on_row_click: false,
118 hover_repaint: true,
119 focused: false,
120 hovered_row: None,
121 cursor_node: None,
122 drag: None,
123 drop_target: None,
124 hovered_scrollbar: false,
125 dragging_scrollbar: false,
126 sb_drag_start_y: 0.0,
127 sb_drag_start_offset: 0.0,
128 }
129 }
130
131 pub fn with_row_height(mut self, h: f64) -> Self {
132 self.row_height = h;
133 self
134 }
135 pub fn with_indent_width(mut self, w: f64) -> Self {
136 self.indent_width = w;
137 self
138 }
139 pub fn with_font_size(mut self, s: f64) -> Self {
140 self.font_size = s;
141 self
142 }
143 pub fn with_drag_enabled(mut self) -> Self {
144 self.drag_enabled = true;
145 self
146 }
147 pub fn with_toggle_on_row_click(mut self) -> Self {
148 self.toggle_on_row_click = true;
149 self
150 }
151 pub fn with_hover_repaint(mut self, repaint: bool) -> Self {
152 self.hover_repaint = repaint;
153 self
154 }
155
156 pub fn with_margin(mut self, m: Insets) -> Self {
157 self.base.margin = m;
158 self
159 }
160 pub fn with_h_anchor(mut self, h: HAnchor) -> Self {
161 self.base.h_anchor = h;
162 self
163 }
164 pub fn with_v_anchor(mut self, v: VAnchor) -> Self {
165 self.base.v_anchor = v;
166 self
167 }
168 pub fn with_min_size(mut self, s: Size) -> Self {
169 self.base.min_size = s;
170 self
171 }
172 pub fn with_max_size(mut self, s: Size) -> Self {
173 self.base.max_size = s;
174 self
175 }
176
177 pub fn add_root(&mut self, label: impl Into<String>, icon: NodeIcon) -> usize {
179 let order = self.nodes.iter().filter(|n| n.parent.is_none()).count() as u32;
180 let idx = self.nodes.len();
181 self.nodes.push(TreeNode::new(label, icon, None, order));
182 idx
183 }
184
185 pub fn add_child(
187 &mut self,
188 parent_idx: usize,
189 label: impl Into<String>,
190 icon: NodeIcon,
191 ) -> usize {
192 let order = self
193 .nodes
194 .iter()
195 .filter(|n| n.parent == Some(parent_idx))
196 .count() as u32;
197 let idx = self.nodes.len();
198 self.nodes
199 .push(TreeNode::new(label, icon, Some(parent_idx), order));
200 idx
201 }
202
203 pub fn expand(&mut self, idx: usize) {
205 if idx < self.nodes.len() {
206 self.nodes[idx].is_expanded = true;
207 }
208 }
209}
210
211impl TreeView {
216 fn scrollbar_x(&self) -> f64 {
217 self.bounds.width - SCROLLBAR_W
218 }
219
220 fn max_scroll(&self) -> f64 {
221 (self.content_height - self.bounds.height).max(0.0)
222 }
223
224 fn thumb_metrics(&self) -> Option<(f64, f64)> {
225 let h = self.bounds.height;
226 if self.content_height <= h {
227 return None;
228 }
229 let ratio = h / self.content_height;
230 let thumb_h = (h * ratio).max(20.0);
231 let track_h = h - thumb_h;
232 let thumb_y = track_h * (1.0 - self.scroll_offset / self.max_scroll());
233 Some((thumb_y, thumb_h))
234 }
235
236 fn in_scrollbar(&self, local_pos: Point) -> bool {
238 local_pos.x >= self.scrollbar_x()
239 }
240
241 fn row_index_at(&self, pos: Point) -> Option<usize> {
244 for (i, widget) in self.row_widgets.iter().enumerate() {
245 let b = widget.bounds();
246 if pos.y >= b.y.max(0.0)
249 && pos.y < (b.y + b.height).min(self.bounds.height)
250 && pos.x >= 0.0
251 && pos.x < self.bounds.width - SCROLLBAR_W
252 {
253 return Some(i);
254 }
255 }
256 None
257 }
258}
259
260impl TreeView {
265 fn select_single(&mut self, node_idx: usize) {
266 for n in &mut self.nodes {
267 n.is_selected = false;
268 }
269 self.nodes[node_idx].is_selected = true;
270 self.cursor_node = Some(node_idx);
271 }
272
273 fn toggle_select(&mut self, node_idx: usize) {
274 self.nodes[node_idx].is_selected = !self.nodes[node_idx].is_selected;
275 self.cursor_node = Some(node_idx);
276 }
277
278 fn range_select(&mut self, anchor_node: usize, target_node: usize, rows: &[FlatRow]) {
279 let a = rows.iter().position(|r| r.node_idx == anchor_node);
280 let b = rows.iter().position(|r| r.node_idx == target_node);
281 if let (Some(a), Some(b)) = (a, b) {
282 let (lo, hi) = if a <= b { (a, b) } else { (b, a) };
283 for n in &mut self.nodes {
284 n.is_selected = false;
285 }
286 for r in &rows[lo..=hi] {
287 self.nodes[r.node_idx].is_selected = true;
288 }
289 }
290 self.cursor_node = Some(target_node);
291 }
292
293 fn move_cursor(&mut self, delta: i32, rows: &[FlatRow]) {
294 if rows.is_empty() {
295 return;
296 }
297 let cur_flat = self
298 .cursor_node
299 .and_then(|ni| rows.iter().position(|r| r.node_idx == ni))
300 .unwrap_or(0);
301 let new_flat = (cur_flat as i32 + delta).clamp(0, rows.len() as i32 - 1) as usize;
302 let ni = rows[new_flat].node_idx;
303 self.select_single(ni);
304 self.scroll_to_row(new_flat);
306 }
307
308 pub fn hovered_node_idx(&self) -> Option<usize> {
310 self.hovered_row
311 .and_then(|ri| self.row_metas.get(ri).map(|m| m.node_idx))
312 }
313
314 fn scroll_to_row(&mut self, flat_idx: usize) {
315 let y_bottom =
320 self.bounds.height - (flat_idx as f64 + 1.0) * self.row_height + self.scroll_offset;
321 let y_top = y_bottom + self.row_height;
322 if y_bottom < 0.0 {
323 self.scroll_offset = (self.scroll_offset - y_bottom).min(self.max_scroll());
324 } else if y_top > self.bounds.height {
325 self.scroll_offset = (self.scroll_offset - (y_top - self.bounds.height)).max(0.0);
326 }
327 }
328}
329
330impl Widget for TreeView {
335 fn type_name(&self) -> &'static str {
336 "TreeView"
337 }
338 fn bounds(&self) -> Rect {
339 self.bounds
340 }
341 fn set_bounds(&mut self, b: Rect) {
342 self.bounds = b;
343 }
344 fn children(&self) -> &[Box<dyn Widget>] {
345 &self.row_widgets
346 }
347 fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
348 &mut self.row_widgets
349 }
350 fn is_focusable(&self) -> bool {
351 true
352 }
353
354 fn margin(&self) -> Insets {
355 self.base.margin
356 }
357 fn h_anchor(&self) -> HAnchor {
358 self.base.h_anchor
359 }
360 fn v_anchor(&self) -> VAnchor {
361 self.base.v_anchor
362 }
363 fn min_size(&self) -> Size {
364 self.base.min_size
365 }
366 fn max_size(&self) -> Size {
367 self.base.max_size
368 }
369
370 fn hit_test(&self, local_pos: Point) -> bool {
371 if self.drag.is_some() || self.dragging_scrollbar {
373 return true;
374 }
375 let b = self.bounds();
376 local_pos.x >= 0.0
377 && local_pos.x <= b.width
378 && local_pos.y >= 0.0
379 && local_pos.y <= b.height
380 }
381
382 fn layout(&mut self, available: Size) -> Size {
383 let rows = flatten_visible(&self.nodes);
384 self.content_height = rows.len() as f64 * self.row_height;
385 self.scroll_offset = self.scroll_offset.clamp(0.0, self.max_scroll());
386
387 let h = available.height;
388 let w = available.width - SCROLLBAR_W;
389 let rh = self.row_height;
390 let ind = self.indent_width;
391 let font_size = self.font_size;
392
393 self.row_widgets.clear();
394 self.row_metas.clear();
395
396 for (i, flat) in rows.iter().enumerate() {
397 if self
399 .drag
400 .as_ref()
401 .map_or(false, |d| d.live && d.node_idx == flat.node_idx)
402 {
403 continue;
404 }
405
406 let node = &self.nodes[flat.node_idx];
407
408 let y_bot = h - (i as f64 + 1.0) * rh + self.scroll_offset;
410
411 let mut tree_row = TreeRow::new(
412 flat.node_idx,
413 flat.depth,
414 flat.has_children,
415 node.is_expanded,
416 node.is_selected,
417 self.hovered_row == Some(i),
418 self.focused,
419 node.icon,
420 node.label.clone(),
421 Arc::clone(&self.font),
422 font_size,
423 ind,
424 rh,
425 );
426
427 tree_row.layout(Size::new(w, rh));
428 tree_row.set_bounds(Rect::new(0.0, y_bot, w, rh));
429
430 let toggle_rect = if flat.has_children {
432 let tlb = tree_row.toggle_local_bounds;
433 Some(Rect::new(tlb.x, y_bot + tlb.y, tlb.width, tlb.height))
434 } else {
435 None
436 };
437
438 self.row_metas.push(RowMeta {
439 node_idx: flat.node_idx,
440 toggle_rect,
441 });
442 self.row_widgets.push(Box::new(tree_row));
443 }
444
445 available
446 }
447
448 fn paint(&mut self, ctx: &mut dyn DrawCtx) {
449 let h = self.bounds.height;
450 let w = self.bounds.width;
451 let content_w = w - SCROLLBAR_W;
452 let v = ctx.visuals().clone();
453
454 ctx.set_fill_color(v.window_fill);
456 ctx.begin_path();
457 ctx.rect(0.0, 0.0, w, h);
458 ctx.fill();
459
460 let sb_x = self.scrollbar_x();
462 if self.content_height > h {
463 ctx.set_fill_color(v.scroll_track);
464 ctx.begin_path();
465 ctx.rect(sb_x, 0.0, SCROLLBAR_W, h);
466 ctx.fill();
467 if let Some((thumb_y, thumb_h)) = self.thumb_metrics() {
468 let thumb_color = if self.dragging_scrollbar {
469 v.scroll_thumb_dragging
470 } else if self.hovered_scrollbar {
471 v.scroll_thumb_hovered
472 } else {
473 v.scroll_thumb
474 };
475 ctx.set_fill_color(thumb_color);
476 ctx.begin_path();
477 ctx.rounded_rect(sb_x + 2.0, thumb_y, SCROLLBAR_W - 4.0, thumb_h, 3.0);
478 ctx.fill();
479 }
480 }
481
482 ctx.clip_rect(0.0, 0.0, content_w, h);
485
486 let rows = flatten_visible(&self.nodes);
488 if let Some(drop_target) = self.drop_target {
489 if self.drag.as_ref().map_or(false, |d| d.live) {
490 let rh = self.row_height;
491 let off = self.scroll_offset;
492 let ind = self.indent_width;
493 let ref_node = match drop_target {
494 DropPosition::Before(ni)
495 | DropPosition::After(ni)
496 | DropPosition::AsChild(ni) => ni,
497 };
498 if let Some(ri) = rows.iter().position(|r| r.node_idx == ref_node) {
499 let y_bot = h - (ri as f64 + 1.0) * rh + off;
500 let indent = rows[ri].depth as f64 * ind + EXPAND_W;
501 match drop_target {
502 DropPosition::Before(_) => {
503 paint_drop_line(ctx, indent, y_bot + rh, content_w - indent)
504 }
505 DropPosition::After(_) => {
506 paint_drop_line(ctx, indent, y_bot, content_w - indent)
507 }
508 DropPosition::AsChild(_) => {
509 paint_drop_child_highlight(ctx, y_bot, content_w, rh)
510 }
511 }
512 }
513 }
514 }
515 if let Some(drag) = &self.drag {
516 if drag.live {
517 let label = self.nodes[drag.node_idx].label.clone();
518 let ic = icon_color(self.nodes[drag.node_idx].icon);
519 let pos = drag.current_pos;
520 let rh = self.row_height;
521 let font = Arc::clone(&self.font);
522 let fs = self.font_size;
523 paint_ghost(ctx, &label, pos, content_w, rh, &font, fs, ic);
524 }
525 }
526 }
527
528 fn on_event(&mut self, event: &Event) -> EventResult {
529 let result = match event {
535 Event::FocusGained => {
536 self.focused = true;
537 EventResult::Consumed
538 }
539 Event::FocusLost => {
540 self.focused = false;
541 EventResult::Consumed
542 }
543
544 Event::MouseWheel { delta_y, .. } => {
545 self.scroll_offset =
548 (self.scroll_offset + delta_y * 40.0).clamp(0.0, self.max_scroll());
549 self.hovered_row = None; EventResult::Consumed
551 }
552
553 Event::MouseMove { pos } => self.handle_mouse_move(*pos),
554 Event::MouseDown {
555 pos,
556 button: MouseButton::Left,
557 modifiers,
558 } => self.handle_mouse_down(*pos, *modifiers),
559 Event::MouseUp {
560 button: MouseButton::Left,
561 pos,
562 ..
563 } => self.handle_mouse_up(*pos),
564 Event::KeyDown { key, modifiers } => self.handle_key_down(key, *modifiers),
565 _ => EventResult::Ignored,
566 };
567 if result == EventResult::Consumed {
568 crate::animation::request_draw();
569 }
570 result
571 }
572}
573
574impl TreeView {
579 fn handle_mouse_move(&mut self, pos: Point) -> EventResult {
580 let old_hovered_scrollbar = self.hovered_scrollbar;
581 let old_hovered_row = self.hovered_row;
582 self.hovered_scrollbar = self.in_scrollbar(pos);
583
584 if self.dragging_scrollbar {
585 if let Some((_, thumb_h)) = self.thumb_metrics() {
586 let h = self.bounds.height;
587 let track_h = (h - thumb_h).max(1.0);
588 let delta_y = self.sb_drag_start_y - pos.y;
589 let spp = self.max_scroll() / track_h;
590 self.scroll_offset =
591 (self.sb_drag_start_offset + delta_y * spp).clamp(0.0, self.max_scroll());
592 }
593 return EventResult::Consumed;
594 }
595
596 if let Some(drag) = &mut self.drag {
597 let dx = pos.x - drag.current_pos.x;
598 let dy = pos.y - drag.current_pos.y;
599 drag.current_pos = pos;
600 if !drag.live && (dx * dx + dy * dy).sqrt() > DRAG_THRESHOLD {
601 drag.live = true;
602 }
603 if drag.live {
604 let node_idx = drag.node_idx;
605 let rows = flatten_visible(&self.nodes);
606 self.drop_target = compute_drop_target(
607 pos,
608 &rows,
609 &self.nodes,
610 self.bounds.height,
611 self.row_height,
612 self.scroll_offset,
613 self.drag.as_ref().unwrap(),
614 );
615 let _ = node_idx;
616 }
617 return EventResult::Consumed;
618 }
619
620 self.hovered_row = self.row_index_at(pos);
621 if self.hover_repaint
622 && (self.hovered_scrollbar != old_hovered_scrollbar
623 || self.hovered_row != old_hovered_row)
624 {
625 EventResult::Consumed
626 } else {
627 EventResult::Ignored
628 }
629 }
630
631 fn handle_mouse_down(&mut self, pos: Point, mods: Modifiers) -> EventResult {
632 if self.in_scrollbar(pos) {
633 self.dragging_scrollbar = true;
634 self.sb_drag_start_y = pos.y;
635 self.sb_drag_start_offset = self.scroll_offset;
636 return EventResult::Consumed;
637 }
638
639 let Some(flat_i) = self.row_index_at(pos) else {
640 return EventResult::Ignored;
641 };
642 let meta = &self.row_metas[flat_i];
643 let node_idx = meta.node_idx;
644
645 if self.toggle_on_row_click {
651 if meta.toggle_rect.is_some() {
652 self.nodes[node_idx].is_expanded = !self.nodes[node_idx].is_expanded;
653 }
654 } else if let Some(tr) = meta.toggle_rect {
655 if pos.x >= tr.x && pos.x < tr.x + tr.width && pos.y >= tr.y && pos.y < tr.y + tr.height
656 {
657 self.nodes[node_idx].is_expanded = !self.nodes[node_idx].is_expanded;
658 }
659 }
660
661 if mods.ctrl {
663 self.toggle_select(node_idx);
664 } else if mods.shift {
665 if let Some(a) = self.cursor_node {
666 let rows2 = flatten_visible(&self.nodes);
667 self.range_select(a, node_idx, &rows2);
668 } else {
669 self.select_single(node_idx);
670 }
671 } else {
672 self.select_single(node_idx);
673 if self.drag_enabled {
674 let y_bot = self.row_widgets[flat_i].bounds().y;
675 self.drag = Some(DragState {
676 node_idx,
677 _cursor_row_offset: pos.y - y_bot,
678 current_pos: pos,
679 live: false,
680 });
681 }
682 }
683
684 EventResult::Consumed
685 }
686
687 fn handle_mouse_up(&mut self, pos: Point) -> EventResult {
688 if self.dragging_scrollbar {
690 self.dragging_scrollbar = false;
691 return EventResult::Consumed;
692 }
693
694 if let Some(drag) = self.drag.take() {
696 if drag.live {
697 if let Some(target) = self.drop_target.take() {
698 apply_drop(&mut self.nodes, drag.node_idx, target);
699 }
700 } else {
701 self.select_single(drag.node_idx);
703 }
704 self.drop_target = None;
705 return EventResult::Consumed;
706 }
707
708 let _ = pos;
709 EventResult::Ignored
710 }
711
712 fn handle_key_down(&mut self, key: &Key, mods: Modifiers) -> EventResult {
713 let rows = flatten_visible(&self.nodes);
714 match key {
715 Key::ArrowDown => {
716 self.move_cursor(1, &rows);
717 EventResult::Consumed
718 }
719 Key::ArrowUp => {
720 self.move_cursor(-1, &rows);
721 EventResult::Consumed
722 }
723 Key::ArrowRight => {
724 if let Some(ni) = self.cursor_node {
725 if !self.nodes[ni].is_expanded
726 && rows.iter().any(|r| r.node_idx == ni && r.has_children)
727 {
728 self.nodes[ni].is_expanded = true;
729 } else {
730 if rows.iter().any(|r| r.node_idx == ni) {
732 self.move_cursor(1, &rows);
733 }
734 }
735 }
736 EventResult::Consumed
737 }
738 Key::ArrowLeft => {
739 if let Some(ni) = self.cursor_node {
740 if self.nodes[ni].is_expanded {
741 self.nodes[ni].is_expanded = false;
742 } else if let Some(parent_idx) = self.nodes[ni].parent {
743 self.select_single(parent_idx);
744 if let Some(fi) = rows.iter().position(|r| r.node_idx == parent_idx) {
745 self.scroll_to_row(fi);
746 }
747 }
748 }
749 EventResult::Consumed
750 }
751 Key::Char(' ') | Key::Enter => {
752 if let Some(ni) = self.cursor_node {
753 if rows.iter().any(|r| r.node_idx == ni && r.has_children) {
754 self.nodes[ni].is_expanded = !self.nodes[ni].is_expanded;
755 }
756 }
757 EventResult::Consumed
758 }
759 Key::Tab => EventResult::Ignored, _ => {
761 let _ = mods;
762 EventResult::Ignored
763 }
764 }
765 }
766}