use crate::{ViewExt, ext::SignalExt, prelude::*, widget::condition::when};
use alloc::vec::Vec;
use core::hash::Hash;
use std::collections::HashSet;
use waterui_core::id::Identifiable;
use waterui_layout::stack::{VStack, hstack, vstack};
#[derive(Debug, Clone)]
pub struct TreeNode<T, ID> {
pub id: ID,
pub data: T,
pub children: Vec<TreeNode<T, ID>>,
}
impl<T, ID: Clone + Hash + Ord> Identifiable for TreeNode<T, ID> {
type Id = ID;
fn id(&self) -> Self::Id {
self.id.clone()
}
}
#[derive(Debug)]
struct NodeView<T, ID, F>
where
T: Clone + 'static,
ID: Clone + Hash + Eq + Ord + 'static,
F: Fn(T) -> AnyView + Clone + 'static,
{
node: TreeNode<T, ID>,
expanded: Binding<HashSet<ID>>,
render_leaf: F,
}
impl<T, ID, F> View for NodeView<T, ID, F>
where
T: Clone + 'static,
ID: Clone + Hash + Eq + Ord + 'static,
F: Fn(T) -> AnyView + Clone + 'static,
{
fn body(self, _env: &Environment) -> impl View {
let expanded = self.expanded.clone();
let is_expanded = expanded.clone().map({
let id = self.node.id();
move |expanded_set| expanded_set.contains(&id)
});
let has_children = !self.node.children.is_empty();
let toggle_button = {
let expanded_for_action = expanded.clone();
let id = self.node.id();
let icon = is_expanded
.clone()
.map(|expanded| if expanded { "▼ " } else { "▶ " });
button(hstack((
when(has_children, move || text(icon.clone())),
(self.render_leaf)(self.node.data.clone()),
)))
.action(move || {
if has_children {
let mut set = expanded_for_action.get();
if set.contains(&id) {
set.remove(&id);
} else {
set.insert(id.clone());
}
expanded_for_action.set(set);
}
})
};
let children_view = {
let children = self.node.children.clone();
let render_leaf = self.render_leaf;
when(is_expanded, move || {
children
.clone()
.into_iter()
.map(|child_node| Self {
node: child_node,
expanded: expanded.clone(),
render_leaf: render_leaf.clone(),
})
.collect::<VStack<_>>()
})
};
vstack((toggle_button, children_view))
}
}
#[derive(Debug)]
pub struct TreeView<T, ID, F>
where
T: Clone + 'static,
ID: Clone + Hash + Eq + Ord + 'static,
F: Fn(T) -> AnyView + Clone + 'static,
{
nodes: Vec<TreeNode<T, ID>>,
expanded: Binding<HashSet<ID>>,
render_leaf: F,
}
impl<T, ID, F> TreeView<T, ID, F>
where
T: Clone + 'static,
ID: Clone + Hash + Eq + Ord + 'static,
F: Fn(T) -> AnyView + Clone + 'static,
{
pub const fn new(
nodes: Vec<TreeNode<T, ID>>,
expanded: Binding<HashSet<ID>>,
render_leaf: F,
) -> Self {
Self {
nodes,
expanded,
render_leaf,
}
}
}
impl<T, ID, F> View for TreeView<T, ID, F>
where
T: Clone + 'static,
ID: Clone + Hash + Eq + Ord + 'static,
F: Fn(T) -> AnyView + Clone + 'static,
{
fn body(self, _env: &Environment) -> impl View {
self.nodes
.into_iter()
.map(move |node| NodeView {
node,
expanded: self.expanded.clone(),
render_leaf: self.render_leaf.clone(),
})
.collect::<VStack<_>>()
}
}
#[allow(clippy::implicit_hasher)]
pub const fn tree_view<T, ID, F>(
nodes: Vec<TreeNode<T, ID>>,
expanded: Binding<HashSet<ID>>,
render_leaf: F,
) -> TreeView<T, ID, F>
where
T: Clone + 'static,
ID: Clone + Hash + Eq + Ord + 'static,
F: Fn(T) -> AnyView + Clone + 'static,
{
TreeView::new(nodes, expanded, render_leaf)
}