core: Add debug feature to find display objects by mouse

This commit is contained in:
Nathan Adams 2023-06-21 21:23:17 +02:00
parent 898b2c8948
commit 5e608764ec
8 changed files with 279 additions and 7 deletions

View File

@ -7,7 +7,7 @@ mod movie;
use crate::context::{RenderContext, UpdateContext}; use crate::context::{RenderContext, UpdateContext};
use crate::debug_ui::avm1::Avm1ObjectWindow; use crate::debug_ui::avm1::Avm1ObjectWindow;
use crate::debug_ui::avm2::Avm2ObjectWindow; 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::handle::{AVM1ObjectHandle, AVM2ObjectHandle, DisplayObjectHandle};
use crate::debug_ui::movie::{MovieListWindow, MovieWindow}; use crate::debug_ui::movie::{MovieListWindow, MovieWindow};
use crate::display_object::TDisplayObject; use crate::display_object::TDisplayObject;
@ -30,6 +30,7 @@ pub struct DebugUi {
queued_messages: Vec<Message>, queued_messages: Vec<Message>,
items_to_save: Vec<ItemToSave>, items_to_save: Vec<ItemToSave>,
movie_list: Option<MovieListWindow>, movie_list: Option<MovieListWindow>,
display_object_search: Option<DisplayObjectSearchWindow>,
} }
#[derive(Debug)] #[derive(Debug)]
@ -42,10 +43,16 @@ pub enum Message {
TrackTopLevelMovie, TrackTopLevelMovie,
ShowKnownMovies, ShowKnownMovies,
SaveFile(ItemToSave), SaveFile(ItemToSave),
SearchForDisplayObject,
} }
impl DebugUi { 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); let mut messages = std::mem::take(&mut self.queued_messages);
self.display_objects.retain(|object, window| { 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 { for message in messages {
match message { match message {
Message::TrackDisplayObject(object) => { Message::TrackDisplayObject(object) => {
@ -98,10 +111,17 @@ impl DebugUi {
Message::ShowKnownMovies => { Message::ShowKnownMovies => {
self.movie_list = Some(Default::default()); 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<ItemToSave> { pub fn items_to_save(&mut self) -> Vec<ItemToSave> {
std::mem::take(&mut self.items_to_save) 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() { for (_object, window) in self.avm1_objects.iter() {
if let Some(object) = window.hovered_debug_rect() { if let Some(object) = window.hovered_debug_rect() {
let object = object.fetch(dynamic_root_set); let object = object.fetch(dynamic_root_set);

View File

@ -1,3 +1,7 @@
mod search;
pub use search::DisplayObjectSearchWindow;
use crate::avm1::TObject as _; use crate::avm1::TObject as _;
use crate::avm2::object::TObject as _; use crate::avm2::object::TObject as _;
use crate::context::UpdateContext; use crate::context::UpdateContext;

View File

@ -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<DisplayObjectTree>,
color: [f32; 3],
}
#[derive(Debug, Default)]
pub struct DisplayObjectSearchWindow {
finding: bool,
results: Vec<DisplayObjectTree>,
unique_results: FnvHashMap<DisplayObjectHandle, swf::Color>,
hovered_debug_rect: Option<DisplayObjectHandle>,
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<Message>,
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<Twips>) -> 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<Twips>,
object: DisplayObject<'gc>,
add_to: &mut Vec<DisplayObjectTree>,
) {
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<Message>,
hovered_debug_rect: &mut Option<DisplayObjectHandle>,
) {
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<Message>,
hovered_debug_rect: &mut Option<DisplayObjectHandle>,
) {
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)");
}
});
}

View File

@ -28,6 +28,10 @@ impl DisplayObjectHandle {
pub fn fetch<'gc>(&self, dynamic_root_set: DynamicRootSet<'gc>) -> DisplayObject<'gc> { pub fn fetch<'gc>(&self, dynamic_root_set: DynamicRootSet<'gc>) -> DisplayObject<'gc> {
*dynamic_root_set.fetch(&self.root) *dynamic_root_set.fetch(&self.root)
} }
pub fn as_ptr(&self) -> *const DisplayObjectPtr {
self.ptr
}
} }
impl Debug for DisplayObjectHandle { impl Debug for DisplayObjectHandle {

View File

@ -1863,14 +1863,14 @@ impl Player {
} }
#[cfg(feature = "egui")] #[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, // To allow using `mutate_with_update_context` and passing the context inside the debug ui,
// we avoid borrowing directly from self here. // we avoid borrowing directly from self here.
// This method should only be called once and it will panic if it tries to recursively render. // 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 debug_ui = self.debug_ui.clone();
let mut debug_ui = debug_ui.borrow_mut(); let mut debug_ui = debug_ui.borrow_mut();
self.mutate_with_update_context(|context| { self.mutate_with_update_context(|context| {
debug_ui.show(egui_ctx, context); debug_ui.show(egui_ctx, context, movie_offset);
}); });
} }

View File

@ -29,3 +29,5 @@ debug-menu = Debug Tools
debug-menu-open-stage = View Stage Info debug-menu-open-stage = View Stage Info
debug-menu-open-movie = View Movie debug-menu-open-movie = View Movie
debug-menu-open-movie-list = Show Known Movies debug-menu-open-movie-list = Show Known Movies
debug-menu-search-display-objects = Search Display Objects...

View File

@ -71,6 +71,7 @@ pub struct RuffleGui {
locale: LanguageIdentifier, locale: LanguageIdentifier,
default_player_options: PlayerOptions, default_player_options: PlayerOptions,
currently_opened: Option<(Url, PlayerOptions)>, currently_opened: Option<(Url, PlayerOptions)>,
was_suspended_before_debug: bool,
} }
impl RuffleGui { impl RuffleGui {
@ -91,6 +92,7 @@ impl RuffleGui {
is_about_visible: false, is_about_visible: false,
is_as3_warning_visible: false, is_as3_warning_visible: false,
is_open_dialog_visible: false, is_open_dialog_visible: false,
was_suspended_before_debug: false,
context_menu: vec![], context_menu: vec![],
open_dialog: OpenDialog::new( open_dialog: OpenDialog::new(
@ -113,6 +115,7 @@ impl RuffleGui {
egui_ctx: &egui::Context, egui_ctx: &egui::Context,
show_menu: bool, show_menu: bool,
mut player: Option<&mut Player>, mut player: Option<&mut Player>,
menu_height_offset: f64,
) { ) {
if show_menu { if show_menu {
self.main_menu_bar(egui_ctx, player.as_deref_mut()); self.main_menu_bar(egui_ctx, player.as_deref_mut());
@ -124,7 +127,16 @@ impl RuffleGui {
self.as3_warning(egui_ctx); self.as3_warning(egui_ctx);
if let Some(player) = player { 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() { for item in player.debug_ui().items_to_save() {
std::thread::spawn(move || { std::thread::spawn(move || {
if let Some(path) = FileDialog::new() if let Some(path) = FileDialog::new()
@ -272,6 +284,12 @@ impl RuffleGui {
player.debug_ui().queue_message(DebugMessage::ShowKnownMovies); 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| { menu::menu_button(ui, text(&self.locale, "help-menu"), |ui| {

View File

@ -2,7 +2,7 @@ use crate::backends::DesktopUiBackend;
use crate::cli::Opt; use crate::cli::Opt;
use crate::custom_event::RuffleEvent; use crate::custom_event::RuffleEvent;
use crate::gui::movie::{MovieView, MovieViewRenderer}; use crate::gui::movie::{MovieView, MovieViewRenderer};
use crate::gui::RuffleGui; use crate::gui::{RuffleGui, MENU_HEIGHT};
use crate::player::{PlayerController, PlayerOptions}; use crate::player::{PlayerController, PlayerOptions};
use anyhow::anyhow; use anyhow::anyhow;
use egui::Context; use egui::Context;
@ -189,11 +189,17 @@ impl GuiController {
.expect("Surface became unavailable"); .expect("Surface became unavailable");
let raw_input = self.egui_winit.take_egui_input(&self.window); 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| { let mut full_output = self.egui_ctx.run(raw_input, |context| {
self.gui.update( self.gui.update(
context, context,
self.window.fullscreen().is_none(), show_menu,
player.as_deref_mut(), 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; self.repaint_after = full_output.repaint_after;