1use crate::genesis::widgets::{BreakpointWindowResponse, BreakpointsWidget, U24};
2use crate::{AddressSet, non_selectable_label};
3use egui::style::ScrollStyle;
4use egui::{Align, CentralPanel, Grid, Layout, Panel, RichText, TextEdit, Ui, Window};
5use egui_extras::{Column, TableBuilder};
6use genesis_components::cartridge::Cartridge;
7use genesis_core::api::debug::{
8    DebugPendingWrite, GenesisDebugState, M68000BreakStatus, M68000Breakpoint, M68000Breakpoints,
9};
10use jgenesis_common::num::{GetBit, U16Ext};
11use m68000_emu::disassemble::{DisassembledInstruction, MemoryAccess, MemoryReadType};
12use m68000_emu::{M68000, OpSize};
13use segacd_core::WordRam;
14use segacd_core::api::debug::SegaCdDebugState;
15
16pub trait M68kInterruptBreakpoints {
17    fn render(&mut self, ui: &mut Ui) -> BreakpointWindowResponse;
18
19    fn levels(&self) -> Vec<u8>;
20}
21
22pub struct DummyM68kInterruptBreakpoints;
23
24impl M68kInterruptBreakpoints for DummyM68kInterruptBreakpoints {
25    fn render(&mut self, _ui: &mut Ui) -> BreakpointWindowResponse {
26        BreakpointWindowResponse::NotChanged
27    }
28
29    fn levels(&self) -> Vec<u8> {
30        vec![]
31    }
32}
33
34#[derive(Debug, Clone, Default)]
35pub struct Main68kInterruptBreakpoints {
36    vertical: bool,
37    horizontal: bool,
38}
39
40impl M68kInterruptBreakpoints for Main68kInterruptBreakpoints {
41    fn render(&mut self, ui: &mut Ui) -> BreakpointWindowResponse {
42        ui.separator();
43
44        let mut changed = false;
45
46        changed |= ui.checkbox(&mut self.vertical, "Break on vertical interrupt (INT6)").changed();
47        changed |=
48            ui.checkbox(&mut self.horizontal, "Break on horizontal interrupt (INT4)").changed();
49
50        BreakpointWindowResponse::from_changed(changed)
51    }
52
53    fn levels(&self) -> Vec<u8> {
54        let mut levels = vec![];
55
56        if self.vertical {
57            levels.push(6);
58        }
59
60        if self.horizontal {
61            levels.push(4);
62        }
63
64        levels
65    }
66}
67
68#[derive(Debug, Clone, Default)]
69pub struct Sub68kInterruptBreakpoints {
70    cdc: bool,
71    cdd: bool,
72    timer: bool,
73    software: bool,
74    graphics: bool,
75}
76
77impl M68kInterruptBreakpoints for Sub68kInterruptBreakpoints {
78    fn render(&mut self, ui: &mut Ui) -> BreakpointWindowResponse {
79        ui.separator();
80
81        let mut changed = false;
82
83        for (field, label) in [
84            (&mut self.cdc, "Break on CDC interrupt (INT5)"),
85            (&mut self.cdd, "Break on CDD interrupt (INT4)"),
86            (&mut self.timer, "Break on timer interrupt (INT3)"),
87            (&mut self.software, "Break on software interrupt (INT2)"),
88            (&mut self.graphics, "Break on graphics interrupt (INT1)"),
89        ] {
90            changed |= ui.checkbox(field, label).changed();
91        }
92
93        BreakpointWindowResponse::from_changed(changed)
94    }
95
96    fn levels(&self) -> Vec<u8> {
97        [(self.cdc, 5), (self.cdd, 4), (self.timer, 3), (self.software, 2), (self.graphics, 1)]
98            .into_iter()
99            .filter_map(|(field, level)| field.then_some(level))
100            .collect()
101    }
102}
103
104#[derive(Debug, Clone, Copy, PartialEq, Eq)]
105pub enum M68kBreakCommand {
106    Pause,
107    Resume,
108    Step,
109}
110
111pub struct M68kDebugWindowState {
112    pub disassembly_window_title: String,
113    pub breakpoints_window_title: String,
114    pub disassembly_open: bool,
115    pub breakpoints_open: bool,
116    pub disassemble_start: u32,
117    pub disassemble_end_addr: Option<u32>,
118    pub disassemble_jump_addr: String,
119    pub disassemble_reset_table: bool,
120    pub disassemble_table_offset: f32,
121    pub disassemble_table_height: f32,
122    pub disassemble_selected_pcs: AddressSet<u32>,
123    pub break_status_last_frame: M68000BreakStatus,
124    pub breakpoints: BreakpointsWidget<U24>,
125    pub interrupt_breakpoints: Box<dyn M68kInterruptBreakpoints>,
126}
127
128impl M68kDebugWindowState {
129    pub fn new_default_titles() -> Self {
130        Self::new_with_titles("68000 Disassembly", "68000 Breakpoints")
131    }
132
133    pub fn new_with_titles(
134        disassembly_window_title: impl Into<String>,
135        breakpoints_window_title: impl Into<String>,
136    ) -> Self {
137        let disassembly_window_title = disassembly_window_title.into();
138        let breakpoints_window_title = breakpoints_window_title.into();
139        let breakpoints = BreakpointsWidget::new(&breakpoints_window_title);
140
141        Self {
142            disassembly_window_title,
143            breakpoints_window_title,
144            disassembly_open: false,
145            breakpoints_open: false,
146            disassemble_start: 0,
147            disassemble_end_addr: None,
148            disassemble_jump_addr: String::new(),
149            disassemble_reset_table: false,
150            disassemble_table_offset: 0.0,
151            disassemble_table_height: 1.0,
152            disassemble_selected_pcs: AddressSet::new(),
153            break_status_last_frame: M68000BreakStatus::default(),
154            breakpoints,
155            interrupt_breakpoints: Box::new(DummyM68kInterruptBreakpoints),
156        }
157    }
158
159    pub fn with_interrupt_breakpoints(
160        mut self,
161        interrupt_breakpoints: Box<dyn M68kInterruptBreakpoints>,
162    ) -> Self {
163        self.interrupt_breakpoints = interrupt_breakpoints;
164        self
165    }
166
167    pub fn open_disassembly_window(&mut self, ctx: &egui::Context) {
168        self.disassembly_open = true;
169        crate::move_to_top(ctx, &self.disassembly_window_title);
170    }
171
172    pub fn open_breakpoints_window(&mut self, ctx: &egui::Context) {
173        self.breakpoints_open = true;
174        crate::move_to_top(ctx, &self.breakpoints_window_title);
175    }
176
177    fn maybe_move_disassembly_table(&mut self, address: u32) {
178        if self
179            .disassemble_end_addr
180            .is_none_or(|end_addr| !(self.disassemble_start..end_addr).contains(&address))
181        {
182            self.move_disassembly_table(address);
183        }
184    }
185
186    fn move_disassembly_table(&mut self, address: u32) {
187        self.disassemble_start = address;
188        self.disassemble_reset_table = true;
189    }
190}
191
192pub trait M68kDebugMemoryMap {
193    fn peek(&self, address: u32) -> Option<u16>;
194
195    fn extra_info(&self) -> Vec<(&'static str, String)> {
196        vec![]
197    }
198}
199
200#[derive(Debug, Clone, Copy)]
201pub struct Genesis68kMemoryMap<'a> {
202    pub cartridge: Option<&'a Cartridge>,
203    pub working_ram: &'a [u16],
204    pub audio_ram: &'a [u8],
205    pub main_pending_writes: &'a [DebugPendingWrite],
206    pub sega_cd: Option<SegaCd68kMemoryMap<'a>>,
207    pub sega_32x: Option<Sega32X68kMemoryMap>,
208}
209
210#[derive(Debug, Clone, Copy)]
211pub struct SegaCd68kMemoryMap<'a> {
212    pub bios_rom: &'a [u16],
213    pub prg_ram: &'a [u16],
214    pub word_ram: &'a WordRam,
215    pub prg_ram_base_addr: usize,
216}
217
218impl SegaCd68kMemoryMap<'_> {
219    fn peek(&self, address: u32) -> u16 {
220        if address & 0x200000 == 0 {
221            if address & 0x20000 == 0 {
222                self.bios_rom[((address & 0x1FFFF) >> 1) as usize]
223            } else {
224                let prg_ram_addr = self.prg_ram_base_addr | (address & 0x1FFFF) as usize;
225                self.prg_ram[prg_ram_addr >> 1]
226            }
227        } else {
228            let msb = self.word_ram.main_cpu_read_ram(address);
229            let lsb = self.word_ram.main_cpu_read_ram(address + 1);
230            u16::from_be_bytes([msb, lsb])
231        }
232    }
233}
234
235#[derive(Debug, Clone, Copy)]
236pub struct Sega32X68kMemoryMap {
237    pub banked_rom_base_addr: u32,
238}
239
240impl<'a> Genesis68kMemoryMap<'a> {
241    pub fn new(debug_state: &'a GenesisDebugState) -> Self {
242        Self {
243            cartridge: debug_state.cartridge.as_ref(),
244            working_ram: debug_state.working_ram.as_ref(),
245            audio_ram: debug_state.audio_ram.as_ref(),
246            main_pending_writes: debug_state.pending_writes.as_slice(),
247            sega_cd: debug_state.sega_cd.as_ref().map(|sega_cd| SegaCd68kMemoryMap {
248                bios_rom: sega_cd.bios_rom(),
249                prg_ram: sega_cd.prg_ram(),
250                word_ram: sega_cd.word_ram(),
251                prg_ram_base_addr: usize::from(sega_cd.main_cpu_prg_ram_bank()) << 17,
252            }),
253            sega_32x: debug_state.sega_32x.as_ref().map(|sega_32x| Sega32X68kMemoryMap {
254                banked_rom_base_addr: u32::from(sega_32x.m68k_rom_bank()) << 20,
255            }),
256        }
257    }
258}
259
260impl M68kDebugMemoryMap for Genesis68kMemoryMap<'_> {
261    fn peek(&self, address: u32) -> Option<u16> {
262        let address = address & 0xFFFFFF & !1;
263
264        match address {
265            0x000000..=0x3FFFFF => {
266                if let Some(cartridge) = self.cartridge {
267                    Some(cartridge.peek_word(address))
268                } else {
269                    self.sega_cd.as_ref().map(|sega_cd| sega_cd.peek(address))
270                }
271            }
272            0x400000..=0x7FFFFF => match (self.cartridge, self.sega_cd) {
273                (Some(_cartridge), Some(sega_cd)) => Some(sega_cd.peek(address)),
274                (Some(cartridge), None) => Some(cartridge.peek_word(address)),
275                (None, _) => None,
276            },
277            0x880000..=0x8FFFFF
278                if self.sega_32x.is_some()
279                    && let Some(cartridge) = self.cartridge =>
280            {
281                Some(cartridge.peek_word(address & 0x7FFFF))
282            }
283            0x900000..=0x9FFFFF
284                if let Some(sega_32x) = self.sega_32x
285                    && let Some(cartridge) = self.cartridge =>
286            {
287                let cartridge_addr = sega_32x.banked_rom_base_addr | (address & 0xFFFFF);
288                Some(cartridge.peek_word(cartridge_addr))
289            }
290            0xA00000..=0xA03FFF | 0xA08000..=0xA0BFFF => {
291                let byte = self.audio_ram[(address & 0x1FFF) as usize];
292                Some(u16::from_le_bytes([byte, byte]))
293            }
294            0xE00000..=0xFFFFFF => Some(self.working_ram[((address & 0xFFFF) >> 1) as usize]),
295            _ => None,
296        }
297    }
298
299    fn extra_info(&self) -> Vec<(&'static str, String)> {
300        let mut extra_info = Vec::new();
301
302        if !self.main_pending_writes.is_empty() {
303            extra_info.push(("Buffered writes", String::new()));
304
305            for &write in self.main_pending_writes {
306                match write {
307                    DebugPendingWrite::Word { address, value } => {
308                        extra_info.push(("", format!("{address:06X} {value:04X}")));
309                    }
310                    DebugPendingWrite::Byte { address, value } => {
311                        extra_info.push(("", format!("{address:06X} {value:02X}")));
312                    }
313                }
314            }
315        }
316
317        if let Some(sega_cd) = self.sega_cd {
318            extra_info.push((
319                "PRG RAM Bank",
320                format!(
321                    "{} ({:05X}-{:05X})",
322                    sega_cd.prg_ram_base_addr >> 17,
323                    sega_cd.prg_ram_base_addr,
324                    sega_cd.prg_ram_base_addr | 0x1FFFF
325                ),
326            ));
327        }
328
329        if let Some(sega_32x) = self.sega_32x {
330            let banked_rom_range = format!(
331                "{} ({:06X}-{:06X})",
332                sega_32x.banked_rom_base_addr >> 20,
333                sega_32x.banked_rom_base_addr,
334                sega_32x.banked_rom_base_addr | 0xFFFFF
335            );
336            extra_info.push(("32X ROM Bank", banked_rom_range));
337        }
338
339        extra_info
340    }
341}
342
343pub struct SegaCdSubMemoryMap<'a> {
344    prg_ram: &'a [u16],
345    word_ram: &'a WordRam,
346}
347
348impl<'a> SegaCdSubMemoryMap<'a> {
349    pub fn new(debug_state: &'a SegaCdDebugState) -> Self {
350        Self { prg_ram: debug_state.prg_ram(), word_ram: debug_state.word_ram() }
351    }
352}
353
354impl M68kDebugMemoryMap for SegaCdSubMemoryMap<'_> {
355    fn peek(&self, address: u32) -> Option<u16> {
356        let address = address & 0x0FFFFF;
357
358        let word = match address {
359            0x00000..=0x7FFFF => self.prg_ram[(address >> 1) as usize],
360            0x80000..=0xBFFFF => {
361                let msb = self.word_ram.sub_cpu_peek_ram(address);
362                let lsb = self.word_ram.sub_cpu_peek_ram(address + 1);
363                u16::from_be_bytes([msb, lsb])
364            }
365            _ => return None,
366        };
367
368        Some(word)
369    }
370}
371
372pub fn render_disassembly_window(
373    ctx: &egui::Context,
374    m68k: &M68000,
375    memory_map: &dyn M68kDebugMemoryMap,
376    state: &mut M68kDebugWindowState,
377    break_status: M68000BreakStatus,
378    handle_command: Option<impl FnMut(M68kBreakCommand)>,
379) {
380    if break_status.breaking && break_status != state.break_status_last_frame {
381        let mut move_to_pc = break_status.pc;
382        for previous_pc in break_status.previous_pcs {
383            if previous_pc > move_to_pc || previous_pc < move_to_pc.saturating_sub(16) {
384                break;
385            }
386            move_to_pc = previous_pc;
387        }
388
389        state.maybe_move_disassembly_table(move_to_pc);
390        state.disassembly_open = true;
391        crate::move_to_top(ctx, &state.disassembly_window_title);
392    }
393    state.break_status_last_frame = break_status;
394
395    // Prevent window from spawning partially offscreen due to large default width
396    let default_pos = [50.0, crate::rand_window_pos()[1]];
397
398    let mut open = state.disassembly_open;
399    Window::new(&state.disassembly_window_title)
400        .open(&mut open)
401        .constrain(false)
402        .resizable([true, true])
403        .default_pos(default_pos)
404        .default_width(800.0)
405        .show(ctx, |ui| {
406            if let Some(handle_command) = handle_command {
407                render_disasm_top_panel(state, handle_command, ui);
408            }
409
410            render_disasm_right_panel(m68k, memory_map, state, ui);
411            render_disasm_central_panel(m68k, memory_map, state, break_status, ui);
412        });
413    state.disassembly_open = open;
414}
415
416fn render_disasm_top_panel(
417    state: &mut M68kDebugWindowState,
418    mut handle_command: impl FnMut(M68kBreakCommand),
419    ui: &mut Ui,
420) {
421    Panel::top(format!("{}_top_panel", state.disassembly_window_title)).show_inside(ui, |ui| {
422        ui.horizontal(|ui| {
423            if ui.button("Pause").clicked() {
424                handle_command(M68kBreakCommand::Pause);
425            }
426
427            if ui.button("Resume").clicked() {
428                handle_command(M68kBreakCommand::Resume);
429            }
430
431            if ui.button("Step").clicked() {
432                handle_command(M68kBreakCommand::Step);
433            }
434
435            ui.with_layout(Layout::right_to_left(Align::Min), |ui| {
436                if ui.button("Breakpoints").clicked() {
437                    state.open_breakpoints_window(ui.ctx());
438                }
439            });
440        });
441
442        ui.add_space(5.0);
443    });
444}
445
446fn render_disasm_right_panel(
447    m68k: &M68000,
448    memory_map: &dyn M68kDebugMemoryMap,
449    state: &mut M68kDebugWindowState,
450    ui: &mut Ui,
451) {
452    Panel::right(format!("{}_right_panel", state.disassembly_window_title)).show_inside(ui, |ui| {
453        ui.horizontal(|ui| {
454            let text_resp =
455                ui.add(TextEdit::singleline(&mut state.disassemble_jump_addr).desired_width(60.0));
456            let button_resp = ui.button("Jump to address");
457
458            let should_jump = button_resp.clicked()
459                || (text_resp.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)));
460            if should_jump
461                && let Ok(address) = u32::from_str_radix(&state.disassemble_jump_addr, 16)
462            {
463                state.move_disassembly_table(address & 0xFFFFFF & !1);
464            }
465        });
466
467        ui.add_space(3.0);
468
469        if ui.button("Jump to PC").clicked() {
470            state.move_disassembly_table(m68k.pc());
471        }
472
473        ui.separator();
474
475        let data_registers = m68k.data_registers();
476        let address_registers = m68k.address_registers();
477        let status_register = m68k.status_register();
478
479        Grid::new(format!("{}_registers", state.disassembly_window_title)).show(ui, |ui| {
480            for i in 0..7 {
481                ui.label(format!("D{i}"));
482                ui.label(monospace_u32(data_registers[i]));
483
484                ui.label(format!("A{i}"));
485                ui.label(monospace_u32(address_registers[i]));
486
487                ui.end_row();
488            }
489
490            ui.label("D7");
491            ui.label(monospace_u32(data_registers[7]));
492
493            ui.label("A7");
494            ui.label(monospace_u32(m68k.stack_pointer()));
495
496            ui.end_row();
497
498            ui.label("SSP");
499            ui.label(monospace_u32(m68k.supervisor_stack_pointer()));
500
501            ui.label("USP");
502            ui.label(monospace_u32(m68k.user_stack_pointer()));
503
504            ui.end_row();
505
506            ui.label("SR");
507            ui.label(monospace_u16(status_register));
508
509            ui.label("PC");
510            ui.label(monospace_u32(m68k.pc()));
511
512            ui.end_row();
513        });
514
515        ui.horizontal(|ui| {
516            ui.label("CCR");
517
518            ui.add_space(20.0);
519
520            let carry: u8 = status_register.bit(0).into();
521            let overflow: u8 = status_register.bit(1).into();
522            let zero: u8 = status_register.bit(2).into();
523            let negative: u8 = status_register.bit(3).into();
524            let extend: u8 = status_register.bit(4).into();
525            ui.label(
526                RichText::new(format!("C={carry} V={overflow} Z={zero} N={negative} X={extend}"))
527                    .monospace(),
528            );
529        });
530
531        for (label, text) in memory_map.extra_info() {
532            ui.horizontal(|ui| {
533                ui.label(label);
534                ui.add_space(5.0);
535                ui.label(RichText::new(text).monospace());
536            });
537        }
538    });
539}
540
541fn render_disasm_central_panel(
542    m68k: &M68000,
543    memory_map: &dyn M68kDebugMemoryMap,
544    state: &mut M68kDebugWindowState,
545    break_status: M68000BreakStatus,
546    ui: &mut Ui,
547) {
548    let ctx = ui.ctx().clone();
549
550    CentralPanel::default().show_inside(ui, |ui| {
551        ui.spacing_mut().scroll = ScrollStyle { bar_width: 10.0, ..ScrollStyle::solid() };
552
553        let mut table_builder = TableBuilder::new(ui)
554            .column(Column::auto().at_least(10.0))
555            .column(Column::auto().at_least(60.0))
556            .column(Column::auto().at_least(270.0))
557            .column(Column::remainder())
558            .striped(true)
559            .sense(egui::Sense::click());
560
561        if state.disassemble_reset_table {
562            state.disassemble_reset_table = false;
563            table_builder = table_builder.scroll_to_row(0, Some(Align::Min));
564        } else if crate::window_on_top(&ctx, &state.disassembly_window_title) {
565            let keys = crate::scroll_keys_pressed(&ctx);
566            if let Some(offset) = keys.relative_scroll_offset(state.disassemble_table_height) {
567                table_builder =
568                    table_builder.vertical_scroll_offset(state.disassemble_table_offset + offset);
569            }
570        }
571
572        let m68k_pc = if break_status.breaking { break_status.pc } else { m68k.pc() };
573
574        let highlight_color = crate::highlight_color(ctx.theme());
575
576        let scroll_output = table_builder.body(|mut body| {
577            let mut pc = state.disassemble_start;
578            let mut instruction = DisassembledInstruction::new();
579
580            for _ in 0..100 {
581                body.row(15.0, |mut row| {
582                    let is_pc_row = pc == m68k_pc;
583                    let original_pc = pc;
584
585                    row.set_selected(state.disassemble_selected_pcs.contains(pc));
586
587                    row.col(|ui| {
588                        state.breakpoints.render_clickable_widget(
589                            U24::new(pc),
590                            format!("{}_break_row_{pc}", state.breakpoints_window_title),
591                            ui,
592                        );
593
594                        if is_pc_row {
595                            ui.add(non_selectable_label(monospace_str("→").color(highlight_color)));
596                        }
597                    });
598
599                    row.col(|ui| {
600                        let mut text = monospace_u24(pc);
601                        if is_pc_row {
602                            text = text.color(highlight_color);
603                        }
604
605                        ui.add(non_selectable_label(text));
606                    });
607
608                    m68000_emu::disassemble::disassemble_into(&mut instruction, pc, || {
609                        let word = memory_map.peek(pc).unwrap_or(0xFFFF);
610                        pc = (pc + 2) & 0xFFFFFF;
611                        word
612                    });
613
614                    row.col(|ui| {
615                        let mut text = RichText::new(&instruction.text).monospace();
616                        if is_pc_row {
617                            text = text.color(highlight_color);
618                        }
619
620                        ui.add(non_selectable_label(text));
621                    });
622
623                    row.col(|ui| {
624                        if let Some(memory_access) =
625                            instruction.memory_read.or(instruction.memory_write)
626                        {
627                            render_memory_access_col(
628                                memory_access,
629                                m68k,
630                                memory_map,
631                                &instruction,
632                                ui,
633                            );
634                        }
635                    });
636
637                    if row.response().clicked() {
638                        state
639                            .disassemble_selected_pcs
640                            .handle_click(original_pc, ctx.input(|i| i.modifiers));
641                    }
642                });
643            }
644
645            state.disassemble_end_addr = Some(pc);
646        });
647        state.disassemble_table_offset = scroll_output.state.offset.y;
648        state.disassemble_table_height = scroll_output.inner_rect.height();
649    });
650}
651
652fn render_memory_access_col(
653    memory_read: MemoryAccess,
654    m68k: &M68000,
655    memory_map: &dyn M68kDebugMemoryMap,
656    instruction: &DisassembledInstruction,
657    ui: &mut Ui,
658) {
659    let (mut address, size) = memory_read.resolve_address(m68k);
660    address &= 0xFFFFFF;
661
662    let value = match size {
663        OpSize::Byte => memory_map
664            .peek(address)
665            .map(|word| if !address.bit(0) { word.msb().into() } else { word.lsb().into() }),
666        OpSize::Word => memory_map.peek(address).map(u32::from),
667        OpSize::LongWord => {
668            let high = memory_map.peek(address);
669            let low = memory_map.peek(address.wrapping_add(2));
670            match (high, low) {
671                (Some(high), Some(low)) => Some(u32::from(low) | (u32::from(high) << 16)),
672                (Some(high), None) => Some(u32::from(high) << 16),
673                (None, Some(low)) => Some(low.into()),
674                (None, None) => None,
675            }
676        }
677    };
678
679    let value_str = value.map(|value| match size {
680        OpSize::Byte => format!("0x{value:02X}"),
681        OpSize::Word => format!("0x{value:04X}"),
682        OpSize::LongWord => format!("0x{value:08X}"),
683    });
684
685    let text = match (memory_read, instruction.memory_read_type, value_str) {
686        (MemoryAccess::Absolute { .. }, MemoryReadType::Read, Some(value_str)) => {
687            monospace_str(format!("; = {value_str}"))
688        }
689        (MemoryAccess::Absolute { .. }, ..) => return,
690        (_, MemoryReadType::Read, Some(value_str)) => {
691            monospace_str(format!("; ${address:06X} = {value_str}"))
692        }
693        (_, MemoryReadType::Read, None) | (_, MemoryReadType::EffectiveAddress, _) => {
694            monospace_str(format!("; ${address:06X}"))
695        }
696    };
697
698    ui.add(non_selectable_label(text));
699}
700
701pub fn render_breakpoints_window(
702    ctx: &egui::Context,
703    state: &mut M68kDebugWindowState,
704    mut update_breakpoints: impl FnMut(M68000Breakpoints),
705) {
706    let response = state.breakpoints.show_window(
707        ctx,
708        &state.breakpoints_window_title,
709        &mut state.breakpoints_open,
710        |ui| state.interrupt_breakpoints.render(ui),
711    );
712    if response == BreakpointWindowResponse::Changed {
713        let memory_breakpoints = state
714            .breakpoints
715            .breakpoints()
716            .iter()
717            .map(|breakpoint| M68000Breakpoint {
718                start_address: breakpoint.start_address.get(),
719                end_address: breakpoint.end_address.get(),
720                read: breakpoint.read,
721                write: breakpoint.write,
722                execute: breakpoint.execute,
723            })
724            .collect();
725
726        update_breakpoints(M68000Breakpoints {
727            memory: memory_breakpoints,
728            interrupt: state.interrupt_breakpoints.levels(),
729        });
730    }
731}
732
733fn monospace_u16(value: u16) -> RichText {
734    RichText::new(format!("{value:04X}")).monospace()
735}
736
737fn monospace_u24(value: u32) -> RichText {
738    RichText::new(format!("{:06X}", value & 0xFFFFFF)).monospace()
739}
740
741fn monospace_u32(value: u32) -> RichText {
742    RichText::new(format!("{value:08X}")).monospace()
743}
744
745fn monospace_str(s: impl Into<String>) -> RichText {
746    RichText::new(s).monospace()
747}