diff --git a/src/window/utils.rs b/src/window/utils.rs index 7aab7b7..d85d631 100644 --- a/src/window/utils.rs +++ b/src/window/utils.rs @@ -1,9 +1,10 @@ use std::{ - fmt::Display, + fmt::{Display, UpperHex}, ops::{Bound, RangeBounds}, str::FromStr, }; +use atoi::FromRadix16; use egui::{ ecolor::HexColor, Align, Color32, CursorIcon, Event, Frame, Key, Layout, Margin, Rect, Response, RichText, Rounding, Sense, Shape, Stroke, TextEdit, Ui, UiBuilder, Vec2, Widget, @@ -38,7 +39,12 @@ impl UiExt for Ui { let mut frame = Frame::group(self.style()); frame.outer_margin.top += 10.0; frame.inner_margin.top += 2.0; - let res = self.push_id(&title, |ui| frame.show(ui, add_contents)); + let res = self.push_id(&title, |ui| { + frame.show(ui, |ui| { + ui.set_min_width(ui.available_width()); + add_contents(ui); + }) + }); let text = RichText::new(title).background_color(self.style().visuals.panel_fill); let old_rect = res.response.rect; let mut text_rect = old_rect; @@ -98,11 +104,35 @@ enum Direction { } pub trait Number: - Copy + One + CheckedAdd + CheckedSub + Eq + Ord + Display + FromStr + Send + Sync + 'static + Copy + + One + + CheckedAdd + + CheckedSub + + Eq + + Ord + + Display + + FromStr + + FromRadix16 + + UpperHex + + Send + + Sync + + 'static { } impl< - T: Copy + One + CheckedAdd + CheckedSub + Eq + Ord + Display + FromStr + Send + Sync + 'static, + T: Copy + + One + + CheckedAdd + + CheckedSub + + Eq + + Ord + + Display + + FromStr + + FromRadix16 + + UpperHex + + Send + + Sync + + 'static, > Number for T { } @@ -113,6 +143,8 @@ pub struct NumberEdit<'a, T: Number> { precision: usize, min: Option, max: Option, + arrows: bool, + hex: bool, } impl<'a, T: Number> NumberEdit<'a, T> { @@ -123,6 +155,8 @@ impl<'a, T: Number> NumberEdit<'a, T> { precision: 3, min: None, max: None, + arrows: true, + hex: false, } } @@ -143,12 +177,35 @@ impl<'a, T: Number> NumberEdit<'a, T> { }; Self { min, max, ..self } } + + pub fn arrows(self, arrows: bool) -> Self { + Self { arrows, ..self } + } + + pub fn hex(self, hex: bool) -> Self { + Self { hex, ..self } + } } impl Widget for NumberEdit<'_, T> { fn ui(self, ui: &mut Ui) -> Response { let id = ui.id(); - let to_string = |val: &T| format!("{val:.0$}", self.precision); + let to_string = |val: &T| { + if self.hex { + format!("{val:.0$X}", self.precision) + } else { + format!("{val:.0$}", self.precision) + } + }; + let from_string = |val: &str| { + if self.hex { + let bytes = val.as_bytes(); + let (result, consumed) = T::from_radix_16(bytes); + (consumed == bytes.len()).then_some(result) + } else { + val.parse::().ok() + } + }; let (last_value, mut str, focus) = ui.memory(|m| { let (lv, s) = m @@ -163,7 +220,7 @@ impl Widget for NumberEdit<'_, T> { str = to_string(self.value); stale = true; } - let valid = str.parse().is_ok_and(|v: T| v == *self.value); + let valid = from_string(&str).is_some_and(|v: T| v == *self.value); let mut up_pressed = false; let mut down_pressed = false; if focus { @@ -194,17 +251,25 @@ impl Widget for NumberEdit<'_, T> { .id(id) .margin(Margin { left: 4.0, - right: 20.0, + right: if self.arrows { 20.0 } else { 4.0 }, top: 2.0, bottom: 2.0, }); - let res = if valid { + let mut 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}."), + (Some(min), Some(max)) => format!( + "Please enter a number between {} and {}.", + to_string(&min), + to_string(&max) + ), + (Some(min), None) => { + format!("Please enter a number greater than {}.", to_string(&min)) + } + (None, Some(max)) => { + format!("Please enter a number less than {}.", to_string(&max)) + } (None, None) => "Please enter a number.".to_string(), }; ui.scope(|ui| { @@ -218,26 +283,28 @@ impl Widget for NumberEdit<'_, T> { .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 = 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 = 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 = Some(Direction::Down); + if self.arrows { + 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 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 = 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 = Some(Direction::Down); + } } let in_range = @@ -248,12 +315,18 @@ impl Widget for NumberEdit<'_, T> { Direction::Down => self.value.checked_sub(&self.increment), }; if let Some(new_value) = value.filter(in_range) { + if *self.value != new_value { + res.mark_changed(); + } *self.value = new_value; } str = to_string(self.value); stale = true; } else if res.changed { - if let Some(new_value) = str.parse().ok().filter(in_range) { + if let Some(new_value) = from_string(&str).filter(in_range) { + if *self.value != new_value { + res.mark_changed(); + } *self.value = new_value; } stale = true; diff --git a/src/window/vram/registers.rs b/src/window/vram/registers.rs index 882b2b5..2301d40 100644 --- a/src/window/vram/registers.rs +++ b/src/window/vram/registers.rs @@ -1,20 +1,22 @@ use std::sync::Arc; use egui::{ - Align, Button, CentralPanel, Checkbox, Context, Label, Layout, ScrollArea, TextEdit, Ui, - ViewportBuilder, ViewportId, + Align, Button, CentralPanel, Checkbox, Color32, Context, Direction, Label, Layout, ScrollArea, + TextEdit, Ui, ViewportBuilder, ViewportId, }; use egui_extras::{Column, Size, StripBuilder, TableBuilder}; use crate::{ emulator::SimId, - memory::{MemoryClient, MemoryValue, MemoryView}, + memory::{MemoryClient, MemoryRef, MemoryValue, MemoryView}, window::{ utils::{NumberEdit, UiExt}, AppWindow, }, }; +use super::utils; + pub struct RegisterWindow { sim_id: SimId, memory: Arc, @@ -26,7 +28,7 @@ impl RegisterWindow { Self { sim_id, memory: memory.clone(), - registers: memory.watch(sim_id, 0x0005f800, 0x70), + registers: memory.watch(sim_id, 0x0005f800, 0x72), } } @@ -36,21 +38,21 @@ impl RegisterWindow { let mut intenb = InterruptReg::parse(raw_intenb); let mut intpnd = InterruptReg::parse(raw_intpnd); ui.section("Interrupt", |ui| { - ui.vertical(|ui| { + let width = ui.available_width(); + ui.with_layout(Layout::top_down_justified(Align::Center), |ui| { TableBuilder::new(ui) .id_salt("raw_values") - .column(Column::auto()) - .column(Column::remainder()) + .columns(Column::exact(width * 0.5), 2) .cell_layout(Layout::left_to_right(Align::Max)) .body(|mut body| { body.row(row_height, |mut row| { row.col(|ui| { - ui.label("INTENB"); + ui.add_sized(ui.available_size(), Label::new("INTENB")); }); row.col(|ui| { let mut text = format!("{raw_intenb:04x}"); - ui.add_enabled( - false, + ui.add_sized( + ui.available_size(), TextEdit::singleline(&mut text).horizontal_align(Align::Max), ); }); @@ -71,17 +73,17 @@ impl RegisterWindow { ui.add_space(8.0); TableBuilder::new(ui) .id_salt("flags") - .column(Column::auto()) - .columns(Column::remainder(), 2) + .column(Column::exact(width * 0.5)) + .columns(Column::exact(width * 0.25), 2) .cell_layout(Layout::left_to_right(Align::Center).with_main_align(Align::RIGHT)) .body(|mut body| { body.row(row_height, |mut row| { row.col(|_ui| {}); row.col(|ui| { - ui.add_sized([ui.available_width(), 0.0], Label::new("ENB")); + ui.add_sized(ui.available_size(), Label::new("ENB")); }); row.col(|ui| { - ui.add_sized([ui.available_width(), 0.0], Label::new("PND")); + ui.add_sized(ui.available_size(), Label::new("PND")); }); }); let mut add_row = |label: &str, enb: &mut bool, pnd: &mut bool| { @@ -112,7 +114,7 @@ impl RegisterWindow { add_row("LFBEND", &mut intenb.lfbend, &mut intpnd.lfbend); add_row("SCANERR", &mut intenb.scanerr, &mut intpnd.scanerr); }); - ui.add_space(ui.available_height()); + ui.allocate_space(ui.available_size()); }); }); if intpnd.update(&mut raw_intpnd) { @@ -128,9 +130,10 @@ impl RegisterWindow { let mut raw_dpstts = self.read_address(0x0005f820); let mut dpstts = DisplayReg::parse(raw_dpstts); ui.section("Display", |ui| { + let width = ui.available_width(); TableBuilder::new(ui) - .column(Column::auto()) - .column(Column::remainder()) + .column(Column::exact(width * 0.5)) + .column(Column::exact(width * 0.5)) .cell_layout(Layout::left_to_right(Align::Max)) .body(|mut body| { body.row(row_height, |mut row| { @@ -197,7 +200,7 @@ impl RegisterWindow { }); }); }); - ui.vertical(|ui| ui.add_space((ui.available_height() - row_height).max(0.0))); + ui.allocate_space(ui.available_size()); }); if dpstts.update(&mut raw_dpstts) { self.memory.write(self.sim_id, 0x0005f822, &raw_dpstts); @@ -209,9 +212,10 @@ impl RegisterWindow { let [mut raw_xpstts, raw_xpctrl] = self.read_address(0x0005f840); let mut xpstts = DrawingReg::parse(raw_xpstts); ui.section("Drawing", |ui| { + let width = ui.available_width(); TableBuilder::new(ui) - .column(Column::auto()) - .column(Column::remainder()) + .column(Column::exact(width * 0.5)) + .column(Column::exact(width * 0.5)) .cell_layout(Layout::left_to_right(Align::Max)) .body(|mut body| { body.row(row_height, |mut row| { @@ -257,7 +261,9 @@ impl RegisterWindow { row.col(|ui| { ui.add_enabled( false, - NumberEdit::new(&mut xpstts.sbcount).range(0..32), + NumberEdit::new(&mut xpstts.sbcount) + .arrows(false) + .range(0..32), ); }); }); @@ -291,7 +297,7 @@ impl RegisterWindow { }); }); }); - ui.vertical(|ui| ui.add_space(ui.available_height())); + ui.allocate_space(ui.available_size()); }); if xpstts.update(&mut raw_xpstts) { xpstts.update(&mut raw_xpstts); @@ -299,10 +305,185 @@ impl RegisterWindow { } } - fn read_address(&self, address: usize) -> T { - let index = (address - 0x0005f800) / size_of::(); - self.registers.borrow().read(index) + fn show_colors(&mut self, ui: &mut Ui) { + let row_height = ui.spacing().interact_size.y; + let registers = self.registers.borrow(); + ui.section("Colors", |ui| { + TableBuilder::new(ui) + .column(Column::auto()) + .columns(Column::remainder(), 4) + .cell_layout(Layout::left_to_right(Align::Max)) + .body(|mut body| { + body.row(row_height, |mut row| { + row.col(|_ui| {}); + row.col(|_ui| {}); + row.col(|ui| { + ui.with_layout( + Layout::centered_and_justified(Direction::LeftToRight), + |ui| ui.label("1"), + ); + }); + row.col(|ui| { + ui.with_layout( + Layout::centered_and_justified(Direction::LeftToRight), + |ui| ui.label("2"), + ); + }); + row.col(|ui| { + ui.with_layout( + Layout::centered_and_justified(Direction::LeftToRight), + |ui| ui.label("3"), + ); + }); + }); + let mut brts: [u16; 3] = [ + read_address(®isters, 0x0005f824), + read_address(®isters, 0x0005f826), + read_address(®isters, 0x0005f828), + ]; + body.row(row_height, |mut row| { + let mut stale = false; + row.col(|ui| { + ui.label("BRT"); + }); + row.col(|_ui| {}); + for brt in brts.iter_mut() { + row.col(|ui| { + if ui + .add(NumberEdit::new(brt).range(0..256).arrows(false).hex(true)) + .changed() + { + stale = true; + } + }); + } + if stale { + self.memory.write(self.sim_id, 0x0005f824, &brts); + } + }); + body.row(row_height, |mut row| { + row.col(|_ui| {}); + for shade in utils::parse_brts(&brts, Color32::RED) { + row.col(|ui| { + ui.painter().rect_filled( + ui.available_rect_before_wrap(), + 0.0, + shade, + ); + }); + } + }); + let mut palettes = read_address::<[u16; 8]>(®isters, 0x0005f860); + let mut add_row = |name: &str, address: u32, value: &mut u16| { + let mut c1 = (*value >> 2) & 0x03; + let mut c2 = (*value >> 4) & 0x03; + let mut c3 = (*value >> 6) & 0x03; + let mut stale = false; + body.row(row_height, |mut row| { + row.col(|ui| { + ui.label(name); + }); + row.col(|ui| { + if ui + .add( + NumberEdit::new(value) + .range(0..256) + .arrows(false) + .hex(true), + ) + .changed() + { + stale = true; + }; + }); + row.col(|ui| { + if ui + .add(NumberEdit::new(&mut c1).range(0..4).arrows(false)) + .changed() + { + *value = (*value & 0xfff3) | (c1 << 2); + stale = true; + } + }); + row.col(|ui| { + if ui + .add(NumberEdit::new(&mut c2).range(0..4).arrows(false)) + .changed() + { + *value = (*value & 0xffcf) | (c2 << 4); + stale = true; + } + }); + row.col(|ui| { + if ui + .add(NumberEdit::new(&mut c3).range(0..4).arrows(false)) + .changed() + { + *value = (*value & 0xff3f) | (c3 << 6); + stale = true; + } + }); + }); + if stale { + self.memory.write(self.sim_id, address, value); + } + }; + + add_row("GPLT0", 0x0005f860, &mut palettes[0]); + add_row("GPLT1", 0x0005f862, &mut palettes[1]); + add_row("GPLT2", 0x0005f864, &mut palettes[2]); + add_row("GPLT3", 0x0005f866, &mut palettes[3]); + add_row("JPLT0", 0x0005f868, &mut palettes[4]); + add_row("JPLT1", 0x0005f86a, &mut palettes[5]); + add_row("JPLT2", 0x0005f86c, &mut palettes[6]); + add_row("JPLT3", 0x0005f86e, &mut palettes[7]); + + body.row(row_height, |mut row| { + row.col(|ui| { + ui.label("BKCOL"); + }); + row.col(|ui| { + let mut bkcol: u16 = read_address(®isters, 0x0005f870); + if ui + .add(NumberEdit::new(&mut bkcol).range(0..4).arrows(false)) + .changed() + { + self.memory.write(self.sim_id, 0x0005f870, &bkcol); + } + }); + row.col(|_ui| {}); + row.col(|ui| { + ui.label("REST"); + }); + row.col(|ui| { + let mut rest: u16 = read_address(®isters, 0x0005f82a); + if ui + .add( + NumberEdit::new(&mut rest) + .range(0..256) + .arrows(false) + .hex(true), + ) + .changed() + { + self.memory.write(self.sim_id, 0x0005f82a, &rest); + } + }); + }); + }); + + ui.allocate_space(ui.available_size_before_wrap()); + }); } + + fn read_address(&self, address: usize) -> T { + read_address(&self.registers.borrow(), address) + } +} + +fn read_address(registers: &MemoryRef, address: usize) -> T { + let index = (address - 0x0005f800) / size_of::(); + registers.read(index) } impl AppWindow for RegisterWindow { @@ -317,15 +498,19 @@ impl AppWindow for RegisterWindow { fn initial_viewport(&self) -> ViewportBuilder { ViewportBuilder::default() .with_title(format!("Registers ({})", self.sim_id)) - .with_inner_size((640.0, 480.0)) + .with_inner_size((800.0, 480.0)) } fn show(&mut self, ctx: &Context) { CentralPanel::default().show(ctx, |ui| { ScrollArea::vertical().show(ui, |ui| { ui.horizontal_top(|ui| { + let width = ui.available_width() - (ui.spacing().item_spacing.x * 6.0); StripBuilder::new(ui) - .sizes(Size::remainder(), 4) + .size(Size::exact(width * 0.2)) + .size(Size::exact(width * 0.2)) + .size(Size::exact(width * 0.3)) + .size(Size::exact(width * 0.2)) .horizontal(|mut strip| { strip.cell(|ui| { self.show_interrupts(ui); @@ -340,6 +525,9 @@ impl AppWindow for RegisterWindow { }); }); }); + strip.cell(|ui| { + self.show_colors(ui); + }); }); }); }); diff --git a/src/window/vram/utils.rs b/src/window/vram/utils.rs index 98154da..726b18f 100644 --- a/src/window/vram/utils.rs +++ b/src/window/vram/utils.rs @@ -28,6 +28,15 @@ pub const fn parse_shades(brts: &[u8; 8]) -> [u8; 4] { ] } +pub fn parse_brts(brts: &[u16; 3], color: Color32) -> [Color32; 4] { + [ + Color32::BLACK, + shade(brts[0] as u8, color), + shade(brts[1] as u8, color), + shade((brts[0] + brts[1] + brts[2]) as u8, color), + ] +} + pub fn palette_colors(palette: u8, brts: &[u8; 8], color: Color32) -> [Color32; 4] { let colors = parse_shades(brts).map(|s| shade(s, color)); parse_palette(palette).map(|p| colors[p as usize])