mod node;
mod drag;
pub mod row;
pub use node::{NodeIcon, TreeNode};
pub use row::{ExpandToggle, NodeIconWidget, TreeRow};
use node::{DragState, DropPosition, FlatRow, flatten_visible};
use drag::{apply_drop, compute_drop_target, paint_drop_child_highlight,
paint_drop_line, paint_ghost};
use row::{EXPAND_W, icon_color};
use std::sync::Arc;
use crate::event::{Event, EventResult, Key, Modifiers, MouseButton};
use crate::geometry::{Point, Rect, Size};
use crate::draw_ctx::DrawCtx;
use crate::layout_props::{HAnchor, Insets, VAnchor, WidgetBase};
use crate::text::Font;
use crate::widget::Widget;
const SCROLLBAR_W: f64 = 10.0;
const DRAG_THRESHOLD: f64 = 4.0;
struct RowMeta {
node_idx: usize,
toggle_rect: Option<Rect>,
}
pub struct TreeView {
bounds: Rect,
row_widgets: Vec<Box<dyn Widget>>,
base: WidgetBase,
row_metas: Vec<RowMeta>,
pub nodes: Vec<TreeNode>,
scroll_offset: f64,
content_height: f64,
pub row_height: f64,
pub indent_width: f64,
pub font: Arc<Font>,
pub font_size: f64,
pub drag_enabled: bool,
pub toggle_on_row_click: bool,
focused: bool,
hovered_row: Option<usize>,
cursor_node: Option<usize>,
drag: Option<DragState>,
drop_target: Option<DropPosition>,
hovered_scrollbar: bool,
dragging_scrollbar: bool,
sb_drag_start_y: f64,
sb_drag_start_offset: f64,
}
impl TreeView {
pub fn new(font: Arc<Font>) -> Self {
Self {
bounds: Rect::default(),
row_widgets: Vec::new(),
base: WidgetBase::new(),
row_metas: Vec::new(),
nodes: Vec::new(),
scroll_offset: 0.0,
content_height: 0.0,
row_height: 24.0,
indent_width: 16.0,
font,
font_size: 13.0,
drag_enabled: false,
toggle_on_row_click: false,
focused: false,
hovered_row: None,
cursor_node: None,
drag: None,
drop_target: None,
hovered_scrollbar: false,
dragging_scrollbar: false,
sb_drag_start_y: 0.0,
sb_drag_start_offset: 0.0,
}
}
pub fn with_row_height(mut self, h: f64) -> Self { self.row_height = h; self }
pub fn with_indent_width(mut self, w: f64) -> Self { self.indent_width = w; self }
pub fn with_font_size(mut self, s: f64) -> Self { self.font_size = s; self }
pub fn with_drag_enabled(mut self) -> Self { self.drag_enabled = true; self }
pub fn with_toggle_on_row_click(mut self) -> Self { self.toggle_on_row_click = true; self }
pub fn with_margin(mut self, m: Insets) -> Self { self.base.margin = m; self }
pub fn with_h_anchor(mut self, h: HAnchor) -> Self { self.base.h_anchor = h; self }
pub fn with_v_anchor(mut self, v: VAnchor) -> Self { self.base.v_anchor = v; self }
pub fn with_min_size(mut self, s: Size) -> Self { self.base.min_size = s; self }
pub fn with_max_size(mut self, s: Size) -> Self { self.base.max_size = s; self }
pub fn add_root(&mut self, label: impl Into<String>, icon: NodeIcon) -> usize {
let order = self.nodes.iter().filter(|n| n.parent.is_none()).count() as u32;
let idx = self.nodes.len();
self.nodes.push(TreeNode::new(label, icon, None, order));
idx
}
pub fn add_child(
&mut self,
parent_idx: usize,
label: impl Into<String>,
icon: NodeIcon,
) -> usize {
let order = self.nodes
.iter()
.filter(|n| n.parent == Some(parent_idx))
.count() as u32;
let idx = self.nodes.len();
self.nodes.push(TreeNode::new(label, icon, Some(parent_idx), order));
idx
}
pub fn expand(&mut self, idx: usize) {
if idx < self.nodes.len() { self.nodes[idx].is_expanded = true; }
}
}
impl TreeView {
fn scrollbar_x(&self) -> f64 { self.bounds.width - SCROLLBAR_W }
fn max_scroll(&self) -> f64 {
(self.content_height - self.bounds.height).max(0.0)
}
fn thumb_metrics(&self) -> Option<(f64, f64)> {
let h = self.bounds.height;
if self.content_height <= h { return None; }
let ratio = h / self.content_height;
let thumb_h = (h * ratio).max(20.0);
let track_h = h - thumb_h;
let thumb_y = track_h * (1.0 - self.scroll_offset / self.max_scroll());
Some((thumb_y, thumb_h))
}
fn in_scrollbar(&self, local_pos: Point) -> bool {
local_pos.x >= self.scrollbar_x()
}
fn row_index_at(&self, pos: Point) -> Option<usize> {
for (i, widget) in self.row_widgets.iter().enumerate() {
let b = widget.bounds();
if pos.y >= b.y.max(0.0)
&& pos.y < (b.y + b.height).min(self.bounds.height)
&& pos.x >= 0.0
&& pos.x < self.bounds.width - SCROLLBAR_W
{
return Some(i);
}
}
None
}
}
impl TreeView {
fn select_single(&mut self, node_idx: usize) {
for n in &mut self.nodes { n.is_selected = false; }
self.nodes[node_idx].is_selected = true;
self.cursor_node = Some(node_idx);
}
fn toggle_select(&mut self, node_idx: usize) {
self.nodes[node_idx].is_selected = !self.nodes[node_idx].is_selected;
self.cursor_node = Some(node_idx);
}
fn range_select(&mut self, anchor_node: usize, target_node: usize, rows: &[FlatRow]) {
let a = rows.iter().position(|r| r.node_idx == anchor_node);
let b = rows.iter().position(|r| r.node_idx == target_node);
if let (Some(a), Some(b)) = (a, b) {
let (lo, hi) = if a <= b { (a, b) } else { (b, a) };
for n in &mut self.nodes { n.is_selected = false; }
for r in &rows[lo..=hi] {
self.nodes[r.node_idx].is_selected = true;
}
}
self.cursor_node = Some(target_node);
}
fn move_cursor(&mut self, delta: i32, rows: &[FlatRow]) {
if rows.is_empty() { return; }
let cur_flat = self.cursor_node
.and_then(|ni| rows.iter().position(|r| r.node_idx == ni))
.unwrap_or(0);
let new_flat = (cur_flat as i32 + delta)
.clamp(0, rows.len() as i32 - 1) as usize;
let ni = rows[new_flat].node_idx;
self.select_single(ni);
self.scroll_to_row(new_flat);
}
pub fn hovered_node_idx(&self) -> Option<usize> {
self.hovered_row.and_then(|ri| self.row_metas.get(ri).map(|m| m.node_idx))
}
fn scroll_to_row(&mut self, flat_idx: usize) {
let y_bottom = self.bounds.height
- (flat_idx as f64 + 1.0) * self.row_height
+ self.scroll_offset;
let y_top = y_bottom + self.row_height;
if y_bottom < 0.0 {
self.scroll_offset = (self.scroll_offset - y_bottom).min(self.max_scroll());
} else if y_top > self.bounds.height {
self.scroll_offset = (self.scroll_offset - (y_top - self.bounds.height)).max(0.0);
}
}
}
impl Widget for TreeView {
fn type_name(&self) -> &'static str { "TreeView" }
fn bounds(&self) -> Rect { self.bounds }
fn set_bounds(&mut self, b: Rect) { self.bounds = b; }
fn children(&self) -> &[Box<dyn Widget>] { &self.row_widgets }
fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> { &mut self.row_widgets }
fn is_focusable(&self) -> bool { true }
fn margin(&self) -> Insets { self.base.margin }
fn h_anchor(&self) -> HAnchor { self.base.h_anchor }
fn v_anchor(&self) -> VAnchor { self.base.v_anchor }
fn min_size(&self) -> Size { self.base.min_size }
fn max_size(&self) -> Size { self.base.max_size }
fn hit_test(&self, local_pos: Point) -> bool {
if self.drag.is_some() || self.dragging_scrollbar { return true; }
let b = self.bounds();
local_pos.x >= 0.0 && local_pos.x <= b.width
&& local_pos.y >= 0.0 && local_pos.y <= b.height
}
fn layout(&mut self, available: Size) -> Size {
let rows = flatten_visible(&self.nodes);
self.content_height = rows.len() as f64 * self.row_height;
self.scroll_offset = self.scroll_offset.clamp(0.0, self.max_scroll());
let h = available.height;
let w = available.width - SCROLLBAR_W;
let rh = self.row_height;
let ind = self.indent_width;
let font_size = self.font_size;
self.row_widgets.clear();
self.row_metas.clear();
for (i, flat) in rows.iter().enumerate() {
if self.drag.as_ref().map_or(false, |d| d.live && d.node_idx == flat.node_idx) {
continue;
}
let node = &self.nodes[flat.node_idx];
let y_bot = h - (i as f64 + 1.0) * rh + self.scroll_offset;
let mut tree_row = TreeRow::new(
flat.node_idx,
flat.depth,
flat.has_children,
node.is_expanded,
node.is_selected,
self.hovered_row == Some(i),
self.focused,
node.icon,
node.label.clone(),
Arc::clone(&self.font),
font_size,
ind,
rh,
);
tree_row.layout(Size::new(w, rh));
tree_row.set_bounds(Rect::new(0.0, y_bot, w, rh));
let toggle_rect = if flat.has_children {
let tlb = tree_row.toggle_local_bounds;
Some(Rect::new(tlb.x, y_bot + tlb.y, tlb.width, tlb.height))
} else {
None
};
self.row_metas.push(RowMeta { node_idx: flat.node_idx, toggle_rect });
self.row_widgets.push(Box::new(tree_row));
}
available
}
fn paint(&mut self, ctx: &mut dyn DrawCtx) {
let h = self.bounds.height;
let w = self.bounds.width;
let content_w = w - SCROLLBAR_W;
let v = ctx.visuals().clone();
ctx.set_fill_color(v.window_fill);
ctx.begin_path();
ctx.rect(0.0, 0.0, w, h);
ctx.fill();
let sb_x = self.scrollbar_x();
if self.content_height > h {
ctx.set_fill_color(v.scroll_track);
ctx.begin_path();
ctx.rect(sb_x, 0.0, SCROLLBAR_W, h);
ctx.fill();
if let Some((thumb_y, thumb_h)) = self.thumb_metrics() {
let thumb_color = if self.dragging_scrollbar {
v.scroll_thumb_dragging
} else if self.hovered_scrollbar {
v.scroll_thumb_hovered
} else {
v.scroll_thumb
};
ctx.set_fill_color(thumb_color);
ctx.begin_path();
ctx.rounded_rect(sb_x + 2.0, thumb_y, SCROLLBAR_W - 4.0, thumb_h, 3.0);
ctx.fill();
}
}
ctx.clip_rect(0.0, 0.0, content_w, h);
let rows = flatten_visible(&self.nodes);
if let Some(drop_target) = self.drop_target {
if self.drag.as_ref().map_or(false, |d| d.live) {
let rh = self.row_height;
let off = self.scroll_offset;
let ind = self.indent_width;
let ref_node = match drop_target {
DropPosition::Before(ni) | DropPosition::After(ni) | DropPosition::AsChild(ni) => ni,
};
if let Some(ri) = rows.iter().position(|r| r.node_idx == ref_node) {
let y_bot = h - (ri as f64 + 1.0) * rh + off;
let indent = rows[ri].depth as f64 * ind + EXPAND_W;
match drop_target {
DropPosition::Before(_) => paint_drop_line(ctx, indent, y_bot + rh, content_w - indent),
DropPosition::After(_) => paint_drop_line(ctx, indent, y_bot, content_w - indent),
DropPosition::AsChild(_) => paint_drop_child_highlight(ctx, y_bot, content_w, rh),
}
}
}
}
if let Some(drag) = &self.drag {
if drag.live {
let label = self.nodes[drag.node_idx].label.clone();
let ic = icon_color(self.nodes[drag.node_idx].icon);
let pos = drag.current_pos;
let rh = self.row_height;
let font = Arc::clone(&self.font);
let fs = self.font_size;
paint_ghost(ctx, &label, pos, content_w, rh, &font, fs, ic);
}
}
}
fn on_event(&mut self, event: &Event) -> EventResult {
let result = match event {
Event::FocusGained => { self.focused = true; EventResult::Consumed }
Event::FocusLost => { self.focused = false; EventResult::Consumed }
Event::MouseWheel { delta_y, .. } => {
self.scroll_offset =
(self.scroll_offset + delta_y * 40.0).clamp(0.0, self.max_scroll());
self.hovered_row = None; EventResult::Consumed
}
Event::MouseMove { pos } => self.handle_mouse_move(*pos),
Event::MouseDown { pos, button: MouseButton::Left, modifiers } => {
self.handle_mouse_down(*pos, *modifiers)
}
Event::MouseUp { button: MouseButton::Left, pos, .. } => {
self.handle_mouse_up(*pos)
}
Event::KeyDown { key, modifiers } => self.handle_key_down(key, *modifiers),
_ => EventResult::Ignored,
};
if result == EventResult::Consumed {
crate::animation::request_tick();
}
result
}
}
impl TreeView {
fn handle_mouse_move(&mut self, pos: Point) -> EventResult {
self.hovered_scrollbar = self.in_scrollbar(pos);
if self.dragging_scrollbar {
if let Some((_, thumb_h)) = self.thumb_metrics() {
let h = self.bounds.height;
let track_h = (h - thumb_h).max(1.0);
let delta_y = self.sb_drag_start_y - pos.y;
let spp = self.max_scroll() / track_h;
self.scroll_offset =
(self.sb_drag_start_offset + delta_y * spp).clamp(0.0, self.max_scroll());
}
return EventResult::Consumed;
}
if let Some(drag) = &mut self.drag {
let dx = pos.x - drag.current_pos.x;
let dy = pos.y - drag.current_pos.y;
drag.current_pos = pos;
if !drag.live && (dx * dx + dy * dy).sqrt() > DRAG_THRESHOLD {
drag.live = true;
}
if drag.live {
let node_idx = drag.node_idx;
let rows = flatten_visible(&self.nodes);
self.drop_target = compute_drop_target(
pos, &rows, &self.nodes,
self.bounds.height, self.row_height,
self.scroll_offset, self.drag.as_ref().unwrap(),
);
let _ = node_idx;
}
return EventResult::Consumed;
}
self.hovered_row = self.row_index_at(pos);
EventResult::Ignored
}
fn handle_mouse_down(&mut self, pos: Point, mods: Modifiers) -> EventResult {
if self.in_scrollbar(pos) {
self.dragging_scrollbar = true;
self.sb_drag_start_y = pos.y;
self.sb_drag_start_offset = self.scroll_offset;
return EventResult::Consumed;
}
let Some(flat_i) = self.row_index_at(pos) else {
return EventResult::Ignored;
};
let meta = &self.row_metas[flat_i];
let node_idx = meta.node_idx;
if self.toggle_on_row_click {
if meta.toggle_rect.is_some() {
self.nodes[node_idx].is_expanded = !self.nodes[node_idx].is_expanded;
}
} else if let Some(tr) = meta.toggle_rect {
if pos.x >= tr.x && pos.x < tr.x + tr.width
&& pos.y >= tr.y && pos.y < tr.y + tr.height
{
self.nodes[node_idx].is_expanded = !self.nodes[node_idx].is_expanded;
}
}
if mods.ctrl {
self.toggle_select(node_idx);
} else if mods.shift {
if let Some(a) = self.cursor_node {
let rows2 = flatten_visible(&self.nodes);
self.range_select(a, node_idx, &rows2);
} else {
self.select_single(node_idx);
}
} else {
self.select_single(node_idx);
if self.drag_enabled {
let y_bot = self.row_widgets[flat_i].bounds().y;
self.drag = Some(DragState {
node_idx,
_cursor_row_offset: pos.y - y_bot,
current_pos: pos,
live: false,
});
}
}
EventResult::Consumed
}
fn handle_mouse_up(&mut self, pos: Point) -> EventResult {
if self.dragging_scrollbar {
self.dragging_scrollbar = false;
return EventResult::Consumed;
}
if let Some(drag) = self.drag.take() {
if drag.live {
if let Some(target) = self.drop_target.take() {
apply_drop(&mut self.nodes, drag.node_idx, target);
}
} else {
self.select_single(drag.node_idx);
}
self.drop_target = None;
return EventResult::Consumed;
}
let _ = pos;
EventResult::Ignored
}
fn handle_key_down(&mut self, key: &Key, mods: Modifiers) -> EventResult {
let rows = flatten_visible(&self.nodes);
match key {
Key::ArrowDown => { self.move_cursor(1, &rows); EventResult::Consumed }
Key::ArrowUp => { self.move_cursor(-1, &rows); EventResult::Consumed }
Key::ArrowRight => {
if let Some(ni) = self.cursor_node {
if !self.nodes[ni].is_expanded
&& rows.iter().any(|r| r.node_idx == ni && r.has_children)
{
self.nodes[ni].is_expanded = true;
} else {
if rows.iter().any(|r| r.node_idx == ni) {
self.move_cursor(1, &rows);
}
}
}
EventResult::Consumed
}
Key::ArrowLeft => {
if let Some(ni) = self.cursor_node {
if self.nodes[ni].is_expanded {
self.nodes[ni].is_expanded = false;
} else if let Some(parent_idx) = self.nodes[ni].parent {
self.select_single(parent_idx);
if let Some(fi) = rows.iter().position(|r| r.node_idx == parent_idx) {
self.scroll_to_row(fi);
}
}
}
EventResult::Consumed
}
Key::Char(' ') | Key::Enter => {
if let Some(ni) = self.cursor_node {
if rows.iter().any(|r| r.node_idx == ni && r.has_children) {
self.nodes[ni].is_expanded = !self.nodes[ni].is_expanded;
}
}
EventResult::Consumed
}
Key::Tab => EventResult::Ignored, _ => {
let _ = mods;
EventResult::Ignored
}
}
}
}