diff --git a/docs/guide/ui.md b/docs/guide/ui.md index 5a416da..1be1c0a 100644 --- a/docs/guide/ui.md +++ b/docs/guide/ui.md @@ -24,6 +24,7 @@ The app shortcut modifier is `Cmd` on macOS and `Alt` on Linux/Windows. | `Cmd+Shift+A` | `Alt+Shift+A` | Cycle the audio output: Default, each host device, then Disabled (also the menu's Audio Out item) | | `Cmd+Shift++` / `Cmd+Shift+-` | `Alt+Shift++` / `Alt+Shift+-` | Raise / lower the parallel-port sampler input gain (only when a sampler is attached; also the menu's Sampler Gain item) | | `Cmd+F` | `Alt+F` | Toggle fullscreen on / off | +| `Cmd+Shift+F` | `Alt+Shift+F` | Show / hide the status bar | | `Cmd+W` | `Alt+W` | Toggle Warp Speed (turbo) on / off | | `Cmd+Shift+W` | `Alt+Shift+W` | Cycle the Warp Speed limit: 2x, 4x, 8x, 16x, Max | | `Esc` | `Esc` | Close an open menu, tool window, or overlay panel; otherwise passed through to the Amiga | @@ -161,6 +162,9 @@ tool window or overlay. window's current monitor. The picture keeps its aspect and letterboxes as needed, exactly as when resizing the window; the same shortcut (or menu item) restores the window. +- **Status Bar** (also `Cmd+Shift+F` / `Alt+Shift+F`): show or hide the + status bar. Handy alongside fullscreen for a clean, chrome-free picture; + the same shortcut or menu item brings it back. - **Warp Speed** (also `Cmd+W` / `Alt+W`): runs the emulator unpaced for fast-forward. Toggling back re-anchors real-time pacing cleanly. - **Warp Limit** (also `Cmd+Shift+W` / `Alt+Shift+W`): cycles how fast warp diff --git a/src/video/mod.rs b/src/video/mod.rs index ff1e3ae..ce5c26e 100644 --- a/src/video/mod.rs +++ b/src/video/mod.rs @@ -106,6 +106,19 @@ pub fn set_pixel_aspect(aspect: crate::config::PixelAspect) { ); } +/// Whether the status bar is hidden, so the emulated display scales to fill the +/// whole window. Toggled live from the window/menu (main thread only, like +/// [`SQUARE_PIXEL_ASPECT`]); the atomic only satisfies `static` safety. +static STATUS_BAR_HIDDEN: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false); + +pub fn set_status_bar_hidden(hidden: bool) { + STATUS_BAR_HIDDEN.store(hidden, std::sync::atomic::Ordering::Relaxed); +} + +pub fn status_bar_hidden() -> bool { + STATUS_BAR_HIDDEN.load(std::sync::atomic::Ordering::Relaxed) +} + pub fn pixel_aspect() -> crate::config::PixelAspect { if SQUARE_PIXEL_ASPECT.load(std::sync::atomic::Ordering::Relaxed) { crate::config::PixelAspect::Square diff --git a/src/video/ui.rs b/src/video/ui.rs index 48bfc29..72381db 100644 --- a/src/video/ui.rs +++ b/src/video/ui.rs @@ -90,6 +90,7 @@ pub enum MenuItem { PixelAspect, FloppySpeed, Fullscreen, + StatusBar, AudioOutput, Warp, WarpLimit, @@ -107,8 +108,8 @@ pub enum MenuItem { pub fn menu_items(midi_active: bool, sampler_active: bool) -> Vec { let _ = midi_active; // 9 leading + up to 2 MIDI + 2 sampler + pixel aspect + floppy speed + - // 10 trailing items = 25, sized so appending never reallocates. - let mut items = Vec::with_capacity(25); + // 11 trailing items = 26, sized so appending never reallocates. + let mut items = Vec::with_capacity(26); items.extend([ MenuItem::MachineConfig, MenuItem::FrameAnalyzer, @@ -133,6 +134,7 @@ pub fn menu_items(midi_active: bool, sampler_active: bool) -> Vec { items.push(MenuItem::FloppySpeed); items.extend([ MenuItem::Fullscreen, + MenuItem::StatusBar, MenuItem::Warp, MenuItem::WarpLimit, MenuItem::Record, @@ -153,6 +155,8 @@ pub struct MenuLabels<'a> { pub warp_speed: WarpSpeed, /// True while the host window is fullscreen (the menu item toggles it). pub fullscreen: bool, + /// True while the status bar is hidden (the menu item toggles it). + pub status_bar_hidden: bool, pub recording: bool, pub input_recording: bool, pub joystick_input_mode: JoystickInputMode, @@ -225,6 +229,8 @@ fn menu_item_label(item: MenuItem, s: MenuLabels) -> String { MenuItem::SamplerGain => format!("Sampler Gain {:>5}", format!("[{}]", s.sampler_gain)), MenuItem::Fullscreen if s.fullscreen => "Fullscreen [on]".to_string(), MenuItem::Fullscreen => "Fullscreen [off]".to_string(), + MenuItem::StatusBar if s.status_bar_hidden => "Status Bar [off]".to_string(), + MenuItem::StatusBar => "Status Bar [on]".to_string(), MenuItem::Warp if s.warp => "Warp Speed [on]".to_string(), MenuItem::Warp => "Warp Speed [off]".to_string(), // Right-pad so the closing bracket stays put as the value width @@ -873,7 +879,7 @@ pub enum UiControl { fn panel_dims(panel: &Panel) -> (usize, usize) { match panel { Panel::About => (560, 380), - Panel::Shortcuts => (600, 464), + Panel::Shortcuts => (600, 486), Panel::Calibration(_) => (620, 372), Panel::Debugger(_) => (684, 520), Panel::FrameAnalyzer(_) => (700, 526), @@ -1898,7 +1904,7 @@ pub fn draw_drop_hint(frame: &mut [u8], texture_scale: usize) { draw_panel_text(frame, x, y, text, PANEL_TEXT_HILIGHT, px, texture_scale); } -const SHORTCUT_ROWS: [(&str, &str, bool); 17] = [ +const SHORTCUT_ROWS: [(&str, &str, bool); 18] = [ ("Q", "Quit", true), ("S", "Save screenshot", true), ("R", "Record video on/off", true), @@ -1912,6 +1918,7 @@ const SHORTCUT_ROWS: [(&str, &str, bool); 17] = [ ("J", "Joystick input mode", true), ("Shift+A", "Cycle audio output", true), ("F", "Fullscreen on/off", true), + ("Shift+F", "Status bar on/off", true), ("W", "Warp speed on/off", true), ("Shift+W", "Warp limit (2x..Max)", true), ("Esc", "Close menu/window", false), @@ -4893,6 +4900,7 @@ mod tests { warp: false, warp_speed: WarpSpeed::Max, fullscreen, + status_bar_hidden: false, recording: false, input_recording: false, joystick_input_mode: JoystickInputMode::Gamepad, @@ -4918,6 +4926,40 @@ mod tests { ); } + #[test] + fn status_bar_menu_item_label_tracks_state() { + assert!(menu_items(false, false).contains(&MenuItem::StatusBar)); + let labels = |status_bar_hidden| MenuLabels { + warp: false, + warp_speed: WarpSpeed::Max, + fullscreen: false, + status_bar_hidden, + recording: false, + input_recording: false, + joystick_input_mode: JoystickInputMode::Gamepad, + port_devices: [ + crate::bus::PortDevice::Mouse, + crate::bus::PortDevice::Joystick, + ], + pixel_aspect: PixelAspect::Tv, + floppy_speed: 100, + midi_in: "", + midi_out: "", + audio_output: "", + sampler_input: "", + sampler_gain: "", + }; + // "on" means the bar is shown. + assert_eq!( + menu_item_label(MenuItem::StatusBar, labels(false)), + "Status Bar [on]" + ); + assert_eq!( + menu_item_label(MenuItem::StatusBar, labels(true)), + "Status Bar [off]" + ); + } + #[test] fn every_menu_label_fits_inside_the_popup() { // The label is drawn at `item_rect.x + MENU_TEXT_INSET`; its glyphs @@ -4943,6 +4985,7 @@ mod tests { // Rides warp's sweep so both label // states are width-checked. fullscreen: warp, + status_bar_hidden: warp, recording, input_recording, joystick_input_mode: mode, @@ -5501,6 +5544,7 @@ mod tests { warp: true, warp_speed: WarpSpeed::Max, fullscreen: false, + status_bar_hidden: false, recording: false, input_recording: false, joystick_input_mode: JoystickInputMode::Gamepad, @@ -5549,6 +5593,7 @@ mod tests { warp: false, warp_speed: WarpSpeed::Max, fullscreen: false, + status_bar_hidden: false, recording: false, input_recording: false, joystick_input_mode: JoystickInputMode::Gamepad, @@ -5585,6 +5630,7 @@ mod tests { warp: false, warp_speed: WarpSpeed::Max, fullscreen: false, + status_bar_hidden: false, recording: false, input_recording: false, joystick_input_mode: JoystickInputMode::Gamepad, @@ -5637,6 +5683,7 @@ mod tests { warp: false, warp_speed: WarpSpeed::Max, fullscreen: false, + status_bar_hidden: false, recording: false, input_recording: false, joystick_input_mode: JoystickInputMode::Gamepad, @@ -5713,6 +5760,7 @@ mod tests { warp: false, warp_speed: WarpSpeed::Max, fullscreen: false, + status_bar_hidden: false, recording: false, input_recording: false, joystick_input_mode: JoystickInputMode::Gamepad, @@ -5776,6 +5824,7 @@ mod tests { warp: false, warp_speed: WarpSpeed::Max, fullscreen: false, + status_bar_hidden: false, recording: false, input_recording: false, joystick_input_mode: JoystickInputMode::Gamepad, @@ -5836,6 +5885,7 @@ mod tests { warp: false, warp_speed: WarpSpeed::Max, fullscreen: false, + status_bar_hidden: false, recording: false, input_recording: false, joystick_input_mode: JoystickInputMode::Gamepad, @@ -5897,6 +5947,7 @@ mod tests { warp: false, warp_speed: WarpSpeed::Max, fullscreen: false, + status_bar_hidden: false, recording: false, input_recording: false, joystick_input_mode: JoystickInputMode::Gamepad, @@ -6011,6 +6062,7 @@ mod tests { warp: false, warp_speed: WarpSpeed::Max, fullscreen: false, + status_bar_hidden: false, recording: false, input_recording: false, joystick_input_mode: JoystickInputMode::Gamepad, @@ -6085,6 +6137,7 @@ mod tests { warp: false, warp_speed: WarpSpeed::Max, fullscreen: false, + status_bar_hidden: false, recording: false, input_recording: false, joystick_input_mode: JoystickInputMode::Gamepad, @@ -6170,6 +6223,7 @@ mod tests { warp: false, warp_speed: WarpSpeed::Max, fullscreen: false, + status_bar_hidden: false, recording: false, input_recording: false, joystick_input_mode: JoystickInputMode::Gamepad, @@ -6292,6 +6346,7 @@ mod tests { warp: false, warp_speed: WarpSpeed::Max, fullscreen: false, + status_bar_hidden: false, recording: false, input_recording: false, joystick_input_mode: JoystickInputMode::Gamepad, @@ -6344,6 +6399,7 @@ mod tests { warp: false, warp_speed: WarpSpeed::Max, fullscreen: false, + status_bar_hidden: false, recording: false, input_recording: false, joystick_input_mode: JoystickInputMode::Gamepad, @@ -6387,6 +6443,7 @@ mod tests { warp: false, warp_speed: WarpSpeed::Max, fullscreen: false, + status_bar_hidden: false, recording: false, input_recording: false, joystick_input_mode: JoystickInputMode::Gamepad, @@ -6470,6 +6527,7 @@ mod tests { warp: false, warp_speed: WarpSpeed::Max, fullscreen: false, + status_bar_hidden: false, recording: false, input_recording: false, joystick_input_mode: JoystickInputMode::Gamepad, @@ -6520,6 +6578,7 @@ mod tests { warp: false, warp_speed: WarpSpeed::Max, fullscreen: false, + status_bar_hidden: false, recording: false, input_recording: false, joystick_input_mode: JoystickInputMode::Gamepad, @@ -6563,6 +6622,7 @@ mod tests { warp: false, warp_speed: WarpSpeed::Max, fullscreen: false, + status_bar_hidden: false, recording: false, input_recording: false, joystick_input_mode: JoystickInputMode::Gamepad, @@ -6610,6 +6670,7 @@ mod tests { warp: false, warp_speed: WarpSpeed::Max, fullscreen: false, + status_bar_hidden: false, recording: false, input_recording: false, joystick_input_mode: JoystickInputMode::Gamepad, diff --git a/src/video/window.rs b/src/video/window.rs index 017eced..66c4204 100644 --- a/src/video/window.rs +++ b/src/video/window.rs @@ -308,10 +308,20 @@ pub const DEFAULT_KEY_HOLD_MS: u32 = 100; const DEBUGGER_REVERSE_INTERVAL_FRAMES: u64 = 10; const MAX_TEXTURE_SCALE: usize = 2; const STATUS_BAR_HEIGHT: usize = 44; -/// Logical window height: the presentation canvas for the active pixel -/// aspect plus the status bar below it. +/// Logical window height: the presentation canvas for the active pixel aspect, +/// plus the status bar below it unless it is hidden (in which case the display +/// scales to fill the whole window). fn window_present_height() -> usize { - present_height() + STATUS_BAR_HEIGHT + present_height() + status_bar_height() +} + +/// The status bar's height, or 0 while it is hidden. +fn status_bar_height() -> usize { + if super::status_bar_hidden() { + 0 + } else { + STATUS_BAR_HEIGHT + } } const STATUS_LABEL_X: usize = 18; const STATUS_LED_X: usize = 58; @@ -1857,6 +1867,12 @@ impl ApplicationHandler for App { { self.cycle_joystick_input_mode() } + (KeyCode::KeyF, ElementState::Pressed) + if host_shortcut_modifier_pressed(self.modifiers) + && self.modifiers.shift_key() => + { + self.toggle_status_bar() + } (KeyCode::KeyF, ElementState::Pressed) if host_shortcut_modifier_pressed(self.modifiers) => { @@ -2238,7 +2254,9 @@ impl ApplicationHandler for App { self.present_standard_tv_aperture && self.rtg_present_dims.is_none(), ); } - draw_status_bar(frame, &view, r.texture_scale); + if !super::status_bar_hidden() { + draw_status_bar(frame, &view, r.texture_scale); + } if recording { // Painted into the presentation texture only, so // the badge never appears in the recorded file. @@ -2259,6 +2277,7 @@ impl ApplicationHandler for App { warp, warp_speed, fullscreen: r.window.fullscreen().is_some(), + status_bar_hidden: super::status_bar_hidden(), recording, input_recording, joystick_input_mode: self.joystick_input_mode, @@ -3424,6 +3443,7 @@ impl App { ui::MenuItem::FloppySpeed => self.cycle_floppy_speed(), ui::MenuItem::AudioOutput => self.cycle_audio_output(), ui::MenuItem::Fullscreen => self.toggle_fullscreen(), + ui::MenuItem::StatusBar => self.toggle_status_bar(), ui::MenuItem::Warp => self.toggle_warp(), ui::MenuItem::WarpLimit => self.cycle_warp_speed(), ui::MenuItem::Record => self.toggle_recording(), @@ -7139,6 +7159,40 @@ impl App { self.request_redraw(); } + /// Show or hide the status bar. Hidden, the emulated display scales to fill + /// the whole window (the canvas loses the bar's height, so the presentation + /// grows into it while keeping its aspect). This is the same canvas-height + /// change as a pixel-aspect switch, so it resizes the texture and window the + /// same way; the recorder captures only the display, so a recording is + /// unaffected. Bound to the status-bar shortcut and the menu. + fn toggle_status_bar(&mut self) { + let hidden = !super::status_bar_hidden(); + super::set_status_bar_hidden(hidden); + if let Some(r) = self.render.as_mut() { + if let Err(e) = r.pixels.resize_buffer( + texture_width(r.texture_scale) as u32, + texture_height(r.texture_scale) as u32, + ) { + // The draw helpers size themselves from the hidden flag, so a + // failed resize must not commit the toggle: a taller canvas over + // an unchanged, shorter buffer would index past it. Revert and + // leave the flag and buffer consistent. + warn!("resize texture buffer for status bar toggle failed: {e}"); + super::set_status_bar_hidden(!hidden); + return; + } + } + self.resize_for_active_panel(); + self.request_redraw(); + if hidden { + self.show_osd(format!( + "Status bar hidden ({HOST_SHORTCUT_MODIFIER_LABEL}+Shift+F restores)" + )); + } else { + self.show_osd("Status bar restored"); + } + } + fn request_main_redraw(&self) { if let Some(render) = self.render.as_ref() { if !render.minimized {