From 5e608764ecb62b7cf9c1302198e28c9eff2c9a66 Mon Sep 17 00:00:00 2001 From: Nathan Adams Date: Wed, 21 Jun 2023 21:23:17 +0200 Subject: [PATCH] core: Add debug feature to find display objects by mouse --- core/src/debug_ui.rs | 33 +++- core/src/debug_ui/display_object.rs | 4 + core/src/debug_ui/display_object/search.rs | 209 +++++++++++++++++++++ core/src/debug_ui/handle.rs | 4 + core/src/player.rs | 4 +- desktop/assets/texts/en-US/main_menu.ftl | 2 + desktop/src/gui.rs | 20 +- desktop/src/gui/controller.rs | 10 +- 8 files changed, 279 insertions(+), 7 deletions(-) create mode 100644 core/src/debug_ui/display_object/search.rs diff --git a/core/src/debug_ui.rs b/core/src/debug_ui.rs index 886a21813..3f823ba4c 100644 --- a/core/src/debug_ui.rs +++ b/core/src/debug_ui.rs @@ -7,7 +7,7 @@ mod movie; use crate::context::{RenderContext, UpdateContext}; use crate::debug_ui::avm1::Avm1ObjectWindow; use crate::debug_ui::avm2::Avm2ObjectWindow; -use crate::debug_ui::display_object::DisplayObjectWindow; +use crate::debug_ui::display_object::{DisplayObjectSearchWindow, DisplayObjectWindow}; use crate::debug_ui::handle::{AVM1ObjectHandle, AVM2ObjectHandle, DisplayObjectHandle}; use crate::debug_ui::movie::{MovieListWindow, MovieWindow}; use crate::display_object::TDisplayObject; @@ -30,6 +30,7 @@ pub struct DebugUi { queued_messages: Vec, items_to_save: Vec, movie_list: Option, + display_object_search: Option, } #[derive(Debug)] @@ -42,10 +43,16 @@ pub enum Message { TrackTopLevelMovie, ShowKnownMovies, SaveFile(ItemToSave), + SearchForDisplayObject, } impl DebugUi { - pub(crate) fn show(&mut self, egui_ctx: &egui::Context, context: &mut UpdateContext) { + pub(crate) fn show( + &mut self, + egui_ctx: &egui::Context, + context: &mut UpdateContext, + movie_offset: f64, + ) { let mut messages = std::mem::take(&mut self.queued_messages); self.display_objects.retain(|object, window| { @@ -72,6 +79,12 @@ impl DebugUi { } } + if let Some(mut search) = self.display_object_search.take() { + if search.show(egui_ctx, context, &mut messages, movie_offset) { + self.display_object_search = Some(search); + } + } + for message in messages { match message { Message::TrackDisplayObject(object) => { @@ -98,10 +111,17 @@ impl DebugUi { Message::ShowKnownMovies => { self.movie_list = Some(Default::default()); } + Message::SearchForDisplayObject => { + self.display_object_search = Some(Default::default()); + } } } } + pub fn should_suspend_player(&self) -> bool { + self.display_object_search.is_some() + } + pub fn items_to_save(&mut self) -> Vec { std::mem::take(&mut self.items_to_save) } @@ -137,6 +157,15 @@ impl DebugUi { } } + if let Some(window) = &self.display_object_search { + for (color, object) in window.hovered_debug_rects() { + let object = object.fetch(dynamic_root_set); + let bounds = world_matrix * object.world_bounds(); + + draw_debug_rect(context, color, bounds, 5.0); + } + } + for (_object, window) in self.avm1_objects.iter() { if let Some(object) = window.hovered_debug_rect() { let object = object.fetch(dynamic_root_set); diff --git a/core/src/debug_ui/display_object.rs b/core/src/debug_ui/display_object.rs index 719abccdc..c2fbd4edc 100644 --- a/core/src/debug_ui/display_object.rs +++ b/core/src/debug_ui/display_object.rs @@ -1,3 +1,7 @@ +mod search; + +pub use search::DisplayObjectSearchWindow; + use crate::avm1::TObject as _; use crate::avm2::object::TObject as _; use crate::context::UpdateContext; diff --git a/core/src/debug_ui/display_object/search.rs b/core/src/debug_ui/display_object/search.rs new file mode 100644 index 000000000..f760bf731 --- /dev/null +++ b/core/src/debug_ui/display_object/search.rs @@ -0,0 +1,209 @@ +use crate::context::UpdateContext; +use crate::debug_ui::display_object::{open_display_object_button, DEFAULT_DEBUG_COLORS}; +use crate::debug_ui::handle::DisplayObjectHandle; +use crate::debug_ui::Message; +use crate::display_object::{ + DisplayObject, TDisplayObject, TDisplayObjectContainer, TInteractiveObject, +}; +use egui::collapsing_header::CollapsingState; +use egui::color_picker::show_color; +use egui::{Rgba, Ui, Vec2, Window}; +use fnv::FnvHashMap; +use swf::{Point, Twips}; + +#[derive(Debug)] +struct DisplayObjectTree { + handle: DisplayObjectHandle, + children: Vec, + color: [f32; 3], +} + +#[derive(Debug, Default)] +pub struct DisplayObjectSearchWindow { + finding: bool, + results: Vec, + unique_results: FnvHashMap, + hovered_debug_rect: Option, + include_hidden: bool, + only_mouse_enabled: bool, +} + +impl DisplayObjectSearchWindow { + pub fn hovered_debug_rects(&self) -> Vec<(swf::Color, DisplayObjectHandle)> { + if let Some(hovered_debug_rect) = &self.hovered_debug_rect { + vec![(swf::Color::RED, hovered_debug_rect.clone())] + } else { + self.unique_results + .iter() + .map(|(k, v)| (*v, k.clone())) + .collect() + } + } + + pub fn show( + &mut self, + egui_ctx: &egui::Context, + context: &mut UpdateContext, + messages: &mut Vec, + movie_offset: f64, + ) -> bool { + let mut keep_open = true; + self.hovered_debug_rect = None; + + if self.finding { + self.generate_results(egui_ctx, context, movie_offset); + } + + Window::new("Display Object Picker") + .open(&mut keep_open) + .scroll2([true, true]) + .show(egui_ctx, |ui| { + ui.horizontal(|ui| { + ui.checkbox(&mut self.include_hidden, "Include Hidden"); + ui.checkbox(&mut self.only_mouse_enabled, "Only Mouse Enabled Objects"); + }); + if self.finding { + ui.label("Click somewhere to finish searching"); + } else if ui.button("Start Searching").clicked() { + self.finding = true; + } + if !self.results.is_empty() { + ui.separator(); + ui.heading("Results"); + for tree in &self.results { + show_object_tree(ui, context, tree, messages, &mut self.hovered_debug_rect); + } + } + }); + + keep_open + } + + fn generate_results( + &mut self, + egui_ctx: &egui::Context, + context: &mut UpdateContext, + movie_offset: f64, + ) { + self.results.clear(); + self.unique_results.clear(); + + if let Some(pointer) = egui_ctx.pointer_latest_pos() { + let inverse_view_matrix = context.stage.inverse_view_matrix(); + let pos = inverse_view_matrix + * Point::from_pixels(pointer.x as f64, pointer.y as f64 - movie_offset); + + let mut results = vec![]; + for child in context.stage.iter_render_list() { + self.create_result_tree(context, pos, child, &mut results); + } + self.results = results; + } + + if egui_ctx.input_mut(|input| input.pointer.any_click()) { + self.finding = false; + } + } + + fn object_matches(&self, object: DisplayObject, cursor: Point) -> bool { + if !self.include_hidden && !object.visible() { + return false; + } + if self.only_mouse_enabled + && !object + .as_interactive() + .map(|i| i.mouse_enabled()) + .unwrap_or_default() + { + return false; + } + object.world_bounds().contains(cursor) + } + + fn create_result_tree<'gc>( + &mut self, + context: &mut UpdateContext<'_, 'gc>, + cursor: Point, + object: DisplayObject<'gc>, + add_to: &mut Vec, + ) { + if self.object_matches(object, cursor) { + let handle = DisplayObjectHandle::new(context, object); + let color = + DEFAULT_DEBUG_COLORS[self.unique_results.len() % DEFAULT_DEBUG_COLORS.len()]; + let mut tree = DisplayObjectTree { + handle: handle.clone(), + children: vec![], + color, + }; + self.unique_results.insert( + handle, + swf::Color { + r: (color[0] * 255.0) as u8, + g: (color[1] * 255.0) as u8, + b: (color[2] * 255.0) as u8, + a: 255, + }, + ); + if let Some(container) = object.as_container() { + for child in container.iter_render_list() { + self.create_result_tree(context, cursor, child, &mut tree.children); + } + } + add_to.push(tree); + } else if let Some(container) = object.as_container() { + for child in container.iter_render_list() { + self.create_result_tree(context, cursor, child, add_to); + } + } + } +} + +fn show_object_tree( + ui: &mut Ui, + context: &mut UpdateContext, + tree: &DisplayObjectTree, + messages: &mut Vec, + hovered_debug_rect: &mut Option, +) { + if tree.children.is_empty() { + show_item(ui, context, tree, messages, hovered_debug_rect); + } else { + CollapsingState::load_with_default_open(ui.ctx(), ui.id().with(tree.handle.as_ptr()), true) + .show_header(ui, |ui| { + show_item(ui, context, tree, messages, hovered_debug_rect); + }) + .body(|ui| { + for child in &tree.children { + show_object_tree(ui, context, child, messages, hovered_debug_rect); + } + }); + } +} + +fn show_item( + ui: &mut Ui, + context: &mut UpdateContext, + tree: &DisplayObjectTree, + messages: &mut Vec, + hovered_debug_rect: &mut Option, +) { + ui.horizontal(|ui| { + show_color( + ui, + Rgba::from_rgb(tree.color[0], tree.color[1], tree.color[2]), + Vec2::new(ui.spacing().interact_size.y, ui.spacing().interact_size.y), + ); + let object = tree.handle.fetch(context.dynamic_root); + open_display_object_button( + ui, + context, + messages, + tree.handle.fetch(context.dynamic_root), + hovered_debug_rect, + ); + if !object.visible() { + ui.weak("(Hidden)"); + } + }); +} diff --git a/core/src/debug_ui/handle.rs b/core/src/debug_ui/handle.rs index bc2ff1cf7..ca99bb63e 100644 --- a/core/src/debug_ui/handle.rs +++ b/core/src/debug_ui/handle.rs @@ -28,6 +28,10 @@ impl DisplayObjectHandle { pub fn fetch<'gc>(&self, dynamic_root_set: DynamicRootSet<'gc>) -> DisplayObject<'gc> { *dynamic_root_set.fetch(&self.root) } + + pub fn as_ptr(&self) -> *const DisplayObjectPtr { + self.ptr + } } impl Debug for DisplayObjectHandle { diff --git a/core/src/player.rs b/core/src/player.rs index 8f5f8ba05..251e92aaf 100644 --- a/core/src/player.rs +++ b/core/src/player.rs @@ -1863,14 +1863,14 @@ impl Player { } #[cfg(feature = "egui")] - pub fn show_debug_ui(&mut self, egui_ctx: &egui::Context) { + pub fn show_debug_ui(&mut self, egui_ctx: &egui::Context, movie_offset: f64) { // To allow using `mutate_with_update_context` and passing the context inside the debug ui, // we avoid borrowing directly from self here. // This method should only be called once and it will panic if it tries to recursively render. let debug_ui = self.debug_ui.clone(); let mut debug_ui = debug_ui.borrow_mut(); self.mutate_with_update_context(|context| { - debug_ui.show(egui_ctx, context); + debug_ui.show(egui_ctx, context, movie_offset); }); } diff --git a/desktop/assets/texts/en-US/main_menu.ftl b/desktop/assets/texts/en-US/main_menu.ftl index c7a676270..68bb93cbf 100644 --- a/desktop/assets/texts/en-US/main_menu.ftl +++ b/desktop/assets/texts/en-US/main_menu.ftl @@ -29,3 +29,5 @@ debug-menu = Debug Tools debug-menu-open-stage = View Stage Info debug-menu-open-movie = View Movie debug-menu-open-movie-list = Show Known Movies +debug-menu-search-display-objects = Search Display Objects... + diff --git a/desktop/src/gui.rs b/desktop/src/gui.rs index 3f7fe4047..a6ce04f16 100644 --- a/desktop/src/gui.rs +++ b/desktop/src/gui.rs @@ -71,6 +71,7 @@ pub struct RuffleGui { locale: LanguageIdentifier, default_player_options: PlayerOptions, currently_opened: Option<(Url, PlayerOptions)>, + was_suspended_before_debug: bool, } impl RuffleGui { @@ -91,6 +92,7 @@ impl RuffleGui { is_about_visible: false, is_as3_warning_visible: false, is_open_dialog_visible: false, + was_suspended_before_debug: false, context_menu: vec![], open_dialog: OpenDialog::new( @@ -113,6 +115,7 @@ impl RuffleGui { egui_ctx: &egui::Context, show_menu: bool, mut player: Option<&mut Player>, + menu_height_offset: f64, ) { if show_menu { self.main_menu_bar(egui_ctx, player.as_deref_mut()); @@ -124,7 +127,16 @@ impl RuffleGui { self.as3_warning(egui_ctx); if let Some(player) = player { - player.show_debug_ui(egui_ctx); + let was_suspended = player.debug_ui().should_suspend_player(); + player.show_debug_ui(egui_ctx, menu_height_offset); + if was_suspended != player.debug_ui().should_suspend_player() { + if player.debug_ui().should_suspend_player() { + self.was_suspended_before_debug = !player.is_playing(); + player.set_is_playing(false); + } else { + player.set_is_playing(!self.was_suspended_before_debug); + } + } for item in player.debug_ui().items_to_save() { std::thread::spawn(move || { if let Some(path) = FileDialog::new() @@ -272,6 +284,12 @@ impl RuffleGui { player.debug_ui().queue_message(DebugMessage::ShowKnownMovies); } } + if Button::new(text(&self.locale, "debug-menu-search-display-objects")).ui(ui).clicked() { + ui.close_menu(); + if let Some(player) = &mut player { + player.debug_ui().queue_message(DebugMessage::SearchForDisplayObject); + } + } }); }); menu::menu_button(ui, text(&self.locale, "help-menu"), |ui| { diff --git a/desktop/src/gui/controller.rs b/desktop/src/gui/controller.rs index c008606fc..3223ce6ae 100644 --- a/desktop/src/gui/controller.rs +++ b/desktop/src/gui/controller.rs @@ -2,7 +2,7 @@ use crate::backends::DesktopUiBackend; use crate::cli::Opt; use crate::custom_event::RuffleEvent; use crate::gui::movie::{MovieView, MovieViewRenderer}; -use crate::gui::RuffleGui; +use crate::gui::{RuffleGui, MENU_HEIGHT}; use crate::player::{PlayerController, PlayerOptions}; use anyhow::anyhow; use egui::Context; @@ -189,11 +189,17 @@ impl GuiController { .expect("Surface became unavailable"); let raw_input = self.egui_winit.take_egui_input(&self.window); + let show_menu = self.window.fullscreen().is_none(); let mut full_output = self.egui_ctx.run(raw_input, |context| { self.gui.update( context, - self.window.fullscreen().is_none(), + show_menu, player.as_deref_mut(), + if show_menu { + MENU_HEIGHT as f64 * self.window.scale_factor() + } else { + 0.0 + }, ); }); self.repaint_after = full_output.repaint_after;