Split WASM and native versions, and move iced support code to native

This commit is contained in:
2026-03-27 00:27:34 -05:00
parent 3010469c8a
commit b433148843
23 changed files with 2330 additions and 1012 deletions

597
src/bin/native/hex_view.rs Normal file
View File

@@ -0,0 +1,597 @@
use std::{
fmt::{self, Display}, ops::Deref
};
use iced::{
advanced::{layout::{Limits, Node}, overlay, renderer::{Quad, Style}, text::Renderer, widget::{tree::{State, Tag}, Operation, Tree}, Clipboard, Layout, Shell, Text, Widget}, alignment::Vertical, keyboard::{key::Named, Key}, mouse::{Button, Cursor, Interaction, ScrollDelta}, widget::{column, lazy, text}, Color, Element, Event, Fill, Font, Length, Padding, Pixels, Point, Rectangle, Size, Task, Vector
};
// use iced_core::{
// Clipboard, Color, Event, Layout, Length, Pixels, Point, Rectangle, Shell, Size, Text, Vector,
// Widget,
// layout::{Limits, Node},
// mouse::{Cursor, Interaction},
// overlay,
// renderer::{Quad, Style},
// widget::{
// Operation, Tree,
// tree::{State, Tag},
// },
// };
use nes_emu::Memory;
use nes_emu::{PPU, Mapped, PPUMMRegisters};
#[derive(Debug, Clone, Copy)]
struct Cpu<'a>(&'a Mapped);
impl Memory for Cpu<'_> {
fn peek(&self, val: usize) -> Option<u8> {
self.0.peek_cpu(val as u16)
}
fn len(&self) -> usize {
0x10000
}
fn edit_ver(&self) -> usize {
self.0.cpu_edit_ver()
}
}
#[derive(Debug, Clone, Copy)]
struct Ppu<'a>(&'a Mapped, &'a PPU);
impl Memory for Ppu<'_> {
fn peek(&self, val: usize) -> Option<u8> {
match self.0.peek_ppu(val as u16) {
Ok(v) => Some(v),
Err(Some((PPUMMRegisters::Palette, off))) => Some(self.1.palette.ram(off as u8)),
Err(None) => None,
}
}
fn len(&self) -> usize {
0x4000
}
fn edit_ver(&self) -> usize {
self.0.ppu_edit_ver()
}
}
#[derive(Debug, Clone, Copy)]
pub struct Oam<'a>(pub &'a PPU);
impl Memory for Oam<'_> {
fn peek(&self, val: usize) -> Option<u8> {
Some(self.0.peek_oam(val as u8))
}
fn len(&self) -> usize {
0x100
}
fn edit_ver(&self) -> usize {
self.0.oam_edit_ver()
}
}
#[derive(Debug, Clone)]
pub enum HexEvent {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HexView {}
struct Val(Option<u8>);
impl fmt::Display for Val {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(val) = self.0 {
write!(f, "{:02X}", val)
} else {
write!(f, "XX")
}
}
}
impl HexView {
pub fn new() -> Self {
Self {}
}
pub fn render_any<'a, M: Memory + Copy + 'a>(&self, mem: M) -> Element<'a, HexEvent> {
struct Row<M: Memory>(usize, M);
impl<'a, M: Memory + 'a> fmt::Display for Row<M> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", Val(self.1.peek(self.0)))?;
for i in 1..16 {
write!(f, " {}", Val(self.1.peek(self.0 + i)))?;
}
Ok(())
}
}
column![
text!("Hex view"),
iced::widget::scrollable(lazy(mem.edit_ver(), move |_| column(
[
text!(" | 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F")
.font(Font::MONOSPACE)
.into()
]
.into_iter()
.chain((0..mem.len()).step_by(16).map(|off| {
text!(" {off:04X} | {}", Row(off, mem))
.font(Font::MONOSPACE)
.into()
}))
)))
.width(Fill),
]
.width(Fill)
.into()
}
pub fn render_cpu<'a>(&self, mem: &'a Mapped) -> Element<'a, HexEvent> {
// self.render_any(Cpu(mem))
Element::new(
hex_editor::<Cpu<'a>, HexEvent, iced::Renderer>(Cpu(mem)).font(Font::MONOSPACE),
)
}
pub fn render_ppu<'a>(&self, mem: &'a Mapped, ppu: &'a PPU) -> Element<'a, HexEvent> {
// self.render_any(Ppu(mem, ppu))
Element::new(
hex_editor::<Ppu<'a>, HexEvent, iced::Renderer>(Ppu(mem, ppu)).font(Font::MONOSPACE),
)
}
pub fn render_oam<'a>(&self, ppu: &'a PPU) -> Element<'a, HexEvent> {
Element::new(
hex_editor::<Oam<'a>, HexEvent, iced::Renderer>(Oam(ppu)).font(Font::MONOSPACE),
)
}
pub fn update(&mut self, ev: HexEvent) -> Task<HexEvent> {
todo!()
}
}
pub struct BufferSlice<'a>(pub &'a [u8]);
impl Deref for BufferSlice<'_> {
type Target = [u8];
fn deref(&self) -> &Self::Target {
self.0
}
}
pub trait Buffer {
fn peek(&self, val: usize) -> Option<u8>;
fn len(&self) -> usize;
}
impl Buffer for BufferSlice<'_> {
fn peek(&self, val: usize) -> Option<u8> {
self.get(val).copied()
}
fn len(&self) -> usize {
self.iter().len()
}
}
impl<M: Memory> Buffer for M {
fn peek(&self, val: usize) -> Option<u8> {
self.peek(val)
}
fn len(&self) -> usize {
self.len()
}
}
pub fn hex_editor<B: Buffer, M, R: Renderer>(raw: B) -> HexEditor<B, M, R> {
HexEditor {
val: raw,
on_edit: None,
font: None,
font_size: None,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct HexEdit {
position: usize,
old_value: u8,
new_value: u8,
}
pub struct HexEditor<B, M, R: Renderer> {
val: B,
font_size: Option<Pixels>,
font: Option<R::Font>,
on_edit: Option<Box<dyn Fn(HexEdit) -> M>>,
}
impl<B, M, R> HexEditor<B, M, R>
where
R: Renderer,
{
pub fn font(mut self, font: R::Font) -> Self {
self.font = Some(font);
self
}
}
impl<B: Buffer, M, R> HexEditor<B, M, R>
where
R: Renderer,
{
fn value_bounds(&self, renderer: &R, layout: Layout<'_>) -> Rectangle {
let size = self.font_size.unwrap_or(renderer.default_size());
layout
.bounds()
.shrink(Padding::new(0.).top(size.0 * 1.5).right(10.))
}
fn scrollbar_bounds(&self, renderer: &R, layout: Layout<'_>) -> Rectangle {
let size = self.font_size.unwrap_or(renderer.default_size());
layout.bounds().shrink(
Padding::new(0.)
.top(size.0 * 1.5)
.left(layout.bounds().width - 10.),
)
}
fn row_height(&self, renderer: &R) -> f32 {
self.font_size.unwrap_or(renderer.default_size()).0 * LINE_HEIGHT
}
fn scroll_max(&self, renderer: &R, layout: Layout<'_>) -> f32 {
let size = self.font_size.unwrap_or(renderer.default_size());
let bounds = layout.bounds().shrink(Padding::new(0.).top(size.0 * 1.5));
let rows = self.val.len().div_ceil(0x10);
(self.row_height(renderer) * rows as f32 - bounds.height).max(0.)
}
fn scroll(
&self,
state: &mut HexEditorState,
renderer: &R,
layout: Layout<'_>,
delta: &ScrollDelta,
) {
// let size = self.font_size.unwrap_or(renderer.default_size());
// let bounds = layout.bounds().shrink(Padding::new(0.).top(size.0 * 1.5));
// let rows = self.val.len().div_ceil(0x10);
let max = self.scroll_max(renderer, layout);
// println!("max: {max}, rows: {rows}");
match delta {
ScrollDelta::Lines { y, .. } => {
state.offset_y += y * self.row_height(renderer);
}
ScrollDelta::Pixels { y, .. } => {
state.offset_y += y;
}
}
if state.offset_y > 0. {
state.offset_y = 0.;
} else if state.offset_y < -max {
state.offset_y = -max;
}
}
}
#[derive(Default)]
struct HexEditorState {
offset_y: f32,
selected: usize,
dragging: bool,
}
const LINE_HEIGHT: f32 = 1.3;
impl<B: Buffer, M, T, R> Widget<M, T, R> for HexEditor<B, M, R>
where
R: Renderer,
{
fn tag(&self) -> Tag {
Tag::of::<HexEditorState>()
}
fn state(&self) -> State {
State::new(HexEditorState::default())
}
fn size(&self) -> Size<Length> {
Size::new(Length::Fill, Length::Fill)
}
fn layout(&mut self, _tree: &mut Tree, _renderer: &R, limits: &Limits) -> Node {
Node::new(limits.max())
}
fn draw(
&self,
tree: &Tree,
renderer: &mut R,
theme: &T,
style: &Style,
layout: Layout<'_>,
cursor: Cursor,
viewport: &Rectangle,
) {
let state: &HexEditorState = tree.state.downcast_ref();
let size = self.font_size.unwrap_or(renderer.default_size());
let font = self.font.unwrap_or(renderer.default_font());
// let fonts = font_system();
// let mut font_sys = fonts.write().unwrap();
// let id = font_sys.raw().db().query(&Query {
// families: &[Family::Monospace],
// ..Query::default()
// });
// let f = font_sys.raw().get_font(id.unwrap(), iced::advanced::graphics::text::cosmic_text::Weight(1)).unwrap();
// let width = f.metrics();
// println!("Width: {width:?}");
// iced::advanced::graphics::text::cosmic_text::Buffer::new(
// font_sys.raw(),
// Metrics::new(size.0, size.0),
// ).layout_runs();
// TODO: this needs to be computed from the font
let col_width = 0.6 * size.0;
let rows = self.val.len().div_ceil(0x10);
let row_label_length = rows.ilog2().div_ceil(4) as usize;
let mut draw = |v: &str, pos: Point| {
renderer.fill_text(
Text {
content: format!("{}", v),
bounds: viewport.size(),
size,
line_height: size.into(),
font,
align_x: iced::advanced::text::Alignment::Left,
align_y: Vertical::Top,
shaping: iced::advanced::text::Shaping::Basic,
wrapping: iced::advanced::text::Wrapping::None,
hint_factor: None,
},
pos,
Color::WHITE,
layout.bounds(),
);
};
draw("0", Point::new(0., 0.));
for i in 0..0x10 {
draw(
"0",
layout.position()
+ Vector::new((3 + row_label_length + 3 * i) as f32 * col_width, 0.),
);
draw(
"0",
layout.position()
+ Vector::new((4 + row_label_length + 3 * i) as f32 * col_width, 0.),
);
}
// renderer.fill_text(
// Text {
// content: format!(
// "{:width$} 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F {}",
// "",
// state.offset_y,
// width = row_label_length
// ),
// bounds: viewport.size(),
// size,
// line_height: size.into(),
// font,
// align_x: iced_core::text::Alignment::Left,
// align_y: iced_core::alignment::Vertical::Top,
// shaping: iced_core::text::Shaping::Basic,
// wrapping: iced_core::text::Wrapping::None,
// hint_factor: None,
// },
// layout.position(),
// Color::WHITE,
// layout.bounds(),
// );
struct HexV(Option<u8>);
impl Display for HexV {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.0 {
Some(v) => write!(f, "{:02X}", v),
None => write!(f, "XX"),
}
}
}
// if rows > 0 {
// rows.ilog2()
// }
let bounds = self.value_bounds(renderer, layout);
let mut pos = bounds.position() + Vector::new(0., state.offset_y);
for row in 0..rows {
if bounds.contains(pos) || bounds.contains(pos + Vector::new(0., size.0 * LINE_HEIGHT))
{
renderer.fill_text(
Text {
content: format!(
"{:0width$X}0: {} {} {} {} {} {} {} {} {} {} {} {} {} {} {} {}",
row,
HexV(self.val.peek(row * 0x10 + 0x0)),
HexV(self.val.peek(row * 0x10 + 0x1)),
HexV(self.val.peek(row * 0x10 + 0x2)),
HexV(self.val.peek(row * 0x10 + 0x3)),
HexV(self.val.peek(row * 0x10 + 0x4)),
HexV(self.val.peek(row * 0x10 + 0x5)),
HexV(self.val.peek(row * 0x10 + 0x6)),
HexV(self.val.peek(row * 0x10 + 0x7)),
HexV(self.val.peek(row * 0x10 + 0x8)),
HexV(self.val.peek(row * 0x10 + 0x9)),
HexV(self.val.peek(row * 0x10 + 0xA)),
HexV(self.val.peek(row * 0x10 + 0xB)),
HexV(self.val.peek(row * 0x10 + 0xC)),
HexV(self.val.peek(row * 0x10 + 0xD)),
HexV(self.val.peek(row * 0x10 + 0xE)),
HexV(self.val.peek(row * 0x10 + 0xF)),
width = row_label_length,
),
bounds: viewport.size(),
size,
line_height: size.into(),
font,
align_x: iced::advanced::text::Alignment::Left,
align_y: Vertical::Top,
shaping: iced::advanced::text::Shaping::Basic,
wrapping: iced::advanced::text::Wrapping::None,
hint_factor: None,
},
pos,
Color::WHITE,
bounds,
);
}
pos += Vector::new(0., size.0 * LINE_HEIGHT);
}
let scrollbar = self.scrollbar_bounds(renderer, layout);
renderer.fill_quad(
Quad {
bounds: scrollbar,
..Quad::default()
},
Color::BLACK,
);
let pos = state.offset_y / self.scroll_max(renderer, layout);
renderer.fill_quad(
Quad {
bounds: Rectangle::new(
Point::new(scrollbar.x, scrollbar.y - pos * (scrollbar.height - 20.)),
Size::new(10., 20.),
),
..Quad::default()
},
Color::WHITE,
);
}
fn operate(
&mut self,
_tree: &mut Tree,
_layout: Layout<'_>,
_renderer: &R,
_operation: &mut dyn Operation,
) {
}
fn update(
&mut self,
tree: &mut Tree,
event: &Event,
layout: Layout<'_>,
cursor: Cursor,
renderer: &R,
_clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, M>,
_viewport: &Rectangle,
) {
// if !matches!(event, Event::Window(_)) {
// println!("Event: {:#?}", event);
// }
match event {
Event::Keyboard(iced::keyboard::Event::KeyPressed {
key: Key::Named(Named::PageUp),
..
}) => {
let state: &mut HexEditorState = tree.state.downcast_mut();
self.scroll(
state,
renderer,
layout,
&ScrollDelta::Pixels {
x: 0.,
y: self.value_bounds(renderer, layout).height,
},
);
shell.request_redraw();
shell.capture_event();
}
Event::Keyboard(iced::keyboard::Event::KeyPressed {
key: Key::Named(Named::PageDown),
..
}) => {
let state: &mut HexEditorState = tree.state.downcast_mut();
self.scroll(
state,
renderer,
layout,
&ScrollDelta::Pixels {
x: 0.,
y: -self.value_bounds(renderer, layout).height,
},
);
shell.request_redraw();
shell.capture_event();
}
Event::Mouse(iced::mouse::Event::WheelScrolled { delta }) => {
let state: &mut HexEditorState = tree.state.downcast_mut();
let bounds = self.value_bounds(renderer, layout);
if cursor.is_over(bounds) {
self.scroll(state, renderer, layout, delta);
shell.request_redraw();
shell.capture_event();
}
}
Event::Mouse(iced::mouse::Event::ButtonPressed(Button::Left)) => {
let state: &mut HexEditorState = tree.state.downcast_mut();
let bounds = self.scrollbar_bounds(renderer, layout);
if let Some(pos) = cursor.position_in(bounds) {
state.offset_y = -(pos.y / bounds.height * self.scroll_max(renderer, layout));
state.dragging = true;
shell.request_redraw();
shell.capture_event();
}
}
Event::Mouse(iced::mouse::Event::ButtonReleased(Button::Left)) => {
let state: &mut HexEditorState = tree.state.downcast_mut();
state.dragging = false;
}
// Event::Mouse(iced::mouse::Event::CursorLeft) => {
// let state: &mut HexEditorState = tree.state.downcast_mut();
// state.dragging = false;
// }
Event::Mouse(iced::mouse::Event::CursorMoved { .. }) => {
let state: &mut HexEditorState = tree.state.downcast_mut();
if state.dragging {
let bounds = self.scrollbar_bounds(renderer, layout);
if let Some(pos) = cursor.position_in(bounds) {
state.offset_y =
-(pos.y / bounds.height * self.scroll_max(renderer, layout));
shell.request_redraw();
shell.capture_event();
}
}
}
_ => (),
}
}
fn mouse_interaction(
&self,
_tree: &Tree,
_layout: Layout<'_>,
_cursor: Cursor,
_viewport: &Rectangle,
_renderer: &R,
) -> Interaction {
Interaction::None
}
fn overlay<'a>(
&'a mut self,
_tree: &'a mut Tree,
_layout: Layout<'a>,
_renderer: &R,
_viewport: &Rectangle,
_translation: Vector,
) -> Option<overlay::Element<'a, M, T, R>> {
None
}
}