Skip to content

Commit 73919c7

Browse files
core: Make some avm1 object properties editable in debug ui (#17279)
1 parent 1098a7d commit 73919c7

File tree

1 file changed

+265
-56
lines changed

1 file changed

+265
-56
lines changed

core/src/debug_ui/avm1.rs

Lines changed: 265 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,19 @@ use crate::context::UpdateContext;
33
use crate::debug_ui::display_object::open_display_object_button;
44
use crate::debug_ui::handle::{AVM1ObjectHandle, DisplayObjectHandle};
55
use crate::debug_ui::Message;
6-
use egui::{Grid, Id, TextEdit, Ui, Window};
6+
use crate::string::AvmString;
7+
use egui::{Grid, Id, TextBuffer, TextEdit, Ui, Window};
8+
use gc_arena::Mutation;
9+
use ruffle_wstr::{WStr, WString};
710

811
#[derive(Debug, Default)]
912
pub struct Avm1ObjectWindow {
1013
hovered_debug_rect: Option<DisplayObjectHandle>,
14+
key_filter_string: String,
15+
edited_key: Option<WString>,
16+
value_edit_buf: String,
17+
/// True if the active text edit should be focused (after clicking 'edit', etc.)
18+
focus_text_edit: bool,
1119
}
1220

1321
impl Avm1ObjectWindow {
@@ -36,23 +44,273 @@ impl Avm1ObjectWindow {
3644
.show(ui, |ui| {
3745
let mut keys = object.get_keys(&mut activation, true);
3846
keys.sort();
47+
ui.add(
48+
egui::TextEdit::singleline(&mut self.key_filter_string)
49+
.hint_text("🔍 Filter"),
50+
);
51+
ui.end_row();
52+
keys.retain(|key| {
53+
self.key_filter_string.is_empty()
54+
|| key
55+
.to_string()
56+
.to_ascii_lowercase()
57+
.contains(&self.key_filter_string.to_ascii_lowercase())
58+
});
3959

4060
for key in keys {
4161
let value = object.get(key, &mut activation);
4262

4363
ui.label(key.to_string());
44-
show_avm1_value(
45-
ui,
46-
&mut activation,
64+
if let Some(new) =
65+
self.show_avm1_value(ui, &mut activation, &key, value, messages)
66+
{
67+
if let Err(e) = object.set(key, new, &mut activation) {
68+
tracing::error!("Failed to set key {key}: {e}");
69+
}
70+
}
71+
ui.end_row();
72+
}
73+
});
74+
});
75+
keep_open
76+
}
77+
/// Shows an egui widget to inspect and (for certain value types) edit an AVM1 value.
78+
///
79+
/// Optionally returns the updated value, if the user edited it.
80+
pub fn show_avm1_value<'gc>(
81+
&mut self,
82+
ui: &mut Ui,
83+
activation: &mut Activation<'_, 'gc>,
84+
key: &AvmString,
85+
value: Result<Value<'gc>, Error<'gc>>,
86+
messages: &mut Vec<Message>,
87+
) -> Option<Value<'gc>> {
88+
match value {
89+
Ok(value) => {
90+
match value {
91+
Value::Undefined | Value::Null => {}
92+
Value::Bool(mut value) => {
93+
if ui.checkbox(&mut value, "").clicked() {
94+
return Some(Value::Bool(value));
95+
}
96+
}
97+
Value::Number(value) => {
98+
if let Some(new) = self.num_edit_ui(ui, key, value).map(Value::Number) {
99+
return Some(new);
100+
}
101+
}
102+
Value::String(value) => {
103+
if let Some(new) = self.string_edit_ui(ui, key, value).map(|string| {
104+
Value::String(AvmString::new_utf8(activation.gc(), string))
105+
}) {
106+
return Some(new);
107+
};
108+
}
109+
Value::Object(value) => {
110+
if value.as_executable().is_some() {
111+
ui.label("Function");
112+
} else if ui.button(object_name(value)).clicked() {
113+
messages.push(Message::TrackAVM1Object(AVM1ObjectHandle::new(
114+
activation.context,
47115
value,
116+
)));
117+
}
118+
}
119+
Value::MovieClip(value) => {
120+
if let Some((_, _, object)) = value.resolve_reference(activation) {
121+
open_display_object_button(
122+
ui,
123+
activation.context,
48124
messages,
125+
object,
49126
&mut self.hovered_debug_rect,
50127
);
51-
ui.end_row();
128+
} else {
129+
ui.colored_label(
130+
ui.style().visuals.error_fg_color,
131+
format!("Unknown movieclip {}", value.path()),
132+
);
52133
}
53-
});
134+
}
135+
};
136+
return show_value_type_combo_box(ui, key.as_wstr(), &value, activation.gc());
137+
}
138+
Err(e) => {
139+
ui.colored_label(ui.style().visuals.error_fg_color, e.to_string());
140+
}
141+
}
142+
None
143+
}
144+
fn num_edit_ui(&mut self, ui: &mut Ui, key: &AvmString, num: f64) -> Option<f64> {
145+
let mut new_val = None;
146+
if self
147+
.edited_key
148+
.as_ref()
149+
.is_some_and(|edit_key| *edit_key == key.as_wstr())
150+
{
151+
ui.horizontal(|ui| {
152+
let re = ui
153+
.add(egui::TextEdit::singleline(&mut self.value_edit_buf).desired_width(96.0));
154+
if self.focus_text_edit {
155+
re.request_focus();
156+
self.focus_text_edit = false;
157+
}
158+
match self.value_edit_buf.parse::<f64>() {
159+
Ok(num) => {
160+
if ui.input(|inp| inp.key_pressed(egui::Key::Enter))
161+
|| ui.set_button().clicked()
162+
{
163+
new_val = Some(num);
164+
self.edited_key = None;
165+
}
166+
}
167+
Err(e) => {
168+
ui.add_enabled(false, egui::Button::new(CHECKMARK_ICON))
169+
.on_disabled_hover_text(e.to_string());
170+
}
171+
}
172+
if ui.cancel_button().clicked() {
173+
self.edited_key = None;
174+
}
54175
});
55-
keep_open
176+
} else {
177+
ui.horizontal(|ui| {
178+
let num_str = num.to_string();
179+
ui.label(&num_str);
180+
if ui.edit_button().clicked() {
181+
self.edited_key = Some(key.as_wstr().to_owned());
182+
self.value_edit_buf = num_str;
183+
self.focus_text_edit = true;
184+
}
185+
});
186+
}
187+
new_val
188+
}
189+
fn string_edit_ui(
190+
&mut self,
191+
ui: &mut Ui,
192+
key: &AvmString,
193+
string: AvmString,
194+
) -> Option<String> {
195+
let mut new_val = None;
196+
ui.horizontal(|ui| {
197+
if self
198+
.edited_key
199+
.as_ref()
200+
.is_some_and(|edit_key| *edit_key == key.as_wstr())
201+
{
202+
let re = ui.add(TextEdit::singleline(&mut self.value_edit_buf).desired_width(96.0));
203+
if self.focus_text_edit {
204+
re.request_focus();
205+
self.focus_text_edit = false;
206+
}
207+
if ui.set_button().clicked() {
208+
new_val = Some(self.value_edit_buf.take());
209+
self.edited_key = None;
210+
}
211+
if ui.cancel_button().clicked() {
212+
self.edited_key = None;
213+
}
214+
} else {
215+
ui.label(string.to_utf8_lossy());
216+
if ui.edit_button().clicked() {
217+
self.value_edit_buf = string.to_string();
218+
self.edited_key = Some(key.as_wstr().to_owned());
219+
self.focus_text_edit = true;
220+
}
221+
}
222+
});
223+
new_val
224+
}
225+
}
226+
227+
/// Dropdown menu indicating the type of the value, as well as letting the
228+
/// user set a new type.
229+
fn show_value_type_combo_box<'gc>(
230+
ui: &mut Ui,
231+
key: &WStr,
232+
value: &Value<'gc>,
233+
mutation: &Mutation<'gc>,
234+
) -> Option<Value<'gc>> {
235+
let mut new = None;
236+
egui::ComboBox::new(egui::Id::new("value_combo").with(key), "Type")
237+
.selected_text(value_label(value))
238+
.show_ui(ui, |ui| {
239+
if ui
240+
.selectable_label(matches!(value, Value::Undefined), "Undefined")
241+
.clicked()
242+
{
243+
new = Some(Value::Undefined);
244+
}
245+
if ui
246+
.selectable_label(matches!(value, Value::Null), "Null")
247+
.clicked()
248+
{
249+
new = Some(Value::Null);
250+
}
251+
if ui
252+
.selectable_label(matches!(value, Value::Bool(_)), "Bool")
253+
.clicked()
254+
{
255+
new = Some(Value::Bool(false));
256+
}
257+
if ui
258+
.selectable_label(matches!(value, Value::Number(_)), "Number")
259+
.clicked()
260+
{
261+
new = Some(Value::Number(0.0));
262+
}
263+
if ui
264+
.selectable_label(matches!(value, Value::String(_)), "String")
265+
.clicked()
266+
{
267+
new = Some(Value::String(AvmString::new(mutation, WString::new())));
268+
}
269+
// There is no sensible way to create default values for these types,
270+
// so just disable the selectable labels to prevent setting to these types.
271+
ui.add_enabled(
272+
false,
273+
egui::SelectableLabel::new(matches!(value, Value::Object(_)), "Object"),
274+
);
275+
ui.add_enabled(
276+
false,
277+
egui::SelectableLabel::new(matches!(value, Value::MovieClip(_)), "MovieClip"),
278+
);
279+
});
280+
new
281+
}
282+
283+
fn value_label(value: &Value) -> &'static str {
284+
match value {
285+
Value::Undefined => "Undefined",
286+
Value::Null => "Null",
287+
Value::Bool(_) => "Bool",
288+
Value::Number(_) => "Number",
289+
Value::String(_) => "String",
290+
Value::Object(_) => "Object",
291+
Value::MovieClip(_) => "MovieClip",
292+
}
293+
}
294+
295+
const PENCIL_ICON: &str = "✏";
296+
const CHECKMARK_ICON: &str = "✔";
297+
const CANCEL_ICON: &str = "🗙";
298+
299+
trait UiExt {
300+
fn edit_button(&mut self) -> egui::Response;
301+
fn set_button(&mut self) -> egui::Response;
302+
fn cancel_button(&mut self) -> egui::Response;
303+
}
304+
305+
impl UiExt for egui::Ui {
306+
fn edit_button(&mut self) -> egui::Response {
307+
self.button(PENCIL_ICON).on_hover_text("Edit")
308+
}
309+
fn set_button(&mut self) -> egui::Response {
310+
self.button(CHECKMARK_ICON).on_hover_text("Set")
311+
}
312+
fn cancel_button(&mut self) -> egui::Response {
313+
self.button(CANCEL_ICON).on_hover_text("Cancel")
56314
}
57315
}
58316

@@ -67,52 +325,3 @@ fn object_name(object: Object) -> String {
67325
format!("Object {:p}", object.as_ptr())
68326
}
69327
}
70-
71-
pub fn show_avm1_value<'gc>(
72-
ui: &mut Ui,
73-
activation: &mut Activation<'_, 'gc>,
74-
value: Result<Value<'gc>, Error<'gc>>,
75-
messages: &mut Vec<Message>,
76-
hover: &mut Option<DisplayObjectHandle>,
77-
) {
78-
match value {
79-
Ok(Value::Undefined) => {
80-
ui.label("Undefined");
81-
}
82-
Ok(Value::Null) => {
83-
ui.label("Null");
84-
}
85-
Ok(Value::Bool(value)) => {
86-
ui.label(value.to_string());
87-
}
88-
Ok(Value::Number(value)) => {
89-
ui.label(value.to_string());
90-
}
91-
Ok(Value::String(value)) => {
92-
TextEdit::singleline(&mut value.to_string()).show(ui);
93-
}
94-
Ok(Value::Object(value)) => {
95-
if value.as_executable().is_some() {
96-
ui.label("Function");
97-
} else if ui.button(object_name(value)).clicked() {
98-
messages.push(Message::TrackAVM1Object(AVM1ObjectHandle::new(
99-
activation.context,
100-
value,
101-
)));
102-
}
103-
}
104-
Ok(Value::MovieClip(value)) => {
105-
if let Some((_, _, object)) = value.resolve_reference(activation) {
106-
open_display_object_button(ui, activation.context, messages, object, hover);
107-
} else {
108-
ui.colored_label(
109-
ui.style().visuals.error_fg_color,
110-
format!("Unknown movieclip {}", value.path()),
111-
);
112-
}
113-
}
114-
Err(e) => {
115-
ui.colored_label(ui.style().visuals.error_fg_color, e.to_string());
116-
}
117-
}
118-
}

0 commit comments

Comments
 (0)