use super::{TreeNodeCollection, ViewModelBase};
use ferroui_base::collections::NotifyCollectionChangedEventArgs;
use ferroui_base::data::model::{Event, INotifyPropertyChanged, ModelTypes};
use ferroui_base::media::FontWeight;
use ferroui_base::reactive::{Disposable, IDisposable};
use ferroui_base::{ferro_markup_type, FerroObject, Ref, StyledElement};
use ferroui_controls::primitives::{OverlayPopupHost, PopupRoot};
use ferroui_controls::{ContextMenu, TopLevel};
use std::cell::{Cell, RefCell};
use std::rc::{Rc, Weak};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TreeNodeKind {
Logical,
Visual { is_in_template: bool },
Combined { visual_only: bool },
}
pub struct TreeNode {
base: ViewModelBase,
classes_subscription: RefCell<Option<Rc<dyn IDisposable>>>,
classes: RefCell<String>,
is_expanded: Cell<bool>,
font_weight: FontWeight,
children: Rc<TreeNodeCollection>,
element_name: Option<String>,
visual: Ref<FerroObject>,
parent: Weak<TreeNode>,
type_: String,
kind: TreeNodeKind,
}
impl PartialEq for TreeNode {
fn eq(&self, other: &Self) -> bool {
std::ptr::eq(self, other)
}
}
impl INotifyPropertyChanged for TreeNode {
fn property_changed(&self) -> &Event<str> {
self.base.property_changed()
}
}
impl TreeNode {
pub(crate) fn new_node(
ferro_object: Ref<FerroObject>,
parent: Option<&Rc<TreeNode>>,
custom_type_name: Option<String>,
kind: TreeNodeKind,
children: impl FnOnce(Weak<TreeNode>) -> Rc<TreeNodeCollection>,
) -> Rc<TreeNode> {
let type_ = custom_type_name.unwrap_or_else(|| ferro_object.get_type().name().to_string());
let font_weight = if Self::is_root(&ferro_object) { FontWeight::Bold } else { FontWeight::Normal };
let element_name = ferro_object.cast::<StyledElement>().and_then(|named| named.name());
let parent = parent.map_or_else(Weak::new, Rc::downgrade);
let this = Rc::new_cyclic(|this: &Weak<TreeNode>| TreeNode {
base: ViewModelBase::new(),
classes_subscription: RefCell::new(None),
classes: RefCell::new(String::new()),
is_expanded: Cell::new(false),
font_weight,
children: children(this.clone()),
element_name,
visual: ferro_object,
parent,
type_,
kind,
});
if let Some(element) = this.visual.cast::<StyledElement>() {
this.subscribe_to_classes(&element);
}
this
}
fn subscribe_to_classes(self: &Rc<Self>, element: &Ref<StyledElement>) {
let classes = element.classes();
let node = Rc::downgrade(self);
let weak_element = element.downgrade();
let update: Rc<dyn Fn()> = Rc::new(move || {
let (Some(node), Some(element)) = (node.upgrade(), weak_element.upgrade()) else {
return;
};
let classes = element.classes();
if classes.count() > 0 {
node.set_classes(format!("({})", classes.snapshot().join(" ")));
} else {
node.set_classes(String::new());
}
});
update();
let token = classes.add_collection_changed(Rc::new(move |_: &NotifyCollectionChangedEventArgs<'_, String>| update()));
*self.classes_subscription.borrow_mut() = Some(Disposable::create(move || {
classes.remove_collection_changed(token);
}));
}
fn is_root(visual: &Ref<FerroObject>) -> bool {
visual.is::<TopLevel>()
|| visual.is::<ContextMenu>()
|| visual.is::<PopupRoot>()
|| visual.is::<OverlayPopupHost>()
}
pub fn font_weight(&self) -> FontWeight {
self.font_weight
}
pub fn children(&self) -> Rc<TreeNodeCollection> {
self.children.clone()
}
pub fn classes(&self) -> String {
self.classes.borrow().clone()
}
fn set_classes(&self, value: String) {
self.base.raise_and_set_if_changed(&self.classes, value, "Classes");
}
pub fn element_name(&self) -> Option<String> {
self.element_name.clone()
}
pub fn visual(&self) -> Ref<FerroObject> {
self.visual.clone()
}
pub fn is_expanded(&self) -> bool {
self.is_expanded.get()
}
pub fn set_is_expanded(&self, value: bool) {
self.base.raise_and_set_if_changed_cell(&self.is_expanded, value, "IsExpanded");
}
pub fn parent(&self) -> Option<Rc<TreeNode>> {
self.parent.upgrade()
}
pub fn type_(&self) -> String {
self.type_.clone()
}
pub fn kind(&self) -> TreeNodeKind {
self.kind
}
pub fn is_in_template(&self) -> bool {
matches!(self.kind, TreeNodeKind::Visual { is_in_template: true } | TreeNodeKind::Combined { visual_only: true })
}
pub fn is_visual_only(&self) -> bool {
matches!(self.kind, TreeNodeKind::Combined { visual_only: true })
}
pub(crate) fn register_model() {
if !ModelTypes::is_registered(std::any::TypeId::of::<TreeNode>()) {
ModelTypes::register::<TreeNode>(|builder| builder.notify_property_changed());
}
}
pub fn dispose(&self) {
let classes_subscription = self.classes_subscription.borrow().clone();
if let Some(classes_subscription) = classes_subscription {
classes_subscription.dispose();
}
self.children.dispose();
}
}
ferro_markup_type!(class TreeNode {
this: Rc<TreeNode>,
handles: [TreeNode, Rc<TreeNode>, Option<Rc<TreeNode>>],
properties: [
FontWeight: FontWeight { get: TreeNode::font_weight },
Children: Rc<TreeNodeCollection> { get: TreeNode::children },
Classes: String { get: TreeNode::classes },
ElementName: Option<String> { get: TreeNode::element_name },
Visual: Ref<FerroObject> { get: TreeNode::visual },
IsExpanded: bool { get: TreeNode::is_expanded, set: TreeNode::set_is_expanded },
Parent: Option<Rc<TreeNode>> { get: TreeNode::parent },
Type: String { get: TreeNode::type_ },
IsInTemplate: bool { get: TreeNode::is_in_template },
IsVisualOnly: bool { get: TreeNode::is_visual_only },
],
notify_property_changed: TreeNode,
});