Compare commits
1 Commits
ad6970d4b5
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| badbe0979f |
@@ -1,4 +1,3 @@
|
||||
use gtk4::gdk;
|
||||
use nesemu::{InputProvider, JoypadButton, JoypadButtons, set_button_pressed};
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -17,31 +16,3 @@ impl InputProvider for InputState {
|
||||
self.buttons
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn key_to_p1_button(key: gdk::Key) -> Option<JoypadButton> {
|
||||
match key {
|
||||
gdk::Key::Up => Some(JoypadButton::Up),
|
||||
gdk::Key::Down => Some(JoypadButton::Down),
|
||||
gdk::Key::Left => Some(JoypadButton::Left),
|
||||
gdk::Key::Right => Some(JoypadButton::Right),
|
||||
gdk::Key::x | gdk::Key::X => Some(JoypadButton::A),
|
||||
gdk::Key::z | gdk::Key::Z => Some(JoypadButton::B),
|
||||
gdk::Key::Return => Some(JoypadButton::Start),
|
||||
gdk::Key::Shift_L | gdk::Key::Shift_R => Some(JoypadButton::Select),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn key_to_p2_button(key: gdk::Key) -> Option<JoypadButton> {
|
||||
match key {
|
||||
gdk::Key::w | gdk::Key::W => Some(JoypadButton::Up),
|
||||
gdk::Key::s | gdk::Key::S => Some(JoypadButton::Down),
|
||||
gdk::Key::a | gdk::Key::A => Some(JoypadButton::Left),
|
||||
gdk::Key::d | gdk::Key::D => Some(JoypadButton::Right),
|
||||
gdk::Key::k | gdk::Key::K => Some(JoypadButton::A),
|
||||
gdk::Key::j | gdk::Key::J => Some(JoypadButton::B),
|
||||
gdk::Key::i | gdk::Key::I => Some(JoypadButton::Start),
|
||||
gdk::Key::u | gdk::Key::U => Some(JoypadButton::Select),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
446
crates/nesemu-desktop/src/input_config.rs
Normal file
446
crates/nesemu-desktop/src/input_config.rs
Normal file
@@ -0,0 +1,446 @@
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
|
||||
use gtk::gdk;
|
||||
use gtk::glib::translate::IntoGlib;
|
||||
use gtk::prelude::*;
|
||||
use gtk4 as gtk;
|
||||
use nesemu::{JOYPAD_BUTTONS_COUNT, JOYPAD_BUTTON_ORDER, JoypadButton};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// KeyBindings — maps each JoypadButton to a gdk::Key for one player
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct KeyBindings {
|
||||
keys: [gdk::Key; JOYPAD_BUTTONS_COUNT],
|
||||
reverse: HashMap<gdk::Key, JoypadButton>,
|
||||
}
|
||||
|
||||
impl KeyBindings {
|
||||
fn new(keys: [gdk::Key; JOYPAD_BUTTONS_COUNT]) -> Self {
|
||||
let reverse = Self::build_reverse(&keys);
|
||||
Self { keys, reverse }
|
||||
}
|
||||
|
||||
pub(crate) fn default_p1() -> Self {
|
||||
Self::new([
|
||||
gdk::Key::Up,
|
||||
gdk::Key::Down,
|
||||
gdk::Key::Left,
|
||||
gdk::Key::Right,
|
||||
gdk::Key::x,
|
||||
gdk::Key::z,
|
||||
gdk::Key::Return,
|
||||
gdk::Key::Shift_L,
|
||||
])
|
||||
}
|
||||
|
||||
pub(crate) fn default_p2() -> Self {
|
||||
Self::new([
|
||||
gdk::Key::w,
|
||||
gdk::Key::s,
|
||||
gdk::Key::a,
|
||||
gdk::Key::d,
|
||||
gdk::Key::k,
|
||||
gdk::Key::j,
|
||||
gdk::Key::i,
|
||||
gdk::Key::u,
|
||||
])
|
||||
}
|
||||
|
||||
pub(crate) fn lookup(&self, key: gdk::Key) -> Option<JoypadButton> {
|
||||
let normalized = normalize_key(key);
|
||||
self.reverse.get(&normalized).copied()
|
||||
}
|
||||
|
||||
pub(crate) fn key_for(&self, button: JoypadButton) -> gdk::Key {
|
||||
self.keys[button.index()]
|
||||
}
|
||||
|
||||
pub(crate) fn set_key(&mut self, button: JoypadButton, key: gdk::Key) {
|
||||
let normalized = normalize_key(key);
|
||||
// Clear duplicate: if another button has this key, unbind it
|
||||
if let Some(&old_button) = self.reverse.get(&normalized) {
|
||||
if old_button != button {
|
||||
self.keys[old_button.index()] = gdk::Key::VoidSymbol;
|
||||
}
|
||||
}
|
||||
// Remove old reverse entry for the button being rebound
|
||||
let old_key = self.keys[button.index()];
|
||||
self.reverse.remove(&old_key);
|
||||
// Set new binding
|
||||
self.keys[button.index()] = normalized;
|
||||
self.rebuild_reverse();
|
||||
}
|
||||
|
||||
fn rebuild_reverse(&mut self) {
|
||||
self.reverse = Self::build_reverse(&self.keys);
|
||||
}
|
||||
|
||||
fn build_reverse(keys: &[gdk::Key; JOYPAD_BUTTONS_COUNT]) -> HashMap<gdk::Key, JoypadButton> {
|
||||
let mut map = HashMap::with_capacity(JOYPAD_BUTTONS_COUNT);
|
||||
for &button in &JOYPAD_BUTTON_ORDER {
|
||||
let key = keys[button.index()];
|
||||
if key != gdk::Key::VoidSymbol {
|
||||
map.insert(key, button);
|
||||
}
|
||||
}
|
||||
map
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// InputConfig — shared state for both players
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub(crate) struct InputConfig {
|
||||
pub(crate) p1: KeyBindings,
|
||||
pub(crate) p2: KeyBindings,
|
||||
}
|
||||
|
||||
impl InputConfig {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
p1: KeyBindings::default_p1(),
|
||||
p2: KeyBindings::default_p2(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn lookup_p1(&self, key: gdk::Key) -> Option<JoypadButton> {
|
||||
self.p1.lookup(key)
|
||||
}
|
||||
|
||||
pub(crate) fn lookup_p2(&self, key: gdk::Key) -> Option<JoypadButton> {
|
||||
self.p2.lookup(key)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Key normalization & display
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn normalize_key(key: gdk::Key) -> gdk::Key {
|
||||
let lower = key.to_lower();
|
||||
if lower != gdk::Key::VoidSymbol {
|
||||
lower
|
||||
} else {
|
||||
key
|
||||
}
|
||||
}
|
||||
|
||||
fn display_key_name(key: gdk::Key) -> String {
|
||||
if key == gdk::Key::VoidSymbol {
|
||||
return "—".to_string();
|
||||
}
|
||||
match key {
|
||||
gdk::Key::Return => "Enter".to_string(),
|
||||
gdk::Key::Shift_L => "LShift".to_string(),
|
||||
gdk::Key::Shift_R => "RShift".to_string(),
|
||||
gdk::Key::Control_L => "LCtrl".to_string(),
|
||||
gdk::Key::Control_R => "RCtrl".to_string(),
|
||||
gdk::Key::Alt_L => "LAlt".to_string(),
|
||||
gdk::Key::Alt_R => "RAlt".to_string(),
|
||||
gdk::Key::space => "Space".to_string(),
|
||||
gdk::Key::BackSpace => "Backspace".to_string(),
|
||||
gdk::Key::Tab => "Tab".to_string(),
|
||||
gdk::Key::Escape => "Escape".to_string(),
|
||||
gdk::Key::Up => "↑".to_string(),
|
||||
gdk::Key::Down => "↓".to_string(),
|
||||
gdk::Key::Left => "←".to_string(),
|
||||
gdk::Key::Right => "→".to_string(),
|
||||
other => {
|
||||
if let Some(name) = other.name() {
|
||||
let s = name.to_string();
|
||||
if s.len() == 1 {
|
||||
s.to_uppercase()
|
||||
} else {
|
||||
s
|
||||
}
|
||||
} else {
|
||||
format!("0x{:04x}", other.into_glib())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn button_display_name(button: JoypadButton) -> &'static str {
|
||||
match button {
|
||||
JoypadButton::Up => "Up",
|
||||
JoypadButton::Down => "Down",
|
||||
JoypadButton::Left => "Left",
|
||||
JoypadButton::Right => "Right",
|
||||
JoypadButton::A => "A",
|
||||
JoypadButton::B => "B",
|
||||
JoypadButton::Start => "Start",
|
||||
JoypadButton::Select => "Select",
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Dialog
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub(crate) fn show_input_config_dialog(
|
||||
parent: >k::ApplicationWindow,
|
||||
config: Rc<RefCell<InputConfig>>,
|
||||
) {
|
||||
let dialog = gtk::Window::builder()
|
||||
.title("Controls")
|
||||
.modal(true)
|
||||
.transient_for(parent)
|
||||
.resizable(false)
|
||||
.default_width(340)
|
||||
.build();
|
||||
|
||||
let root_box = gtk::Box::new(gtk::Orientation::Vertical, 8);
|
||||
root_box.set_margin_top(12);
|
||||
root_box.set_margin_bottom(12);
|
||||
root_box.set_margin_start(12);
|
||||
root_box.set_margin_end(12);
|
||||
|
||||
// --- Top-level notebook: Keyboard / Gamepad ---
|
||||
let top_notebook = gtk::Notebook::new();
|
||||
|
||||
// --- Keyboard tab ---
|
||||
let keyboard_box = gtk::Box::new(gtk::Orientation::Vertical, 8);
|
||||
|
||||
let player_notebook = gtk::Notebook::new();
|
||||
|
||||
// State for key capture mode
|
||||
let capturing: Rc<RefCell<Option<(usize, JoypadButton)>>> = Rc::new(RefCell::new(None));
|
||||
// Store all key buttons for updating labels
|
||||
let key_buttons: Rc<RefCell<Vec<Vec<gtk::Button>>>> =
|
||||
Rc::new(RefCell::new(vec![Vec::new(), Vec::new()]));
|
||||
|
||||
for player_idx in 0..2 {
|
||||
let page_box = gtk::Box::new(gtk::Orientation::Vertical, 4);
|
||||
page_box.set_margin_top(8);
|
||||
page_box.set_margin_bottom(8);
|
||||
page_box.set_margin_start(8);
|
||||
page_box.set_margin_end(8);
|
||||
|
||||
for &button in &JOYPAD_BUTTON_ORDER {
|
||||
let row = gtk::Box::new(gtk::Orientation::Horizontal, 0);
|
||||
row.set_hexpand(true);
|
||||
|
||||
let label = gtk::Label::new(Some(button_display_name(button)));
|
||||
label.set_halign(gtk::Align::Start);
|
||||
label.set_hexpand(true);
|
||||
label.set_width_chars(8);
|
||||
|
||||
let bindings = if player_idx == 0 {
|
||||
&config.borrow().p1
|
||||
} else {
|
||||
&config.borrow().p2
|
||||
};
|
||||
let key_name = display_key_name(bindings.key_for(button));
|
||||
|
||||
let key_button = gtk::Button::with_label(&key_name);
|
||||
key_button.set_width_request(100);
|
||||
key_button.set_halign(gtk::Align::End);
|
||||
|
||||
// On click: enter capture mode
|
||||
{
|
||||
let capturing = Rc::clone(&capturing);
|
||||
let key_buttons = Rc::clone(&key_buttons);
|
||||
key_button.connect_clicked(move |btn| {
|
||||
// Reset any previous capturing button label
|
||||
if let Some((prev_player, prev_btn)) = *capturing.borrow() {
|
||||
let buttons = key_buttons.borrow();
|
||||
let prev_widget = &buttons[prev_player][prev_btn.index()];
|
||||
// Restore its label — we'll just mark it needs refresh
|
||||
prev_widget.set_label("—");
|
||||
}
|
||||
*capturing.borrow_mut() = Some((player_idx, button));
|
||||
btn.set_label("Press a key...");
|
||||
});
|
||||
}
|
||||
|
||||
row.append(&label);
|
||||
row.append(&key_button);
|
||||
page_box.append(&row);
|
||||
|
||||
key_buttons.borrow_mut()[player_idx].push(key_button);
|
||||
}
|
||||
|
||||
let tab_label = gtk::Label::new(Some(if player_idx == 0 {
|
||||
"Player 1"
|
||||
} else {
|
||||
"Player 2"
|
||||
}));
|
||||
player_notebook.append_page(&page_box, Some(&tab_label));
|
||||
}
|
||||
|
||||
keyboard_box.append(&player_notebook);
|
||||
|
||||
// --- Reset to Defaults button ---
|
||||
let button_box = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
||||
button_box.set_halign(gtk::Align::Center);
|
||||
button_box.set_margin_top(8);
|
||||
|
||||
let reset_button = gtk::Button::with_label("Reset to Defaults");
|
||||
{
|
||||
let config = Rc::clone(&config);
|
||||
let player_notebook = player_notebook.clone();
|
||||
let key_buttons = Rc::clone(&key_buttons);
|
||||
reset_button.connect_clicked(move |_| {
|
||||
let current_page = player_notebook.current_page().unwrap_or(0) as usize;
|
||||
let mut cfg = config.borrow_mut();
|
||||
if current_page == 0 {
|
||||
cfg.p1 = KeyBindings::default_p1();
|
||||
} else {
|
||||
cfg.p2 = KeyBindings::default_p2();
|
||||
}
|
||||
// Refresh button labels for the reset player
|
||||
let bindings = if current_page == 0 { &cfg.p1 } else { &cfg.p2 };
|
||||
let buttons = key_buttons.borrow();
|
||||
for &btn in &JOYPAD_BUTTON_ORDER {
|
||||
let label = display_key_name(bindings.key_for(btn));
|
||||
buttons[current_page][btn.index()].set_label(&label);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let close_button = gtk::Button::with_label("Close");
|
||||
{
|
||||
let dialog = dialog.clone();
|
||||
close_button.connect_clicked(move |_| {
|
||||
dialog.close();
|
||||
});
|
||||
}
|
||||
|
||||
button_box.append(&reset_button);
|
||||
button_box.append(&close_button);
|
||||
keyboard_box.append(&button_box);
|
||||
|
||||
let keyboard_tab_label = gtk::Label::new(Some("Keyboard"));
|
||||
top_notebook.append_page(&keyboard_box, Some(&keyboard_tab_label));
|
||||
|
||||
// --- Gamepad tab (stub) ---
|
||||
let gamepad_box = gtk::Box::new(gtk::Orientation::Vertical, 0);
|
||||
gamepad_box.set_valign(gtk::Align::Center);
|
||||
gamepad_box.set_halign(gtk::Align::Center);
|
||||
gamepad_box.set_vexpand(true);
|
||||
gamepad_box.set_margin_top(32);
|
||||
gamepad_box.set_margin_bottom(32);
|
||||
|
||||
let gamepad_label = gtk::Label::new(Some(
|
||||
"Gamepad support coming soon\nXbox / DualSense",
|
||||
));
|
||||
gamepad_label.set_justify(gtk::Justification::Center);
|
||||
gamepad_label.add_css_class("dim-label");
|
||||
gamepad_box.append(&gamepad_label);
|
||||
|
||||
let gamepad_tab_label = gtk::Label::new(Some("Gamepad"));
|
||||
top_notebook.append_page(&gamepad_box, Some(&gamepad_tab_label));
|
||||
|
||||
root_box.append(&top_notebook);
|
||||
dialog.set_child(Some(&root_box));
|
||||
|
||||
// --- Key capture via EventControllerKey on the dialog window ---
|
||||
{
|
||||
let config = Rc::clone(&config);
|
||||
let capturing = Rc::clone(&capturing);
|
||||
let key_buttons = Rc::clone(&key_buttons);
|
||||
|
||||
let key_controller = gtk::EventControllerKey::new();
|
||||
key_controller.connect_key_pressed(move |_, keyval, _, _| {
|
||||
let Some((player_idx, button)) = *capturing.borrow() else {
|
||||
return gtk::glib::Propagation::Proceed;
|
||||
};
|
||||
|
||||
// Escape cancels capture
|
||||
if keyval == gdk::Key::Escape {
|
||||
// Restore the label to current binding
|
||||
let cfg = config.borrow();
|
||||
let bindings = if player_idx == 0 { &cfg.p1 } else { &cfg.p2 };
|
||||
let label = display_key_name(bindings.key_for(button));
|
||||
key_buttons.borrow()[player_idx][button.index()].set_label(&label);
|
||||
*capturing.borrow_mut() = None;
|
||||
return gtk::glib::Propagation::Stop;
|
||||
}
|
||||
|
||||
// Assign the key
|
||||
{
|
||||
let mut cfg = config.borrow_mut();
|
||||
let bindings = if player_idx == 0 {
|
||||
&mut cfg.p1
|
||||
} else {
|
||||
&mut cfg.p2
|
||||
};
|
||||
bindings.set_key(button, keyval);
|
||||
}
|
||||
|
||||
// Refresh all button labels for this player (duplicate might have been cleared)
|
||||
{
|
||||
let cfg = config.borrow();
|
||||
let bindings = if player_idx == 0 { &cfg.p1 } else { &cfg.p2 };
|
||||
let buttons = key_buttons.borrow();
|
||||
for &btn in &JOYPAD_BUTTON_ORDER {
|
||||
let label = display_key_name(bindings.key_for(btn));
|
||||
buttons[player_idx][btn.index()].set_label(&label);
|
||||
}
|
||||
}
|
||||
|
||||
*capturing.borrow_mut() = None;
|
||||
gtk::glib::Propagation::Stop
|
||||
});
|
||||
|
||||
dialog.add_controller(key_controller);
|
||||
}
|
||||
|
||||
dialog.present();
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn default_bindings_lookup() {
|
||||
let config = InputConfig::new();
|
||||
assert_eq!(config.lookup_p1(gdk::Key::x), Some(JoypadButton::A));
|
||||
assert_eq!(config.lookup_p1(gdk::Key::z), Some(JoypadButton::B));
|
||||
assert_eq!(config.lookup_p1(gdk::Key::Up), Some(JoypadButton::Up));
|
||||
assert_eq!(config.lookup_p2(gdk::Key::w), Some(JoypadButton::Up));
|
||||
assert_eq!(config.lookup_p2(gdk::Key::k), Some(JoypadButton::A));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn case_insensitive_lookup() {
|
||||
let config = InputConfig::new();
|
||||
// X (uppercase) should also match since we normalize to lowercase
|
||||
assert_eq!(config.lookup_p1(gdk::Key::X), Some(JoypadButton::A));
|
||||
assert_eq!(config.lookup_p2(gdk::Key::W), Some(JoypadButton::Up));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_key_clears_duplicate() {
|
||||
let mut bindings = KeyBindings::default_p1();
|
||||
// Bind Up to 'x' — should clear A's binding to 'x'
|
||||
bindings.set_key(JoypadButton::Up, gdk::Key::x);
|
||||
assert_eq!(bindings.key_for(JoypadButton::Up), gdk::Key::x);
|
||||
assert_eq!(bindings.key_for(JoypadButton::A), gdk::Key::VoidSymbol);
|
||||
assert_eq!(bindings.lookup(gdk::Key::x), Some(JoypadButton::Up));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_key_names() {
|
||||
assert_eq!(display_key_name(gdk::Key::Return), "Enter");
|
||||
assert_eq!(display_key_name(gdk::Key::Up), "↑");
|
||||
assert_eq!(display_key_name(gdk::Key::Shift_L), "LShift");
|
||||
assert_eq!(display_key_name(gdk::Key::VoidSymbol), "—");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_to_defaults() {
|
||||
let mut config = InputConfig::new();
|
||||
config.p1.set_key(JoypadButton::A, gdk::Key::q);
|
||||
assert_eq!(config.lookup_p1(gdk::Key::q), Some(JoypadButton::A));
|
||||
config.p1 = KeyBindings::default_p1();
|
||||
assert_eq!(config.lookup_p1(gdk::Key::q), None);
|
||||
assert_eq!(config.lookup_p1(gdk::Key::x), Some(JoypadButton::A));
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
mod app;
|
||||
mod audio;
|
||||
mod input;
|
||||
mod input_config;
|
||||
mod scheduling;
|
||||
mod video;
|
||||
|
||||
@@ -20,6 +21,7 @@ use nesemu::prelude::EmulationState;
|
||||
use nesemu::{FRAME_HEIGHT, FRAME_WIDTH};
|
||||
|
||||
use app::DesktopApp;
|
||||
use input_config::InputConfig;
|
||||
use scheduling::DesktopFrameScheduler;
|
||||
use video::NesScreen;
|
||||
|
||||
@@ -103,6 +105,13 @@ fn build_ui(app: >k::Application, initial_rom: Option<PathBuf>) {
|
||||
volume_box.append(&volume_scale);
|
||||
header.pack_end(&volume_box);
|
||||
|
||||
let controls_button = gtk::Button::builder()
|
||||
.icon_name("preferences-system-symbolic")
|
||||
.tooltip_text("Controls")
|
||||
.focusable(false)
|
||||
.build();
|
||||
header.pack_end(&controls_button);
|
||||
|
||||
window.set_titlebar(Some(&header));
|
||||
|
||||
// --- NES screen widget (GPU-accelerated, nearest-neighbor scaling) ---
|
||||
@@ -138,6 +147,7 @@ fn build_ui(app: >k::Application, initial_rom: Option<PathBuf>) {
|
||||
// --- State ---
|
||||
let desktop = Rc::new(RefCell::new(DesktopApp::new(Arc::clone(&volume))));
|
||||
let scheduler = Rc::new(RefCell::new(DesktopFrameScheduler::new()));
|
||||
let input_config = Rc::new(RefCell::new(InputConfig::new()));
|
||||
|
||||
// --- Helper to sync UI with emulation state ---
|
||||
let current_rom_name: Rc<RefCell<Option<String>>> = Rc::new(RefCell::new(None));
|
||||
@@ -277,6 +287,14 @@ fn build_ui(app: >k::Application, initial_rom: Option<PathBuf>) {
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let input_config = Rc::clone(&input_config);
|
||||
let window = window.clone();
|
||||
controls_button.connect_clicked(move |_| {
|
||||
input_config::show_input_config_dialog(&window, Rc::clone(&input_config));
|
||||
});
|
||||
}
|
||||
|
||||
// --- Keyboard shortcuts via actions ---
|
||||
let action_open = gio::SimpleAction::new("open", None);
|
||||
{
|
||||
@@ -395,26 +413,30 @@ fn build_ui(app: >k::Application, initial_rom: Option<PathBuf>) {
|
||||
// --- Keyboard controller for joypad input ---
|
||||
{
|
||||
let desktop = Rc::clone(&desktop);
|
||||
let input_config = Rc::clone(&input_config);
|
||||
let key_controller = gtk::EventControllerKey::new();
|
||||
|
||||
let desktop_for_press = Rc::clone(&desktop);
|
||||
let config_for_press = Rc::clone(&input_config);
|
||||
key_controller.connect_key_pressed(move |_, key, _, _| {
|
||||
let config = config_for_press.borrow();
|
||||
let mut app_state = desktop_for_press.borrow_mut();
|
||||
if let Some(btn) = input::key_to_p1_button(key) {
|
||||
if let Some(btn) = config.lookup_p1(key) {
|
||||
app_state.input_p1_mut().set_button(btn, true);
|
||||
}
|
||||
if let Some(btn) = input::key_to_p2_button(key) {
|
||||
if let Some(btn) = config.lookup_p2(key) {
|
||||
app_state.input_p2_mut().set_button(btn, true);
|
||||
}
|
||||
gtk::glib::Propagation::Proceed
|
||||
});
|
||||
|
||||
key_controller.connect_key_released(move |_, key, _, _| {
|
||||
let config = input_config.borrow();
|
||||
let mut app_state = desktop.borrow_mut();
|
||||
if let Some(btn) = input::key_to_p1_button(key) {
|
||||
if let Some(btn) = config.lookup_p1(key) {
|
||||
app_state.input_p1_mut().set_button(btn, false);
|
||||
}
|
||||
if let Some(btn) = input::key_to_p2_button(key) {
|
||||
if let Some(btn) = config.lookup_p2(key) {
|
||||
app_state.input_p2_mut().set_button(btn, false);
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user