Skip to main content

ferroui_diagnostics/diagnostics/view_models/
main_view_model.rs

1//! Port of `Diagnostics/ViewModels/MainViewModel.cs`, with the pages that
2//! other crates add to the developer tools (an addition of the port, see
3//! [`DevToolsPage`]).
4
5use super::{
6    CombinedTreeNode, EventsPageViewModel, HotKeyPageViewModel, LogicalTreeNode, TreePageViewModel, ViewModelBase, VisualTreeNode,
7};
8use crate::diagnostics::controls::{Application, ControlHighlightAdorner};
9use crate::diagnostics::{DevTools, DevToolsOptions, DevToolsViewKind, IScreenshotHandler};
10use ferroui_base::data::model::{Event, INotifyPropertyChanged};
11use ferroui_base::input::raw::RawPointerEventArgs;
12use ferroui_base::input::{IInputRoot, InputElement, InputManager, KeyboardDevice};
13use ferroui_base::media::IBrush;
14use ferroui_base::metadata::{from_markup_value, into_markup_value};
15use ferroui_base::reactive::{Disposable, IDisposable, ObservableExt};
16use ferroui_base::rendering::{IRenderer, RendererDebugOverlays};
17use ferroui_base::threading::{Dispatcher, DispatcherPriority, DispatcherTimer};
18use ferroui_base::{ferro_markup_type, BoxedValue, FerroObject, Ref};
19use ferroui_controls::{Control, TopLevel};
20use std::cell::{Cell, OnceCell, RefCell};
21use std::collections::HashSet;
22use std::rc::{Rc, Weak};
23use std::time::Duration;
24
25/// Addition of the port (not in the original): a page that another crate
26/// adds to the developer tools, shown as a tab after the tabs of the
27/// original (the logical tree, the visual tree, the events and the hidden
28/// hot keys tab).
29///
30/// A page is a title and a factory of its view. The factory is called with
31/// the view model of the developer tools window the first time the tab of
32/// the page is selected in that window; the view is kept for the lifetime of
33/// the window. [`MainViewModel::root`] is the object the window inspects.
34///
35/// ```ignore
36/// let registration = MainViewModel::register_page(DevToolsPage::new("Components", |main| {
37///     ComponentsPageView::new(main.root()).upcast()
38/// }));
39/// // ...
40/// registration.dispose(); // removes the page from the windows opened afterwards
41/// ```
42///
43/// Pages are registered per thread (the UI thread), as everything the
44/// developer tools show. A registration applies to the developer tools
45/// windows opened after it.
46#[derive(Clone)]
47pub struct DevToolsPage {
48    title: String,
49    create_view: Rc<dyn Fn(&Rc<MainViewModel>) -> Ref<Control>>,
50}
51
52impl DevToolsPage {
53    /// A page with the title of its tab and the factory of its view.
54    pub fn new(title: impl Into<String>, create_view: impl Fn(&Rc<MainViewModel>) -> Ref<Control> + 'static) -> Self {
55        Self { title: title.into(), create_view: Rc::new(create_view) }
56    }
57
58    /// The title of the tab of the page.
59    pub fn title(&self) -> &str {
60        &self.title
61    }
62
63    /// Creates the view of the page for the view model of a developer tools
64    /// window.
65    pub fn create_view(&self, main_view_model: &Rc<MainViewModel>) -> Ref<Control> {
66        (self.create_view)(main_view_model)
67    }
68}
69
70ferroui_base::ferro_thread_local! {
71    /// The pages registered on this thread, with the identifiers of their registrations.
72    static REGISTERED_PAGES: RefCell<Vec<(u64, DevToolsPage)>> = const { RefCell::new(Vec::new()) };
73    /// The identifier of the next registration.
74    static NEXT_PAGE_ID: Cell<u64> = const { Cell::new(0) };
75}
76
77/// A page of a window: the registered page and its view, once created.
78struct ExtensionPage {
79    page: DevToolsPage,
80    view: RefCell<Option<Ref<Control>>>,
81}
82
83/// The view model that the main view shows as the content of the selected
84/// tab (`ViewModelBase? Content` of the original), or the view of an
85/// extension page.
86#[derive(Clone)]
87enum MainContent {
88    Tree(Rc<TreePageViewModel>),
89    Events(Rc<EventsPageViewModel>),
90    HotKeys(Rc<HotKeyPageViewModel>),
91    /// Addition of the port: the view of the extension page at an index.
92    Page(usize, Ref<Control>),
93}
94
95impl PartialEq for MainContent {
96    /// Reference equality of the view models, as the original compares them.
97    fn eq(&self, other: &Self) -> bool {
98        match (self, other) {
99            (MainContent::Tree(x), MainContent::Tree(y)) => Rc::ptr_eq(x, y),
100            (MainContent::Events(x), MainContent::Events(y)) => Rc::ptr_eq(x, y),
101            (MainContent::HotKeys(x), MainContent::HotKeys(y)) => Rc::ptr_eq(x, y),
102            (MainContent::Page(x, _), MainContent::Page(y, _)) => x == y,
103            _ => false,
104        }
105    }
106}
107
108impl MainContent {
109    /// The content as an untyped value: a view model as markup holds it
110    /// (`into_markup_value`), the view of a page as a control.
111    fn boxed(&self) -> Option<BoxedValue> {
112        match self {
113            MainContent::Tree(tree) => into_markup_value(tree.clone()),
114            MainContent::Events(events) => into_markup_value(events.clone()),
115            MainContent::HotKeys(hot_keys) => into_markup_value(hot_keys.clone()),
116            MainContent::Page(_, view) => Some(Control::boxed(view.clone())),
117        }
118    }
119}
120
121/// `a == b` of two input roots that may be null: reference equality.
122fn same_root(a: &Option<Rc<dyn IInputRoot>>, b: &Option<Rc<dyn IInputRoot>>) -> bool {
123    match (a, b) {
124        (Some(a), Some(b)) => std::ptr::addr_eq(Rc::as_ptr(a), Rc::as_ptr(b)),
125        (None, None) => true,
126        _ => false,
127    }
128}
129
130/// The view model of the developer tools window: the pages (the logical
131/// and visual trees, the events and the hot keys), the selected tab, the
132/// focused element and the element under the pointer, the overlays of the
133/// renderer, the screenshot of the selected control and the options.
134pub struct MainViewModel {
135    base: ViewModelBase,
136    this: Weak<MainViewModel>,
137    root: Ref<FerroObject>,
138    combined_tree: OnceCell<Rc<TreePageViewModel>>,
139    logical_tree: OnceCell<Rc<TreePageViewModel>>,
140    visual_tree: OnceCell<Rc<TreePageViewModel>>,
141    events: Rc<EventsPageViewModel>,
142    hot_keys: Rc<HotKeyPageViewModel>,
143    pointer_over_subscription: RefCell<Option<Rc<dyn IDisposable>>>,
144    keyboard_property_changed_subscription: RefCell<Option<Rc<dyn IDisposable>>>,
145    content: RefCell<Option<MainContent>>,
146    selected_tab: Cell<i32>,
147    focused_control: RefCell<Option<String>>,
148    pointer_over_element: RefCell<Option<Ref<InputElement>>>,
149    should_visualize_margin_padding: Cell<bool>,
150    freeze_popups: Cell<bool>,
151    /// Addition of the port: whether the left pane of the pages (the tree, the routed events) is
152    /// shown.
153    is_tree_pane_open: Cell<bool>,
154    /// Addition of the port: whether the side pane of the details of a control (the layout, the
155    /// value frames, the pseudo-classes) is shown.
156    is_side_pane_open: Cell<bool>,
157    pointer_over_element_name: RefCell<Option<String>>,
158    pointer_over_root: RefCell<Option<Rc<dyn IInputRoot>>>,
159    screenshot_handler: RefCell<Option<Rc<dyn IScreenshotHandler>>>,
160    show_property_type: Cell<bool>,
161    show_implemented_interfaces: Cell<bool>,
162    pinned_properties: Rc<RefCell<HashSet<String>>>,
163    focus_highlighter: RefCell<Option<Rc<dyn IBrush>>>,
164    current_focus_highlight_adorner: RefCell<Option<Rc<dyn IDisposable>>>,
165    startup_screen_index: Cell<Option<i32>>,
166    /// Addition of the port: the pages registered when the window was
167    /// created.
168    extension_pages: Vec<ExtensionPage>,
169}
170
171/// Reference equality, as the class of the original.
172impl PartialEq for MainViewModel {
173    fn eq(&self, other: &Self) -> bool {
174        std::ptr::eq(self, other)
175    }
176}
177
178impl INotifyPropertyChanged for MainViewModel {
179    fn property_changed(&self) -> &Event<str> {
180        self.base.property_changed()
181    }
182}
183
184impl MainViewModel {
185    /// Addition of the port: the tab of the combined tree, the first one.
186    pub const COMBINED_TREE_TAB: i32 = 0;
187    /// The tab of the logical tree (upstream's first).
188    pub const LOGICAL_TREE_TAB: i32 = 1;
189    /// The tab of the visual tree.
190    pub const VISUAL_TREE_TAB: i32 = 2;
191    /// The tab of the routed events.
192    pub const EVENTS_TAB: i32 = 3;
193    /// The tab of the hot keys (hidden; the View menu selects it).
194    pub const HOT_KEYS_TAB: i32 = 4;
195    /// Addition of the port: the index of the tab of the first extension
196    /// page ([`DevToolsPage`]), after the five tabs of the tools.
197    pub const FIRST_EXTENSION_TAB: i32 = 5;
198
199    /// C# `new MainViewModel(FerroObject root)`: the developer tools of
200    /// `root`, a top level or the application the tools are attached to.
201    pub fn new(root: Ref<FerroObject>) -> Rc<Self> {
202        let extension_pages = Self::registered_pages()
203            .into_iter()
204            .map(|page| ExtensionPage { page, view: RefCell::new(None) })
205            .collect();
206
207        let this = Rc::new_cyclic(|this: &Weak<MainViewModel>| Self {
208            base: ViewModelBase::new(),
209            this: this.clone(),
210            root: root.clone(),
211            combined_tree: OnceCell::new(),
212            logical_tree: OnceCell::new(),
213            visual_tree: OnceCell::new(),
214            events: EventsPageViewModel::new(this.clone()),
215            hot_keys: HotKeyPageViewModel::new(),
216            pointer_over_subscription: RefCell::new(None),
217            keyboard_property_changed_subscription: RefCell::new(None),
218            content: RefCell::new(None),
219            selected_tab: Cell::new(0),
220            focused_control: RefCell::new(None),
221            pointer_over_element: RefCell::new(None),
222            should_visualize_margin_padding: Cell::new(true),
223            freeze_popups: Cell::new(false),
224            is_tree_pane_open: Cell::new(true),
225            is_side_pane_open: Cell::new(true),
226            pointer_over_element_name: RefCell::new(None),
227            pointer_over_root: RefCell::new(None),
228            screenshot_handler: RefCell::new(None),
229            show_property_type: Cell::new(false),
230            show_implemented_interfaces: Cell::new(false),
231            pinned_properties: Rc::new(RefCell::new(HashSet::new())),
232            focus_highlighter: RefCell::new(None),
233            current_focus_highlight_adorner: RefCell::new(None),
234            startup_screen_index: Cell::new(None),
235            extension_pages,
236        });
237
238        let combined_tree =
239            TreePageViewModel::new(&this, CombinedTreeNode::create(root.clone()), this.pinned_properties.clone());
240        let _ = this.combined_tree.set(combined_tree);
241        let logical_tree =
242            TreePageViewModel::new(&this, LogicalTreeNode::create(root.clone()), this.pinned_properties.clone());
243        let _ = this.logical_tree.set(logical_tree);
244        let visual_tree =
245            TreePageViewModel::new(&this, VisualTreeNode::create(root.clone()), this.pinned_properties.clone());
246        let _ = this.visual_tree.set(visual_tree);
247
248        this.update_focused_control();
249
250        if let Some(keyboard_device) = KeyboardDevice::instance() {
251            let weak = Rc::downgrade(&this);
252            let subscription = keyboard_device.property_changed(move |property_name| {
253                if let Some(this) = weak.upgrade() {
254                    this.keyboard_property_changed(property_name);
255                }
256            });
257            *this.keyboard_property_changed_subscription.borrow_mut() = Some(subscription);
258        }
259        this.set_selected_tab(0);
260
261        let pointer_over_subscription = if let Some(top_level) = root.cast::<TopLevel>() {
262            let input_root = top_level.input_root();
263            *this.pointer_over_root.borrow_mut() = Some(input_root.clone());
264            // Deviation (DEVIATIONS.md, Developer tools): TopLevel.PointerOverElementProperty (the
265            // observable property of the element under the pointer) does not exist in the port; the
266            // element under the pointer is the `pointer_over_element` of the input root of the top
267            // level, which the input pipeline updates without a notification. It is read now and
268            // after every pointer event of the root has been processed.
269            this.set_pointer_over_element(input_root.pointer_over_element());
270            match InputManager::instance() {
271                Some(input_manager) => {
272                    let weak = Rc::downgrade(&this);
273                    input_manager.post_process().subscribe_fn(move |e| {
274                        if e.downcast_ref::<RawPointerEventArgs>().is_some()
275                            && std::ptr::addr_eq(Rc::as_ptr(e.root()), Rc::as_ptr(&input_root))
276                        {
277                            if let Some(this) = weak.upgrade() {
278                                this.set_pointer_over_element(input_root.pointer_over_element());
279                            }
280                        }
281                    })
282                }
283                None => Disposable::empty(),
284            }
285        } else {
286            // `InputManager.Instance!` upstream: without an input manager there is no input to
287            // observe.
288            match InputManager::instance() {
289                Some(input_manager) => {
290                    let weak = Rc::downgrade(&this);
291                    input_manager.pre_process().subscribe_fn(move |e| {
292                        if let Some(pointer_event_args) = e.downcast_ref::<RawPointerEventArgs>() {
293                            if let Some(this) = weak.upgrade() {
294                                let root = pointer_event_args.root().clone();
295                                let element = root
296                                    .try_root_element()
297                                    .and_then(|root_element| root_element.input_hit_test(pointer_event_args.position()));
298                                this.set_pointer_over_root(Some(root));
299                                this.set_pointer_over_element(element);
300                            }
301                        }
302                    })
303                }
304                None => Disposable::empty(),
305            }
306        };
307        *this.pointer_over_subscription.borrow_mut() = Some(pointer_over_subscription);
308
309        this
310    }
311
312    /// Addition of the port: the object the developer tools inspect (the
313    /// root the view model was created with).
314    pub fn root(&self) -> Ref<FerroObject> {
315        self.root.clone()
316    }
317
318    /// The page of the logical tree.
319    /// The page of the combined tree (an addition of the port).
320    pub fn combined_tree(&self) -> Rc<TreePageViewModel> {
321        self.combined_tree.get().expect("the trees are created with the view model").clone()
322    }
323
324    pub fn logical_tree(&self) -> Rc<TreePageViewModel> {
325        self.logical_tree.get().expect("the trees are created with the view model").clone()
326    }
327
328    /// The page of the visual tree.
329    pub fn visual_tree(&self) -> Rc<TreePageViewModel> {
330        self.visual_tree.get().expect("the trees are created with the view model").clone()
331    }
332
333    /// The events page.
334    pub fn events(&self) -> Rc<EventsPageViewModel> {
335        self.events.clone()
336    }
337
338    /// The hot keys page.
339    pub fn hot_keys(&self) -> Rc<HotKeyPageViewModel> {
340        self.hot_keys.clone()
341    }
342
343    pub fn freeze_popups(&self) -> bool {
344        self.freeze_popups.get()
345    }
346
347    pub fn set_freeze_popups(&self, value: bool) {
348        self.base.raise_and_set_if_changed_cell(&self.freeze_popups, value, "FreezePopups");
349    }
350
351    /// Addition of the port: whether the left pane of the pages (the tree of a tree page, the
352    /// routed events of the events page) is shown; the button at the left of the header of the
353    /// tools toggles it.
354    pub fn is_tree_pane_open(&self) -> bool {
355        self.is_tree_pane_open.get()
356    }
357
358    pub fn set_is_tree_pane_open(&self, value: bool) {
359        self.base.raise_and_set_if_changed_cell(&self.is_tree_pane_open, value, "IsTreePaneOpen");
360    }
361
362    /// Shows or hides the left pane of the pages.
363    pub fn toggle_tree_pane(&self) {
364        self.set_is_tree_pane_open(!self.is_tree_pane_open());
365    }
366
367    /// Addition of the port: whether the side pane of the details of a control (the layout, the
368    /// value frames, the pseudo-classes, at the right of the properties) is shown; a toggle of the
369    /// command bar of the header shows or hides it.
370    pub fn is_side_pane_open(&self) -> bool {
371        self.is_side_pane_open.get()
372    }
373
374    pub fn set_is_side_pane_open(&self, value: bool) {
375        self.base.raise_and_set_if_changed_cell(&self.is_side_pane_open, value, "IsSidePaneOpen");
376    }
377
378    /// Shows or hides the side pane of the details of a control.
379    pub fn toggle_side_pane(&self) {
380        self.set_is_side_pane_open(!self.is_side_pane_open());
381    }
382
383    pub fn should_visualize_margin_padding(&self) -> bool {
384        self.should_visualize_margin_padding.get()
385    }
386
387    pub fn set_should_visualize_margin_padding(&self, value: bool) {
388        self.base.raise_and_set_if_changed_cell(
389            &self.should_visualize_margin_padding,
390            value,
391            "ShouldVisualizeMarginPadding",
392        );
393    }
394
395    pub fn toggle_visualize_margin_padding(&self) {
396        self.set_should_visualize_margin_padding(!self.should_visualize_margin_padding());
397    }
398
399    /// The renderer of the root: the renderer of a top level, the renderer
400    /// of the main window or view of an application.
401    fn try_get_renderer(&self) -> Option<Rc<dyn IRenderer>> {
402        if let Some(top_level) = self.root.cast::<TopLevel>() {
403            let renderer: Rc<dyn IRenderer> = top_level.renderer();
404            return Some(renderer);
405        }
406        if let Some(app) = self.root.cast::<Application>() {
407            return app.renderer_root().map(|renderer| renderer as Rc<dyn IRenderer>);
408        }
409        None
410    }
411
412    fn get_debug_overlay(&self, overlay: RendererDebugOverlays) -> bool {
413        self.try_get_renderer()
414            .map_or(RendererDebugOverlays::NONE, |renderer| renderer.diagnostics().debug_overlays())
415            .intersects(overlay)
416    }
417
418    fn set_debug_overlay(&self, overlay: RendererDebugOverlays, enable: bool, property_name: &str) {
419        let Some(renderer) = self.try_get_renderer() else {
420            return;
421        };
422
423        let diagnostics = renderer.diagnostics();
424        let old_value = diagnostics.debug_overlays();
425        let new_value = if enable { old_value | overlay } else { old_value & !overlay };
426
427        if old_value == new_value {
428            return;
429        }
430
431        diagnostics.set_debug_overlays(new_value);
432        self.base.raise_property_changed(property_name);
433    }
434
435    pub fn show_dirty_rects_overlay(&self) -> bool {
436        self.get_debug_overlay(RendererDebugOverlays::DIRTY_RECTS)
437    }
438
439    pub fn set_show_dirty_rects_overlay(&self, value: bool) {
440        self.set_debug_overlay(RendererDebugOverlays::DIRTY_RECTS, value, "ShowDirtyRectsOverlay");
441    }
442
443    pub fn toggle_dirty_rects_overlay(&self) {
444        self.set_show_dirty_rects_overlay(!self.show_dirty_rects_overlay());
445    }
446
447    pub fn show_fps_overlay(&self) -> bool {
448        self.get_debug_overlay(RendererDebugOverlays::FPS)
449    }
450
451    pub fn set_show_fps_overlay(&self, value: bool) {
452        self.set_debug_overlay(RendererDebugOverlays::FPS, value, "ShowFpsOverlay");
453    }
454
455    pub fn toggle_fps_overlay(&self) {
456        self.set_show_fps_overlay(!self.show_fps_overlay());
457    }
458
459    pub fn show_layout_time_graph_overlay(&self) -> bool {
460        self.get_debug_overlay(RendererDebugOverlays::LAYOUT_TIME_GRAPH)
461    }
462
463    pub fn set_show_layout_time_graph_overlay(&self, value: bool) {
464        self.set_debug_overlay(RendererDebugOverlays::LAYOUT_TIME_GRAPH, value, "ShowLayoutTimeGraphOverlay");
465    }
466
467    pub fn toggle_layout_time_graph_overlay(&self) {
468        self.set_show_layout_time_graph_overlay(!self.show_layout_time_graph_overlay());
469    }
470
471    pub fn show_render_time_graph_overlay(&self) -> bool {
472        self.get_debug_overlay(RendererDebugOverlays::RENDER_TIME_GRAPH)
473    }
474
475    pub fn set_show_render_time_graph_overlay(&self, value: bool) {
476        self.set_debug_overlay(RendererDebugOverlays::RENDER_TIME_GRAPH, value, "ShowRenderTimeGraphOverlay");
477    }
478
479    pub fn toggle_render_time_graph_overlay(&self) {
480        self.set_show_render_time_graph_overlay(!self.show_render_time_graph_overlay());
481    }
482
483    /// The content of the selected tab: the view model of the page, or the
484    /// view of an extension page.
485    pub fn content(&self) -> Option<BoxedValue> {
486        self.content.borrow().as_ref().and_then(MainContent::boxed)
487    }
488
489    /// The tree page that is the content, if the content is a tree page
490    /// (C# `Content as TreePageViewModel`).
491    fn content_tree(&self) -> Option<Rc<TreePageViewModel>> {
492        match &*self.content.borrow() {
493            Some(MainContent::Tree(tree)) => Some(tree.clone()),
494            _ => None,
495        }
496    }
497
498    fn set_content(&self, value: MainContent) {
499        if let (Some(old_tree), MainContent::Tree(new_tree)) = (self.content_tree(), &value) {
500            if let Some(control) = old_tree.selected_node().and_then(|node| node.visual().cast::<Control>()) {
501                // HACK: We want to select the currently selected control in the new tree, but
502                // to select nested nodes in TreeView, currently the TreeView has to be able to
503                // expand the parent nodes. Because at this point the TreeView isn't visible,
504                // this will fail unless we schedule the selection to run after layout.
505                let new_tree = new_tree.clone();
506                let _ = DispatcherTimer::run_once(
507                    move || {
508                        new_tree.select_control(&control);
509                    },
510                    Duration::ZERO,
511                    DispatcherPriority::default(),
512                );
513            }
514        }
515
516        self.base.raise_and_set_if_changed(&self.content, Some(value), "Content");
517    }
518
519    /// The index of the selected tab: 0 the combined tree (an addition of the
520    /// port), 1 the logical tree, 2 the visual tree, 3 the events, 4 the hot
521    /// keys (upstream: 0 to 3, from the logical tree); from
522    /// [`FIRST_EXTENSION_TAB`](Self::FIRST_EXTENSION_TAB) on, the extension
523    /// pages (an addition of the port). Any other index selects the combined
524    /// tree.
525    pub fn selected_tab(&self) -> i32 {
526        self.selected_tab.get()
527    }
528
529    pub fn set_selected_tab(&self, value: i32) {
530        self.selected_tab.set(value);
531
532        match value {
533            Self::LOGICAL_TREE_TAB => self.set_content(MainContent::Tree(self.logical_tree())),
534            Self::VISUAL_TREE_TAB => self.set_content(MainContent::Tree(self.visual_tree())),
535            Self::EVENTS_TAB => self.set_content(MainContent::Events(self.events.clone())),
536            Self::HOT_KEYS_TAB => self.set_content(MainContent::HotKeys(self.hot_keys.clone())),
537            _ => match self.extension_page_view(value) {
538                Some((index, view)) => self.set_content(MainContent::Page(index, view)),
539                None => self.set_content(MainContent::Tree(self.combined_tree())),
540            },
541        }
542
543        self.base.raise_property_changed("SelectedTab");
544    }
545
546    pub fn focused_control(&self) -> Option<String> {
547        self.focused_control.borrow().clone()
548    }
549
550    fn set_focused_control(&self, value: Option<String>) {
551        self.base.raise_and_set_if_changed(&self.focused_control, value, "FocusedControl");
552    }
553
554    /// The input root the pointer is over.
555    pub fn pointer_over_root(&self) -> Option<Rc<dyn IInputRoot>> {
556        self.pointer_over_root.borrow().clone()
557    }
558
559    fn set_pointer_over_root(&self, value: Option<Rc<dyn IInputRoot>>) {
560        let changed = !same_root(&self.pointer_over_root.borrow(), &value);
561        if changed {
562            *self.pointer_over_root.borrow_mut() = value;
563            self.base.raise_property_changed("PointerOverRoot");
564        }
565    }
566
567    /// The element under the pointer.
568    pub fn pointer_over_element(&self) -> Option<Ref<InputElement>> {
569        self.pointer_over_element.borrow().clone()
570    }
571
572    fn set_pointer_over_element(&self, value: Option<Ref<InputElement>>) {
573        let name = value.as_ref().map(|element| element.get_type().name().to_string());
574        self.base.raise_and_set_if_changed(&self.pointer_over_element, value, "PointerOverElement");
575        self.set_pointer_over_element_name(name);
576    }
577
578    /// The name of the type of the element under the pointer.
579    pub fn pointer_over_element_name(&self) -> Option<String> {
580        self.pointer_over_element_name.borrow().clone()
581    }
582
583    fn set_pointer_over_element_name(&self, value: Option<String>) {
584        self.base.raise_and_set_if_changed(&self.pointer_over_element_name, value, "PointerOverElementName");
585    }
586
587    /// Selects the hot keys tab.
588    pub fn show_hot_keys(&self) {
589        self.set_selected_tab(Self::HOT_KEYS_TAB);
590    }
591
592    /// Selects `control` in the tree page that is the content.
593    pub fn select_control(&self, control: &Ref<Control>) {
594        let tree = self.content_tree();
595
596        if let Some(tree) = tree {
597            tree.select_control(control);
598        }
599    }
600
601    /// Freezes (`true`) or resumes the value frames of the control details
602    /// of the tree page that is the content.
603    pub fn enable_snapshot_styles(&self, enable: bool) {
604        if let Some(tree_vm) = self.content_tree() {
605            if let Some(details) = tree_vm.details() {
606                details.set_snapshot_frames(enable);
607            }
608        }
609    }
610
611    /// C# `Dispose()`: ends the subscriptions, disposes the trees and the
612    /// highlight of the focused element, and turns the overlays of the
613    /// renderer off.
614    pub fn dispose(&self) {
615        if let Some(subscription) = self.keyboard_property_changed_subscription.borrow_mut().take() {
616            subscription.dispose();
617        }
618        let pointer_over_subscription = self.pointer_over_subscription.borrow().clone();
619        if let Some(pointer_over_subscription) = pointer_over_subscription {
620            pointer_over_subscription.dispose();
621        }
622        self.combined_tree().dispose();
623        self.logical_tree().dispose();
624        self.visual_tree().dispose();
625        let current_focus_highlight_adorner = self.current_focus_highlight_adorner.borrow().clone();
626        if let Some(current_focus_highlight_adorner) = current_focus_highlight_adorner {
627            current_focus_highlight_adorner.dispose();
628        }
629        if let Some(renderer) = self.try_get_renderer() {
630            renderer.diagnostics().set_debug_overlays(RendererDebugOverlays::NONE);
631        }
632    }
633
634    fn update_focused_control(&self) {
635        let element = KeyboardDevice::instance().and_then(|keyboard_device| keyboard_device.focused_element());
636        self.set_focused_control(element.as_ref().map(|element| element.get_type().name().to_string()));
637        let current_focus_highlight_adorner = self.current_focus_highlight_adorner.borrow_mut().take();
638        if let Some(current_focus_highlight_adorner) = current_focus_highlight_adorner {
639            current_focus_highlight_adorner.dispose();
640        }
641        if let (Some(brush), Some(input)) = (self.focus_highlighter(), element) {
642            if !DevTools::does_belong_to_dev_tool(&input) {
643                let adorner = ControlHighlightAdorner::add(&input, brush);
644                *self.current_focus_highlight_adorner.borrow_mut() = adorner;
645            }
646        }
647    }
648
649    fn keyboard_property_changed(&self, property_name: &str) {
650        if property_name == "FocusedElement" {
651            self.update_focused_control();
652        }
653    }
654
655    /// Selects `control` in the logical or the visual tree page, and that
656    /// page, if the tree has a node of it.
657    pub fn request_tree_navigate_to(&self, control: &Ref<Control>, is_visual_tree: bool) {
658        let tree = if is_visual_tree { self.visual_tree() } else { self.logical_tree() };
659
660        let node = tree.find_node(control);
661
662        if node.is_some() {
663            self.set_selected_tab(if is_visual_tree { Self::VISUAL_TREE_TAB } else { Self::LOGICAL_TREE_TAB });
664
665            tree.select_control(control);
666        }
667    }
668
669    /// The index of the screen the window opens maximized on.
670    pub fn startup_screen_index(&self) -> Option<i32> {
671        self.startup_screen_index.get()
672    }
673
674    /// Whether a screenshot of the selected control can be taken: the
675    /// content is a tree page whose selected node is a visual in a visual
676    /// tree.
677    pub fn can_shot(&self, _parameter: Option<BoxedValue>) -> bool {
678        self.content_tree()
679            .and_then(|tree| tree.selected_node())
680            .and_then(|selected_node| selected_node.visual().cast::<ferroui_base::Visual>())
681            .is_some_and(|visual| visual.visual_root().is_some())
682    }
683
684    /// Takes a screenshot of the control selected in the tree page that is
685    /// the content, with the screenshot handler of the options.
686    pub fn shot(&self, _parameter: Option<BoxedValue>) {
687        let control = self
688            .content_tree()
689            .and_then(|tree| tree.selected_node())
690            .and_then(|selected_node| selected_node.visual().cast::<Control>());
691        let screenshot_handler = self.screenshot_handler.borrow().clone();
692        if let (Some(control), Some(screenshot_handler)) = (control, screenshot_handler) {
693            // `async void`: the screenshot is taken on the dispatcher; a failure is written to
694            // the debug output.
695            let take = screenshot_handler.take(&control);
696            let _ = Dispatcher::ui_thread().invoke_async_task_local(move || async move {
697                if let Err(ex) = take.await {
698                    if cfg!(debug_assertions) {
699                        // `Debug.WriteLine` of upstream: written in debug builds only.
700                        eprintln!("{ex}");
701                    }
702                    //TODO: Notify error
703                }
704            });
705        }
706    }
707
708    /// Applies the options the developer tools were opened with.
709    pub fn set_options(&self, options: &DevToolsOptions) {
710        *self.screenshot_handler.borrow_mut() = Some(options.screenshot_handler());
711        self.startup_screen_index.set(options.startup_screen_index());
712        self.set_show_implemented_interfaces(options.show_implemented_interfaces());
713        self.set_focus_highlighter(options.focus_highlighter_brush());
714        self.set_selected_tab(match options.launch_view() {
715            DevToolsViewKind::CombinedTree => Self::COMBINED_TREE_TAB,
716            DevToolsViewKind::LogicalTree => Self::LOGICAL_TREE_TAB,
717            DevToolsViewKind::VisualTree => Self::VISUAL_TREE_TAB,
718            DevToolsViewKind::Events => Self::EVENTS_TAB,
719        });
720
721        self.hot_keys.set_options(options);
722    }
723
724    pub fn show_implemented_interfaces(&self) -> bool {
725        self.show_implemented_interfaces.get()
726    }
727
728    fn set_show_implemented_interfaces(&self, value: bool) {
729        self.base.raise_and_set_if_changed_cell(&self.show_implemented_interfaces, value, "ShowImplementedInterfaces");
730    }
731
732    pub fn toggle_show_implemented_interfaces(&self, _parameter: Option<BoxedValue>) {
733        self.set_show_implemented_interfaces(!self.show_implemented_interfaces());
734        if let Some(view_model) = self.content_tree() {
735            view_model.update_properties_view();
736        }
737    }
738
739    pub fn show_details_property_type(&self) -> bool {
740        self.show_property_type.get()
741    }
742
743    fn set_show_details_property_type(&self, value: bool) {
744        self.base.raise_and_set_if_changed_cell(&self.show_property_type, value, "ShowDetailsPropertyType");
745    }
746
747    pub fn toggle_show_details_property_type(&self, _parameter: Option<BoxedValue>) {
748        self.set_show_details_property_type(!self.show_details_property_type());
749    }
750
751    /// The brush that highlights the focused element; `None` for no
752    /// highlight.
753    pub fn focus_highlighter(&self) -> Option<Rc<dyn IBrush>> {
754        self.focus_highlighter.borrow().clone()
755    }
756
757    fn set_focus_highlighter(&self, value: Option<Rc<dyn IBrush>>) {
758        self.base.raise_and_set_if_changed(&self.focus_highlighter, value, "FocusHighlighter");
759    }
760
761    /// Selects the brush that highlights the focused element (C#
762    /// `parameter as IBrush`).
763    pub fn select_focus_highlighter(&self, parameter: Option<BoxedValue>) {
764        self.set_focus_highlighter(from_markup_value::<Rc<dyn IBrush>>(&parameter));
765    }
766
767    /// The pinned properties of the control details, shared by the tree
768    /// pages.
769    pub fn pinned_properties(&self) -> Rc<RefCell<HashSet<String>>> {
770        self.pinned_properties.clone()
771    }
772
773    // --- Addition of the port: the extension pages ------------------------------------------
774    // Deviation (DEVIATIONS.md, Developer tools): upstream has no pages besides its own.
775
776    /// Registers a page for the developer tools windows opened afterwards on
777    /// this thread. Disposing the returned handle removes the registration.
778    pub fn register_page(page: DevToolsPage) -> Rc<dyn IDisposable> {
779        let id = NEXT_PAGE_ID.with(|next| {
780            let id = next.get();
781            next.set(id + 1);
782            id
783        });
784        REGISTERED_PAGES.with(|pages| pages.borrow_mut().push((id, page)));
785        Disposable::create(move || {
786            REGISTERED_PAGES.with(|pages| pages.borrow_mut().retain(|(page_id, _)| *page_id != id));
787        })
788    }
789
790    /// The pages registered on this thread, in the order of their
791    /// registrations.
792    pub fn registered_pages() -> Vec<DevToolsPage> {
793        REGISTERED_PAGES.with(|pages| pages.borrow().iter().map(|(_, page)| page.clone()).collect())
794    }
795
796    /// The extension pages of this window, in the order of their tabs: the
797    /// page at index `i` has the tab `FIRST_EXTENSION_TAB + i`.
798    pub fn extension_pages(&self) -> Vec<DevToolsPage> {
799        self.extension_pages.iter().map(|page| page.page.clone()).collect()
800    }
801
802    /// The view of the extension page of a tab, created on first use, with
803    /// the index of the page; `None` when no extension page has the tab.
804    fn extension_page_view(&self, tab: i32) -> Option<(usize, Ref<Control>)> {
805        let index = usize::try_from(tab.checked_sub(Self::FIRST_EXTENSION_TAB)?).ok()?;
806        let page = self.extension_pages.get(index)?;
807        let existing = page.view.borrow().clone();
808        let view = match existing {
809            Some(view) => view,
810            None => {
811                let this = self.this.upgrade()?;
812                let view = page.page.create_view(&this);
813                *page.view.borrow_mut() = Some(view.clone());
814                view
815            }
816        };
817        Some((index, view))
818    }
819}
820
821ferro_markup_type!(class MainViewModel {
822    this: Rc<MainViewModel>,
823    handles: [MainViewModel, Rc<MainViewModel>, Option<Rc<MainViewModel>>],
824    base: ViewModelBase,
825    properties: [
826        FreezePopups: bool { get: MainViewModel::freeze_popups, set: MainViewModel::set_freeze_popups },
827        IsTreePaneOpen: bool { get: MainViewModel::is_tree_pane_open, set: MainViewModel::set_is_tree_pane_open },
828        IsSidePaneOpen: bool { get: MainViewModel::is_side_pane_open, set: MainViewModel::set_is_side_pane_open },
829        ShouldVisualizeMarginPadding: bool {
830            get: MainViewModel::should_visualize_margin_padding,
831            set: MainViewModel::set_should_visualize_margin_padding
832        },
833        ShowDirtyRectsOverlay: bool {
834            get: MainViewModel::show_dirty_rects_overlay,
835            set: MainViewModel::set_show_dirty_rects_overlay
836        },
837        ShowFpsOverlay: bool { get: MainViewModel::show_fps_overlay, set: MainViewModel::set_show_fps_overlay },
838        ShowLayoutTimeGraphOverlay: bool {
839            get: MainViewModel::show_layout_time_graph_overlay,
840            set: MainViewModel::set_show_layout_time_graph_overlay
841        },
842        ShowRenderTimeGraphOverlay: bool {
843            get: MainViewModel::show_render_time_graph_overlay,
844            set: MainViewModel::set_show_render_time_graph_overlay
845        },
846        Content: Option<BoxedValue> { get: MainViewModel::content },
847        SelectedTab: i32 { get: MainViewModel::selected_tab, set: MainViewModel::set_selected_tab },
848        FocusedControl: Option<String> { get: MainViewModel::focused_control },
849        PointerOverElement: Option<Ref<InputElement>> { get: MainViewModel::pointer_over_element },
850        PointerOverElementName: Option<String> { get: MainViewModel::pointer_over_element_name },
851        StartupScreenIndex: Option<i32> { get: MainViewModel::startup_screen_index },
852        ShowImplementedInterfaces: bool { get: MainViewModel::show_implemented_interfaces },
853        ShowDetailsPropertyType: bool { get: MainViewModel::show_details_property_type },
854        FocusHighlighter: Option<Rc<dyn IBrush>> { get: MainViewModel::focus_highlighter },
855    ],
856    methods: [
857        fn ToggleVisualizeMarginPadding() => MainViewModel::toggle_visualize_margin_padding,
858        fn ToggleDirtyRectsOverlay() => MainViewModel::toggle_dirty_rects_overlay,
859        fn ToggleFpsOverlay() => MainViewModel::toggle_fps_overlay,
860        fn ToggleLayoutTimeGraphOverlay() => MainViewModel::toggle_layout_time_graph_overlay,
861        fn ToggleRenderTimeGraphOverlay() => MainViewModel::toggle_render_time_graph_overlay,
862        fn ShowHotKeys() => MainViewModel::show_hot_keys,
863        fn ToggleTreePane() => MainViewModel::toggle_tree_pane,
864        fn ToggleSidePane() => MainViewModel::toggle_side_pane,
865        fn SelectControl(Ref<Control>) =>
866            (|this: &Rc<MainViewModel>, control: Ref<Control>| this.select_control(&control)),
867        fn EnableSnapshotStyles(bool) => MainViewModel::enable_snapshot_styles,
868        fn RequestTreeNavigateTo(Ref<Control>, bool) =>
869            (|this: &Rc<MainViewModel>, control: Ref<Control>, is_visual_tree: bool| {
870                this.request_tree_navigate_to(&control, is_visual_tree)
871            }),
872        fn CanShot(Option<BoxedValue>) -> bool => MainViewModel::can_shot [DependsOn("SelectedNode"), DependsOn("Content")],
873        fn Shot(Option<BoxedValue>) => MainViewModel::shot,
874        fn ToggleShowImplementedInterfaces(Option<BoxedValue>) => MainViewModel::toggle_show_implemented_interfaces,
875        fn ToggleShowDetailsPropertyType(Option<BoxedValue>) => MainViewModel::toggle_show_details_property_type,
876        fn SelectFocusHighlighter(Option<BoxedValue>) => MainViewModel::select_focus_highlighter,
877    ],
878    notify_property_changed: MainViewModel,
879});
880
881/// What the untyped value conversions must know about the view models of
882/// the main window, the events page and the hot keys page: they are
883/// reference types. Registered once with `ValueTypes::register_global` from
884/// the `register_types()` of the crate, which also registers their markup
885/// types and their lists (`EventTreeNodeList`, `EventChainLinkList`,
886/// `FiredEventList`, `HotKeyDescriptionList`).
887pub fn register_main_value_types() {
888    use ferroui_base::data::core::ValueTypes;
889
890    ValueTypes::register_reference::<MainViewModel>();
891    ValueTypes::register_reference::<EventsPageViewModel>();
892    ValueTypes::register_reference::<super::EventTreeNodeBase>();
893    ValueTypes::register_reference::<super::FiredEvent>();
894    ValueTypes::register_reference::<HotKeyPageViewModel>();
895    ValueTypes::register_reference::<super::HotKeyDescription>();
896}