aboutsummaryrefslogtreecommitdiff
path: root/src/client/gui/dimension_indicator.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/gui/dimension_indicator.rs')
-rw-r--r--src/client/gui/dimension_indicator.rs311
1 files changed, 311 insertions, 0 deletions
diff --git a/src/client/gui/dimension_indicator.rs b/src/client/gui/dimension_indicator.rs
new file mode 100644
index 0000000..ebad78b
--- /dev/null
+++ b/src/client/gui/dimension_indicator.rs
@@ -0,0 +1,311 @@
+//! 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::input::{Button, Input, Scancode};
+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;
+use std::sync::mpsc::Receiver;
+
+/// 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,
+ text_pipe: Receiver<char>,
+ },
+}
+
+/// 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<f64>,
+}
+
+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(input: &mut Input) -> Self {
+ input.add_global(Button::Scancode(Scancode::Tab).into());
+
+ Self {
+ state: State::default(),
+ bounds: Rect::new(0., 0., 0., 0.),
+ }
+ }
+
+ /// Update whatever is selected on the map according to the dimension indicator rules and rulers.
+ pub fn update(&mut self, editor: &Editor, input: &mut Input) {
+ match self.state {
+ State::Watching => self.update_watching(editor.map(), input),
+ State::Ruling { .. } => self.update_ruling(editor, input),
+ };
+ }
+
+ fn update_watching(&mut self, map: &Map, input: &mut Input) {
+ let mut min: Vec2<f64> = Vec2::default();
+ let mut max: Vec2<f64> = 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 && input.poll_global(&Button::Scancode(Scancode::Tab).into()) {
+ // Try to capture the keyboard and go into the ruling state.
+ if let Some(text_pipe) = input.try_capture_keyboard() {
+ self.state = State::Ruling {
+ dim_x: self.bounds.w.to_string(),
+ dim_y: self.bounds.h.to_string(),
+ editing_x: true,
+ text_pipe,
+ };
+ }
+ }
+ }
+
+ fn update_ruling(&mut self, editor: &Editor, input: &mut Input) {
+ // Get the currently edited dimension for processing.
+ let (edited_dim, editing_x, text_pipe) = match &mut self.state {
+ State::Watching => panic!("Called ruler update when in watching state"),
+ State::Ruling {
+ dim_x,
+ dim_y,
+ editing_x,
+ text_pipe,
+ } => {
+ if *editing_x {
+ (dim_x, editing_x, text_pipe)
+ } else {
+ (dim_y, editing_x, text_pipe)
+ }
+ }
+ };
+
+ let next_char = match text_pipe.try_recv() {
+ Ok(c) => c,
+ Err(_) => return,
+ };
+
+ let update_bounds = match next_char {
+ '\t' => {
+ // Switch the currently edited dimension when pressing tab.
+ *editing_x = !*editing_x;
+ return;
+ }
+ '\n' => {
+ // Finish editing mode on enter.
+ self.state = State::Watching;
+ return;
+ }
+ '\x7f' => {
+ // Delete last char on backspace
+ edited_dim.pop();
+ true
+ }
+ key => {
+ match key {
+ // Add a decimal point to the dimension if possible.
+ '.' => {
+ if !edited_dim.contains('.') {
+ edited_dim.push('.');
+ }
+ // Nothing changed here, since there is an implicit .0 at the end.
+ false
+ }
+ // Handle the entered key if it is a number to append it to the currently edited dimension.
+ '0'..='9' => {
+ edited_dim.push(key);
+ true
+ }
+ _ => false,
+ }
+ }
+ };
+
+ if update_bounds {
+ /* 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::<f64>() {
+ 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, dbg!(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<f64>) {
+ 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,
+ );
+ }
+ }
+}
+
+impl Default for State {
+ fn default() -> Self {
+ Self::Watching
+ }
+}