ferroui_diagnostics/diagnostics/view_models/
tree_node.rs1use super::{TreeNodeCollection, ViewModelBase};
4use ferroui_base::collections::NotifyCollectionChangedEventArgs;
5use ferroui_base::data::model::{Event, INotifyPropertyChanged, ModelTypes};
6use ferroui_base::media::FontWeight;
7use ferroui_base::reactive::{Disposable, IDisposable};
8use ferroui_base::{ferro_markup_type, FerroObject, Ref, StyledElement};
9use ferroui_controls::primitives::{OverlayPopupHost, PopupRoot};
10use ferroui_controls::{ContextMenu, TopLevel};
11use std::cell::{Cell, RefCell};
12use std::rc::{Rc, Weak};
13
14#[derive(Clone, Copy, Debug, PartialEq, Eq)]
19pub enum TreeNodeKind {
20 Logical,
22 Visual { is_in_template: bool },
24 Combined { visual_only: bool },
27}
28
29pub struct TreeNode {
33 base: ViewModelBase,
34 classes_subscription: RefCell<Option<Rc<dyn IDisposable>>>,
35 classes: RefCell<String>,
36 is_expanded: Cell<bool>,
37 font_weight: FontWeight,
38 children: Rc<TreeNodeCollection>,
39 element_name: Option<String>,
40 visual: Ref<FerroObject>,
41 parent: Weak<TreeNode>,
42 type_: String,
43 kind: TreeNodeKind,
44}
45
46impl PartialEq for TreeNode {
47 fn eq(&self, other: &Self) -> bool {
48 std::ptr::eq(self, other)
49 }
50}
51
52impl INotifyPropertyChanged for TreeNode {
53 fn property_changed(&self) -> &Event<str> {
54 self.base.property_changed()
55 }
56}
57
58impl TreeNode {
59 pub(crate) fn new_node(
64 ferro_object: Ref<FerroObject>,
65 parent: Option<&Rc<TreeNode>>,
66 custom_type_name: Option<String>,
67 kind: TreeNodeKind,
68 children: impl FnOnce(Weak<TreeNode>) -> Rc<TreeNodeCollection>,
69 ) -> Rc<TreeNode> {
70 let type_ = custom_type_name.unwrap_or_else(|| ferro_object.get_type().name().to_string());
71 let font_weight = if Self::is_root(&ferro_object) { FontWeight::Bold } else { FontWeight::Normal };
72 let element_name = ferro_object.cast::<StyledElement>().and_then(|named| named.name());
73 let parent = parent.map_or_else(Weak::new, Rc::downgrade);
74
75 let this = Rc::new_cyclic(|this: &Weak<TreeNode>| TreeNode {
76 base: ViewModelBase::new(),
77 classes_subscription: RefCell::new(None),
78 classes: RefCell::new(String::new()),
79 is_expanded: Cell::new(false),
80 font_weight,
81 children: children(this.clone()),
82 element_name,
83 visual: ferro_object,
84 parent,
85 type_,
86 kind,
87 });
88
89 if let Some(element) = this.visual.cast::<StyledElement>() {
90 this.subscribe_to_classes(&element);
91 }
92
93 this
94 }
95
96 fn subscribe_to_classes(self: &Rc<Self>, element: &Ref<StyledElement>) {
102 let classes = element.classes();
103 let node = Rc::downgrade(self);
104 let weak_element = element.downgrade();
105 let update: Rc<dyn Fn()> = Rc::new(move || {
106 let (Some(node), Some(element)) = (node.upgrade(), weak_element.upgrade()) else {
107 return;
108 };
109 let classes = element.classes();
110 if classes.count() > 0 {
111 node.set_classes(format!("({})", classes.snapshot().join(" ")));
112 } else {
113 node.set_classes(String::new());
114 }
115 });
116
117 update();
118
119 let token = classes.add_collection_changed(Rc::new(move |_: &NotifyCollectionChangedEventArgs<'_, String>| update()));
120 *self.classes_subscription.borrow_mut() = Some(Disposable::create(move || {
121 classes.remove_collection_changed(token);
122 }));
123 }
124
125 fn is_root(visual: &Ref<FerroObject>) -> bool {
128 visual.is::<TopLevel>()
129 || visual.is::<ContextMenu>()
130 || visual.is::<PopupRoot>()
131 || visual.is::<OverlayPopupHost>()
132 }
133
134 pub fn font_weight(&self) -> FontWeight {
136 self.font_weight
137 }
138
139 pub fn children(&self) -> Rc<TreeNodeCollection> {
141 self.children.clone()
142 }
143
144 pub fn classes(&self) -> String {
147 self.classes.borrow().clone()
148 }
149
150 fn set_classes(&self, value: String) {
151 self.base.raise_and_set_if_changed(&self.classes, value, "Classes");
152 }
153
154 pub fn element_name(&self) -> Option<String> {
156 self.element_name.clone()
157 }
158
159 pub fn visual(&self) -> Ref<FerroObject> {
161 self.visual.clone()
162 }
163
164 pub fn is_expanded(&self) -> bool {
165 self.is_expanded.get()
166 }
167
168 pub fn set_is_expanded(&self, value: bool) {
169 self.base.raise_and_set_if_changed_cell(&self.is_expanded, value, "IsExpanded");
170 }
171
172 pub fn parent(&self) -> Option<Rc<TreeNode>> {
175 self.parent.upgrade()
176 }
177
178 pub fn type_(&self) -> String {
181 self.type_.clone()
182 }
183
184 pub fn kind(&self) -> TreeNodeKind {
187 self.kind
188 }
189
190 pub fn is_in_template(&self) -> bool {
194 matches!(self.kind, TreeNodeKind::Visual { is_in_template: true } | TreeNodeKind::Combined { visual_only: true })
195 }
196
197 pub fn is_visual_only(&self) -> bool {
200 matches!(self.kind, TreeNodeKind::Combined { visual_only: true })
201 }
202
203 pub(crate) fn register_model() {
209 if !ModelTypes::is_registered(std::any::TypeId::of::<TreeNode>()) {
210 ModelTypes::register::<TreeNode>(|builder| builder.notify_property_changed());
211 }
212 }
213
214 pub fn dispose(&self) {
217 let classes_subscription = self.classes_subscription.borrow().clone();
218 if let Some(classes_subscription) = classes_subscription {
219 classes_subscription.dispose();
220 }
221 self.children.dispose();
222 }
223}
224
225ferro_markup_type!(class TreeNode {
226 this: Rc<TreeNode>,
227 handles: [TreeNode, Rc<TreeNode>, Option<Rc<TreeNode>>],
228 properties: [
229 FontWeight: FontWeight { get: TreeNode::font_weight },
230 Children: Rc<TreeNodeCollection> { get: TreeNode::children },
231 Classes: String { get: TreeNode::classes },
232 ElementName: Option<String> { get: TreeNode::element_name },
233 Visual: Ref<FerroObject> { get: TreeNode::visual },
234 IsExpanded: bool { get: TreeNode::is_expanded, set: TreeNode::set_is_expanded },
235 Parent: Option<Rc<TreeNode>> { get: TreeNode::parent },
236 Type: String { get: TreeNode::type_ },
237 IsInTemplate: bool { get: TreeNode::is_in_template },
241 IsVisualOnly: bool { get: TreeNode::is_visual_only },
242 ],
243 notify_property_changed: TreeNode,
244});