From 73919c71c07122ddacaa87159dd60d6f45347d3e Mon Sep 17 00:00:00 2001 From: crumblingstatue Date: Wed, 21 Aug 2024 23:04:58 +0200 Subject: [PATCH] core: Make some avm1 object properties editable in debug ui (#17279) --- core/src/debug_ui/avm1.rs | 323 +++++++++++++++++++++++++++++++------- 1 file changed, 266 insertions(+), 57 deletions(-) diff --git a/core/src/debug_ui/avm1.rs b/core/src/debug_ui/avm1.rs index 37154b1e9..1890b272a 100644 --- a/core/src/debug_ui/avm1.rs +++ b/core/src/debug_ui/avm1.rs @@ -3,11 +3,19 @@ use crate::context::UpdateContext; use crate::debug_ui::display_object::open_display_object_button; use crate::debug_ui::handle::{AVM1ObjectHandle, DisplayObjectHandle}; use crate::debug_ui::Message; -use egui::{Grid, Id, TextEdit, Ui, Window}; +use crate::string::AvmString; +use egui::{Grid, Id, TextBuffer, TextEdit, Ui, Window}; +use gc_arena::Mutation; +use ruffle_wstr::{WStr, WString}; #[derive(Debug, Default)] pub struct Avm1ObjectWindow { hovered_debug_rect: Option, + key_filter_string: String, + edited_key: Option, + value_edit_buf: String, + /// True if the active text edit should be focused (after clicking 'edit', etc.) + focus_text_edit: bool, } impl Avm1ObjectWindow { @@ -36,24 +44,274 @@ impl Avm1ObjectWindow { .show(ui, |ui| { let mut keys = object.get_keys(&mut activation, true); keys.sort(); + ui.add( + egui::TextEdit::singleline(&mut self.key_filter_string) + .hint_text("🔍 Filter"), + ); + ui.end_row(); + keys.retain(|key| { + self.key_filter_string.is_empty() + || key + .to_string() + .to_ascii_lowercase() + .contains(&self.key_filter_string.to_ascii_lowercase()) + }); for key in keys { let value = object.get(key, &mut activation); ui.label(key.to_string()); - show_avm1_value( - ui, - &mut activation, - value, - messages, - &mut self.hovered_debug_rect, - ); + if let Some(new) = + self.show_avm1_value(ui, &mut activation, &key, value, messages) + { + if let Err(e) = object.set(key, new, &mut activation) { + tracing::error!("Failed to set key {key}: {e}"); + } + } ui.end_row(); } }); }); keep_open } + /// Shows an egui widget to inspect and (for certain value types) edit an AVM1 value. + /// + /// Optionally returns the updated value, if the user edited it. + pub fn show_avm1_value<'gc>( + &mut self, + ui: &mut Ui, + activation: &mut Activation<'_, 'gc>, + key: &AvmString, + value: Result, Error<'gc>>, + messages: &mut Vec, + ) -> Option> { + match value { + Ok(value) => { + match value { + Value::Undefined | Value::Null => {} + Value::Bool(mut value) => { + if ui.checkbox(&mut value, "").clicked() { + return Some(Value::Bool(value)); + } + } + Value::Number(value) => { + if let Some(new) = self.num_edit_ui(ui, key, value).map(Value::Number) { + return Some(new); + } + } + Value::String(value) => { + if let Some(new) = self.string_edit_ui(ui, key, value).map(|string| { + Value::String(AvmString::new_utf8(activation.gc(), string)) + }) { + return Some(new); + }; + } + Value::Object(value) => { + if value.as_executable().is_some() { + ui.label("Function"); + } else if ui.button(object_name(value)).clicked() { + messages.push(Message::TrackAVM1Object(AVM1ObjectHandle::new( + activation.context, + value, + ))); + } + } + Value::MovieClip(value) => { + if let Some((_, _, object)) = value.resolve_reference(activation) { + open_display_object_button( + ui, + activation.context, + messages, + object, + &mut self.hovered_debug_rect, + ); + } else { + ui.colored_label( + ui.style().visuals.error_fg_color, + format!("Unknown movieclip {}", value.path()), + ); + } + } + }; + return show_value_type_combo_box(ui, key.as_wstr(), &value, activation.gc()); + } + Err(e) => { + ui.colored_label(ui.style().visuals.error_fg_color, e.to_string()); + } + } + None + } + fn num_edit_ui(&mut self, ui: &mut Ui, key: &AvmString, num: f64) -> Option { + let mut new_val = None; + if self + .edited_key + .as_ref() + .is_some_and(|edit_key| *edit_key == key.as_wstr()) + { + ui.horizontal(|ui| { + let re = ui + .add(egui::TextEdit::singleline(&mut self.value_edit_buf).desired_width(96.0)); + if self.focus_text_edit { + re.request_focus(); + self.focus_text_edit = false; + } + match self.value_edit_buf.parse::() { + Ok(num) => { + if ui.input(|inp| inp.key_pressed(egui::Key::Enter)) + || ui.set_button().clicked() + { + new_val = Some(num); + self.edited_key = None; + } + } + Err(e) => { + ui.add_enabled(false, egui::Button::new(CHECKMARK_ICON)) + .on_disabled_hover_text(e.to_string()); + } + } + if ui.cancel_button().clicked() { + self.edited_key = None; + } + }); + } else { + ui.horizontal(|ui| { + let num_str = num.to_string(); + ui.label(&num_str); + if ui.edit_button().clicked() { + self.edited_key = Some(key.as_wstr().to_owned()); + self.value_edit_buf = num_str; + self.focus_text_edit = true; + } + }); + } + new_val + } + fn string_edit_ui( + &mut self, + ui: &mut Ui, + key: &AvmString, + string: AvmString, + ) -> Option { + let mut new_val = None; + ui.horizontal(|ui| { + if self + .edited_key + .as_ref() + .is_some_and(|edit_key| *edit_key == key.as_wstr()) + { + let re = ui.add(TextEdit::singleline(&mut self.value_edit_buf).desired_width(96.0)); + if self.focus_text_edit { + re.request_focus(); + self.focus_text_edit = false; + } + if ui.set_button().clicked() { + new_val = Some(self.value_edit_buf.take()); + self.edited_key = None; + } + if ui.cancel_button().clicked() { + self.edited_key = None; + } + } else { + ui.label(string.to_utf8_lossy()); + if ui.edit_button().clicked() { + self.value_edit_buf = string.to_string(); + self.edited_key = Some(key.as_wstr().to_owned()); + self.focus_text_edit = true; + } + } + }); + new_val + } +} + +/// Dropdown menu indicating the type of the value, as well as letting the +/// user set a new type. +fn show_value_type_combo_box<'gc>( + ui: &mut Ui, + key: &WStr, + value: &Value<'gc>, + mutation: &Mutation<'gc>, +) -> Option> { + let mut new = None; + egui::ComboBox::new(egui::Id::new("value_combo").with(key), "Type") + .selected_text(value_label(value)) + .show_ui(ui, |ui| { + if ui + .selectable_label(matches!(value, Value::Undefined), "Undefined") + .clicked() + { + new = Some(Value::Undefined); + } + if ui + .selectable_label(matches!(value, Value::Null), "Null") + .clicked() + { + new = Some(Value::Null); + } + if ui + .selectable_label(matches!(value, Value::Bool(_)), "Bool") + .clicked() + { + new = Some(Value::Bool(false)); + } + if ui + .selectable_label(matches!(value, Value::Number(_)), "Number") + .clicked() + { + new = Some(Value::Number(0.0)); + } + if ui + .selectable_label(matches!(value, Value::String(_)), "String") + .clicked() + { + new = Some(Value::String(AvmString::new(mutation, WString::new()))); + } + // There is no sensible way to create default values for these types, + // so just disable the selectable labels to prevent setting to these types. + ui.add_enabled( + false, + egui::SelectableLabel::new(matches!(value, Value::Object(_)), "Object"), + ); + ui.add_enabled( + false, + egui::SelectableLabel::new(matches!(value, Value::MovieClip(_)), "MovieClip"), + ); + }); + new +} + +fn value_label(value: &Value) -> &'static str { + match value { + Value::Undefined => "Undefined", + Value::Null => "Null", + Value::Bool(_) => "Bool", + Value::Number(_) => "Number", + Value::String(_) => "String", + Value::Object(_) => "Object", + Value::MovieClip(_) => "MovieClip", + } +} + +const PENCIL_ICON: &str = "✏"; +const CHECKMARK_ICON: &str = "✔"; +const CANCEL_ICON: &str = "🗙"; + +trait UiExt { + fn edit_button(&mut self) -> egui::Response; + fn set_button(&mut self) -> egui::Response; + fn cancel_button(&mut self) -> egui::Response; +} + +impl UiExt for egui::Ui { + fn edit_button(&mut self) -> egui::Response { + self.button(PENCIL_ICON).on_hover_text("Edit") + } + fn set_button(&mut self) -> egui::Response { + self.button(CHECKMARK_ICON).on_hover_text("Set") + } + fn cancel_button(&mut self) -> egui::Response { + self.button(CANCEL_ICON).on_hover_text("Cancel") + } } fn object_name(object: Object) -> String { @@ -67,52 +325,3 @@ fn object_name(object: Object) -> String { format!("Object {:p}", object.as_ptr()) } } - -pub fn show_avm1_value<'gc>( - ui: &mut Ui, - activation: &mut Activation<'_, 'gc>, - value: Result, Error<'gc>>, - messages: &mut Vec, - hover: &mut Option, -) { - 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::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(value)).clicked() { - messages.push(Message::TrackAVM1Object(AVM1ObjectHandle::new( - activation.context, - value, - ))); - } - } - Ok(Value::MovieClip(value)) => { - if let Some((_, _, object)) = value.resolve_reference(activation) { - open_display_object_button(ui, activation.context, messages, object, hover); - } else { - ui.colored_label( - ui.style().visuals.error_fg_color, - format!("Unknown movieclip {}", value.path()), - ); - } - } - Err(e) => { - ui.colored_label(ui.style().visuals.error_fg_color, e.to_string()); - } - } -}