#![allow(dead_code)] use super::render_target::WinitRenderTarget;
use crate::frontends::native::gl_backend::{Crosshair, GlBackend, ProcAddressLoader};
use crate::frontends::native::input::{
InputEvent, MouseButton as RenderMouseButton, UiKey, UiModifiers,
};
use crate::nes::debugging::ui::DebuggerUiAction;
use crate::platform::app_context::SharedAppContext;
use crate::platform::debugging::breakpoints::BreakpointList;
use crate::platform::emulator::SystemType;
use glutin::config::ConfigTemplateBuilder;
use glutin::context::{ContextApi, ContextAttributesBuilder, Version};
use glutin::display::GetGlDisplay;
use glutin::prelude::*;
use glutin::surface::SurfaceAttributesBuilder;
use glutin_winit::DisplayBuilder;
use raw_window_handle::HasWindowHandle;
use winit::dpi::LogicalSize;
use winit::event::{ElementState, KeyEvent, MouseButton, MouseScrollDelta};
use winit::keyboard::{KeyCode, ModifiersState, PhysicalKey};
use winit::window::{Window, WindowAttributes};
use std::num::NonZeroU32;
use std::rc::Rc;
use std::sync::Arc;
pub struct NativeGlWrapper {
gl_backend: GlBackend,
render_target_window: Arc<Window>,
}
impl NativeGlWrapper {
pub fn new(
event_loop: &winit::event_loop::ActiveEventLoop,
app_context: SharedAppContext,
system_type: SystemType,
allowed_shaders: &'static [&'static str],
) -> Result<Self, String> {
let (fullscreen, vsync_enabled, shader_path, debugger_alpha, fullscreen_display, height) = {
let ctx = app_context.borrow();
let config = ctx.config();
(
config.frontend.fullscreen,
config.frontend.vsync_enabled,
config.frontend.shader_path.clone(),
config.frontend.debugger_alpha,
config.frontend.fullscreen_display,
config.frontend.window_height,
)
};
let (window_width, window_height) = system_type.windowed_dimensions(height, &app_context);
let monitors: Vec<_> = event_loop.available_monitors().collect();
let target_display = select_target_display(fullscreen, fullscreen_display, monitors.len())?;
let mut window_attrs = WindowAttributes::default()
.with_title("NES Emulator in Rust")
.with_inner_size(LogicalSize::new(window_width, window_height))
.with_resizable(!fullscreen);
if let Some(display_index) = target_display {
let monitor = monitors.into_iter().nth(display_index);
window_attrs =
window_attrs.with_fullscreen(Some(winit::window::Fullscreen::Borderless(monitor)));
}
let config_template = ConfigTemplateBuilder::new()
.with_alpha_size(8)
.with_depth_size(0)
.with_stencil_size(0);
let display_builder = DisplayBuilder::new().with_window_attributes(Some(window_attrs));
let (window, gl_config) = display_builder
.build(event_loop, config_template, |configs| {
configs
.reduce(|accum, config| {
if config.num_samples() > accum.num_samples() {
config
} else {
accum
}
})
.expect("no compatible GL configurations found")
})
.map_err(|e| format!("failed to build display: {e}"))?;
let window = Arc::new(window.ok_or("failed to create window")?);
let raw_window_handle = window
.window_handle()
.map_err(|e| format!("failed to get window handle: {e}"))?
.as_raw();
let gl_display = gl_config.display();
let context_attributes = ContextAttributesBuilder::new()
.with_context_api(ContextApi::OpenGl(Some(Version::new(3, 2))))
.build(Some(raw_window_handle));
let not_current_context = unsafe {
gl_display
.create_context(&gl_config, &context_attributes)
.map_err(|e| format!("failed to create GL context: {e}"))?
};
let size = window.inner_size();
let surface_attrs = SurfaceAttributesBuilder::<glutin::surface::WindowSurface>::new()
.build(
raw_window_handle,
NonZeroU32::new(size.width.max(1)).expect("non-zero width"),
NonZeroU32::new(size.height.max(1)).expect("non-zero height"),
);
let surface = unsafe {
gl_display
.create_window_surface(&gl_config, &surface_attrs)
.map_err(|e| format!("failed to create window surface: {e}"))?
};
let gl_context = not_current_context
.make_current(&surface)
.map_err(|e| format!("failed to make GL context current: {e}"))?;
gl::load_with(|s| {
gl_display
.get_proc_address(std::ffi::CString::new(s).expect("valid CString").as_c_str())
as *const _
});
let proc_address: ProcAddressLoader = {
let display = gl_display.clone();
Rc::new(move |s| {
display
.get_proc_address(std::ffi::CString::new(s).expect("valid CString").as_c_str())
.cast()
})
};
let render_target = WinitRenderTarget::new(Arc::clone(&window), surface, gl_context);
render_target.set_swap_interval(vsync_enabled)?;
let render_target_boxed: Box<dyn crate::frontends::native::gl_backend::RenderTarget> =
Box::new(render_target);
let mut gl_backend = GlBackend::new(
render_target_boxed,
proc_address,
shader_path.as_deref(),
app_context,
allowed_shaders,
)?;
gl_backend.set_debugger_alpha(debugger_alpha);
Ok(Self {
gl_backend,
render_target_window: window,
})
}
pub fn window_size(&self) -> (u32, u32) {
self.gl_backend.window_size()
}
pub fn render(
&mut self,
console: &crate::platform::emulator::Console,
show_debugger: bool,
overlay_text: Option<&str>,
overlay_blink_red: bool,
crosshair: Option<Crosshair>,
fps: Option<usize>,
) -> DebuggerUiAction {
self.gl_backend.render(
console,
show_debugger,
overlay_text,
overlay_blink_red,
crosshair,
fps,
)
}
pub fn cycle_shader(&mut self) -> Option<String> {
self.gl_backend.cycle_shader()
}
pub fn update_breakpoints(&mut self, breakpoints: &BreakpointList) {
self.gl_backend.update_breakpoints(breakpoints);
}
pub fn update_gb_breakpoints(&mut self, breakpoints: &BreakpointList) {
self.gl_backend.update_gb_breakpoints(breakpoints);
}
pub fn take_gb_debugger_action(&mut self) -> crate::gb::debugging::ui::GbDebuggerUiAction {
self.gl_backend.take_gb_debugger_action()
}
pub fn watch_addresses(&self) -> Vec<u16> {
self.gl_backend.watch_addresses()
}
pub fn set_watch_addresses(&mut self, addresses: Vec<u16>) {
self.gl_backend.set_watch_addresses(addresses);
}
pub fn set_fullscreen(&mut self, enabled: bool) -> Result<(), String> {
self.gl_backend.set_fullscreen(enabled)
}
pub fn set_mouse_grab(&mut self, enabled: bool) -> Result<(), String> {
self.gl_backend.set_mouse_grab(enabled)
}
pub fn notify_resize(&mut self, w: u32, h: u32) {
self.gl_backend.notify_resize(w, h);
}
pub fn set_mouse_grab_locked(&mut self) -> Result<(), String> {
self.render_target_window
.set_cursor_grab(winit::window::CursorGrabMode::Locked)
.map_err(|e| format!("failed to set locked cursor grab: {e}"))
}
pub fn window(&self) -> &Window {
&self.render_target_window
}
pub fn handle_key_event(&mut self, event: &KeyEvent) {
if event.repeat {
return;
}
let down = event.state == ElementState::Pressed;
if let Some(key) = map_winit_key(&event.physical_key) {
self.gl_backend.handle_input(&InputEvent::Key { key, down });
}
}
pub fn handle_cursor_moved(&mut self, position: winit::dpi::PhysicalPosition<f64>) {
let scale_factor = self.render_target_window.scale_factor();
self.gl_backend.handle_input(&InputEvent::MouseMotion {
x: (position.x / scale_factor) as f32,
y: (position.y / scale_factor) as f32,
});
}
pub fn handle_mouse_button(&mut self, button: MouseButton, state: ElementState) {
let render_btn = match button {
MouseButton::Left => Some(RenderMouseButton::Left),
MouseButton::Right => Some(RenderMouseButton::Right),
MouseButton::Middle => Some(RenderMouseButton::Middle),
_ => None,
};
if let Some(btn) = render_btn {
self.gl_backend.handle_input(&InputEvent::MouseButton {
button: btn,
pressed: state == ElementState::Pressed,
});
}
}
pub fn handle_mouse_wheel(&mut self, delta: MouseScrollDelta) {
let (x, y) = match delta {
MouseScrollDelta::LineDelta(x, y) => (x, y),
MouseScrollDelta::PixelDelta(pos) => {
let scale_factor = self.render_target_window.scale_factor();
((pos.x / scale_factor) as f32, (pos.y / scale_factor) as f32)
}
};
self.gl_backend
.handle_input(&InputEvent::MouseWheel { x, y });
}
pub fn handle_text_input(&mut self, text: String) {
self.gl_backend.handle_input(&InputEvent::TextInput(text));
}
pub fn handle_modifiers_changed(&mut self, state: ModifiersState) {
self.gl_backend
.handle_input(&InputEvent::ModifiersChanged(ui_modifiers_from_winit(
state,
)));
}
}
fn ui_modifiers_from_winit(state: ModifiersState) -> UiModifiers {
let super_key = state.super_key();
UiModifiers {
alt: state.alt_key(),
ctrl: state.control_key(),
shift: state.shift_key(),
mac_cmd: cfg!(target_os = "macos") && super_key,
command: if cfg!(target_os = "macos") {
super_key
} else {
state.control_key()
},
}
}
fn map_winit_key(key: &PhysicalKey) -> Option<UiKey> {
match key {
PhysicalKey::Code(code) => match code {
KeyCode::Tab => Some(UiKey::Tab),
KeyCode::ArrowLeft => Some(UiKey::LeftArrow),
KeyCode::ArrowRight => Some(UiKey::RightArrow),
KeyCode::ArrowUp => Some(UiKey::UpArrow),
KeyCode::ArrowDown => Some(UiKey::DownArrow),
KeyCode::PageUp => Some(UiKey::PageUp),
KeyCode::PageDown => Some(UiKey::PageDown),
KeyCode::Home => Some(UiKey::Home),
KeyCode::End => Some(UiKey::End),
KeyCode::Insert => Some(UiKey::Insert),
KeyCode::Delete => Some(UiKey::Delete),
KeyCode::Backspace => Some(UiKey::Backspace),
KeyCode::Space => Some(UiKey::Space),
KeyCode::Enter => Some(UiKey::Enter),
KeyCode::Escape => Some(UiKey::Escape),
KeyCode::KeyA => Some(UiKey::A),
KeyCode::KeyC => Some(UiKey::C),
KeyCode::KeyV => Some(UiKey::V),
KeyCode::KeyX => Some(UiKey::X),
KeyCode::KeyY => Some(UiKey::Y),
KeyCode::KeyZ => Some(UiKey::Z),
KeyCode::F1 => Some(UiKey::F1),
KeyCode::F5 => Some(UiKey::F5),
KeyCode::F10 => Some(UiKey::F10),
KeyCode::F11 => Some(UiKey::F11),
_ => None,
},
_ => None,
}
}
fn select_target_display(
fullscreen: bool,
fullscreen_display: Option<i32>,
monitor_count: usize,
) -> Result<Option<usize>, String> {
if !fullscreen {
return Ok(None);
}
if monitor_count == 0 {
return Err("No monitors detected. Cannot enter fullscreen mode.".to_string());
}
let target = match fullscreen_display {
Some(display) => display,
None => {
if monitor_count >= 2 {
1
} else {
0
}
}
};
if target < 0 || (target as usize) >= monitor_count {
return Err(format!(
"Invalid --display {target}. Available displays: 0..{}",
monitor_count - 1
));
}
Ok(Some(target as usize))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn map_winit_key_returns_correct_ui_key_for_tab() {
let key = PhysicalKey::Code(KeyCode::Tab);
assert_eq!(map_winit_key(&key), Some(UiKey::Tab));
}
#[test]
fn map_winit_key_returns_correct_ui_key_for_arrows() {
assert_eq!(
map_winit_key(&PhysicalKey::Code(KeyCode::ArrowLeft)),
Some(UiKey::LeftArrow)
);
assert_eq!(
map_winit_key(&PhysicalKey::Code(KeyCode::ArrowRight)),
Some(UiKey::RightArrow)
);
assert_eq!(
map_winit_key(&PhysicalKey::Code(KeyCode::ArrowUp)),
Some(UiKey::UpArrow)
);
assert_eq!(
map_winit_key(&PhysicalKey::Code(KeyCode::ArrowDown)),
Some(UiKey::DownArrow)
);
}
#[test]
fn map_winit_key_returns_none_for_unmapped_keys() {
assert_eq!(map_winit_key(&PhysicalKey::Code(KeyCode::F12)), None);
}
#[test]
fn map_winit_key_returns_correct_ui_key_for_letters() {
assert_eq!(
map_winit_key(&PhysicalKey::Code(KeyCode::KeyA)),
Some(UiKey::A)
);
assert_eq!(
map_winit_key(&PhysicalKey::Code(KeyCode::KeyZ)),
Some(UiKey::Z)
);
}
#[test]
fn map_winit_key_returns_none_for_unspecified_physical_key() {
let key = PhysicalKey::Unidentified(winit::keyboard::NativeKeyCode::Unidentified);
assert_eq!(map_winit_key(&key), None);
}
#[test]
fn map_winit_key_maps_debugger_function_keys() {
assert_eq!(
map_winit_key(&PhysicalKey::Code(KeyCode::F5)),
Some(UiKey::F5)
);
assert_eq!(
map_winit_key(&PhysicalKey::Code(KeyCode::F10)),
Some(UiKey::F10)
);
assert_eq!(
map_winit_key(&PhysicalKey::Code(KeyCode::F11)),
Some(UiKey::F11)
);
}
#[test]
fn ui_modifiers_from_winit_preserves_modifier_flags() {
let modifiers = ui_modifiers_from_winit(ModifiersState::CONTROL | ModifiersState::SHIFT);
assert_eq!(
modifiers,
UiModifiers {
ctrl: true,
shift: true,
command: !cfg!(target_os = "macos"),
..Default::default()
}
);
}
#[test]
fn ui_modifiers_from_winit_maps_super_to_macos_command() {
let modifiers = ui_modifiers_from_winit(ModifiersState::SUPER);
assert_eq!(
modifiers,
UiModifiers {
mac_cmd: cfg!(target_os = "macos"),
command: cfg!(target_os = "macos"),
..Default::default()
}
);
}
#[test]
fn select_target_display_returns_none_when_not_fullscreen() {
assert_eq!(select_target_display(false, None, 2).unwrap(), None);
assert_eq!(select_target_display(false, Some(1), 2).unwrap(), None);
}
#[test]
fn select_target_display_defaults_to_1_with_multiple_monitors() {
assert_eq!(
select_target_display(true, None, 3).unwrap(),
Some(1),
"should default to display 1 when ≥2 monitors are available"
);
}
#[test]
fn select_target_display_defaults_to_0_with_single_monitor() {
assert_eq!(
select_target_display(true, None, 1).unwrap(),
Some(0),
"should default to display 0 when only 1 monitor available"
);
}
#[test]
fn select_target_display_uses_configured_display() {
assert_eq!(
select_target_display(true, Some(2), 4).unwrap(),
Some(2),
"should use the configured display index"
);
}
#[test]
fn select_target_display_rejects_out_of_range() {
let result = select_target_display(true, Some(5), 3);
assert!(result.is_err(), "should error for out-of-range display");
}
#[test]
fn select_target_display_rejects_negative_index() {
let result = select_target_display(true, Some(-1), 2);
assert!(result.is_err(), "should error for negative display index");
}
#[test]
fn select_target_display_errors_when_no_monitors_detected() {
let result = select_target_display(true, None, 0);
assert!(
result.is_err(),
"should error when no monitors are detected"
);
}
}