From 6e08830b4316f828a6d60ebee040f0112ba5c673 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Sun, 1 Nov 2026 15:57:27 -0500 Subject: [PATCH] Edit hex inline --- src/window/memory.rs | 249 +++++++++++++++++++++++++++++++------------ 1 file changed, 181 insertions(+), 68 deletions(-) diff --git a/src/window/memory.rs b/src/window/memory.rs index 4a29a0e..b61e2e9 100644 --- a/src/window/memory.rs +++ b/src/window/memory.rs @@ -1,10 +1,11 @@ use std::{fmt::LowerHex, sync::Arc}; use egui::{ - Align, CentralPanel, Label, Layout, Response, RichText, Separator, TextEdit, Ui, - ViewportBuilder, Widget, + Align, CentralPanel, FontSelection, Label, Layout, Response, RichText, Separator, TextEdit, Ui, + ViewportBuilder, Widget, text::LayoutJob, }; use egui_extras::{Column, Size, StripBuilder, TableBuilder}; +use itertools::Itertools; use serde::{Deserialize, Serialize}; use crate::{ @@ -19,6 +20,15 @@ enum ValueSize { Halfword, Word, } +impl ValueSize { + fn size(self) -> usize { + match self { + Self::Byte => 1, + Self::Halfword => 2, + Self::Word => 4, + } + } +} struct HexEditResponse { submitted: bool, @@ -79,7 +89,7 @@ pub struct MemoryWindow { view: MemoryView, scroll_to: Option, jump_to: String, - editing_value: String, + editing_buffer: Vec, state: UiData, } @@ -92,15 +102,30 @@ impl MemoryWindow { view: memory.watch(sim_id, 0x0, 0), scroll_to: None, jump_to: String::new(), - editing_value: String::new(), + editing_buffer: vec![], state, } } fn show_top_bar(&mut self, ui: &mut Ui) { - ui.radio_value(&mut self.state.value_size, ValueSize::Byte, "8-bit"); - ui.radio_value(&mut self.state.value_size, ValueSize::Halfword, "16-bit"); - ui.radio_value(&mut self.state.value_size, ValueSize::Word, "32-bit"); + if ui + .radio_value(&mut self.state.value_size, ValueSize::Byte, "8-bit") + .clicked() + { + self.editing_buffer.clear(); + } + if ui + .radio_value(&mut self.state.value_size, ValueSize::Halfword, "16-bit") + .clicked() + { + self.editing_buffer.clear(); + } + if ui + .radio_value(&mut self.state.value_size, ValueSize::Word, "32-bit") + .clicked() + { + self.editing_buffer.clear(); + } self.state.address &= match self.state.value_size { ValueSize::Byte => 0x07ffffff, ValueSize::Halfword => 0x07fffffe, @@ -114,6 +139,7 @@ impl MemoryWindow { if let Ok(address) = u64::from_str_radix(&address_str, 16) { let address = (address & 0x07ffffff) as u32; self.state.address = address; + self.editing_buffer.clear(); self.scroll_to = Some(address); } } @@ -122,6 +148,34 @@ impl MemoryWindow { fn show_memory(&mut self, ui: &mut Ui) { let row_height_sans_spacing = ui.text_style_height(&egui::TextStyle::Monospace); let max_height = ui.max_rect().height(); + + let something_has_focus = ui.memory(|m| m.focused().is_some()); + let mut selected_changed = false; + ui.input(|i| { + if !i.focused || something_has_focus { + return; + } + let arrow_keys: [(egui::Key, i32); 4] = [ + (egui::Key::ArrowUp, -0x10), + (egui::Key::ArrowDown, 0x10), + (egui::Key::ArrowLeft, -(self.state.value_size.size() as i32)), + (egui::Key::ArrowRight, self.state.value_size.size() as i32), + ]; + for (key, delta) in arrow_keys { + if !selected_changed && i.key_pressed(key) { + self.state.address = self.state.address.wrapping_add_signed(delta) & 0x07ffffff; + self.editing_buffer.clear(); + selected_changed = true; + } + } + if !self.editing_buffer.is_empty() && i.key_pressed(egui::Key::Enter) { + let full_size = self.state.value_size.size() * 2; + while self.editing_buffer.len() < full_size { + self.editing_buffer.insert(0, 0); + } + } + }); + let mut table = TableBuilder::new(ui) .column(Column::auto()) .column(Column::initial(0.0)) @@ -146,6 +200,7 @@ impl MemoryWindow { let rows = self.view.borrow(); let mut new_start = None; let mut new_length = 0; + body.rows(row_height_sans_spacing, total_rows, |mut row| { let address = row.index() * 0x10; let offset = address @@ -160,6 +215,8 @@ impl MemoryWindow { let label = Label::new(RichText::new(format!("{address:08x}")).monospace()); if ui.add(label).clicked() { self.state.address = address as u32; + self.editing_buffer.clear(); + selected_changed = true; }; }); row.col(|ui| { @@ -170,60 +227,145 @@ impl MemoryWindow { fn show_memory( ui: &mut Ui, rows: &MemoryRef, - start: usize, - offset: usize, + (start, offset): (usize, usize), selected: u32, + memory: &MemoryClient, + sim: SimId, + buffer: &mut Vec, ) -> Option { - let mut clicked = None; + let mut new_selected = None; for (i, value) in rows .range::(offset / size_of::(), 0x10 / size_of::()) .enumerate() { - let val_str = - format!("{value:08x}").split_off(8 - size_of::() * 2); - let text = RichText::new(val_str).monospace(); let address = (start + offset + i * size_of::()) as u32; - let label = if address == selected { - Label::new( + let mut job = LayoutJob::default(); + + let rich_text = |value: &str| { + let text = RichText::new(value).monospace(); + if address == selected { text.background_color(ui.visuals().selection.bg_fill) - .color(ui.visuals().selection.stroke.color), - ) - } else { - Label::new(text) + .color(ui.visuals().selection.stroke.color) + } else { + text + } }; - if ui.add(label).clicked() { - clicked = Some(address); + + let edit_len = if address == selected { buffer.len() } else { 0 }; + + if edit_len > 0 { + let buffer_str = + buffer.iter().map(|d| format!("{d:x}")).join(""); + let text = rich_text(&buffer_str).underline(); + text.append_to( + &mut job, + ui.style(), + FontSelection::default(), + Align::Center, + ); + } + + let val_str = format!("{value:08x}") + .split_off(8 - size_of::() * 2 + edit_len); + let text = rich_text(&val_str); + text.append_to( + &mut job, + ui.style(), + FontSelection::default(), + Align::Center, + ); + + let response = ui.label(job); + if response.clicked() { + new_selected = Some(address); + } + if ui.memory(|m| m.focused().is_none()) + && ui.input(|i| i.focused) + && address == selected + { + const KEYS: [egui::Key; 16] = [ + egui::Key::Num0, + egui::Key::Num1, + egui::Key::Num2, + egui::Key::Num3, + egui::Key::Num4, + egui::Key::Num5, + egui::Key::Num6, + egui::Key::Num7, + egui::Key::Num8, + egui::Key::Num9, + egui::Key::A, + egui::Key::B, + egui::Key::C, + egui::Key::D, + egui::Key::E, + egui::Key::F, + ]; + let pressed = ui.input(|i| { + for (index, key) in KEYS.into_iter().enumerate() { + if i.key_pressed(key) { + return Some(index as u8); + } + } + None + }); + if let Some(value) = pressed { + buffer.push(value); + } + if buffer.len() == size_of::() * 2 { + let bytes = buffer + .chunks(2) + .map(|letters| (letters[0] << 4) | letters[1]) + .rev() + .collect::>(); + let data = T::from_bytes(&bytes); + memory.write(sim, address, &data); + new_selected = Some( + address.wrapping_add(size_of::() as u32) + & 0x07ffffff, + ); + } } } - clicked + new_selected } let selected = match self.state.value_size { ValueSize::Byte => show_memory::( ui, &rows, - current_start, - offset, + (current_start, offset), self.state.address, + &self.memory, + self.sim_id, + &mut self.editing_buffer, ), ValueSize::Halfword => show_memory::( ui, &rows, - current_start, - offset, + (current_start, offset), self.state.address, + &self.memory, + self.sim_id, + &mut self.editing_buffer, ), ValueSize::Word => show_memory::( ui, &rows, - current_start, - offset, + (current_start, offset), self.state.address, + &self.memory, + self.sim_id, + &mut self.editing_buffer, ), }; if let Some(selected) = selected { self.state.address = selected; + selected_changed = true; } }; + if selected_changed { + self.editing_buffer.clear(); + } }); row.col(|ui| { ui.separator(); @@ -252,40 +394,17 @@ impl MemoryWindow { .retarget(&mut self.view, self.sim_id, start as u32, new_length); } }); - } - fn show_bottom_bar(&mut self, ui: &mut Ui) { - ui.label(format!("Current address: {:08x}", self.state.address)); - if HexEdit::new(&mut self.editing_value, "Set value") - .show(ui) - .submitted - { - let value = std::mem::take(&mut self.editing_value); - fn set_memory( - address: u32, - value: String, - sim: SimId, - memory: &MemoryClient, - ) { - let bytes = value.as_bytes(); - let (data, consumed) = - ::from_radix_16_checked(bytes); - if let Some(data) = data - && consumed == bytes.len() - { - memory.write(sim, address, &data); - } - } - match self.state.value_size { - ValueSize::Byte => { - set_memory::(self.state.address, value, self.sim_id, &self.memory) - } - ValueSize::Halfword => { - set_memory::(self.state.address, value, self.sim_id, &self.memory) - } - ValueSize::Word => { - set_memory::(self.state.address, value, self.sim_id, &self.memory) - } + if selected_changed && self.scroll_to.is_none() { + let range = self.view.range(); + if self.state.address < range.start { + self.scroll_to = Some(self.state.address); + } else if let Some(delta) = self + .state + .address + .checked_sub(range.start + range.length as u32 - 0x20) + { + self.scroll_to = Some(range.start + delta + 0x40); } } } @@ -305,7 +424,7 @@ impl AppWindow for MemoryWindow { fn show(&mut self, ui: &mut Ui) { let row_height = ui.text_style_height(&egui::TextStyle::Monospace) + ui.spacing().item_spacing.y; - let table_height = ui.available_height() - (row_height * 4.5); + let table_height = ui.available_height() - (row_height * 2.5); if self.state.load(ui) { self.scroll_to = Some(self.state.address); } @@ -314,8 +433,6 @@ impl AppWindow for MemoryWindow { .size(Size::exact(row_height)) .size(Size::initial(0.0)) .size(Size::exact(table_height)) - .size(Size::initial(0.0)) - .size(Size::exact(row_height)) .cell_layout(Layout::left_to_right(Align::Center)) .vertical(|mut strip| { strip.cell(|ui| self.show_top_bar(ui)); @@ -323,10 +440,6 @@ impl AppWindow for MemoryWindow { Separator::default().horizontal().ui(ui); }); strip.cell(|ui| self.show_memory(ui)); - strip.cell(|ui| { - Separator::default().horizontal().ui(ui); - }); - strip.cell(|ui| self.show_bottom_bar(ui)); }); }); self.state.save(ui);