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 {
145 use std::hash::{Hash, Hasher};
146 let mut h = std::collections::hash_map::DefaultHasher::new();
147 self.nodes.len().hash(&mut h);
148 for n in &self.nodes {
149 n.label.hash(&mut h);
150 n.parent.hash(&mut h);
151 n.order.hash(&mut h);
152 n.is_expanded.hash(&mut h);
153 n.is_selected.hash(&mut h);
154 (n.icon as u8).hash(&mut h);
155 }
156 self.hovered_row.hash(&mut h);
157 self.focused.hash(&mut h);
158 self.drag
160 .as_ref()
161 .map(|d| (d.live, d.node_idx))
162 .hash(&mut h);
163 self.font_size.to_bits().hash(&mut h);
164 self.row_height.to_bits().hash(&mut h);
165 self.indent_width.to_bits().hash(&mut h);
166 h.finish()
167 }
168
169 pub fn with_row_height(mut self, h: f64) -> Self {
170 self.row_height = h;
171 self
172 }
173 pub fn with_indent_width(mut self, w: f64) -> Self {
174 self.indent_width = w;
175 self
176 }
177 pub fn with_font_size(mut self, s: f64) -> Self {
178 self.font_size = s;
179 self
180 }
181 pub fn with_drag_enabled(mut self) -> Self {
182 self.drag_enabled = true;
183 self
184 }
185 pub fn with_toggle_on_row_click(mut self) -> Self {
186 self.toggle_on_row_click = true;
187 self
188 }
189 pub fn with_hover_repaint(mut self, repaint: bool) -> Self {
190 self.hover_repaint = repaint;
191 self
192 }
193
194 pub fn with_margin(mut self, m: Insets) -> Self {
195 self.base.margin = m;
196 self
197 }
198 pub fn with_h_anchor(mut self, h: HAnchor) -> Self {
199 self.base.h_anchor = h;
200 self
201 }
202 pub fn with_v_anchor(mut self, v: VAnchor) -> Self {
203 self.base.v_anchor = v;
204 self
205 }
206 pub fn with_min_size(mut self, s: Size) -> Self {
207 self.base.min_size = s;
208 self
209 }
210 pub fn with_max_size(mut self, s: Size) -> Self {
211 self.base.max_size = s;
212 self
213 }
214
215 pub fn add_root(&mut self, label: impl Into<String>, icon: NodeIcon) -> usize {
217 let order = self.nodes.iter().filter(|n| n.parent.is_none()).count() as u32;
218 let idx = self.nodes.len();
219 self.nodes.push(TreeNode::new(label, icon, None, order));
220 idx
221 }
222
223 pub fn add_child(
225 &mut self,
226 parent_idx: usize,
227 label: impl Into<String>,
228 icon: NodeIcon,
229 ) -> usize {
230 let order = self
231 .nodes
232 .iter()
233 .filter(|n| n.parent == Some(parent_idx))
234 .count() as u32;
235 let idx = self.nodes.len();
236 self.nodes
237 .push(TreeNode::new(label, icon, Some(parent_idx), order));
238 idx
239 }
240
241 pub fn expand(&mut self, idx: usize) {
243 if idx < self.nodes.len() {
244 self.nodes[idx].is_expanded = true;
245 }
246 }
247}
248
249impl TreeView {
254 fn scrollbar_x(&self) -> f64 {
255 self.bounds.width - SCROLLBAR_W
256 }
257
258 fn max_scroll(&self) -> f64 {
259 (self.content_height - self.bounds.height).max(0.0)
260 }
261
262 fn thumb_metrics(&self) -> Option<(f64, f64)> {
263 let h = self.bounds.height;
264 if self.content_height <= h {
265 return None;
266 }
267 let ratio = h / self.content_height;
268 let thumb_h = (h * ratio).max(20.0);
269 let track_h = h - thumb_h;
270 let thumb_y = track_h * (1.0 - self.scroll_offset / self.max_scroll());
271 Some((thumb_y, thumb_h))
272 }
273
274 fn in_scrollbar(&self, local_pos: Point) -> bool {
276 local_pos.x >= self.scrollbar_x()
277 }
278
279 fn row_index_at(&self, pos: Point) -> Option<usize> {
282 for (i, widget) in self.row_widgets.iter().enumerate() {
283 let b = widget.bounds();
284 if pos.y >= b.y.max(0.0)
287 && pos.y < (b.y + b.height).min(self.bounds.height)
288 && pos.x >= 0.0
289 && pos.x < self.bounds.width - SCROLLBAR_W
290 {
291 return Some(i);
292 }
293 }
294 None
295 }
296}
297
298impl TreeView {
303 fn select_single(&mut self, node_idx: usize) {
304 for n in &mut self.nodes {
305 n.is_selected = false;
306 }
307 self.nodes[node_idx].is_selected = true;
308 self.cursor_node = Some(node_idx);
309 }
310
311 fn toggle_select(&mut self, node_idx: usize) {
312 self.nodes[node_idx].is_selected = !self.nodes[node_idx].is_selected;
313 self.cursor_node = Some(node_idx);
314 }
315
316 fn range_select(&mut self, anchor_node: usize, target_node: usize, rows: &[FlatRow]) {
317 let a = rows.iter().position(|r| r.node_idx == anchor_node);
318 let b = rows.iter().position(|r| r.node_idx == target_node);
319 if let (Some(a), Some(b)) = (a, b) {
320 let (lo, hi) = if a <= b { (a, b) } else { (b, a) };
321 for n in &mut self.nodes {
322 n.is_selected = false;
323 }
324 for r in &rows[lo..=hi] {
325 self.nodes[r.node_idx].is_selected = true;
326 }
327 }
328 self.cursor_node = Some(target_node);
329 }
330
331 fn move_cursor(&mut self, delta: i32, rows: &[FlatRow]) {
332 if rows.is_empty() {
333 return;
334 }
335 let cur_flat = self
336 .cursor_node
337 .and_then(|ni| rows.iter().position(|r| r.node_idx == ni))
338 .unwrap_or(0);
339 let new_flat = (cur_flat as i32 + delta).clamp(0, rows.len() as i32 - 1) as usize;
340 let ni = rows[new_flat].node_idx;
341 self.select_single(ni);
342 self.scroll_to_row(new_flat);
344 }
345
346 pub fn hovered_node_idx(&self) -> Option<usize> {
348 self.hovered_row
349 .and_then(|ri| self.row_metas.get(ri).map(|m| m.node_idx))
350 }
351
352 fn scroll_to_row(&mut self, flat_idx: usize) {
353 let y_bottom =
358 self.bounds.height - (flat_idx as f64 + 1.0) * self.row_height + self.scroll_offset;
359 let y_top = y_bottom + self.row_height;
360 if y_bottom < 0.0 {
361 self.scroll_offset = (self.scroll_offset - y_bottom).min(self.max_scroll());
362 } else if y_top > self.bounds.height {
363 self.scroll_offset = (self.scroll_offset - (y_top - self.bounds.height)).max(0.0);
364 }
365 }
366}
367
368impl TreeView {
377 fn handle_mouse_move(&mut self, pos: Point) -> EventResult {
378 let old_hovered_scrollbar = self.hovered_scrollbar;
379 let old_hovered_row = self.hovered_row;
380 self.hovered_scrollbar = self.in_scrollbar(pos);
381
382 if self.dragging_scrollbar {
383 if let Some((_, thumb_h)) = self.thumb_metrics() {
384 let h = self.bounds.height;
385 let track_h = (h - thumb_h).max(1.0);
386 let delta_y = self.sb_drag_start_y - pos.y;
387 let spp = self.max_scroll() / track_h;
388 self.scroll_offset =
389 (self.sb_drag_start_offset + delta_y * spp).clamp(0.0, self.max_scroll());
390 }
391 return EventResult::Consumed;
392 }
393
394 if let Some(drag) = &mut self.drag {
395 let dx = pos.x - drag.current_pos.x;
396 let dy = pos.y - drag.current_pos.y;
397 drag.current_pos = pos;
398 if !drag.live && (dx * dx + dy * dy).sqrt() > DRAG_THRESHOLD {
399 drag.live = true;
400 }
401 if drag.live {
402 let node_idx = drag.node_idx;
403 let rows = flatten_visible(&self.nodes);
404 self.drop_target = compute_drop_target(
405 pos,
406 &rows,
407 &self.nodes,
408 self.bounds.height,
409 self.row_height,
410 self.scroll_offset,
411 self.drag.as_ref().unwrap(),
412 );
413 let _ = node_idx;
414 }
415 return EventResult::Consumed;
416 }
417
418 self.hovered_row = self.row_index_at(pos);
419 if self.hover_repaint
420 && (self.hovered_scrollbar != old_hovered_scrollbar
421 || self.hovered_row != old_hovered_row)
422 {
423 EventResult::Consumed
424 } else {
425 EventResult::Ignored
426 }
427 }
428
429 fn handle_mouse_down(&mut self, pos: Point, mods: Modifiers) -> EventResult {
430 if self.in_scrollbar(pos) {
431 self.dragging_scrollbar = true;
432 self.sb_drag_start_y = pos.y;
433 self.sb_drag_start_offset = self.scroll_offset;
434 return EventResult::Consumed;
435 }
436
437 let Some(flat_i) = self.row_index_at(pos) else {
438 return EventResult::Ignored;
439 };
440 let meta = &self.row_metas[flat_i];
441 let node_idx = meta.node_idx;
442
443 if self.toggle_on_row_click {
449 if meta.toggle_rect.is_some() {
450 self.nodes[node_idx].is_expanded = !self.nodes[node_idx].is_expanded;
451 }
452 } else if let Some(tr) = meta.toggle_rect {
453 if pos.x >= tr.x && pos.x < tr.x + tr.width && pos.y >= tr.y && pos.y < tr.y + tr.height
454 {
455 self.nodes[node_idx].is_expanded = !self.nodes[node_idx].is_expanded;
456 }
457 }
458
459 if mods.ctrl {
461 self.toggle_select(node_idx);
462 } else if mods.shift {
463 if let Some(a) = self.cursor_node {
464 let rows2 = flatten_visible(&self.nodes);
465 self.range_select(a, node_idx, &rows2);
466 } else {
467 self.select_single(node_idx);
468 }
469 } else {
470 self.select_single(node_idx);
471 if self.drag_enabled {
472 let y_bot = self.row_widgets[flat_i].bounds().y;
473 self.drag = Some(DragState {
474 node_idx,
475 _cursor_row_offset: pos.y - y_bot,
476 current_pos: pos,
477 live: false,
478 });
479 }
480 }
481
482 EventResult::Consumed
483 }
484
485 fn handle_mouse_up(&mut self, pos: Point) -> EventResult {
486 if self.dragging_scrollbar {
488 self.dragging_scrollbar = false;
489 return EventResult::Consumed;
490 }
491
492 if let Some(drag) = self.drag.take() {
494 if drag.live {
495 if let Some(target) = self.drop_target.take() {
496 apply_drop(&mut self.nodes, drag.node_idx, target);
497 }
498 } else {
499 self.select_single(drag.node_idx);
501 }
502 self.drop_target = None;
503 return EventResult::Consumed;
504 }
505
506 let _ = pos;
507 EventResult::Ignored
508 }
509
510 fn handle_key_down(&mut self, key: &Key, mods: Modifiers) -> EventResult {
511 let rows = flatten_visible(&self.nodes);
512 match key {
513 Key::ArrowDown => {
514 self.move_cursor(1, &rows);
515 EventResult::Consumed
516 }
517 Key::ArrowUp => {
518 self.move_cursor(-1, &rows);
519 EventResult::Consumed
520 }
521 Key::ArrowRight => {
522 if let Some(ni) = self.cursor_node {
523 if !self.nodes[ni].is_expanded
524 && rows.iter().any(|r| r.node_idx == ni && r.has_children)
525 {
526 self.nodes[ni].is_expanded = true;
527 } else {
528 if rows.iter().any(|r| r.node_idx == ni) {
530 self.move_cursor(1, &rows);
531 }
532 }
533 }
534 EventResult::Consumed
535 }
536 Key::ArrowLeft => {
537 if let Some(ni) = self.cursor_node {
538 if self.nodes[ni].is_expanded {
539 self.nodes[ni].is_expanded = false;
540 } else if let Some(parent_idx) = self.nodes[ni].parent {
541 self.select_single(parent_idx);
542 if let Some(fi) = rows.iter().position(|r| r.node_idx == parent_idx) {
543 self.scroll_to_row(fi);
544 }
545 }
546 }
547 EventResult::Consumed
548 }
549 Key::Char(' ') | Key::Enter => {
550 if let Some(ni) = self.cursor_node {
551 if rows.iter().any(|r| r.node_idx == ni && r.has_children) {
552 self.nodes[ni].is_expanded = !self.nodes[ni].is_expanded;
553 }
554 }
555 EventResult::Consumed
556 }
557 Key::Tab => EventResult::Ignored, _ => {
559 let _ = mods;
560 EventResult::Ignored
561 }
562 }
563 }
564}