From f92e9f6f07b1e3834c2ca58ce3510734819d08e4 Mon Sep 17 00:00:00 2001 From: Arne Dußin Date: Wed, 27 Jan 2021 14:01:50 +0100 Subject: Rework graf karto to fit the client/server structure --- src/client/cli/cmd/edit.rs | 41 +++++ src/client/cli/cmd/mod.rs | 59 +++++++ src/client/cli/cmd/read.rs | 44 +++++ src/client/cli/cmd/write.rs | 41 +++++ src/client/cli/mod.rs | 128 ++++++++++++++ src/client/colours.rs | 158 +++++++++++++++++ src/client/config.rs | 166 ++++++++++++++++++ src/client/editor.rs | 189 ++++++++++++++++++++ src/client/grid.rs | 56 ++++++ src/client/gui/decimal_num_box.rs | 173 ++++++++++++++++++ src/client/gui/dimension_indicator.rs | 308 +++++++++++++++++++++++++++++++++ src/client/gui/mod.rs | 18 ++ src/client/gui/position_indicator.rs | 37 ++++ src/client/gui/tool_sidebar.rs | 91 ++++++++++ src/client/input/binding.rs | 123 +++++++++++++ src/client/input/button.rs | 177 +++++++++++++++++++ src/client/input/mod.rs | 200 +++++++++++++++++++++ src/client/map/icon_mark.rs | 95 ++++++++++ src/client/map/icon_texture_manager.rs | 91 ++++++++++ src/client/map/mappable.rs | 23 +++ src/client/map/mod.rs | 206 ++++++++++++++++++++++ src/client/map/room_mark.rs | 75 ++++++++ src/client/map/wall_mark.rs | 106 ++++++++++++ src/client/mod.rs | 137 +++++++++++++++ src/client/snapping.rs | 81 +++++++++ src/client/svg/mod.rs | 178 +++++++++++++++++++ src/client/svg/style.rs | 180 +++++++++++++++++++ src/client/tool/deletion_tool.rs | 82 +++++++++ src/client/tool/icon_tool.rs | 88 ++++++++++ src/client/tool/mod.rs | 98 +++++++++++ src/client/tool/polygon_room_tool.rs | 141 +++++++++++++++ src/client/tool/rect_room_tool.rs | 91 ++++++++++ src/client/tool/selection_tool.rs | 74 ++++++++ src/client/tool/wall_tool.rs | 76 ++++++++ src/client/transform.rs | 142 +++++++++++++++ 35 files changed, 3973 insertions(+) create mode 100644 src/client/cli/cmd/edit.rs create mode 100644 src/client/cli/cmd/mod.rs create mode 100644 src/client/cli/cmd/read.rs create mode 100644 src/client/cli/cmd/write.rs create mode 100644 src/client/cli/mod.rs create mode 100644 src/client/colours.rs create mode 100644 src/client/config.rs create mode 100644 src/client/editor.rs create mode 100644 src/client/grid.rs create mode 100644 src/client/gui/decimal_num_box.rs create mode 100644 src/client/gui/dimension_indicator.rs create mode 100644 src/client/gui/mod.rs create mode 100644 src/client/gui/position_indicator.rs create mode 100644 src/client/gui/tool_sidebar.rs create mode 100644 src/client/input/binding.rs create mode 100644 src/client/input/button.rs create mode 100644 src/client/input/mod.rs create mode 100644 src/client/map/icon_mark.rs create mode 100644 src/client/map/icon_texture_manager.rs create mode 100644 src/client/map/mappable.rs create mode 100644 src/client/map/mod.rs create mode 100644 src/client/map/room_mark.rs create mode 100644 src/client/map/wall_mark.rs create mode 100644 src/client/mod.rs create mode 100644 src/client/snapping.rs create mode 100644 src/client/svg/mod.rs create mode 100644 src/client/svg/style.rs create mode 100644 src/client/tool/deletion_tool.rs create mode 100644 src/client/tool/icon_tool.rs create mode 100644 src/client/tool/mod.rs create mode 100644 src/client/tool/polygon_room_tool.rs create mode 100644 src/client/tool/rect_room_tool.rs create mode 100644 src/client/tool/selection_tool.rs create mode 100644 src/client/tool/wall_tool.rs create mode 100644 src/client/transform.rs (limited to 'src/client') diff --git a/src/client/cli/cmd/edit.rs b/src/client/cli/cmd/edit.rs new file mode 100644 index 0000000..7a02959 --- /dev/null +++ b/src/client/cli/cmd/edit.rs @@ -0,0 +1,41 @@ +//! Replace the contents of the currently edited map with contents from a file. + +use super::Command; +use super::{CmdParseError, FromArgs}; +use crate::client::map::MapData; +use crate::client::Editor; +use std::path::PathBuf; + +/// Command to load a file from the disk and replace the current editor contents with it's info. +pub struct Edit { + file: PathBuf, +} + +impl FromArgs for Edit { + fn from_args(args: &[&str]) -> Result { + if args.len() != 1 { + return Err(CmdParseError::WrongNumberOfArgs(args.len(), 1..=1)); + } + + Ok(Self { + file: PathBuf::from(args[0]), + }) + } +} + +impl Command for Edit { + fn process(&self, editor: &mut Editor) -> Result { + let data = match MapData::load_from_file(&self.file) { + Ok(data) => data, + Err(err) => { + return Err(format!( + "Unable to read file: {:?}, reason: {:?}", + &self.file, err + )) + } + }; + + editor.map_mut().set_data(data); + Ok(format!("Map data from {:?} loaded.", &self.file)) + } +} diff --git a/src/client/cli/cmd/mod.rs b/src/client/cli/cmd/mod.rs new file mode 100644 index 0000000..b403772 --- /dev/null +++ b/src/client/cli/cmd/mod.rs @@ -0,0 +1,59 @@ +//! The commands that can be performed in the CLI + +pub mod edit; +pub mod read; +pub mod write; + +pub use edit::*; +pub use read::*; +pub use write::*; + +use crate::client::Editor; +use std::ops::RangeInclusive; + +/// Errors that can occur when parsing a command. This is for syntax checking, the +/// semantics are checked when trying to execute the command. +#[allow(missing_docs)] +#[derive(thiserror::Error, Debug)] +pub enum CmdParseError { + #[error("no command specified")] + StringEmpty, + #[error("the command {0} is unknown")] + NoSuchCmd(String), + #[error("wrong number of arguments. Expected in range {1:?}, but received {0}")] + WrongNumberOfArgs(usize, RangeInclusive), + #[error("{0} cannot be converted into a {1}, which is required")] + InvalidArgType(String, &'static str), +} + +/// Attempts to parse a command from the given string. If it is unsuccessful, it returns a +/// [CmdParseError]. +pub fn parse_command(string: &str) -> Result, CmdParseError> { + if string.is_empty() { + return Err(CmdParseError::StringEmpty); + } + + let parts: Vec<&str> = string.split_whitespace().collect(); + match parts[0] { + "w" => Ok(Box::new(Write::from_args(&parts[1..])?)), + "e" => Ok(Box::new(Edit::from_args(&parts[1..])?)), + "r" => Ok(Box::new(Read::from_args(&parts[1..])?)), + other => Err(CmdParseError::NoSuchCmd(other.to_owned())), + } +} + +/// Indicates that this entity (command) can be created from arguments. Make sure to check what is +/// expected, to pass the arguments to the correct command. +pub trait FromArgs: Sized { + /// Creates a new instance from the arguments provided. If for whatever reason the syntax of the + /// given arguments is correct an [ArgParseError] is returned. + fn from_args(args: &[&str]) -> Result; +} + +/// A common trait for all commands. +pub trait Command { + /// Process this command on the provided context. Returns either a string with the output of the + /// command when everything went right with it, or an error string explaining what went wrong, + /// which can be displayed to the user. + fn process(&self, editor: &mut Editor) -> Result; +} diff --git a/src/client/cli/cmd/read.rs b/src/client/cli/cmd/read.rs new file mode 100644 index 0000000..313530a --- /dev/null +++ b/src/client/cli/cmd/read.rs @@ -0,0 +1,44 @@ +//! Read the contents of a file and add it to the currently edited map. + +use super::Command; +use super::{CmdParseError, FromArgs}; +use crate::client::Editor; +use crate::map::MapData; +use std::path::PathBuf; + +/// Command to read a file from the system +pub struct Read { + file: PathBuf, +} + +impl FromArgs for Read { + fn from_args(args: &[&str]) -> Result { + if args.len() != 1 { + return Err(CmdParseError::WrongNumberOfArgs(args.len(), 1..=1)); + } + + Ok(Self { + file: PathBuf::from(args[0]), + }) + } +} + +impl Command for Read { + fn process(&self, editor: &mut Editor) -> Result { + let data = match MapData::load_from_file(&self.file) { + Ok(data) => data, + Err(err) => { + return Err(format!( + "Unable to read file: {:?}, reason: {:?}", + &self.file, err + )) + } + }; + + editor.map_mut().add_data(data); + Ok(format!( + "Map data from {:?} read and added to the current buffer.", + &self.file + )) + } +} diff --git a/src/client/cli/cmd/write.rs b/src/client/cli/cmd/write.rs new file mode 100644 index 0000000..3114f63 --- /dev/null +++ b/src/client/cli/cmd/write.rs @@ -0,0 +1,41 @@ +//! Save the contents of the map to disk + +use super::Command; +use super::{CmdParseError, FromArgs}; +use crate::client::Editor; +use std::path::PathBuf; + +/// The save command can take any destination in the filesystem the user can write to. Processing +/// will then save the map contents to that destination, overwriting anything that may be there. +pub struct Write { + destination: PathBuf, +} + +impl FromArgs for Write { + fn from_args(args: &[&str]) -> Result { + if args.len() != 1 { + return Err(CmdParseError::WrongNumberOfArgs(args.len(), 1..=1)); + } + + Ok(Self { + destination: PathBuf::from(args[0]), + }) + } +} + +impl Command for Write { + fn process(&self, editor: &mut Editor) -> Result { + let data = MapData::extract_data(editor.map()); + + match data.write_to_file(&self.destination) { + Ok(_) => Ok(format!( + "Successfully wrote contents to `{:?}`", + &self.destination + )), + Err(e) => Err(format!( + "Unable to write to `{:?}`. Error: {:?}", + &self.destination, e + )), + } + } +} diff --git a/src/client/cli/mod.rs b/src/client/cli/mod.rs new file mode 100644 index 0000000..ed2a1bf --- /dev/null +++ b/src/client/cli/mod.rs @@ -0,0 +1,128 @@ +//! In-window Command line interface. Used for operations that are just easier than with GUI. +//! +//! Sometimes it is nice to have a GUI, for instance when a selection has to be made, things have to +//! be moved etc., however for operations like saving/loading and exporting, no such thing has to be +//! done and the GUI is really just slowing you down (at least in my opinion). For these operations, +//! it is much better to simply have a command do that specific thing. It is also much easier to +//! implement a new command, so features can be tested more quickly. For some things, there should +//! still be a GUI option. With the example of saving/loading, it is much easier to find some hidden +//! folder in a GUI, so that is definitely a consideration for the future. + +pub mod cmd; +pub use self::cmd::*; + +use crate::client::colours::DEFAULT_COLOURS; +use crate::client::input::{Button, Input}; +use crate::client::Editor; +use crate::math::Vec2; +use raylib::drawing::{RaylibDraw, RaylibDrawHandle}; +use std::sync::mpsc::Receiver; + +/// The command line interface. Should be created only once per program instance. +pub struct CLI { + text: String, + char_pipe: Option>, +} + +impl CLI { + /// Create a CLI for this instance + pub fn new(input: &mut Input) -> Self { + input.add_global(Button::Text(':').into()); + + Self { + text: String::new(), + char_pipe: None, + } + } + + /// Activates the CLI, which will now capture keyboard input and execute commands accordingly. + 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.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, 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 input.poll_global(&Button::Text(':').into()) { + self.try_activate(input); + if !self.active() { + return; + } + } else { + return; + } + } + + 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.deactivate() + } + } + + /// Draw the command line at the bottom of the window. + pub fn draw(&self, rld: &mut RaylibDrawHandle) { + let pos = Vec2::new(150., rld.get_screen_height() as f32 - 25.); + + rld.draw_rectangle_v( + pos, + Vec2::new(rld.get_screen_width() as f32 - pos.x, 25.), + DEFAULT_COLOURS.cli_background, + ); + rld.draw_text( + &self.text, + 155, + rld.get_screen_height() - 22, + 20, + DEFAULT_COLOURS.cli_foreground, + ); + } +} diff --git a/src/client/colours.rs b/src/client/colours.rs new file mode 100644 index 0000000..d7c728c --- /dev/null +++ b/src/client/colours.rs @@ -0,0 +1,158 @@ +//! The colour definitions used for items drawn in graf karto. + +use raylib::ffi::Color; + +/// Contains the default colours used throughout, if nothing else is set. +pub const DEFAULT_COLOURS: Colours = Colours::default(); + +/// All the different colours that may be used for different elements of the program. Contains one +/// entry for each colourable component. +pub struct Colours { + /// Colour the rectangle used for the deletion tool is filled with. + pub deletion_rect: Color, + /// The colour of the outline of the deletion tool rectangle. + pub deletion_rect_outline: Color, + /// The colour that is used for filling the selection tool's rectangle. + pub selection_rect: Color, + /// Colour of the selection tool rectangle outline. + pub selection_rect_outline: Color, + /// Colour of the rooms that are currently not selected. + pub room_normal: Color, + /// The Colour the rooms should be tinted in when they have been selected. + pub room_selected: Color, + /// Colour of the walls when they are not selected. + pub wall_normal: Color, + /// Colour of the walls when they have been selected. + pub wall_selected: Color, + /// Colour of the icons when they are not selected. + pub icon_normal: Color, + /// Colour of the icons when they are selected. + pub icon_selected: Color, + /// Colour used to draw the rulers (the ruling lines) of the dimension indicator. + pub dimension_indicators: Color, + /// Colour of the text used to display the size of the dimension indicators dimensions. + pub dimension_text: Color, + /// Colour the point to show where something is will be drawn in. + pub position_indicator: Color, + /// Colour that is used for the text stating the position of the position indicator in meters. + pub position_text: Color, + /// The colour used for drawing the lines of the grid which divides the map into chunks of evenly + /// spaced cells. + pub grid_lines: Color, + /// Color used to draw the background of the Command Line Interface + pub cli_background: Color, + /// Color used to draw the normal text of the Command Line Interface + pub cli_foreground: Color, +} + +impl Colours { + // NOTE: Unfortunately the default function cannot be made const, since Default is a trait. This + // feature is, as far as I can tell, planned in Rust, but not yet implemented. Once it is, Colours + // should implement Default instead. + const fn default() -> Self { + Self { + deletion_rect: Color { + r: 200, + g: 150, + b: 150, + a: 50, + }, + deletion_rect_outline: Color { + r: 200, + g: 150, + b: 150, + a: 150, + }, + selection_rect: Color { + r: 255, + g: 129, + b: 0, + a: 50, + }, + selection_rect_outline: Color { + r: 255, + g: 129, + b: 0, + a: 150, + }, + room_normal: Color { + r: 180, + g: 180, + b: 180, + a: 255, + }, + room_selected: Color { + r: 150, + g: 200, + b: 150, + a: 255, + }, + wall_normal: Color { + r: 200, + g: 120, + b: 120, + a: 255, + }, + wall_selected: Color { + r: 150, + g: 200, + b: 150, + a: 255, + }, + icon_normal: Color { + r: 255, + g: 255, + b: 255, + a: 255, + }, + icon_selected: Color { + r: 150, + g: 200, + b: 150, + a: 255, + }, + dimension_indicators: Color { + r: 200, + g: 200, + b: 200, + a: 255, + }, + dimension_text: Color { + r: 200, + g: 200, + b: 200, + a: 255, + }, + position_indicator: Color { + r: 200, + g: 200, + b: 200, + a: 255, + }, + position_text: Color { + r: 200, + g: 200, + b: 200, + a: 255, + }, + grid_lines: Color { + r: 255, + g: 255, + b: 255, + a: 75, + }, + cli_background: Color { + r: 100, + g: 100, + b: 100, + a: 150, + }, + cli_foreground: Color { + r: 255, + g: 255, + b: 255, + a: 200, + }, + } + } +} diff --git a/src/client/config.rs b/src/client/config.rs new file mode 100644 index 0000000..96ff3f5 --- /dev/null +++ b/src/client/config.rs @@ -0,0 +1,166 @@ +//! Home of the user configuratable content of graf karto, like keybindings and (TODO) colours etc. + +use crate::client::input::{Binding, Button, Input, MouseButton, Scancode}; +use ron::de::from_reader; +use ron::ser::{to_string_pretty, PrettyConfig}; +use serde::{Deserialize, Serialize}; +use std::fs::File; +use std::io::{self, Write}; +use std::path::Path; + +/// All configuration parameters the user can set are contained in this struct. +#[allow(missing_docs)] +#[derive(Deserialize, Serialize)] +pub struct Config { + 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 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 ToolGeneralBinds { + /// Keybinding to, where applicable, place a single node (usually a vertex) for the tool in + /// question. + pub place_single: Binding, + /// Finish up whatever one is doing with the current tool, without removing information. + 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: Binding, +} + +#[derive(Clone, Serialize, Deserialize)] +/// Key bindings that are individually interesting to the icon tool. +pub struct IconToolBinds { + /// Key to change to the next icon of the icon list. + pub next: Binding, + /// Key to change to the previous icon of the icon list. + pub previous: Binding, + /// Rotate the working icon clockwise by a certain amount (currently 45 degrees) + pub rotate_clockwise: Binding, + /// Rotate the working icon counterclockwise by a certain amount (currently 45 degrees) + pub rotate_counterclockwise: Binding, +} + +impl Config { + /// Try to parse a configuration from the file located at path. + /// + /// # Errors + /// If the file is not found or can not be read or parsed for a different reason, an IO-Error is + /// returned. + pub fn from_file>(path: P) -> io::Result { + let file = File::open(&path)?; + match from_reader(file) { + Ok(data) => Ok(data), + Err(err) => Err(io::Error::new(io::ErrorKind::InvalidData, err)), + } + } + + /// Try to write the configuration to the file at path. If the file exists, it will be overwritten. + /// + /// # Errors + /// If the file can not be written, for example for lack of permissions, an IO-Error is returned. + pub fn write_file>(&self, path: P) -> io::Result<()> { + let mut file = File::create(&path)?; + + let pretty_conf = PrettyConfig::new() + .with_depth_limit(4) + .with_decimal_floats(true) + .with_separate_tuple_members(true) + .with_indentor("\t".to_owned()); + let string = match to_string_pretty(&self, pretty_conf) { + Ok(string) => string, + Err(err) => { + return Err(io::Error::new(io::ErrorKind::InvalidInput, err)); + } + }; + + file.write_all(&string.as_bytes()) + } +} + +/// 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_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_binds: ToolGeneralBinds { + place_single: Button::Mouse(MouseButton::Left).into(), + finish: Button::Scancode(Scancode::Enter).into(), + abort: Button::Mouse(MouseButton::Right).into(), + }, + 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/client/editor.rs b/src/client/editor.rs new file mode 100644 index 0000000..0fb5794 --- /dev/null +++ b/src/client/editor.rs @@ -0,0 +1,189 @@ +//! Element creation base +//! +//! The actual editor environment sits here. This especially means all tools that require low-level +//! access to the data of items currently being created. While this may be subject to change, there is +//! currently a difference between things that are being created (inside the editor) and things that +//! are part of the environment (the map). + +use crate::client::config::Config; +use crate::client::input::{Binding, Input}; +use crate::client::map::Map; +use crate::client::snapping::Snapper; +use crate::client::tool::*; +use crate::client::transform::Transform; +use crate::net::{Cargo, Connection}; +use raylib::core::drawing::RaylibDrawHandle; +use raylib::{RaylibHandle, RaylibThread}; +use std::collections::HashMap; + +/// The editor containing all tools and currently the map of the stuff that has been created. +pub struct Editor { + map: Map, + /// HashMap that matches the ToolType with its proper activation key and of course the tool + /// itself. + tools: HashMap, Binding)>, + active: ToolType, + config: Config, + server: Connection, +} + +impl Editor { + /// Create a new editor with all tools necessary. There should be only one editor per program + /// instance. + pub fn new( + rl: &mut RaylibHandle, + rlt: &RaylibThread, + config: Config, + server: Connection, + ) -> Self { + let map = Map::new(rl, rlt); + + let mut tools: HashMap, Binding)> = + HashMap::with_capacity(ToolType::NumTools as usize); + + tools.insert( + ToolType::RectRoomTool, + ( + Box::new(RectRoomTool::new()), + config.tool_activation_binds.rect_room.clone(), + ), + ); + tools.insert( + ToolType::PolygonRoomTool, + ( + Box::new(PolygonRoomTool::new()), + config.tool_activation_binds.polygon_room.clone(), + ), + ); + tools.insert( + ToolType::WallTool, + ( + Box::new(WallTool::new()), + config.tool_activation_binds.wall.clone(), + ), + ); + tools.insert( + ToolType::IconTool, + ( + 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_binds.deletion.clone(), + ), + ); + tools.insert( + ToolType::SelectionTool, + ( + Box::new(SelectionTool::new()), + config.tool_activation_binds.selection.clone(), + ), + ); + + assert_eq!(ToolType::NumTools as usize, tools.len()); + + Self { + map, + tools, + active: ToolType::RectRoomTool, + config, + server, + } + } + + /// Get the currently active tool. Since the every tool exists only once, it is entirely indexable + /// by its type, which is what is actually returned. + pub fn active(&self) -> ToolType { + self.active + } + + /// Set the currently active tool. Any process currently going on in a different tool will be + /// aborted. + pub fn set_active(&mut self, tool: ToolType) { + if tool != self.active { + self.tools.get_mut(&self.active).unwrap().0.deactivate(); + self.active = tool; + self.tools + .get_mut(&self.active) + .expect("{:?} is not a Tool in the Editor. Maybe you forgot to register it?") + .0 + .activate(); + } + } + + /// Update the internal editor data where necessary and handle selecting different tools, aswell + /// as updating the currently active tool. Should be called once every frame. + pub fn update( + &mut self, + rl: &mut RaylibHandle, + transform: &Transform, + snapper: &Snapper, + input: &mut Input, + ) { + // Handle keybindings for tool change + 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; + } + } + + let mouse_pos_m = transform.point_px_to_m(&rl.get_mouse_position().into()); + let snapped_mouse_pos = snapper.snap(mouse_pos_m); + + // Update the currently active tool + let active_tool = &mut self.tools.get_mut(&self.active).unwrap().0; + active_tool.update(&self.map, &snapped_mouse_pos); + + // Handle common keybindings many of the tools have. + if input.poll_global(&self.config.tool_general_binds.place_single) { + active_tool.place_single(&mut self.map, &self.server, &snapped_mouse_pos); + } + if input.poll_global(&self.config.tool_general_binds.finish) { + active_tool.finish(&mut self.map, &self.server); + } + if input.poll_global(&self.config.tool_general_binds.abort) { + active_tool.abort(); + } + + // Handle custom keybindings in case the tool has any. + active_tool.handle_custom_bindings(&mut self.map, &self.server, input); + } + + /// Draw all tools and in case of the active tool also what is currently being edited by it, if + /// that exists. + pub fn draw_tools(&self, rld: &mut RaylibDrawHandle, transform: &Transform) { + for (tool, _) in self.tools.values() { + tool.draw(rld, transform); + } + } + + /// Get the world containing all finished elements. + pub fn map(&self) -> &Map { + &self.map + } + + /// Get the server this editor is connected to. Even if the program is executed locally, this will + /// return a server, since one must have been started locally. + pub fn server(&self) -> &Connection { + &self.server + } + + /// Get the server this editor is connected to mutably. + pub fn server_mut(&mut self) -> &mut Connection { + &mut self.server + } +} diff --git a/src/client/grid.rs b/src/client/grid.rs new file mode 100644 index 0000000..17d537d --- /dev/null +++ b/src/client/grid.rs @@ -0,0 +1,56 @@ +//! The grid used to divide the map into evenly sized chunks. + +use crate::client::colours::DEFAULT_COLOURS; +use crate::client::transform::Transform; +use crate::math; +use raylib::drawing::RaylibDraw; + +/// Draw an infinite grid that can be moved around on the screen and zoomed in and out of. +pub fn draw_grid(rld: &mut D, screen_width: i32, screen_height: i32, transform: &Transform) +where + D: RaylibDraw, +{ + /* Calculate the first whole meter that can be seen on the grid. This is the first meter that + * will be seen on screen. + */ + let mut first_cell = *transform.translation_px() / -transform.pixels_per_m(); + first_cell.x = first_cell.x.floor(); + first_cell.y = first_cell.y.floor(); + + let mut cell = first_cell; + let mut draw_y = transform.point_m_to_px(&cell).y; + loop { + draw_y = math::round(draw_y, 1.); + rld.draw_line( + 0, + draw_y as i32, + screen_width, + draw_y as i32, + DEFAULT_COLOURS.grid_lines, + ); + cell.y += 1.; + draw_y = transform.point_m_to_px(&cell).y; + + if draw_y as i32 > screen_height { + break; + } + } + + let mut draw_x = transform.point_m_to_px(&cell).x; + loop { + draw_x = math::round(draw_x, 1.); + rld.draw_line( + draw_x as i32, + 0, + draw_x as i32, + screen_height, + DEFAULT_COLOURS.grid_lines, + ); + cell.x += 1.; + draw_x = transform.point_m_to_px(&cell).x; + + if draw_x as i32 > screen_width { + break; + } + } +} diff --git a/src/client/gui/decimal_num_box.rs b/src/client/gui/decimal_num_box.rs new file mode 100644 index 0000000..e9395f7 --- /dev/null +++ b/src/client/gui/decimal_num_box.rs @@ -0,0 +1,173 @@ +//! Functions similarly to a text-bux, but only accepts floating point (decimal) numbers +//! +//! Since a lot of functions require the user to input measurements in meters, it is useful to have a +//! singular entity that reads these in an intuitive way. Inputting of such numbers is handled in +//! this module. + +use crate::math::{self, Vec2}; +use nalgebra::RealField; +use num_traits::Pow; +use raylib::drawing::RaylibDraw; +use raylib::ffi::{Color, KeyboardKey}; +use raylib::text; +use raylib::RaylibHandle; +use std::str::FromStr; + +/// The number of decimal places that can be edited and will be shown by a decimal text field. +pub const DECIMAL_PLACES: u16 = 4; + +/// The decimal num box can handle any decimal number, like f32 or f64. Currently has a hard limit +/// of four decimal places, but that may change in the future. +pub struct DecimalNumBox + FromStr> { + input: String, + last_value: F, + active: bool, +} + +impl + FromStr> DecimalNumBox { + /// Create a new Number box showing the value specified. Should the value have more then the + /// maximum number of decimal places, it will be rounded. + pub fn new(value: F) -> Self { + let value = math::round_nth_decimal(value, DECIMAL_PLACES); + let input = format!("{:.4}", value); + + Self { + input, + last_value: value, + active: false, + } + } + + /// Get the value entered by the user. If the user has something that cannot be parsed into a + /// decimal value, this differs from the string that is shown and is instead the last value + /// entered by the user that is still a valid decimal number. + pub fn value(&self) -> F { + self.last_value + } + + /// Set the value directly. This may only be done, if the box is currently not active, to protect + /// user input. Returns true if the value could be set, otherwise false. + pub fn set_value(&mut self, value: F) -> bool { + if !self.active { + self.last_value = math::round_nth_decimal(value, DECIMAL_PLACES); + // XXX: Don't use the magical 4 + self.input = format!("{:.4}", self.last_value); + true + } else { + false + } + } + + /// Check if this number box is currently active. Active means, it's capturing keyboard input. + /// If it's not active, it does not attempt to capture any keystrokes. + pub fn active(&self) -> bool { + self.active + } + + /// Set if the box is active (capturing keyboard input and adjusting it's value accordingly) or + /// not. + pub fn set_active(&mut self, active: bool) { + self.active = active + } + + /// Update this decimal box. If it is inactive, this doesn't do anything, but if it is active, it + /// captures the keyboard input, if available. Returns `true`, if the value changed, otherwise + /// `false`. Note that the string that is displayed may change, but the value does not have to. + /// This happens, if the user types something invalid. In this case, `false` is returned as well. + pub fn update(&mut self, rl: &mut RaylibHandle) -> bool { + /* If the box is currently inactive, nothing must be changed, and this function will do + * nothing. + */ + if !self.active { + return false; + } + + // TODO: Check for movement keys. + + // Delete the last character when pressing backspace. + let string_changed = if rl.is_key_pressed(KeyboardKey::KEY_BACKSPACE) { + self.input.pop().is_some() + } + // Check the entered numbers or decimal point. + else if let Some(key) = rl.get_key_pressed() { + match key { + // Add (at most one) decimal point to the input when entering a dot. + KeyboardKey::KEY_PERIOD => { + if !self.input.contains('.') { + self.input.push('.'); + true + } else { + false + } + } + _ => { + if key as u16 >= KeyboardKey::KEY_ZERO as u16 + && key as u16 <= KeyboardKey::KEY_NINE as u16 + { + self.input.push(key as u8 as char); + true + } else { + false + } + } + } + } else { + false + }; + + if string_changed { + // Try to parse the new string. If it doesn't work, keep the old one. + match self.input.parse::() { + Ok(value) => { + let value = math::round_nth_decimal(value, DECIMAL_PLACES); + if value != self.last_value { + self.last_value = value; + true + } else { + false + } + } + Err(_) => false, + } + } else { + false + } + } + + /// Draw the number box at the given position. the `unit` parameter is used to append this text, + /// let's say for instance 'm' for meters to the text drawn to screen. Most of the time, a unit + /// makes sense to show on this number box, otherwise it can be left as an empty string. The unit + /// has no relevance to internal processes and cannot be edited by the user. + pub fn draw(&self, rld: &mut impl RaylibDraw, unit: &str, pos: &Vec2) { + let text = format!("{}{}", self.input, unit); + let width = text::measure_text(&text, 20); + + // Draw background to highlight this box if it's active. + if self.active { + rld.draw_rectangle_v( + *pos - Vec2::new(5., 5.), + Vec2::new(width as f32 + 10., 20. + 10.), + Color { + r: 120, + g: 120, + b: 120, + a: 180, + }, + ); + } + + // Draw the text of the box. + rld.draw_text( + &text, + pos.x as i32, + pos.y as i32, + 20, + Color { + r: 255, + g: 255, + b: 255, + a: 255, + }, + ) + } +} diff --git a/src/client/gui/dimension_indicator.rs b/src/client/gui/dimension_indicator.rs new file mode 100644 index 0000000..fb6394a --- /dev/null +++ b/src/client/gui/dimension_indicator.rs @@ -0,0 +1,308 @@ +//! An interface element that shows the size of the selected map items and provides a means to +//! manually change the size of them in a precise manner should need be. + +use crate::client::colours::DEFAULT_COLOURS; +use crate::client::map::Map; +use crate::client::transform::Transform; +use crate::client::Editor; +use crate::math::{self, Rect, Vec2}; +use crate::net::Cargo; +use nalgebra::{Matrix3, Vector2}; +use raylib::drawing::RaylibDraw; +use raylib::ffi::KeyboardKey; +use raylib::RaylibHandle; + +/// A state the [DimensionIndicator] is currently in. This determines the behaviour of it and what +/// inputs it might be waiting for. +enum State { + /// In this state, the indicator is not trying to read any keyboard input, but will instead watch + /// for any changes to the dimensions from a different source, updating its display. + Watching, + /// In this state, the indicator will capture keyboard input and attempt to set the dimensions + /// according to whatever was entered. If the dimensions cannot be set, the indicator will use + /// the last valid dimensions. + Ruling { + dim_x: String, + dim_y: String, + editing_x: bool, + }, +} + +/// Used to render the horizontal and vertical dimensions of whatever is selected on the map and, if +/// the user so desires edit them directly by entering values into it. +pub struct DimensionIndicator { + /// The [State] the dimension indicator is currently in. + state: State, + /// The last dimensions that were valid. + bounds: Rect, +} + +impl Default for State { + fn default() -> Self { + Self::Watching + } +} + +impl Default for DimensionIndicator { + fn default() -> Self { + Self { + state: State::default(), + bounds: Rect::new(0., 0., 0., 0.), + } + } +} + +impl DimensionIndicator { + /// Create a new dimension indicator. While it is possible to have multiple instances, this is + /// not generally recommended, since they will need to be managed carefully or otherwise steal + /// keystrokes from each other. + pub fn new() -> Self { + Self::default() + } + + /// Update whatever is selected on the map according to the dimension indicator rules and rulers. + pub fn update(&mut self, editor: &Editor, rl: &mut RaylibHandle) { + match self.state { + State::Watching => self.update_watching(editor.map(), rl), + State::Ruling { .. } => self.update_ruling(editor, rl), + }; + } + + fn update_watching(&mut self, map: &Map, rl: &RaylibHandle) { + let mut min: Vec2 = Vec2::default(); + let mut max: Vec2 = Vec2::default(); + + /* Try to find selected items. If no items exist, the dimension indicator is set to its + * default, otherwise it is adjusted to the size of the combined selection. + */ + let mut selection_exists = false; + for (_id, e) in map.elements() { + if e.selected() { + let element_bounds = e.as_component().bounding_rect(); + if selection_exists { + // Adjust the currently detected selection size. + min.x = math::partial_min(min.x, element_bounds.x); + min.y = math::partial_min(min.y, element_bounds.y); + max.x = math::partial_max(max.x, element_bounds.x + element_bounds.w); + max.y = math::partial_max(max.y, element_bounds.y + element_bounds.h); + } else { + // No selection size detected yet. Set now. + min.x = element_bounds.x; + min.y = element_bounds.y; + max.x = element_bounds.x + element_bounds.w; + max.y = element_bounds.y + element_bounds.h; + } + selection_exists = true; + } + } + + // Set the current selection limits, if any. + self.bounds = if selection_exists { + Rect::bounding_rect(min, max) + } else { + Rect::new(0., 0., 0., 0.) + }; + + // Check if the user wants to change into editing mode, which the user can only do if there + // is a selection to begin with. + if selection_exists && rl.is_key_pressed(KeyboardKey::KEY_TAB) { + self.state = State::Ruling { + dim_x: self.bounds.w.to_string(), + dim_y: self.bounds.h.to_string(), + editing_x: true, + }; + } + } + + fn update_ruling(&mut self, editor: &Editor, rl: &mut RaylibHandle) { + // Get the currently edited dimension for processing. + let (edited_dim, editing_x) = match &mut self.state { + State::Watching => panic!("Called ruler update when in watching state"), + State::Ruling { + dim_x, + dim_y, + editing_x, + } => { + if *editing_x { + (dim_x, editing_x) + } else { + (dim_y, editing_x) + } + } + }; + + // Switch the currently edited dimension when pressing tab. + if rl.is_key_pressed(KeyboardKey::KEY_TAB) { + *editing_x = !*editing_x; + return; + } + // Finish editing mode on enter. + if rl.is_key_pressed(KeyboardKey::KEY_ENTER) { + self.state = State::Watching; + return; + } + + // Marker to see if the dimensions will have to be checked for an update. + let mut dimension_changed = false; + // Delete the last character of the dimension on backspace. + if rl.is_key_pressed(KeyboardKey::KEY_BACKSPACE) { + edited_dim.pop(); + dimension_changed = true; + } + /* Capture the current key and try to add it to the string of the current dimension, + * if possible. + */ + else if let Some(key) = rl.get_key_pressed() { + match key { + // Add a decimal point to the dimension if possible. + KeyboardKey::KEY_PERIOD => { + if !edited_dim.contains('.') { + edited_dim.push('.'); + } + // Nothing changed here, since there is an implicit .0 at the end. + } + // Handle the entered key if it is a number to append it to the currently edited dimension. + _ => { + if key as u16 >= KeyboardKey::KEY_ZERO as u16 + && key as u16 <= KeyboardKey::KEY_NINE as u16 + { + edited_dim.push(key as u8 as char); + dimension_changed = true; + } + } + }; + } + + if dimension_changed { + /* Try to parse the dimension from the currently edited string. If it + * is valid, change the dimensions of the currently selected items. If + * not, ignore the change and wait for a valid dimension. + */ + if let Ok(dim) = edited_dim.parse::() { + let new_bounds = if *editing_x { + Rect::new(self.bounds.x, self.bounds.y, dim, self.bounds.h) + } else { + Rect::new(self.bounds.x, self.bounds.y, self.bounds.h, dim) + }; + + self.set_bounds(editor, new_bounds); + } + } + } + + /// Set the selection boundaries to the given bounds. Tries to transform the + /// currently selected items in the map so they fit inside of the new bounding box. + /// + /// # Panics + /// If the `bounds` have a negative value for width or height, the dimensions + /// cannot be set and the function will panic. + pub fn set_bounds(&mut self, editor: &Editor, bounds: Rect) { + if bounds.w <= 0. || bounds.h <= 0. { + panic!("Cannot set dimensions of elements to zero."); + } + + // If the bounds are the same as before, there is nothing to do. + if self.bounds == bounds { + return; + } + + /* Create a matrix to transform from the current rectangle bounds into the + * new bounds. Internally, this is a three-step process. First, we + * translate the points currently in the bounding box to the origin + * (0, 0) origin vector of the map, then scale and finally move it to the + * origin of the new rectangle. This needs to be applied to all vertices + * of all elements that can be scaled. + */ + let scale = Vector2::new(bounds.w / self.bounds.w, bounds.h / self.bounds.h); + let transform = Matrix3::new_translation(&Vector2::new(-self.bounds.x, -self.bounds.y)) + .append_nonuniform_scaling(&scale) + .append_translation(&Vector2::new(bounds.x, bounds.y)); + + for (id, element) in editor.map().elements() { + if element.selected() { + if element.as_component().as_non_rigid().is_some() { + editor + .server() + .send(Cargo::ApplyMatrix((id, transform.clone()))); + } + } + } + + self.bounds = bounds; + } + + /// Draw the dimensions detected on the current selection. + pub fn draw(&self, rld: &mut impl RaylibDraw, transform: &Transform) { + /* Ignore a selection that has no non-null dimensions, since this usually + * indicates that there is nothing to be scaled. + */ + if self.bounds.w == 0. && self.bounds.h == 0. { + return; + } + + let (dim_str_width, dim_str_height) = match &self.state { + State::Watching => (self.bounds.w.to_string(), self.bounds.h.to_string()), + State::Ruling { dim_x, dim_y, .. } => (dim_x.clone(), dim_y.clone()), + }; + + // Draw the horizontal dimension at the bottom and the vertical dimension to the right. + // Use the valid dimensions, but show the edited dimensions in the String (should they differ) + let top_right = Vec2::new(self.bounds.x + self.bounds.w, self.bounds.y); + let bot_left = Vec2::new(self.bounds.x, self.bounds.y + self.bounds.h); + let bot_right = Vec2::new(self.bounds.x + self.bounds.w, self.bounds.y + self.bounds.h); + let dimensions = [ + (bot_left, bot_right, dim_str_width), + (bot_right, top_right, dim_str_height), + ]; + for (start, end, dim_str) in &dimensions { + // Don't draw anything if the length is zero. + if start == end { + continue; + } + + /* Get the vector that is perpendicular and points right/down from the line, assuming + * the lines prefer left as start over right and bottom over top. + */ + let line_normal = { + // Start with the direction of the line vector. + let dir = *start - *end; + // Calculate perpendicular vec and normalise. + dir.rotated_90_clockwise() / dir.length() + }; + + // To not have the line directly in the rect, move start and end outside a bit. + let start_px = transform.point_m_to_px(start) + line_normal * 10.; + let end_px = transform.point_m_to_px(end) + line_normal * 10.; + + /* Draw the indicator line, with stubs at both ends. */ + // First the two stubs. + rld.draw_line_ex( + start_px - line_normal * 5., + start_px + line_normal * 5., + 2., + DEFAULT_COLOURS.dimension_indicators, + ); + rld.draw_line_ex( + end_px - line_normal * 5., + end_px + line_normal * 5., + 2., + DEFAULT_COLOURS.dimension_indicators, + ); + // Then the actual indicator line. + rld.draw_line_ex(start_px, end_px, 2., DEFAULT_COLOURS.dimension_indicators); + + /* Draw the indicator text showing how long this line is in meters. + * It should be placed in the middle of the line, but not into the line directly, so it + * will be moved out by the normal. + */ + let text_pos = transform.point_m_to_px(&((*end + *start) / 2.)) + line_normal * 20.; + rld.draw_text( + &format!("{}m", dim_str), + text_pos.x as i32, + text_pos.y as i32, + 20, + DEFAULT_COLOURS.dimension_text, + ); + } + } +} diff --git a/src/client/gui/mod.rs b/src/client/gui/mod.rs new file mode 100644 index 0000000..62173ec --- /dev/null +++ b/src/client/gui/mod.rs @@ -0,0 +1,18 @@ +//! General graphical user interfaces +//! +//! This mod does not contain all graphical content on screen, but all user interfaces that is drawn +//! that is not contained in a different category. This means all interface elements where it does not +//! make sense to bind it to any other part of the program, for instance a tool or type of element. +//! It also does *not* contain anything that does anything that is not triggered by the user, which +//! means everything is called top-down from this module. A function in this module should not be +//! called from any point in the program except the main loop, where the user input is polled. + +pub mod decimal_num_box; +pub mod dimension_indicator; +pub mod position_indicator; +pub mod tool_sidebar; + +pub use self::decimal_num_box::*; +pub use self::dimension_indicator::*; +pub use self::position_indicator::*; +pub use self::tool_sidebar::*; diff --git a/src/client/gui/position_indicator.rs b/src/client/gui/position_indicator.rs new file mode 100644 index 0000000..4d68b86 --- /dev/null +++ b/src/client/gui/position_indicator.rs @@ -0,0 +1,37 @@ +//! The position indicator shows the mouse position on the map +//! +//! The exact position the mouse is currently on is shown unless hidden by the user (TODO). This +//! helps to place things exactly where they should be on the map and let the user know where they +//! are looking and where relative to them other things should be easily at all times. Currently, this +//! is a simple HUD so it doesn't interact with anything in the world, but that may change in the +//! future. + +use crate::client::colours::DEFAULT_COLOURS; +use crate::client::snapping::Snapper; +use crate::client::transform::Transform; +use crate::math::Vec2; +use raylib::drawing::{RaylibDraw, RaylibDrawHandle}; + +/// Function to draw a dot at the mouse position and the coordinates associated with it. +// TODO: Snap this, when the user wants to snap, don't if they don't want to. +pub fn position_indicator_draw( + rld: &mut RaylibDrawHandle, + mouse_pos_px: Vec2, + transform: &Transform, + snapper: &Snapper, +) { + let mouse_pos_snapped_m = snapper.snap(transform.point_px_to_m(&mouse_pos_px)); + let mouse_pos_snapped_px = transform.point_m_to_px(&mouse_pos_snapped_m); + + rld.draw_circle_v(mouse_pos_snapped_px, 2., DEFAULT_COLOURS.position_indicator); + rld.draw_text( + &format!( + "({:.3}m, {:.3}m)", + mouse_pos_snapped_m.x, mouse_pos_snapped_m.y + ), + mouse_pos_snapped_px.x as i32 - 30, + mouse_pos_snapped_px.y as i32 - 30, + 20, + DEFAULT_COLOURS.position_text, + ); +} diff --git a/src/client/gui/tool_sidebar.rs b/src/client/gui/tool_sidebar.rs new file mode 100644 index 0000000..3147bf8 --- /dev/null +++ b/src/client/gui/tool_sidebar.rs @@ -0,0 +1,91 @@ +//! The sidebar showing all tools available to the user. This toolbar handles changing the active tool +//! based on the mouse input and (TODO!) keyboard inputs. +// 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::client::input::Input; +use crate::client::tool::ToolType; +use crate::client::Editor; +use crate::math::Rect; +use raylib::core::texture::Texture2D; +use raylib::rgui::RaylibDrawGui; +use raylib::{RaylibHandle, RaylibThread}; +use std::mem; + +/// The file containing textures for all buttons describing the tools. +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, +} + +impl ToolSidebar { + /// Create a new tool sidebar. There should be only one sidebar per program instance. + 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."); + + 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 { + /* 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) + } + + /// 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, 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; + if rld.gui_image_button_ex( + Rect::new(20., i as f32 * 100. + 20., 64., 64.), + None, + &self.button_texture, + Rect::new( + is_current_active as u8 as f32 * 64., + i as f32 * 64., + 64., + 64., + ), + ) { + active = unsafe { mem::transmute(i as u8) }; + } + } + + editor.set_active(active); + } +} diff --git a/src/client/input/binding.rs b/src/client/input/binding.rs new file mode 100644 index 0000000..386fb66 --- /dev/null +++ b/src/client/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