Skip to main content

ferroui_diagnostics/diagnostics/
dev_tools.rs

1//! Port of `Diagnostics/DevTools.cs`: attaches DevTools to a top level or an application and
2//! opens its window.
3
4use super::controls;
5use super::views::MainWindow;
6use super::{
7    ClassicDesktopStyleApplicationLifetimeTopLevelGroup, DevToolsOptions, IDevToolsTopLevelGroup, KeyGestureExtesions,
8    SingleViewTopLevelGroup,
9};
10use ferroui_base::input::raw::{IRawInputEventArgs, RawKeyEventArgs, RawKeyEventType};
11use ferroui_base::input::{IInputManager, InputElement, KeyEventArgs, KeyGesture};
12use ferroui_base::interactivity::RoutingStrategies;
13use ferroui_base::reactive::{CompositeDisposable, Disposable, IDisposable, ObservableExt, SerialDisposable};
14use ferroui_base::{BoxedValue, FerroObject, Ref, Visual};
15use ferroui_controls::primitives::PopupRoot;
16use ferroui_controls::{Control, Design, TopLevel, Window};
17use std::cell::RefCell;
18use std::rc::Rc;
19
20ferroui_base::ferro_thread_local! {
21    // `s_open`: the DevTools windows that are open, by the group of top levels they inspect.
22    static S_OPEN: RefCell<Vec<(Rc<dyn IDevToolsTopLevelGroup>, Ref<MainWindow>)>> = RefCell::new(Vec::new());
23}
24
25/// Attaches DevTools and opens its window.
26pub struct DevTools;
27
28impl DevTools {
29    /// C# `Attach(TopLevel root, KeyGesture gesture)`: DevTools opens on `gesture` in `root`.
30    pub fn attach_with_gesture(root: &TopLevel, gesture: KeyGesture) -> Rc<dyn IDisposable> {
31        Self::attach(root, DevToolsOptions::new().with_gesture(gesture))
32    }
33
34    /// C# `Attach(TopLevel root, DevToolsOptions options)`: DevTools opens on the gesture of
35    /// `options` in `root`. Disposing the result detaches it.
36    pub fn attach(root: &TopLevel, options: Rc<DevToolsOptions>) -> Rc<dyn IDisposable> {
37        // The handler is added to the root itself: it holds the root weakly.
38        let weak_root = root.to_ref().downgrade();
39
40        root.add_disposable_handler_with(
41            InputElement::key_down_event(),
42            move |_, e: &KeyEventArgs| {
43                if options.gesture().matches(Some(e)) {
44                    if let Some(root) = weak_root.upgrade() {
45                        Self::open(&root, &options);
46                    }
47                }
48            },
49            RoutingStrategies::TUNNEL,
50            false,
51        )
52    }
53
54    /// C# `Open(TopLevel root, DevToolsOptions options)`: opens DevTools for `root`. Disposing
55    /// the result closes its window.
56    pub fn open(root: &TopLevel, options: &Rc<DevToolsOptions>) -> Rc<dyn IDisposable> {
57        Self::open_core(
58            Rc::new(SingleViewTopLevelGroup::new(root.to_ref())),
59            options,
60            root.to_ref().cast::<Window>(),
61            None,
62        )
63    }
64
65    /// C# `Open(IDevToolsTopLevelGroup group, DevToolsOptions options)`.
66    pub fn open_group(group: Rc<dyn IDevToolsTopLevelGroup>, options: &Rc<DevToolsOptions>) -> Rc<dyn IDisposable> {
67        Self::open_core(group, options, None, None)
68    }
69
70    /// C# `Attach(Application application, DevToolsOptions options)`: DevTools opens on the
71    /// gesture of `options` in any window of `application`. Disposing the result detaches it and
72    /// closes the window it opened.
73    ///
74    /// # Panics
75    /// When the application does not have a classic desktop lifetime.
76    pub fn attach_application(
77        application: &ferroui_controls::Application,
78        options: Rc<DevToolsOptions>,
79    ) -> Rc<dyn IDisposable> {
80        let opened_disposable = Rc::new(SerialDisposable::new());
81        let result = Rc::new(CompositeDisposable::with_capacity(2));
82        result.add(opened_disposable.clone());
83
84        // Skip if call on Design Mode
85        if !Design::is_design_mode() {
86            let life_time = application
87                .application_lifetime()
88                .filter(|lifetime| lifetime.as_classic_desktop_style_application_lifetime().is_some());
89
90            let Some(life_time) = life_time else {
91                panic!(
92                    "DevTools can only attach to applications that support IClassicDesktopStyleApplicationLifetime. (Parameter 'application')"
93                );
94            };
95
96            if let Some(input_manager) = application.input_manager() {
97                let input_manager: Rc<dyn IInputManager> = input_manager;
98                // The application owns its input manager: the handler holds the application
99                // weakly.
100                let weak_application = application.to_ref().downgrade();
101                result.add(input_manager.pre_process().subscribe_fn(move |e: Rc<dyn IRawInputEventArgs>| {
102                    let owner = life_time
103                        .as_classic_desktop_style_application_lifetime()
104                        .and_then(|lifetime| lifetime.main_window());
105
106                    if let Some(key_event_args) = e.downcast_ref::<RawKeyEventArgs>() {
107                        if key_event_args.type_() == RawKeyEventType::KeyUp
108                            && options.gesture().matches_raw(key_event_args)
109                        {
110                            opened_disposable.set(Some(Self::open_core(
111                                Rc::new(ClassicDesktopStyleApplicationLifetimeTopLevelGroup::new(life_time.clone())),
112                                &options,
113                                owner,
114                                weak_application.upgrade(),
115                            )));
116                            e.set_handled(true);
117                        }
118                    }
119                }));
120            }
121        }
122
123        result
124    }
125
126    fn open_core(
127        top_level_group: Rc<dyn IDevToolsTopLevelGroup>,
128        options: &Rc<DevToolsOptions>,
129        owner: Option<Ref<Window>>,
130        app: Option<Ref<ferroui_controls::Application>>,
131    ) -> Rc<dyn IDisposable> {
132        let focused_control = owner
133            .as_ref()
134            .and_then(|owner| owner.focus_manager().get_focused_element())
135            .and_then(|element| element.cast::<Control>());
136
137        let group = top_level_group.as_any();
138        let root: Ref<FerroObject> = if group.is::<ClassicDesktopStyleApplicationLifetimeTopLevelGroup>() {
139            let application = app
140                .or_else(ferroui_controls::Application::current)
141                .expect("an application is running when DevTools opens for its windows");
142            controls::Application::new(top_level_group.clone(), application).upcast()
143        } else if group.is::<SingleViewTopLevelGroup>() {
144            top_level_group.items().into_iter().next().expect("a single view group has its top level").upcast()
145        } else {
146            controls::TopLevelGroup::new(top_level_group.clone()).upcast()
147        };
148
149        // If single static toplevel is already visible in another devtools window, focus it.
150        let main_window = S_OPEN.with(|open| {
151            open.borrow().iter().find(|(group, _)| **group == *top_level_group).map(|(_, window)| window.clone())
152        });
153        if let Some(main_window) = main_window {
154            main_window.activate();
155            main_window.selected_control(focused_control);
156            return Disposable::empty();
157        }
158
159        let items = top_level_group.items();
160        if items.len() == 1 && top_level_group.observable_items().is_none() {
161            let single_top_level = items[0].clone();
162
163            let main_window = S_OPEN.with(|open| {
164                open.borrow()
165                    .iter()
166                    .find(|(group, _)| group.items().contains(&single_top_level))
167                    .map(|(_, window)| window.clone())
168            });
169            if let Some(main_window) = main_window {
170                main_window.activate();
171                main_window.selected_control(focused_control);
172                return Disposable::empty();
173            }
174        }
175
176        let window = MainWindow::new();
177        window.set_root(Some(root));
178        window.set_width(options.size().width);
179        window.set_height(options.size().height);
180        window.set_tag(Some(Rc::new(top_level_group.clone()) as BoxedValue));
181        window.set_options(options);
182        window.selected_control(focused_control);
183
184        // `window.Closed += DevToolsClosed`; the handler removes itself.
185        let closed_subscription: Rc<RefCell<Option<Rc<dyn IDisposable>>>> = Rc::new(RefCell::new(None));
186        let handler_subscription = closed_subscription.clone();
187        let weak_window = window.downgrade();
188        let closed = window.closed(move || {
189            if let Some(window) = weak_window.upgrade() {
190                Self::dev_tools_closed(&window, &handler_subscription);
191            }
192        });
193        *closed_subscription.borrow_mut() = Some(closed);
194
195        S_OPEN.with(|open| open.borrow_mut().push((top_level_group, window.clone())));
196        match owner.filter(|_| options.show_as_child_window()) {
197            Some(owner) => window.show_with_owner(&owner),
198            None => window.show(),
199        }
200
201        Disposable::create(move || window.close())
202    }
203
204    fn dev_tools_closed(window: &Ref<MainWindow>, subscription: &RefCell<Option<Rc<dyn IDisposable>>>) {
205        let own = subscription.borrow_mut().take();
206        if let Some(own) = own {
207            own.dispose();
208        }
209
210        let group = window.tag().and_then(|tag| tag.downcast_ref::<Rc<dyn IDevToolsTopLevelGroup>>().cloned());
211        if let Some(group) = group {
212            S_OPEN.with(|open| open.borrow_mut().retain(|(open_group, _)| **open_group != *group));
213        }
214    }
215
216    /// C# `DoesBelongToDevTool(this Visual v)`: whether `v` is in a DevTools window, or in a
217    /// popup of one. A visual that is in no top level belongs to DevTools, as upstream.
218    pub fn does_belong_to_dev_tool(v: &Visual) -> bool {
219        let mut top_level = TopLevel::get_top_level(Some(v));
220
221        while let Some(current) = top_level {
222            if current.is::<MainWindow>() {
223                break;
224            }
225            match current.cast::<PopupRoot>() {
226                Some(pr) => top_level = pr.parent_top_level(),
227                None => return false,
228            }
229        }
230        true
231    }
232}
233
234/// The extension method of `DevTools` on visuals.
235pub trait DevToolsVisualExtensions {
236    /// Whether the visual is in a DevTools window, or in a popup of one
237    /// ([`DevTools::does_belong_to_dev_tool`]).
238    fn does_belong_to_dev_tool(&self) -> bool;
239}
240
241impl DevToolsVisualExtensions for Visual {
242    fn does_belong_to_dev_tool(&self) -> bool {
243        DevTools::does_belong_to_dev_tool(self)
244    }
245}