use crate::core::Color;
use crate::core::HorizontalAlignment;
use crate::core::Point;
use crate::core::Rect;
use crate::render::RenderContext;
use crate::signal::{ConnectionScope, GenericSignal, Signal1};
use crate::widget::capability::coercion::expect_usize;
use crate::widget::capability::properties_trait::{base_property_get, base_property_set};
use crate::widget::capability::types::{CapabilityAccessError, CapabilityValue};
use crate::widget::capability::WidgetProperties;
use crate::widget::metrics::{effective_font, ControlMetrics};
use crate::widget::view_widgets::table_widget::ItemDelegate;
use crate::widget::{BaseWidget, Draw, Widget, WidgetKind};
use crate::{impl_widget_property_hooks, property_names_of};
use std::collections::HashMap;
use std::sync::Arc;
const TREE_INSET: u32 = 2;
const TREE_ROW_HEIGHT: i32 = 20;
const TREE_INDENT: i32 = 15;
const TREE_DISCLOSURE_SIZE: i32 = 6;
fn point_in_rect(point: Point, rect: Rect) -> bool {
let right = rect.x + rect.width as i32;
let bottom = rect.y + rect.height as i32;
point.x >= rect.x && point.x < right && point.y >= rect.y && point.y < bottom
}
fn disclosure_box(row: Rect, depth: usize, has_children: bool) -> Option<Rect> {
if !has_children {
return None;
}
let depth = i32::try_from(depth).ok()?;
let gutter_x = row.x + TREE_INDENT.saturating_mul(depth.saturating_add(1));
let size = TREE_DISCLOSURE_SIZE;
let top = row.y + (TREE_ROW_HEIGHT - size) / 2;
Some(Rect::new(
gutter_x - TREE_INDENT + (TREE_INDENT - size) / 2,
top,
size as u32,
size as u32,
))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NodeView {
pub label: String,
pub depth: usize,
pub has_children: bool,
pub expanded: bool,
}
impl NodeView {
pub fn leaf(label: impl Into<String>) -> Self {
Self { label: label.into(), depth: 0, has_children: false, expanded: true }
}
}
pub trait TreeModel: Send + Sync {
fn node_count(&self) -> usize;
fn node_path(&self, index: usize) -> Option<String>;
fn node(&self, index: usize) -> Option<NodeView> {
self.node_path(index).map(NodeView::leaf)
}
fn data_changed_signal(&self) -> Option<&GenericSignal> {
None
}
}
pub struct VecTreeModel {
nodes: Vec<NodeView>,
data_changed: GenericSignal,
}
impl VecTreeModel {
pub fn new(nodes: Vec<String>) -> Self {
Self {
nodes: nodes.into_iter().map(NodeView::leaf).collect(),
data_changed: GenericSignal::new(),
}
}
pub fn from_views(nodes: Vec<NodeView>) -> Self {
Self { nodes, data_changed: GenericSignal::new() }
}
pub fn data_changed_signal(&self) -> &GenericSignal {
&self.data_changed
}
pub fn append(&mut self, node: String) {
self.nodes.push(NodeView::leaf(node));
self.data_changed.emit();
}
pub fn append_view(&mut self, node: NodeView) {
self.nodes.push(node);
self.data_changed.emit();
}
pub fn remove(&mut self, index: usize) -> Option<String> {
if index < self.nodes.len() {
let node = self.nodes.remove(index);
self.data_changed.emit();
Some(node.label)
} else {
None
}
}
pub fn clear(&mut self) {
self.nodes.clear();
self.data_changed.emit();
}
}
impl TreeModel for VecTreeModel {
fn node_count(&self) -> usize {
self.nodes.len()
}
fn node_path(&self, index: usize) -> Option<String> {
self.nodes.get(index).map(|node| node.label.clone())
}
fn node(&self, index: usize) -> Option<NodeView> {
self.nodes.get(index).cloned()
}
fn data_changed_signal(&self) -> Option<&GenericSignal> {
Some(&self.data_changed)
}
}
pub struct TreeView {
base: BaseWidget,
model: Option<Arc<dyn TreeModel>>,
model_connection_scope: ConnectionScope,
selected_node: Option<usize>,
focused_node: Option<usize>,
hovered_node: Option<usize>,
expanded_override: HashMap<usize, bool>,
delegate: Option<Arc<dyn ItemDelegate>>,
editing: Option<usize>,
pub selection_changed: Signal1<usize>,
pub focused_node_changed: Signal1<Option<usize>>,
}
impl TreeView {
pub fn new(geometry: Rect) -> Self {
Self {
base: BaseWidget::new(WidgetKind::TreeView, geometry, "TreeView"),
model: None,
model_connection_scope: ConnectionScope::new(),
selected_node: None,
focused_node: None,
hovered_node: None,
expanded_override: HashMap::new(),
delegate: None,
editing: None,
selection_changed: Signal1::new(),
focused_node_changed: Signal1::new(),
}
}
pub fn set_model(&mut self, model: Arc<dyn TreeModel>) {
self.model_connection_scope = ConnectionScope::new();
if let Some(data_changed) = model.data_changed_signal() {
let redraw = self.base.redraw_requested_signal().clone();
let layout = self.base.layout_requested_signal().clone();
data_changed.connect_scoped(&self.model_connection_scope, move || {
redraw.emit();
layout.emit();
});
}
self.model = Some(model);
self.expanded_override.clear();
self.editing = None;
self.normalize_projection_state();
self.base.request_layout();
self.base.request_redraw();
}
pub fn has_model(&self) -> bool {
self.model.is_some()
}
pub fn model_ref(&self) -> Option<&Arc<dyn TreeModel>> {
self.model.as_ref()
}
pub fn visible_indices(&self) -> Vec<usize> {
let Some(model) = self.model.as_ref() else {
return Vec::new();
};
let count = model.node_count();
let mut rows = Vec::with_capacity(count);
let mut ancestors: Vec<(usize, bool)> = Vec::new();
for index in 0..count {
let Some(node) = model.node(index) else { continue };
while ancestors.last().is_some_and(|(depth, _)| *depth >= node.depth) {
ancestors.pop();
}
if ancestors.iter().all(|(_, expanded)| *expanded) {
rows.push(index);
}
if node.has_children {
ancestors.push((node.depth, self.is_expanded(index)));
}
}
rows
}
pub fn is_expanded(&self, index: usize) -> bool {
if let Some(value) = self.expanded_override.get(&index) {
return *value;
}
self.model
.as_ref()
.and_then(|model| model.node(index))
.map(|node| node.expanded)
.unwrap_or(false)
}
pub fn expand(&mut self, index: usize) -> bool {
self.set_expanded(index, true)
}
pub fn collapse(&mut self, index: usize) -> bool {
self.set_expanded(index, false)
}
pub fn toggle_expanded(&mut self, index: usize) -> bool {
let Some(model_index) = self.visible_indices().get(index).copied() else {
return false;
};
self.set_expanded(index, !self.is_expanded(model_index))
}
fn set_expanded(&mut self, visible_index: usize, expanded: bool) -> bool {
let Some(model_index) = self.visible_indices().get(visible_index).copied() else {
return false;
};
let has_children = self
.model
.as_ref()
.and_then(|model| model.node(model_index))
.map(|node| node.has_children)
.unwrap_or(false);
if !has_children {
return false;
}
if self.is_expanded(model_index) == expanded {
return true;
}
self.expanded_override.insert(model_index, expanded);
self.normalize_projection_state();
self.base.request_layout();
self.base.request_redraw();
true
}
pub fn node_count(&self) -> usize {
self.visible_row_count()
}
pub fn node_path(&self, index: usize) -> Option<String> {
let model_index = *self.visible_indices().get(index)?;
self.model.as_ref()?.node(model_index).map(|node| node.label)
}
pub fn node_view(&self, index: usize) -> Option<NodeView> {
let model_index = *self.visible_indices().get(index)?;
self.model.as_ref()?.node(model_index)
}
pub fn model_index(&self, index: usize) -> Option<usize> {
self.visible_indices().get(index).copied()
}
pub fn select_node(&mut self, index: usize) -> bool {
let Some(model_index) = self.visible_indices().get(index).copied() else {
return false;
};
self.selected_node = Some(model_index);
self.selection_changed.emit(index);
self.set_focused_node(index);
self.base.request_redraw();
true
}
pub fn clear_selection(&mut self) {
if self.selected_node.is_none() {
return;
}
self.selected_node = None;
self.base.request_redraw();
}
pub fn set_focused_node(&mut self, index: usize) -> bool {
let Some(model_index) = self.visible_indices().get(index).copied() else {
return false;
};
if self.focused_node == Some(model_index) {
return true;
}
self.focused_node = Some(model_index);
self.focused_node_changed.emit(Some(index));
self.base.request_redraw();
true
}
pub fn clear_focused_node(&mut self) {
if self.focused_node.is_none() {
return;
}
self.focused_node = None;
self.focused_node_changed.emit(None);
self.base.request_redraw();
}
pub fn focused_node(&self) -> Option<usize> {
let focused = self.focused_node?;
self.visible_indices().iter().position(|index| *index == focused)
}
pub fn node_row_rect(&self, index: usize) -> Option<Rect> {
let content = ControlMetrics::band_inset(self.base.geometry(), TREE_INSET);
let row_offset = i32::try_from(index).ok()?;
let y = content.y + TREE_ROW_HEIGHT.saturating_mul(row_offset);
if y + TREE_ROW_HEIGHT > content.y + content.height as i32 {
return None;
}
Some(Rect::new(content.x, y, content.width, TREE_ROW_HEIGHT as u32))
}
pub fn node_at(&self, pos: crate::core::Point) -> Option<usize> {
let content = ControlMetrics::band_inset(self.base.geometry(), TREE_INSET);
if pos.y < content.y || pos.y >= content.y + content.height as i32 {
return None;
}
let index = ((pos.y - content.y) / TREE_ROW_HEIGHT).max(0) as usize;
(index < self.visible_row_count()).then_some(index)
}
pub fn visible_row_count(&self) -> usize {
let Some(model) = self.model.as_ref() else {
return 0;
};
let mut shown = 0usize;
let mut ancestors: Vec<(usize, bool)> = Vec::new();
for index in 0..model.node_count() {
let Some(node) = model.node(index) else { continue };
while ancestors.last().is_some_and(|(depth, _)| *depth >= node.depth) {
ancestors.pop();
}
if ancestors.iter().all(|(_, expanded)| *expanded) {
shown += 1;
}
if node.has_children {
ancestors.push((node.depth, self.is_expanded(index)));
}
}
shown
}
pub fn disclosure_rect(&self, index: usize) -> Option<Rect> {
let node = self.node_view(index)?;
let row = self.node_row_rect(index)?;
disclosure_box(row, node.depth, node.has_children)
}
pub fn selected_node(&self) -> Option<usize> {
let selected = self.selected_node?;
self.visible_indices().iter().position(|index| *index == selected)
}
fn normalize_projection_state(&mut self) {
let model_count = self.model.as_ref().map(|model| model.node_count()).unwrap_or(0);
self.selected_node = self.selected_node.filter(|index| *index < model_count);
self.focused_node = self.focused_node.filter(|index| *index < model_count);
self.editing = self.editing.filter(|index| *index < model_count);
self.expanded_override.retain(|index, _| *index < model_count);
}
pub fn set_delegate(&mut self, delegate: Arc<dyn ItemDelegate>) {
self.delegate = Some(delegate);
}
pub fn has_delegate(&self) -> bool {
self.delegate.is_some()
}
pub fn delegate_ref(&self) -> Option<&Arc<dyn ItemDelegate>> {
self.delegate.as_ref()
}
pub fn begin_edit(&mut self, index: usize) -> Option<Box<dyn Widget>> {
let delegate = self.delegate.clone()?;
let model_index = self.model_index(index)?;
let mut editor = delegate.create_editor(&mut self.base, model_index, 0)?;
delegate.set_editor_data(editor.as_mut(), model_index, 0);
self.editing = Some(model_index);
self.base.request_redraw();
Some(editor)
}
pub fn commit_edit(&mut self, editor: &dyn Widget) -> Option<String> {
let delegate = self.delegate.clone()?;
let model_index = self.editing?;
let text = delegate.get_editor_data(editor, model_index, 0);
self.editing = None;
self.base.request_redraw();
text
}
pub fn editing_node(&self) -> Option<usize> {
self.editing
}
pub fn cancel_edit(&mut self) {
if self.editing.take().is_some() {
self.base.request_redraw();
}
}
pub fn focused_node_expanded(&self) -> Option<bool> {
let visible = self.focused_node()?;
let model_index = self.model_index(visible)?;
let node = self.node_view(visible)?;
node.has_children.then(|| self.is_expanded(model_index))
}
pub fn expand_focused(&mut self) -> bool {
match self.focused_node() {
Some(index) => self.expand(index),
None => false,
}
}
pub fn collapse_focused(&mut self) -> bool {
match self.focused_node() {
Some(index) => self.collapse(index),
None => false,
}
}
pub fn toggle_focused(&mut self) -> bool {
match self.focused_node() {
Some(index) => self.toggle_expanded(index),
None => false,
}
}
}
impl Widget for TreeView {
fn base(&self) -> &BaseWidget {
&self.base
}
fn set_state_theme_hook(&mut self) {
crate::style::reapply_active_theme_state(self);
}
fn base_mut(&mut self) -> &mut BaseWidget {
&mut self.base
}
fn size_hint(&self) -> crate::core::Size {
crate::core::Size::new(
crate::widget::metrics::dimensions::TREE_VIEW_DEFAULT_WIDTH,
crate::widget::metrics::dimensions::TREE_VIEW_DEFAULT_HEIGHT,
)
}
impl_draw_bridge!();
impl_widget_property_hooks!();
fn event_signal_dyn(&self, name: &str) -> Option<crate::signal::EventSignalRef> {
use crate::signal::EventSignalRef;
match name {
"selection_changed" => {
Some(EventSignalRef::mapped("selection_changed", &self.selection_changed, |v| {
CapabilityValue::UInt(*v as u64)
}))
}
"focused_node_changed" => Some(EventSignalRef::mapped(
"focused_node_changed",
&self.focused_node_changed,
|v| match v {
Some(value) => CapabilityValue::UInt(*value as u64),
None => CapabilityValue::Null,
},
)),
_ => None,
}
}
}
impl WidgetProperties for TreeView {
fn get(&self, name: &str) -> Result<CapabilityValue, CapabilityAccessError> {
match name {
"has_model" => Ok(CapabilityValue::Bool(self.has_model())),
"node_count" => Ok(CapabilityValue::UInt(self.node_count() as u64)),
"has_delegate" => Ok(CapabilityValue::Bool(self.has_delegate())),
"expanded_focused_node" => match self.focused_node_expanded() {
Some(expanded) => Ok(CapabilityValue::Bool(expanded)),
None => Ok(CapabilityValue::Null),
},
"focused_node" => match self.focused_node() {
Some(node) => Ok(CapabilityValue::UInt(node as u64)),
None => Ok(CapabilityValue::Null),
},
"selected_node" => match self.selected_node() {
Some(node) => Ok(CapabilityValue::UInt(node as u64)),
None => Ok(CapabilityValue::Null),
},
_ => base_property_get(self, name),
}
}
fn set(&mut self, name: &str, value: CapabilityValue) -> Result<(), CapabilityAccessError> {
match name {
"focused_node" => match value {
CapabilityValue::Null => {
self.clear_focused_node();
Ok(())
}
other => {
let node = expect_usize(other)?;
if self.set_focused_node(node) {
Ok(())
} else {
Err(CapabilityAccessError::OutOfRange)
}
}
},
"has_model" => Err(CapabilityAccessError::ReadOnlyProperty),
"node_count" => Err(CapabilityAccessError::ReadOnlyProperty),
"selected_node" => Err(CapabilityAccessError::ReadOnlyProperty),
"has_delegate" => Err(CapabilityAccessError::ReadOnlyProperty),
"expanded_focused_node" => Err(CapabilityAccessError::ReadOnlyProperty),
_ => base_property_set(self, name, value),
}
}
fn property_names(&self) -> &'static [&'static str] {
property_names_of![
"has_model",
"node_count",
"focused_node",
"selected_node",
"has_delegate",
"expanded_focused_node",
BASE_PROPERTY_NAMES
]
}
fn command(&mut self, name: &str) -> Result<(), CapabilityAccessError> {
match name {
"clear_selection" => {
self.clear_selection();
Ok(())
}
"clear_focused_node" => {
self.clear_focused_node();
Ok(())
}
"expand" => {
if self.expand_focused() {
Ok(())
} else {
Err(CapabilityAccessError::OutOfRange)
}
}
"collapse" => {
if self.collapse_focused() {
Ok(())
} else {
Err(CapabilityAccessError::OutOfRange)
}
}
"toggle" => {
if self.toggle_focused() {
Ok(())
} else {
Err(CapabilityAccessError::OutOfRange)
}
}
_ => Err(CapabilityAccessError::UnknownCommand),
}
}
}
impl Draw for TreeView {
fn draw(&mut self, context: &mut RenderContext) {
let rect = self.base.geometry();
let style = self.base.style().clone();
let theme = crate::style::resolved_theme_style("tree_view");
let surface = style
.background_color
.or_else(|| theme.as_ref().and_then(|t| t.background_color))
.unwrap_or(Color::WHITE);
let ink = style
.text_color
.or_else(|| theme.as_ref().and_then(|t| t.text_color))
.unwrap_or(Color::BLACK);
let border = style
.border_color
.or_else(|| theme.as_ref().and_then(|t| t.border_color))
.filter(|resolved| *resolved != surface)
.unwrap_or_else(|| surface.blend(&ink, 0.20));
let accent = crate::style::theme_manager()
.current_theme()
.map(|active| active.colors.primary)
.unwrap_or(Color::PRIMARY);
let focused_bg = surface.blend(&accent, 0.30);
context.face_with_gradient(
rect,
surface,
self.style().background_gradient.as_ref(),
self.style().surface.unwrap_or_default(),
self.style().border_radius.unwrap_or(0),
Color::BLACK,
);
context.draw_rect(rect, border);
if let Some(ref model) = self.model {
let rows = self.visible_indices();
let font = effective_font(&style);
for (visible, &model_index) in rows.iter().enumerate() {
let Some(row) = self.node_row_rect(visible) else {
break;
};
if Some(model_index) == self.focused_node {
context.fill_rect(row, focused_bg);
} else if Some(model_index) == self.hovered_node {
context.fill_rect(row, surface.blend(&accent, 0.12));
}
let Some(node) = model.node(model_index) else { continue };
let depth = i32::try_from(node.depth).unwrap_or(i32::MAX);
let indent = TREE_INDENT.saturating_mul(depth.saturating_add(1));
if node.has_children {
if let Some(box_rect) = disclosure_box(row, node.depth, node.has_children) {
let center_y = box_rect.y + box_rect.height as i32 / 2;
let left = box_rect.x;
let size = box_rect.width as i32;
let (a, b, c) = if self.is_expanded(model_index) {
(
Point::new(left, center_y - size / 2),
Point::new(left + size, center_y - size / 2),
Point::new(left + size / 2, center_y + size / 2),
)
} else {
(
Point::new(left, center_y - size / 2),
Point::new(left, center_y + size / 2),
Point::new(left + size, center_y),
)
};
context.draw_line(a, b, ink);
context.draw_line(b, c, ink);
context.draw_line(c, a, ink);
}
}
if !node.label.is_empty() {
let cell = crate::core::Rect::new(
row.x + indent,
row.y,
row.width.saturating_sub(indent.max(0) as u32),
row.height,
);
context.draw_text_fitted(
context.text_line(cell, font),
&node.label,
font,
ink,
HorizontalAlignment::Left,
);
}
}
}
}
}
impl crate::event::EventHandler for TreeView {
fn handle_event(&mut self, event: &crate::event::Event) {
self.base.handle_event(event);
if !self.base.is_enabled() {
return;
}
match event {
crate::event::Event::MousePress { pos, button, .. } if *button == 1 => {
if let Some(index) = self.node_at(*pos) {
let on_glyph =
self.disclosure_rect(index).is_some_and(|glyph| point_in_rect(*pos, glyph));
self.select_node(index);
if on_glyph {
self.toggle_expanded(index);
}
}
}
crate::event::Event::MouseMove { pos } => {
let hovered = self.node_at(*pos).and_then(|visible| self.model_index(visible));
if hovered != self.hovered_node {
self.hovered_node = hovered;
self.base.request_redraw();
}
}
crate::event::Event::MouseLeave { .. } => {
if self.hovered_node.take().is_some() {
self.base.request_redraw();
}
}
crate::event::Event::KeyPress { key, .. } => {
let changed = match *key {
crate::event::key_codes::RIGHT => self.expand_focused(),
crate::event::key_codes::LEFT => self.collapse_focused(),
_ => false,
};
if changed {
self.base.request_redraw();
}
}
#[cfg(feature = "touch")]
crate::event::Event::Tap { pos } => {
if let Some(index) = self.node_at(*pos) {
self.select_node(index);
}
}
_ => { }
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
struct StaticTreeModel;
impl TreeModel for StaticTreeModel {
fn node_count(&self) -> usize {
2
}
fn node_path(&self, index: usize) -> Option<String> {
match index {
0 => Some("root".to_string()),
1 => Some("root/child".to_string()),
_ => None,
}
}
}
#[test]
fn out_of_range_node_is_reported_as_out_of_range_not_unsupported() {
use crate::widget::capability::types::CapabilityAccessError;
let mut view = TreeView::new(Rect::new(0, 0, 120, 100));
view.set_model(Arc::new(StaticTreeModel));
assert_eq!(view.node_count(), 2, "the fixture must have nodes to be out of range of");
assert_eq!(
view.set("focused_node", CapabilityValue::UInt(99)),
Err(CapabilityAccessError::OutOfRange),
"an index past the last node is the caller's argument, not a capability gap"
);
assert_eq!(view.set("focused_node", CapabilityValue::UInt(1)), Ok(()));
assert_eq!(view.focused_node(), Some(1));
}
#[test]
fn tree_view_model_binding_roundtrip() {
let mut view = TreeView::new(Rect::new(0, 0, 120, 100));
assert!(!view.has_model());
assert!(view.model_ref().is_none());
view.set_model(Arc::new(StaticTreeModel));
assert!(view.has_model());
assert!(view.model_ref().is_some());
assert_eq!(view.node_count(), 2);
assert_eq!(view.node_path(99), None);
}
#[test]
fn a_click_selects_the_node_it_landed_on() {
use crate::event::{Event, EventHandler};
let mut view = TreeView::new(Rect::new(0, 0, 120, 100));
view.set_model(Arc::new(StaticTreeModel));
let row = view.node_row_rect(1).expect("a roomy view paints node 1");
let centre = crate::core::Point::new(row.x + 4, row.y + row.height as i32 / 2);
view.handle_event(&Event::MousePress { pos: centre, button: 1, modifiers: 0 });
assert_eq!(
view.selected_node(),
Some(1),
"a click inside node 1's own painted band must select node 1"
);
let mut fresh = TreeView::new(Rect::new(0, 0, 120, 100));
fresh.set_model(Arc::new(StaticTreeModel));
fresh.handle_event(&Event::MousePress {
pos: crate::core::Point::new(4, 0),
button: 1,
modifiers: 0,
});
assert_eq!(fresh.selected_node(), None, "the inset margin is not a row");
}
#[test]
fn hovering_a_row_highlights_it_and_reaches_the_base() {
use crate::event::{Event, EventHandler};
use crate::style::WidgetState;
let mut view = TreeView::new(Rect::new(0, 0, 120, 100));
view.set_model(Arc::new(StaticTreeModel));
let row = view.node_row_rect(1).expect("a roomy view paints node 1");
let centre = crate::core::Point::new(row.x + 4, row.y + row.height as i32 / 2);
view.handle_event(&Event::MouseEnter { pos: centre });
view.handle_event(&Event::MouseMove { pos: centre });
assert_eq!(view.hovered_node, Some(1), "the row under the pointer is the one lit");
assert_eq!(
view.widget_state(),
WidgetState::Hover,
"the base must have been told the pointer arrived"
);
view.handle_event(&Event::MouseLeave { pos: crate::core::Point::new(500, 500) });
assert_eq!(view.hovered_node, None, "leaving clears the row rather than latching it");
assert_eq!(view.widget_state(), WidgetState::Normal);
}
struct CountingModel {
nodes: Vec<NodeView>,
lookups: std::sync::atomic::AtomicUsize,
}
impl CountingModel {
fn nested(count: usize) -> Self {
let mut nodes = vec![NodeView {
label: "root".to_string(),
depth: 0,
has_children: true,
expanded: true,
}];
for i in 0..count {
nodes.push(NodeView {
label: format!("branch {i}"),
depth: 1,
has_children: true,
expanded: true,
});
}
Self { nodes, lookups: std::sync::atomic::AtomicUsize::new(0) }
}
fn lookups(&self) -> usize {
self.lookups.load(std::sync::atomic::Ordering::SeqCst)
}
fn reset_lookups(&self) {
self.lookups.store(0, std::sync::atomic::Ordering::SeqCst);
}
}
impl TreeModel for CountingModel {
fn node_count(&self) -> usize {
self.nodes.len()
}
fn node_path(&self, index: usize) -> Option<String> {
self.nodes.get(index).map(|node| node.label.clone())
}
fn node(&self, index: usize) -> Option<NodeView> {
self.lookups.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
self.nodes.get(index).cloned()
}
}
#[test]
fn a_frame_does_not_look_up_the_model_once_per_row() {
use crate::render::{PaintBackend, SoftwarePaintBackend};
const LEAVES: usize = 400;
let model = Arc::new(CountingModel::nested(LEAVES));
let mut view = TreeView::new(Rect::new(0, 0, 400, 300));
view.set_model(model.clone());
assert_eq!(view.node_count(), LEAVES + 1, "the fixture must be fully expanded");
model.reset_lookups();
let mut backend = SoftwarePaintBackend::new(crate::core::Size::new(400, 300), 1.0);
backend.begin_frame(Color::WHITE);
let mut context = RenderContext::new(&mut backend);
view.draw(&mut context);
backend.end_frame();
let lookups = model.lookups();
let rows = view.node_count();
assert!(
lookups <= 4 * (LEAVES + 1),
"one frame made {lookups} model lookups for {} nodes and {rows} rows; a budget of \
nodes × rows would be {}, so this is per-row re-derivation rather than one walk",
LEAVES + 1,
rows * (LEAVES + 1)
);
}
struct NestedModel;
const NESTED: &[(usize, &str, bool, bool)] = &[
(0, "root", true, true),
(1, "child_a", false, true),
(1, "child_b", true, true),
(2, "grandchild", false, true),
(0, "other", false, true),
];
impl TreeModel for NestedModel {
fn node_count(&self) -> usize {
NESTED.len()
}
fn node_path(&self, index: usize) -> Option<String> {
NESTED.get(index).map(|(_, label, _, _)| (*label).to_string())
}
fn node(&self, index: usize) -> Option<NodeView> {
NESTED.get(index).map(|(depth, label, has_children, expanded)| NodeView {
label: (*label).to_string(),
depth: *depth,
has_children: *has_children,
expanded: *expanded,
})
}
}
#[test]
fn a_flat_model_shows_every_node_at_the_original_indent() {
let mut view = TreeView::new(Rect::new(0, 0, 120, 100));
view.set_model(Arc::new(StaticTreeModel));
assert_eq!(view.visible_indices(), vec![0, 1], "no branch exists, so nothing is hidden");
assert_eq!(view.node_count(), 2);
assert_eq!(view.node_path(0).as_deref(), Some("root"));
assert_eq!(
view.disclosure_rect(0),
None,
"a flat model has no branches, so no disclosure glyph is drawn"
);
assert_eq!(TREE_INDENT, 15);
}
#[test]
fn collapsing_a_branch_hides_descendants_and_the_mapping_is_reported() {
let mut view = TreeView::new(Rect::new(0, 0, 200, 200));
view.set_model(Arc::new(NestedModel));
assert_eq!(view.visible_indices(), vec![0, 1, 2, 3, 4]);
assert!(view.collapse(2), "collapsing a branch reports that it happened");
assert_eq!(view.visible_indices(), vec![0, 1, 2, 4]);
assert_eq!(view.node_count(), 4);
assert_eq!(view.model_index(3), Some(4), "the row after child_b is `other` now");
assert_eq!(
view.node_path(3).as_deref(),
Some("other"),
"the visible projection is the only index space the public accessors use"
);
assert!(!view.is_expanded(2));
assert!(view.collapse(0));
assert_eq!(view.visible_indices(), vec![0, 4]);
assert_eq!(view.model_index(0), Some(0));
assert_eq!(view.model_index(1), Some(4));
}
#[test]
fn a_leaf_refuses_expansion() {
let mut view = TreeView::new(Rect::new(0, 0, 200, 200));
view.set_model(Arc::new(NestedModel));
assert!(!view.expand(1), "`child_a` has no children, so there is nothing to expand");
assert!(!view.collapse(1));
assert_eq!(view.visible_indices(), vec![0, 1, 2, 3, 4], "a refused toggle changes nothing");
assert!(!view.expand(99));
}
#[test]
fn selection_follows_the_node_across_a_collapse() {
let mut view = TreeView::new(Rect::new(0, 0, 200, 200));
view.set_model(Arc::new(NestedModel));
assert!(view.select_node(3));
assert_eq!(view.selected_node(), Some(3));
assert!(view.collapse(2));
assert_eq!(view.selected_node(), None, "a hidden node has no visible index to report");
assert!(view.expand(2));
assert_eq!(view.selected_node(), Some(3), "the selection was the node, not the row");
}
#[test]
fn only_branches_have_a_disclosure_glyph_in_their_own_gutter() {
let mut view = TreeView::new(Rect::new(0, 0, 200, 200));
view.set_model(Arc::new(NestedModel));
let root_glyph = view.disclosure_rect(0).expect("root is a branch");
let child_glyph = view.disclosure_rect(2).expect("child_b is a branch");
assert_eq!(view.disclosure_rect(1), None, "child_a is a leaf");
assert_eq!(view.disclosure_rect(3), None, "grandchild is a leaf");
let row0 = view.node_row_rect(0).unwrap();
let row2 = view.node_row_rect(2).unwrap();
assert_eq!(root_glyph.y, row0.y + (TREE_ROW_HEIGHT - TREE_DISCLOSURE_SIZE) / 2);
assert!(
child_glyph.x > root_glyph.x,
"a deeper row's glyph must sit deeper: {} vs {}",
child_glyph.x,
root_glyph.x
);
assert_eq!(child_glyph.y, row2.y + (TREE_ROW_HEIGHT - TREE_DISCLOSURE_SIZE) / 2);
}
#[test]
fn a_press_on_the_glyph_toggles_and_a_press_on_the_label_selects() {
use crate::event::{Event, EventHandler};
let mut view = TreeView::new(Rect::new(0, 0, 200, 200));
view.set_model(Arc::new(NestedModel));
let glyph = view.disclosure_rect(2).expect("child_b is a branch");
let on_glyph = crate::core::Point::new(glyph.x + 1, glyph.y + 1);
view.handle_event(&Event::MousePress { pos: on_glyph, button: 1, modifiers: 0 });
assert_eq!(view.visible_indices(), vec![0, 1, 2, 4], "the glyph closed child_b");
assert_eq!(view.selected_node(), Some(2), "and the press still selected the row");
let label = crate::core::Point::new(glyph.x + TREE_INDENT + 4, glyph.y + 1);
view.handle_event(&Event::MousePress { pos: label, button: 1, modifiers: 0 });
assert_eq!(view.visible_indices(), vec![0, 1, 2, 4], "a label press does not toggle");
assert_eq!(view.selected_node(), Some(2));
}
struct RecordingDelegate;
impl ItemDelegate for RecordingDelegate {
fn create_editor(
&self,
_parent: &mut BaseWidget,
_row: usize,
_column: usize,
) -> Option<Box<dyn Widget>> {
Some(Box::new(crate::widget::base_widgets::label::Label::new(
String::new(),
Rect::new(0, 0, 10, 10),
)))
}
fn set_editor_data(&self, _editor: &mut dyn Widget, _row: usize, _column: usize) {}
fn get_editor_data(
&self,
_editor: &dyn Widget,
row: usize,
column: usize,
) -> Option<String> {
Some(format!("{row}:{column}"))
}
}
#[test]
fn in_place_edit_runs_through_the_delegate_and_is_addressed_by_model_index() {
let mut view = TreeView::new(Rect::new(0, 0, 200, 200));
view.set_model(Arc::new(NestedModel));
assert!(!view.has_delegate());
assert!(view.begin_edit(0).is_none(), "with no delegate there is no editor to build");
assert_eq!(
view.set("has_delegate", CapabilityValue::Bool(true)),
Err(crate::widget::capability::types::CapabilityAccessError::ReadOnlyProperty)
);
view.set_delegate(Arc::new(RecordingDelegate));
assert!(view.has_delegate());
assert!(view.delegate_ref().is_some());
let editor = view.begin_edit(3).expect("grandchild is a row");
assert_eq!(view.editing_node(), Some(3), "the edit is keyed by the node's model index");
assert_eq!(view.commit_edit(editor.as_ref()).as_deref(), Some("3:0"));
let editor = view.begin_edit(1).expect("child_a is a row");
assert_eq!(view.editing_node(), Some(1));
view.cancel_edit();
assert_eq!(view.editing_node(), None);
drop(editor);
assert!(view.begin_edit(99).is_none());
}
#[test]
fn the_published_expand_commands_act_on_the_focused_node() {
use crate::widget::capability::types::CapabilityAccessError;
let mut view = TreeView::new(Rect::new(0, 0, 200, 200));
view.set_model(Arc::new(NestedModel));
assert_eq!(view.command("expand"), Err(CapabilityAccessError::OutOfRange));
assert!(view.set_focused_node(2));
assert_eq!(view.focused_node_expanded(), Some(true));
assert_eq!(view.command("collapse"), Ok(()));
assert_eq!(view.focused_node_expanded(), Some(false));
assert_eq!(view.command("toggle"), Ok(()));
assert_eq!(view.focused_node_expanded(), Some(true));
assert!(view.set_focused_node(1));
assert_eq!(view.focused_node_expanded(), None);
assert_eq!(view.command("expand"), Err(CapabilityAccessError::OutOfRange));
}
#[test]
fn an_out_of_range_row_index_is_refused() {
let view = TreeView::new(Rect::new(0, 0, 200, 200));
assert_eq!(view.node_row_rect(usize::MAX), None);
assert_eq!(view.node_row_rect(i32::MAX as usize + 1), None);
assert!(view.node_row_rect(0).is_some());
}
}