diff --git a/tetanes/src/nes.rs b/tetanes/src/nes.rs index b3614eb4..6a89ab34 100644 --- a/tetanes/src/nes.rs +++ b/tetanes/src/nes.rs @@ -166,7 +166,10 @@ impl Nes { /// /// If GPU resources failed to be requested, the emulation or renderer fails to build, then an /// error is returned. - pub(crate) fn init_running(&mut self) -> anyhow::Result<()> { + pub(crate) fn init_running( + &mut self, + event_loop: &EventLoopWindowTarget, + ) -> anyhow::Result<()> { match std::mem::take(&mut self.state) { State::Pending { ctx, @@ -191,7 +194,7 @@ impl Nes { cfg.input.update_gamepad_assignments(&gamepads); let emulation = Emulation::new(tx.clone(), frame_tx.clone(), &cfg)?; - let renderer = Renderer::new(tx.clone(), resources, frame_rx, &cfg)?; + let renderer = Renderer::new(tx.clone(), event_loop, resources, frame_rx, &cfg)?; let mut running = Running { cfg, diff --git a/tetanes/src/nes/event.rs b/tetanes/src/nes/event.rs index 06aae9d1..9df5a63f 100644 --- a/tetanes/src/nes/event.rs +++ b/tetanes/src/nes/event.rs @@ -265,7 +265,7 @@ impl Nes { } Event::UserEvent(event) => match event { NesEvent::Renderer(RendererEvent::ResourcesReady) => { - if let Err(err) = self.init_running() { + if let Err(err) = self.init_running(event_loop) { error!("failed to create window: {err:?}"); event_loop.exit(); return; @@ -585,7 +585,7 @@ impl Running { ConfigEvent::ZapperConnected(connected) => deck.zapper = *connected, } - self.renderer.update(&self.gamepads, &self.cfg); + self.renderer.prepare(&self.gamepads, &self.cfg); } NesEvent::Renderer(RendererEvent::RequestRedraw { viewport_id, when }) => { if let Some(window_id) = self.renderer.window_id_for_viewport(*viewport_id) @@ -790,7 +790,7 @@ impl Running { } } - self.renderer.update(&self.gamepads, &self.cfg); + self.renderer.prepare(&self.gamepads, &self.cfg); } /// Handle user input mapped to key bindings. diff --git a/tetanes/src/nes/renderer.rs b/tetanes/src/nes/renderer.rs index fcbd2d9f..a77e7f5b 100644 --- a/tetanes/src/nes/renderer.rs +++ b/tetanes/src/nes/renderer.rs @@ -16,9 +16,9 @@ use crate::{ }; use anyhow::Context; use egui::{ - ahash::HashMap, DeferredViewportUiCallback, SystemTheme, Vec2, ViewportBuilder, ViewportClass, - ViewportCommand, ViewportId, ViewportIdMap, ViewportIdPair, ViewportIdSet, ViewportInfo, - ViewportOutput, WindowLevel, + ahash::HashMap, DeferredViewportUiCallback, ImmediateViewport, SystemTheme, Vec2, + ViewportBuilder, ViewportClass, ViewportCommand, ViewportId, ViewportIdMap, ViewportIdPair, + ViewportIdSet, ViewportInfo, ViewportOutput, WindowLevel, }; use egui_wgpu::{winit::Painter, RenderState}; use egui_winit::EventResponse; @@ -34,7 +34,7 @@ use thingbuf::{ mpsc::{blocking::Receiver as BufReceiver, errors::TryRecvError}, Recycle, }; -use tracing::{debug, error, info}; +use tracing::{debug, error, info, warn}; use winit::{ dpi::{LogicalSize, PhysicalSize}, event::WindowEvent, @@ -158,6 +158,7 @@ impl Renderer { /// Initializes the renderer in a platform-agnostic way. pub fn new( tx: NesEventProxy, + event_loop: &EventLoopWindowTarget, resources: Resources, frame_rx: BufReceiver, cfg: &Config, @@ -253,6 +254,27 @@ impl Renderer { focused: Some(ViewportId::ROOT), })); + { + let tx = tx.clone(); + let state = Rc::downgrade(&state); + let event_loop: *const EventLoopWindowTarget = event_loop; + egui::Context::set_immediate_viewport_renderer(move |ctx, viewport| { + if let Some(state) = state.upgrade() { + // SAFETY: the event loop lives longer than the Rcs we just upgraded above. + match unsafe { event_loop.as_ref() } { + Some(event_loop) => { + Self::render_immediate_viewport(&tx, event_loop, ctx, &state, viewport); + } + None => tracing::error!( + "failed to get event_loop in set_immediate_viewport_renderer" + ), + } + } else { + warn!("set_immediate_viewport_renderer called after window closed"); + } + }); + } + if let Err(err) = Self::load(&ctx, cfg) { tracing::error!("{err:?}"); } @@ -438,12 +460,14 @@ impl Renderer { self.ctx .set_embed_viewports(*fullscreen || cfg.renderer.embed_viewports); } - self.ctx - .send_viewport_cmd_to(ViewportId::ROOT, ViewportCommand::Focus); - self.ctx.send_viewport_cmd_to( - ViewportId::ROOT, - ViewportCommand::Fullscreen(*fullscreen), - ); + if self.fullscreen() != *fullscreen { + self.ctx + .send_viewport_cmd_to(ViewportId::ROOT, ViewportCommand::Focus); + self.ctx.send_viewport_cmd_to( + ViewportId::ROOT, + ViewportCommand::Fullscreen(*fullscreen), + ); + } } ConfigEvent::Region(_) | ConfigEvent::HideOverscan(_) | ConfigEvent::Scale(_) => { self.resize_texture = true; @@ -942,6 +966,103 @@ impl Renderer { } } + fn render_immediate_viewport( + tx: &NesEventProxy, + event_loop: &EventLoopWindowTarget, + ctx: &egui::Context, + state: &RefCell, + immediate_viewport: ImmediateViewport<'_>, + ) { + #[cfg(feature = "profiling")] + puffin::profile_function!(); + + let ImmediateViewport { + ids, + builder, + viewport_ui_cb, + } = immediate_viewport; + + let input = { + let State { + viewports, + painter, + viewport_from_window, + .. + } = &mut *state.borrow_mut(); + + let viewport = Self::create_or_update_viewport( + ctx, + viewports, + ids, + ViewportClass::Immediate, + builder, + None, + None, + ); + + if viewport.window.is_none() { + viewport.initialize_window( + tx.clone(), + event_loop, + ctx, + viewport_from_window, + painter, + ); + } + + match (&viewport.window, &mut viewport.egui_state) { + (Some(window), Some(egui_state)) => { + egui_winit::update_viewport_info(&mut viewport.info, ctx, window); + + let mut input = egui_state.take_egui_input(window); + input.viewports = viewports + .iter() + .map(|(id, viewport)| (*id, viewport.info.clone())) + .collect(); + input + } + _ => return, + } + }; + + let output = ctx.run(input, |ctx| { + viewport_ui_cb(ctx); + }); + let viewport_id = ids.this; + let State { + viewports, + painter, + focused, + .. + } = &mut *state.borrow_mut(); + + if let Some(viewport) = viewports.get_mut(&viewport_id) { + viewport.info.events.clear(); + + if let (Some(window), Some(egui_state)) = (&viewport.window, &mut viewport.egui_state) { + Renderer::set_painter_window( + tx.clone(), + Rc::clone(painter), + viewport_id, + Some(Arc::clone(window)), + ); + + let clipped_primitives = ctx.tessellate(output.shapes, output.pixels_per_point); + painter.borrow_mut().paint_and_update_textures( + viewport_id, + output.pixels_per_point, + [0.0; 4], + &clipped_primitives, + &output.textures_delta, + false, + ); + + egui_state.handle_platform_output(window, output.platform_output); + Self::handle_viewport_output(ctx, viewports, output.viewport_output, *focused); + }; + }; + } + pub fn process_input( ctx: &egui::Context, state: &Rc>, @@ -992,8 +1113,8 @@ impl Renderer { EventResponse::default() } - pub fn update(&mut self, gamepads: &Gamepads, cfg: &Config) { - self.gui.borrow_mut().update(gamepads, cfg); + pub fn prepare(&mut self, gamepads: &Gamepads, cfg: &Config) { + self.gui.borrow_mut().prepare(gamepads, cfg); self.ctx.request_repaint(); } @@ -1025,7 +1146,7 @@ impl Renderer { #[cfg(feature = "profiling")] puffin::GlobalProfiler::lock().new_frame(); - self.gui.borrow_mut().update(gamepads, cfg); + self.gui.borrow_mut().prepare(gamepads, cfg); self.handle_resize(viewport_id, cfg); @@ -1039,7 +1160,11 @@ impl Renderer { return Ok(()); }; - if viewport.occluded { + if viewport.occluded + || (viewport_id != ViewportId::ROOT && viewport.viewport_ui_cb.is_none()) + { + // This will only happen if this is an immediate viewport. + // That means that the viewport cannot be rendered by itself and needs his parent to be rendered. return Ok(()); } @@ -1105,6 +1230,9 @@ impl Renderer { }); { + // Required to get mutable reference again to avoid double borrow when calling gui.ui + // above because internally gui.ui calls show_viewport_immediate, which requires + // borrowing state to render let State { viewports, painter, diff --git a/tetanes/src/nes/renderer/gui.rs b/tetanes/src/nes/renderer/gui.rs index 14034a2c..c7da2399 100644 --- a/tetanes/src/nes/renderer/gui.rs +++ b/tetanes/src/nes/renderer/gui.rs @@ -288,10 +288,11 @@ impl Gui { region.aspect_ratio() } - pub fn update(&mut self, gamepads: &Gamepads, cfg: &Config) { + pub fn prepare(&mut self, gamepads: &Gamepads, cfg: &Config) { self.cfg = cfg.clone(); self.preferences.prepare(&self.cfg); self.keybinds.prepare(gamepads, &self.cfg); + self.ppu_viewer.prepare(&self.cfg); } /// Create the UI. diff --git a/tetanes/src/nes/renderer/gui/keybinds.rs b/tetanes/src/nes/renderer/gui/keybinds.rs index 1ec0e211..18d66b55 100644 --- a/tetanes/src/nes/renderer/gui/keybinds.rs +++ b/tetanes/src/nes/renderer/gui/keybinds.rs @@ -5,8 +5,9 @@ use crate::nes::{ input::{Gamepads, Input}, renderer::gui::lib::ViewportOptions, }; +use cfg_if::cfg_if; use egui::{Align2, Button, CentralPanel, Context, Grid, ScrollArea, Ui, Vec2, ViewportClass}; -use parking_lot::RwLock; +use parking_lot::Mutex; use std::sync::{ atomic::{AtomicBool, Ordering}, Arc, @@ -36,7 +37,7 @@ pub struct State { #[must_use] pub struct Keybinds { open: Arc, - state: Arc>, + state: Arc>, resources: Option<(Config, GamepadState)>, } @@ -64,10 +65,12 @@ pub struct ConnectedGamepad { } impl Keybinds { + const TITLE: &'static str = "Keybinds"; + pub fn new(tx: NesEventProxy) -> Self { Self { open: Arc::new(AtomicBool::new(false)), - state: Arc::new(RwLock::new(State { + state: Arc::new(Mutex::new(State { tx, tab: Tab::default(), pending_input: None, @@ -78,7 +81,7 @@ impl Keybinds { } pub fn wants_input(&self) -> bool { - let state = self.state.read(); + let state = self.state.lock(); state.pending_input.is_some() || state.gamepad_unassign_confirm.is_some() } @@ -122,7 +125,7 @@ impl Keybinds { pub fn show(&mut self, ctx: &Context, opts: ViewportOptions) { if !self.open() { - let mut state = self.state.write(); + let mut state = self.state.lock(); state.pending_input = None; state.gamepad_unassign_confirm = None; return; @@ -131,8 +134,7 @@ impl Keybinds { #[cfg(feature = "profiling")] puffin::profile_function!(); - let title = "Keybinds"; - let mut viewport_builder = egui::ViewportBuilder::default().with_title(title); + let mut viewport_builder = egui::ViewportBuilder::default().with_title(Self::TITLE); if opts.always_on_top { viewport_builder = viewport_builder.with_always_on_top(); } @@ -144,28 +146,62 @@ impl Keybinds { return; }; - ctx.show_viewport_deferred( - egui::ViewportId::from_hash_of("keybinds"), - viewport_builder, - move |ctx, class| { - if class == ViewportClass::Embedded { - let mut window_open = open.load(Ordering::Acquire); - egui::Window::new("Keybinds") - .open(&mut window_open) - .show(ctx, |ui| { - state.write().ui(ui, opts.enabled, &cfg, &gamepad_state); - }); - open.store(window_open, Ordering::Release); - } else { - CentralPanel::default().show(ctx, |ui| { - state.write().ui(ui, opts.enabled, &cfg, &gamepad_state); + let viewport_id = egui::ViewportId::from_hash_of("keybinds"); + fn viewport_cb( + ctx: &Context, + class: ViewportClass, + open: &Arc, + enabled: bool, + state: &Arc>, + cfg: &Config, + gamepad_state: &GamepadState, + ) { + if class == ViewportClass::Embedded { + let mut window_open = open.load(Ordering::Acquire); + egui::Window::new(Keybinds::TITLE) + .open(&mut window_open) + .default_rect(ctx.available_rect()) + .show(ctx, |ui| { + state.lock().ui(ui, enabled, cfg, gamepad_state); }); - if ctx.input(|i| i.viewport().close_requested()) { - open.store(false, Ordering::Release); - } + open.store(window_open, Ordering::Release); + } else { + CentralPanel::default().show(ctx, |ui| { + state.lock().ui(ui, enabled, cfg, gamepad_state); + }); + if ctx.input(|i| i.viewport().close_requested()) { + open.store(false, Ordering::Release); } - }, - ); + } + } + + cfg_if! { + if #[cfg(target_arch = "wasm32")] { + ctx.show_viewport_immediate(viewport_id, viewport_builder, move |ctx, class| { + viewport_cb( + ctx, + class, + &open, + opts.enabled, + &state, + &cfg, + &gamepad_state, + ); + }); + } else { + ctx.show_viewport_deferred(viewport_id, viewport_builder, move |ctx, class| { + viewport_cb( + ctx, + class, + &open, + opts.enabled, + &state, + &cfg, + &gamepad_state, + ); + }); + } + } } } diff --git a/tetanes/src/nes/renderer/gui/ppu_viewer.rs b/tetanes/src/nes/renderer/gui/ppu_viewer.rs index bc7b8ede..6f535e6d 100644 --- a/tetanes/src/nes/renderer/gui/ppu_viewer.rs +++ b/tetanes/src/nes/renderer/gui/ppu_viewer.rs @@ -1,10 +1,12 @@ -use crate::nes::{event::NesEventProxy, renderer::gui::lib::ViewportOptions}; +use crate::nes::{config::Config, event::NesEventProxy, renderer::gui::lib::ViewportOptions}; +use cfg_if::cfg_if; use egui::{CentralPanel, Context, Ui, ViewportClass}; -use parking_lot::RwLock; +use parking_lot::Mutex; use std::sync::{ atomic::{AtomicBool, Ordering}, Arc, }; +use tracing::warn; #[derive(Debug)] #[must_use] @@ -17,7 +19,8 @@ pub struct State { #[must_use] pub struct PpuViewer { open: Arc, - state: Arc>, + state: Arc>, + resources: Option, } #[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] @@ -30,13 +33,16 @@ pub enum Tab { } impl PpuViewer { + const TITLE: &'static str = "PPU Viewer"; + pub fn new(tx: NesEventProxy) -> Self { Self { open: Arc::new(AtomicBool::new(false)), - state: Arc::new(RwLock::new(State { + state: Arc::new(Mutex::new(State { _tx: tx, tab: Tab::default(), })), + resources: None, } } @@ -54,6 +60,10 @@ impl PpuViewer { .fetch_update(Ordering::Release, Ordering::Acquire, |open| Some(!open)); } + pub fn prepare(&mut self, cfg: &Config) { + self.resources = Some(cfg.clone()); + } + pub fn show(&mut self, ctx: &Context, opts: ViewportOptions) { if !self.open.load(Ordering::Relaxed) { return; @@ -62,38 +72,57 @@ impl PpuViewer { #[cfg(feature = "profiling")] puffin::profile_function!(); - let title = "PPU Viewer"; - let mut viewport_builder = egui::ViewportBuilder::default().with_title(title); + let mut viewport_builder = egui::ViewportBuilder::default().with_title(Self::TITLE); if opts.always_on_top { viewport_builder = viewport_builder.with_always_on_top(); } let open = Arc::clone(&self.open); let state = Arc::clone(&self.state); - - ctx.show_viewport_deferred( - egui::ViewportId::from_hash_of("ppu_viewer"), - viewport_builder, - move |ctx, class| { - if class == ViewportClass::Embedded { - let mut window_open = open.load(Ordering::Acquire); - egui::Window::new(title) - .open(&mut window_open) - .show(ctx, |ui| state.write().ui(ui, opts.enabled)); - open.store(window_open, Ordering::Release); - } else { - CentralPanel::default().show(ctx, |ui| state.write().ui(ui, opts.enabled)); - if ctx.input(|i| i.viewport().close_requested()) { - open.store(false, Ordering::Release); - } + let Some(cfg) = self.resources.take() else { + warn!("PpuViewer::prepare was not called with required resources"); + return; + }; + + let viewport_id = egui::ViewportId::from_hash_of("ppu_viewer"); + fn viewport_cb( + ctx: &Context, + class: ViewportClass, + open: &Arc, + enabled: bool, + state: &Arc>, + cfg: &Config, + ) { + if class == ViewportClass::Embedded { + let mut window_open = open.load(Ordering::Acquire); + egui::Window::new(PpuViewer::TITLE) + .open(&mut window_open) + .show(ctx, |ui| state.lock().ui(ui, enabled, cfg)); + open.store(window_open, Ordering::Release); + } else { + CentralPanel::default().show(ctx, |ui| state.lock().ui(ui, enabled, cfg)); + if ctx.input(|i| i.viewport().close_requested()) { + open.store(false, Ordering::Release); } - }, - ); + } + } + + cfg_if! { + if #[cfg(target_arch = "wasm32")] { + ctx.show_viewport_immediate(viewport_id, viewport_builder, move |ctx, class| { + viewport_cb(ctx, class, &open, opts.enabled, &state, &cfg); + }); + } else { + ctx.show_viewport_deferred(viewport_id, viewport_builder, move |ctx, class| { + viewport_cb(ctx, class, &open, opts.enabled, &state, &cfg); + }); + } + } } } impl State { - fn ui(&mut self, ui: &mut Ui, enabled: bool) { + fn ui(&mut self, ui: &mut Ui, enabled: bool, _cfg: &Config) { #[cfg(feature = "profiling")] puffin::profile_function!(); diff --git a/tetanes/src/nes/renderer/gui/preferences.rs b/tetanes/src/nes/renderer/gui/preferences.rs index bf4b8ddc..6a8366ab 100644 --- a/tetanes/src/nes/renderer/gui/preferences.rs +++ b/tetanes/src/nes/renderer/gui/preferences.rs @@ -12,11 +12,12 @@ use crate::{ }, platform, }; +use cfg_if::cfg_if; use egui::{ Align, CentralPanel, Checkbox, Context, CursorIcon, DragValue, Grid, Key, Layout, ScrollArea, Slider, TextEdit, Ui, Vec2, ViewportClass, }; -use parking_lot::RwLock; +use parking_lot::Mutex; use std::sync::{ atomic::{AtomicBool, Ordering}, Arc, @@ -40,7 +41,7 @@ pub struct State { #[must_use] pub struct Preferences { open: Arc, - state: Arc>, + state: Arc>, resources: Option, } @@ -66,10 +67,12 @@ impl GenieEntry { } impl Preferences { + const TITLE: &'static str = "Preferences"; + pub fn new(tx: NesEventProxy) -> Self { Self { open: Arc::new(AtomicBool::new(false)), - state: Arc::new(RwLock::new(State { + state: Arc::new(Mutex::new(State { tx, tab: Tab::default(), genie_entry: GenieEntry::default(), @@ -104,8 +107,7 @@ impl Preferences { #[cfg(feature = "profiling")] puffin::profile_function!(); - let title = "Preferences"; - let mut viewport_builder = egui::ViewportBuilder::default().with_title(title); + let mut viewport_builder = egui::ViewportBuilder::default().with_title(Self::TITLE); if opts.always_on_top { viewport_builder = viewport_builder.with_always_on_top(); } @@ -117,29 +119,45 @@ impl Preferences { return; }; - ctx.show_viewport_deferred( - egui::ViewportId::from_hash_of("preferences"), - viewport_builder, - move |ctx, class| { - if class == ViewportClass::Embedded { - let mut window_open = open.load(Ordering::Acquire); - egui::Window::new(title) - .open(&mut window_open) - .show(ctx, |ui| state.write().ui(ui, opts.enabled, &cfg)); - open.store(window_open, Ordering::Release); - } else { - CentralPanel::default() - .show(ctx, |ui| state.write().ui(ui, opts.enabled, &cfg)); - if ctx.input(|i| i.viewport().close_requested()) { - open.store(false, Ordering::Release); - } + let viewport_id = egui::ViewportId::from_hash_of("preferences"); + fn viewport_cb( + ctx: &Context, + class: ViewportClass, + open: &Arc, + enabled: bool, + state: &Arc>, + cfg: &Config, + ) { + if class == ViewportClass::Embedded { + let mut window_open = open.load(Ordering::Acquire); + egui::Window::new(Preferences::TITLE) + .open(&mut window_open) + .default_rect(ctx.available_rect()) + .show(ctx, |ui| state.lock().ui(ui, enabled, cfg)); + open.store(window_open, Ordering::Release); + } else { + CentralPanel::default().show(ctx, |ui| state.lock().ui(ui, enabled, cfg)); + if ctx.input(|i| i.viewport().close_requested()) { + open.store(false, Ordering::Release); } - }, - ); + } + } + + cfg_if! { + if #[cfg(target_arch = "wasm32")] { + ctx.show_viewport_immediate(viewport_id, viewport_builder, move |ctx, class| { + viewport_cb(ctx, class, &open, opts.enabled, &state, &cfg); + }); + } else { + ctx.show_viewport_deferred(viewport_id, viewport_builder, move |ctx, class| { + viewport_cb(ctx, class, &open, opts.enabled, &state, &cfg); + }); + } + } } pub fn show_genie_codes_entry(&mut self, ui: &mut Ui, cfg: &Config) { - self.state.write().genie_codes_entry(ui, cfg); + self.state.lock().genie_codes_entry(ui, cfg); } pub fn genie_codes_list(tx: &NesEventProxy, ui: &mut Ui, cfg: &Config, scroll: bool) { @@ -611,153 +629,155 @@ impl State { .. } = cfg.deck; - let grid = Grid::new("emulation_checkboxes") - .num_columns(2) - .spacing([80.0, 6.0]); - grid.show(ui, |ui| { - let tx = &self.tx; - - Preferences::cycle_accurate_checkbox(tx, ui, cycle_accurate, ""); - let res = ui.checkbox(&mut auto_load, "Auto-Load") - .on_hover_text("Automatically load game state from the current save slot on load."); - if res.changed() { - tx.event(ConfigEvent::AutoLoad( - auto_load, - )); - } - ui.end_row(); + ScrollArea::both().show(ui, |ui| { + let grid = Grid::new("emulation_checkboxes") + .num_columns(2) + .spacing([80.0, 6.0]); + grid.show(ui, |ui| { + let tx = &self.tx; - ui.vertical(|ui| { - Preferences::rewind_checkbox(tx, ui, rewind, ""); - - ui.add_enabled_ui(rewind, |ui| { - ui.indent("rewind_settings", |ui| { - ui.label("Seconds:") - .on_hover_text("The maximum number of seconds to rewind."); - let drag = DragValue::new(&mut rewind_seconds) - .clamp_range(1..=360) - .suffix(" seconds"); - let res = ui.add(drag); - if res.changed() { - tx.event(ConfigEvent::RewindSeconds(rewind_seconds)); - } + Preferences::cycle_accurate_checkbox(tx, ui, cycle_accurate, ""); + let res = ui.checkbox(&mut auto_load, "Auto-Load") + .on_hover_text("Automatically load game state from the current save slot on load."); + if res.changed() { + tx.event(ConfigEvent::AutoLoad( + auto_load, + )); + } + ui.end_row(); - ui.label("Interval:") - .on_hover_text("The frame interval to save rewind states."); - let drag = DragValue::new(&mut rewind_interval) - .clamp_range(1..=60) - .suffix(" frames"); - let res = ui.add(drag); - if res.changed() { - tx.event(ConfigEvent::RewindInterval(rewind_interval)); - } + ui.vertical(|ui| { + Preferences::rewind_checkbox(tx, ui, rewind, ""); + + ui.add_enabled_ui(rewind, |ui| { + ui.indent("rewind_settings", |ui| { + ui.label("Seconds:") + .on_hover_text("The maximum number of seconds to rewind."); + let drag = DragValue::new(&mut rewind_seconds) + .clamp_range(1..=360) + .suffix(" seconds"); + let res = ui.add(drag); + if res.changed() { + tx.event(ConfigEvent::RewindSeconds(rewind_seconds)); + } + + ui.label("Interval:") + .on_hover_text("The frame interval to save rewind states."); + let drag = DragValue::new(&mut rewind_interval) + .clamp_range(1..=60) + .suffix(" frames"); + let res = ui.add(drag); + if res.changed() { + tx.event(ConfigEvent::RewindInterval(rewind_interval)); + } + }); }); }); - }); - ui.vertical(|ui| { - let res = ui.checkbox(&mut auto_save, "Auto-Save") - .on_hover_text(concat!( - "Automatically save game state to the current save slot ", - "on exit or unloading and an optional interval. ", - "Setting to 0 will disable saving on an interval.", - )); - if res.changed() { - tx.event(ConfigEvent::AutoSave( - auto_save, - )); - } + ui.vertical(|ui| { + let res = ui.checkbox(&mut auto_save, "Auto-Save") + .on_hover_text(concat!( + "Automatically save game state to the current save slot ", + "on exit or unloading and an optional interval. ", + "Setting to 0 will disable saving on an interval.", + )); + if res.changed() { + tx.event(ConfigEvent::AutoSave( + auto_save, + )); + } - ui.add_enabled_ui(auto_save, |ui| { - ui.indent("auto_save_settings", |ui| { - let mut auto_save_interval = auto_save_interval.as_secs(); - ui.label("Interval:") - .on_hover_text(concat!( - "Set the interval to auto-save game state. ", - "A value of `0` will still save on exit or unload while Auto-Save is enabled." - )); - let drag = DragValue::new(&mut auto_save_interval) - .clamp_range(0..=60) - .suffix(" seconds"); - let res = ui.add(drag); - if res.changed() { - tx.event(ConfigEvent::AutoSaveInterval(Duration::from_secs(auto_save_interval))); - } + ui.add_enabled_ui(auto_save, |ui| { + ui.indent("auto_save_settings", |ui| { + let mut auto_save_interval = auto_save_interval.as_secs(); + ui.label("Interval:") + .on_hover_text(concat!( + "Set the interval to auto-save game state. ", + "A value of `0` will still save on exit or unload while Auto-Save is enabled." + )); + let drag = DragValue::new(&mut auto_save_interval) + .clamp_range(0..=60) + .suffix(" seconds"); + let res = ui.add(drag); + if res.changed() { + tx.event(ConfigEvent::AutoSaveInterval(Duration::from_secs(auto_save_interval))); + } + }); }); }); - }); - ui.end_row(); - - let res = ui.checkbox(&mut emulate_ppu_warmup, "Emulate PPU Warmup") - .on_hover_text(concat!( - "Set whether to emulate PPU warmup where writes to certain registers are ignored. ", - "Can result in some games not working correctly" - )); - if res.clicked() { - tx.event(EmulationEvent::EmulatePpuWarmup(emulate_ppu_warmup)); - } - ui.end_row(); - }); + ui.end_row(); - ui.separator(); + let res = ui.checkbox(&mut emulate_ppu_warmup, "Emulate PPU Warmup") + .on_hover_text(concat!( + "Set whether to emulate PPU warmup where writes to certain registers are ignored. ", + "Can result in some games not working correctly" + )); + if res.clicked() { + tx.event(EmulationEvent::EmulatePpuWarmup(emulate_ppu_warmup)); + } + ui.end_row(); + }); - let grid = Grid::new("emulation_preferences") - .num_columns(4) - .spacing([40.0, 6.0]); - grid.show(ui, |ui| { - let tx = &self.tx; + ui.separator(); - ui.strong("Emulation Speed:"); - Preferences::speed_slider(tx, ui, speed); + let grid = Grid::new("emulation_preferences") + .num_columns(4) + .spacing([40.0, 6.0]); + grid.show(ui, |ui| { + let tx = &self.tx; - ui.strong("Run Ahead:") - .on_hover_cursor(CursorIcon::Help) - .on_hover_text("Simulate a number of frames in the future to reduce input lag."); - Preferences::run_ahead_slider(tx, ui, run_ahead); - ui.end_row(); + ui.strong("Emulation Speed:"); + Preferences::speed_slider(tx, ui, speed); - ui.with_layout(Layout::left_to_right(Align::Min), |ui| { - ui.strong("Save Slot:") + ui.strong("Run Ahead:") .on_hover_cursor(CursorIcon::Help) - .on_hover_text("Select which slot to use when saving or loading game state."); - }); - Grid::new("save_slots") - .num_columns(2) - .spacing([20.0, 6.0]) - .show(ui, |ui| { - Preferences::save_slot_radio(tx, ui, save_slot, cfg, ShowShortcut::No) + .on_hover_text("Simulate a number of frames in the future to reduce input lag."); + Preferences::run_ahead_slider(tx, ui, run_ahead); + ui.end_row(); + + ui.with_layout(Layout::left_to_right(Align::Min), |ui| { + ui.strong("Save Slot:") + .on_hover_cursor(CursorIcon::Help) + .on_hover_text("Select which slot to use when saving or loading game state."); + }); + Grid::new("save_slots") + .num_columns(2) + .spacing([20.0, 6.0]) + .show(ui, |ui| { + Preferences::save_slot_radio(tx, ui, save_slot, cfg, ShowShortcut::No) + }); + + ui.with_layout(Layout::left_to_right(Align::Min), |ui| { + ui.strong("Four Player:") + .on_hover_cursor(CursorIcon::Help) + .on_hover_text( + "Some game titles support up to 4 players (requires connected controllers).", + ); }); + ui.vertical(|ui| Preferences::four_player_radio(tx, ui, four_player)); + ui.end_row(); - ui.with_layout(Layout::left_to_right(Align::Min), |ui| { - ui.strong("Four Player:") - .on_hover_cursor(CursorIcon::Help) - .on_hover_text( - "Some game titles support up to 4 players (requires connected controllers).", - ); - }); - ui.vertical(|ui| Preferences::four_player_radio(tx, ui, four_player)); - ui.end_row(); + ui.with_layout(Layout::left_to_right(Align::Min), |ui| { + ui.strong("NES Region:") + .on_hover_cursor(CursorIcon::Help) + .on_hover_text("Which regional NES hardware to emulate."); + }); + ui.vertical(|ui| Preferences::nes_region_radio(tx, ui, region)); - ui.with_layout(Layout::left_to_right(Align::Min), |ui| { - ui.strong("NES Region:") - .on_hover_cursor(CursorIcon::Help) - .on_hover_text("Which regional NES hardware to emulate."); + ui.with_layout(Layout::left_to_right(Align::Min), |ui| { + ui.strong("RAM State:") + .on_hover_cursor(CursorIcon::Help) + .on_hover_text("What values are read from NES RAM on load."); + }); + ui.vertical(|ui| Preferences::ram_state_radio(tx, ui, ram_state)); + ui.end_row(); }); - ui.vertical(|ui| Preferences::nes_region_radio(tx, ui, region)); - ui.with_layout(Layout::left_to_right(Align::Min), |ui| { - ui.strong("RAM State:") - .on_hover_cursor(CursorIcon::Help) - .on_hover_text("What values are read from NES RAM on load."); + let grid = Grid::new("genie_codes").num_columns(2).spacing([40.0, 6.0]); + grid.show(ui, |ui| { + self.genie_codes_entry(ui, cfg); + Preferences::genie_codes_list(&self.tx, ui, cfg, false); }); - ui.vertical(|ui| Preferences::ram_state_radio(tx, ui, ram_state)); - ui.end_row(); - }); - - let grid = Grid::new("genie_codes").num_columns(2).spacing([40.0, 6.0]); - grid.show(ui, |ui| { - self.genie_codes_entry(ui, cfg); - Preferences::genie_codes_list(&self.tx, ui, cfg, false); }); }