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, persistence: Persistence, shortcuts: ShortcutProvider, sim_id: SimId, config: SimConfig, toasts: Toasts, screen: Option, messages: mpsc::Receiver, message_sink: mpsc::Sender, color_picker: Option, file_picker: FilePicker, memory: Arc, images: Arc, mappings: MappingProvider, children: ViewportIdMap, child_states: UiData>, queued_children: Vec, show_player_name: bool, } impl GameWindow { #[expect(clippy::too_many_arguments)] pub fn new( viewport_id: ViewportId, client: EmulatorClient, proxy: EventLoopProxy, persistence: Persistence, shortcuts: ShortcutProvider, memory: &Arc, images: &Arc, 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(&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> { 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 { 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>, title: String, sim_id: Option, updates: Option, close_requested: Arc, } enum AppWrapper { Game(Box), Dyn(Box), } impl AppWrapper { fn of_game(game: GameWindow) -> Self { Self::Game(Box::new(game)) } fn of_dyn(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 }, 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, }