Edit hex inline

This commit is contained in:
2026-11-01 15:57:27 -05:00
parent 8083bc8069
commit 6e08830b43
+181 -68
View File
@@ -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<u32>,
jump_to: String,
editing_value: String,
editing_buffer: Vec<u8>,
state: UiData<State>,
}
@@ -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<T: MemoryValue + LowerHex>(
ui: &mut Ui,
rows: &MemoryRef,
start: usize,
offset: usize,
(start, offset): (usize, usize),
selected: u32,
memory: &MemoryClient,
sim: SimId,
buffer: &mut Vec<u8>,
) -> Option<u32> {
let mut clicked = None;
let mut new_selected = None;
for (i, value) in rows
.range::<T>(offset / size_of::<T>(), 0x10 / size_of::<T>())
.enumerate()
{
let val_str =
format!("{value:08x}").split_off(8 - size_of::<T>() * 2);
let text = RichText::new(val_str).monospace();
let address = (start + offset + i * size_of::<T>()) 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::<T>() * 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::<T>() * 2 {
let bytes = buffer
.chunks(2)
.map(|letters| (letters[0] << 4) | letters[1])
.rev()
.collect::<Vec<u8>>();
let data = T::from_bytes(&bytes);
memory.write(sim, address, &data);
new_selected = Some(
address.wrapping_add(size_of::<T>() as u32)
& 0x07ffffff,
);
}
}
}
clicked
new_selected
}
let selected = match self.state.value_size {
ValueSize::Byte => show_memory::<u8>(
ui,
&rows,
current_start,
offset,
(current_start, offset),
self.state.address,
&self.memory,
self.sim_id,
&mut self.editing_buffer,
),
ValueSize::Halfword => show_memory::<u16>(
ui,
&rows,
current_start,
offset,
(current_start, offset),
self.state.address,
&self.memory,
self.sim_id,
&mut self.editing_buffer,
),
ValueSize::Word => show_memory::<u32>(
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<T: MemoryValue + atoi::FromRadix16Checked>(
address: u32,
value: String,
sim: SimId,
memory: &MemoryClient,
) {
let bytes = value.as_bytes();
let (data, consumed) =
<T as atoi::FromRadix16Checked>::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::<u8>(self.state.address, value, self.sim_id, &self.memory)
}
ValueSize::Halfword => {
set_memory::<u16>(self.state.address, value, self.sim_id, &self.memory)
}
ValueSize::Word => {
set_memory::<u32>(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);