| 1 | //! Memory window: IRAM, SFRs, XRAM and code as a hex grid. Double-click a |
| 2 | //! byte to change it. |
| 3 | |
| 4 | use bench51_core::session::{MemSpace, Session}; |
| 5 | use bench51_core::sfr; |
| 6 | use eframe::egui::{ |
| 7 | self, Align, Align2, FontId, Key, Layout, Rect, Sense, TextEdit, Ui, pos2, vec2, |
| 8 | }; |
| 9 | |
| 10 | use crate::state::{MemView, State}; |
| 11 | use crate::theme::{ACCENT, WARNING, palette}; |
| 12 | use crate::widgets; |
| 13 | |
| 14 | const PER_ROW: usize = 16; |
| 15 | |
| 16 | pub fn show(ui: &mut Ui, st: &mut State) { |
| 17 | let p = palette(ui.ctx()); |
| 18 | let mut space = st.mem.space; |
| 19 | ui.horizontal(|ui| { |
| 20 | if widgets::segmented( |
| 21 | ui, |
| 22 | &mut space, |
| 23 | &[ |
| 24 | (MemSpace::Iram, "IRAM"), |
| 25 | (MemSpace::Sfr, "SFR"), |
| 26 | (MemSpace::Xram, "XRAM"), |
| 27 | (MemSpace::Code, "CODE"), |
| 28 | ], |
| 29 | ) { |
| 30 | st.mem.edit = None; |
| 31 | } |
| 32 | if space != MemSpace::Sfr { |
| 33 | ui.with_layout(Layout::right_to_left(Align::Center), |ui| { |
| 34 | let r = widgets::underline_input(ui, &mut st.mem.goto, "addr", 64.0); |
| 35 | ui.label(p.label("Go to")); |
| 36 | if r.lost_focus() && ui.input(|i| i.key_pressed(Key::Enter)) { |
| 37 | let t = st.mem.goto.trim().trim_start_matches("0x"); |
| 38 | if let Ok(a) = u32::from_str_radix(t.trim_end_matches(['h', 'H']), 16) { |
| 39 | st.mem.scroll_to = Some(a); |
| 40 | } |
| 41 | } |
| 42 | }); |
| 43 | } |
| 44 | }); |
| 45 | st.mem.space = space; |
| 46 | ui.add_space(8.0); |
| 47 | let Some(doc) = st.docs.get_mut(st.active) else { |
| 48 | widgets::empty_state(ui, "No program", "Open or create a source file."); |
| 49 | return; |
| 50 | }; |
| 51 | if doc.session.build.is_none() { |
| 52 | widgets::empty_state(ui, "Not built", "Press F7 to assemble."); |
| 53 | return; |
| 54 | } |
| 55 | match space { |
| 56 | MemSpace::Sfr => sfr_table(ui, &mut doc.session, &mut st.mem), |
| 57 | _ => hex_grid(ui, &mut doc.session, &mut st.mem), |
| 58 | } |
| 59 | } |
| 60 | |
| 61 | fn parse_byte(s: &str) -> Option<u8> { |
| 62 | u8::from_str_radix(s.trim().trim_end_matches(['h', 'H']), 16).ok() |
| 63 | } |
| 64 | |
| 65 | /// Inline editor for one byte. Returns true when editing ended. |
| 66 | fn edit_cell(ui: &mut Ui, rect: Rect, text: &mut String) -> (bool, Option<u8>) { |
| 67 | let p = palette(ui.ctx()); |
| 68 | ui.painter() |
| 69 | .rect_filled(rect.expand(2.0), 2.0, p.surface_raised); |
| 70 | let r = ui.put( |
| 71 | rect.expand(2.0), |
| 72 | TextEdit::singleline(text) |
| 73 | .font(FontId::monospace(13.0)) |
| 74 | .char_limit(2) |
| 75 | .frame(egui::Frame::NONE) |
| 76 | .margin(egui::Margin::ZERO) |
| 77 | .text_color(p.text_display), |
| 78 | ); |
| 79 | if !r.has_focus() && !r.lost_focus() { |
| 80 | r.request_focus(); |
| 81 | } |
| 82 | if r.lost_focus() { |
| 83 | let commit = ui.input(|i| i.key_pressed(Key::Enter)); |
| 84 | return (true, commit.then(|| parse_byte(text)).flatten()); |
| 85 | } |
| 86 | (false, None) |
| 87 | } |
| 88 | |
| 89 | fn hex_grid(ui: &mut Ui, s: &mut Session, mem: &mut MemView) { |
| 90 | let p = palette(ui.ctx()); |
| 91 | let space = mem.space; |
| 92 | let font = FontId::monospace(13.0); |
| 93 | let cw = ui.fonts_mut(|f| f.glyph_width(&font, '0')); |
| 94 | let row_h = ui.fonts_mut(|f| f.row_height(&font)) + 4.0; |
| 95 | let total = if space == MemSpace::Iram { |
| 96 | 256 |
| 97 | } else { |
| 98 | 0x10000 |
| 99 | }; |
| 100 | let addr_digits = if total > 256 { 4 } else { 2 }; |
| 101 | let x_bytes = (addr_digits + 2) as f32 * cw; |
| 102 | let byte_x = |i: usize| x_bytes + (i * 3) as f32 * cw + if i >= 8 { cw } else { 0.0 }; |
| 103 | let x_ascii = byte_x(PER_ROW) + cw; |
| 104 | |
| 105 | let pc = s.sim.pc as usize; |
| 106 | let pc_len = bench51_core::isa::info(s.sim.code[pc]).map_or(1, |i| i.size as usize); |
| 107 | let bps = s.bp_addrs(); |
| 108 | let bank = s.sim.reg_addr(0) as usize; |
| 109 | let sp = s.sim.sp() as usize; |
| 110 | |
| 111 | let mut area = egui::ScrollArea::vertical() |
| 112 | .id_salt(("mem", space.name())) |
| 113 | .auto_shrink([false; 2]); |
| 114 | if let Some(a) = mem.scroll_to.take() { |
| 115 | area = area.vertical_scroll_offset((a as usize % total / PER_ROW) as f32 * row_h); |
| 116 | } |
| 117 | let mut poke = None; |
| 118 | area.show_rows(ui, row_h, total / PER_ROW, |ui, range| { |
| 119 | ui.spacing_mut().item_spacing.y = 0.0; |
| 120 | for row in range { |
| 121 | let base = row * PER_ROW; |
| 122 | let (rect, resp) = |
| 123 | ui.allocate_exact_size(vec2(ui.available_width(), row_h), Sense::click()); |
| 124 | if !ui.is_rect_visible(rect) { |
| 125 | continue; |
| 126 | } |
| 127 | let painter = ui.painter(); |
| 128 | let cy = rect.center().y; |
| 129 | let addr = if addr_digits == 4 { |
| 130 | format!("{base:04X}") |
| 131 | } else { |
| 132 | format!("{base:02X}") |
| 133 | }; |
| 134 | painter.text( |
| 135 | pos2(rect.left(), cy), |
| 136 | Align2::LEFT_CENTER, |
| 137 | addr, |
| 138 | font.clone(), |
| 139 | p.text_secondary, |
| 140 | ); |
| 141 | let mut ascii = String::with_capacity(PER_ROW); |
| 142 | for i in 0..PER_ROW { |
| 143 | let a = base + i; |
| 144 | let (v, fg, inverted) = match space { |
| 145 | MemSpace::Iram => { |
| 146 | let v = s.sim.iram[a]; |
| 147 | let fg = if v != s.prev_iram[a] { |
| 148 | WARNING |
| 149 | } else if (bank..bank + 8).contains(&a) { |
| 150 | p.text_display |
| 151 | } else if v == 0 { |
| 152 | p.text_disabled |
| 153 | } else { |
| 154 | p.text_primary |
| 155 | }; |
| 156 | (v, fg, a == sp) |
| 157 | } |
| 158 | MemSpace::Xram => { |
| 159 | let v = s.sim.xram[a]; |
| 160 | ( |
| 161 | v, |
| 162 | if v == 0 { |
| 163 | p.text_disabled |
| 164 | } else { |
| 165 | p.text_primary |
| 166 | }, |
| 167 | false, |
| 168 | ) |
| 169 | } |
| 170 | _ => { |
| 171 | let v = s.sim.code[a]; |
| 172 | let fg = if bps.contains(&(a as u16)) { |
| 173 | ACCENT |
| 174 | } else if v == 0 { |
| 175 | p.text_disabled |
| 176 | } else { |
| 177 | p.text_primary |
| 178 | }; |
| 179 | (v, fg, (pc..pc + pc_len).contains(&a)) |
| 180 | } |
| 181 | }; |
| 182 | let cell = Rect::from_min_size( |
| 183 | pos2(rect.left() + byte_x(i) - 2.0, rect.top() + 1.0), |
| 184 | vec2(2.0 * cw + 4.0, row_h - 2.0), |
| 185 | ); |
| 186 | let editing = mem.edit.as_ref().is_some_and(|(ea, _)| *ea as usize == a); |
| 187 | if editing { |
| 188 | let (_, text) = mem.edit.as_mut().unwrap(); |
| 189 | let (done, value) = edit_cell(ui, cell.shrink2(vec2(2.0, 1.0)), text); |
| 190 | if done { |
| 191 | if let Some(v) = value { |
| 192 | poke = Some((a as u32, v)); |
| 193 | } |
| 194 | mem.edit = None; |
| 195 | } |
| 196 | } else { |
| 197 | let painter = ui.painter(); |
| 198 | let fg = if inverted { |
| 199 | painter.rect_filled(cell, 2.0, p.text_display); |
| 200 | p.black |
| 201 | } else { |
| 202 | fg |
| 203 | }; |
| 204 | painter.text( |
| 205 | pos2(rect.left() + byte_x(i), cy), |
| 206 | Align2::LEFT_CENTER, |
| 207 | format!("{v:02X}"), |
| 208 | font.clone(), |
| 209 | fg, |
| 210 | ); |
| 211 | } |
| 212 | ascii.push(if v.is_ascii_graphic() || v == b' ' { |
| 213 | v as char |
| 214 | } else { |
| 215 | '·' |
| 216 | }); |
| 217 | } |
| 218 | ui.painter().text( |
| 219 | pos2(rect.left() + x_ascii, cy), |
| 220 | Align2::LEFT_CENTER, |
| 221 | ascii, |
| 222 | font.clone(), |
| 223 | p.text_disabled, |
| 224 | ); |
| 225 | if resp.double_clicked() |
| 226 | && let Some(pos) = resp.interact_pointer_pos() |
| 227 | { |
| 228 | let x = pos.x - rect.left(); |
| 229 | if let Some(i) = |
| 230 | (0..PER_ROW).find(|&i| x >= byte_x(i) - 4.0 && x < byte_x(i) + 2.5 * cw) |
| 231 | { |
| 232 | let a = base + i; |
| 233 | let v = match space { |
| 234 | MemSpace::Iram => s.sim.iram[a], |
| 235 | MemSpace::Xram => s.sim.xram[a], |
| 236 | _ => s.sim.code[a], |
| 237 | }; |
| 238 | mem.edit = Some((a as u32, format!("{v:02X}"))); |
| 239 | } |
| 240 | } |
| 241 | } |
| 242 | }); |
| 243 | if let Some((a, v)) = poke { |
| 244 | s.poke(space, a, v); |
| 245 | } |
| 246 | } |
| 247 | |
| 248 | fn sfr_table(ui: &mut Ui, s: &mut Session, mem: &mut MemView) { |
| 249 | let p = palette(ui.ctx()); |
| 250 | let cols = if ui.available_width() > 620.0 { 3 } else { 2 }; |
| 251 | let per_col = sfr::SFRS.len().div_ceil(cols); |
| 252 | let mut poke = None; |
| 253 | egui::ScrollArea::vertical() |
| 254 | .auto_shrink([false; 2]) |
| 255 | .show(ui, |ui| { |
| 256 | egui::Grid::new("sfr_table") |
| 257 | .num_columns(cols * 4) |
| 258 | .spacing([14.0, 5.0]) |
| 259 | .show(ui, |ui| { |
| 260 | for r in 0..per_col { |
| 261 | for c in 0..cols { |
| 262 | let Some(&(name, addr)) = sfr::SFRS.get(c * per_col + r) else { |
| 263 | continue; |
| 264 | }; |
| 265 | let v = s.sim.read_direct(addr, true); |
| 266 | let changed = super::sfr_changed(s, addr); |
| 267 | ui.label(p.label(name).color(p.text_secondary)); |
| 268 | ui.label(p.data(format!("{addr:02X}")).color(p.text_disabled)); |
| 269 | let editing = mem.edit.as_ref().is_some_and(|(a, _)| *a == addr as u32); |
| 270 | if editing { |
| 271 | let (rect, _) = |
| 272 | ui.allocate_exact_size(vec2(24.0, 18.0), Sense::hover()); |
| 273 | let (_, text) = mem.edit.as_mut().unwrap(); |
| 274 | let (done, value) = edit_cell(ui, rect, text); |
| 275 | if done { |
| 276 | if let Some(v) = value { |
| 277 | poke = Some((addr as u32, v)); |
| 278 | } |
| 279 | mem.edit = None; |
| 280 | } |
| 281 | } else { |
| 282 | let color = if changed { WARNING } else { p.text_primary }; |
| 283 | let resp = ui.add( |
| 284 | egui::Label::new(p.data(format!("{v:02X}")).color(color)) |
| 285 | .sense(Sense::click()), |
| 286 | ); |
| 287 | if resp.double_clicked() { |
| 288 | mem.edit = Some((addr as u32, format!("{v:02X}"))); |
| 289 | } |
| 290 | } |
| 291 | let bits = p.data(format!("{v:08b}")).size(12.0).color(if changed { |
| 292 | WARNING |
| 293 | } else { |
| 294 | p.text_disabled |
| 295 | }); |
| 296 | ui.label(bits); |
| 297 | } |
| 298 | ui.end_row(); |
| 299 | } |
| 300 | }); |
| 301 | }); |
| 302 | if let Some((a, v)) = poke { |
| 303 | s.poke(MemSpace::Sfr, a, v); |
| 304 | } |
| 305 | } |