1use 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#[derive(Clone)]
47pub struct DevToolsPage {
48 title: String,
49 create_view: Rc<dyn Fn(&Rc<MainViewModel>) -> Ref<Control>>,
50}
51
52impl DevToolsPage {
53 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 pub fn title(&self) -> &str {
60 &self.title
61 }
62
63 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 static REGISTERED_PAGES: RefCell<Vec<(u64, DevToolsPage)>> = const { RefCell::new(Vec::new()) };
73 static NEXT_PAGE_ID: Cell<u64> = const { Cell::new(0) };
75}
76
77struct ExtensionPage {
79 page: DevToolsPage,
80 view: RefCell<Option<Ref<Control>>>,
81}
82
83#[derive(Clone)]
87enum MainContent {
88 Tree(Rc<TreePageViewModel>),
89 Events(Rc<EventsPageViewModel>),
90 HotKeys(Rc<HotKeyPageViewModel>),
91 Page(usize, Ref<Control>),
93}
94
95impl PartialEq for MainContent {
96 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 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
121fn 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
130pub 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 is_tree_pane_open: Cell<bool>,
154 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 extension_pages: Vec<ExtensionPage>,
169}
170
171impl 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 pub const COMBINED_TREE_TAB: i32 = 0;
187 pub const LOGICAL_TREE_TAB: i32 = 1;
189 pub const VISUAL_TREE_TAB: i32 = 2;
191 pub const EVENTS_TAB: i32 = 3;
193 pub const HOT_KEYS_TAB: i32 = 4;
195 pub const FIRST_EXTENSION_TAB: i32 = 5;
198
199 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 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 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 pub fn root(&self) -> Ref<FerroObject> {
315 self.root.clone()
316 }
317
318 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 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 pub fn events(&self) -> Rc<EventsPageViewModel> {
335 self.events.clone()
336 }
337
338 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 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 pub fn toggle_tree_pane(&self) {
364 self.set_is_tree_pane_open(!self.is_tree_pane_open());
365 }
366
367 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 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 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 pub fn content(&self) -> Option<BoxedValue> {
486 self.content.borrow().as_ref().and_then(MainContent::boxed)
487 }
488
489 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 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 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 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 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 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 pub fn show_hot_keys(&self) {
589 self.set_selected_tab(Self::HOT_KEYS_TAB);
590 }
591
592 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 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 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 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 pub fn startup_screen_index(&self) -> Option<i32> {
671 self.startup_screen_index.get()
672 }
673
674 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 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 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 eprintln!("{ex}");
701 }
702 }
704 });
705 }
706 }
707
708 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 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 pub fn select_focus_highlighter(&self, parameter: Option<BoxedValue>) {
764 self.set_focus_highlighter(from_markup_value::<Rc<dyn IBrush>>(¶meter));
765 }
766
767 pub fn pinned_properties(&self) -> Rc<RefCell<HashSet<String>>> {
770 self.pinned_properties.clone()
771 }
772
773 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 pub fn registered_pages() -> Vec<DevToolsPage> {
793 REGISTERED_PAGES.with(|pages| pages.borrow().iter().map(|(_, page)| page.clone()).collect())
794 }
795
796 pub fn extension_pages(&self) -> Vec<DevToolsPage> {
799 self.extension_pages.iter().map(|page| page.page.clone()).collect()
800 }
801
802 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
881pub 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}