aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorArne Dußin2021-01-17 14:19:59 +0100
committerArne Dußin2021-01-17 14:19:59 +0100
commitb019d10df4080fdb0ab57445040d24f9b14abdac (patch)
tree268305b1024f8a15a88eb72f593fcfab0f0e8d61 /src
parentb58e965327deef14d6414a912bb6698c6f745ce9 (diff)
parent51b7747e62c189d430318c67368a5c84e50ece61 (diff)
downloadgraf_karto-b019d10df4080fdb0ab57445040d24f9b14abdac.tar.gz
graf_karto-b019d10df4080fdb0ab57445040d24f9b14abdac.zip
Merge branch 'master' into net
Diffstat (limited to 'src')
-rw-r--r--src/cli/mod.rs94
-rw-r--r--src/client.rs25
-rw-r--r--src/config.rs117
-rw-r--r--src/editor.rs86
-rw-r--r--src/gui/tool_sidebar.rs42
-rw-r--r--src/input/binding.rs123
-rw-r--r--src/input/button.rs (renamed from src/button.rs)38
-rw-r--r--src/input/mod.rs200
-rw-r--r--src/math/rect.rs9
-rw-r--r--src/math/surface.rs4
-rw-r--r--src/stable_vec.rs107
-rw-r--r--src/tool/icon_tool.rs21
-rw-r--r--src/tool/mod.rs4
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) {}
}