core: Add filtering to avm2 debug window

This commit is contained in:
Nathan Adams 2023-06-07 19:44:10 +02:00
parent 89962cf970
commit 5b429e3bf5
3 changed files with 177 additions and 85 deletions

11
Cargo.lock generated
View File

@ -1312,6 +1312,16 @@ dependencies = [
"winit", "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]] [[package]]
name = "either" name = "either"
version = "1.8.1" version = "1.8.1"
@ -3699,6 +3709,7 @@ dependencies = [
"dasp", "dasp",
"downcast-rs", "downcast-rs",
"egui", "egui",
"egui_extras",
"encoding_rs", "encoding_rs",
"enumset", "enumset",
"flash-lso", "flash-lso",

View File

@ -52,6 +52,7 @@ hashbrown = { version = "0.14.0", features = ["raw"] }
scopeguard = "1.1.0" scopeguard = "1.1.0"
fluent-templates = "0.8.0" fluent-templates = "0.8.0"
egui = { version = "0.22.0", optional = true } egui = { version = "0.22.0", optional = true }
egui_extras = { version = "0.22.0", optional = true }
[target.'cfg(not(target_family = "wasm"))'.dependencies.futures] [target.'cfg(not(target_family = "wasm"))'.dependencies.futures]
version = "0.3.28" version = "0.3.28"
@ -71,6 +72,7 @@ nellymoser = ["nellymoser-rs"]
audio = ["dasp"] audio = ["dasp"]
known_stubs = ["linkme"] known_stubs = ["linkme"]
default_compatibility_rules = [] default_compatibility_rules = []
egui = ["dep:egui", "dep:egui_extras"]
[build-dependencies] [build-dependencies]
build_playerglobal = { path = "build_playerglobal" } build_playerglobal = { path = "build_playerglobal" }

View File

@ -3,13 +3,19 @@ use crate::avm2::{Activation, Error, Namespace, Object, TObject, Value};
use crate::context::UpdateContext; use crate::context::UpdateContext;
use crate::debug_ui::handle::{AVM2ObjectHandle, DisplayObjectHandle}; use crate::debug_ui::handle::{AVM2ObjectHandle, DisplayObjectHandle};
use crate::debug_ui::Message; 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 gc_arena::MutationContext;
use std::borrow::Cow; use std::borrow::Cow;
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct Avm2ObjectWindow { pub struct Avm2ObjectWindow {
hovered_debug_rect: Option<DisplayObjectHandle>, hovered_debug_rect: Option<DisplayObjectHandle>,
show_private_items: bool,
call_getters: bool,
getter_values: FnvHashMap<(String, String), Option<ValueWidget>>,
search: String,
} }
impl Avm2ObjectWindow { impl Avm2ObjectWindow {
@ -30,50 +36,167 @@ impl Avm2ObjectWindow {
Window::new(object_name(activation.context.gc_context, object)) Window::new(object_name(activation.context.gc_context, object))
.id(Id::new(object.as_ptr())) .id(Id::new(object.as_ptr()))
.open(&mut keep_open) .open(&mut keep_open)
.scroll2([true, true]) .scroll2([false, false]) // Table will provide its own scrolling
.show(egui_ctx, |ui| { .show(egui_ctx, |ui| {
if let Some(vtable) = object.vtable() { self.show_properties(object, messages, &mut activation, ui);
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: _ } => {}
}
}
}
});
}
}); });
keep_open keep_open
} }
fn show_properties<'gc>(
&mut self,
object: Object<'gc>,
messages: &mut Vec<Message>,
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<Value<'gc>, 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<Message>) {
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 { 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")); .unwrap_or(Cow::Borrowed("Object"));
format!("{} {:p}", name, object.as_ptr()) format!("{} {:p}", name, object.as_ptr())
} }
pub fn show_avm2_value<'gc>(
ui: &mut Ui,
activation: &mut Activation<'_, 'gc>,
value: Result<Value<'gc>, Error<'gc>>,
messages: &mut Vec<Message>,
) {
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());
}
}
}