From 4cb15af8a32348c5b7627c78f0152eea50b816c9 Mon Sep 17 00:00:00 2001
From: Simon Gellis <simongellis@gmail.com>
Date: Thu, 13 Feb 2025 23:37:40 -0500
Subject: [PATCH] Support signed numbers in number picker

---
 src/window/utils.rs       | 55 ++++++++++++++++++++++++++-------------
 src/window/vram/object.rs | 30 +++++++++++++++++++++
 2 files changed, 67 insertions(+), 18 deletions(-)

diff --git a/src/window/utils.rs b/src/window/utils.rs
index 7465794..ec3fe98 100644
--- a/src/window/utils.rs
+++ b/src/window/utils.rs
@@ -1,10 +1,15 @@
-use std::ops::{Bound, RangeBounds};
+use std::{
+    fmt::Display,
+    ops::{Bound, RangeBounds},
+    str::FromStr,
+};
 
 use egui::{
     ecolor::HexColor, Align, Color32, CursorIcon, Event, Frame, Key, Layout, Margin, Rect,
     Response, RichText, Rounding, Sense, Shape, Stroke, TextEdit, Ui, UiBuilder, Vec2, Widget,
     WidgetText,
 };
+use num_traits::PrimInt;
 
 pub trait UiExt {
     fn section(&mut self, title: impl Into<String>, add_contents: impl FnOnce(&mut Ui));
@@ -86,37 +91,47 @@ impl UiExt for Ui {
     }
 }
 
-pub struct NumberEdit<'a> {
-    value: &'a mut usize,
-    min: Option<usize>,
-    max: Option<usize>,
+enum Direction {
+    Up,
+    Down,
 }
 
-impl<'a> NumberEdit<'a> {
-    pub fn new(value: &'a mut usize) -> Self {
+pub trait Number: PrimInt + Display + FromStr + Send + Sync + 'static {}
+impl<T: PrimInt + Display + FromStr + Send + Sync + 'static> Number for T {}
+
+pub struct NumberEdit<'a, T: Number> {
+    value: &'a mut T,
+    increment: T,
+    min: Option<T>,
+    max: Option<T>,
+}
+
+impl<'a, T: Number> NumberEdit<'a, T> {
+    pub fn new(value: &'a mut T) -> Self {
         Self {
             value,
+            increment: T::one(),
             min: None,
             max: None,
         }
     }
 
-    pub fn range(self, range: impl RangeBounds<usize>) -> Self {
+    pub fn range(self, range: impl RangeBounds<T>) -> Self {
         let min = match range.start_bound() {
             Bound::Unbounded => None,
             Bound::Included(t) => Some(*t),
-            Bound::Excluded(t) => t.checked_add(1),
+            Bound::Excluded(t) => t.checked_add(&self.increment),
         };
         let max = match range.end_bound() {
             Bound::Unbounded => None,
             Bound::Included(t) => Some(*t),
-            Bound::Excluded(t) => t.checked_sub(1),
+            Bound::Excluded(t) => t.checked_sub(&self.increment),
         };
         Self { min, max, ..self }
     }
 }
 
-impl Widget for NumberEdit<'_> {
+impl<T: Number> Widget for NumberEdit<'_, T> {
     fn ui(self, ui: &mut Ui) -> Response {
         let (last_value, mut str, focus) = ui.memory(|m| {
             let (lv, s) = m
@@ -131,7 +146,7 @@ impl Widget for NumberEdit<'_> {
             str = self.value.to_string();
             stale = true;
         }
-        let valid = str.parse().is_ok_and(|v: usize| v == *self.value);
+        let valid = str.parse().is_ok_and(|v: T| v == *self.value);
         let mut up_pressed = false;
         let mut down_pressed = false;
         if focus {
@@ -192,26 +207,30 @@ impl Widget for NumberEdit<'_> {
         let arrow_middle = (res.rect.min.y + res.rect.max.y) / 2.0;
         let arrow_bottom = res.rect.max.y + 2.0;
 
-        let mut delta = 0;
+        let mut delta = None;
         let top_arrow_rect = Rect {
             min: (arrow_left, arrow_top).into(),
             max: (arrow_right, arrow_middle).into(),
         };
         if draw_arrow(ui, top_arrow_rect, true).clicked_or_dragged() || up_pressed {
-            delta = 1;
+            delta = Some(Direction::Up);
         }
         let bottom_arrow_rect = Rect {
             min: (arrow_left, arrow_middle).into(),
             max: (arrow_right, arrow_bottom).into(),
         };
         if draw_arrow(ui, bottom_arrow_rect, false).clicked_or_dragged() || down_pressed {
-            delta = -1;
+            delta = Some(Direction::Down);
         }
 
         let in_range =
-            |&val: &usize| self.min.is_none_or(|m| m <= val) && self.max.is_none_or(|m| m >= val);
-        if delta != 0 {
-            if let Some(new_value) = self.value.checked_add_signed(delta).filter(in_range) {
+            |val: &T| self.min.is_none_or(|m| &m <= val) && self.max.is_none_or(|m| &m >= val);
+        if let Some(dir) = delta {
+            let value = match dir {
+                Direction::Up => self.value.checked_add(&self.increment),
+                Direction::Down => self.value.checked_sub(&self.increment),
+            };
+            if let Some(new_value) = value.filter(in_range) {
                 *self.value = new_value;
             }
             str = self.value.to_string();
diff --git a/src/window/vram/object.rs b/src/window/vram/object.rs
index 23dbb60..756aff0 100644
--- a/src/window/vram/object.rs
+++ b/src/window/vram/object.rs
@@ -62,6 +62,9 @@ impl ObjectWindow {
                 });
             ui.section("Properties", |ui| {
                 let object = self.objects.borrow().read::<[u16; 4]>(self.index);
+                let mut x = ((object[0] & 0x3ff) << 6 >> 6) as i16;
+                let mut parallax = ((object[1] & 0x3ff) << 6 >> 6) as i16;
+                let mut y = ((object[2] & 0x0ff) << 8 >> 8) as i16;
                 let (mut char_index, mut vflip, mut hflip, palette_index) =
                     utils::parse_cell(object[3]);
                 TableBuilder::new(ui)
@@ -91,6 +94,33 @@ impl ObjectWindow {
                                 );
                             });
                         });
+                        body.row(row_height, |mut row| {
+                            row.col(|ui| {
+                                ui.label("X");
+                            });
+                            row.col(|ui| {
+                                ui.add_enabled(false, NumberEdit::new(&mut x).range(-512..512));
+                            });
+                        });
+                        body.row(row_height, |mut row| {
+                            row.col(|ui| {
+                                ui.label("Y");
+                            });
+                            row.col(|ui| {
+                                ui.add_enabled(false, NumberEdit::new(&mut y).range(-8..=224));
+                            });
+                        });
+                        body.row(row_height, |mut row| {
+                            row.col(|ui| {
+                                ui.label("Parallax");
+                            });
+                            row.col(|ui| {
+                                ui.add_enabled(
+                                    false,
+                                    NumberEdit::new(&mut parallax).range(-512..512),
+                                );
+                            });
+                        });
                         body.row(row_height, |mut row| {
                             row.col(|ui| {
                                 let checkbox = Checkbox::new(&mut hflip, "H-flip");