ferroui_diagnostics/diagnostics/
dev_tools.rs1use 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 static S_OPEN: RefCell<Vec<(Rc<dyn IDevToolsTopLevelGroup>, Ref<MainWindow>)>> = RefCell::new(Vec::new());
23}
24
25pub struct DevTools;
27
28impl DevTools {
29 pub fn attach_with_gesture(root: &TopLevel, gesture: KeyGesture) -> Rc<dyn IDisposable> {
31 Self::attach(root, DevToolsOptions::new().with_gesture(gesture))
32 }
33
34 pub fn attach(root: &TopLevel, options: Rc<DevToolsOptions>) -> Rc<dyn IDisposable> {
37 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 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 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 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 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 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 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 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 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
234pub trait DevToolsVisualExtensions {
236 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}