core: Add more avm2 debug info
This commit is contained in:
parent
5b429e3bf5
commit
647006b8d0
|
@ -3728,6 +3728,7 @@ dependencies = [
|
|||
"num-derive",
|
||||
"num-traits",
|
||||
"percent-encoding",
|
||||
"png",
|
||||
"quick-xml",
|
||||
"rand",
|
||||
"realfft",
|
||||
|
|
|
@ -53,6 +53,7 @@ scopeguard = "1.1.0"
|
|||
fluent-templates = "0.8.0"
|
||||
egui = { version = "0.22.0", optional = true }
|
||||
egui_extras = { version = "0.22.0", optional = true }
|
||||
png = { version = "0.17.8", optional = true }
|
||||
|
||||
[target.'cfg(not(target_family = "wasm"))'.dependencies.futures]
|
||||
version = "0.3.28"
|
||||
|
@ -72,7 +73,7 @@ nellymoser = ["nellymoser-rs"]
|
|||
audio = ["dasp"]
|
||||
known_stubs = ["linkme"]
|
||||
default_compatibility_rules = []
|
||||
egui = ["dep:egui", "dep:egui_extras"]
|
||||
egui = ["dep:egui", "dep:egui_extras", "png"]
|
||||
|
||||
[build-dependencies]
|
||||
build_playerglobal = { path = "build_playerglobal" }
|
||||
|
|
|
@ -451,6 +451,21 @@ mod wrapper {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "egui")]
|
||||
pub fn debug_sync_status(&self) -> std::borrow::Cow<'static, str> {
|
||||
match self.0.read().dirty_state {
|
||||
DirtyState::Clean => std::borrow::Cow::Borrowed("Clean"),
|
||||
DirtyState::CpuModified(area) => std::borrow::Cow::Owned(format!(
|
||||
"CPU modified from {}, {} to {}, {}",
|
||||
area.x_min, area.y_min, area.x_max, area.y_max
|
||||
)),
|
||||
DirtyState::GpuModified(_, area) => std::borrow::Cow::Owned(format!(
|
||||
"GPU modified from {}, {} to {}, {}",
|
||||
area.x_min, area.y_min, area.x_max, area.y_max
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_point_in_bounds(&self, x: i32, y: i32) -> bool {
|
||||
x >= 0 && x < self.width() as i32 && y >= 0 && y < self.height() as i32
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ use gc_arena::DynamicRootSet;
|
|||
use hashbrown::HashMap;
|
||||
use ruffle_render::commands::CommandHandler;
|
||||
use ruffle_render::matrix::Matrix;
|
||||
use std::fmt::{Debug, Formatter};
|
||||
use std::sync::{Arc, Weak};
|
||||
use swf::{Color, Rectangle, Twips};
|
||||
use weak_table::PtrWeakKeyHashMap;
|
||||
|
@ -27,6 +28,7 @@ pub struct DebugUi {
|
|||
avm1_objects: HashMap<AVM1ObjectHandle, Avm1ObjectWindow>,
|
||||
avm2_objects: HashMap<AVM2ObjectHandle, Avm2ObjectWindow>,
|
||||
queued_messages: Vec<Message>,
|
||||
items_to_save: Vec<ItemToSave>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -37,10 +39,11 @@ pub enum Message {
|
|||
TrackAVM2Object(AVM2ObjectHandle),
|
||||
TrackStage,
|
||||
TrackTopLevelMovie,
|
||||
SaveFile(ItemToSave),
|
||||
}
|
||||
|
||||
impl DebugUi {
|
||||
pub fn show(&mut self, egui_ctx: &egui::Context, context: &mut UpdateContext) {
|
||||
pub(crate) fn show(&mut self, egui_ctx: &egui::Context, context: &mut UpdateContext) {
|
||||
let mut messages = std::mem::take(&mut self.queued_messages);
|
||||
|
||||
self.display_objects.retain(|object, window| {
|
||||
|
@ -59,13 +62,15 @@ impl DebugUi {
|
|||
});
|
||||
|
||||
self.movies
|
||||
.retain(|movie, window| window.show(egui_ctx, context, movie));
|
||||
.retain(|movie, window| window.show(egui_ctx, context, movie, &mut messages));
|
||||
|
||||
for message in messages {
|
||||
match message {
|
||||
Message::TrackDisplayObject(object) => self.track_display_object(object),
|
||||
Message::TrackDisplayObject(object) => {
|
||||
self.track_display_object(object);
|
||||
}
|
||||
Message::TrackStage => {
|
||||
self.track_display_object(DisplayObjectHandle::new(context, context.stage))
|
||||
self.track_display_object(DisplayObjectHandle::new(context, context.stage));
|
||||
}
|
||||
Message::TrackMovie(movie) => {
|
||||
self.movies.insert(movie, Default::default());
|
||||
|
@ -79,9 +84,16 @@ impl DebugUi {
|
|||
Message::TrackAVM2Object(object) => {
|
||||
self.avm2_objects.insert(object, Default::default());
|
||||
}
|
||||
Message::SaveFile(file) => {
|
||||
self.items_to_save.push(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn items_to_save(&mut self) -> Vec<ItemToSave> {
|
||||
std::mem::take(&mut self.items_to_save)
|
||||
}
|
||||
|
||||
pub fn queue_message(&mut self, message: Message) {
|
||||
self.queued_messages.push(message);
|
||||
|
@ -134,6 +146,20 @@ impl DebugUi {
|
|||
}
|
||||
}
|
||||
|
||||
pub struct ItemToSave {
|
||||
pub suggested_name: String,
|
||||
pub data: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Debug for ItemToSave {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("ItemToSave")
|
||||
.field("suggested_name", &self.suggested_name)
|
||||
.field("data", &self.data.len())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_debug_rect(
|
||||
context: &mut RenderContext,
|
||||
color: Color,
|
||||
|
|
|
@ -1,14 +1,23 @@
|
|||
use crate::avm2::property::Property;
|
||||
use crate::avm2::{Activation, Error, Namespace, Object, TObject, Value};
|
||||
use crate::avm2::{Activation, ClassObject, Error, Namespace, Object, TObject, Value};
|
||||
use crate::context::UpdateContext;
|
||||
use crate::debug_ui::display_object::open_display_object_button;
|
||||
use crate::debug_ui::handle::{AVM2ObjectHandle, DisplayObjectHandle};
|
||||
use crate::debug_ui::Message;
|
||||
use egui::{Align, Id, Layout, TextEdit, Ui, Widget, Window};
|
||||
use egui_extras::{Column, TableBuilder};
|
||||
use crate::debug_ui::{ItemToSave, Message};
|
||||
use egui::{Align, Checkbox, Grid, Id, Layout, TextEdit, Ui, Window};
|
||||
use egui_extras::{Column, TableBody, TableBuilder, TableRow};
|
||||
use fnv::FnvHashMap;
|
||||
use gc_arena::MutationContext;
|
||||
use std::borrow::Cow;
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Hash, Default, Copy, Clone)]
|
||||
enum Panel {
|
||||
Information,
|
||||
#[default]
|
||||
Properties,
|
||||
Class,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Avm2ObjectWindow {
|
||||
hovered_debug_rect: Option<DisplayObjectHandle>,
|
||||
|
@ -16,6 +25,7 @@ pub struct Avm2ObjectWindow {
|
|||
call_getters: bool,
|
||||
getter_values: FnvHashMap<(String, String), Option<ValueWidget>>,
|
||||
search: String,
|
||||
open_panel: Panel,
|
||||
}
|
||||
|
||||
impl Avm2ObjectWindow {
|
||||
|
@ -36,13 +46,178 @@ impl Avm2ObjectWindow {
|
|||
Window::new(object_name(activation.context.gc_context, object))
|
||||
.id(Id::new(object.as_ptr()))
|
||||
.open(&mut keep_open)
|
||||
.scroll2([false, false]) // Table will provide its own scrolling
|
||||
.scroll2([true, true])
|
||||
.show(egui_ctx, |ui| {
|
||||
self.show_properties(object, messages, &mut activation, ui);
|
||||
ui.horizontal(|ui| {
|
||||
ui.selectable_value(&mut self.open_panel, Panel::Information, "Information");
|
||||
ui.selectable_value(&mut self.open_panel, Panel::Properties, "Properties");
|
||||
if object.as_class_object().is_some() {
|
||||
ui.selectable_value(&mut self.open_panel, Panel::Class, "Class Info");
|
||||
}
|
||||
});
|
||||
ui.separator();
|
||||
|
||||
match self.open_panel {
|
||||
Panel::Information => {
|
||||
self.show_information(object, messages, &mut activation, ui)
|
||||
}
|
||||
Panel::Properties => {
|
||||
self.show_properties(object, messages, &mut activation, ui)
|
||||
}
|
||||
Panel::Class => {
|
||||
if let Some(class) = object.as_class_object() {
|
||||
self.show_class(class, messages, &mut activation, ui)
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
keep_open
|
||||
}
|
||||
|
||||
fn show_information<'gc>(
|
||||
&mut self,
|
||||
object: Object<'gc>,
|
||||
messages: &mut Vec<Message>,
|
||||
activation: &mut Activation<'_, 'gc>,
|
||||
ui: &mut Ui,
|
||||
) {
|
||||
Grid::new(ui.id().with("info"))
|
||||
.num_columns(2)
|
||||
.striped(true)
|
||||
.show(ui, |ui| {
|
||||
if let Some(class) = object.instance_of() {
|
||||
ui.label("Instance Of");
|
||||
ValueWidget::new(activation, Ok(class.into())).show(ui, messages);
|
||||
ui.end_row();
|
||||
}
|
||||
|
||||
if let Some(object) = object.as_display_object() {
|
||||
ui.label("Display Object");
|
||||
open_display_object_button(
|
||||
ui,
|
||||
&mut activation.context,
|
||||
messages,
|
||||
object,
|
||||
&mut self.hovered_debug_rect,
|
||||
);
|
||||
ui.end_row();
|
||||
}
|
||||
|
||||
if let Some(bmd) = object.as_bitmap_data() {
|
||||
ui.label("Bitmap Data Size");
|
||||
ui.label(format!("{} x {}", bmd.width(), bmd.height()));
|
||||
ui.end_row();
|
||||
|
||||
ui.label("Bitmap Data Status");
|
||||
if bmd.disposed() {
|
||||
ui.label("Disposed");
|
||||
} else {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Alive");
|
||||
ui.add_space(10.0);
|
||||
if ui.button("Save File...").clicked() {
|
||||
let mut data = Vec::new();
|
||||
let mut encoder =
|
||||
png::Encoder::new(&mut data, bmd.width(), bmd.height());
|
||||
encoder.set_color(png::ColorType::Rgba);
|
||||
encoder.set_depth(png::BitDepth::Eight);
|
||||
if let Err(e) = encoder.write_header().and_then(|mut w| {
|
||||
w.write_image_data(&bmd.sync().read().pixels_rgba())
|
||||
}) {
|
||||
tracing::error!("Couldn't create png: {e}");
|
||||
} else {
|
||||
messages.push(Message::SaveFile(ItemToSave {
|
||||
suggested_name: format!("{:p}.png", object.as_ptr()),
|
||||
data,
|
||||
}));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
ui.end_row();
|
||||
|
||||
ui.label("Bitmap Data Transparency");
|
||||
ui.add_enabled(false, Checkbox::new(&mut bmd.transparency(), "Transparent"));
|
||||
ui.end_row();
|
||||
|
||||
ui.label("Bitmap Data Sync");
|
||||
ui.label(bmd.debug_sync_status());
|
||||
ui.end_row();
|
||||
}
|
||||
|
||||
if let Some(ba) = object.as_bytearray() {
|
||||
ui.label("Byte Array");
|
||||
|
||||
if ba.len() > 0 {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(format!("{} bytes", ba.len()));
|
||||
ui.add_space(10.0);
|
||||
if ui.button("Sync & Save PNG...").clicked() {
|
||||
messages.push(Message::SaveFile(ItemToSave {
|
||||
suggested_name: format!("{:p}.png", object.as_ptr()),
|
||||
data: ba.bytes().to_vec(),
|
||||
}));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
ui.label("0 bytes");
|
||||
}
|
||||
|
||||
ui.end_row();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn show_class<'gc>(
|
||||
&mut self,
|
||||
class: ClassObject<'gc>,
|
||||
messages: &mut Vec<Message>,
|
||||
activation: &mut Activation<'_, 'gc>,
|
||||
ui: &mut Ui,
|
||||
) {
|
||||
Grid::new(ui.id().with("class"))
|
||||
.num_columns(2)
|
||||
.striped(true)
|
||||
.spacing([8.0, 8.0])
|
||||
.show(ui, |ui| {
|
||||
let definition = class.inner_class_definition();
|
||||
let name = definition.read().name();
|
||||
|
||||
ui.label("Namespace");
|
||||
ui.text_edit_singleline(&mut name.namespace().as_uri().to_string().as_str());
|
||||
ui.end_row();
|
||||
|
||||
ui.label("Name");
|
||||
ui.text_edit_singleline(&mut name.local_name().to_string().as_str());
|
||||
ui.end_row();
|
||||
|
||||
ui.label("Super Chain");
|
||||
ui.vertical(|ui| {
|
||||
let mut superclass = Some(class);
|
||||
while let Some(class) = superclass {
|
||||
ValueWidget::new(activation, Ok(class.into())).show(ui, messages);
|
||||
superclass = class.superclass_object();
|
||||
}
|
||||
});
|
||||
ui.end_row();
|
||||
|
||||
ui.label("Interfaces");
|
||||
ui.vertical(|ui| {
|
||||
for interface in class.interfaces() {
|
||||
ui.text_edit_singleline(
|
||||
&mut interface
|
||||
.read()
|
||||
.name()
|
||||
.to_qualified_name_err_message(activation.context.gc_context)
|
||||
.to_string()
|
||||
.as_str(),
|
||||
);
|
||||
}
|
||||
});
|
||||
ui.end_row();
|
||||
});
|
||||
}
|
||||
|
||||
fn show_properties<'gc>(
|
||||
&mut self,
|
||||
object: Object<'gc>,
|
||||
|
@ -59,13 +234,15 @@ impl Avm2ObjectWindow {
|
|||
}
|
||||
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.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
||||
ui.checkbox(&mut self.show_private_items, "Show Private Items");
|
||||
ui.checkbox(&mut self.call_getters, "Call Getters");
|
||||
ui.add_sized(
|
||||
ui.available_size(),
|
||||
TextEdit::singleline(&mut self.search).hint_text("Search..."),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
let search = self.search.to_ascii_lowercase();
|
||||
|
@ -91,17 +268,39 @@ impl Avm2ObjectWindow {
|
|||
})
|
||||
.body(|mut body| {
|
||||
for (name, ns, prop) in entries {
|
||||
if (ns.is_public() || self.show_private_items)
|
||||
if (self.show_private_items || ns.is_public())
|
||||
&& name.to_ascii_lowercase().contains(&search)
|
||||
{
|
||||
match prop {
|
||||
Property::Slot { slot_id } | Property::ConstSlot { slot_id } => {
|
||||
body.row(18.0, |mut row| {
|
||||
self.show_property(
|
||||
object, messages, activation, &mut body, &name, ns, prop,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn show_property<'gc>(
|
||||
&mut self,
|
||||
object: Object<'gc>,
|
||||
messages: &mut Vec<Message>,
|
||||
activation: &mut Activation<'_, 'gc>,
|
||||
body: &mut TableBody,
|
||||
name: &str,
|
||||
ns: Namespace<'gc>,
|
||||
prop: Property,
|
||||
) {
|
||||
let label_col = |row: &mut TableRow| {
|
||||
row.col(|ui| {
|
||||
ui.label(&name).on_hover_ui(|ui| {
|
||||
ui.label(name).on_hover_ui(|ui| {
|
||||
ui.label(format!("{ns:?}"));
|
||||
});
|
||||
});
|
||||
};
|
||||
match prop {
|
||||
Property::Slot { slot_id } | Property::ConstSlot { slot_id } => {
|
||||
body.row(18.0, |mut row| {
|
||||
label_col(&mut row);
|
||||
row.col(|ui| {
|
||||
let value = object.get_slot(slot_id);
|
||||
ValueWidget::new(activation, value).show(ui, messages);
|
||||
|
@ -110,13 +309,9 @@ impl Avm2ObjectWindow {
|
|||
});
|
||||
}
|
||||
Property::Virtual { get: Some(get), .. } => {
|
||||
let key = (ns.as_uri().to_string(), name.clone());
|
||||
let key = (ns.as_uri().to_string(), name.to_string());
|
||||
body.row(18.0, |mut row| {
|
||||
row.col(|ui| {
|
||||
ui.label(&name).on_hover_ui(|ui| {
|
||||
ui.label(format!("{ns:?}"));
|
||||
});
|
||||
});
|
||||
label_col(&mut row);
|
||||
row.col(|ui| {
|
||||
if self.call_getters {
|
||||
let value = object.call_method(get, &[], activation);
|
||||
|
@ -127,8 +322,7 @@ impl Avm2ObjectWindow {
|
|||
// 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);
|
||||
let value = object.call_method(get, &[], activation);
|
||||
ValueWidget::new(activation, value)
|
||||
});
|
||||
widget.show(ui, messages);
|
||||
|
@ -145,9 +339,6 @@ impl Avm2ObjectWindow {
|
|||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
@ -174,7 +365,7 @@ impl ValueWidget {
|
|||
AVM2ObjectHandle::new(&mut activation.context, value),
|
||||
object_name(activation.context.gc_context, value),
|
||||
),
|
||||
Err(e) => ValueWidget::Error(e.to_string()),
|
||||
Err(e) => ValueWidget::Error(format!("{e:?}")),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -200,9 +391,18 @@ impl ValueWidget {
|
|||
}
|
||||
|
||||
fn object_name<'gc>(mc: MutationContext<'gc, '_>, object: Object<'gc>) -> String {
|
||||
if let Some(class) = object.as_class_object() {
|
||||
class
|
||||
.inner_class_definition()
|
||||
.read()
|
||||
.name()
|
||||
.to_qualified_name_err_message(mc)
|
||||
.to_string()
|
||||
} else {
|
||||
let name = object
|
||||
.instance_of_class_definition()
|
||||
.map(|r| Cow::Owned(r.read().name().to_qualified_name(mc).to_string()))
|
||||
.map(|r| Cow::Owned(r.read().name().local_name().to_string()))
|
||||
.unwrap_or(Cow::Borrowed("Object"));
|
||||
format!("{} {:p}", name, object.as_ptr())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use crate::character::Character;
|
||||
use crate::context::UpdateContext;
|
||||
use crate::debug_ui::Message;
|
||||
use crate::debug_ui::{ItemToSave, Message};
|
||||
use crate::tag_utils::SwfMovie;
|
||||
use egui::{CollapsingHeader, Grid, Id, TextEdit, Ui, Window};
|
||||
use std::sync::Arc;
|
||||
|
@ -25,6 +25,7 @@ impl MovieWindow {
|
|||
egui_ctx: &egui::Context,
|
||||
context: &mut UpdateContext,
|
||||
movie: Arc<SwfMovie>,
|
||||
messages: &mut Vec<Message>,
|
||||
) -> bool {
|
||||
let mut keep_open = true;
|
||||
|
||||
|
@ -49,7 +50,7 @@ impl MovieWindow {
|
|||
ui.separator();
|
||||
|
||||
match self.open_panel {
|
||||
Panel::Information => self.show_information(ui, &movie),
|
||||
Panel::Information => self.show_information(ui, &movie, messages),
|
||||
Panel::Characters => self.show_characters(ui, context, &movie),
|
||||
}
|
||||
});
|
||||
|
@ -101,7 +102,24 @@ impl MovieWindow {
|
|||
});
|
||||
}
|
||||
|
||||
fn show_information(&mut self, ui: &mut Ui, movie: &Arc<SwfMovie>) {
|
||||
fn show_information(
|
||||
&mut self,
|
||||
ui: &mut Ui,
|
||||
movie: &Arc<SwfMovie>,
|
||||
messages: &mut Vec<Message>,
|
||||
) {
|
||||
if !movie.data().is_empty() && ui.button("Save File...").clicked() {
|
||||
let suggested_name = movie
|
||||
.url()
|
||||
.rsplit_once('.')
|
||||
.map(|(_left, right)| right.to_string())
|
||||
.unwrap_or_else(|| format!("{:p}.swf", Arc::as_ptr(movie)));
|
||||
messages.push(Message::SaveFile(ItemToSave {
|
||||
suggested_name,
|
||||
data: movie.data().to_vec(),
|
||||
}));
|
||||
}
|
||||
|
||||
Grid::new(ui.id().with("information"))
|
||||
.num_columns(2)
|
||||
.show(ui, |ui| {
|
||||
|
|
|
@ -1870,8 +1870,8 @@ impl Player {
|
|||
}
|
||||
|
||||
#[cfg(feature = "egui")]
|
||||
pub fn debug_ui_message(&mut self, message: crate::debug_ui::Message) {
|
||||
self.debug_ui.borrow_mut().queue_message(message)
|
||||
pub fn debug_ui(&mut self) -> core::cell::RefMut<'_, crate::debug_ui::DebugUi> {
|
||||
self.debug_ui.borrow_mut()
|
||||
}
|
||||
|
||||
/// Update the current state of the player.
|
||||
|
|
|
@ -14,10 +14,12 @@ use chrono::DateTime;
|
|||
use egui::*;
|
||||
use fluent_templates::fluent_bundle::FluentValue;
|
||||
use fluent_templates::{static_loader, Loader};
|
||||
use rfd::FileDialog;
|
||||
use ruffle_core::backend::ui::US_ENGLISH;
|
||||
use ruffle_core::debug_ui::Message as DebugMessage;
|
||||
use ruffle_core::Player;
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use sys_locale::get_locale;
|
||||
use unic_langid::LanguageIdentifier;
|
||||
use winit::event_loop::EventLoopProxy;
|
||||
|
@ -123,6 +125,21 @@ impl RuffleGui {
|
|||
|
||||
if let Some(player) = player {
|
||||
player.show_debug_ui(egui_ctx);
|
||||
for item in player.debug_ui().items_to_save() {
|
||||
std::thread::spawn(move || {
|
||||
if let Some(path) = FileDialog::new()
|
||||
.set_file_name(&item.suggested_name)
|
||||
.save_file()
|
||||
{
|
||||
if let Err(e) = fs::write(&path, item.data) {
|
||||
tracing::error!(
|
||||
"Couldn't save {} to {path:?}: {e}",
|
||||
item.suggested_name,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if !self.context_menu.is_empty() {
|
||||
|
@ -240,13 +257,13 @@ impl RuffleGui {
|
|||
if Button::new(text(&self.locale, "debug-menu-open-stage")).ui(ui).clicked() {
|
||||
ui.close_menu();
|
||||
if let Some(player) = &mut player {
|
||||
player.debug_ui_message(DebugMessage::TrackStage);
|
||||
player.debug_ui().queue_message(DebugMessage::TrackStage);
|
||||
}
|
||||
}
|
||||
if Button::new(text(&self.locale, "debug-menu-open-movie")).ui(ui).clicked() {
|
||||
ui.close_menu();
|
||||
if let Some(player) = &mut player {
|
||||
player.debug_ui_message(DebugMessage::TrackTopLevelMovie);
|
||||
player.debug_ui().queue_message(DebugMessage::TrackTopLevelMovie);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue