Skip to main content

ferroui_diagnostics/diagnostics/views/
main_window.rs

1//! Port of `Diagnostics/Views/MainWindow.xaml.cs`: the class of the document
2//! `MainWindow.xaml`, the window of the developer tools.
3
4use crate::diagnostics::controls::Application;
5use crate::diagnostics::view_models::MainViewModel;
6use crate::diagnostics::{DevToolsOptions, HotKeyConfiguration};
7use ferroui_base::controls::ResourceKey;
8use ferroui_base::input::raw::{RawKeyEventArgs, RawKeyEventType, RawPointerEventArgs};
9use ferroui_base::input::{ICloseable, IInputRoot, InputElement, InputElementImpl, InputManager, Key, KeyGesture, KeyModifiers};
10use ferroui_base::interactivity::InteractiveImpl;
11use ferroui_base::layout::LayoutableImpl;
12use ferroui_base::metadata::{from_markup_value, into_markup_value};
13use ferroui_base::reactive::{IDisposable, ObservableExt};
14use ferroui_base::styling::{ControlTheme, StyleHostRef};
15use ferroui_base::utilities::CancelEventArgs;
16use ferroui_base::{
17    ferro_class, ferro_class_info, ferro_impl_classes, instantiate, FerroObject, FerroObjectImpl, PixelPoint, Ref,
18    StyledElementImpl, Visual, VisualImpl,
19};
20use ferroui_controls::diagnostics::{IPopupHostProvider, ToolTipDiagnostics};
21use ferroui_controls::platform::PlatformManager;
22use ferroui_controls::primitives::{
23    AdornerLayer, FlyoutBase, Popup, PopupFlyoutBase, PopupRoot, TemplatedControlImpl,
24};
25use ferroui_controls::{
26    Button, ContentControlImpl, Control, ControlImpl, TopLevel, TopLevelImpl, TopLevelImplExt, Window,
27    WindowBaseImpl, WindowImpl, WindowState,
28};
29use std::cell::{Cell, RefCell};
30use std::rc::Rc;
31
32/// The popup root of the host of a popup host provider, if it has one and
33/// it is a popup root (C# `popupProvider.PopupHost is PopupRoot popupRoot`).
34fn popup_root_of(popup_provider: Option<Rc<dyn IPopupHostProvider>>) -> Option<Ref<PopupRoot>> {
35    popup_provider.and_then(|popup_provider| popup_provider.popup_host()).and_then(|popup_host| popup_host.as_popup_root())
36}
37
38/// The popup host provider of a flyout, if the flyout is one (C#
39/// `flyout is IPopupHostProvider`).
40fn flyout_popup_host_provider(flyout: Option<Ref<FlyoutBase>>) -> Option<Rc<dyn IPopupHostProvider>> {
41    flyout.and_then(|flyout| flyout.cast::<PopupFlyoutBase>()).map(|flyout| flyout.to_popup_host_provider())
42}
43
44/// The window of the developer tools: the main view over the view model of
45/// the root it inspects, the hot keys that pick the control under the
46/// pointer, freeze the popups, freeze the value frames and take screenshots.
47#[repr(C)]
48pub struct MainWindow {
49    base: Window,
50    input_subscription: RefCell<Option<Rc<dyn IDisposable>>>,
51    /// The frozen popups, with the subscriptions of their `Closing` events.
52    frozen_popup_states: RefCell<Vec<(Ref<Popup>, Rc<dyn IDisposable>)>>,
53    root: RefCell<Option<Ref<FerroObject>>>,
54    /// The subscription to the `Closed` event of the root.
55    root_closed_subscription: RefCell<Option<Rc<dyn IDisposable>>>,
56    last_pointer_position: Cell<PixelPoint>,
57    hot_keys: RefCell<Option<Rc<HotKeyConfiguration>>>,
58}
59
60ferro_class!(MainWindow: Window);
61ferro_impl_classes!(
62    MainWindow: FerroObjectImpl,
63    VisualImpl,
64    LayoutableImpl,
65    InteractiveImpl,
66    InputElementImpl,
67    ControlImpl,
68    TemplatedControlImpl,
69    ContentControlImpl,
70    WindowBaseImpl,
71    WindowImpl
72);
73ferro_class_info!(MainWindow { new: MainWindow::new });
74
75impl StyledElementImpl for MainWindow {
76    /// C# `IStyleHost? IStyleHost.StylingParent => null`: the window does
77    /// not take the styles of the application it inspects.
78    fn styling_parent(_this: &Self) -> Option<StyleHostRef> {
79        None
80    }
81}
82
83impl TopLevelImpl for MainWindow {
84    fn on_closed(this: &Self) {
85        Self::parent_on_closed(this);
86        let input_subscription = this.input_subscription.borrow().clone();
87        if let Some(input_subscription) = input_subscription {
88            input_subscription.dispose();
89        }
90
91        let frozen_popup_states: Vec<_> = this.frozen_popup_states.borrow_mut().drain(..).collect();
92        for (_, closing_subscription) in frozen_popup_states {
93            closing_subscription.dispose();
94        }
95
96        let root = this.root.borrow().clone();
97        if root.as_ref().and_then(Self::closeable_of).is_some() {
98            let root_closed_subscription = this.root_closed_subscription.borrow_mut().take();
99            if let Some(root_closed_subscription) = root_closed_subscription {
100                root_closed_subscription.dispose();
101            }
102            *this.root.borrow_mut() = None;
103        }
104
105        if let Some(view_model) = this.view_model() {
106            view_model.dispose();
107        }
108    }
109}
110
111impl MainWindow {
112    /// The URI of the document of the class.
113    pub const DOCUMENT_URI: &'static str = "ferres://FerroUI.Diagnostics/diagnostics/views/MainWindow.xaml";
114
115    pub fn construct() -> Self {
116        Self {
117            base: Window::construct(PlatformManager::create_window()),
118            input_subscription: RefCell::new(None),
119            frozen_popup_states: RefCell::new(Vec::new()),
120            root: RefCell::new(None),
121            root_closed_subscription: RefCell::new(None),
122            last_pointer_position: Cell::new(PixelPoint::default()),
123            hot_keys: RefCell::new(None),
124        }
125    }
126
127    /// Creates the window and populates it from its document
128    /// (`InitializeComponent()`: the compiled markup of the document, which
129    /// the build of the crate generates), with the theme of a window of the
130    /// WinUI 3 theme, the observation of the input and the placement on the
131    /// startup screen.
132    ///
133    /// # Panics
134    /// Panics when no windowing platform is registered, and when the
135    /// document fails to load (an exception of the constructor in the
136    /// original).
137    pub fn new() -> Ref<Self> {
138        crate::register_types();
139        let this = instantiate(Self::construct());
140        Self::initialize_component(&this);
141
142        // Deviation (DEVIATIONS.md, Developer tools): the platform as a class of the window, for
143        // the room the header in the title bar leaves to the caption buttons of the system.
144        let platform = if cfg!(target_os = "macos") {
145            "macos"
146        } else if cfg!(windows) {
147            "windows"
148        } else {
149            "other"
150        };
151        this.classes().add(platform);
152
153        // Apply the SimpleTheme.Window theme; this must be done after the XAML is parsed as
154        // the theme is included in the MainWindow's XAML.
155        // Deviation (DEVIATIONS.md, Developer tools): the theme of a window of the WinUI 3 theme,
156        // which the document includes in place of the Simple theme.
157        if this.theme().is_none() {
158            let window_theme = this
159                .find_resource(&ResourceKey::Type(Window::TYPE))
160                .and_then(|value| value.downcast_ref::<Ref<ControlTheme>>().cloned());
161            if let Some(window_theme) = window_theme {
162                this.set_theme(window_theme);
163            }
164        }
165
166        let weak = this.downgrade();
167        let input_subscription = InputManager::instance().map(|input_manager| {
168            input_manager.process().subscribe_fn(move |x| {
169                let Some(this) = weak.upgrade() else {
170                    return;
171                };
172                if let Some(pointer_event_args) = x.downcast_ref::<RawPointerEventArgs>() {
173                    if let Some(root) = x.root().try_root_element() {
174                        this.last_pointer_position.set(root.point_to_screen(pointer_event_args.position()));
175                    }
176                } else if let Some(key_event_args) = x.downcast_ref::<RawKeyEventArgs>() {
177                    if key_event_args.type_() == RawKeyEventType::KeyDown {
178                        this.raw_key_down(key_event_args);
179                    }
180                }
181            })
182        });
183        *this.input_subscription.borrow_mut() = input_subscription;
184
185        // `this.Opened += lh`, where the handler removes itself.
186        let opened_subscription: Rc<RefCell<Option<Rc<dyn IDisposable>>>> = Rc::new(RefCell::new(None));
187        let handler_subscription = opened_subscription.clone();
188        let weak = this.downgrade();
189        let subscription = this.opened(move || {
190            let own = handler_subscription.borrow_mut().take();
191            if let Some(own) = own {
192                own.dispose();
193            }
194            let Some(this) = weak.upgrade() else {
195                return;
196            };
197            if let Some(index) = this.view_model().and_then(|view_model| view_model.startup_screen_index()) {
198                let screens = this.screens();
199                if index > -1 && index < screens.screen_count() {
200                    let screen = screens.all()[index as usize].clone();
201                    this.set_position(screen.bounds().top_left());
202                    this.set_window_state(WindowState::Maximized);
203                }
204            }
205        });
206        *opened_subscription.borrow_mut() = Some(subscription);
207
208        this
209    }
210
211    /// `InitializeComponent()`.
212    fn initialize_component(this: &Ref<Self>) {
213        if let Err(error) = crate::compiled_diagnostics_views_main_window::populate(None, this) {
214            panic!("{}: {error}", Self::DOCUMENT_URI);
215        }
216    }
217
218    /// The view model of the window (C# `DataContext as MainViewModel`).
219    fn view_model(&self) -> Option<Rc<MainViewModel>> {
220        from_markup_value::<Rc<MainViewModel>>(&self.data_context())
221    }
222
223    /// The root as `ICloseable` (C# `root is ICloseable`): a top level, or
224    /// the application of the developer tools.
225    fn closeable_of(root: &Ref<FerroObject>) -> Option<Rc<dyn ICloseable>> {
226        if let Some(top_level) = root.cast::<TopLevel>() {
227            return Some(top_level.as_closeable());
228        }
229        if let Some(application) = root.cast::<Application>() {
230            return Some(Application::as_closeable(application));
231        }
232        None
233    }
234
235    /// The object the window inspects: a top level, or the application of
236    /// the developer tools.
237    pub fn root(&self) -> Option<Ref<FerroObject>> {
238        self.root.borrow().clone()
239    }
240
241    /// Sets the object the window inspects; a root that tells when it is
242    /// closed gets a view model and closes the window when it closes.
243    pub fn set_root(&self, value: Option<Ref<FerroObject>>) {
244        if *self.root.borrow() != value {
245            let old_closable = self.root.borrow().as_ref().and_then(Self::closeable_of);
246            if old_closable.is_some() {
247                let root_closed_subscription = self.root_closed_subscription.borrow_mut().take();
248                if let Some(root_closed_subscription) = root_closed_subscription {
249                    root_closed_subscription.dispose();
250                }
251            }
252
253            *self.root.borrow_mut() = value.clone();
254
255            match value.as_ref().and_then(|root| Self::closeable_of(root).map(|closeable| (root.clone(), closeable))) {
256                Some((root, new_closable)) => {
257                    let weak = self.to_ref().downgrade();
258                    let subscription = new_closable.closed(Rc::new(move || {
259                        if let Some(this) = weak.upgrade() {
260                            this.root_closed();
261                        }
262                    }));
263                    *self.root_closed_subscription.borrow_mut() = Some(subscription);
264                    self.set_data_context(into_markup_value(MainViewModel::new(root)));
265                }
266                None => self.set_data_context(None),
267            }
268        }
269    }
270
271    /// The control under the pointer in a top level, the topmost visible
272    /// one that is hit test visible, outside of the adorner layer.
273    fn get_hovered_control(&self, top_level: &TopLevel) -> Option<Ref<Control>> {
274        let point = top_level.point_to_client(self.last_pointer_position.get());
275
276        top_level
277            .get_visuals_at_filtered(point, &|x: &Visual| {
278                if x.downcast_ref::<AdornerLayer>().is_some() || !x.is_visible() {
279                    return false;
280                }
281
282                x.downcast_ref::<InputElement>().is_none_or(|ie| ie.is_hit_test_visible())
283            })
284            .into_iter()
285            .next()
286            // `(Control?)`: the topmost visual, as a control.
287            .and_then(|visual| visual.cast::<Control>())
288    }
289
290    /// The roots of the popups that are open in the visual tree of a top
291    /// level: of popups, context flyouts and menus, attached flyouts, tool
292    /// tips and flyouts of buttons.
293    fn get_popup_roots(root: &TopLevel) -> Vec<Ref<PopupRoot>> {
294        let mut popup_roots = Vec::new();
295
296        fn process_property(popup_roots: &mut Vec<Ref<PopupRoot>>, popup_provider: Option<Rc<dyn IPopupHostProvider>>) {
297            if let Some(popup_root) = popup_root_of(popup_provider) {
298                popup_roots.push(popup_root);
299            }
300        }
301
302        for control in root.get_visual_descendants().filter_map(|visual| visual.cast::<Control>()) {
303            if let Some(p) = control.cast::<Popup>() {
304                if let Some(popup_root) = p.host().and_then(|host| host.as_popup_root()) {
305                    popup_roots.push(popup_root);
306                }
307            }
308
309            process_property(
310                &mut popup_roots,
311                flyout_popup_host_provider(control.get_value(Control::context_flyout_property())),
312            );
313            process_property(
314                &mut popup_roots,
315                control.get_value(Control::context_menu_property()).map(|context_menu| context_menu.to_popup_host_provider()),
316            );
317            process_property(
318                &mut popup_roots,
319                flyout_popup_host_provider(control.get_value(FlyoutBase::attached_flyout_property())),
320            );
321            process_property(
322                &mut popup_roots,
323                control.get_value(ToolTipDiagnostics::tool_tip_property()).map(|tool_tip| tool_tip.to_popup_host_provider()),
324            );
325            process_property(&mut popup_roots, flyout_popup_host_provider(control.get_value(Button::flyout_property())));
326        }
327
328        popup_roots
329    }
330
331    fn raw_key_down(&self, e: &RawKeyEventArgs) {
332        let hot_keys = self.hot_keys.borrow().clone();
333        let Some(hot_keys) = hot_keys else {
334            return;
335        };
336        let Some(vm) = self.view_model() else {
337            return;
338        };
339        // `vm.PointerOverRoot is TopLevel root`: the top level of the input root.
340        let Some(mut root) = vm.pointer_over_root().and_then(|pointer_over_root| top_level_of_root(&*pointer_over_root))
341        else {
342            return;
343        };
344
345        if let Some(parent_top_level) = root.cast::<PopupRoot>().and_then(|pr| pr.parent_top_level()) {
346            root = parent_top_level;
347        }
348
349        let modifiers = Self::merge_modifiers(e.key(), e.modifiers().to_key_modifiers());
350
351        if Self::is_matched(&hot_keys.value_frames_freeze(), e.key(), modifiers) {
352            self.freeze_value_frames(&vm);
353        } else if Self::is_matched(&hot_keys.value_frames_unfreeze(), e.key(), modifiers) {
354            self.unfreeze_value_frames(&vm);
355        } else if Self::is_matched(&hot_keys.toggle_popup_freeze(), e.key(), modifiers) {
356            self.toggle_freeze_popups(&root, &vm);
357        } else if Self::is_matched(&hot_keys.screenshot_selected_control(), e.key(), modifiers) {
358            self.screenshot_selected_control(&vm);
359        } else if Self::is_matched(&hot_keys.inspect_hovered_control(), e.key(), modifiers) {
360            self.inspect_hovered_control(&root, &vm);
361        }
362    }
363
364    fn is_matched(gesture: &KeyGesture, key: Key, modifiers: KeyModifiers) -> bool {
365        (gesture.key() == key || gesture.key() == Key::None) && modifiers.contains(gesture.key_modifiers())
366    }
367
368    /// When Control, Shift, or Alt are initially pressed, they are the Key and not part of Modifiers
369    /// This merges so modifier keys alone can more easily trigger actions
370    fn merge_modifiers(key: Key, modifiers: KeyModifiers) -> KeyModifiers {
371        match key {
372            Key::LeftCtrl | Key::RightCtrl => modifiers | KeyModifiers::CONTROL,
373            Key::LeftShift | Key::RightShift => modifiers | KeyModifiers::SHIFT,
374            Key::LeftAlt | Key::RightAlt => modifiers | KeyModifiers::ALT,
375            _ => modifiers,
376        }
377    }
378
379    fn freeze_value_frames(&self, vm: &MainViewModel) {
380        vm.enable_snapshot_styles(true);
381    }
382
383    fn unfreeze_value_frames(&self, vm: &MainViewModel) {
384        vm.enable_snapshot_styles(false);
385    }
386
387    fn toggle_freeze_popups(&self, root: &TopLevel, vm: &MainViewModel) {
388        vm.set_freeze_popups(!vm.freeze_popups());
389
390        for popup_root in Self::get_popup_roots(root) {
391            if let Some(popup) = popup_root.parent().and_then(|parent| parent.cast::<Popup>()) {
392                if vm.freeze_popups() {
393                    let weak = self.to_ref().downgrade();
394                    let closing_subscription = popup.closing(move |e| {
395                        if let Some(this) = weak.upgrade() {
396                            this.popup_on_closing(e);
397                        }
398                    });
399                    self.frozen_popup_states.borrow_mut().push((popup, closing_subscription));
400                } else {
401                    let removed: Vec<_> = {
402                        let mut frozen_popup_states = self.frozen_popup_states.borrow_mut();
403                        let (removed, kept) =
404                            frozen_popup_states.drain(..).partition(|(frozen, _)| frozen.ptr_eq(&popup));
405                        *frozen_popup_states = kept;
406                        removed
407                    };
408                    for (_, closing_subscription) in removed {
409                        closing_subscription.dispose();
410                    }
411                }
412            }
413        }
414    }
415
416    fn screenshot_selected_control(&self, vm: &MainViewModel) {
417        vm.shot(None);
418    }
419
420    fn inspect_hovered_control(&self, root: &TopLevel, vm: &MainViewModel) {
421        let mut control = None;
422
423        for popup_root in Self::get_popup_roots(root) {
424            control = self.get_hovered_control(&popup_root);
425
426            if control.is_some() {
427                break;
428            }
429        }
430
431        let control = control.or_else(|| self.get_hovered_control(root));
432
433        if let Some(control) = control {
434            vm.select_control(&control);
435        }
436    }
437
438    fn popup_on_closing(&self, e: &CancelEventArgs) {
439        if self.view_model().is_some_and(|vm| vm.freeze_popups()) {
440            e.set_cancel(true);
441        }
442    }
443
444    fn root_closed(&self) {
445        self.close();
446    }
447
448    /// Applies the options the developer tools were opened with: the hot
449    /// keys, the options of the view model and the theme variant.
450    pub fn set_options(&self, options: &DevToolsOptions) {
451        *self.hot_keys.borrow_mut() = Some(options.hot_keys());
452
453        if let Some(view_model) = self.view_model() {
454            view_model.set_options(options);
455        }
456        if let Some(theme_variant) = options.theme_variant() {
457            self.set_requested_theme_variant(Some(theme_variant));
458        }
459    }
460
461    /// Selects a control in the tree page of the window.
462    pub fn selected_control(&self, control: Option<Ref<Control>>) {
463        if let Some(control) = control {
464            if let Some(view_model) = self.view_model() {
465                view_model.select_control(&control);
466            }
467        }
468    }
469}
470
471/// The top level of an input root.
472///
473/// Deviation (DEVIATIONS.md, Developer tools): the root element of the input root of a top level
474/// is its host (the `TopLevelHost` that holds the window decorations), with the top level as its
475/// visual child; the developer tools were written for the framework version whose input root was
476/// the top level itself (`vm.PointerOverRoot is TopLevel root`), and with the host in between the
477/// hot keys that need the hovered top level (Ctrl+Shift, Ctrl+Alt+F) did nothing.
478pub(crate) fn top_level_of_root(root: &dyn IInputRoot) -> Option<Ref<TopLevel>> {
479    let element = root.try_root_element()?;
480    if let Some(top_level) = element.clone().cast::<TopLevel>() {
481        return Some(top_level);
482    }
483    element.visual_children().to_vec().into_iter().find_map(|child| child.cast::<TopLevel>())
484}