agg_gui/widgets/tree_view/
mod.rs1mod drag;
10mod node;
11pub mod row;
12mod widget_impl;
13
14use drag::{apply_drop, compute_drop_target};
15use node::{flatten_visible, DragState, DropPosition, FlatRow};
16pub use node::{NodeIcon, TreeNode};
17pub use row::{ExpandToggle, NodeIconWidget, TreeRow};
18
19use std::sync::Arc;
20
21use crate::event::{EventResult, Key, Modifiers};
22use crate::geometry::{Point, Rect, Size};
23use crate::layout_props::{HAnchor, Insets, VAnchor, WidgetBase};
24use crate::text::Font;
25use crate::widget::Widget;
26
27const SCROLLBAR_W: f64 = 10.0;
28const DRAG_THRESHOLD: f64 = 4.0;
29
30struct RowMeta {
36 node_idx: usize,
38 toggle_rect: Option<Rect>,
41}
42
43pub struct TreeView {
48 bounds: Rect,
49 row_widgets: Vec<Box<dyn Widget>>,
51 base: WidgetBase,
52 row_metas: Vec<RowMeta>,
54
55 pub nodes: Vec<TreeNode>,
56
57 scroll_offset: f64,
59 content_height: f64,
60
61 pub row_height: f64,
63 pub indent_width: f64,
64 pub font: Arc<Font>,
65 pub font_size: f64,
66
67 pub drag_enabled: bool,
69 pub toggle_on_row_click: bool,
77 hover_repaint: bool,
78 focused: bool,
79 hovered_row: Option<usize>,
81 cursor_node: Option<usize>,
83 drag: Option<DragState>,
85 drop_target: Option<DropPosition>,
87
88 hovered_scrollbar: bool,
90 dragging_scrollbar: bool,
91 sb_drag_start_y: f64,
92 sb_drag_start_offset: f64,
93
94 last_row_content_sig: Option<u64>,
104}
105
106impl TreeView {
111 pub fn new(font: Arc<Font>) -> Self {
112 Self {
113 bounds: Rect::default(),
114 row_widgets: Vec::new(),
115 base: WidgetBase::new(),
116 row_metas: Vec::new(),
117 nodes: Vec::new(),
118 scroll_offset: 0.0,
119 content_height: 0.0,
120 row_height: 24.0,
121 indent_width: 16.0,
122 font,
123 font_size: 13.0,
124 drag_enabled: false,
125 toggle_on_row_click: false,
126 hover_repaint: true,
127 focused: false,
128 hovered_row: None,
129 cursor_node: None,
130 drag: None,
131 drop_target: None,
132 hovered_scrollbar: false,
133 dragging_scrollbar: false,
134 sb_drag_start_y: 0.0,
135 sb_drag_start_offset: 0.0,
136 last_row_content_sig: None,
137 }
138 }
139
140 fn row_content_signature(&self) -> u64 {
151 use std::hash::{Hash, Hasher};
152 let mut h = std::collections::hash_map::DefaultHasher::new();
153 self.nodes.len().hash(&mut h);
154 for n in &self.nodes {
155 n.label.hash(&mut h);
156 n.parent.hash(&mut h);
157 n.order.hash(&mut h);
158 n.is_expanded.hash(&mut h);
159 n.is_selected.hash(&mut h);
160 (n.icon as u8).hash(&mut h);
161 }
162 self.focused.hash(&mut h);
163 self.drag
165 .as_ref()
166 .map(|d| (d.live, d.node_idx))
167 .hash(&mut h);
168 self.font_size.to_bits().hash(&mut h);
169 self.row_height.to_bits().hash(&mut h);
170 self.indent_width.to_bits().hash(&mut h);
171 h.finish()
172 }
173
174 pub fn with_row_height(mut self, h: f64) -> Self {
175 self.row_height = h;
176 self
177 }
178 pub fn with_indent_width(mut self, w: f64) -> Self {
179 self.indent_width = w;
180 self
181 }
182 pub fn with_font_size(mut self, s: f64) -> Self {
183 self.font_size = s;
184 self
185 }
186 pub fn with_drag_enabled(mut self) -> Self {
187 self.drag_enabled = true;
188 self
189 }
190 pub fn with_toggle_on_row_click(mut self) -> Self {
191 self.toggle_on_row_click = true;
192 self
193 }
194 pub fn with_hover_repaint(mut self, repaint: bool) -> Self {
195 self.hover_repaint = repaint;
196 self
197 }
198
199 pub fn with_margin(mut self, m: Insets) -> Self {
200 self.base.margin = m;
201 self
202 }
203 pub fn with_h_anchor(mut self, h: HAnchor) -> Self {
204 self.base.h_anchor = h;
205 self
206 }
207 pub fn with_v_anchor(mut self, v: VAnchor) -> Self {
208 self.base.v_anchor = v;
209 self
210 }
211 pub fn with_min_size(mut self, s: Size) -> Self {
212 self.base.min_size = s;
213 self
214 }
215 pub fn with_max_size(mut self, s: Size) -> Self {
216 self.base.max_size = s;
217 self
218 }
219
220 pub fn add_root(&mut self, label: impl Into<String>, icon: NodeIcon) -> usize {
222 let order = self.nodes.iter().filter(|n| n.parent.is_none()).count() as u32;
223 let idx = self.nodes.len();
224 self.nodes.push(TreeNode::new(label, icon, None, order));
225 idx
226 }
227
228 pub fn add_child(
230 &mut self,
231 parent_idx: usize,
232 label: impl Into<String>,
233 icon: NodeIcon,
234 ) -> usize {
235 let order = self
236 .nodes
237 .iter()
238 .filter(|n| n.parent == Some(parent_idx))
239 .count() as u32;
240 let idx = self.nodes.len();
241 self.nodes
242 .push(TreeNode::new(label, icon, Some(parent_idx), order));
243 idx
244 }
245
246 pub fn expand(&mut self, idx: usize) {
248 if idx < self.nodes.len() {
249 self.nodes[idx].is_expanded = true;
250 }
251 }
252}
253
254impl TreeView {
259 fn scrollbar_x(&self) -> f64 {
260 self.bounds.width - SCROLLBAR_W
261 }
262
263 fn max_scroll(&self) -> f64 {
264 (self.content_height - self.bounds.height).max(0.0)
265 }
266
267 fn thumb_metrics(&self) -> Option<(f64, f64)> {
268 let h = self.bounds.height;
269 if self.content_height <= h {
270 return None;
271 }
272 let ratio = h / self.content_height;
273 let thumb_h = (h * ratio).max(20.0);
274 let track_h = h - thumb_h;
275 let thumb_y = track_h * (1.0 - self.scroll_offset / self.max_scroll());
276 Some((thumb_y, thumb_h))
277 }
278
279 fn in_scrollbar(&self, local_pos: Point) -> bool {
281 local_pos.x >= self.scrollbar_x()
282 }
283
284 fn row_index_at(&self, pos: Point) -> Option<usize> {
287 for (i, widget) in self.row_widgets.iter().enumerate() {
288 let b = widget.bounds();
289 if pos.y >= b.y.max(0.0)
292 && pos.y < (b.y + b.height).min(self.bounds.height)
293 && pos.x >= 0.0
294 && pos.x < self.bounds.width - SCROLLBAR_W
295 {
296 return Some(i);
297 }
298 }
299 None
300 }
301}
302
303impl TreeView {
308 fn select_single(&mut self, node_idx: usize) {
309 for n in &mut self.nodes {
310 n.is_selected = false;
311 }
312 self.nodes[node_idx].is_selected = true;
313 self.cursor_node = Some(node_idx);
314 }
315
316 fn toggle_select(&mut self, node_idx: usize) {
317 self.nodes[node_idx].is_selected = !self.nodes[node_idx].is_selected;
318 self.cursor_node = Some(node_idx);
319 }
320
321 fn range_select(&mut self, anchor_node: usize, target_node: usize, rows: &[FlatRow]) {
322 let a = rows.iter().position(|r| r.node_idx == anchor_node);
323 let b = rows.iter().position(|r| r.node_idx == target_node);
324 if let (Some(a), Some(b)) = (a, b) {
325 let (lo, hi) = if a <= b { (a, b) } else { (b, a) };
326 for n in &mut self.nodes {
327 n.is_selected = false;
328 }
329 for r in &rows[lo..=hi] {
330 self.nodes[r.node_idx].is_selected = true;
331 }
332 }
333 self.cursor_node = Some(target_node);
334 }
335
336 fn move_cursor(&mut self, delta: i32, rows: &[FlatRow]) {
337 if rows.is_empty() {
338 return;
339 }
340 let cur_flat = self
341 .cursor_node
342 .and_then(|ni| rows.iter().position(|r| r.node_idx == ni))
343 .unwrap_or(0);
344 let new_flat = (cur_flat as i32 + delta).clamp(0, rows.len() as i32 - 1) as usize;
345 let ni = rows[new_flat].node_idx;
346 self.select_single(ni);
347 self.scroll_to_row(new_flat);
349 }
350
351 pub fn hovered_node_idx(&self) -> Option<usize> {
353 self.hovered_row
354 .and_then(|ri| self.row_metas.get(ri).map(|m| m.node_idx))
355 }
356
357 pub fn clear_hover(&mut self) {
362 if self.hovered_row.is_some() || self.hovered_scrollbar {
363 self.hovered_row = None;
364 self.hovered_scrollbar = false;
365 crate::animation::request_draw();
366 }
367 }
368
369 fn scroll_to_row(&mut self, flat_idx: usize) {
370 let y_bottom =
375 self.bounds.height - (flat_idx as f64 + 1.0) * self.row_height + self.scroll_offset;
376 let y_top = y_bottom + self.row_height;
377 if y_bottom < 0.0 {
378 self.scroll_offset = (self.scroll_offset - y_bottom).min(self.max_scroll());
379 } else if y_top > self.bounds.height {
380 self.scroll_offset = (self.scroll_offset - (y_top - self.bounds.height)).max(0.0);
381 }
382 }
383}
384
385impl TreeView {
394 fn handle_mouse_move(&mut self, pos: Point) -> EventResult {
395 let old_hovered_scrollbar = self.hovered_scrollbar;
396 let old_hovered_row = self.hovered_row;
397 self.hovered_scrollbar = self.in_scrollbar(pos);
398
399 if self.dragging_scrollbar {
400 if let Some((_, thumb_h)) = self.thumb_metrics() {
401 let h = self.bounds.height;
402 let track_h = (h - thumb_h).max(1.0);
403 let delta_y = self.sb_drag_start_y - pos.y;
404 let spp = self.max_scroll() / track_h;
405 self.scroll_offset =
406 (self.sb_drag_start_offset + delta_y * spp).clamp(0.0, self.max_scroll());
407 }
408 return EventResult::Consumed;
409 }
410
411 if let Some(drag) = &mut self.drag {
412 let dx = pos.x - drag.current_pos.x;
413 let dy = pos.y - drag.current_pos.y;
414 drag.current_pos = pos;
415 if !drag.live && (dx * dx + dy * dy).sqrt() > DRAG_THRESHOLD {
416 drag.live = true;
417 }
418 if drag.live {
419 let node_idx = drag.node_idx;
420 let rows = flatten_visible(&self.nodes);
421 self.drop_target = compute_drop_target(
422 pos,
423 &rows,
424 &self.nodes,
425 self.bounds.height,
426 self.row_height,
427 self.scroll_offset,
428 self.drag.as_ref().unwrap(),
429 );
430 let _ = node_idx;
431 }
432 return EventResult::Consumed;
433 }
434
435 self.hovered_row = self.row_index_at(pos);
436 if self.hover_repaint
437 && (self.hovered_scrollbar != old_hovered_scrollbar
438 || self.hovered_row != old_hovered_row)
439 {
440 EventResult::Consumed
441 } else {
442 EventResult::Ignored
443 }
444 }
445
446 fn handle_mouse_down(&mut self, pos: Point, mods: Modifiers) -> EventResult {
447 if self.in_scrollbar(pos) {
448 self.dragging_scrollbar = true;
449 self.sb_drag_start_y = pos.y;
450 self.sb_drag_start_offset = self.scroll_offset;
451 return EventResult::Consumed;
452 }
453
454 let Some(flat_i) = self.row_index_at(pos) else {
455 return EventResult::Ignored;
456 };
457 let meta = &self.row_metas[flat_i];
458 let node_idx = meta.node_idx;
459
460 if self.toggle_on_row_click {
466 if meta.toggle_rect.is_some() {
467 self.nodes[node_idx].is_expanded = !self.nodes[node_idx].is_expanded;
468 }
469 } else if let Some(tr) = meta.toggle_rect {
470 if pos.x >= tr.x && pos.x < tr.x + tr.width && pos.y >= tr.y && pos.y < tr.y + tr.height
471 {
472 self.nodes[node_idx].is_expanded = !self.nodes[node_idx].is_expanded;
473 }
474 }
475
476 if mods.ctrl {
478 self.toggle_select(node_idx);
479 } else if mods.shift {
480 if let Some(a) = self.cursor_node {
481 let rows2 = flatten_visible(&self.nodes);
482 self.range_select(a, node_idx, &rows2);
483 } else {
484 self.select_single(node_idx);
485 }
486 } else {
487 self.select_single(node_idx);
488 if self.drag_enabled {
489 let y_bot = self.row_widgets[flat_i].bounds().y;
490 self.drag = Some(DragState {
491 node_idx,
492 _cursor_row_offset: pos.y - y_bot,
493 current_pos: pos,
494 live: false,
495 });
496 }
497 }
498
499 EventResult::Consumed
500 }
501
502 fn handle_mouse_up(&mut self, pos: Point) -> EventResult {
503 if self.dragging_scrollbar {
505 self.dragging_scrollbar = false;
506 return EventResult::Consumed;
507 }
508
509 if let Some(drag) = self.drag.take() {
511 if drag.live {
512 if let Some(target) = self.drop_target.take() {
513 apply_drop(&mut self.nodes, drag.node_idx, target);
514 }
515 } else {
516 self.select_single(drag.node_idx);
518 }
519 self.drop_target = None;
520 return EventResult::Consumed;
521 }
522
523 let _ = pos;
524 EventResult::Ignored
525 }
526
527 fn handle_key_down(&mut self, key: &Key, mods: Modifiers) -> EventResult {
528 let rows = flatten_visible(&self.nodes);
529 match key {
530 Key::ArrowDown => {
531 self.move_cursor(1, &rows);
532 EventResult::Consumed
533 }
534 Key::ArrowUp => {
535 self.move_cursor(-1, &rows);
536 EventResult::Consumed
537 }
538 Key::ArrowRight => {
539 if let Some(ni) = self.cursor_node {
540 if !self.nodes[ni].is_expanded
541 && rows.iter().any(|r| r.node_idx == ni && r.has_children)
542 {
543 self.nodes[ni].is_expanded = true;
544 } else {
545 if rows.iter().any(|r| r.node_idx == ni) {
547 self.move_cursor(1, &rows);
548 }
549 }
550 }
551 EventResult::Consumed
552 }
553 Key::ArrowLeft => {
554 if let Some(ni) = self.cursor_node {
555 if self.nodes[ni].is_expanded {
556 self.nodes[ni].is_expanded = false;
557 } else if let Some(parent_idx) = self.nodes[ni].parent {
558 self.select_single(parent_idx);
559 if let Some(fi) = rows.iter().position(|r| r.node_idx == parent_idx) {
560 self.scroll_to_row(fi);
561 }
562 }
563 }
564 EventResult::Consumed
565 }
566 Key::Char(' ') | Key::Enter => {
567 if let Some(ni) = self.cursor_node {
568 if rows.iter().any(|r| r.node_idx == ni && r.has_children) {
569 self.nodes[ni].is_expanded = !self.nodes[ni].is_expanded;
570 }
571 }
572 EventResult::Consumed
573 }
574 Key::Tab => EventResult::Ignored, _ => {
576 let _ = mods;
577 EventResult::Ignored
578 }
579 }
580 }
581}