1mod node;
10mod drag;
11pub mod row;
12
13pub use node::{NodeIcon, TreeNode};
14pub use row::{ExpandToggle, NodeIconWidget, TreeRow};
15use node::{DragState, DropPosition, FlatRow, flatten_visible};
16use drag::{apply_drop, compute_drop_target, paint_drop_child_highlight,
17 paint_drop_line, paint_ghost};
18use row::{EXPAND_W, icon_color};
19
20use std::sync::Arc;
21
22use crate::event::{Event, EventResult, Key, Modifiers, MouseButton};
23use crate::geometry::{Point, Rect, Size};
24use crate::draw_ctx::DrawCtx;
25use crate::layout_props::{HAnchor, Insets, VAnchor, WidgetBase};
26use crate::text::Font;
27use crate::widget::Widget;
28
29const SCROLLBAR_W: f64 = 10.0;
30const DRAG_THRESHOLD: f64 = 4.0;
31
32struct RowMeta {
38 node_idx: usize,
40 toggle_rect: Option<Rect>,
43}
44
45pub struct TreeView {
50 bounds: Rect,
51 row_widgets: Vec<Box<dyn Widget>>,
53 base: WidgetBase,
54 row_metas: Vec<RowMeta>,
56
57 pub nodes: Vec<TreeNode>,
58
59 scroll_offset: f64,
61 content_height: f64,
62
63 pub row_height: f64,
65 pub indent_width: f64,
66 pub font: Arc<Font>,
67 pub font_size: f64,
68
69 pub drag_enabled: bool,
71 pub toggle_on_row_click: bool,
79 focused: bool,
80 hovered_row: Option<usize>,
82 cursor_node: Option<usize>,
84 drag: Option<DragState>,
86 drop_target: Option<DropPosition>,
88
89 hovered_scrollbar: bool,
91 dragging_scrollbar: bool,
92 sb_drag_start_y: f64,
93 sb_drag_start_offset: f64,
94}
95
96impl TreeView {
101 pub fn new(font: Arc<Font>) -> Self {
102 Self {
103 bounds: Rect::default(),
104 row_widgets: Vec::new(),
105 base: WidgetBase::new(),
106 row_metas: Vec::new(),
107 nodes: Vec::new(),
108 scroll_offset: 0.0,
109 content_height: 0.0,
110 row_height: 24.0,
111 indent_width: 16.0,
112 font,
113 font_size: 13.0,
114 drag_enabled: false,
115 toggle_on_row_click: false,
116 focused: false,
117 hovered_row: None,
118 cursor_node: None,
119 drag: None,
120 drop_target: None,
121 hovered_scrollbar: false,
122 dragging_scrollbar: false,
123 sb_drag_start_y: 0.0,
124 sb_drag_start_offset: 0.0,
125 }
126 }
127
128 pub fn with_row_height(mut self, h: f64) -> Self { self.row_height = h; self }
129 pub fn with_indent_width(mut self, w: f64) -> Self { self.indent_width = w; self }
130 pub fn with_font_size(mut self, s: f64) -> Self { self.font_size = s; self }
131 pub fn with_drag_enabled(mut self) -> Self { self.drag_enabled = true; self }
132 pub fn with_toggle_on_row_click(mut self) -> Self { self.toggle_on_row_click = true; self }
133
134 pub fn with_margin(mut self, m: Insets) -> Self { self.base.margin = m; self }
135 pub fn with_h_anchor(mut self, h: HAnchor) -> Self { self.base.h_anchor = h; self }
136 pub fn with_v_anchor(mut self, v: VAnchor) -> Self { self.base.v_anchor = v; self }
137 pub fn with_min_size(mut self, s: Size) -> Self { self.base.min_size = s; self }
138 pub fn with_max_size(mut self, s: Size) -> Self { self.base.max_size = s; self }
139
140 pub fn add_root(&mut self, label: impl Into<String>, icon: NodeIcon) -> usize {
142 let order = self.nodes.iter().filter(|n| n.parent.is_none()).count() as u32;
143 let idx = self.nodes.len();
144 self.nodes.push(TreeNode::new(label, icon, None, order));
145 idx
146 }
147
148 pub fn add_child(
150 &mut self,
151 parent_idx: usize,
152 label: impl Into<String>,
153 icon: NodeIcon,
154 ) -> usize {
155 let order = self.nodes
156 .iter()
157 .filter(|n| n.parent == Some(parent_idx))
158 .count() as u32;
159 let idx = self.nodes.len();
160 self.nodes.push(TreeNode::new(label, icon, Some(parent_idx), order));
161 idx
162 }
163
164 pub fn expand(&mut self, idx: usize) {
166 if idx < self.nodes.len() { self.nodes[idx].is_expanded = true; }
167 }
168}
169
170impl TreeView {
175 fn scrollbar_x(&self) -> f64 { self.bounds.width - SCROLLBAR_W }
176
177 fn max_scroll(&self) -> f64 {
178 (self.content_height - self.bounds.height).max(0.0)
179 }
180
181 fn thumb_metrics(&self) -> Option<(f64, f64)> {
182 let h = self.bounds.height;
183 if self.content_height <= h { return None; }
184 let ratio = h / self.content_height;
185 let thumb_h = (h * ratio).max(20.0);
186 let track_h = h - thumb_h;
187 let thumb_y = track_h * (1.0 - self.scroll_offset / self.max_scroll());
188 Some((thumb_y, thumb_h))
189 }
190
191 fn in_scrollbar(&self, local_pos: Point) -> bool {
193 local_pos.x >= self.scrollbar_x()
194 }
195
196 fn row_index_at(&self, pos: Point) -> Option<usize> {
199 for (i, widget) in self.row_widgets.iter().enumerate() {
200 let b = widget.bounds();
201 if pos.y >= b.y.max(0.0)
204 && pos.y < (b.y + b.height).min(self.bounds.height)
205 && pos.x >= 0.0
206 && pos.x < self.bounds.width - SCROLLBAR_W
207 {
208 return Some(i);
209 }
210 }
211 None
212 }
213}
214
215impl TreeView {
220 fn select_single(&mut self, node_idx: usize) {
221 for n in &mut self.nodes { n.is_selected = false; }
222 self.nodes[node_idx].is_selected = true;
223 self.cursor_node = Some(node_idx);
224 }
225
226 fn toggle_select(&mut self, node_idx: usize) {
227 self.nodes[node_idx].is_selected = !self.nodes[node_idx].is_selected;
228 self.cursor_node = Some(node_idx);
229 }
230
231 fn range_select(&mut self, anchor_node: usize, target_node: usize, rows: &[FlatRow]) {
232 let a = rows.iter().position(|r| r.node_idx == anchor_node);
233 let b = rows.iter().position(|r| r.node_idx == target_node);
234 if let (Some(a), Some(b)) = (a, b) {
235 let (lo, hi) = if a <= b { (a, b) } else { (b, a) };
236 for n in &mut self.nodes { n.is_selected = false; }
237 for r in &rows[lo..=hi] {
238 self.nodes[r.node_idx].is_selected = true;
239 }
240 }
241 self.cursor_node = Some(target_node);
242 }
243
244 fn move_cursor(&mut self, delta: i32, rows: &[FlatRow]) {
245 if rows.is_empty() { return; }
246 let cur_flat = self.cursor_node
247 .and_then(|ni| rows.iter().position(|r| r.node_idx == ni))
248 .unwrap_or(0);
249 let new_flat = (cur_flat as i32 + delta)
250 .clamp(0, rows.len() as i32 - 1) as usize;
251 let ni = rows[new_flat].node_idx;
252 self.select_single(ni);
253 self.scroll_to_row(new_flat);
255 }
256
257 pub fn hovered_node_idx(&self) -> Option<usize> {
259 self.hovered_row.and_then(|ri| self.row_metas.get(ri).map(|m| m.node_idx))
260 }
261
262 fn scroll_to_row(&mut self, flat_idx: usize) {
263 let y_bottom = self.bounds.height
268 - (flat_idx as f64 + 1.0) * self.row_height
269 + self.scroll_offset;
270 let y_top = y_bottom + self.row_height;
271 if y_bottom < 0.0 {
272 self.scroll_offset = (self.scroll_offset - y_bottom).min(self.max_scroll());
273 } else if y_top > self.bounds.height {
274 self.scroll_offset = (self.scroll_offset - (y_top - self.bounds.height)).max(0.0);
275 }
276 }
277}
278
279impl Widget for TreeView {
284 fn type_name(&self) -> &'static str { "TreeView" }
285 fn bounds(&self) -> Rect { self.bounds }
286 fn set_bounds(&mut self, b: Rect) { self.bounds = b; }
287 fn children(&self) -> &[Box<dyn Widget>] { &self.row_widgets }
288 fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> { &mut self.row_widgets }
289 fn is_focusable(&self) -> bool { true }
290
291 fn margin(&self) -> Insets { self.base.margin }
292 fn h_anchor(&self) -> HAnchor { self.base.h_anchor }
293 fn v_anchor(&self) -> VAnchor { self.base.v_anchor }
294 fn min_size(&self) -> Size { self.base.min_size }
295 fn max_size(&self) -> Size { self.base.max_size }
296
297 fn hit_test(&self, local_pos: Point) -> bool {
298 if self.drag.is_some() || self.dragging_scrollbar { return true; }
300 let b = self.bounds();
301 local_pos.x >= 0.0 && local_pos.x <= b.width
302 && local_pos.y >= 0.0 && local_pos.y <= b.height
303 }
304
305 fn layout(&mut self, available: Size) -> Size {
306 let rows = flatten_visible(&self.nodes);
307 self.content_height = rows.len() as f64 * self.row_height;
308 self.scroll_offset = self.scroll_offset.clamp(0.0, self.max_scroll());
309
310 let h = available.height;
311 let w = available.width - SCROLLBAR_W;
312 let rh = self.row_height;
313 let ind = self.indent_width;
314 let font_size = self.font_size;
315
316 self.row_widgets.clear();
317 self.row_metas.clear();
318
319 for (i, flat) in rows.iter().enumerate() {
320 if self.drag.as_ref().map_or(false, |d| d.live && d.node_idx == flat.node_idx) {
322 continue;
323 }
324
325 let node = &self.nodes[flat.node_idx];
326
327 let y_bot = h - (i as f64 + 1.0) * rh + self.scroll_offset;
329
330 let mut tree_row = TreeRow::new(
331 flat.node_idx,
332 flat.depth,
333 flat.has_children,
334 node.is_expanded,
335 node.is_selected,
336 self.hovered_row == Some(i),
337 self.focused,
338 node.icon,
339 node.label.clone(),
340 Arc::clone(&self.font),
341 font_size,
342 ind,
343 rh,
344 );
345
346 tree_row.layout(Size::new(w, rh));
347 tree_row.set_bounds(Rect::new(0.0, y_bot, w, rh));
348
349 let toggle_rect = if flat.has_children {
351 let tlb = tree_row.toggle_local_bounds;
352 Some(Rect::new(tlb.x, y_bot + tlb.y, tlb.width, tlb.height))
353 } else {
354 None
355 };
356
357 self.row_metas.push(RowMeta { node_idx: flat.node_idx, toggle_rect });
358 self.row_widgets.push(Box::new(tree_row));
359 }
360
361 available
362 }
363
364 fn paint(&mut self, ctx: &mut dyn DrawCtx) {
365 let h = self.bounds.height;
366 let w = self.bounds.width;
367 let content_w = w - SCROLLBAR_W;
368 let v = ctx.visuals().clone();
369
370 ctx.set_fill_color(v.window_fill);
372 ctx.begin_path();
373 ctx.rect(0.0, 0.0, w, h);
374 ctx.fill();
375
376 let sb_x = self.scrollbar_x();
378 if self.content_height > h {
379 ctx.set_fill_color(v.scroll_track);
380 ctx.begin_path();
381 ctx.rect(sb_x, 0.0, SCROLLBAR_W, h);
382 ctx.fill();
383 if let Some((thumb_y, thumb_h)) = self.thumb_metrics() {
384 let thumb_color = if self.dragging_scrollbar {
385 v.scroll_thumb_dragging
386 } else if self.hovered_scrollbar {
387 v.scroll_thumb_hovered
388 } else {
389 v.scroll_thumb
390 };
391 ctx.set_fill_color(thumb_color);
392 ctx.begin_path();
393 ctx.rounded_rect(sb_x + 2.0, thumb_y, SCROLLBAR_W - 4.0, thumb_h, 3.0);
394 ctx.fill();
395 }
396 }
397
398 ctx.clip_rect(0.0, 0.0, content_w, h);
401
402 let rows = flatten_visible(&self.nodes);
404 if let Some(drop_target) = self.drop_target {
405 if self.drag.as_ref().map_or(false, |d| d.live) {
406 let rh = self.row_height;
407 let off = self.scroll_offset;
408 let ind = self.indent_width;
409 let ref_node = match drop_target {
410 DropPosition::Before(ni) | DropPosition::After(ni) | DropPosition::AsChild(ni) => ni,
411 };
412 if let Some(ri) = rows.iter().position(|r| r.node_idx == ref_node) {
413 let y_bot = h - (ri as f64 + 1.0) * rh + off;
414 let indent = rows[ri].depth as f64 * ind + EXPAND_W;
415 match drop_target {
416 DropPosition::Before(_) => paint_drop_line(ctx, indent, y_bot + rh, content_w - indent),
417 DropPosition::After(_) => paint_drop_line(ctx, indent, y_bot, content_w - indent),
418 DropPosition::AsChild(_) => paint_drop_child_highlight(ctx, y_bot, content_w, rh),
419 }
420 }
421 }
422 }
423 if let Some(drag) = &self.drag {
424 if drag.live {
425 let label = self.nodes[drag.node_idx].label.clone();
426 let ic = icon_color(self.nodes[drag.node_idx].icon);
427 let pos = drag.current_pos;
428 let rh = self.row_height;
429 let font = Arc::clone(&self.font);
430 let fs = self.font_size;
431 paint_ghost(ctx, &label, pos, content_w, rh, &font, fs, ic);
432 }
433 }
434 }
435
436 fn on_event(&mut self, event: &Event) -> EventResult {
437 let result = match event {
443 Event::FocusGained => { self.focused = true; EventResult::Consumed }
444 Event::FocusLost => { self.focused = false; EventResult::Consumed }
445
446 Event::MouseWheel { delta_y, .. } => {
447 self.scroll_offset =
450 (self.scroll_offset + delta_y * 40.0).clamp(0.0, self.max_scroll());
451 self.hovered_row = None; EventResult::Consumed
453 }
454
455 Event::MouseMove { pos } => self.handle_mouse_move(*pos),
456 Event::MouseDown { pos, button: MouseButton::Left, modifiers } => {
457 self.handle_mouse_down(*pos, *modifiers)
458 }
459 Event::MouseUp { button: MouseButton::Left, pos, .. } => {
460 self.handle_mouse_up(*pos)
461 }
462 Event::KeyDown { key, modifiers } => self.handle_key_down(key, *modifiers),
463 _ => EventResult::Ignored,
464 };
465 if result == EventResult::Consumed {
466 crate::animation::request_tick();
467 }
468 result
469 }
470}
471
472impl TreeView {
477 fn handle_mouse_move(&mut self, pos: Point) -> EventResult {
478 self.hovered_scrollbar = self.in_scrollbar(pos);
479
480 if self.dragging_scrollbar {
481 if let Some((_, thumb_h)) = self.thumb_metrics() {
482 let h = self.bounds.height;
483 let track_h = (h - thumb_h).max(1.0);
484 let delta_y = self.sb_drag_start_y - pos.y;
485 let spp = self.max_scroll() / track_h;
486 self.scroll_offset =
487 (self.sb_drag_start_offset + delta_y * spp).clamp(0.0, self.max_scroll());
488 }
489 return EventResult::Consumed;
490 }
491
492 if let Some(drag) = &mut self.drag {
493 let dx = pos.x - drag.current_pos.x;
494 let dy = pos.y - drag.current_pos.y;
495 drag.current_pos = pos;
496 if !drag.live && (dx * dx + dy * dy).sqrt() > DRAG_THRESHOLD {
497 drag.live = true;
498 }
499 if drag.live {
500 let node_idx = drag.node_idx;
501 let rows = flatten_visible(&self.nodes);
502 self.drop_target = compute_drop_target(
503 pos, &rows, &self.nodes,
504 self.bounds.height, self.row_height,
505 self.scroll_offset, self.drag.as_ref().unwrap(),
506 );
507 let _ = node_idx;
508 }
509 return EventResult::Consumed;
510 }
511
512 self.hovered_row = self.row_index_at(pos);
513 EventResult::Ignored
514 }
515
516 fn handle_mouse_down(&mut self, pos: Point, mods: Modifiers) -> EventResult {
517 if self.in_scrollbar(pos) {
518 self.dragging_scrollbar = true;
519 self.sb_drag_start_y = pos.y;
520 self.sb_drag_start_offset = self.scroll_offset;
521 return EventResult::Consumed;
522 }
523
524 let Some(flat_i) = self.row_index_at(pos) else {
525 return EventResult::Ignored;
526 };
527 let meta = &self.row_metas[flat_i];
528 let node_idx = meta.node_idx;
529
530 if self.toggle_on_row_click {
536 if meta.toggle_rect.is_some() {
537 self.nodes[node_idx].is_expanded = !self.nodes[node_idx].is_expanded;
538 }
539 } else if let Some(tr) = meta.toggle_rect {
540 if pos.x >= tr.x && pos.x < tr.x + tr.width
541 && pos.y >= tr.y && pos.y < tr.y + tr.height
542 {
543 self.nodes[node_idx].is_expanded = !self.nodes[node_idx].is_expanded;
544 }
545 }
546
547 if mods.ctrl {
549 self.toggle_select(node_idx);
550 } else if mods.shift {
551 if let Some(a) = self.cursor_node {
552 let rows2 = flatten_visible(&self.nodes);
553 self.range_select(a, node_idx, &rows2);
554 } else {
555 self.select_single(node_idx);
556 }
557 } else {
558 self.select_single(node_idx);
559 if self.drag_enabled {
560 let y_bot = self.row_widgets[flat_i].bounds().y;
561 self.drag = Some(DragState {
562 node_idx,
563 _cursor_row_offset: pos.y - y_bot,
564 current_pos: pos,
565 live: false,
566 });
567 }
568 }
569
570 EventResult::Consumed
571 }
572
573 fn handle_mouse_up(&mut self, pos: Point) -> EventResult {
574 if self.dragging_scrollbar {
576 self.dragging_scrollbar = false;
577 return EventResult::Consumed;
578 }
579
580 if let Some(drag) = self.drag.take() {
582 if drag.live {
583 if let Some(target) = self.drop_target.take() {
584 apply_drop(&mut self.nodes, drag.node_idx, target);
585 }
586 } else {
587 self.select_single(drag.node_idx);
589 }
590 self.drop_target = None;
591 return EventResult::Consumed;
592 }
593
594 let _ = pos;
595 EventResult::Ignored
596 }
597
598 fn handle_key_down(&mut self, key: &Key, mods: Modifiers) -> EventResult {
599 let rows = flatten_visible(&self.nodes);
600 match key {
601 Key::ArrowDown => { self.move_cursor(1, &rows); EventResult::Consumed }
602 Key::ArrowUp => { self.move_cursor(-1, &rows); EventResult::Consumed }
603 Key::ArrowRight => {
604 if let Some(ni) = self.cursor_node {
605 if !self.nodes[ni].is_expanded
606 && rows.iter().any(|r| r.node_idx == ni && r.has_children)
607 {
608 self.nodes[ni].is_expanded = true;
609 } else {
610 if rows.iter().any(|r| r.node_idx == ni) {
612 self.move_cursor(1, &rows);
613 }
614 }
615 }
616 EventResult::Consumed
617 }
618 Key::ArrowLeft => {
619 if let Some(ni) = self.cursor_node {
620 if self.nodes[ni].is_expanded {
621 self.nodes[ni].is_expanded = false;
622 } else if let Some(parent_idx) = self.nodes[ni].parent {
623 self.select_single(parent_idx);
624 if let Some(fi) = rows.iter().position(|r| r.node_idx == parent_idx) {
625 self.scroll_to_row(fi);
626 }
627 }
628 }
629 EventResult::Consumed
630 }
631 Key::Char(' ') | Key::Enter => {
632 if let Some(ni) = self.cursor_node {
633 if rows.iter().any(|r| r.node_idx == ni && r.has_children) {
634 self.nodes[ni].is_expanded = !self.nodes[ni].is_expanded;
635 }
636 }
637 EventResult::Consumed
638 }
639 Key::Tab => EventResult::Ignored, _ => {
641 let _ = mods;
642 EventResult::Ignored
643 }
644 }
645 }
646}