Skip to main content

ferroui_diagnostics/diagnostics/view_models/
visual_tree_node.rs

1//! Port of `Diagnostics/ViewModels/VisualTreeNode.cs`.
2
3use super::{TreeNode, TreeNodeCollection, TreeNodeCollectionImpl, TreeNodeKind};
4use crate::diagnostics::controls::Application;
5use crate::diagnostics::views::MainWindow;
6use ferroui_base::collections::{FerroList, FerroListExtensions};
7use ferroui_base::interactivity::RoutedEventArgs;
8use ferroui_base::reactive::{CompositeDisposable, IDisposable, IObservable, IObserver, Observable, ObservableExt};
9use ferroui_base::{FerroObject, FerroObjectExtensions, Nullable, Ref, StyledElement, Visual};
10use ferroui_controls::diagnostics::{IPopupHostProvider, ToolTipDiagnostics};
11use ferroui_controls::primitives::{FlyoutBase, IPopupHost, Popup, PopupFlyoutBase};
12use ferroui_controls::{Button, ContextMenu, Control, Window};
13use std::cell::RefCell;
14use std::rc::{Rc, Weak};
15
16/// A node of the visual tree.
17///
18/// The original derives the node class from [`TreeNode`]; here a visual
19/// tree node is a [`TreeNode`] of the kind [`TreeNodeKind::Visual`], made by
20/// the functions of this type.
21pub struct VisualTreeNode;
22
23impl VisualTreeNode {
24    /// `VisualTreeNode(FerroObject ferroObject, TreeNode? parent, string? customName = null)`:
25    /// the node of an object of the visual tree, whose children are the
26    /// visual children of the visual and the root of the popup it hosts, or
27    /// the top levels of an application.
28    pub fn new(ferro_object: Ref<FerroObject>, parent: Option<&Rc<TreeNode>>, custom_name: Option<String>) -> Rc<TreeNode> {
29        let visual = ferro_object.cast::<Visual>();
30        let host = ferro_object.cast::<Application>();
31        let is_in_template = ferro_object
32            .cast::<StyledElement>()
33            .is_some_and(|styleable| styleable.templated_parent().is_some());
34        TreeNode::new_node(ferro_object, parent, custom_name, TreeNodeKind::Visual { is_in_template }, move |this| {
35            if let Some(visual) = visual {
36                VisualTreeNodeCollection::new(this, visual)
37            } else if let Some(host) = host {
38                ApplicationHostVisuals::new(this, host)
39            } else {
40                TreeNodeCollection::empty()
41            }
42        })
43    }
44
45    /// `Create(object control)`: the root node of the visual tree of
46    /// `control`, or no node when it is not an object of the framework.
47    pub fn create(control: impl Into<Nullable<FerroObject>>) -> Vec<Rc<TreeNode>> {
48        match control.into().0 {
49            Some(visual) => vec![VisualTreeNode::new(visual, None, None)],
50            None => Vec::new(),
51        }
52    }
53}
54
55/// The root of a popup that a visual hosts: the control at the root of the
56/// popup and the name its node is shown with.
57#[derive(Clone)]
58struct PopupRoot {
59    root: Ref<Control>,
60    custom_name: Option<String>,
61}
62
63impl PopupRoot {
64    fn new(root: Ref<Control>, custom_name: Option<String>) -> Self {
65        Self { root, custom_name }
66    }
67}
68
69/// `VisualTreeNode.VisualTreeNodeCollection`: the nodes of the visual
70/// children of a visual, and of the root of the popup it hosts, following
71/// them.
72pub struct VisualTreeNodeCollection {
73    control: Ref<Visual>,
74    subscriptions: CompositeDisposable,
75}
76
77impl VisualTreeNodeCollection {
78    pub fn new(owner: Weak<TreeNode>, control: Ref<Visual>) -> Rc<TreeNodeCollection> {
79        TreeNodeCollection::new(owner, Box::new(Self { control, subscriptions: CompositeDisposable::with_capacity(2) }))
80    }
81
82    /// The roots of the popups a visual hosts: the host of the popup of a
83    /// popup; for a control, the context menu itself, or the host of its
84    /// context flyout, attached flyout, tooltip or button flyout, whichever
85    /// is set first in that order. `None` for any other visual.
86    fn get_hosted_popup_root_observable(visual: &Ref<Visual>) -> Option<Rc<dyn IObservable<Option<PopupRoot>>>> {
87        fn get_popup_host_observable(
88            popup_host_provider: Rc<dyn IPopupHostProvider>,
89            provider_name: Option<&'static str>,
90        ) -> Rc<dyn IObservable<Option<PopupRoot>>> {
91            let provider = popup_host_provider.clone();
92            Observable::create(move |observer: Rc<dyn IObserver<Option<Rc<dyn IPopupHost>>>>| {
93                let handler: Rc<dyn Fn(Option<&Rc<dyn IPopupHost>>)> =
94                    Rc::new(move |args: Option<&Rc<dyn IPopupHost>>| observer.on_next(args.cloned()));
95                provider.popup_host_changed(handler)
96            })
97            .start_with(popup_host_provider.popup_host())
98            .select(move |popup_host: Option<Rc<dyn IPopupHost>>| {
99                // Every popup host of the framework is a control.
100                popup_host.map(|popup_host| {
101                    let control = popup_host.as_control();
102                    let custom_name =
103                        provider_name.map(|provider_name| format!("{provider_name} ({})", control.get_type().name()));
104                    PopupRoot::new(control, custom_name)
105                })
106            })
107        }
108
109        if let Some(p) = visual.cast::<Popup>() {
110            return Some(get_popup_host_observable(p.to_popup_host_provider(), None));
111        }
112
113        let c = visual.cast::<Control>()?;
114
115        fn object<T: ferroui_base::ObjectType + ferroui_base::Upcast<FerroObject>>(
116            value: Option<Ref<T>>,
117        ) -> Option<Ref<FerroObject>> {
118            value.map(|value| value.upcast())
119        }
120
121        // The property observables of the object (`Interactive` has a `get_observable` of its own,
122        // for routed events).
123        let o: &FerroObject = &c;
124        let observables: Vec<Rc<dyn IObservable<Option<Ref<FerroObject>>>>> = vec![
125            FerroObjectExtensions::get_observable(o, Control::context_flyout_property()).select(object),
126            FerroObjectExtensions::get_observable(o, Control::context_menu_property()).select(object),
127            FerroObjectExtensions::get_observable(o, FlyoutBase::attached_flyout_property()).select(object),
128            FerroObjectExtensions::get_observable(o, ToolTipDiagnostics::tool_tip_property()).select(object),
129            FerroObjectExtensions::get_observable(o, Button::flyout_property()).select(object),
130        ];
131
132        let as_popup_host_provider = |item: &Option<Ref<FerroObject>>| -> Option<Rc<dyn IPopupHostProvider>> {
133            item.as_ref().and_then(|item| item.cast::<PopupFlyoutBase>()).map(|flyout| flyout.to_popup_host_provider())
134        };
135
136        let popup_roots = Observable::combine_latest_all(observables).select(
137            move |items: Vec<Option<Ref<FerroObject>>>| -> Rc<dyn IObservable<Option<PopupRoot>>> {
138                let context_flyout = as_popup_host_provider(&items[0]);
139                let context_menu = items[1].as_ref().and_then(|item| item.cast::<ContextMenu>());
140                let attached_flyout = as_popup_host_provider(&items[2]);
141                let tool_tip = items[3]
142                    .as_ref()
143                    .and_then(|item| item.cast::<ferroui_controls::ToolTip>())
144                    .map(|tool_tip| tool_tip.to_popup_host_provider());
145                let button_flyout = as_popup_host_provider(&items[4]);
146
147                if let Some(context_menu) = context_menu {
148                    // Note: ContextMenus are special since all the items are added as visual children.
149                    // So we don't need to go via Popup
150                    return Observable::return_(Some(PopupRoot::new(context_menu.upcast(), None)));
151                }
152
153                if let Some(context_flyout) = context_flyout {
154                    return get_popup_host_observable(context_flyout, Some("ContextFlyout"));
155                }
156
157                if let Some(attached_flyout) = attached_flyout {
158                    return get_popup_host_observable(attached_flyout, Some("AttachedFlyout"));
159                }
160
161                if let Some(tool_tip) = tool_tip {
162                    return get_popup_host_observable(tool_tip, Some("ToolTip"));
163                }
164
165                if let Some(button_flyout) = button_flyout {
166                    return get_popup_host_observable(button_flyout, Some("Flyout"));
167                }
168
169                Observable::return_(None)
170            },
171        );
172
173        Some(Observable::switch(popup_roots))
174    }
175}
176
177impl TreeNodeCollectionImpl for VisualTreeNodeCollection {
178    /// The original disposes the subscriptions only, not the nodes.
179    fn dispose(&self, _this: &TreeNodeCollection) {
180        self.subscriptions.dispose();
181    }
182
183    fn initialize(&self, this: &TreeNodeCollection, nodes: &FerroList<Rc<TreeNode>>) {
184        self.subscriptions.clear();
185
186        let owner = this.owner();
187        let owner = owner.as_ref().map(Rc::downgrade).unwrap_or_default();
188
189        if let Some(popup_root_observable) = Self::get_hosted_popup_root_observable(&self.control) {
190            let child_node: RefCell<Option<Rc<TreeNode>>> = RefCell::new(None);
191            let nodes = nodes.clone();
192            let owner = owner.clone();
193
194            self.subscriptions.add(popup_root_observable.subscribe_fn(move |popup_root: Option<PopupRoot>| {
195                if let Some(popup_root) = popup_root {
196                    let node = VisualTreeNode::new(
197                        popup_root.root.upcast(),
198                        owner.upgrade().as_ref(),
199                        popup_root.custom_name,
200                    );
201                    *child_node.borrow_mut() = Some(node.clone());
202
203                    nodes.add(node);
204                } else {
205                    let child = child_node.borrow().clone();
206                    if let Some(child) = child {
207                        nodes.remove(&child);
208                    }
209                }
210            }));
211        }
212
213        let (added, removed, reset) = (nodes.clone(), nodes.clone(), nodes.clone());
214        self.subscriptions.add(FerroListExtensions::for_each_item_indexed(
215            self.control.visual_children(),
216            move |i, item: &Ref<Visual>| {
217                added.insert(i, VisualTreeNode::new(item.clone().upcast(), owner.upgrade().as_ref(), None));
218            },
219            move |i, _| removed.remove_at(i),
220            move || reset.clear(),
221            false,
222        ));
223    }
224}
225
226/// `VisualTreeNode.ApplicationHostVisuals`: the nodes of the top levels of
227/// an application (its main view, or its windows except the window of the
228/// developer tools), following the windows that open and close.
229pub struct ApplicationHostVisuals {
230    application: Ref<Application>,
231    subscriptions: RefCell<Rc<CompositeDisposable>>,
232}
233
234impl ApplicationHostVisuals {
235    pub fn new(owner: Weak<TreeNode>, host: Ref<Application>) -> Rc<TreeNodeCollection> {
236        TreeNodeCollection::new(
237            owner,
238            Box::new(Self {
239                application: host,
240                subscriptions: RefCell::new(Rc::new(CompositeDisposable::with_capacity(2))),
241            }),
242        )
243    }
244}
245
246impl TreeNodeCollectionImpl for ApplicationHostVisuals {
247    fn initialize(&self, this: &TreeNodeCollection, nodes: &FerroList<Rc<TreeNode>>) {
248        let owner = this.owner();
249        let application_lifetime = self.application.application_lifetime();
250
251        if let Some(single) =
252            application_lifetime.as_ref().and_then(|lifetime| lifetime.as_single_view_application_lifetime())
253        {
254            if let Some(main_view) = single.main_view() {
255                nodes.add(VisualTreeNode::new(main_view.upcast(), owner.as_ref(), None));
256            }
257        }
258
259        if let Some(classic) =
260            application_lifetime.as_ref().and_then(|lifetime| lifetime.as_classic_desktop_style_application_lifetime())
261        {
262            for window in classic.windows() {
263                if window.is::<MainWindow>() {
264                    continue;
265                }
266                nodes.add(VisualTreeNode::new(window.upcast(), owner.as_ref(), None));
267            }
268
269            let owner = owner.as_ref().map(Rc::downgrade).unwrap_or_default();
270
271            let opened = {
272                let nodes = nodes.clone();
273                let owner = owner.clone();
274                Window::window_opened_event().add_class_handler::<Window>(move |s: &Window, _e: &RoutedEventArgs| {
275                    let s = s.to_ref();
276                    if s.is::<MainWindow>() {
277                        return;
278                    }
279                    nodes.add(VisualTreeNode::new(s.upcast(), owner.upgrade().as_ref(), None));
280                })
281            };
282            let closed = {
283                let nodes = nodes.clone();
284                Window::window_closed_event().add_class_handler::<Window>(move |s: &Window, _e: &RoutedEventArgs| {
285                    let s = s.to_ref();
286                    if s.is::<MainWindow>() {
287                        return;
288                    }
289                    let item = nodes.snapshot().iter().find(|node| node.visual().ptr_eq(&s)).cloned();
290                    if let Some(item) = item {
291                        nodes.remove(&item);
292                    }
293                })
294            };
295
296            *self.subscriptions.borrow_mut() = Rc::new(CompositeDisposable::from_disposables([opened, closed]));
297        }
298    }
299
300    fn dispose(&self, this: &TreeNodeCollection) {
301        let subscriptions = self.subscriptions.borrow().clone();
302        subscriptions.dispose();
303        this.base_dispose();
304    }
305}