Skip to main content

ferroui_diagnostics/diagnostics/view_models/
tree_node.rs

1//! Port of `Diagnostics/ViewModels/TreeNode.cs`.
2
3use 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/// The class of a [`TreeNode`]: the abstract node of the original is
15/// derived by [`LogicalTreeNode`](super::LogicalTreeNode) and
16/// [`VisualTreeNode`](super::VisualTreeNode), whose constructors make nodes
17/// of this type with the members they add.
18#[derive(Clone, Copy, Debug, PartialEq, Eq)]
19pub enum TreeNodeKind {
20    /// A node of the logical tree (`LogicalTreeNode`).
21    Logical,
22    /// A node of the visual tree (`VisualTreeNode`), with its `IsInTemplate`.
23    Visual { is_in_template: bool },
24    /// A node of the combined tree (an addition of the port, `combined_tree_node.rs`): an element
25    /// of the logical tree, or a part of a template (`visual_only`), shown greyed out.
26    Combined { visual_only: bool },
27}
28
29/// A node of a tree of the developer tools: an object of the inspected
30/// application, its type, name and classes, whether it is expanded in the
31/// tree view, and its children.
32pub 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    /// `protected TreeNode(FerroObject ferroObject, TreeNode? parent, string? customTypeName = null)`
60    /// and the constructor of the deriving class: `kind` is the class of the
61    /// node and `children` creates its `Children`, the collection of the
62    /// children of the node it is given.
63    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    /// The subscription of the constructor to the classes of the element:
97    /// `Classes` is updated now (`StartWith(null)`) and whenever the classes
98    /// change. The original subscribes weakly; here the classes hold a
99    /// handler that reaches the node and the element through weak
100    /// references, and disposing the node removes it.
101    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    /// `IsRoot`: whether the object is the root of a tree of its own (a top
126    /// level, a context menu or the host of a popup).
127    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    /// The weight of the font of the node in the tree: bold for a root.
135    pub fn font_weight(&self) -> FontWeight {
136        self.font_weight
137    }
138
139    /// The children of the node.
140    pub fn children(&self) -> Rc<TreeNodeCollection> {
141        self.children.clone()
142    }
143
144    /// The classes of the element, in parentheses, or empty when it has
145    /// none.
146    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    /// The name of the element.
155    pub fn element_name(&self) -> Option<String> {
156        self.element_name.clone()
157    }
158
159    /// The object of the node.
160    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    /// The parent node; `None` for a root, and once the parent is gone (the
173    /// nodes hold their parents weakly).
174    pub fn parent(&self) -> Option<Rc<TreeNode>> {
175        self.parent.upgrade()
176    }
177
178    /// The `Type` property: the name of the type of the object, or the name
179    /// the node was created with.
180    pub fn type_(&self) -> String {
181        self.type_.clone()
182    }
183
184    /// The class of the node: [`LogicalTreeNode`](super::LogicalTreeNode) or
185    /// [`VisualTreeNode`](super::VisualTreeNode).
186    pub fn kind(&self) -> TreeNodeKind {
187        self.kind
188    }
189
190    /// `VisualTreeNode.IsInTemplate`: whether the element is part of a
191    /// template (it has a templated parent). `false` for a node of the
192    /// logical tree.
193    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    /// Whether the node is a part of a template in the combined tree, which the tree shows
198    /// greyed out (an addition of the port).
199    pub fn is_visual_only(&self) -> bool {
200        matches!(self.kind, TreeNodeKind::Combined { visual_only: true })
201    }
202
203    /// States what the TreeDataGrid of a tree page observes of the nodes: their property change
204    /// notifications (the expansion of a node, its classes). Once per thread.
205    ///
206    /// Deviation (DEVIATIONS.md, Developer tools): the tree pages show the nodes in a TreeDataGrid,
207    /// not in a tree view.
208    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    /// `Dispose()`: ends the subscription to the classes and disposes the
215    /// children.
216    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        // Deviation (DEVIATIONS.md, Developer tools): not a member of the base class upstream (it
238        // is `VisualTreeNode.IsInTemplate`); the cells of the tree pages show the nodes of
239        // template parts in their own style.
240        IsInTemplate: bool { get: TreeNode::is_in_template },
241        IsVisualOnly: bool { get: TreeNode::is_visual_only },
242    ],
243    notify_property_changed: TreeNode,
244});