use crate::event::{Event, MouseButton, MouseKind};
use crate::geometry::{Rect, Size, clamp_u16};
use crate::keymap::Key;
use crate::style::{CellStyle, WidgetStyle};
use crate::text;
use crate::theme::State;
use crate::widget::{EventCx, MeasureCx, PaintCx, Widget};
use super::click::Click;
use super::delayed::DelayedIndicator;
use super::row::{self, LEAD};
use super::rows::{self, RowScroll, Step};
use super::select_box;
use super::{ContextItem, SpinnerStyle, tab_model};
mod drop;
mod edit;
#[cfg(test)]
mod multi_tests;
mod select;
#[cfg(test)]
mod tests;
pub use drop::TreeDrop;
use drop::{Aim, Dropping};
use edit::Arrange;
pub use edit::TreeMove;
use select::TreeBox;
const INDENT: u16 = 2;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TreeNode {
key: String,
label: String,
icon: Option<String>,
icon_color: Option<String>,
detail: Option<String>,
children: Vec<TreeNode>,
expandable: bool,
expanded: bool,
loading: bool,
faint: bool,
}
impl TreeNode {
#[must_use]
pub fn new(key: impl Into<String>, label: impl Into<String>) -> Self {
Self {
key: key.into(),
label: label.into(),
icon: None,
icon_color: None,
detail: None,
children: Vec::new(),
expandable: false,
expanded: false,
loading: false,
faint: false,
}
}
#[must_use]
pub fn children(mut self, children: impl IntoIterator<Item = Self>) -> Self {
self.children = children.into_iter().collect();
self.expandable = self.expandable || !self.children.is_empty();
self
}
#[must_use]
pub fn expandable(mut self, expandable: bool) -> Self {
self.expandable = expandable || !self.children.is_empty();
self
}
#[must_use]
pub fn expanded(mut self, expanded: bool) -> Self {
self.expanded = expanded;
self
}
#[must_use]
pub fn loading(mut self, loading: bool) -> Self {
self.loading = loading;
self
}
#[must_use]
pub fn icon(mut self, key: impl Into<String>, color: Option<&str>) -> Self {
self.icon = Some(key.into());
self.icon_color = color.map(str::to_owned);
self
}
#[must_use]
pub fn detail(mut self, detail: impl Into<String>) -> Self {
self.detail = Some(detail.into());
self
}
#[must_use]
pub fn faint(mut self, faint: bool) -> Self {
self.faint = faint;
self
}
}
struct Flat<'a> {
node: &'a TreeNode,
depth: u16,
parent: Option<usize>,
index: usize,
}
#[derive(Debug, Clone, Copy)]
struct RowFlags {
hovered: bool,
selected: bool,
focused: bool,
pressed: bool,
spinning: bool,
pillar: bool,
}
#[derive(Debug, Default)]
struct LoadingMarks(Vec<(String, DelayedIndicator)>);
type KeyMessage<Msg> = Box<dyn Fn(&str) -> Msg>;
type ExpandMessage<Msg> = Box<dyn Fn(&str, bool) -> Msg>;
type MoveMessage<Msg> = Box<dyn Fn(TreeMove) -> Msg>;
type SelectionMessage<Msg> = Box<dyn Fn(Vec<String>) -> Msg>;
type DropMessage<Msg> = Box<dyn Fn(TreeDrop) -> Msg>;
type MenuItems<Msg> = Box<dyn Fn(&str) -> Vec<ContextItem<Msg>>>;
pub struct Tree<Msg> {
roots: Vec<TreeNode>,
selected: Option<String>,
empty: String,
on_select: Option<KeyMessage<Msg>>,
on_activate: Option<KeyMessage<Msg>>,
on_expand: Option<ExpandMessage<Msg>>,
on_move: Option<MoveMessage<Msg>>,
menu: Option<MenuItems<Msg>>,
chosen: Vec<String>,
on_choose: Option<SelectionMessage<Msg>>,
dropping: Option<Dropping<Msg>>,
copy_drop: Option<DropMessage<Msg>>,
activate_on: Click,
box_select: bool,
}
impl<Msg: 'static> Tree<Msg> {
#[must_use]
pub fn new(roots: impl IntoIterator<Item = TreeNode>) -> Self {
Self {
roots: roots.into_iter().collect(),
selected: None,
empty: String::new(),
on_select: None,
on_activate: None,
on_expand: None,
on_move: None,
menu: None,
chosen: Vec::new(),
on_choose: None,
dropping: None,
copy_drop: None,
activate_on: Click::Single,
box_select: false,
}
}
#[must_use]
pub fn selected(mut self, key: Option<&str>) -> Self {
self.selected = key.map(str::to_owned);
self
}
#[must_use]
pub fn multi_select(mut self, selected: &[String], message: impl Fn(Vec<String>) -> Msg + 'static) -> Self {
self.chosen = selected.to_vec();
self.on_choose = Some(Box::new(message));
self
}
#[must_use]
pub fn empty_text(mut self, text: impl Into<String>) -> Self {
self.empty = text.into();
self
}
#[must_use]
pub fn on_select(mut self, message: impl Fn(&str) -> Msg + 'static) -> Self {
self.on_select = Some(Box::new(message));
self
}
#[must_use]
pub fn on_activate(mut self, message: impl Fn(&str) -> Msg + 'static) -> Self {
self.on_activate = Some(Box::new(message));
self
}
#[must_use]
pub fn on_expand(mut self, message: impl Fn(&str, bool) -> Msg + 'static) -> Self {
self.on_expand = Some(Box::new(message));
self
}
#[must_use]
pub fn reorderable(mut self, message: impl Fn(TreeMove) -> Msg + 'static) -> Self {
self.on_move = Some(Box::new(message));
self
}
#[must_use]
pub fn droppable(
mut self,
message: impl Fn(TreeDrop) -> Msg + 'static,
accepts: impl Fn(&str) -> bool + 'static,
) -> Self {
self.dropping = Some(Dropping::new(message, accepts));
self
}
#[must_use]
pub fn on_copy_drop(mut self, message: impl Fn(TreeDrop) -> Msg + 'static) -> Self {
self.copy_drop = Some(Box::new(message));
self
}
#[must_use]
pub fn activate_on(mut self, click: Click) -> Self {
self.activate_on = click;
self
}
#[must_use]
pub fn box_select(mut self, on: bool) -> Self {
self.box_select = on;
self
}
#[must_use]
pub fn context_menu(mut self, items: impl Fn(&str) -> Vec<ContextItem<Msg>> + 'static) -> Self {
self.menu = Some(Box::new(items));
self
}
fn flatten(&self) -> Vec<Flat<'_>> {
self.flatten_with(None)
}
fn flatten_with(&self, arrange: Option<&Arrange<'_>>) -> Vec<Flat<'_>> {
fn walk<'a>(
nodes: &'a [TreeNode],
parent_key: Option<&str>,
(depth, parent): (u16, Option<usize>),
arrange: Option<&Arrange<'_>>,
out: &mut Vec<Flat<'a>>,
) {
let preview = arrange.filter(|arrange| arrange.parent == parent_key).and_then(|arrange| arrange.order);
for index in tab_model::preview_order(nodes.len(), preview) {
let node = &nodes[index];
let at = out.len();
out.push(Flat { node, depth, parent, index });
let folded = arrange.is_some_and(|arrange| arrange.key == node.key);
if node.expanded && !folded {
walk(&node.children, Some(&node.key), (depth.saturating_add(1), Some(at)), arrange, out);
}
}
}
let mut out = Vec::new();
walk(&self.roots, None, (0, None), arrange, &mut out);
out
}
fn selected_index(&self, flat: &[Flat<'_>]) -> Option<usize> {
let key = self.selected.as_deref()?;
flat.iter().position(|row| row.node.key == key)
}
fn select(&self, cx: &mut EventCx<'_, Msg>, flat: &[Flat<'_>], index: usize) {
let Some(row) = flat.get(index) else { return };
if self.selected.as_deref() != Some(row.node.key.as_str())
&& let Some(message) = &self.on_select
{
cx.emit(message(&row.node.key));
}
}
fn expand(&self, cx: &mut EventCx<'_, Msg>, node: &TreeNode, open: bool) -> bool {
match &self.on_expand {
Some(message) if node.expandable && node.expanded != open => {
cx.emit(message(&node.key, open));
true
}
_ => false,
}
}
fn activate(&self, cx: &mut EventCx<'_, Msg>, index: usize, node: &TreeNode) -> bool {
let Some(message) = &self.on_activate else {
return false;
};
cx.memory::<RowScroll>().flashed = Some(index);
cx.flash();
cx.emit(message(&node.key));
true
}
fn open_or_activate(&self, cx: &mut EventCx<'_, Msg>, index: usize, node: &TreeNode) -> bool {
if node.expandable { self.expand(cx, node, !node.expanded) } else { self.activate(cx, index, node) }
}
fn loading_marks(cx: &mut PaintCx<'_>, flat: &[Flat<'_>]) -> Vec<usize> {
let now = cx.now();
let mut marks = std::mem::take(&mut cx.memory::<LoadingMarks>().0);
let mut kept = Vec::new();
let mut spinning = Vec::new();
let mut next: Option<std::time::Duration> = None;
for (index, row) in flat.iter().enumerate() {
let node = row.node;
let known = marks.iter().position(|(key, _)| *key == node.key);
if !node.expandable || (!node.loading && known.is_none()) {
continue;
}
let mut mark = known.map(|at| marks.swap_remove(at).1).unwrap_or_default();
if mark.update(node.loading, now) {
spinning.push(index);
}
if let Some(change) = mark.next_change(node.loading, now) {
next = Some(next.map_or(change, |soonest| soonest.min(change)));
}
if !mark.is_idle() {
kept.push((node.key.clone(), mark));
}
}
if let Some(delay) = next {
cx.request_frame_in(delay);
}
cx.memory::<LoadingMarks>().0 = kept;
spinning
}
fn paint_target(cx: &mut PaintCx<'_>, rect: Rect, row: &Flat<'_>, aim: &Aim) -> bool {
let (widget, variant) = match aim {
Aim::Into(Some(key)) if *key == row.node.key => ("tree-drop", None),
Aim::Refused(key) if *key == row.node.key => ("list-item", Some("faint")),
_ => return false,
};
let style = cx.style(widget, variant, &[]);
Self::paint_node(cx, rect, row, (&style, &[]), false, false);
true
}
fn chevron_x(area: Rect, depth: u16) -> i32 {
area.x + i32::from(LEAD) + i32::from(depth.saturating_mul(INDENT))
}
fn paint_row(&self, cx: &mut PaintCx<'_>, rect: Rect, index: usize, row: &Flat<'_>, flags: RowFlags) {
let flashed = cx.memory::<RowScroll>().flashed == Some(index);
let states = rows::row_states(flags.hovered, flags.selected, flags.focused, flags.pressed && flashed);
let style = cx.style("list-item", row.node.faint.then_some("faint"), &states);
let slide = flags.pillar && rows::slide(cx, &states) > 0;
let style = if flags.pillar { style } else { style.without("pillar") };
Self::paint_node(cx, rect, row, (&style, &states), flags.spinning, slide);
}
fn paint_node(
cx: &mut PaintCx<'_>,
rect: Rect,
row: &Flat<'_>,
(style, states): (&WidgetStyle, &[State]),
spinning: bool,
slide: bool,
) {
let node = row.node;
let text_style = style.text();
let detail_width = node.detail.as_deref().map_or(0, |d| text::width(d).saturating_add(2));
let chevron = if !node.expandable {
(" ".to_owned(), CellStyle::default())
} else if spinning {
let style = cx.style("spinner", None, &[]).text();
let cell = cx.animation(SpinnerStyle::Dots.animation(), style, Some(std::time::Duration::ZERO));
(text::truncate(&cell.glyph, 1).into_owned(), cell.style)
} else {
let key = if node.expanded { "tree-expanded" } else { "tree-collapsed" };
let glyph = text::truncate(&cx.env().icons().glyph(key), 1).into_owned();
(glyph, cx.style("tree-chevron", None, states).text())
};
let icon: Vec<row::Mark> =
node.icon.iter().map(|key| row::icon(cx, key, node.icon_color.as_deref(), text_style.fg)).collect();
let parts = row::Parts {
indent: row.depth.saturating_mul(INDENT),
fixed: &[chevron],
sliding: &icon,
label: &node.label,
trailing: detail_width,
};
row::paint_parts(cx, rect, style, slide, &parts);
if let Some(detail) = &node.detail {
let detail_style = cx.style("list-detail", None, states).text();
row::paint_trailing(cx, rect, detail, detail_style);
}
}
}
impl<Msg: 'static> Widget<Msg> for Tree<Msg> {
fn measure(&self, _cx: &mut MeasureCx<'_>, available: Size) -> Size {
let flat = self.flatten();
let widest = flat
.iter()
.map(|row| {
[
LEAD,
row.depth.saturating_mul(INDENT),
2,
row.node.icon.as_ref().map_or(0, |_| 2),
text::width(&row.node.label),
row.node.detail.as_deref().map_or(0, |d| text::width(d).saturating_add(2)),
2,
]
.into_iter()
.fold(0, u16::saturating_add)
})
.max()
.unwrap_or_else(|| text::width(&self.empty).saturating_add(LEAD));
let rows = clamp_u16(i32::try_from(flat.len().max(1)).unwrap_or(i32::MAX));
Size::new(widest, rows).min(available)
}
fn paint(&self, cx: &mut PaintCx<'_>, area: Rect) {
cx.register_hit(area);
if self.roots.is_empty() {
let faint = cx.style("list-header", None, &[]).text();
cx.text(area.x + i32::from(LEAD), area.y, &self.empty, faint, area.width.saturating_sub(LEAD));
return;
}
let drag = self.drag(cx);
let menu_node = self.menu_node(cx);
if menu_node.is_some() {
cx.request_overlay(area);
}
let aim = drag.as_ref().map(|drag| {
let offset = cx.memory::<RowScroll>().offset;
self.aim(&drag.keys, drag.pointer, Self::rows_area(area, self.flatten().len()), offset)
});
let flat = match (&drag, &aim) {
(Some(drag), Some(Aim::Reorder(order))) => {
let parent = self.siblings(&drag.key).and_then(|(parent, _)| parent);
self.flatten_with(Some(&Arrange { key: &drag.key, parent, order: *order }))
}
(Some(drag), _) => self.drag_layout(&drag.keys),
(None, _) => self.flatten(),
};
let slot = drag.as_ref().filter(|drag| self.reorders(&drag.keys)).map(|drag| drag.key.as_str());
let focused = cx.is_focused();
let pressed = cx.is_pressed();
let selected = self.selected_index(&flat);
let visible = usize::from(area.height);
let offset = cx.memory::<RowScroll>().follow(selected, flat.len(), visible);
let width = Self::rows_area(area, flat.len()).width;
let spinning = Self::loading_marks(cx, &flat);
let pointer = cx.pointer().filter(|_| drag.is_none() && menu_node.is_none());
for (row, index) in (offset..flat.len()).take(visible).enumerate() {
let rect = Rect::new(area.x, area.y + i32::try_from(row).unwrap_or(0), width, 1);
let key = flat[index].node.key.as_str();
if slot == Some(key) {
tab_model::paint_drop_slot(cx, rect);
continue;
}
if let Some(aim) = &aim
&& Self::paint_target(cx, rect, &flat[index], aim)
{
continue;
}
let touched = pointer.is_some_and(|(x, y)| rect.contains(x, y)) || menu_node.as_deref() == Some(key);
let cursor = selected == Some(index);
let chosen = self.is_chosen(key);
let hovered = touched || (cursor && !chosen);
let flags = RowFlags {
hovered,
selected: chosen,
focused: focused && cursor,
pressed,
spinning: spinning.contains(&index),
pillar: cursor || hovered || !self.is_multi(),
};
self.paint_row(cx, rect, index, &flat[index], flags);
}
if aim == Some(Aim::Into(None)) {
let used = i32::try_from(flat.len().saturating_sub(offset)).unwrap_or(i32::MAX);
let top = area.y.saturating_add(used);
if top < area.bottom() {
let free = Rect::new(area.x, top, width, clamp_u16(area.bottom() - top));
let bg = cx.style("tree-drop", None, &[]).text().bg;
if let Some(bg) = bg {
cx.fill(free, bg);
}
}
}
if let Some(drawn) = cx.memory::<TreeBox>().drawn() {
select_box::paint(cx, drawn, Self::rows_area(area, flat.len()));
}
if let Some(drag) = &drag
&& matches!(aim, Some(Aim::Reorder(_)))
&& let Some(row) = flat.iter().find(|row| row.node.key == drag.key)
&& !area.is_empty()
{
let y = drag.pointer.1.clamp(area.y, area.bottom() - 1);
let rect = Rect::new(area.x, y, width, 1);
tab_model::paint_ghost_surface(cx, rect);
let ghost = cx.style("tab-ghost", None, &[]);
Self::paint_node(cx, rect, row, (&ghost, &[]), false, false);
}
rows::paint_scrollbar(cx, area, flat.len(), offset, None);
}
fn paint_overlay(&self, cx: &mut PaintCx<'_>, anchor: Rect) {
self.paint_menu(cx, anchor);
}
fn event(&self, cx: &mut EventCx<'_, Msg>, event: &Event) -> bool {
let area = cx.area();
let flat = self.flatten();
if self.menu_event(cx, event, &flat) {
return true;
}
let current = self.selected_index(&flat);
match event {
Event::Key(key) => {
if self.move_key(cx, key) || self.selection_key(cx, key, &flat) {
return true;
}
if let Some(step) = Step::from_key(key) {
let Some(target) = step.apply(current, flat.len(), usize::from(area.height)) else {
return false;
};
self.select_one(cx, &flat, target);
return true;
}
let Some(index) = current else { return false };
let row = &flat[index];
if key.is_plain(Key::Right) || key.is_plain(Key::Char('l')) {
if !row.node.expanded {
return self.expand(cx, row.node, true);
}
if !row.node.children.is_empty() {
self.select_one(cx, &flat, index + 1);
return true;
}
return false;
}
if key.is_plain(Key::Left) || key.is_plain(Key::Char('h')) {
if row.node.expanded {
return self.expand(cx, row.node, false);
}
return row.parent.is_some_and(|parent| {
self.select_one(cx, &flat, parent);
true
});
}
if key.is_plain(Key::Enter) {
return self.open_or_activate(cx, index, row.node);
}
if key.is_plain(Key::Space) {
return self.activate(cx, index, row.node);
}
false
}
Event::Mouse(mouse) => {
if rows::scroll_mouse(cx, mouse, area, flat.len()) {
return true;
}
let offset = cx.memory::<RowScroll>().offset;
let index = usize::try_from(mouse.y - area.y).ok().map(|r| offset + r).filter(|i| *i < flat.len());
let on_chevron = index.is_some_and(|index| {
let row = &flat[index];
let chevron = Self::chevron_x(area, row.depth);
row.node.expandable && (chevron..=chevron + 1).contains(&mouse.x)
});
if let Some(used) = self.box_pointer(cx, mouse, &flat, index) {
return used;
}
if (self.on_move.is_some() || self.dropping.is_some())
&& !on_chevron
&& let Some(used) = self.drag_pointer(cx, mouse, &flat, index)
{
return used;
}
if mouse.kind != MouseKind::Down(MouseButton::Left) {
return false;
}
let Some(index) = index else {
return false;
};
let row = &flat[index];
if on_chevron {
return self.expand(cx, row.node, !row.node.expanded);
}
if self.modified_press(cx, &flat, index, mouse.mods) {
return true;
}
self.select_one(cx, &flat, index);
if self.activate_on == Click::Single || self.double_press(cx, &row.node.key) {
self.open_or_activate(cx, index, row.node);
}
true
}
_ => false,
}
}
fn focusable(&self) -> bool {
!self.roots.is_empty()
}
}