From 4601f1b52f6d2ef6ceb391e4e4f348fb5c0c0c21 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Thu, 6 Feb 2025 23:17:11 -0500 Subject: [PATCH] Flesh out bgmap viewer --- src/memory.rs | 7 +- src/window.rs | 1 + src/window/game.rs | 69 +--------- src/window/utils.rs | 107 +++++++++++++++ src/window/vram/bgmap.rs | 259 +++++++++++++++++++++++++++++++++--- src/window/vram/chardata.rs | 104 +++------------ src/window/vram/utils.rs | 99 ++++++++++++++ 7 files changed, 469 insertions(+), 177 deletions(-) create mode 100644 src/window/utils.rs diff --git a/src/memory.rs b/src/memory.rs index 9279b44..dcafe7a 100644 --- a/src/memory.rs +++ b/src/memory.rs @@ -70,9 +70,12 @@ pub struct MemoryRef<'a> { } impl MemoryRef<'_> { - pub fn read(&self, index: usize) -> u8 { - self.inner[index] + pub fn read(&self, index: usize) -> T { + let from = index * size_of::(); + let to = from + size_of::(); + *bytemuck::from_bytes(&self.inner[from..to]) } + pub fn range(&self, start: usize, count: usize) -> &[T] { let from = start * size_of::(); let to = from + (count * size_of::()); diff --git a/src/window.rs b/src/window.rs index da1e265..ac5922e 100644 --- a/src/window.rs +++ b/src/window.rs @@ -13,6 +13,7 @@ mod game; mod game_screen; mod gdb; mod input; +mod utils; mod vram; pub trait AppWindow { diff --git a/src/window/game.rs b/src/window/game.rs index aa7c2ca..1130b65 100644 --- a/src/window/game.rs +++ b/src/window/game.rs @@ -6,9 +6,8 @@ use crate::{ persistence::Persistence, }; use egui::{ - ecolor::HexColor, menu, Align2, Button, CentralPanel, Color32, Context, Direction, Frame, - Layout, Response, Sense, TopBottomPanel, Ui, Vec2, ViewportBuilder, ViewportCommand, - ViewportId, WidgetText, Window, + menu, Align2, Button, CentralPanel, Color32, Context, Direction, Frame, TopBottomPanel, Ui, + Vec2, ViewportBuilder, ViewportCommand, ViewportId, Window, }; use egui_toast::{Toast, Toasts}; use serde::{Deserialize, Serialize}; @@ -16,6 +15,7 @@ use winit::event_loop::EventLoopProxy; use super::{ game_screen::{DisplayMode, GameScreen}, + utils::UiExt as _, AppWindow, }; @@ -397,69 +397,6 @@ impl AppWindow for GameWindow { } } -trait UiExt { - fn selectable_button(&mut self, selected: bool, text: impl Into) -> Response; - fn selectable_option( - &mut self, - current_value: &mut T, - selected_value: T, - text: impl Into, - ) -> Response { - let response = self.selectable_button(*current_value == selected_value, text); - if response.clicked() { - *current_value = selected_value; - } - response - } - - fn color_pair_button(&mut self, left: Color32, right: Color32) -> Response; - - fn color_picker(&mut self, color: &mut Color32, hex: &mut String) -> Response; -} - -impl UiExt for Ui { - fn selectable_button(&mut self, selected: bool, text: impl Into) -> Response { - self.style_mut().visuals.widgets.inactive.bg_fill = Color32::TRANSPARENT; - self.style_mut().visuals.widgets.hovered.bg_fill = Color32::TRANSPARENT; - self.style_mut().visuals.widgets.active.bg_fill = Color32::TRANSPARENT; - let mut selected = selected; - self.checkbox(&mut selected, text) - } - - fn color_pair_button(&mut self, left: Color32, right: Color32) -> Response { - let button_size = Vec2::new(60.0, 20.0); - let (rect, response) = self.allocate_at_least(button_size, Sense::click()); - let center_x = rect.center().x; - let left_rect = rect.with_max_x(center_x); - self.painter().rect_filled(left_rect, 0.0, left); - let right_rect = rect.with_min_x(center_x); - self.painter().rect_filled(right_rect, 0.0, right); - - let style = self.style().interact(&response); - self.painter().rect_stroke(rect, 0.0, style.fg_stroke); - response - } - - fn color_picker(&mut self, color: &mut Color32, hex: &mut String) -> Response { - self.allocate_ui_with_layout( - Vec2::new(100.0, 130.0), - Layout::top_down_justified(egui::Align::Center), - |ui| { - let (rect, _) = ui.allocate_at_least(Vec2::new(100.0, 100.0), Sense::hover()); - ui.painter().rect_filled(rect, 0.0, *color); - let resp = ui.text_edit_singleline(hex); - if resp.changed() { - if let Ok(new_color) = HexColor::from_str_without_hash(hex) { - *color = new_color.color(); - } - } - resp - }, - ) - .inner - } -} - struct ColorPickerState { color_codes: [String; 2], just_opened: bool, diff --git a/src/window/utils.rs b/src/window/utils.rs new file mode 100644 index 0000000..1887671 --- /dev/null +++ b/src/window/utils.rs @@ -0,0 +1,107 @@ +use std::{ops::Range, str::FromStr}; + +use egui::{ + ecolor::HexColor, Align, Color32, Frame, Layout, Response, RichText, Sense, TextEdit, Ui, + UiBuilder, Vec2, WidgetText, +}; + +pub trait UiExt { + fn section(&mut self, title: impl Into, add_contents: impl FnOnce(&mut Ui)); + fn selectable_button(&mut self, selected: bool, text: impl Into) -> Response; + fn selectable_option( + &mut self, + current_value: &mut T, + selected_value: T, + text: impl Into, + ) -> Response { + let response = self.selectable_button(*current_value == selected_value, text); + if response.clicked() { + *current_value = selected_value; + } + 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 number_picker( + &mut self, + text: &mut String, + value: &mut T, + range: Range, + ); +} + +impl UiExt for Ui { + fn section(&mut self, title: impl Into, add_contents: impl FnOnce(&mut Ui)) { + let mut frame = Frame::group(self.style()); + frame.outer_margin.top += 10.0; + frame.inner_margin.top += 2.0; + let res = frame.show(self, add_contents); + let text = RichText::new(title).background_color(self.style().visuals.panel_fill); + let old_rect = res.response.rect; + let mut text_rect = old_rect; + text_rect.min.x += 6.0; + let new_rect = self + .allocate_new_ui(UiBuilder::new().max_rect(text_rect), |ui| ui.label(text)) + .response + .rect; + self.allocate_space((old_rect.max - new_rect.max) - (old_rect.min - new_rect.min)); + } + + fn selectable_button(&mut self, selected: bool, text: impl Into) -> Response { + self.style_mut().visuals.widgets.inactive.bg_fill = Color32::TRANSPARENT; + self.style_mut().visuals.widgets.hovered.bg_fill = Color32::TRANSPARENT; + self.style_mut().visuals.widgets.active.bg_fill = Color32::TRANSPARENT; + let mut selected = selected; + self.checkbox(&mut selected, text) + } + + fn color_pair_button(&mut self, left: Color32, right: Color32) -> Response { + let button_size = Vec2::new(60.0, 20.0); + let (rect, response) = self.allocate_at_least(button_size, Sense::click()); + let center_x = rect.center().x; + let left_rect = rect.with_max_x(center_x); + self.painter().rect_filled(left_rect, 0.0, left); + let right_rect = rect.with_min_x(center_x); + self.painter().rect_filled(right_rect, 0.0, right); + + let style = self.style().interact(&response); + self.painter().rect_stroke(rect, 0.0, style.fg_stroke); + response + } + + fn color_picker(&mut self, color: &mut Color32, hex: &mut String) -> Response { + self.allocate_ui_with_layout( + Vec2::new(100.0, 130.0), + Layout::top_down_justified(egui::Align::Center), + |ui| { + let (rect, _) = ui.allocate_at_least(Vec2::new(100.0, 100.0), Sense::hover()); + ui.painter().rect_filled(rect, 0.0, *color); + let resp = ui.text_edit_singleline(hex); + if resp.changed() { + if let Ok(new_color) = HexColor::from_str_without_hash(hex) { + *color = new_color.color(); + } + } + resp + }, + ) + .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; + } + } + } +} diff --git a/src/window/vram/bgmap.rs b/src/window/vram/bgmap.rs index 5997e3a..2a4962d 100644 --- a/src/window/vram/bgmap.rs +++ b/src/window/vram/bgmap.rs @@ -1,20 +1,30 @@ use std::sync::Arc; -use egui::{CentralPanel, ColorImage, Context, Image, TextureOptions, ViewportBuilder, ViewportId}; +use egui::{ + Align, CentralPanel, Checkbox, Color32, ColorImage, Context, Image, ScrollArea, Slider, + TextEdit, TextureOptions, Ui, ViewportBuilder, ViewportId, +}; +use egui_extras::{Column, Size, StripBuilder, TableBuilder}; use serde::{Deserialize, Serialize}; use crate::{ emulator::SimId, memory::{MemoryMonitor, MemoryView}, vram::{VramImageLoader, VramResource as _, VramTextureLoader}, - window::AppWindow, + window::{utils::UiExt, AppWindow}, }; -use super::utils::parse_palette; +use super::utils::{parse_palette, CharacterGrid, GENERIC_PALETTE}; pub struct BgMapWindow { sim_id: SimId, loader: Option, + bgmaps: MemoryView, + cell_index: usize, + cell_index_str: String, + scale: f32, + show_grid: bool, + generic_palette: bool, } impl BgMapWindow { @@ -22,6 +32,142 @@ impl BgMapWindow { Self { sim_id, loader: Some(BgMapLoader::new(sim_id, memory)), + bgmaps: memory.view(sim_id, 0x00020000, 0x1d800), + cell_index: 0, + cell_index_str: "0".into(), + scale: 1.0, + show_grid: false, + generic_palette: false, + } + } + + fn show_form(&mut self, ui: &mut Ui) { + let row_height = ui.spacing().interact_size.y; + ui.vertical(|ui| { + TableBuilder::new(ui) + .column(Column::auto()) + .column(Column::remainder()) + .body(|mut body| { + body.row(row_height, |mut row| { + row.col(|ui| { + ui.label("Map"); + }); + 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); + 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(); + } + }); + }); + body.row(row_height, |mut row| { + row.col(|ui| { + ui.label("Cell"); + }); + row.col(|ui| { + ui.number_picker( + &mut self.cell_index_str, + &mut self.cell_index, + 0..16 * 4096, + ); + }); + }); + body.row(row_height, |mut row| { + row.col(|ui| { + ui.label("Address"); + }); + row.col(|ui| { + let address = 0x00020000 + (self.cell_index * 2); + let mut address_str = format!("{address:08x}"); + ui.add_enabled( + false, + TextEdit::singleline(&mut address_str).horizontal_align(Align::Max), + ); + }); + }); + }); + let resource = BgMapResource::Cell { + index: self.cell_index, + generic_palette: self.generic_palette, + }; + let image = Image::new(resource.to_uri()) + .maintain_aspect_ratio(true) + .tint(Color32::RED) + .texture_options(TextureOptions::NEAREST); + ui.add(image); + ui.section("Cell", |ui| { + let cell = self.bgmaps.borrow().read::(self.cell_index); + let (char_index, mut vflip, mut hflip, palette_index) = parse_cell(cell); + TableBuilder::new(ui) + .column(Column::remainder()) + .column(Column::remainder()) + .body(|mut body| { + body.row(row_height, |mut row| { + row.col(|ui| { + ui.label("Character"); + }); + row.col(|ui| { + let mut character_str = char_index.to_string(); + ui.add_enabled( + false, + TextEdit::singleline(&mut character_str) + .horizontal_align(Align::Max), + ); + }); + }); + body.row(row_height, |mut row| { + row.col(|ui| { + ui.label("Palette"); + }); + row.col(|ui| { + let mut palette = format!("BG {}", palette_index); + ui.add_enabled( + false, + TextEdit::singleline(&mut palette).horizontal_align(Align::Max), + ); + }); + }); + body.row(row_height, |mut row| { + row.col(|ui| { + let checkbox = Checkbox::new(&mut hflip, "H-flip"); + ui.add_enabled(false, checkbox); + }); + row.col(|ui| { + let checkbox = Checkbox::new(&mut vflip, "V-flip"); + ui.add_enabled(false, checkbox); + }); + }); + }); + ui.section("Display", |ui| { + ui.horizontal(|ui| { + ui.label("Scale"); + ui.spacing_mut().slider_width = ui.available_width(); + let slider = Slider::new(&mut self.scale, 1.0..=10.0) + .step_by(1.0) + .show_value(false); + ui.add(slider); + }); + ui.checkbox(&mut self.show_grid, "Show grid"); + ui.checkbox(&mut self.generic_palette, "Generic palette"); + }); + }); + }); + } + + fn show_bgmap(&mut self, ui: &mut Ui) { + let resource = BgMapResource::BgMap { + index: self.cell_index / 4096, + generic_palette: self.generic_palette, + }; + let grid = CharacterGrid::new(resource.to_uri()) + .with_scale(self.scale) + .with_grid(self.show_grid) + .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(); } } } @@ -48,16 +194,35 @@ impl AppWindow for BgMapWindow { fn show(&mut self, ctx: &Context) { CentralPanel::default().show(ctx, |ui| { - let resource = BgMapResource { index: 0 }; - let image = Image::new(resource.to_uri()).texture_options(TextureOptions::NEAREST); - ui.add(image); + ui.horizontal_top(|ui| { + StripBuilder::new(ui) + .size(Size::relative(0.3)) + .size(Size::remainder()) + .horizontal(|mut strip| { + strip.cell(|ui| { + ScrollArea::vertical().show(ui, |ui| self.show_form(ui)); + }); + strip.cell(|ui| { + ScrollArea::both().show(ui, |ui| self.show_bgmap(ui)); + }); + }) + }); }); } } +fn parse_cell(cell: u16) -> (usize, bool, bool, usize) { + let char_index = (cell & 0x7ff) as usize; + let vflip = cell & 0x1000 != 0; + let hflip = cell & 0x2000 != 0; + let palette_index = (cell >> 14) as usize; + (char_index, vflip, hflip, palette_index) +} + #[derive(Serialize, Deserialize, PartialEq, Eq, Hash)] -struct BgMapResource { - index: usize, +enum BgMapResource { + BgMap { index: usize, generic_palette: bool }, + Cell { index: usize, generic_palette: bool }, } struct BgMapLoader { @@ -77,27 +242,37 @@ impl BgMapLoader { } } - fn load_bgmap(&self, index: usize) -> Option { + fn load_bgmap(&self, bgmap_index: usize, generic_palette: bool) -> Option { let chardata = self.chardata.borrow(); let bgmaps = self.bgmaps.borrow(); let brightness = self.brightness.borrow(); let palettes = self.palettes.borrow(); let brts = brightness.range::(0, 8); - let colors = [ - parse_palette(palettes.read(0), brts), - parse_palette(palettes.read(2), brts), - parse_palette(palettes.read(4), brts), - parse_palette(palettes.read(6), brts), - ]; + let colors = if generic_palette { + [ + GENERIC_PALETTE, + GENERIC_PALETTE, + GENERIC_PALETTE, + GENERIC_PALETTE, + ] + } else { + [ + parse_palette(palettes.read(0), brts), + parse_palette(palettes.read(2), brts), + parse_palette(palettes.read(4), brts), + parse_palette(palettes.read(6), brts), + ] + }; let mut data = vec![0u8; 512 * 512]; - for (i, cell) in bgmaps.range::(index * 4096, 4096).iter().enumerate() { - let char_index = (cell & 0x7ff) as usize; + for (i, cell) in bgmaps + .range::(bgmap_index * 4096, 4096) + .iter() + .enumerate() + { + let (char_index, vflip, hflip, palette_index) = parse_cell(*cell); let char = chardata.range::(char_index * 8, 8); - let vflip = cell & 0x1000 != 0; - let hflip = cell & 0x2000 != 0; - let palette_index = (cell >> 14) as usize; let palette = &colors[palette_index]; let mut target_idx = (i % 64) * 8 + (i / 64) * 8 * 512; @@ -114,6 +289,38 @@ impl BgMapLoader { Some(ColorImage::from_gray([512, 512], &data)) } + fn load_bgmap_cell(&self, index: usize, generic_palette: bool) -> Option { + let chardata = self.chardata.borrow(); + let bgmaps = self.bgmaps.borrow(); + let brightness = self.brightness.borrow(); + let palettes = self.palettes.borrow(); + + let brts = brightness.range::(0, 8); + + let mut data = vec![0u8; 8 * 8]; + let cell = bgmaps.read::(index); + + let (char_index, vflip, hflip, palette_index) = parse_cell(cell); + let char = chardata.range::(char_index * 8, 8); + let palette = if generic_palette { + GENERIC_PALETTE + } else { + parse_palette(palettes.read(palette_index * 2), brts) + }; + + let mut target_idx = 0; + for row in 0..8 { + let dests = &mut data[target_idx..target_idx + 8]; + let pixels = self.read_char_row(char, hflip, vflip, row); + for (dest, pixel) in dests.iter_mut().zip(pixels) { + *dest = palette[pixel as usize]; + } + target_idx += 8; + } + + Some(ColorImage::from_gray([8, 8], &data)) + } + fn read_char_row( &self, char: &[u16], @@ -137,8 +344,16 @@ impl VramImageLoader for BgMapLoader { } fn add(&self, resource: &Self::Resource) -> Option { - let BgMapResource { index } = resource; - self.load_bgmap(*index) + match resource { + BgMapResource::BgMap { + index, + generic_palette, + } => self.load_bgmap(*index, *generic_palette), + BgMapResource::Cell { + index, + generic_palette, + } => self.load_bgmap_cell(*index, *generic_palette), + } } fn update<'a>( diff --git a/src/window/vram/chardata.rs b/src/window/vram/chardata.rs index 65be884..a8eb3f7 100644 --- a/src/window/vram/chardata.rs +++ b/src/window/vram/chardata.rs @@ -1,9 +1,8 @@ use std::{fmt::Display, sync::Arc}; use egui::{ - Align, CentralPanel, Color32, ColorImage, ComboBox, Context, Frame, Image, RichText, - ScrollArea, Sense, Slider, TextEdit, TextureOptions, Ui, UiBuilder, Vec2, ViewportBuilder, - ViewportId, + Align, CentralPanel, Color32, ColorImage, ComboBox, Context, Image, ScrollArea, Slider, + TextEdit, TextureOptions, Ui, Vec2, ViewportBuilder, ViewportId, }; use egui_extras::{Column, Size, StripBuilder, TableBuilder}; use serde::{Deserialize, Serialize}; @@ -12,10 +11,10 @@ use crate::{ emulator::SimId, memory::{MemoryMonitor, MemoryView}, vram::{VramImageLoader, VramResource as _, VramTextureLoader}, - window::AppWindow, + window::{utils::UiExt as _, AppWindow}, }; -use super::utils; +use super::utils::{self, CharacterGrid}; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum VramPalette { @@ -115,17 +114,7 @@ impl CharacterDataWindow { ui.label("Index"); }); row.col(|ui| { - let res = ui.add( - TextEdit::singleline(&mut self.index_str) - .horizontal_align(Align::Max), - ); - if res.changed() { - if let Some(index) = - self.index_str.parse().ok().filter(|id| *id < 2048) - { - self.index = index; - } - } + ui.number_picker(&mut self.index_str, &mut self.index, 0..2048); }); }); body.row(row_height, |mut row| { @@ -134,10 +123,10 @@ impl CharacterDataWindow { }); row.col(|ui| { let address = match self.index { - 0x000..0x200 => 0x00060000 + self.index, - 0x200..0x400 => 0x000e0000 + (self.index - 0x200), - 0x400..0x600 => 0x00160000 + (self.index - 0x400), - 0x600..0x800 => 0x001e0000 + (self.index - 0x600), + 0x000..0x200 => 0x00060000 + self.index * 16, + 0x200..0x400 => 0x000e0000 + (self.index - 0x200) * 16, + 0x400..0x600 => 0x00160000 + (self.index - 0x400) * 16, + 0x600..0x800 => 0x001e0000 + (self.index - 0x600) * 16, _ => unreachable!("can't happen"), }; let mut address_str = format!("{address:08x}"); @@ -231,54 +220,17 @@ impl CharacterDataWindow { } fn show_chardata(&mut self, ui: &mut Ui) { - let start_pos = ui.cursor().min; let resource = CharDataResource::CharacterData { palette: self.palette, }; - let image = Image::new(resource.to_uri()) - .fit_to_original_size(self.scale) - .tint(Color32::RED) - .texture_options(TextureOptions::NEAREST) - .sense(Sense::click()); - let res = ui.add(image); - if res.clicked() { - if let Some(click_pos) = res.interact_pointer_pos() { - let fixed_pos = (click_pos - start_pos) / self.scale; - let x = (fixed_pos.x / 8.0) as usize; - let y = (fixed_pos.y / 8.0) as usize; - self.index = (y * 16) + x; - self.index_str = self.index.to_string(); - } + let grid = CharacterGrid::new(resource.to_uri()) + .with_scale(self.scale) + .with_grid(self.show_grid) + .with_selected(self.index); + if let Some(selected) = grid.show(ui) { + self.index = selected; + self.index_str = selected.to_string(); } - let painter = ui.painter_at(res.rect); - if self.show_grid { - let stroke = ui.style().visuals.widgets.noninteractive.fg_stroke; - for x in (1..16).map(|i| (i as f32) * 8.0 * self.scale) { - let p1 = res.rect.min + (x, 0.0).into(); - let p2 = res.rect.min + (x, 128.0 * 8.0 * self.scale).into(); - painter.line(vec![p1, p2], stroke); - } - for y in (1..128).map(|i| (i as f32) * 8.0 * self.scale) { - let p1 = res.rect.min + (0.0, y).into(); - let p2 = res.rect.min + (16.0 * 8.0 * self.scale, y).into(); - painter.line(vec![p1, p2], stroke); - } - } - // draw box around selected - let x1 = (self.index % 16) as f32 * 8.0 * self.scale; - let x2 = x1 + (8.0 * self.scale); - let y1 = (self.index / 16) as f32 * 8.0 * self.scale; - let y2 = y1 + (8.0 * self.scale); - painter.line( - vec![ - (res.rect.min + (x1, y1).into()), - (res.rect.min + (x2, y1).into()), - (res.rect.min + (x2, y2).into()), - (res.rect.min + (x1, y2).into()), - (res.rect.min + (x1, y1).into()), - ], - ui.style().visuals.widgets.active.fg_stroke, - ); } } @@ -363,7 +315,7 @@ impl CharDataLoader { let palette = self.load_palette(palette); let chardata = self.chardata.borrow(); let mut buffer = vec![0; 8 * 8 * 2048]; - for (i, row) in chardata.range::(0, 2048).iter().enumerate() { + for (i, row) in chardata.range::(0, 8 * 2048).iter().enumerate() { let bytes = [0, 2, 4, 6, 8, 10, 12, 14].map(|off| palette[(*row as usize >> off) & 0x3]); let char_index = i / 8; @@ -409,25 +361,3 @@ impl VramImageLoader for CharDataLoader { vec![] } } - -trait UiExt { - fn section(&mut self, title: impl Into, add_contents: impl FnOnce(&mut Ui)); -} - -impl UiExt for Ui { - fn section(&mut self, title: impl Into, add_contents: impl FnOnce(&mut Ui)) { - let mut frame = Frame::group(self.style()); - frame.outer_margin.top += 10.0; - frame.inner_margin.top += 2.0; - let res = frame.show(self, add_contents); - let text = RichText::new(title).background_color(self.style().visuals.panel_fill); - let old_rect = res.response.rect; - let mut text_rect = old_rect; - text_rect.min.x += 6.0; - let new_rect = self - .allocate_new_ui(UiBuilder::new().max_rect(text_rect), |ui| ui.label(text)) - .response - .rect; - self.allocate_space((old_rect.max - new_rect.max) - (old_rect.min - new_rect.min)); - } -} diff --git a/src/window/vram/utils.rs b/src/window/vram/utils.rs index 1889b3e..d8ddf51 100644 --- a/src/window/vram/utils.rs +++ b/src/window/vram/utils.rs @@ -1,3 +1,5 @@ +use egui::{Color32, Image, ImageSource, Response, Sense, TextureOptions, Ui, Widget}; + pub const GENERIC_PALETTE: [u8; 4] = [0, 64, 128, 255]; pub fn parse_palette(palette: u8, brts: &[u8]) -> [u8; 4] { @@ -14,3 +16,100 @@ pub fn parse_palette(palette: u8, brts: &[u8]) -> [u8; 4] { shades[(palette >> 6) as usize & 0x03], ] } + +pub struct CharacterGrid<'a> { + source: ImageSource<'a>, + scale: f32, + show_grid: bool, + selected: Option, +} + +impl<'a> CharacterGrid<'a> { + pub fn new(source: impl Into>) -> Self { + Self { + source: source.into(), + scale: 1.0, + show_grid: false, + selected: None, + } + } + + pub fn with_scale(self, scale: f32) -> Self { + Self { scale, ..self } + } + + pub fn with_grid(self, show_grid: bool) -> Self { + Self { show_grid, ..self } + } + + pub fn with_selected(self, selected: usize) -> Self { + Self { + selected: Some(selected), + ..self + } + } + + pub fn show(self, ui: &mut Ui) -> Option { + let start_pos = ui.cursor().min; + let cell_size = 8.0 * self.scale; + + let res = self.ui(ui); + + let grid_width_cells = ((res.rect.max.x - res.rect.min.x) / cell_size).round() as usize; + + if res.clicked() { + let click_pos = res.interact_pointer_pos()?; + let grid_pos = (click_pos - start_pos) / cell_size; + Some((grid_pos.y as usize * grid_width_cells) + grid_pos.x as usize) + } else { + None + } + } +} + +impl Widget for CharacterGrid<'_> { + fn ui(self, ui: &mut Ui) -> Response { + let image = Image::new(self.source) + .fit_to_original_size(self.scale) + .tint(Color32::RED) + .texture_options(TextureOptions::NEAREST) + .sense(Sense::click()); + let res = ui.add(image); + + let cell_size = 8.0 * self.scale; + let grid_width_cells = ((res.rect.max.x - res.rect.min.x) / cell_size).round() as usize; + let grid_height_cells = ((res.rect.max.y - res.rect.min.y) / cell_size).round() as usize; + + let painter = ui.painter_at(res.rect); + if self.show_grid { + let stroke = ui.style().visuals.widgets.noninteractive.fg_stroke; + for x in (1..grid_width_cells).map(|i| (i as f32) * cell_size) { + let p1 = (res.rect.min.x + x, res.rect.min.y).into(); + let p2 = (res.rect.min.x + x, res.rect.max.y).into(); + painter.line(vec![p1, p2], stroke); + } + for y in (1..grid_height_cells).map(|i| (i as f32) * cell_size) { + let p1 = (res.rect.min.x, res.rect.min.y + y).into(); + let p2 = (res.rect.max.x, res.rect.min.y + y).into(); + painter.line(vec![p1, p2], stroke); + } + } + if let Some(selected) = self.selected { + let x1 = (selected % grid_width_cells) as f32 * cell_size; + let x2 = x1 + cell_size; + let y1 = (selected / grid_width_cells) as f32 * cell_size; + let y2 = y1 + cell_size; + painter.line( + vec![ + (res.rect.min + (x1, y1).into()), + (res.rect.min + (x2, y1).into()), + (res.rect.min + (x2, y2).into()), + (res.rect.min + (x1, y2).into()), + (res.rect.min + (x1, y1).into()), + ], + ui.style().visuals.widgets.active.fg_stroke, + ); + } + res + } +}