use std::collections::HashMap;
use std::fmt;
use std::sync::Arc;
pub mod drag;
pub mod node;
pub(crate) mod search;
pub(crate) mod transitions;
pub use drag::{DropPosition, ItemDragMsg, ItemDragOutcome};
pub use node::{ItemNode, NodeId, VisibleItem};
pub use search::ItemSearchState;
use drag::ItemDragState;
use node::InternalItem;
pub type DisplayFn<T> = Arc<dyn Fn(&T) -> String + Send + Sync>;
pub struct ItemTree<T: Clone + fmt::Debug + Send + Sync + 'static> {
pub(crate) store: HashMap<NodeId, InternalItem<T>>,
pub(crate) root_id: Option<NodeId>,
pub(crate) order: Vec<NodeId>,
pub(crate) selected_ids: Vec<NodeId>,
pub(crate) active_id: Option<NodeId>,
pub(crate) anchor_id: Option<NodeId>,
pub(crate) search: Option<ItemSearchState>,
pub(crate) display_fn: Option<DisplayFn<T>>,
pub(crate) dnd_enabled: bool,
pub(crate) drag: Option<ItemDragState>,
}
impl<T: Clone + fmt::Debug + Send + Sync + 'static> Clone for ItemTree<T> {
fn clone(&self) -> Self {
Self {
store: self.store.clone(),
root_id: self.root_id,
order: self.order.clone(),
selected_ids: self.selected_ids.clone(),
active_id: self.active_id,
anchor_id: self.anchor_id,
search: self.search.clone(),
display_fn: self.display_fn.clone(),
dnd_enabled: self.dnd_enabled,
drag: self.drag.clone(),
}
}
}
impl<T: Clone + fmt::Debug + Send + Sync + 'static> fmt::Debug for ItemTree<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ItemTree")
.field("root_id", &self.root_id)
.field("nodes", &self.order.len())
.field("selected_ids", &self.selected_ids)
.field("active_id", &self.active_id)
.field("search", &self.search.as_ref().map(|s| &s.query))
.field("dnd_enabled", &self.dnd_enabled)
.field("dragging", &self.drag.is_some())
.finish_non_exhaustive()
}
}
impl<T: Clone + fmt::Debug + Send + Sync + 'static> ItemTree<T> {
pub fn new() -> Self {
Self {
store: HashMap::new(),
root_id: None,
order: Vec::new(),
selected_ids: Vec::new(),
active_id: None,
anchor_id: None,
search: None,
display_fn: None,
dnd_enabled: false,
drag: None,
}
}
pub fn with_display(mut self, f: impl Fn(&T) -> String + Send + Sync + 'static) -> Self {
self.display_fn = Some(Arc::new(f));
self
}
pub fn with_drag_and_drop(mut self, enabled: bool) -> Self {
self.dnd_enabled = enabled;
self
}
}
impl<T: Clone + fmt::Debug + Send + Sync + 'static> Default for ItemTree<T> {
fn default() -> Self {
Self::new()
}
}
impl<T: Clone + fmt::Debug + Send + Sync + 'static> ItemTree<T> {
pub fn visible_rows(&self) -> Vec<VisibleItem> {
let mut rows = Vec::new();
let Some(root_id) = self.root_id else {
return rows;
};
if let Some(search) = &self.search {
self.collect_search(root_id, &mut rows, &search.visible_ids);
} else {
self.collect_normal(root_id, &mut rows);
}
rows
}
pub fn is_selected(&self, id: NodeId) -> bool {
self.selected_ids.contains(&id)
}
pub fn selected_ids(&self) -> &[NodeId] {
&self.selected_ids
}
pub fn active_id(&self) -> Option<NodeId> {
self.active_id
}
pub fn search_state(&self) -> Option<&ItemSearchState> {
self.search.as_ref()
}
pub fn search_query(&self) -> Option<&str> {
self.search.as_ref().map(|s| s.query.as_str())
}
pub fn search_match_count(&self) -> usize {
self.search.as_ref().map_or(0, |s| s.match_count)
}
pub fn node_count(&self) -> usize {
self.store.len()
}
pub fn is_drag_and_drop_enabled(&self) -> bool {
self.dnd_enabled
}
pub fn is_dragging(&self) -> bool {
self.drag.is_some()
}
pub fn drag_sources(&self) -> &[NodeId] {
self.drag.as_ref().map_or(&[], |d| d.sources.as_slice())
}
pub fn drop_target(&self) -> Option<(NodeId, DropPosition)> {
self.drag.as_ref().and_then(|d| d.hover)
}
pub fn is_expanded(&self, id: NodeId) -> Option<bool> {
self.store.get(&id).map(|item| item.is_expanded)
}
fn collect_normal(&self, id: NodeId, rows: &mut Vec<VisibleItem>) {
let Some(item) = self.store.get(&id) else {
return;
};
rows.push(self.to_visible(item));
if item.is_expanded {
for &child_id in &item.children_ids {
self.collect_normal(child_id, rows);
}
}
}
fn collect_search(
&self,
id: NodeId,
rows: &mut Vec<VisibleItem>,
visible_ids: &std::collections::HashSet<NodeId>,
) {
if !visible_ids.contains(&id) {
return;
}
let Some(item) = self.store.get(&id) else {
return;
};
rows.push(self.to_visible(item));
for &child_id in &item.children_ids {
self.collect_search(child_id, rows, visible_ids);
}
}
fn to_visible(&self, item: &InternalItem<T>) -> VisibleItem {
let label = self
.display_fn
.as_ref()
.map(|f| f(&item.data))
.unwrap_or_default();
VisibleItem {
id: item.id,
label,
depth: item.depth,
is_expanded: item.is_expanded,
has_children: item.has_children(),
is_selected: item.is_selected,
is_active: self.active_id == Some(item.id),
}
}
}