lemur/src/window/game.rs

866 lines
31 KiB
Rust

use std::{
ops::{Deref, DerefMut},
sync::{Arc, Mutex, atomic::AtomicBool, mpsc},
time::Duration,
};
use crate::{
app::UserEvent,
config::{COLOR_PRESETS, SimConfig},
emulator::{EmulatorClient, EmulatorCommand, EmulatorState, SimId, SimState},
filepicker::FilePicker,
images::ImageTextureLoader,
input::{Command, MappingProvider, ShortcutProvider},
memory::MemoryClient,
persistence::Persistence,
window::{
AboutWindow, BgMapWindow, CharacterDataWindow, FrameBufferWindow, GdbServerWindow,
HotkeysWindow, InitArgs, InputWindow, ObjectWindow, ProfileWindow, RegisterWindow,
TerminalWindow, WorldWindow, utils::UiData,
},
};
use anyhow::Context as _;
use egui::{
Align2, Button, CentralPanel, Color32, Context, Frame, MenuBar, Panel, Pos2, Ui, Vec2,
ViewportBuilder, ViewportCommand, ViewportId, ViewportIdMap, Window,
};
use egui_notify::{Anchor, Toast, Toasts};
use serde::{Deserialize, Serialize};
use winit::{event::KeyEvent, event_loop::EventLoopProxy};
use super::{
AppWindow,
game_screen::{DisplayMode, GameScreen},
utils::UiExt as _,
};
pub struct GameWindow {
viewport_id: ViewportId,
client: EmulatorClient,
proxy: EventLoopProxy<UserEvent>,
persistence: Persistence,
shortcuts: ShortcutProvider,
sim_id: SimId,
config: SimConfig,
toasts: Toasts,
screen: Option<GameScreen>,
messages: mpsc::Receiver<Toast>,
message_sink: mpsc::Sender<Toast>,
color_picker: Option<ColorPickerState>,
file_picker: FilePicker,
memory: Arc<MemoryClient>,
images: Arc<ImageTextureLoader>,
mappings: MappingProvider,
children: ViewportIdMap<ChildWindowWrapper>,
child_states: UiData<ViewportIdMap<ChildState>>,
queued_children: Vec<ChildWindow>,
show_player_name: bool,
}
impl GameWindow {
#[expect(clippy::too_many_arguments)]
pub fn new(
viewport_id: ViewportId,
client: EmulatorClient,
proxy: EventLoopProxy<UserEvent>,
persistence: Persistence,
shortcuts: ShortcutProvider,
memory: &Arc<MemoryClient>,
images: &Arc<ImageTextureLoader>,
mappings: MappingProvider,
sim_id: SimId,
) -> Self {
let config = SimConfig::load(&persistence, sim_id);
let toasts = Toasts::new()
.with_anchor(Anchor::BottomLeft)
.with_margin((10.0, 10.0).into())
.reverse(true);
let (message_sink, messages) = mpsc::channel();
Self {
viewport_id,
client,
proxy,
persistence,
shortcuts,
sim_id,
config,
toasts,
screen: None,
messages,
message_sink,
color_picker: None,
file_picker: FilePicker::new(),
memory: memory.clone(),
images: images.clone(),
mappings: mappings.clone(),
children: ViewportIdMap::default(),
child_states: UiData::new(),
queued_children: vec![],
show_player_name: false,
}
}
pub fn open(&mut self, window: ChildWindow) {
// queue opening this child until our next render call,
// so that we get a chance to look up its size
self.queued_children.push(window);
}
pub fn close(&mut self, window: ChildWindow) {
let id = window.viewport_id(self.sim_id);
self.children.remove(&id);
}
fn do_open(&mut self, window: ChildWindow) {
let viewport_id = window.viewport_id(self.sim_id);
if self.children.contains_key(&viewport_id) {
return;
}
let child = match window {
ChildWindow::About => AppWrapper::of_dyn(AboutWindow),
ChildWindow::CharacterData => AppWrapper::of_dyn(CharacterDataWindow::new(
self.sim_id,
&self.memory,
&self.images,
)),
ChildWindow::BgMap => {
AppWrapper::of_dyn(BgMapWindow::new(self.sim_id, &self.memory, &self.images))
}
ChildWindow::Objects => {
AppWrapper::of_dyn(ObjectWindow::new(self.sim_id, &self.memory, &self.images))
}
ChildWindow::Worlds => {
AppWrapper::of_dyn(WorldWindow::new(self.sim_id, &self.memory, &self.images))
}
ChildWindow::FrameBuffers => AppWrapper::of_dyn(FrameBufferWindow::new(
self.sim_id,
&self.memory,
&self.images,
)),
ChildWindow::Registers => {
AppWrapper::of_dyn(RegisterWindow::new(self.sim_id, &self.memory))
}
ChildWindow::Terminal => {
AppWrapper::of_dyn(TerminalWindow::new(self.sim_id, &self.client))
}
ChildWindow::Profiler { launch } => {
let mut profile = ProfileWindow::new(self.sim_id, self.client.clone());
if launch {
profile.launch();
}
AppWrapper::of_dyn(profile)
}
ChildWindow::Debugger { port } => {
let mut debugger =
GdbServerWindow::new(self.sim_id, self.client.clone(), self.proxy.clone());
if let Some(port) = port {
debugger.launch(port);
}
AppWrapper::of_dyn(debugger)
}
ChildWindow::Input => AppWrapper::of_dyn(InputWindow::new(self.mappings.clone())),
ChildWindow::Hotkeys => AppWrapper::of_dyn(HotkeysWindow::new(self.shortcuts.clone())),
ChildWindow::Player2 => {
if self.sim_id == SimId::Player2 {
return;
}
AppWrapper::of_game(GameWindow::new(
viewport_id,
self.client.clone(),
self.proxy.clone(),
self.persistence.clone(),
self.shortcuts.clone(),
&self.memory,
&self.images,
self.mappings.clone(),
SimId::Player2,
))
}
};
let mut viewport = child.initial_viewport();
let title = viewport.title.clone().unwrap();
let sim_id = child.sim_id();
if let Some(sim_id) = sim_id
&& self.show_player_name
{
viewport.title = Some(format!("{title} ({sim_id})"));
}
if let Some(state) = self.child_states.get(&viewport_id) {
viewport.position = Some(state.position);
viewport.inner_size = Some(state.size);
}
self.children.insert(
viewport_id,
ChildWindowWrapper {
app: Arc::new(Mutex::new(child)),
title,
sim_id,
updates: Some(viewport),
close_requested: Arc::new(AtomicBool::new(false)),
},
);
}
pub fn handle_init(&mut self, viewport_id: ViewportId, args: InitArgs) {
self.handle_event(viewport_id, |window| window.on_init(args))
}
pub fn handle_key_event(&mut self, viewport_id: ViewportId, event: &KeyEvent) -> bool {
self.handle_event(viewport_id, |window| window.handle_key_event(event))
}
pub fn handle_gamepad_event(&mut self, viewport_id: ViewportId, event: &gilrs::Event) -> bool {
self.handle_event(viewport_id, |window| window.handle_gamepad_event(event))
}
fn handle_event<F, R>(&mut self, viewport_id: ViewportId, cb: F) -> R
where
F: FnOnce(&mut dyn AppWindow) -> R,
R: Default,
{
let p2_viewport_id = ChildWindow::Player2.viewport_id(SimId::Player1);
if self.viewport_id == viewport_id {
cb(self)
} else if let Some(child) = self.children.get_mut(&viewport_id) {
cb(child.app.lock().unwrap().deref_mut().deref_mut())
} else if let Some(mut p2) = self
.children
.get_mut(&p2_viewport_id)
.map(|w| w.app.lock().unwrap())
{
if let AppWrapper::Game(g) = p2.deref_mut() {
g.handle_event(viewport_id, cb)
} else {
R::default()
}
} else {
R::default()
}
}
fn show_menu(&mut self, ui: &mut Ui) {
let state = self.client.emulator_state();
let is_ready = self.client.sim_state(self.sim_id) == SimState::Ready;
let can_pause = is_ready && state == EmulatorState::Running;
let can_resume = is_ready && state == EmulatorState::Paused;
let can_frame_advance = is_ready && state != EmulatorState::Debugging;
for command in ui.input_mut(|input| self.shortcuts.consume_all(input)) {
match command {
Command::OpenRom => {
let rom = self
.file_picker
.new_dialog()
.add_filter("Virtual Boy ROMs", &["vb", "vbrom", "elf", "isx"])
.pick_file();
if let Some(path) = rom {
self.client
.send_command(EmulatorCommand::LoadGame(self.sim_id, path));
}
}
Command::ReloadRom => {
self.client
.send_command(EmulatorCommand::ReloadRom(self.sim_id));
}
Command::Quit => {
let _ = self.proxy.send_event(UserEvent::Quit(self.sim_id));
}
Command::PauseResume => {
if state == EmulatorState::Paused && can_resume {
self.client.send_command(EmulatorCommand::Resume);
}
if state == EmulatorState::Running && can_pause {
self.client.send_command(EmulatorCommand::Pause);
}
}
Command::Reset => {
if is_ready {
self.client
.send_command(EmulatorCommand::Reset(self.sim_id));
}
}
Command::FrameAdvance => {
if can_frame_advance {
self.client.send_command(EmulatorCommand::FrameAdvance);
}
}
Command::FastForward(speed) => {
self.client
.send_command(EmulatorCommand::SetSpeed(speed as f64));
}
Command::Screenshot => {
let autopause = state == EmulatorState::Running && can_pause;
if autopause {
self.client.send_command(EmulatorCommand::Pause);
}
pollster::block_on(self.take_screenshot());
if autopause {
self.client.send_command(EmulatorCommand::Resume);
}
}
}
}
ui.menu_button("ROM", |ui| {
if ui
.add(self.button_for(ui.ctx(), "Open ROM", Command::OpenRom))
.clicked()
{
let rom = self
.file_picker
.new_dialog()
.add_filter("Virtual Boy ROMs", &["vb", "vbrom", "elf", "isx"])
.pick_file();
if let Some(path) = rom {
self.client
.send_command(EmulatorCommand::LoadGame(self.sim_id, path));
}
}
if ui
.add(self.button_for(ui.ctx(), "Reload ROM", Command::ReloadRom))
.clicked()
{
self.client
.send_command(EmulatorCommand::ReloadRom(self.sim_id));
}
let watch_rom = self.client.is_rom_watched(self.sim_id);
if ui.selectable_button(watch_rom, "Watch ROM").clicked() {
self.client
.send_command(EmulatorCommand::WatchRom(self.sim_id, !watch_rom));
}
if ui
.add(self.button_for(ui.ctx(), "Quit", Command::Quit))
.clicked()
{
let _ = self.proxy.send_event(UserEvent::Quit(self.sim_id));
}
});
ui.menu_button("Emulation", |ui| {
if state == EmulatorState::Running {
if ui
.add_enabled(
can_pause,
self.button_for(ui.ctx(), "Pause", Command::PauseResume),
)
.clicked()
{
self.client.send_command(EmulatorCommand::Pause);
}
} else if ui
.add_enabled(
can_resume,
self.button_for(ui.ctx(), "Resume", Command::PauseResume),
)
.clicked()
{
self.client.send_command(EmulatorCommand::Resume);
}
if ui
.add_enabled(is_ready, self.button_for(ui.ctx(), "Reset", Command::Reset))
.clicked()
{
self.client
.send_command(EmulatorCommand::Reset(self.sim_id));
}
ui.separator();
if ui
.add_enabled(
can_frame_advance,
self.button_for(ui.ctx(), "Frame Advance", Command::FrameAdvance),
)
.clicked()
{
self.client.send_command(EmulatorCommand::FrameAdvance);
}
ui.separator();
if ui
.add_enabled(
is_ready,
self.button_for(ui.ctx(), "Screenshot", Command::Screenshot),
)
.clicked()
{
pollster::block_on(self.take_screenshot());
}
});
ui.menu_button("Options", |ui| self.show_options_menu(ui));
ui.menu_button("Multiplayer", |ui| {
let has_player_2 = self.client.has_player_2();
if self.sim_id == SimId::Player1
&& !has_player_2
&& ui.button("Open Player 2").clicked()
{
self.client
.send_command(EmulatorCommand::StartSecondSim(None));
self.open(ChildWindow::Player2);
}
if has_player_2 {
let linked = self.client.are_sims_linked();
if linked && ui.button("Unlink").clicked() {
self.client.send_command(EmulatorCommand::Unlink);
}
if !linked && ui.button("Link").clicked() {
self.client.send_command(EmulatorCommand::Link);
}
}
});
ui.menu_button("Tools", |ui| {
if ui.button("Terminal").clicked() {
self.open(ChildWindow::Terminal);
}
if ui.button("Profiler").clicked() {
self.open(ChildWindow::Profiler { launch: false });
}
if ui.button("GDB Server").clicked() {
self.open(ChildWindow::Debugger { port: None });
}
ui.separator();
if ui.button("Character Data").clicked() {
self.open(ChildWindow::CharacterData);
}
if ui.button("Background Maps").clicked() {
self.open(ChildWindow::BgMap);
}
if ui.button("Objects").clicked() {
self.open(ChildWindow::Objects);
}
if ui.button("Worlds").clicked() {
self.open(ChildWindow::Worlds);
}
if ui.button("Frame Buffers").clicked() {
self.open(ChildWindow::FrameBuffers);
}
if ui.button("Registers").clicked() {
self.open(ChildWindow::Registers);
}
});
ui.menu_button("Help", |ui| {
if ui.button("About").clicked() {
self.open(ChildWindow::About);
}
});
}
async fn take_screenshot(&mut self) {
match self.try_take_screenshot().await {
Ok(Some(path)) => {
let mut toast = Toast::info(format!("Saved to {path}"));
toast.duration(Some(Duration::from_secs(5)));
self.toasts.add(toast);
}
Ok(None) => {}
Err(error) => {
let mut toast = Toast::error(format!("{error:#}"));
toast.duration(Some(Duration::from_secs(5)));
self.toasts.add(toast);
}
}
}
async fn try_take_screenshot(&self) -> anyhow::Result<Option<String>> {
let (tx, rx) = oneshot::channel();
self.client
.send_command(EmulatorCommand::Screenshot(self.sim_id, tx));
let bytes = rx.await.context("Could not take screenshot")?;
let file = self
.file_picker
.new_dialog()
.add_filter("PNG images", &["png"])
.set_file_name("screenshot.png")
.save_file();
let Some(path) = file else {
return Ok(None);
};
if bytes.len() != 384 * 224 * 2 {
anyhow::bail!("Unexpected screenshot size");
}
let mut screencap = image::GrayImage::new(384 * 2, 224);
for (index, pixel) in bytes.into_iter().enumerate() {
let x = (index / 2) % 384 + ((index % 2) * 384);
let y = (index / 2) / 384;
screencap.put_pixel(x as u32, y as u32, image::Luma([pixel]));
}
screencap.save(&path).context("Could not save screenshot")?;
Ok(Some(path.display().to_string()))
}
fn show_options_menu(&mut self, ui: &mut Ui) {
ui.menu_button("Video", |ui| {
ui.menu_button("Screen Size", |ui| {
let current_dims = self.config.dimensions;
for scale in 1..=4 {
let label = format!("x{scale}");
let scale = scale as f32;
let dims = {
let Vec2 { x, y } = self.config.display_mode.proportions();
Vec2::new(x * scale, y * scale + 22.0)
};
if ui
.selectable_button((current_dims - dims).length() < 1.0, label)
.clicked()
{
ui.send_viewport_cmd(ViewportCommand::InnerSize(dims));
}
}
});
ui.menu_button("Display Mode", |ui| {
let old_proportions = self.config.display_mode.proportions();
let mut changed = false;
let mut display_mode = self.config.display_mode;
changed |= ui
.selectable_option(&mut display_mode, DisplayMode::Anaglyph, "Anaglyph")
.clicked();
changed |= ui
.selectable_option(&mut display_mode, DisplayMode::LeftEye, "Left Eye")
.clicked();
changed |= ui
.selectable_option(&mut display_mode, DisplayMode::RightEye, "Right Eye")
.clicked();
changed |= ui
.selectable_option(&mut display_mode, DisplayMode::SideBySide, "Side by Side")
.clicked();
if !changed {
return;
}
let current_dims = self.config.dimensions;
let new_proportions = display_mode.proportions();
let scale = new_proportions / old_proportions;
if scale != Vec2::new(1.0, 1.0) {
ui.send_viewport_cmd(ViewportCommand::InnerSize(current_dims * scale));
}
self.update_config(|c| {
c.display_mode = display_mode;
c.dimensions = current_dims * scale;
});
});
ui.menu_button("Colors", |ui| {
for preset in COLOR_PRESETS {
if ui.color_pair_button(preset[0], preset[1]).clicked() {
self.update_config(|c| c.colors = preset);
}
}
ui.with_layout(ui.layout().with_cross_align(egui::Align::Center), |ui| {
if ui.button("Custom").clicked() {
let color_str = |color: Color32| {
format!("{:02x}{:02x}{:02x}", color.r(), color.g(), color.b())
};
let is_running = self.client.emulator_state() == EmulatorState::Running;
if is_running {
self.client.send_command(EmulatorCommand::Pause);
}
let color_codes = [
color_str(self.config.colors[0]),
color_str(self.config.colors[1]),
];
self.color_picker = Some(ColorPickerState {
color_codes,
just_opened: true,
unpause_on_close: is_running,
});
}
});
});
});
ui.menu_button("Audio", |ui| {
if ui
.selectable_button(self.config.audio_enabled, "Enabled")
.clicked()
{
self.update_config(|c| c.audio_enabled = !c.audio_enabled);
self.client.send_command(EmulatorCommand::SetAudioEnabled(
self.sim_id,
self.config.audio_enabled,
));
}
});
ui.menu_button("Input", |ui| {
if ui.button("Bind Inputs").clicked() {
self.open(ChildWindow::Input);
}
});
if ui.button("Hotkeys").clicked() {
self.open(ChildWindow::Hotkeys);
}
}
fn show_color_picker(&mut self, ui: &mut Ui) {
let mut colors = self.config.colors;
let Some(state) = self.color_picker.as_mut() else {
return;
};
let (open, updated) = ui
.horizontal(|ui| {
let left_color = ui.color_picker(&mut colors[0], &mut state.color_codes[0]);
if state.just_opened {
left_color.request_focus();
state.just_opened = false;
}
let right_color = ui.color_picker(&mut colors[1], &mut state.color_codes[1]);
let open = left_color.has_focus() || right_color.has_focus();
let updated = left_color.changed() || right_color.changed();
(open, updated)
})
.inner;
if !open {
if state.unpause_on_close {
self.client.send_command(EmulatorCommand::Resume);
}
self.color_picker = None;
}
if updated {
self.update_config(|c| c.colors = colors);
}
}
fn update_config(&mut self, update: impl FnOnce(&mut SimConfig)) {
let mut new_config = self.config.clone();
update(&mut new_config);
if self.config != new_config {
let _ = new_config.save(&self.persistence, self.sim_id);
}
self.config = new_config;
}
fn button_for(&self, ctx: &Context, text: &str, command: Command) -> Button<'_> {
let button = Button::new(text);
match self.shortcuts.shortcut_for(command) {
Some(shortcut) => button.shortcut_text(ctx.format_shortcut(&shortcut)),
None => button,
}
}
}
impl AppWindow for GameWindow {
fn sim_id(&self) -> Option<SimId> {
Some(self.sim_id)
}
fn initial_viewport(&self) -> ViewportBuilder {
let builder = ViewportBuilder::default()
.with_title("Lemur")
.with_inner_size(self.config.dimensions);
if let Some(position) = self.config.position {
builder.with_position(position)
} else {
builder
}
}
fn show(&mut self, ui: &mut Ui) {
let has_player_2 = self.client.has_player_2();
if !self.show_player_name && has_player_2 {
ui.send_viewport_cmd(ViewportCommand::Title(format!("Lemur ({})", self.sim_id)));
for (viewport_id, child) in &self.children {
if let Some(sim_id) = child.sim_id {
ui.send_viewport_cmd_to(
*viewport_id,
ViewportCommand::Title(format!("{} ({sim_id})", child.title)),
);
}
}
self.show_player_name = true;
} else if self.show_player_name && !has_player_2 {
ui.send_viewport_cmd(ViewportCommand::Title("Lemur".to_string()));
for (viewport_id, child) in &self.children {
if child.sim_id.is_some() {
ui.send_viewport_cmd_to(
*viewport_id,
ViewportCommand::Title(child.title.clone()),
);
}
}
self.show_player_name = false;
}
self.child_states.load(ui);
let dimensions = {
let bounds = ui.content_rect();
bounds.max - bounds.min
};
let position = ui.input(|i| i.viewport().outer_rect.map(|r| r.min));
self.update_config(|c| {
c.dimensions = dimensions;
c.position = position;
});
while let Ok(toast) = self.messages.try_recv() {
self.toasts.add(toast);
}
Panel::top("menubar")
.exact_size(22.0)
.show_inside(ui, |ui| {
MenuBar::new().ui(ui, |ui| {
self.show_menu(ui);
});
});
if self.color_picker.is_some() {
Window::new("Color Picker")
.title_bar(false)
.resizable(false)
.anchor(Align2::CENTER_CENTER, Vec2::ZERO)
.show(ui, |ui| {
self.show_color_picker(ui);
});
}
let frame = Frame::central_panel(ui.style()).fill(Color32::BLACK);
CentralPanel::default().frame(frame).show_inside(ui, |ui| {
if let Some(screen) = self.screen.as_mut() {
screen.update(self.config.display_mode, self.config.colors);
ui.add(screen);
}
});
self.toasts.show(ui);
for window in std::mem::take(&mut self.queued_children) {
self.do_open(window);
}
self.children.retain(|id, child| {
if child
.close_requested
.load(std::sync::atomic::Ordering::Relaxed)
{
return false;
}
let app = child.app.clone();
let viewport_builder = child.updates.take().unwrap_or_default();
let close_requested = child.close_requested.clone();
if let (Some(outer), Some(inner)) = ui.input_for(*id, |inp| {
let viewport = inp.viewport();
(viewport.outer_rect, viewport.inner_rect)
}) {
self.child_states.insert(
*id,
ChildState {
position: outer.min,
size: inner.size(),
},
);
}
ui.show_viewport_deferred(*id, viewport_builder, move |ui, _| {
app.lock().unwrap().show(ui);
if ui.input(|s| s.viewport().close_requested()) {
close_requested.store(true, std::sync::atomic::Ordering::Relaxed);
}
});
true
});
self.child_states.save(ui);
ui.request_repaint_after(Duration::from_millis(10));
}
fn on_init(&mut self, args: InitArgs) {
if self.screen.is_none() {
let (screen, sink) = GameScreen::init(args.render_state);
self.client.send_command(EmulatorCommand::ConnectToSim(
self.sim_id,
sink,
self.message_sink.clone(),
));
self.screen = Some(screen);
}
self.file_picker.set_window(args.window);
}
}
impl Drop for GameWindow {
fn drop(&mut self) {
if self.sim_id == SimId::Player2 {
self.client.send_command(EmulatorCommand::StopSecondSim);
}
let _ = self.proxy.send_event(UserEvent::Quit(self.sim_id));
}
}
struct ColorPickerState {
color_codes: [String; 2],
just_opened: bool,
unpause_on_close: bool,
}
struct ChildWindowWrapper {
app: Arc<Mutex<AppWrapper>>,
title: String,
sim_id: Option<SimId>,
updates: Option<ViewportBuilder>,
close_requested: Arc<AtomicBool>,
}
enum AppWrapper {
Game(Box<GameWindow>),
Dyn(Box<dyn AppWindow + Send + 'static>),
}
impl AppWrapper {
fn of_game(game: GameWindow) -> Self {
Self::Game(Box::new(game))
}
fn of_dyn<T: AppWindow + Send + 'static>(inner: T) -> Self {
Self::Dyn(Box::new(inner))
}
}
impl Deref for AppWrapper {
type Target = dyn AppWindow + Send + 'static;
fn deref(&self) -> &Self::Target {
match self {
Self::Dyn(inner) => inner.as_ref(),
Self::Game(inner) => inner.as_ref(),
}
}
}
impl DerefMut for AppWrapper {
fn deref_mut(&mut self) -> &mut Self::Target {
match self {
Self::Dyn(inner) => inner.as_mut(),
Self::Game(inner) => inner.as_mut(),
}
}
}
#[derive(Debug)]
pub enum ChildWindow {
About,
CharacterData,
BgMap,
Objects,
Worlds,
FrameBuffers,
Registers,
Terminal,
Profiler { launch: bool },
Debugger { port: Option<u16> },
Input,
Hotkeys,
Player2,
}
impl ChildWindow {
fn viewport_id(&self, sim_id: SimId) -> ViewportId {
ViewportId::from_hash_of(format!("{sim_id:?}{}", self.name()))
}
fn name(&self) -> &'static str {
match self {
Self::About => "About",
Self::CharacterData => "CharacterData",
Self::BgMap => "BgMap",
Self::Objects => "Objects",
Self::Worlds => "Worlds",
Self::FrameBuffers => "FrameBuffers",
Self::Registers => "Registers",
Self::Terminal => "Terminal",
Self::Profiler { .. } => "Profiler",
Self::Debugger { .. } => "Debugger",
Self::Input => "Input",
Self::Hotkeys => "Hotkeys",
Self::Player2 => "Player2",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
struct ChildState {
position: Pos2,
size: Vec2,
}