diff --git a/Cargo.lock b/Cargo.lock index daa0f03d7..e7246d410 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1312,6 +1312,16 @@ dependencies = [ "winit", ] +[[package]] +name = "egui_extras" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9278f4337b526f0d57e5375e5a7340a311fa6ee8f9fcc75721ac50af13face02" +dependencies = [ + "egui", + "serde", +] + [[package]] name = "either" version = "1.8.1" @@ -3699,6 +3709,7 @@ dependencies = [ "dasp", "downcast-rs", "egui", + "egui_extras", "encoding_rs", "enumset", "flash-lso", diff --git a/core/Cargo.toml b/core/Cargo.toml index 544aa6dd2..ba24321cd 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -52,6 +52,7 @@ hashbrown = { version = "0.14.0", features = ["raw"] } scopeguard = "1.1.0" fluent-templates = "0.8.0" egui = { version = "0.22.0", optional = true } +egui_extras = { version = "0.22.0", optional = true } [target.'cfg(not(target_family = "wasm"))'.dependencies.futures] version = "0.3.28" @@ -71,6 +72,7 @@ nellymoser = ["nellymoser-rs"] audio = ["dasp"] known_stubs = ["linkme"] default_compatibility_rules = [] +egui = ["dep:egui", "dep:egui_extras"] [build-dependencies] build_playerglobal = { path = "build_playerglobal" } diff --git a/core/src/debug_ui/avm2.rs b/core/src/debug_ui/avm2.rs index 6214a347d..a989a89e8 100644 --- a/core/src/debug_ui/avm2.rs +++ b/core/src/debug_ui/avm2.rs @@ -3,13 +3,19 @@ use crate::avm2::{Activation, Error, Namespace, Object, TObject, Value}; use crate::context::UpdateContext; use crate::debug_ui::handle::{AVM2ObjectHandle, DisplayObjectHandle}; use crate::debug_ui::Message; -use egui::{Grid, Id, TextEdit, Ui, Window}; +use egui::{Align, Id, Layout, TextEdit, Ui, Widget, Window}; +use egui_extras::{Column, TableBuilder}; +use fnv::FnvHashMap; use gc_arena::MutationContext; use std::borrow::Cow; #[derive(Debug, Default)] pub struct Avm2ObjectWindow { hovered_debug_rect: Option, + show_private_items: bool, + call_getters: bool, + getter_values: FnvHashMap<(String, String), Option>, + search: String, } impl Avm2ObjectWindow { @@ -30,50 +36,167 @@ impl Avm2ObjectWindow { Window::new(object_name(activation.context.gc_context, object)) .id(Id::new(object.as_ptr())) .open(&mut keep_open) - .scroll2([true, true]) + .scroll2([false, false]) // Table will provide its own scrolling .show(egui_ctx, |ui| { - if let Some(vtable) = object.vtable() { - let mut entries = Vec::<(String, Namespace<'gc>, Property)>::new(); - for (name, ns, prop) in vtable.resolved_traits().iter() { - entries.push((name.to_string(), ns, *prop)); - } - entries.sort_by(|a, b| a.0.cmp(&b.0)); - - Grid::new(ui.id().with("properties")) - .num_columns(3) - .show(ui, |ui| { - for (name, ns, prop) in entries { - // TODO: filtering - if ns.is_public() { - match prop { - Property::Slot { slot_id } - | Property::ConstSlot { slot_id } => { - let value = object.get_slot(slot_id); - ui.label(name).on_hover_ui(|ui| { - ui.label(format!("{ns:?}")); - }); - show_avm2_value(ui, &mut activation, value, messages); - ui.end_row(); - } - Property::Virtual { get: Some(get), .. } => { - let value = - object.call_method(get, &[], &mut activation); - ui.label(name).on_hover_ui(|ui| { - ui.label(format!("{ns:?}")); - }); - show_avm2_value(ui, &mut activation, value, messages); - ui.end_row(); - } - Property::Method { .. } => {} - Property::Virtual { get: None, set: _ } => {} - } - } - } - }); - } + self.show_properties(object, messages, &mut activation, ui); }); keep_open } + + fn show_properties<'gc>( + &mut self, + object: Object<'gc>, + messages: &mut Vec, + activation: &mut Activation<'_, 'gc>, + ui: &mut Ui, + ) { + let mut entries = Vec::<(String, Namespace<'gc>, Property)>::new(); + // We can't access things whilst we iterate the vtable, so clone and sort it all here + if let Some(vtable) = object.vtable() { + for (name, ns, prop) in vtable.resolved_traits().iter() { + entries.push((name.to_string(), ns, *prop)); + } + } + entries.sort_by(|a, b| a.0.cmp(&b.0)); + + // [NA] Adding these on the same line seems to break the width of the table :( + TextEdit::singleline(&mut self.search) + .hint_text("Search...") + .ui(ui); + ui.horizontal(|ui| { + ui.checkbox(&mut self.show_private_items, "Show Private Items"); + ui.checkbox(&mut self.call_getters, "Call Getters"); + }); + + let search = self.search.to_ascii_lowercase(); + + TableBuilder::new(ui) + .striped(true) + .resizable(true) + .column(Column::auto()) + .column(Column::remainder()) + .column(Column::exact(75.0)) + .auto_shrink([true, true]) + .cell_layout(Layout::left_to_right(Align::Center)) + .header(20.0, |mut header| { + header.col(|ui| { + ui.strong("Name"); + }); + header.col(|ui| { + ui.strong("Value"); + }); + header.col(|ui| { + ui.strong("Controls"); + }); + }) + .body(|mut body| { + for (name, ns, prop) in entries { + if (ns.is_public() || self.show_private_items) + && name.to_ascii_lowercase().contains(&search) + { + match prop { + Property::Slot { slot_id } | Property::ConstSlot { slot_id } => { + body.row(18.0, |mut row| { + row.col(|ui| { + ui.label(&name).on_hover_ui(|ui| { + ui.label(format!("{ns:?}")); + }); + }); + row.col(|ui| { + let value = object.get_slot(slot_id); + ValueWidget::new(activation, value).show(ui, messages); + }); + row.col(|_| {}); + }); + } + Property::Virtual { get: Some(get), .. } => { + let key = (ns.as_uri().to_string(), name.clone()); + body.row(18.0, |mut row| { + row.col(|ui| { + ui.label(&name).on_hover_ui(|ui| { + ui.label(format!("{ns:?}")); + }); + }); + row.col(|ui| { + if self.call_getters { + let value = object.call_method(get, &[], activation); + ValueWidget::new(activation, value).show(ui, messages); + } else { + let value = self.getter_values.get_mut(&key); + if let Some(value) = value { + // Empty entry means we want to refresh it, + // so let's do that now + let widget = value.get_or_insert_with(|| { + let value = + object.call_method(get, &[], activation); + ValueWidget::new(activation, value) + }); + widget.show(ui, messages); + } + } + }); + row.col(|ui| { + if ui.button("Call Getter").clicked() { + self.getter_values.insert(key, None); + } + }); + }); + } + _ => {} + } + } + } + }); + } +} + +#[derive(Debug, Clone)] +enum ValueWidget { + String(String), + Object(AVM2ObjectHandle, String), + Other(Cow<'static, str>), + Error(String), +} + +impl ValueWidget { + fn new<'gc>( + activation: &mut Activation<'_, 'gc>, + value: Result, Error<'gc>>, + ) -> Self { + match value { + Ok(Value::Undefined) => ValueWidget::Other(Cow::Borrowed("Undefined")), + Ok(Value::Null) => ValueWidget::Other(Cow::Borrowed("Null")), + Ok(Value::Bool(value)) => ValueWidget::Other(Cow::Owned(value.to_string())), + Ok(Value::Number(value)) => ValueWidget::Other(Cow::Owned(value.to_string())), + Ok(Value::Integer(value)) => ValueWidget::Other(Cow::Owned(value.to_string())), + Ok(Value::String(value)) => ValueWidget::String(value.to_string()), + Ok(Value::Object(value)) => ValueWidget::Object( + AVM2ObjectHandle::new(&mut activation.context, value), + object_name(activation.context.gc_context, value), + ), + Err(e) => ValueWidget::Error(e.to_string()), + } + } + + fn show(&self, ui: &mut Ui, messages: &mut Vec) { + match self { + ValueWidget::String(value) => { + // Readonly + TextEdit::singleline(&mut value.as_str()).show(ui); + } + ValueWidget::Object(value, name) => { + if ui.button(name).clicked() { + messages.push(Message::TrackAVM2Object(value.clone())); + } + } + ValueWidget::Other(value) => { + ui.label(value.as_ref()); + } + ValueWidget::Error(value) => { + ui.colored_label(ui.style().visuals.error_fg_color, value); + } + } + } } fn object_name<'gc>(mc: MutationContext<'gc, '_>, object: Object<'gc>) -> String { @@ -83,47 +206,3 @@ fn object_name<'gc>(mc: MutationContext<'gc, '_>, object: Object<'gc>) -> String .unwrap_or(Cow::Borrowed("Object")); format!("{} {:p}", name, object.as_ptr()) } - -pub fn show_avm2_value<'gc>( - ui: &mut Ui, - activation: &mut Activation<'_, 'gc>, - value: Result, Error<'gc>>, - messages: &mut Vec, -) { - match value { - Ok(Value::Undefined) => { - ui.label("Undefined"); - } - Ok(Value::Null) => { - ui.label("Null"); - } - Ok(Value::Bool(value)) => { - ui.label(value.to_string()); - } - Ok(Value::Number(value)) => { - ui.label(value.to_string()); - } - Ok(Value::Integer(value)) => { - ui.label(value.to_string()); - } - Ok(Value::String(value)) => { - TextEdit::singleline(&mut value.to_string()).show(ui); - } - Ok(Value::Object(value)) => { - if value.as_executable().is_some() { - ui.label("Function"); - } else if ui - .button(object_name(activation.context.gc_context, value)) - .clicked() - { - messages.push(Message::TrackAVM2Object(AVM2ObjectHandle::new( - &mut activation.context, - value, - ))); - } - } - Err(e) => { - ui.colored_label(ui.style().visuals.error_fg_color, e.to_string()); - } - } -}