Functional input binding

This commit is contained in:
Simon Gellis 2024-11-10 14:05:10 -05:00
parent a69247dd33
commit 5cb36d0bcc
8 changed files with 254 additions and 81 deletions

View File

@ -1,4 +1,8 @@
use std::{collections::HashMap, fmt::Debug}; use std::{
collections::HashMap,
fmt::Debug,
sync::{Arc, RwLock},
};
use game::GameWindow; use game::GameWindow;
use winit::{ use winit::{
@ -8,7 +12,11 @@ use winit::{
window::WindowId, window::WindowId,
}; };
use crate::emulator::EmulatorClient; use crate::{
controller::ControllerState,
emulator::{EmulatorClient, EmulatorCommand},
input::InputMapper,
};
mod common; mod common;
mod game; mod game;
@ -16,32 +24,35 @@ mod input;
pub struct App { pub struct App {
windows: HashMap<WindowId, Box<dyn AppWindow>>, windows: HashMap<WindowId, Box<dyn AppWindow>>,
focused_window: Option<WindowId>,
client: EmulatorClient, client: EmulatorClient,
input_mapper: Arc<RwLock<InputMapper>>,
controller: ControllerState,
proxy: EventLoopProxy<UserEvent>, proxy: EventLoopProxy<UserEvent>,
} }
impl App { impl App {
pub fn new(client: EmulatorClient, proxy: EventLoopProxy<UserEvent>) -> Self { pub fn new(client: EmulatorClient, proxy: EventLoopProxy<UserEvent>) -> Self {
let input_mapper = Arc::new(RwLock::new(InputMapper::new()));
let controller = ControllerState::new(input_mapper.clone());
Self { Self {
windows: HashMap::new(), windows: HashMap::new(),
focused_window: None,
client, client,
input_mapper,
controller,
proxy, proxy,
} }
} }
fn active_window(&mut self) -> Option<&mut Box<dyn AppWindow>> {
let active_window = self.focused_window?;
self.windows.get_mut(&active_window)
}
} }
impl ApplicationHandler<UserEvent> for App { impl ApplicationHandler<UserEvent> for App {
fn resumed(&mut self, event_loop: &ActiveEventLoop) { fn resumed(&mut self, event_loop: &ActiveEventLoop) {
let mut window = GameWindow::new(event_loop, self.client.clone(), self.proxy.clone()); let mut window = GameWindow::new(
event_loop,
self.client.clone(),
self.input_mapper.clone(),
self.proxy.clone(),
);
window.init(); window.init();
self.focused_window = Some(window.id());
self.windows.insert(window.id(), Box::new(window)); self.windows.insert(window.id(), Box::new(window));
} }
@ -51,11 +62,10 @@ impl ApplicationHandler<UserEvent> for App {
window_id: WindowId, window_id: WindowId,
event: WindowEvent, event: WindowEvent,
) { ) {
if let WindowEvent::Focused(focused) = event { if let WindowEvent::KeyboardInput { event, .. } = &event {
if focused { if self.controller.key_event(event) {
self.focused_window = Some(window_id); self.client
} else { .send_command(EmulatorCommand::SetKeys(self.controller.pressed()));
self.focused_window = None;
} }
} }
let Some(window) = self.windows.get_mut(&window_id) else { let Some(window) = self.windows.get_mut(&window_id) else {
@ -82,17 +92,21 @@ impl ApplicationHandler<UserEvent> for App {
device_id: winit::event::DeviceId, device_id: winit::event::DeviceId,
event: winit::event::DeviceEvent, event: winit::event::DeviceEvent,
) { ) {
let Some(window) = self.active_window() else { for window in self.windows.values_mut() {
return; window.handle_event(
}; event_loop,
window.handle_event(event_loop, &Event::DeviceEvent { device_id, event }); &Event::DeviceEvent {
device_id,
event: event.clone(),
},
);
}
} }
fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) { fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
let Some(window) = self.active_window() else { for window in self.windows.values_mut() {
return; window.handle_event(event_loop, &Event::AboutToWait);
}; }
window.handle_event(event_loop, &Event::AboutToWait);
} }
} }

View File

@ -235,14 +235,23 @@ impl<'a> Drop for ContextLock<'a> {
pub trait UiExt { pub trait UiExt {
fn fullscreen_window(&self) -> Option<WindowToken<'_>>; fn fullscreen_window(&self) -> Option<WindowToken<'_>>;
fn right_align_text<T: AsRef<str>>(&self, text: T, space: f32);
} }
impl UiExt for imgui::Ui { impl UiExt for imgui::Ui {
fn fullscreen_window(&self) -> Option<WindowToken<'_>> { fn fullscreen_window(&self) -> Option<WindowToken<'_>> {
self.window("fullscreen") self.window("fullscreen")
.position([0.0, 0.0], imgui::Condition::Always) .position([0.0, 0.0], imgui::Condition::Always)
.size(self.window_size(), imgui::Condition::Always) .size(self.io().display_size, imgui::Condition::Always)
.flags(imgui::WindowFlags::NO_DECORATION) .flags(imgui::WindowFlags::NO_DECORATION)
.begin() .begin()
} }
fn right_align_text<T: AsRef<str>>(&self, text: T, space: f32) {
let width = self.calc_text_size(text.as_ref())[0];
let [left, y] = self.cursor_pos();
let right = left + space;
self.set_cursor_pos([right - width, y]);
self.text(text);
}
} }

View File

@ -1,15 +1,18 @@
use std::{sync::Arc, time::Instant}; use std::{
sync::{Arc, RwLock},
time::Instant,
};
use wgpu::util::DeviceExt as _; use wgpu::util::DeviceExt as _;
use winit::{ use winit::{
dpi::LogicalSize, dpi::LogicalSize,
event::{Event, KeyEvent, WindowEvent}, event::{Event, WindowEvent},
event_loop::{ActiveEventLoop, EventLoopProxy}, event_loop::{ActiveEventLoop, EventLoopProxy},
window::WindowId, window::WindowId,
}; };
use crate::{ use crate::{
controller::ControllerState,
emulator::{EmulatorClient, EmulatorCommand}, emulator::{EmulatorClient, EmulatorCommand},
input::InputMapper,
renderer::GameRenderer, renderer::GameRenderer,
}; };
@ -25,7 +28,7 @@ pub struct GameWindow {
pipeline: wgpu::RenderPipeline, pipeline: wgpu::RenderPipeline,
bind_group: wgpu::BindGroup, bind_group: wgpu::BindGroup,
client: EmulatorClient, client: EmulatorClient,
controller: ControllerState, input_mapper: Arc<RwLock<InputMapper>>,
proxy: EventLoopProxy<UserEvent>, proxy: EventLoopProxy<UserEvent>,
} }
@ -33,6 +36,7 @@ impl GameWindow {
pub fn new( pub fn new(
event_loop: &ActiveEventLoop, event_loop: &ActiveEventLoop,
client: EmulatorClient, client: EmulatorClient,
input_mapper: Arc<RwLock<InputMapper>>,
proxy: EventLoopProxy<UserEvent>, proxy: EventLoopProxy<UserEvent>,
) -> Self { ) -> Self {
let window = WindowStateBuilder::new(event_loop) let window = WindowStateBuilder::new(event_loop)
@ -153,15 +157,13 @@ impl GameWindow {
cache: None, cache: None,
}); });
let controller = ControllerState::new();
client.send_command(EmulatorCommand::SetKeys(controller.pressed()));
Self { Self {
window, window,
imgui: None, imgui: None,
pipeline: render_pipeline, pipeline: render_pipeline,
bind_group, bind_group,
client, client,
controller, input_mapper,
proxy, proxy,
} }
} }
@ -170,6 +172,7 @@ impl GameWindow {
let window = &mut self.window; let window = &mut self.window;
let imgui = self.imgui.as_mut().unwrap(); let imgui = self.imgui.as_mut().unwrap();
let mut context = imgui.context.lock().unwrap(); let mut context = imgui.context.lock().unwrap();
let mut new_size = None;
let now = Instant::now(); let now = Instant::now();
context.io_mut().update_delta_time(now - imgui.last_frame); context.io_mut().update_delta_time(now - imgui.last_frame);
@ -226,13 +229,18 @@ impl GameWindow {
if ui.menu_item_config(label).selected(selected).build() { if ui.menu_item_config(label).selected(selected).build() {
if let Some(size) = window.window.request_inner_size(dims) { if let Some(size) = window.window.request_inner_size(dims) {
window.handle_resize(&size); window.handle_resize(&size);
new_size = Some(size);
} }
} }
} }
}); });
ui.menu("Input", || { ui.menu("Input", || {
if ui.menu_item("Map Input") { if ui.menu_item("Bind Inputs") {
let input_window = Box::new(InputWindow::new(event_loop, self.proxy.clone())); let input_window = Box::new(InputWindow::new(
event_loop,
self.input_mapper.clone(),
self.proxy.clone(),
));
self.proxy self.proxy
.send_event(UserEvent::OpenWindow(input_window)) .send_event(UserEvent::OpenWindow(input_window))
.unwrap(); .unwrap();
@ -287,17 +295,21 @@ impl GameWindow {
drop(rpass); drop(rpass);
if let Some(size) = new_size {
imgui.platform.handle_event::<UserEvent>(
context.io_mut(),
&window.window,
&Event::WindowEvent {
window_id: window.window.id(),
event: WindowEvent::Resized(size),
},
);
}
window.queue.submit(Some(encoder.finish())); window.queue.submit(Some(encoder.finish()));
frame.present(); frame.present();
} }
fn handle_key_event(&mut self, event: &KeyEvent) {
if self.controller.key_event(event) {
self.client
.send_command(EmulatorCommand::SetKeys(self.controller.pressed()));
}
}
} }
impl AppWindow for GameWindow { impl AppWindow for GameWindow {
@ -307,6 +319,7 @@ impl AppWindow for GameWindow {
fn init(&mut self) { fn init(&mut self) {
self.imgui = Some(ImguiState::new(&self.window)); self.imgui = Some(ImguiState::new(&self.window));
self.window.window.request_redraw();
} }
fn handle_event(&mut self, event_loop: &ActiveEventLoop, event: &Event<UserEvent>) { fn handle_event(&mut self, event_loop: &ActiveEventLoop, event: &Event<UserEvent>) {
@ -314,7 +327,6 @@ impl AppWindow for GameWindow {
Event::WindowEvent { event, .. } => match event { Event::WindowEvent { event, .. } => match event {
WindowEvent::Resized(size) => self.window.handle_resize(size), WindowEvent::Resized(size) => self.window.handle_resize(size),
WindowEvent::CloseRequested => event_loop.exit(), WindowEvent::CloseRequested => event_loop.exit(),
WindowEvent::KeyboardInput { event, .. } => self.handle_key_event(event),
WindowEvent::RedrawRequested => self.draw(event_loop), WindowEvent::RedrawRequested => self.draw(event_loop),
_ => (), _ => (),
}, },

View File

@ -1,29 +1,62 @@
use std::time::Instant; use std::{
sync::{Arc, RwLock},
use winit::{ time::Instant,
event::{Event, WindowEvent},
event_loop::{ActiveEventLoop, EventLoopProxy},
}; };
use winit::{
dpi::LogicalSize,
event::{Event, KeyEvent, WindowEvent},
event_loop::{ActiveEventLoop, EventLoopProxy},
platform::modifier_supplement::KeyEventExtModifierSupplement,
};
use crate::{input::InputMapper, shrooms_vb_core::VBKey};
use super::{ use super::{
common::{ImguiState, UiExt as _, WindowState, WindowStateBuilder}, common::{ImguiState, UiExt, WindowState, WindowStateBuilder},
AppWindow, UserEvent, AppWindow, UserEvent,
}; };
pub struct InputWindow { pub struct InputWindow {
window: WindowState, window: WindowState,
imgui: Option<ImguiState>, imgui: Option<ImguiState>,
input_mapper: Arc<RwLock<InputMapper>>,
proxy: EventLoopProxy<UserEvent>, proxy: EventLoopProxy<UserEvent>,
now_binding: Option<VBKey>,
} }
const KEY_NAMES: [(VBKey, &str); 14] = [
(VBKey::LU, "Up"),
(VBKey::LD, "Down"),
(VBKey::LL, "Left"),
(VBKey::LR, "Right"),
(VBKey::SEL, "Select"),
(VBKey::STA, "Start"),
(VBKey::B, "B"),
(VBKey::A, "A"),
(VBKey::LT, "L-Trigger"),
(VBKey::RT, "R-Trigger"),
(VBKey::RU, "R-Up"),
(VBKey::RD, "R-Down"),
(VBKey::RL, "R-Left"),
(VBKey::RR, "R-Right"),
];
impl InputWindow { impl InputWindow {
pub fn new(event_loop: &ActiveEventLoop, proxy: EventLoopProxy<UserEvent>) -> Self { pub fn new(
event_loop: &ActiveEventLoop,
input_mapper: Arc<RwLock<InputMapper>>,
proxy: EventLoopProxy<UserEvent>,
) -> Self {
let window = WindowStateBuilder::new(event_loop) let window = WindowStateBuilder::new(event_loop)
.with_title("Map Inputs") .with_title("Bind Inputs")
.with_inner_size(LogicalSize::new(600, 400))
.build(); .build();
Self { Self {
window, window,
imgui: None, imgui: None,
input_mapper,
now_binding: None,
proxy, proxy,
} }
} }
@ -50,17 +83,50 @@ impl InputWindow {
.expect("Failed to prepare frame"); .expect("Failed to prepare frame");
let ui = context.new_frame(); let ui = context.new_frame();
if let Some(window) = ui.fullscreen_window() { let mut render_key_bindings = || {
if let Some(table) = ui.begin_table("controls", 2) { if let Some(table) = ui.begin_table("controls", 2) {
let binding_names = {
let mapper = self.input_mapper.read().unwrap();
mapper.binding_names()
};
ui.table_next_row(); ui.table_next_row();
ui.table_next_column(); for (key, name) in KEY_NAMES {
ui.text("Key"); let binding = binding_names.get(&key).map(|s| s.as_str());
ui.table_next_column();
let [space, _] = ui.content_region_avail();
ui.group(|| {
ui.right_align_text(name, space * 0.20);
ui.same_line();
let label_text = if self.now_binding == Some(key) {
"Press any input"
} else {
binding.unwrap_or("")
};
let label = format!("{}##{}", label_text, name);
if ui.button_with_size(label, [space * 0.60, 0.0]) {
self.now_binding = Some(key);
}
});
ui.same_line();
if ui.button(format!("Clear##{name}")) {
let mut mapper = self.input_mapper.write().unwrap();
mapper.clear_binding(key);
}
}
ui.table_next_column();
ui.text("Value");
table.end(); table.end();
} }
};
if let Some(window) = ui.fullscreen_window() {
if let Some(tabs) = ui.tab_bar("tabs") {
if let Some(tab) = ui.tab_item("Player 1") {
render_key_bindings();
tab.end();
}
tabs.end();
}
window.end(); window.end();
} }
let mut encoder: wgpu::CommandEncoder = window let mut encoder: wgpu::CommandEncoder = window
@ -102,6 +168,17 @@ impl InputWindow {
frame.present(); frame.present();
} }
fn try_bind_key(&mut self, event: &KeyEvent) {
if !event.state.is_pressed() {
return;
}
let Some(vb) = self.now_binding.take() else {
return;
};
let mut mapper = self.input_mapper.write().unwrap();
mapper.bind_key(vb, event.key_without_modifiers());
}
} }
impl AppWindow for InputWindow { impl AppWindow for InputWindow {
@ -111,6 +188,7 @@ impl AppWindow for InputWindow {
fn init(&mut self) { fn init(&mut self) {
self.imgui = Some(ImguiState::new(&self.window)); self.imgui = Some(ImguiState::new(&self.window));
self.window.window.request_redraw();
} }
fn handle_event(&mut self, _: &ActiveEventLoop, event: &Event<UserEvent>) { fn handle_event(&mut self, _: &ActiveEventLoop, event: &Event<UserEvent>) {
@ -121,6 +199,7 @@ impl AppWindow for InputWindow {
.proxy .proxy
.send_event(UserEvent::CloseWindow(self.id())) .send_event(UserEvent::CloseWindow(self.id()))
.unwrap(), .unwrap(),
WindowEvent::KeyboardInput { event, .. } => self.try_bind_key(event),
WindowEvent::RedrawRequested => self.draw(), WindowEvent::RedrawRequested => self.draw(),
_ => (), _ => (),
}, },

View File

@ -1,17 +1,18 @@
use winit::{ use std::sync::{Arc, RwLock};
event::{ElementState, KeyEvent},
keyboard::{Key, NamedKey},
};
use crate::shrooms_vb_core::VBKey; use winit::event::{ElementState, KeyEvent};
use crate::{input::InputMapper, shrooms_vb_core::VBKey};
pub struct ControllerState { pub struct ControllerState {
input_mapper: Arc<RwLock<InputMapper>>,
pressed: VBKey, pressed: VBKey,
} }
impl ControllerState { impl ControllerState {
pub fn new() -> Self { pub fn new(input_mapper: Arc<RwLock<InputMapper>>) -> Self {
Self { Self {
input_mapper,
pressed: VBKey::SGN, pressed: VBKey::SGN,
} }
} }
@ -21,7 +22,7 @@ impl ControllerState {
} }
pub fn key_event(&mut self, event: &KeyEvent) -> bool { pub fn key_event(&mut self, event: &KeyEvent) -> bool {
let Some(input) = self.key_to_input(&event.logical_key) else { let Some(input) = self.key_event_to_input(event) else {
return false; return false;
}; };
match event.state { match event.state {
@ -42,23 +43,8 @@ impl ControllerState {
} }
} }
fn key_to_input(&self, key: &Key) -> Option<VBKey> { fn key_event_to_input(&self, event: &KeyEvent) -> Option<VBKey> {
match key.as_ref() { let mapper = self.input_mapper.read().unwrap();
Key::Character("a") => Some(VBKey::SEL), mapper.key_event(event)
Key::Character("s") => Some(VBKey::STA),
Key::Character("d") => Some(VBKey::B),
Key::Character("f") => Some(VBKey::A),
Key::Character("e") => Some(VBKey::LT),
Key::Character("r") => Some(VBKey::RT),
Key::Character("i") => Some(VBKey::RU),
Key::Character("j") => Some(VBKey::RL),
Key::Character("k") => Some(VBKey::RD),
Key::Character("l") => Some(VBKey::RR),
Key::Named(NamedKey::ArrowUp) => Some(VBKey::LU),
Key::Named(NamedKey::ArrowLeft) => Some(VBKey::LL),
Key::Named(NamedKey::ArrowDown) => Some(VBKey::LD),
Key::Named(NamedKey::ArrowRight) => Some(VBKey::LR),
_ => None,
}
} }
} }

71
src/input.rs Normal file
View File

@ -0,0 +1,71 @@
use std::collections::HashMap;
use winit::{
event::KeyEvent,
keyboard::{Key, NamedKey},
platform::modifier_supplement::KeyEventExtModifierSupplement,
};
use crate::shrooms_vb_core::VBKey;
pub struct InputMapper {
vb_bindings: HashMap<VBKey, Key>,
key_bindings: HashMap<Key, VBKey>,
}
impl InputMapper {
pub fn new() -> Self {
let mut mapper = Self {
vb_bindings: HashMap::new(),
key_bindings: HashMap::new(),
};
mapper.bind_key(VBKey::SEL, Key::Character("a".into()));
mapper.bind_key(VBKey::STA, Key::Character("s".into()));
mapper.bind_key(VBKey::B, Key::Character("d".into()));
mapper.bind_key(VBKey::A, Key::Character("f".into()));
mapper.bind_key(VBKey::LT, Key::Character("e".into()));
mapper.bind_key(VBKey::RT, Key::Character("r".into()));
mapper.bind_key(VBKey::RU, Key::Character("i".into()));
mapper.bind_key(VBKey::RL, Key::Character("j".into()));
mapper.bind_key(VBKey::RD, Key::Character("k".into()));
mapper.bind_key(VBKey::RR, Key::Character("l".into()));
mapper.bind_key(VBKey::LU, Key::Named(NamedKey::ArrowUp));
mapper.bind_key(VBKey::LL, Key::Named(NamedKey::ArrowLeft));
mapper.bind_key(VBKey::LD, Key::Named(NamedKey::ArrowDown));
mapper.bind_key(VBKey::LR, Key::Named(NamedKey::ArrowRight));
mapper
}
pub fn binding_names(&self) -> HashMap<VBKey, String> {
self.vb_bindings
.iter()
.map(|(k, v)| {
let name = match v {
Key::Character(char) => char.to_string(),
Key::Named(key) => format!("{:?}", key),
k => format!("{:?}", k),
};
(*k, name)
})
.collect()
}
pub fn bind_key(&mut self, vb: VBKey, key: Key) {
if let Some(old) = self.vb_bindings.insert(vb, key.clone()) {
self.key_bindings.remove(&old);
}
self.key_bindings.insert(key, vb);
}
pub fn clear_binding(&mut self, vb: VBKey) {
if let Some(old) = self.vb_bindings.remove(&vb) {
self.key_bindings.remove(&old);
}
}
pub fn key_event(&self, event: &KeyEvent) -> Option<VBKey> {
self.key_bindings
.get(&event.key_without_modifiers())
.cloned()
}
}

View File

@ -10,6 +10,7 @@ mod app;
mod audio; mod audio;
mod controller; mod controller;
mod emulator; mod emulator;
mod input;
mod renderer; mod renderer;
mod shrooms_vb_core; mod shrooms_vb_core;

View File

@ -26,7 +26,8 @@ enum VBDataType {
} }
bitflags! { bitflags! {
#[derive(Clone, Copy, Debug)] #[repr(transparent)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct VBKey: u16 { pub struct VBKey: u16 {
const PWR = 0x0001; const PWR = 0x0001;
const SGN = 0x0002; const SGN = 0x0002;