diff --git a/src/window/utils.rs b/src/window/utils.rs index 1887671..df2f748 100644 --- a/src/window/utils.rs +++ b/src/window/utils.rs @@ -1,8 +1,8 @@ -use std::{ops::Range, str::FromStr}; +use std::ops::{Bound, RangeBounds}; use egui::{ - ecolor::HexColor, Align, Color32, Frame, Layout, Response, RichText, Sense, TextEdit, Ui, - UiBuilder, Vec2, WidgetText, + ecolor::HexColor, Align, Color32, CursorIcon, Frame, Layout, Margin, Rect, Response, RichText, + Rounding, Sense, Shape, Stroke, TextEdit, Ui, UiBuilder, Vec2, Widget, WidgetText, }; pub trait UiExt { @@ -24,13 +24,6 @@ pub trait UiExt { fn color_pair_button(&mut self, left: Color32, right: Color32) -> Response; fn color_picker(&mut self, color: &mut Color32, hex: &mut String) -> Response; - - fn number_picker( - &mut self, - text: &mut String, - value: &mut T, - range: Range, - ); } impl UiExt for Ui { @@ -90,18 +83,174 @@ impl UiExt for Ui { ) .inner } +} - fn number_picker( - &mut self, - text: &mut String, - value: &mut T, - range: Range, - ) { - let res = self.add(TextEdit::singleline(text).horizontal_align(Align::Max)); - if res.changed() { - if let Some(new_value) = text.parse().ok().filter(|v| range.contains(v)) { - *value = new_value; - } +pub struct NumberEdit<'a> { + value: &'a mut usize, + min: Option, + max: Option, +} + +impl<'a> NumberEdit<'a> { + pub fn new(value: &'a mut usize) -> Self { + Self { + value, + min: None, + max: None, } } + + pub fn range(self, range: impl RangeBounds) -> Self { + let min = match range.start_bound() { + Bound::Unbounded => None, + Bound::Included(t) => Some(*t), + Bound::Excluded(t) => t.checked_add(1), + }; + let max = match range.end_bound() { + Bound::Unbounded => None, + Bound::Included(t) => Some(*t), + Bound::Excluded(t) => t.checked_sub(1), + }; + Self { min, max, ..self } + } +} + +impl Widget for NumberEdit<'_> { + fn ui(self, ui: &mut Ui) -> Response { + let (last_value, mut str) = ui.memory(|m| { + m.data + .get_temp(ui.id()) + .unwrap_or((*self.value, self.value.to_string())) + }); + let mut stale = false; + if *self.value != last_value { + str = self.value.to_string(); + stale = true; + } + let valid = str.parse().is_ok_and(|v: usize| v == *self.value); + let text = TextEdit::singleline(&mut str) + .horizontal_align(Align::Max) + .margin(Margin { + left: 4.0, + right: 20.0, + top: 2.0, + bottom: 2.0, + }); + let res = if valid { + ui.add(text) + } else { + let message = match (self.min, self.max) { + (Some(min), Some(max)) => format!("Please enter a number between {min} and {max}."), + (Some(min), None) => format!("Please enter a number greater than {min}."), + (None, Some(max)) => format!("Please enter a number less than {max}."), + (None, None) => "Please enter a number.".to_string(), + }; + ui.scope(|ui| { + let style = ui.style_mut(); + style.visuals.selection.stroke.color = style.visuals.error_fg_color; + style.visuals.widgets.hovered.bg_stroke.color = + style.visuals.error_fg_color.gamma_multiply(0.60); + ui.add(text) + }) + .inner + .on_hover_text(message) + }; + + let arrow_left = res.rect.max.x + 4.0; + let arrow_right = res.rect.max.x + 20.0; + let arrow_top = res.rect.min.y - 2.0; + 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 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() { + delta = 1; + } + 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() { + delta = -1; + } + + 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) { + *self.value = new_value; + } + str = self.value.to_string(); + stale = true; + } else if res.changed { + if let Some(new_value) = str.parse().ok().filter(in_range) { + *self.value = new_value; + } + stale = true; + } + if stale { + ui.memory_mut(|m| m.data.insert_temp(ui.id(), (*self.value, str))); + } + res + } +} + +fn draw_arrow(ui: &mut Ui, rect: Rect, up: bool) -> Response { + let arrow_res = ui + .allocate_rect(rect, Sense::click_and_drag()) + .on_hover_cursor(CursorIcon::Default); + let visuals = ui.style().visuals.widgets.style(&arrow_res); + let painter = ui.painter_at(arrow_res.rect); + + let rounding = if up { + Rounding { + ne: 2.0, + ..Rounding::ZERO + } + } else { + Rounding { + se: 2.0, + ..Rounding::ZERO + } + }; + painter.rect_filled(arrow_res.rect, rounding, visuals.bg_fill); + + let left = rect.left() + 4.0; + let center = (rect.left() + rect.right()) / 2.0; + let right = rect.right() - 4.0; + let top = rect.top() + 3.0; + let bottom = rect.bottom() - 3.0; + let points = if up { + vec![ + (left, bottom).into(), + (center, top).into(), + (right, bottom).into(), + ] + } else { + vec![ + (right, top).into(), + (center, bottom).into(), + (left, top).into(), + ] + }; + painter.add(Shape::convex_polygon( + points, + visuals.fg_stroke.color, + Stroke::NONE, + )); + arrow_res +} + +trait ResponseExt { + fn clicked_or_dragged(&self) -> bool; +} + +impl ResponseExt for Response { + fn clicked_or_dragged(&self) -> bool { + self.clicked() || self.dragged() + } } diff --git a/src/window/vram/bgmap.rs b/src/window/vram/bgmap.rs index a255914..2119733 100644 --- a/src/window/vram/bgmap.rs +++ b/src/window/vram/bgmap.rs @@ -10,7 +10,10 @@ use crate::{ emulator::SimId, memory::{MemoryMonitor, MemoryView}, vram::{VramImage, VramParams, VramProcessor, VramRenderer, VramTextureLoader}, - window::{utils::UiExt, AppWindow}, + window::{ + utils::{NumberEdit, UiExt}, + AppWindow, + }, }; use super::utils::{parse_palette, CharacterGrid, GENERIC_PALETTE}; @@ -20,7 +23,6 @@ pub struct BgMapWindow { loader: Arc, bgmaps: MemoryView, cell_index: usize, - cell_index_str: String, generic_palette: bool, params: VramParams, scale: f32, @@ -38,7 +40,6 @@ impl BgMapWindow { loader: Arc::new(loader), bgmaps: memory.view(sim_id, 0x00020000, 0x1d800), cell_index: 0, - cell_index_str: "0".into(), generic_palette: false, params, scale: 1.0, @@ -59,11 +60,9 @@ impl BgMapWindow { }); row.col(|ui| { let mut bgmap_index = self.cell_index / 4096; - let mut bgmap_index_str = bgmap_index.to_string(); - ui.number_picker(&mut bgmap_index_str, &mut bgmap_index, 0..14); + ui.add(NumberEdit::new(&mut bgmap_index).range(0..14)); if bgmap_index != self.cell_index / 4096 { self.cell_index = (bgmap_index * 4096) + (self.cell_index % 4096); - self.cell_index_str = self.cell_index.to_string(); } }); }); @@ -72,11 +71,7 @@ impl BgMapWindow { ui.label("Cell"); }); row.col(|ui| { - ui.number_picker( - &mut self.cell_index_str, - &mut self.cell_index, - 0..16 * 4096, - ); + ui.add(NumberEdit::new(&mut self.cell_index).range(0..16 * 4096)); }); }); body.row(row_height, |mut row| { @@ -168,7 +163,6 @@ impl BgMapWindow { .with_selected(self.cell_index % 4096); if let Some(selected) = grid.show(ui) { self.cell_index = (self.cell_index / 4096 * 4096) + selected; - self.cell_index_str = self.cell_index.to_string(); } } } diff --git a/src/window/vram/chardata.rs b/src/window/vram/chardata.rs index dc73b64..d03f0d8 100644 --- a/src/window/vram/chardata.rs +++ b/src/window/vram/chardata.rs @@ -11,7 +11,10 @@ use crate::{ emulator::SimId, memory::{MemoryMonitor, MemoryView}, vram::{VramImage, VramParams, VramProcessor, VramRenderer, VramTextureLoader}, - window::{utils::UiExt as _, AppWindow}, + window::{ + utils::{NumberEdit, UiExt as _}, + AppWindow, + }, }; use super::utils::{self, CharacterGrid}; @@ -83,7 +86,6 @@ pub struct CharacterDataWindow { palettes: MemoryView, palette: VramPalette, index: usize, - index_str: String, params: VramParams, scale: f32, show_grid: bool, @@ -104,7 +106,6 @@ impl CharacterDataWindow { palettes: memory.view(sim_id, 0x0005f860, 16), palette: params.palette, index: params.index, - index_str: params.index.to_string(), params, scale: 4.0, show_grid: true, @@ -123,7 +124,7 @@ impl CharacterDataWindow { ui.label("Index"); }); row.col(|ui| { - ui.number_picker(&mut self.index_str, &mut self.index, 0..2048); + ui.add(NumberEdit::new(&mut self.index).range(0..2048)); }); }); body.row(row_height, |mut row| { @@ -236,7 +237,6 @@ impl CharacterDataWindow { .with_selected(self.index); if let Some(selected) = grid.show(ui) { self.index = selected; - self.index_str = selected.to_string(); } } }