diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/cli/mod.rs | 94 | ||||
| -rw-r--r-- | src/client.rs | 25 | ||||
| -rw-r--r-- | src/config.rs | 117 | ||||
| -rw-r--r-- | src/editor.rs | 86 | ||||
| -rw-r--r-- | src/gui/tool_sidebar.rs | 42 | ||||
| -rw-r--r-- | src/input/binding.rs | 123 | ||||
| -rw-r--r-- | src/input/button.rs (renamed from src/button.rs) | 38 | ||||
| -rw-r--r-- | src/input/mod.rs | 200 | ||||
| -rw-r--r-- | src/math/rect.rs | 9 | ||||
| -rw-r--r-- | src/math/surface.rs | 4 | ||||
| -rw-r--r-- | src/stable_vec.rs | 107 | ||||
| -rw-r--r-- | src/tool/icon_tool.rs | 21 | ||||
| -rw-r--r-- | src/tool/mod.rs | 4 |
13 files changed, 678 insertions, 192 deletions
diff --git a/src/cli/mod.rs b/src/cli/mod.rs index a654e19..370a30b 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -12,81 +12,99 @@ pub mod cmd; pub use self::cmd::*; use crate::colours::DEFAULT_COLOURS; +use crate::input::{Button, Input}; use crate::math::Vec2; use crate::Editor; use raylib::drawing::{RaylibDraw, RaylibDrawHandle}; -use raylib::ffi::KeyboardKey; -use raylib::RaylibHandle; +use std::sync::mpsc::Receiver; /// The command line interface. Should be created only once per program instance. pub struct CLI { text: String, - active: bool, + char_pipe: Option<Receiver<char>>, } impl CLI { /// Create a CLI for this instance - #[allow(clippy::new_without_default)] - pub fn new() -> Self { + pub fn new(input: &mut Input) -> Self { + input.add_global(Button::Text(':').into()); + Self { text: String::new(), - active: false, + char_pipe: None, } } /// Activates the CLI, which will now capture keyboard input and execute commands accordingly. - pub fn activate(&mut self) { - if !self.active { - self.text = ";".to_owned(); - self.active = true; + pub fn try_activate(&mut self, input: &mut Input) { + if !self.active() { + self.char_pipe = input.try_capture_keyboard(); + + if self.char_pipe.is_some() { + self.text = ":".to_owned(); + } } } + /// Deactivate the command line. + pub fn deactivate(&mut self) { + // Hang up, the input handler will get the message. + self.char_pipe = None; + } + /// Checks if the CLI is currently active. This means input to other things should be ignored. pub fn active(&self) -> bool { - self.active + self.char_pipe.is_some() + } + + fn perform_command(&mut self, editor: &mut Editor) { + match cmd::parse_command(&self.text[1..]) { + Ok(cmd) => match cmd.process(editor) { + Ok(res) => self.text = format!("SUCCESS: {}", res), + Err(err) => self.text = format!("ERROR: {}", err), + }, + Err(err) => self.text = format!("SYNTAX ERROR: {}", err), + } } /// Handle input for the command line and perform any commands the user may want to run. - pub fn update(&mut self, rl: &mut RaylibHandle, editor: &mut Editor) { + pub fn update(&mut self, editor: &mut Editor, input: &mut Input) { /* Check if the CLI is currently active. If not and it should not be activated according to * keyboard input, there is nothing to do. */ - if !self.active { - if rl.is_key_pressed(KeyboardKey::KEY_SEMICOLON) { - // Don't write the keypress again. - rl.get_key_pressed(); - self.activate(); + if !self.active() { + if input.poll_global(&Button::Text(':').into()) { + self.try_activate(input); + if !self.active() { + return; + } } else { return; } } - // The CLI is currently active. Handle input to it. - if let Some(key) = rl.get_key_pressed_number() { - self.text.push(key as u8 as char); - } else if rl.is_key_pressed(KeyboardKey::KEY_BACKSPACE) { - self.text.pop(); - } else if rl.is_key_pressed(KeyboardKey::KEY_ESCAPE) { - self.text.clear(); + let rx = self + .char_pipe + .as_mut() + .expect("No character pipe eventhough CLI should be active"); + + if let Ok(c) = rx.try_recv() { + match c { + '\x1B' => self.text.clear(), // Escape + '\x7f' => { + self.text.pop(); + } // Backspace + '\n' => { + self.perform_command(editor); + self.deactivate(); + } + c => self.text.push(c), + } } // When the text is empty, there is also no command marker, so set as inactive and leave. if self.text.is_empty() { - self.active = false; - return; - } - - // Perform the entered command, when the enter-key is pressed. - if rl.is_key_pressed(KeyboardKey::KEY_ENTER) { - self.active = false; - match cmd::parse_command(&self.text[1..]) { - Ok(cmd) => match cmd.process(editor) { - Ok(res) => self.text = format!("SUCCESS: {}", res), - Err(err) => self.text = format!("ERROR: {}", err), - }, - Err(err) => self.text = format!("SYNTAX ERROR: {}", err), - } + self.deactivate() } } diff --git a/src/client.rs b/src/client.rs index e100340..4ed5581 100644 --- a/src/client.rs +++ b/src/client.rs @@ -21,17 +21,18 @@ #[macro_use] extern crate log; -pub mod button; pub mod cli; pub mod colours; pub mod config; pub mod editor; pub mod grid; pub mod gui; +pub mod input; pub mod map; pub mod math; pub mod server; pub mod snapping; +pub mod stable_vec; pub mod svg; pub mod tool; pub mod transform; @@ -42,6 +43,7 @@ use config::Config; use editor::Editor; use float_cmp::F64Margin; use gui::{DimensionIndicator, ToolSidebar}; +use input::Input; use raylib::prelude::*; use snapping::Snapper; use std::ffi::CString; @@ -97,11 +99,13 @@ fn main() { &CString::new(GUI_STYLE).expect("Could not create C string from style file name"), )); + let mut input = Input::new(&rl); + config::register_bindings(&config, &mut input); let mut editor = Editor::new(&mut rl, &thread, config); let mut dimension_indicator = DimensionIndicator::new(); - let tool_sidebar = ToolSidebar::new(&mut rl, &thread); + let mut tool_sidebar = ToolSidebar::new(&mut rl, &thread, &mut input); let mut snapper = Snapper::default(); - let mut cli = CLI::new(); + let mut cli = CLI::new(&mut input); let mut transform = Transform::new(); let mut last_mouse_pos = rl.get_mouse_position(); @@ -109,6 +113,8 @@ fn main() { let screen_width = rl.get_screen_width(); let screen_height = rl.get_screen_height(); + input.update(&mut rl); + // Move the canvas together with the mouse if rl.is_mouse_button_down(MouseButton::MOUSE_MIDDLE_BUTTON) { transform.move_by_px(&(rl.get_mouse_position() - last_mouse_pos).into()); @@ -126,16 +132,11 @@ fn main() { ); } - cli.update(&mut rl, &mut editor); + cli.update(&mut editor, &mut input); dimension_indicator.update(editor.map_mut(), &mut rl); snapper.update(&mut rl, cli.active()); - editor.update( - &mut rl, - &transform, - &snapper, - ToolSidebar::mouse_captured(screen_height as u16, last_mouse_pos.into()), - cli.active(), - ); + editor.update(&mut rl, &transform, &snapper, &mut input); + tool_sidebar.update(screen_height as u16, &mut input); // Drawing section { @@ -145,7 +146,7 @@ fn main() { editor.map().draw(&mut d, &transform); editor.draw_tools(&mut d, &transform); - tool_sidebar.draw(screen_height as u16, &mut d, &mut editor); + tool_sidebar.draw(&mut d, &mut editor); snapper.draw(&mut d); gui::position_indicator_draw(&mut d, last_mouse_pos.into(), &transform, &snapper); dimension_indicator.draw(&mut d, &transform); diff --git a/src/config.rs b/src/config.rs index b5abb1e..6d0680c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,6 +1,6 @@ //! Home of the user configuratable content of graf karto, like keybindings and (TODO) colours etc. -use crate::button::*; +use crate::input::{Binding, Button, Input, MouseButton, Scancode}; use ron::de::from_reader; use ron::ser::{to_string_pretty, PrettyConfig}; use serde::{Deserialize, Serialize}; @@ -12,49 +12,49 @@ use std::path::Path; #[allow(missing_docs)] #[derive(Deserialize, Serialize)] pub struct Config { - pub tool_activation_keys: ToolActivationKeys, - pub tool_general_keys: ToolGeneralKeys, - pub icon_keys: IconToolKeys, + pub tool_activation_binds: ToolActivationBinds, + pub tool_general_binds: ToolGeneralBinds, + pub icon_binds: IconToolBinds, } #[allow(missing_docs)] #[derive(Deserialize, Serialize)] /// The keys used to activate the individual tools. These keystrokes will not be sent to the tools, /// but instead will be handled by the editor where the tools are registered. -pub struct ToolActivationKeys { - pub deletion: Button, - pub icon: Button, - pub polygon_room: Button, - pub rect_room: Button, - pub selection: Button, - pub wall: Button, +pub struct ToolActivationBinds { + pub deletion: Binding, + pub icon: Binding, + pub polygon_room: Binding, + pub rect_room: Binding, + pub selection: Binding, + pub wall: Binding, } #[derive(Deserialize, Serialize)] /// Keys that are useful to most tools. These are packaged so that not every tool has the same n keys /// and then some more. -pub struct ToolGeneralKeys { +pub struct ToolGeneralBinds { /// Keybinding to, where applicable, place a single node (usually a vertex) for the tool in /// question. - pub place_single: Button, + pub place_single: Binding, /// Finish up whatever one is doing with the current tool, without removing information. - pub finish: Button, + pub finish: Binding, /// Abort whatever one is doing with the current tool which means the last atomic action will not /// be pushed into the map items. - pub abort: Button, + pub abort: Binding, } #[derive(Clone, Serialize, Deserialize)] /// Key bindings that are individually interesting to the icon tool. -pub struct IconToolKeys { +pub struct IconToolBinds { /// Key to change to the next icon of the icon list. - pub next: Button, + pub next: Binding, /// Key to change to the previous icon of the icon list. - pub previous: Button, + pub previous: Binding, /// Rotate the working icon clockwise by a certain amount (currently 45 degrees) - pub rotate_clockwise: Button, + pub rotate_clockwise: Binding, /// Rotate the working icon counterclockwise by a certain amount (currently 45 degrees) - pub rotate_counterclockwise: Button, + pub rotate_counterclockwise: Binding, } impl Config { @@ -94,27 +94,72 @@ impl Config { } } +/// Registers all bindings from the given configuration into the input handler. Should the +/// configuration change at runtime, the global bindings of the input handler need to be cleared and +/// this function must be called again. +pub fn register_bindings(config: &Config, input: &mut Input) { + if !input.add_global(config.tool_activation_binds.deletion.clone()) { + warn!("Tried to add deletion binding twice."); + } + if !input.add_global(config.tool_activation_binds.icon.clone()) { + warn!("Tried to add icon binding twice."); + } + if !input.add_global(config.tool_activation_binds.polygon_room.clone()) { + warn!("Tried to add polygon room binding twice."); + } + if !input.add_global(config.tool_activation_binds.rect_room.clone()) { + warn!("Tried to add rect room binding twice."); + } + if !input.add_global(config.tool_activation_binds.selection.clone()) { + warn!("Tried to add selection binding twice."); + } + if !input.add_global(config.tool_activation_binds.wall.clone()) { + warn!("Tried to add wall binding twice."); + } + if !input.add_global(config.tool_general_binds.place_single.clone()) { + warn!("Tried to add place single binding twice."); + } + if !input.add_global(config.tool_general_binds.finish.clone()) { + warn!("Tried to add finish binding twice."); + } + if !input.add_global(config.tool_general_binds.abort.clone()) { + warn!("Tried to add abort binding twice."); + } + if !input.add_global(config.icon_binds.next.clone()) { + warn!("Tried to add next binding twice."); + } + if !input.add_global(config.icon_binds.previous.clone()) { + warn!("Tried to add previous binding twice."); + } + if !input.add_global(config.icon_binds.rotate_clockwise.clone()) { + warn!("Tried to add rotate clockwise binding twice."); + } + if !input.add_global(config.icon_binds.rotate_counterclockwise.clone()) { + warn!("Tried to add rotate counterclockwise binding twice."); + } +} + impl Default for Config { fn default() -> Self { Config { - tool_activation_keys: ToolActivationKeys { - deletion: Button::Keyboard(KeyboardKey::D), - icon: Button::Keyboard(KeyboardKey::I), - polygon_room: Button::Keyboard(KeyboardKey::P), - rect_room: Button::Keyboard(KeyboardKey::R), - selection: Button::Keyboard(KeyboardKey::S), - wall: Button::Keyboard(KeyboardKey::W), + tool_activation_binds: ToolActivationBinds { + deletion: Button::Text('d').into(), + icon: Button::Text('i').into(), + polygon_room: Button::Text('p').into(), + rect_room: Button::Text('r').into(), + selection: Button::Text('s').into(), + wall: Button::Text('w').into(), }, - tool_general_keys: ToolGeneralKeys { - place_single: Button::Mouse(MouseButton::Left), - finish: Button::Keyboard(KeyboardKey::Enter), - abort: Button::Mouse(MouseButton::Right), + tool_general_binds: ToolGeneralBinds { + place_single: Button::Mouse(MouseButton::Left).into(), + finish: Button::Scancode(Scancode::Enter).into(), + abort: Button::Mouse(MouseButton::Right).into(), }, - icon_keys: IconToolKeys { - next: Button::Keyboard(KeyboardKey::I), - previous: Button::Keyboard(KeyboardKey::J), - rotate_clockwise: Button::Mouse(MouseButton::Right), - rotate_counterclockwise: Button::Keyboard(KeyboardKey::Minus), + icon_binds: IconToolBinds { + next: Button::Text('j').into(), + previous: Button::Text('k').into(), + rotate_clockwise: Button::Text('+').into(), + rotate_counterclockwise: Button::Text('-').into(), }, } } diff --git a/src/editor.rs b/src/editor.rs index abbc401..7bf8f5e 100644 --- a/src/editor.rs +++ b/src/editor.rs @@ -5,8 +5,8 @@ //! currently a difference between things that are being created (inside the editor) and things that //! are part of the environment (the map). -use crate::button::{Button, MouseButton}; use crate::config::Config; +use crate::input::{Binding, Input}; use crate::map::Map; use crate::snapping::Snapper; use crate::tool::*; @@ -20,7 +20,7 @@ pub struct Editor { map: Map, /// HashMap that matches the ToolType with its proper activation key and of course the tool /// itself. - tools: HashMap<ToolType, (Box<dyn Tool>, Button)>, + tools: HashMap<ToolType, (Box<dyn Tool>, Binding)>, active: ToolType, config: Config, } @@ -31,46 +31,52 @@ impl Editor { pub fn new(rl: &mut RaylibHandle, rlt: &RaylibThread, config: Config) -> Self { let map = Map::new(rl, rlt); - let mut tools: HashMap<ToolType, (Box<dyn Tool>, Button)> = + let mut tools: HashMap<ToolType, (Box<dyn Tool>, Binding)> = HashMap::with_capacity(ToolType::NumTools as usize); tools.insert( ToolType::RectRoomTool, ( Box::new(RectRoomTool::new()), - config.tool_activation_keys.rect_room, + config.tool_activation_binds.rect_room.clone(), ), ); tools.insert( ToolType::PolygonRoomTool, ( Box::new(PolygonRoomTool::new()), - config.tool_activation_keys.polygon_room, + config.tool_activation_binds.polygon_room.clone(), ), ); tools.insert( ToolType::WallTool, - (Box::new(WallTool::new()), config.tool_activation_keys.wall), + ( + Box::new(WallTool::new()), + config.tool_activation_binds.wall.clone(), + ), ); tools.insert( ToolType::IconTool, ( - Box::new(IconTool::new(config.icon_keys.clone(), map.icon_renderer())), - config.tool_activation_keys.icon, + Box::new(IconTool::new( + config.icon_binds.clone(), + map.icon_renderer(), + )), + config.tool_activation_binds.icon.clone(), ), ); tools.insert( ToolType::DeletionTool, ( Box::new(DeletionTool::new()), - config.tool_activation_keys.deletion, + config.tool_activation_binds.deletion.clone(), ), ); tools.insert( ToolType::SelectionTool, ( Box::new(SelectionTool::new()), - config.tool_activation_keys.selection, + config.tool_activation_binds.selection.clone(), ), ); @@ -111,22 +117,19 @@ impl Editor { rl: &mut RaylibHandle, transform: &Transform, snapper: &Snapper, - mouse_blocked: bool, - keyboard_captured: bool, + input: &mut Input, ) { // Handle keybindings for tool change - if !keyboard_captured { - for (&tool_type, (_, activation_key)) in self.tools.iter() { - if activation_key.is_pressed(rl, false) { - // Don't do anything if the tool does not change. - if tool_type == self.active { - break; - } - - // Activate the tool of which the key binding has been pressed. - self.set_active(tool_type); + for (&tool_type, (_, activation_bind)) in self.tools.iter() { + if input.poll_global(&activation_bind) { + // Don't do anything if the tool does not change. + if tool_type == self.active { break; } + + // Activate the tool of which the key binding has been pressed. + self.set_active(tool_type); + break; } } @@ -138,49 +141,18 @@ impl Editor { active_tool.update(&self.map, &snapped_mouse_pos); // Handle common keybindings many of the tools have. - if self - .config - .tool_general_keys - .place_single - .is_pressed(rl, mouse_blocked) - { + if input.poll_global(&self.config.tool_general_binds.place_single) { active_tool.place_single(&mut self.map, &snapped_mouse_pos); } - if self - .config - .tool_general_keys - .finish - .is_pressed(rl, mouse_blocked) - { + if input.poll_global(&self.config.tool_general_binds.finish) { active_tool.finish(&mut self.map); } - if self - .config - .tool_general_keys - .abort - .is_pressed(rl, mouse_blocked) - { + if input.poll_global(&self.config.tool_general_binds.abort) { active_tool.abort(); } // Handle custom keybindings in case the tool has any. - let latest_button = if let Some(keyboard_key) = rl.get_key_pressed() { - Some(Button::Keyboard(keyboard_key.into())) - } else { - let mouse_buttons = [ - Button::Mouse(MouseButton::Left), - Button::Mouse(MouseButton::Middle), - Button::Mouse(MouseButton::Right), - ]; - mouse_buttons - .iter() - .find(|button| button.is_pressed(rl, mouse_blocked)) - .copied() - }; - - if let Some(latest_button) = latest_button { - active_tool.on_button_pressed(&mut self.map, latest_button); - } + active_tool.handle_custom_bindings(&mut self.map, input); } /// Draw all tools and in case of the active tool also what is currently being edited by it, if diff --git a/src/gui/tool_sidebar.rs b/src/gui/tool_sidebar.rs index 78041e7..af6af74 100644 --- a/src/gui/tool_sidebar.rs +++ b/src/gui/tool_sidebar.rs @@ -3,7 +3,8 @@ // TODO: Currently, the keyboard shortcuts for tools are handled by the editor, but a lot speaks for // them being handled by the ToolSidebar instead. -use crate::math::{ExactSurface, Rect, Vec2}; +use crate::input::Input; +use crate::math::Rect; use crate::tool::ToolType; use crate::Editor; use raylib::core::texture::Texture2D; @@ -17,37 +18,56 @@ pub const BUTTON_FILE: &str = "assets/button/tool_buttons.png"; /// Sidebar that renders and handles input for the tool activation buttons. pub struct ToolSidebar { button_texture: Texture2D, + bindings_id: usize, + panel_rect: Rect<u16>, } impl ToolSidebar { /// Create a new tool sidebar. There should be only one sidebar per program instance. - pub fn new(rl: &mut RaylibHandle, rlt: &RaylibThread) -> Self { + pub fn new(rl: &mut RaylibHandle, rlt: &RaylibThread, input: &mut Input) -> Self { let button_texture = rl .load_texture(rlt, BUTTON_FILE) .expect("Could not read file containing tool icons."); - Self { button_texture } + let panel_rect = Self::panel_rect(rl.get_screen_height() as u16); + let bindings_id = input.add_local_handler(panel_rect.clone()); + + Self { + button_texture, + bindings_id, + panel_rect, + } } - fn panel_rect(screen_height: u16) -> Rect<f32> { + fn panel_rect(screen_height: u16) -> Rect<u16> { /* The width is currently hardcoded as 104, which is * 64 (button-size) + 20 left gap + 20 right gap */ - Rect::new(0., 0., 104., screen_height as f32) + Rect::new(0, 0, 104, screen_height) } - /// Check if the mouse is currently being captured by this GUI-element. In that case, - /// everything else that might want to access the mouse will be blocked. - pub fn mouse_captured(screen_height: u16, mouse_pos: Vec2<f32>) -> bool { - Self::panel_rect(screen_height).contains_point(&mouse_pos) + /// Update the state of the tool sidebar. Due to raylib limitations, this is not where the tools + /// are selected for the editor, which happens in draw. + pub fn update(&mut self, screen_height: u16, input: &mut Input) { + let new_panel_rect = Self::panel_rect(screen_height); + if new_panel_rect != self.panel_rect { + self.panel_rect = new_panel_rect; + input.set_binding_rect(self.bindings_id, self.panel_rect); + } } /// Draw the tool buttons and encasing panel. Because of the way raylib works, this also handles /// clicking on tool buttons, which may be changed in the future, should a different gui be /// chosen. - pub fn draw(&self, screen_height: u16, rld: &mut impl RaylibDrawGui, editor: &mut Editor) { - rld.gui_panel(Self::panel_rect(screen_height)); + pub fn draw(&self, rld: &mut impl RaylibDrawGui, editor: &mut Editor) { + rld.gui_panel(Rect::new( + self.panel_rect.x as f32, + self.panel_rect.y as f32, + self.panel_rect.w as f32, + self.panel_rect.h as f32, + )); + // TODO: Update to new input system. Create buttons that integrate. let mut active = editor.active(); for i in 0..ToolType::NumTools as usize { let is_current_active = active as usize == i; diff --git a/src/input/binding.rs b/src/input/binding.rs new file mode 100644 index 0000000..386fb66 --- /dev/null +++ b/src/input/binding.rs @@ -0,0 +1,123 @@ +//! Bindings module, which is a key combination that does something when pressed. + +use super::Button; +use raylib::RaylibHandle; +use serde::{Deserialize, Serialize}; + +/// Binding struct, which holds any number of buttons (keyboard and mouse may be mixed, if desired) +#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct Binding { + buttons: Vec<Button>, +} + +impl Binding { + /// Create a new binding from a range of buttons. The button order does not matter, but at least + /// one button must be supplied. + pub fn new(buttons: Vec<Button>) -> Self { + if buttons.is_empty() { + panic!("Tried to create a binding without any keys."); + } + + Self { buttons } + } + + /// Returns `true` if only mouse buttons are present in this binding, otherwise false. + pub fn mouse_only(&self) -> bool { + for button in &self.buttons { + match button { + Button::Mouse(_) => continue, + _ => return false, + } + } + + true + } + + /// Returns `true` if only keyboard buttons are present in this binding, otherwise false. + pub fn keyboard_only(&self) -> bool { + for button in &self.buttons { + match button { + Button::Scancode(_) | Button::Text(_) => continue, + _ => return false, + } + } + + true + } + + /// Returns `true` if at least one mouse button is required for this binding to work. + pub fn has_mouse_component(&self) -> bool { + self.buttons.iter().any(|b| { + if let Button::Mouse(_) = b { + true + } else { + false + } + }) + } + + /// Returns `true` if at least one keyboard button is required for this binding to work. + pub fn has_keyboard_component(&self) -> bool { + self.buttons.iter().any(|b| match b { + Button::Scancode(_) | Button::Text(_) => true, + _ => false, + }) + } + + /// Checks if this binding was pressed this frame. Heavily dependent on input struct working + /// correctly. + pub(super) fn is_pressed( + &self, + allow_mouse: bool, + allow_keyboard: bool, + text: &str, + rl: &RaylibHandle, + ) -> bool { + let mut distinct_press = false; + for button in &self.buttons { + match *button { + Button::Mouse(mouse_button) => { + if !allow_mouse || !rl.is_mouse_button_down(mouse_button.into()) { + return false; + } + + /* Check if the button has been pressed in this frame exactly. + * This prevents activating the same keybinding every frame + * while the buttons are being held down. + */ + if rl.is_mouse_button_pressed(mouse_button.into()) { + distinct_press = true; + } + } + Button::Scancode(code) => { + if !allow_keyboard || !rl.is_key_down(code.into()) { + return false; + } + + // Check the same as with the mouse button. + if rl.is_key_pressed(code.into()) { + distinct_press = true; + } + } + Button::Text(c) => { + if !allow_keyboard || !text.contains(c) { + return false; + } + + // Always distinct, since on triggering, the text is cleared. + distinct_press = true; + } + } + } + + distinct_press + } +} + +impl From<Button> for Binding { + fn from(button: Button) -> Self { + Self { + buttons: vec![button], + } + } +} diff --git a/src/button.rs b/src/input/button.rs index 846377e..e9ef45e 100644 --- a/src/button.rs +++ b/src/input/button.rs @@ -3,18 +3,24 @@ //! handle it, feel free. use raylib::ffi::{KeyboardKey as rlKeyboardKey, MouseButton as rlMouseButton}; -use raylib::RaylibHandle; use serde::{Deserialize, Serialize}; use std::mem; -/// Enum to abstract the distinction of keyboard keys and mouse keys that raylib does away so the -/// user has free reign over what key they use for what purpose. +/// Abstraction over different key-types. A binding can be constructed from this button-type or +/// multiple button-presses can be chained together to create a binding. Just be careful to not +/// have bindings where one binding is included in another. This includes bindings where on your +/// keyboard you have Scancode(Shift) + Scancode(A) and another binding of Text(A) (Text(a) would +/// be okay) #[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq, Hash)] pub enum Button { - /// Button on the mouse with internal mouse button representation of raylib. + /// A button on the mouse (raylib supports just three :/ ) Mouse(MouseButton), - /// Keyboard button with internal keyboard key representation of raylib. - Keyboard(KeyboardKey), + /// Scancode that is sent by the OS. This can change between OSes, but stays the same between + /// runs and layout changes in the keyboard. + Scancode(Scancode), + /// The text input read from the operating system. This means even characters composed or + /// non-ASCII characters can be used. I mean, who doesn't want to bind the wall tool to 壁? + Text(char), } #[allow(missing_docs)] @@ -29,7 +35,7 @@ pub enum MouseButton { #[allow(missing_docs)] #[repr(u32)] #[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq, Hash)] -pub enum KeyboardKey { +pub enum Scancode { Apostrophe = 39, Comma = 44, Minus = 45, @@ -137,18 +143,6 @@ pub enum KeyboardKey { KeypadEqual = 336, } -impl Button { - /// Check if this button is pressed. If `mouse_blocked` is true, mouse buttons are ignored which - /// is useful when an element has captured the mouse, but other elements are still queried in the - /// background. - pub fn is_pressed(self, rl: &RaylibHandle, mouse_blocked: bool) -> bool { - match self { - Self::Mouse(button) => !mouse_blocked && rl.is_mouse_button_pressed(button.into()), - Self::Keyboard(key) => rl.is_key_pressed(key.into()), - } - } -} - impl From<rlMouseButton> for Button { fn from(button: rlMouseButton) -> Self { Self::Mouse(MouseButton::from(button)) @@ -156,7 +150,7 @@ impl From<rlMouseButton> for Button { } impl From<rlKeyboardKey> for Button { fn from(key: rlKeyboardKey) -> Self { - Self::Keyboard(KeyboardKey::from(key)) + Self::Scancode(Scancode::from(key)) } } @@ -171,12 +165,12 @@ impl Into<rlMouseButton> for MouseButton { } } -impl From<rlKeyboardKey> for KeyboardKey { +impl From<rlKeyboardKey> for Scancode { fn from(key: rlKeyboardKey) -> Self { unsafe { mem::transmute(key as u32) } } } -impl Into<rlKeyboardKey> for KeyboardKey { +impl Into<rlKeyboardKey> for Scancode { fn into(self) -> rlKeyboardKey { unsafe { mem::transmute(self as u32) } } diff --git a/src/input/mod.rs b/src/input/mod.rs new file mode 100644 index 0000000..e8b1821 --- /dev/null +++ b/src/input/mod.rs @@ -0,0 +1,200 @@ +//! Input with binding abstraction +//! +//! Binding keys or combinations to specific actions with just raylib alone is difficult to handle in +//! input-heavy applications, as opposed to games. This is an optimisation effort. To understand how +//! it works, the first thing to know is that there are two main modes for bindings, which are local +//! and global. A local binding is specific to a certain area of the window and is used to block the +//! mouse from being sent to many different targets (think of a pop-up window over the editor, which +//! must capture the mouse). Global bindings will be processed as long as no local binding has +//! prevalence, but they do not have an area that needs to be managed by a handler. +//! +//! In summary, the local <-> global distinction is used to capture the mouse. +//! +//! Some elements want to capture the keyboard, for instance, when activating a text box, the text +//! input should only go to this box, but should a tool be bound to a character, it should not +//! activate when typing. For this purpose, any element may seize control as long as no other element +//! still has the focus. A channel is opened and no bindings will be processed. Instead the text +//! together with a few control characters is relayed directly to the channel, until the receiver +//! hangs up. +//! +//! In summary, a channel is used to seize control of the keyboard when typing into an element. + +pub mod binding; +pub mod button; + +pub use binding::*; +pub use button::*; + +use crate::math::{ExactSurface, Rect, Vec2}; +use crate::stable_vec::StableVec; +use raylib::ffi::KeyboardKey; +use raylib::RaylibHandle; +use std::collections::HashMap; +use std::sync::mpsc::{self, Receiver, Sender}; + +/// Input and binding handler this should only be created once per instance. +pub struct Input { + global_bindings: HashMap<Binding, bool>, + local_bindings: StableVec<(Rect<u16>, HashMap<Binding, bool>)>, + last_text: String, + text_pipe: Option<Sender<char>>, + mouse_pos: Vec2<u16>, +} + +impl Input { + /// Create a new Input and binding handler. + pub fn new(rl: &RaylibHandle) -> Self { + Self { + global_bindings: HashMap::new(), + local_bindings: StableVec::new(), + last_text: String::new(), + text_pipe: None, + mouse_pos: Vec2::new(rl.get_mouse_x() as u16, rl.get_mouse_y() as u16), + } + } + + /// Must be called on every frame of the program, since keypresses will be processed here. This + /// will not activate the binding function directly, since raylib is heavily polling focused. + pub fn update(&mut self, rl: &mut RaylibHandle) { + self.mouse_pos = Vec2::new(rl.get_mouse_x() as u16, rl.get_mouse_y() as u16); + /* Read the next character to be sent with some extra characters + * raylib doesn't recognize to be valid. + */ + let c = if rl.is_key_pressed(KeyboardKey::KEY_ENTER) { + Some('\n') + } else if rl.is_key_pressed(KeyboardKey::KEY_ESCAPE) { + Some('\x1B') + } else if rl.is_key_pressed(KeyboardKey::KEY_BACKSPACE) { + Some('\x7f') + } else { + rl.get_key_pressed_number().map(|c| c as u8 as char) + }; + + /* Send the character to the listening entity or push it to the text that + * is currently being read for the keybindings. + */ + if let Some(text_pipe) = self.text_pipe.as_mut() { + if let Some(c) = c { + if text_pipe.send(c).is_err() { + self.last_text.push(c); + self.text_pipe = None; + } + } + } else if let Some(c) = c { + self.last_text.push(c); + } + + /* Update the local parts. The local stack has priority over the global + * bindings, so it is processed first, with the priority going from the + * top of the stack to the bottom in that order (reversed vec order) + */ + let mut mouse_blocked = false; + for (_, (rect, bindings)) in self.local_bindings.id_iter_mut().rev() { + if rect.contains_point(&self.mouse_pos) { + for (binding, state) in &mut bindings.iter_mut() { + *state = binding.is_pressed( + !mouse_blocked, + self.text_pipe.is_none(), + &self.last_text, + rl, + ); + + if *state { + self.last_text.clear(); + } + } + + mouse_blocked = true; + break; + } + } + + /* Process the global bindings, as long as nothing prevents the bindings + * from being processed like a local binding or the text being captured. + */ + for (binding, state) in self.global_bindings.iter_mut() { + *state = binding.is_pressed( + !mouse_blocked, + self.text_pipe.is_none(), + &self.last_text, + rl, + ); + + if *state { + self.last_text.clear(); + } + } + } + + /// Add a global binding. This is necessary so the input knows which key presses to monitor. + pub fn add_global(&mut self, binding: Binding) -> bool { + self.global_bindings.insert(binding, false).is_none() + } + + /// Add a local binding handler for the given area. Returns a unique and unchanging handler id. + /// Handlers with higher ids (that have been added later) are preferred over old handlers. + pub fn add_local_handler(&mut self, area: Rect<u16>) -> usize { + self.local_bindings.push((area, HashMap::new())) + } + + /// Add a local binding for the given handler. + pub fn add_local(&mut self, handler_id: usize, binding: Binding) -> bool { + self.local_bindings + .get_mut(handler_id) + .expect("Handler does not exist") + .1 + .insert(binding, false) + .is_none() + } + + /// Update the binding rectangle of a handler. + pub fn set_binding_rect(&mut self, handler_id: usize, rect: Rect<u16>) { + self.local_bindings + .get_mut(handler_id) + .expect("Handler does not exist") + .0 = rect; + } + + /// Check if a global binding has been activated this frame. If so, it returns true. + /// This will only activate once, so there is no need to worry about multiple function calls + /// when the user keeps the button down. + pub fn poll_global(&mut self, binding: &Binding) -> bool { + let state = self.global_bindings.get_mut(&binding); + if state.is_none() { + error!("Tried to poll binding that isn't registered."); + return false; + } + + *state.unwrap() + } + + /// Like `poll_global` bun instead checks the bindings of the local handler with the given id. + pub fn poll_local(&mut self, handler_id: usize, binding: &Binding) -> bool { + let (_, bindings) = self + .local_bindings + .get_mut(handler_id) + .expect("Invalid binding handler id"); + + let state = bindings.get_mut(&binding); + if state.is_none() { + error!("Tried to poll binding that isn't registered."); + return false; + } + + *state.unwrap() + } + + /// Attempts to capture all keyboard input from here on. If no other component is currently + /// capturing, it returns a receiver that can be used. When the entity no longer wants to + /// capture the keyboard, control must be returned by dropping the receiver. + pub fn try_capture_keyboard(&mut self) -> Option<Receiver<char>> { + if self.text_pipe.is_some() { + return None; + } + + let (tx, rx) = mpsc::channel(); + self.text_pipe = Some(tx); + + Some(rx) + } +} diff --git a/src/math/rect.rs b/src/math/rect.rs index adb608b..a8326bc 100644 --- a/src/math/rect.rs +++ b/src/math/rect.rs @@ -3,9 +3,9 @@ use super::{ExactSurface, LineSegment, Polygon, Vec2}; //use alga::general::{Additive, Identity}; use nalgebra::{RealField, Scalar}; -use num_traits::{NumCast, ToPrimitive}; +use num_traits::{NumCast, ToPrimitive, Zero}; use serde::{Deserialize, Serialize}; -use std::ops::{Add, AddAssign}; +use std::ops::{Add, AddAssign, Sub}; /// Represents a Rectangle with the value type T. #[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] @@ -149,7 +149,10 @@ impl<T: Scalar + Copy> Rect<T> { } } -impl<T: RealField> ExactSurface<T> for Rect<T> { +impl<T: Scalar + Copy> ExactSurface<T> for Rect<T> +where + T: Add<Output = T> + PartialOrd + Sub<Output = T> + Zero, +{ fn contains_point(&self, point: &Vec2<T>) -> bool { point.x >= self.x && point.x <= self.x + self.w diff --git a/src/math/surface.rs b/src/math/surface.rs index 088ac47..21c3865 100644 --- a/src/math/surface.rs +++ b/src/math/surface.rs @@ -2,7 +2,7 @@ use super::{LineSegment, Polygon, Rect, Vec2}; use float_cmp::ApproxEq; -use nalgebra::RealField; +use nalgebra::{RealField, Scalar}; /// Trait that describes an area in the vector space on the field of T, with T unable to be /// used without rounding. @@ -28,7 +28,7 @@ where } /// The same as Surface, but the vector space will be assumed to be perfectly divideable or checkable. -pub trait ExactSurface<T: RealField> { +pub trait ExactSurface<T: Scalar + Copy> { /// Checks if a point lies on this surface. fn contains_point(&self, point: &Vec2<T>) -> bool; diff --git a/src/stable_vec.rs b/src/stable_vec.rs new file mode 100644 index 0000000..38eb162 --- /dev/null +++ b/src/stable_vec.rs @@ -0,0 +1,107 @@ +//! Stable vec is a vector that guarantees its content access position does not change. + +use std::ops::Deref; +use std::slice::IterMut; + +/// Works like Vec, but with an additional field, where the position information is saved. Does not +/// support inserting elements in arbitrary positions, since that may shift the data. Removing data +/// in the middle is fine, however. +pub struct StableVec<T> { + data: Vec<(usize, T)>, +} + +/// Mutable iterator over a stable vector. Similar to enumerate, but since only the elements actually +/// in the vector are iterated there can be holes in the enumeration (i -> i+1 not guaranteed). +pub struct IdIterMut<'a, T> { + internal: IterMut<'a, (usize, T)>, +} + +impl<T> StableVec<T> { + /// Create a new, empty StableVec + pub fn new() -> Self { + Self { data: Vec::new() } + } + + /// Add an item to the end of the vector. Returns its stable id. (`len()-1 != last_element_id`) + pub fn push(&mut self, item: T) -> usize { + if self.data.is_empty() { + self.data.push((0, item)); + 0 + } else { + let id = self.data.last().unwrap().0 + 1; + self.data.push((id, item)); + id + } + } + + // Find the internal position of the given id in `O(log n)` + fn find_pos(&self, id: usize) -> Result<usize, usize> { + self.data.binary_search_by(|x| x.0.cmp(&id)) + } + + /// Get the item with the given id from the vec, if it exists. Unlike in a normal vec, this is + /// not `O(1)` but `O(log n)`. + pub fn get(&self, id: usize) -> Option<&T> { + match self.find_pos(id) { + Ok(pos) => Some(&self.data[pos].1), + Err(_) => None, + } + } + + /// Get the item with the id mutably. Like `get()` this is also `O(log n)` + pub fn get_mut(&mut self, id: usize) -> Option<&mut T> { + match self.find_pos(id) { + Ok(pos) => Some(&mut self.data[pos].1), + Err(_) => None, + } + } + + /// Remove the item with the given id from the vector, returning it, if it existed. + pub fn remove(&mut self, id: usize) -> Option<T> { + match self.find_pos(id) { + Ok(pos) => Some(self.data.remove(pos).1), + Err(_) => None, + } + } + + /// Create an id enumerating iterator over the StableVec. + pub fn id_iter_mut(&mut self) -> IdIterMut<'_, T> { + IdIterMut::new(&mut self.data) + } +} + +impl<T> Default for StableVec<T> { + fn default() -> Self { + Self::new() + } +} + +impl<T> Deref for StableVec<T> { + type Target = Vec<(usize, T)>; + + fn deref(&self) -> &Self::Target { + &self.data + } +} + +impl<'a, T> IdIterMut<'a, T> { + pub(super) fn new(id_vec: &'a mut [(usize, T)]) -> Self { + Self { + internal: id_vec.iter_mut(), + } + } +} + +impl<'a, T> Iterator for IdIterMut<'a, T> { + type Item = (usize, &'a mut T); + + fn next(&mut self) -> Option<Self::Item> { + self.internal.next().map(|(id, item)| (*id, item)) + } +} + +impl<'a, T> DoubleEndedIterator for IdIterMut<'a, T> { + fn next_back(&mut self) -> Option<Self::Item> { + self.internal.next_back().map(|(id, item)| (*id, item)) + } +} diff --git a/src/tool/icon_tool.rs b/src/tool/icon_tool.rs index c9e671e..8b4afc0 100644 --- a/src/tool/icon_tool.rs +++ b/src/tool/icon_tool.rs @@ -1,8 +1,8 @@ //! Tool for creating icons. For explanation of icons, please see //! [the icon module](crate::map::icon). -use crate::button::Button; -use crate::config::IconToolKeys; +use crate::config::IconToolBinds; +use crate::input::Input; use crate::map::icon_renderer::IconRenderer; use crate::map::{Icon, Map, Mappable}; use crate::math::Vec2; @@ -13,7 +13,7 @@ use std::rc::Rc; /// The icon tool itself. pub struct IconTool { - keybindings: IconToolKeys, + keybindings: IconToolBinds, /// Saves whether the IconTool is the currently active tool or not. active: bool, /// The information of the icon that should be placed / is currently being placed, if it @@ -25,7 +25,7 @@ pub struct IconTool { impl IconTool { /// Create a new icon tool that renders icons with the provided icon renderer. There should only /// be one instance of the tool for the program, which should be created in the editor. - pub fn new(keybindings: IconToolKeys, renderer: Rc<IconRenderer>) -> Self { + pub fn new(keybindings: IconToolBinds, renderer: Rc<IconRenderer>) -> Self { Self { keybindings, active: false, @@ -58,15 +58,18 @@ impl Tool for IconTool { map.push_icon(self.current_icon.clone()); } - fn on_button_pressed(&mut self, _map: &mut Map, button: Button) { - if button == self.keybindings.next { + fn handle_custom_bindings(&mut self, _map: &mut Map, input: &mut Input) { + if input.poll_global(&self.keybindings.next) { self.current_icon.id = (self.current_icon.id + 1) % self.renderer.num_icons(); - } else if button == self.keybindings.previous { + } + if input.poll_global(&self.keybindings.previous) { self.current_icon.id = (self.current_icon.id + self.renderer.num_icons() - 1) % self.renderer.num_icons(); - } else if button == self.keybindings.rotate_clockwise { + } + if input.poll_global(&self.keybindings.rotate_clockwise) { self.current_icon.rotation += 45.; - } else if button == self.keybindings.rotate_counterclockwise { + } + if input.poll_global(&self.keybindings.rotate_counterclockwise) { self.current_icon.rotation -= 45.; } } diff --git a/src/tool/mod.rs b/src/tool/mod.rs index 4130244..b3fae80 100644 --- a/src/tool/mod.rs +++ b/src/tool/mod.rs @@ -20,7 +20,7 @@ pub use rect_room_tool::RectRoomTool; pub use selection_tool::SelectionTool; pub use wall_tool::WallTool; -use crate::button::Button; +use crate::input::Input; use crate::map::Map; use crate::math::Vec2; use crate::transform::Transform; @@ -82,5 +82,5 @@ pub trait Tool { /// If there are any additional keybindings that need to be handled by this tool, these can be /// handled here. - fn on_button_pressed(&mut self, _map: &mut Map, _button: Button) {} + fn handle_custom_bindings(&mut self, _map: &mut Map, _input: &mut Input) {} } |
