Add a number editor widget
This commit is contained in:
parent
ba15dc77ae
commit
a5676c20d1
|
@ -1,8 +1,8 @@
|
||||||
use std::{ops::Range, str::FromStr};
|
use std::ops::{Bound, RangeBounds};
|
||||||
|
|
||||||
use egui::{
|
use egui::{
|
||||||
ecolor::HexColor, Align, Color32, Frame, Layout, Response, RichText, Sense, TextEdit, Ui,
|
ecolor::HexColor, Align, Color32, CursorIcon, Frame, Layout, Margin, Rect, Response, RichText,
|
||||||
UiBuilder, Vec2, WidgetText,
|
Rounding, Sense, Shape, Stroke, TextEdit, Ui, UiBuilder, Vec2, Widget, WidgetText,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub trait UiExt {
|
pub trait UiExt {
|
||||||
|
@ -24,13 +24,6 @@ pub trait UiExt {
|
||||||
fn color_pair_button(&mut self, left: Color32, right: Color32) -> Response;
|
fn color_pair_button(&mut self, left: Color32, right: Color32) -> Response;
|
||||||
|
|
||||||
fn color_picker(&mut self, color: &mut Color32, hex: &mut String) -> Response;
|
fn color_picker(&mut self, color: &mut Color32, hex: &mut String) -> Response;
|
||||||
|
|
||||||
fn number_picker<T: FromStr + PartialOrd>(
|
|
||||||
&mut self,
|
|
||||||
text: &mut String,
|
|
||||||
value: &mut T,
|
|
||||||
range: Range<T>,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UiExt for Ui {
|
impl UiExt for Ui {
|
||||||
|
@ -90,18 +83,174 @@ impl UiExt for Ui {
|
||||||
)
|
)
|
||||||
.inner
|
.inner
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn number_picker<T: FromStr + PartialOrd>(
|
pub struct NumberEdit<'a> {
|
||||||
&mut self,
|
value: &'a mut usize,
|
||||||
text: &mut String,
|
min: Option<usize>,
|
||||||
value: &mut T,
|
max: Option<usize>,
|
||||||
range: Range<T>,
|
}
|
||||||
) {
|
|
||||||
let res = self.add(TextEdit::singleline(text).horizontal_align(Align::Max));
|
impl<'a> NumberEdit<'a> {
|
||||||
if res.changed() {
|
pub fn new(value: &'a mut usize) -> Self {
|
||||||
if let Some(new_value) = text.parse().ok().filter(|v| range.contains(v)) {
|
Self {
|
||||||
*value = new_value;
|
value,
|
||||||
}
|
min: None,
|
||||||
|
max: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn range(self, range: impl RangeBounds<usize>) -> 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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,10 @@ use crate::{
|
||||||
emulator::SimId,
|
emulator::SimId,
|
||||||
memory::{MemoryMonitor, MemoryView},
|
memory::{MemoryMonitor, MemoryView},
|
||||||
vram::{VramImage, VramParams, VramProcessor, VramRenderer, VramTextureLoader},
|
vram::{VramImage, VramParams, VramProcessor, VramRenderer, VramTextureLoader},
|
||||||
window::{utils::UiExt, AppWindow},
|
window::{
|
||||||
|
utils::{NumberEdit, UiExt},
|
||||||
|
AppWindow,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::utils::{parse_palette, CharacterGrid, GENERIC_PALETTE};
|
use super::utils::{parse_palette, CharacterGrid, GENERIC_PALETTE};
|
||||||
|
@ -20,7 +23,6 @@ pub struct BgMapWindow {
|
||||||
loader: Arc<VramTextureLoader>,
|
loader: Arc<VramTextureLoader>,
|
||||||
bgmaps: MemoryView,
|
bgmaps: MemoryView,
|
||||||
cell_index: usize,
|
cell_index: usize,
|
||||||
cell_index_str: String,
|
|
||||||
generic_palette: bool,
|
generic_palette: bool,
|
||||||
params: VramParams<BgMapParams>,
|
params: VramParams<BgMapParams>,
|
||||||
scale: f32,
|
scale: f32,
|
||||||
|
@ -38,7 +40,6 @@ impl BgMapWindow {
|
||||||
loader: Arc::new(loader),
|
loader: Arc::new(loader),
|
||||||
bgmaps: memory.view(sim_id, 0x00020000, 0x1d800),
|
bgmaps: memory.view(sim_id, 0x00020000, 0x1d800),
|
||||||
cell_index: 0,
|
cell_index: 0,
|
||||||
cell_index_str: "0".into(),
|
|
||||||
generic_palette: false,
|
generic_palette: false,
|
||||||
params,
|
params,
|
||||||
scale: 1.0,
|
scale: 1.0,
|
||||||
|
@ -59,11 +60,9 @@ impl BgMapWindow {
|
||||||
});
|
});
|
||||||
row.col(|ui| {
|
row.col(|ui| {
|
||||||
let mut bgmap_index = self.cell_index / 4096;
|
let mut bgmap_index = self.cell_index / 4096;
|
||||||
let mut bgmap_index_str = bgmap_index.to_string();
|
ui.add(NumberEdit::new(&mut bgmap_index).range(0..14));
|
||||||
ui.number_picker(&mut bgmap_index_str, &mut bgmap_index, 0..14);
|
|
||||||
if bgmap_index != self.cell_index / 4096 {
|
if bgmap_index != self.cell_index / 4096 {
|
||||||
self.cell_index = (bgmap_index * 4096) + (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");
|
ui.label("Cell");
|
||||||
});
|
});
|
||||||
row.col(|ui| {
|
row.col(|ui| {
|
||||||
ui.number_picker(
|
ui.add(NumberEdit::new(&mut self.cell_index).range(0..16 * 4096));
|
||||||
&mut self.cell_index_str,
|
|
||||||
&mut self.cell_index,
|
|
||||||
0..16 * 4096,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
body.row(row_height, |mut row| {
|
body.row(row_height, |mut row| {
|
||||||
|
@ -168,7 +163,6 @@ impl BgMapWindow {
|
||||||
.with_selected(self.cell_index % 4096);
|
.with_selected(self.cell_index % 4096);
|
||||||
if let Some(selected) = grid.show(ui) {
|
if let Some(selected) = grid.show(ui) {
|
||||||
self.cell_index = (self.cell_index / 4096 * 4096) + selected;
|
self.cell_index = (self.cell_index / 4096 * 4096) + selected;
|
||||||
self.cell_index_str = self.cell_index.to_string();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,10 @@ use crate::{
|
||||||
emulator::SimId,
|
emulator::SimId,
|
||||||
memory::{MemoryMonitor, MemoryView},
|
memory::{MemoryMonitor, MemoryView},
|
||||||
vram::{VramImage, VramParams, VramProcessor, VramRenderer, VramTextureLoader},
|
vram::{VramImage, VramParams, VramProcessor, VramRenderer, VramTextureLoader},
|
||||||
window::{utils::UiExt as _, AppWindow},
|
window::{
|
||||||
|
utils::{NumberEdit, UiExt as _},
|
||||||
|
AppWindow,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::utils::{self, CharacterGrid};
|
use super::utils::{self, CharacterGrid};
|
||||||
|
@ -83,7 +86,6 @@ pub struct CharacterDataWindow {
|
||||||
palettes: MemoryView,
|
palettes: MemoryView,
|
||||||
palette: VramPalette,
|
palette: VramPalette,
|
||||||
index: usize,
|
index: usize,
|
||||||
index_str: String,
|
|
||||||
params: VramParams<CharDataParams>,
|
params: VramParams<CharDataParams>,
|
||||||
scale: f32,
|
scale: f32,
|
||||||
show_grid: bool,
|
show_grid: bool,
|
||||||
|
@ -104,7 +106,6 @@ impl CharacterDataWindow {
|
||||||
palettes: memory.view(sim_id, 0x0005f860, 16),
|
palettes: memory.view(sim_id, 0x0005f860, 16),
|
||||||
palette: params.palette,
|
palette: params.palette,
|
||||||
index: params.index,
|
index: params.index,
|
||||||
index_str: params.index.to_string(),
|
|
||||||
params,
|
params,
|
||||||
scale: 4.0,
|
scale: 4.0,
|
||||||
show_grid: true,
|
show_grid: true,
|
||||||
|
@ -123,7 +124,7 @@ impl CharacterDataWindow {
|
||||||
ui.label("Index");
|
ui.label("Index");
|
||||||
});
|
});
|
||||||
row.col(|ui| {
|
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| {
|
body.row(row_height, |mut row| {
|
||||||
|
@ -236,7 +237,6 @@ impl CharacterDataWindow {
|
||||||
.with_selected(self.index);
|
.with_selected(self.index);
|
||||||
if let Some(selected) = grid.show(ui) {
|
if let Some(selected) = grid.show(ui) {
|
||||||
self.index = selected;
|
self.index = selected;
|
||||||
self.index_str = selected.to_string();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue