| 1 | //! Disassembly window: every instruction of the build with its address, |
| 2 | //! bytes and label, decoded from code memory as it is now. |
| 3 | |
| 4 | use bench51_core::sim::disasm; |
| 5 | use bench51_core::syntax::{Tok, tokens}; |
| 6 | use eframe::egui::{self, Align2, CursorIcon, FontId, Id, Rect, Sense, Ui, pos2, vec2}; |
| 7 | |
| 8 | use crate::state::{Action, State}; |
| 9 | use crate::theme::{ACCENT, palette}; |
| 10 | use crate::widgets; |
| 11 | |
| 12 | struct Row { |
| 13 | line: usize, |
| 14 | addr: u16, |
| 15 | len: usize, |
| 16 | label: String, |
| 17 | /// Source text for DB/DW lines, which aren't instructions. |
| 18 | data: Option<String>, |
| 19 | } |
| 20 | |
| 21 | fn rows(source: &[String], build: &bench51_core::session::Build) -> Vec<Row> { |
| 22 | let mut out = Vec::new(); |
| 23 | for (line, lo) in build.out.lines.iter().enumerate() { |
| 24 | let (Some(addr), false) = (lo.addr, lo.bytes.is_empty()) else { |
| 25 | continue; |
| 26 | }; |
| 27 | let text = source.get(line).map_or("", String::as_str); |
| 28 | let toks = tokens(text); |
| 29 | let mut label = String::new(); |
| 30 | let mut data = None; |
| 31 | for (r, t) in &toks { |
| 32 | match t { |
| 33 | Tok::Label if label.is_empty() => label = text[r.clone()].to_string(), |
| 34 | Tok::Directive => { |
| 35 | let rest = &text[r.start..]; |
| 36 | let rest = rest.split(';').next().unwrap_or(rest).trim_end(); |
| 37 | data = Some(rest.to_string()); |
| 38 | break; |
| 39 | } |
| 40 | Tok::Mnemonic => break, |
| 41 | _ => {} |
| 42 | } |
| 43 | } |
| 44 | out.push(Row { |
| 45 | line, |
| 46 | addr, |
| 47 | len: lo.bytes.len(), |
| 48 | label, |
| 49 | data, |
| 50 | }); |
| 51 | } |
| 52 | out |
| 53 | } |
| 54 | |
| 55 | pub fn show(ui: &mut Ui, st: &mut State) { |
| 56 | let p = palette(ui.ctx()); |
| 57 | let Some(doc) = st.docs.get_mut(st.active) else { |
| 58 | widgets::empty_state(ui, "No program", "Open or create a source file."); |
| 59 | return; |
| 60 | }; |
| 61 | let doc_id = doc.id; |
| 62 | let s = &mut doc.session; |
| 63 | let Some(build) = &s.build else { |
| 64 | widgets::empty_state(ui, "Not built", "Press F7 to assemble."); |
| 65 | return; |
| 66 | }; |
| 67 | let rows = rows(&build.source, build); |
| 68 | let font = FontId::monospace(13.0); |
| 69 | let cw = ui.fonts_mut(|f| f.glyph_width(&font, '0')); |
| 70 | let row_h = ui.fonts_mut(|f| f.row_height(&font)) + 6.0; |
| 71 | let col_addr = 20.0; |
| 72 | let col_bytes = col_addr + 6.0 * cw; |
| 73 | let col_label = col_bytes + 10.0 * cw; |
| 74 | let col_insn = col_label + 10.0 * cw; |
| 75 | |
| 76 | // Header. |
| 77 | let (hdr, _) = ui.allocate_exact_size(vec2(ui.available_width(), 18.0), Sense::hover()); |
| 78 | for (x, name) in [ |
| 79 | (col_addr, "Addr"), |
| 80 | (col_bytes, "Bytes"), |
| 81 | (col_label, "Label"), |
| 82 | (col_insn, "Instruction"), |
| 83 | ] { |
| 84 | ui.painter().text( |
| 85 | pos2(hdr.left() + x, hdr.center().y), |
| 86 | Align2::LEFT_CENTER, |
| 87 | name.to_uppercase(), |
| 88 | FontId::monospace(crate::theme::LABEL), |
| 89 | p.text_disabled, |
| 90 | ); |
| 91 | } |
| 92 | ui.add_space(4.0); |
| 93 | |
| 94 | let pc = s.sim.pc; |
| 95 | let show_pc = !s.running || s.speed <= 2; |
| 96 | let pc_row = rows.iter().position(|r| r.addr == pc); |
| 97 | let bps = s.breakpoints.clone(); |
| 98 | |
| 99 | // Follow the PC whenever it moves while stopped. |
| 100 | let last_id = Id::unique(("disasm_last_pc", doc_id)); |
| 101 | let last = ui.data(|d| d.get_temp::<u16>(last_id)); |
| 102 | let mut area = egui::ScrollArea::vertical() |
| 103 | .id_salt(("disasm", doc_id)) |
| 104 | .auto_shrink([false; 2]); |
| 105 | if show_pc && last != Some(pc) { |
| 106 | ui.data_mut(|d| d.insert_temp(last_id, pc)); |
| 107 | if let Some(i) = pc_row { |
| 108 | let view = ui.available_height(); |
| 109 | area = area.vertical_scroll_offset((i as f32 * row_h - view / 2.0).max(0.0)); |
| 110 | } |
| 111 | } |
| 112 | |
| 113 | let mut toggle = None; |
| 114 | let mut goto = None; |
| 115 | area.show_rows(ui, row_h, rows.len(), |ui, range| { |
| 116 | ui.spacing_mut().item_spacing.y = 0.0; |
| 117 | for i in range { |
| 118 | let r = &rows[i]; |
| 119 | let (rect, resp) = |
| 120 | ui.allocate_exact_size(vec2(ui.available_width(), row_h), Sense::click()); |
| 121 | let painter = ui.painter(); |
| 122 | let cy = rect.center().y; |
| 123 | let is_pc = show_pc && pc_row == Some(i); |
| 124 | if is_pc { |
| 125 | painter.rect_filled(rect, 0.0, p.surface_raised); |
| 126 | painter.rect_filled( |
| 127 | Rect::from_min_size(rect.min, vec2(2.0, rect.height())), |
| 128 | 0.0, |
| 129 | ACCENT, |
| 130 | ); |
| 131 | } |
| 132 | if bps.contains(&r.line) { |
| 133 | painter.circle_filled(pos2(rect.left() + 10.0, cy), 4.0, ACCENT); |
| 134 | } |
| 135 | let text = |x: f32, s: String, c| { |
| 136 | painter.text( |
| 137 | pos2(rect.left() + x, cy), |
| 138 | Align2::LEFT_CENTER, |
| 139 | s, |
| 140 | font.clone(), |
| 141 | c, |
| 142 | ); |
| 143 | }; |
| 144 | text( |
| 145 | col_addr, |
| 146 | format!("{:04X}", r.addr), |
| 147 | if is_pc { |
| 148 | p.text_display |
| 149 | } else { |
| 150 | p.text_secondary |
| 151 | }, |
| 152 | ); |
| 153 | let bytes: Vec<String> = (0..r.len.min(3)) |
| 154 | .map(|k| format!("{:02X}", s.sim.code[r.addr.wrapping_add(k as u16) as usize])) |
| 155 | .collect(); |
| 156 | let more = if r.len > 3 { "…" } else { "" }; |
| 157 | text(col_bytes, bytes.join(" ") + more, p.text_disabled); |
| 158 | text(col_label, r.label.clone(), p.text_display); |
| 159 | match &r.data { |
| 160 | Some(d) => text(col_insn, d.clone(), p.text_secondary), |
| 161 | None => text( |
| 162 | col_insn, |
| 163 | disasm::disassemble(&s.sim.code, r.addr).text, |
| 164 | if is_pc { |
| 165 | p.text_display |
| 166 | } else { |
| 167 | p.text_primary |
| 168 | }, |
| 169 | ), |
| 170 | } |
| 171 | let resp = resp.on_hover_cursor(CursorIcon::Default); |
| 172 | if resp.clicked() |
| 173 | && resp |
| 174 | .interact_pointer_pos() |
| 175 | .is_some_and(|pos| pos.x < rect.left() + col_addr) |
| 176 | { |
| 177 | toggle = Some(r.line); |
| 178 | } else if resp.double_clicked() { |
| 179 | goto = Some(r.line); |
| 180 | } |
| 181 | } |
| 182 | }); |
| 183 | if let Some(line) = toggle { |
| 184 | s.toggle_breakpoint(line); |
| 185 | } |
| 186 | if let Some(line) = goto { |
| 187 | st.actions.push(Action::Goto { |
| 188 | doc: doc_id, |
| 189 | line, |
| 190 | col: 0, |
| 191 | }); |
| 192 | } |
| 193 | } |