ferroui_diagnostics/diagnostics/view_models/
visual_tree_node.rs1use 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
16pub struct VisualTreeNode;
22
23impl VisualTreeNode {
24 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 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#[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
69pub 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 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 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 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 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 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
226pub 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}