Skip to main content

systemless/trap/
dialog.rs

1//! Dialog Manager, Cursor Manager, and misc stub trap handlers.
2
3use super::dispatch::{
4    DialogItem, DialogPopupDraw, DialogPopupTrackingState, DialogTrackingState,
5    PendingDialogPopupMenu, PersistentDialogSnapshot, QueuedEvent, RetainedModalDialogClickState,
6};
7use super::types::{decode_mac_roman_for_render, Rect, ShapeOp};
8use crate::cpu::{CpuOps, Register};
9use crate::display::CursorImage;
10use crate::memory::{MacMemoryBus, MemoryBus};
11use crate::quickdraw::fonts::get_font_face_scaled;
12use crate::quickdraw::text::get_font_metrics;
13use crate::ui_theme::{ControlKind, UiThemeId};
14use crate::Result;
15use std::collections::VecDeque;
16use std::sync::OnceLock;
17
18static TRACE_DIALOG_PROCS: OnceLock<bool> = OnceLock::new();
19static TRACE_DIALOG_FILTER: OnceLock<bool> = OnceLock::new();
20static TRACE_TEXTEDIT: OnceLock<bool> = OnceLock::new();
21static TRACE_DIALOG_ITEMS: OnceLock<bool> = OnceLock::new();
22static TRACE_DIALOG_TEXT_INLINE: OnceLock<bool> = OnceLock::new();
23const MANAGER_DIALOG_RECORD_ALIGNMENT: u32 = 256;
24
25struct DialogCIconLayout {
26    width: i16,
27    height: i16,
28    pm_row_bytes: u32,
29    mask_row_bytes: u32,
30    bmap_row_bytes: u32,
31    pixel_size: u16,
32    mask_data_ptr: u32,
33    bmap_data_ptr: u32,
34    pixel_data_ptr: u32,
35}
36
37#[derive(Clone, Copy)]
38struct FullscreenOffscreenDialogRestoreCandidate {
39    base: u32,
40    row_bytes: u32,
41    top: i16,
42    left: i16,
43    bottom: i16,
44    right: i16,
45    ctab_handle: u32,
46    score: u32,
47}
48
49#[derive(Clone, Copy, Debug, PartialEq, Eq)]
50struct TeResolvedStyle {
51    font: i16,
52    face: i16,
53    size: i16,
54    color: (u16, u16, u16),
55    line_height: i16,
56    ascent: i16,
57}
58
59#[derive(Clone, Copy, Debug, PartialEq, Eq)]
60struct TeStyleRun {
61    start: usize,
62    style_index: usize,
63    style: TeResolvedStyle,
64}
65
66fn trace_dialog_procs_enabled() -> bool {
67    *TRACE_DIALOG_PROCS.get_or_init(|| std::env::var_os("SYSTEMLESS_TRACE_DIALOG_PROCS").is_some())
68}
69
70fn trace_dialog_filter_enabled() -> bool {
71    *TRACE_DIALOG_FILTER
72        .get_or_init(|| std::env::var_os("SYSTEMLESS_TRACE_DIALOG_FILTER").is_some())
73}
74
75fn trace_textedit_enabled() -> bool {
76    *TRACE_TEXTEDIT.get_or_init(|| std::env::var_os("SYSTEMLESS_TRACE_TEXTEDIT").is_some())
77}
78
79fn trace_dialog_items_enabled() -> bool {
80    *TRACE_DIALOG_ITEMS.get_or_init(|| std::env::var_os("SYSTEMLESS_TRACE_DIALOG_ITEMS").is_some())
81}
82
83fn trace_dialog_text_inline_enabled() -> bool {
84    *TRACE_DIALOG_TEXT_INLINE
85        .get_or_init(|| std::env::var_os("SYSTEMLESS_TRACE_DIALOG_TEXT").is_some())
86}
87
88fn input_trace_event_name(what: u16) -> &'static str {
89    match what {
90        0 => "nullEvent",
91        1 => "mouseDown",
92        2 => "mouseUp",
93        3 => "keyDown",
94        4 => "keyUp",
95        5 => "autoKey",
96        6 => "updateEvt",
97        7 => "diskEvt",
98        8 => "activateEvt",
99        _ => "unknownEvent",
100    }
101}
102
103fn input_trace_nonzero(value: u32) -> String {
104    if value == 0 {
105        "$00000000".to_string()
106    } else {
107        "$NONZERO".to_string()
108    }
109}
110
111fn input_trace_event_message(what: u16, message: u32) -> String {
112    match what {
113        6 | 8 => input_trace_nonzero(message),
114        _ => format!("${message:08X}"),
115    }
116}
117
118fn input_trace_text_bytes(value: &str) -> String {
119    if value.is_empty() {
120        return "empty".to_string();
121    }
122    let mut out = String::from("hex:");
123    for byte in value.as_bytes() {
124        out.push_str(&format!("{byte:02X}"));
125    }
126    out
127}
128
129impl super::TrapDispatcher {
130    fn record_dialog_input_trace(
131        &mut self,
132        trap: &str,
133        event_ptr: u32,
134        what: u16,
135        message: u32,
136        where_v: i16,
137        where_h: i16,
138        modifiers: u16,
139        active_dialog: Option<u32>,
140        result: bool,
141        detail: &str,
142    ) {
143        if !self.input_trace_enabled {
144            return;
145        }
146        let active_dialog = active_dialog
147            .map(input_trace_nonzero)
148            .unwrap_or_else(|| "none".to_string());
149        let mut line = format!(
150            "{trap} action=guest_event:{} event_ptr={} delivered=EventRecord{{what={}({}), message={}, where=({where_v},{where_h}), modifiers=${modifiers:04X}}} {} active_dialog={} result={}",
151            input_trace_event_name(what),
152            input_trace_nonzero(event_ptr),
153            input_trace_event_name(what),
154            what,
155            input_trace_event_message(what, message),
156            self.input_trace_state_fields(),
157            active_dialog,
158            if result { "true" } else { "false" },
159        );
160        if !detail.is_empty() {
161            line.push(' ');
162            line.push_str(detail);
163        }
164        self.record_input_trace_line(line);
165    }
166
167    fn record_modal_dialog_input_trace(
168        &mut self,
169        action: &str,
170        dialog_ptr: u32,
171        bounds: (i16, i16, i16, i16),
172        item_hit: i16,
173        item_type: Option<u8>,
174        highlighted: Option<bool>,
175        result: &str,
176        outcome: &str,
177    ) {
178        if !self.input_trace_enabled {
179            return;
180        }
181        let item_type = item_type
182            .map(|value| format!("${value:02X}"))
183            .unwrap_or_else(|| "none".to_string());
184        let highlighted = highlighted
185            .map(|value| if value { "true" } else { "false" }.to_string())
186            .unwrap_or_else(|| "none".to_string());
187        self.record_input_trace_line(format!(
188            "A991 action={} live_mouse=({},{}) {} dialog={} bounds=({},{},{},{}) item_hit={} item_type={} highlighted={} result={} outcome={}",
189            action,
190            self.mouse_pos.0,
191            self.mouse_pos.1,
192            self.input_trace_state_fields(),
193            input_trace_nonzero(dialog_ptr),
194            bounds.0,
195            bounds.1,
196            bounds.2,
197            bounds.3,
198            item_hit,
199            item_type,
200            highlighted,
201            result,
202            outcome,
203        ));
204    }
205
206    fn record_modal_dialog_text_input_trace(
207        &mut self,
208        action: &str,
209        dialog_ptr: u32,
210        bounds: (i16, i16, i16, i16),
211        edit_item: i16,
212        item_type: Option<u8>,
213        key_code: u8,
214        char_code: u8,
215        text_before: &str,
216        text_after: &str,
217        result: &str,
218        outcome: &str,
219    ) {
220        if !self.input_trace_enabled {
221            return;
222        }
223        let item_type = item_type
224            .map(|value| format!("${value:02X}"))
225            .unwrap_or_else(|| "none".to_string());
226        self.record_input_trace_line(format!(
227            "A991 action={} live_mouse=({},{}) {} dialog={} bounds=({},{},{},{}) edit_item={} item_type={} key_code=${:02X} char_code=${:02X} text_before={} text_after={} result={} outcome={}",
228            action,
229            self.mouse_pos.0,
230            self.mouse_pos.1,
231            self.input_trace_state_fields(),
232            input_trace_nonzero(dialog_ptr),
233            bounds.0,
234            bounds.1,
235            bounds.2,
236            bounds.3,
237            edit_item,
238            item_type,
239            key_code,
240            char_code,
241            input_trace_text_bytes(text_before),
242            input_trace_text_bytes(text_after),
243            result,
244            outcome,
245        ));
246    }
247
248    fn record_modal_dialog_filter_input_trace(
249        &mut self,
250        action: &str,
251        dialog_ptr: u32,
252        bounds: (i16, i16, i16, i16),
253        filter_proc: u32,
254        event: Option<&QueuedEvent>,
255        item_hit: i16,
256        item_type: Option<u8>,
257        handled_mouse_down: bool,
258        dialog_retained: bool,
259        result: &str,
260        outcome: &str,
261    ) {
262        if !self.input_trace_enabled {
263            return;
264        }
265        let item_type = item_type
266            .map(|value| format!("${value:02X}"))
267            .unwrap_or_else(|| "none".to_string());
268        let (event_name, message, where_text, modifiers) = if let Some(event) = event {
269            (
270                format!("{}({})", input_trace_event_name(event.what), event.what),
271                input_trace_event_message(event.what, event.message),
272                format!("({},{})", event.where_v, event.where_h),
273                format!("${:04X}", event.modifiers),
274            )
275        } else {
276            (
277                "none".to_string(),
278                "none".to_string(),
279                "none".to_string(),
280                "none".to_string(),
281            )
282        };
283        self.record_input_trace_line(format!(
284            "A991 action={} live_mouse=({},{}) {} dialog={} bounds=({},{},{},{}) filter_proc={} event={} message={} where={} modifiers={} item_hit={} item_type={} handled_mouse_down={} dialog_retained={} result={} outcome={}",
285            action,
286            self.mouse_pos.0,
287            self.mouse_pos.1,
288            self.input_trace_state_fields(),
289            input_trace_nonzero(dialog_ptr),
290            bounds.0,
291            bounds.1,
292            bounds.2,
293            bounds.3,
294            input_trace_nonzero(filter_proc),
295            event_name,
296            message,
297            where_text,
298            modifiers,
299            item_hit,
300            item_type,
301            handled_mouse_down,
302            dialog_retained,
303            result,
304            outcome,
305        ));
306    }
307
308    fn dialog_template_res_err(&self, dialog_id: i16) -> i16 {
309        if self.find_resource_any(*b"DLOG", dialog_id).is_some() {
310            0
311        } else {
312            0
313        }
314    }
315
316    fn loaded_resource_handle_in_file(
317        &self,
318        res_type: [u8; 4],
319        res_id: i16,
320        ptr: u32,
321        refnum: u16,
322    ) -> Option<u32> {
323        self.loaded_handles
324            .iter()
325            .find_map(|(&handle, &(loaded_ptr, loaded_type, loaded_id))| {
326                if loaded_ptr == ptr
327                    && loaded_type == res_type
328                    && loaded_id == res_id
329                    && self.resource_handle_files.get(&handle).copied() == Some(refnum)
330                {
331                    Some(handle)
332                } else {
333                    None
334                }
335            })
336    }
337
338    fn set_handle_purgeable(&mut self, handle: u32, purgeable: bool) {
339        if handle == 0 {
340            return;
341        }
342
343        if purgeable {
344            *self.handle_state_bits.entry(handle).or_insert(0) |= 0x40;
345        } else if let Some(bits) = self.handle_state_bits.get_mut(&handle) {
346            *bits &= !0x40;
347            if *bits == 0 {
348                self.handle_state_bits.remove(&handle);
349            }
350        }
351    }
352
353    fn prepare_dialog_resource_handle(
354        &mut self,
355        bus: &mut MacMemoryBus,
356        res_type: [u8; 4],
357        res_id: i16,
358        purgeable: bool,
359        load_if_missing: bool,
360    ) -> Option<(u32, u32)> {
361        let (refnum, ptr) = self.find_resource_any(res_type, res_id)?;
362        let handle = if load_if_missing {
363            let handle =
364                self.get_or_create_resource_handle_in_file(bus, res_type, res_id, ptr, refnum);
365            if ptr != 0 && bus.read_long(handle) == 0 {
366                bus.write_long(handle, ptr);
367                self.ptr_to_handle.insert(ptr, handle);
368            }
369            handle
370        } else {
371            self.loaded_resource_handle_in_file(res_type, res_id, ptr, refnum)?
372        };
373
374        self.set_handle_purgeable(handle, purgeable);
375        Some((handle, ptr))
376    }
377
378    fn dialog_item_resource_type(item_type: u8) -> Option<[u8; 4]> {
379        match item_type & 0x7F {
380            7 => Some(*b"CNTL"),
381            32 => Some(*b"ICON"),
382            64 => Some(*b"PICT"),
383            _ => None,
384        }
385    }
386
387    fn cascade_dialog_resource_purgeability(
388        &mut self,
389        bus: &mut MacMemoryBus,
390        template_type: [u8; 4],
391        template_id: i16,
392        purgeable: bool,
393        load_if_missing: bool,
394    ) {
395        let Some((_, template_ptr)) = self.prepare_dialog_resource_handle(
396            bus,
397            template_type,
398            template_id,
399            purgeable,
400            load_if_missing,
401        ) else {
402            return;
403        };
404
405        let items_id = match template_type {
406            // IM:I I-437: DLOG item-list ID is at offset 18.
407            [b'D', b'L', b'O', b'G'] if bus.get_alloc_size(template_ptr).unwrap_or(0) >= 20 => {
408                bus.read_word(template_ptr + 18) as i16
409            }
410            // IM:I I-425..I-426: ALRT item-list ID is at offset 8.
411            [b'A', b'L', b'R', b'T'] if bus.get_alloc_size(template_ptr).unwrap_or(0) >= 10 => {
412                bus.read_word(template_ptr + 8) as i16
413            }
414            _ => return,
415        };
416
417        let Some((_, ditl_ptr)) = self.prepare_dialog_resource_handle(
418            bus,
419            *b"DITL",
420            items_id,
421            purgeable,
422            load_if_missing,
423        ) else {
424            return;
425        };
426
427        let ditl_len = bus.get_alloc_size(ditl_ptr).unwrap_or(0);
428        let items = Self::parse_ditl(bus, ditl_ptr, ditl_len);
429        for item in items {
430            if let Some(res_type) = Self::dialog_item_resource_type(item.item_type) {
431                self.prepare_dialog_resource_handle(
432                    bus,
433                    res_type,
434                    item.resource_id,
435                    purgeable,
436                    load_if_missing,
437                );
438            }
439        }
440    }
441
442    const TE_DEST_RECT_OFFSET: u32 = 0x00;
443    const TE_VIEW_RECT_OFFSET: u32 = 0x08;
444    const TE_SEL_RECT_OFFSET: u32 = 0x10;
445    const TE_LINE_HEIGHT_OFFSET: u32 = 0x18;
446    const TE_FONT_ASCENT_OFFSET: u32 = 0x1A;
447    const TE_SEL_POINT_OFFSET: u32 = 0x1C;
448    const TE_SEL_START_OFFSET: u32 = 0x20;
449    const TE_SEL_END_OFFSET: u32 = 0x22;
450    const TE_ACTIVE_OFFSET: u32 = 0x24;
451    const TE_CARET_TIME_OFFSET: u32 = 0x34;
452    const TE_CARET_STATE_OFFSET: u32 = 0x38;
453    #[allow(dead_code)]
454    const TE_CR_ONLY_OFFSET: u32 = 0x48;
455    const TE_JUST_OFFSET: u32 = 0x3A;
456    const TE_LENGTH_OFFSET: u32 = 0x3C;
457    const TE_HTEXT_OFFSET: u32 = 0x3E;
458    const TE_TX_FONT_OFFSET: u32 = 0x4A;
459    const TE_TX_FACE_OFFSET: u32 = 0x4C;
460    const TE_TX_MODE_OFFSET: u32 = 0x4E;
461    const TE_TX_SIZE_OFFSET: u32 = 0x50;
462    const TE_IN_PORT_OFFSET: u32 = 0x52;
463    const TE_N_LINES_OFFSET: u32 = 0x5E;
464    const TE_LINE_STARTS_OFFSET: u32 = 0x60;
465    const TE_REC_MIN_SIZE: u32 = 128;
466    const TE_CARET_BLINK_TICKS: u32 = 32;
467    // TextEdit draws inside the destination rectangle (IM:I I-373 to I-374);
468    // BasiliskII/System 7.5.3 `dialog_visual_textedit_smoke` pins the ROM's
469    // flush-left glyph origin one pixel in from destRect.left.
470    const TE_LINE_LEFT_INSET: i16 = 1;
471
472    const DBOX_FRAME_MARGIN: i16 = 8;
473    const EDIT_TEXT_FRAME_OUTSET: i16 = 3;
474    const STANDARD_CONTROL_MARK_SIZE: i16 = 12;
475    const STANDARD_CONTROL_MARK_LEFT_INSET: i16 = 2;
476    const STANDARD_CONTROL_TITLE_GAP: i16 = 4;
477    const INACTIVE_STANDARD_CONTROL_TITLE_RGB: [u16; 3] = [0xA1A1, 0xA1A1, 0xA1A1];
478    const CLASSIC_RADIO_OUTLINE_12: [&'static str; 12] = [
479        "....####....",
480        "..##....##..",
481        ".#........#.",
482        ".#........#.",
483        "#..........#",
484        "#..........#",
485        "#..........#",
486        "#..........#",
487        ".#........#.",
488        ".#........#.",
489        "..##....##..",
490        "....####....",
491    ];
492    const CLASSIC_RADIO_DOT_12: [&'static str; 12] = [
493        "............",
494        "............",
495        "............",
496        "....####....",
497        "...######...",
498        "...######...",
499        "...######...",
500        "...######...",
501        "....####....",
502        "............",
503        "............",
504        "............",
505    ];
506
507    const TE_STYLE_N_RUNS_OFFSET: u32 = 0x00;
508    const TE_STYLE_N_STYLES_OFFSET: u32 = 0x02;
509    const TE_STYLE_STYLE_TABLE_OFFSET: u32 = 0x04;
510    const TE_STYLE_LH_TABLE_OFFSET: u32 = 0x08;
511    const TE_STYLE_NULL_STYLE_OFFSET: u32 = 0x10;
512    const TE_STYLE_RUNS_OFFSET: u32 = 0x14;
513
514    const ST_ELEMENT_REFCOUNT_OFFSET: u32 = 0x00;
515    const ST_ELEMENT_HEIGHT_OFFSET: u32 = 0x02;
516    const ST_ELEMENT_ASCENT_OFFSET: u32 = 0x04;
517    const ST_ELEMENT_FONT_OFFSET: u32 = 0x06;
518    // Style is a one-byte SET followed by one alignment byte in TextStyle,
519    // STElement, and ScrpSTElement records. Inside Macintosh: Text 1993,
520    // pp. 2-72, 2-78, and 2-80.
521    const ST_ELEMENT_FACE_OFFSET: u32 = 0x08;
522    const ST_ELEMENT_SIZE_OFFSET: u32 = 0x0A;
523    const ST_ELEMENT_COLOR_OFFSET: u32 = 0x0C;
524    const ST_ELEMENT_SIZE: u32 = 0x12;
525
526    const LH_ELEMENT_HEIGHT_OFFSET: u32 = 0x00;
527    const LH_ELEMENT_ASCENT_OFFSET: u32 = 0x02;
528    const LH_ELEMENT_SIZE: u32 = 0x04;
529
530    const NULL_STYLE_SCRAP_OFFSET: u32 = 0x04;
531    const NULL_STYLE_REC_SIZE: u32 = 0x08;
532
533    // Inside Macintosh Volume V, V-274: StScrpRec is a count word
534    // followed immediately by ScrpSTElement entries (`scrpStyleTab EQU 2`).
535    const SCRAP_N_STYLES_OFFSET: u32 = 0x00;
536    const SCRAP_STYLE_TAB_OFFSET: u32 = 0x02;
537    const SCRAP_STYLE_START_CHAR_OFFSET: u32 = 0x00;
538    const SCRAP_STYLE_HEIGHT_OFFSET: u32 = 0x04;
539    const SCRAP_STYLE_ASCENT_OFFSET: u32 = 0x06;
540    const SCRAP_STYLE_FONT_OFFSET: u32 = 0x08;
541    const SCRAP_STYLE_FACE_OFFSET: u32 = 0x0A;
542    const SCRAP_STYLE_SIZE_OFFSET: u32 = 0x0C;
543    const SCRAP_STYLE_COLOR_OFFSET: u32 = 0x0E;
544    const SCRAP_STYLE_ELEMENT_SIZE: u32 = 0x14;
545    const STYLE_SCRAP_REC_SIZE: u32 = Self::SCRAP_STYLE_TAB_OFFSET + Self::SCRAP_STYLE_ELEMENT_SIZE;
546
547    const TE_FEATURE_AUTO_SCROLL: u16 = 0;
548    const TE_FEATURE_TEXT_BUFFERING: u16 = 1;
549    const TE_FEATURE_OUTLINE_HILITE: u16 = 2;
550    const TE_FEATURE_INLINE_INPUT: u16 = 3;
551    const TE_FEATURE_USE_TEXT_SERVICES: u16 = 4;
552
553    const TE_BIT_CLEAR: i16 = 0;
554    const TE_BIT_SET: i16 = 1;
555    const TE_BIT_TEST: i16 = -1;
556
557    fn dialog_window_proc_id(&self, bus: &MacMemoryBus, dialog_ptr: u32) -> i16 {
558        self.window_proc_ids
559            .get(&dialog_ptr)
560            .copied()
561            // Older tests seed pre-side-table dialogs by writing the WDEF
562            // procID at +108. Real DialogRecords use +108 as windowKind.
563            .unwrap_or_else(|| bus.read_word(dialog_ptr + 108) as i16)
564    }
565
566    fn dialog_window_title(bus: &MacMemoryBus, dialog_ptr: u32) -> String {
567        if dialog_ptr == 0 {
568            return String::new();
569        }
570        // WindowRecord.titleHandle is a StringHandle at +134; GetWTitle uses
571        // the same documented field (IM:I I-284).
572        let title_handle = bus.read_long(dialog_ptr + 134);
573        if title_handle == 0 {
574            return String::new();
575        }
576        let title_ptr = bus.read_long(title_handle);
577        if title_ptr == 0 {
578            return String::new();
579        }
580        decode_mac_roman_for_render(&bus.read_pstring(title_ptr))
581    }
582
583    fn ensure_text_handle_size(bus: &mut MacMemoryBus, item_handle: u32, size: usize) -> u32 {
584        if item_handle == 0 {
585            return 0;
586        }
587
588        let current_ptr = bus.read_long(item_handle);
589        if size == 0 {
590            if current_ptr != 0 {
591                bus.free(current_ptr);
592                bus.write_long(item_handle, 0);
593            }
594            return 0;
595        }
596
597        let required = size as u32;
598        let current_size = if current_ptr != 0 {
599            bus.get_alloc_size(current_ptr).unwrap_or(0)
600        } else {
601            0
602        };
603
604        if current_ptr != 0 && current_size == required {
605            return current_ptr;
606        }
607
608        let new_ptr = bus.alloc(required);
609        if new_ptr == 0 {
610            return current_ptr;
611        }
612
613        if current_ptr != 0 {
614            bus.free(current_ptr);
615        }
616        bus.write_long(item_handle, new_ptr);
617        new_ptr
618    }
619
620    fn text_item_string_from_handle(bus: &MacMemoryBus, item_handle: u32) -> String {
621        Self::text_item_string_from_handle_if_present(bus, item_handle).unwrap_or_default()
622    }
623
624    fn text_item_string_from_handle_if_present(
625        bus: &MacMemoryBus,
626        item_handle: u32,
627    ) -> Option<String> {
628        Self::text_item_bytes_from_handle_if_present(bus, item_handle)
629            .map(|bytes| decode_mac_roman_for_render(&bytes))
630    }
631
632    fn text_item_bytes_from_handle_if_present(
633        bus: &MacMemoryBus,
634        item_handle: u32,
635    ) -> Option<Vec<u8>> {
636        if item_handle == 0 {
637            return None;
638        }
639
640        let data_ptr = bus.read_long(item_handle);
641        if data_ptr == 0 {
642            return Some(Vec::new());
643        }
644
645        let len = bus.get_alloc_size(data_ptr).unwrap_or(0) as usize;
646        Some(bus.read_bytes(data_ptr, len))
647    }
648
649    fn dialog_item_handle_addr(bus: &MacMemoryBus, dialog_ptr: u32, item_no: i16) -> Option<u32> {
650        if item_no <= 0 {
651            return None;
652        }
653
654        let items_handle = bus.read_long(dialog_ptr + 156);
655        if items_handle == 0 {
656            return None;
657        }
658        let ditl_ptr = bus.read_long(items_handle);
659        if ditl_ptr == 0 {
660            return None;
661        }
662
663        let max_index = bus.read_word(ditl_ptr) as i16;
664        if item_no > max_index + 1 {
665            return None;
666        }
667
668        let ditl_len = bus.get_alloc_size(ditl_ptr).unwrap_or(u32::MAX);
669        let mut offset = 2u32;
670        for current_item in 1..=max_index + 1 {
671            let item_handle_addr = ditl_ptr + offset;
672            offset += 4; // itmhand
673            offset += 8; // itmr
674            let item_type = bus.read_byte(ditl_ptr + offset);
675            let data_len_byte = bus.read_byte(ditl_ptr + offset + 1);
676            offset += 2; // itmtype + itmlen
677            let base_type = item_type & 0x7F;
678            let remaining = ditl_len.saturating_sub(offset);
679            let payload_len = Self::ditl_item_payload_len(base_type, data_len_byte, remaining)?;
680            let padded = (payload_len + 1) & !1;
681            if current_item == item_no {
682                return Some(item_handle_addr);
683            }
684            offset += padded;
685        }
686
687        None
688    }
689
690    fn dialog_item_handle(bus: &MacMemoryBus, dialog_ptr: u32, item_no: i16) -> u32 {
691        Self::dialog_item_handle_addr(bus, dialog_ptr, item_no)
692            .map(|addr| bus.read_long(addr))
693            .unwrap_or(0)
694    }
695
696    fn set_dialog_item_handle(bus: &mut MacMemoryBus, dialog_ptr: u32, item_no: i16, handle: u32) {
697        if let Some(addr) = Self::dialog_item_handle_addr(bus, dialog_ptr, item_no) {
698            bus.write_long(addr, handle);
699        }
700    }
701
702    pub(crate) fn front_dialog_for_user_item_proc(&self, proc_ptr: u32) -> Option<u32> {
703        if proc_ptr == 0 {
704            return None;
705        }
706        let front = self.front_window;
707        if front == 0 {
708            return None;
709        }
710        let Some(items) = self.dialog_items.get(&front) else {
711            return None;
712        };
713        if items
714            .iter()
715            .any(|item| (item.item_type & 0x7F) == 0 && item.proc_ptr == proc_ptr)
716        {
717            Some(front)
718        } else {
719            None
720        }
721    }
722
723    pub(crate) fn front_dialog_has_user_item_proc(&self, proc_ptr: u32) -> bool {
724        self.front_dialog_for_user_item_proc(proc_ptr).is_some()
725    }
726
727    pub(crate) fn redraw_dialog_window_contents(
728        &mut self,
729        bus: &mut MacMemoryBus,
730        dialog_ptr: u32,
731    ) {
732        if let Some(mut items) = self.dialog_items.get(&dialog_ptr).cloned() {
733            Self::refresh_ditl_proc_ptrs(bus, dialog_ptr, &mut items);
734            let bounds = Self::dialog_screen_bounds(bus, dialog_ptr);
735            let proc_id = self.dialog_window_proc_id(bus, dialog_ptr);
736            let (edit_text, edit_item, default_item) =
737                Self::dialog_edit_state(bus, dialog_ptr, &items);
738            self.draw_dialog(
739                bus,
740                bounds,
741                proc_id,
742                "",
743                &items,
744                default_item,
745                &edit_text,
746                edit_item,
747                false,
748                dialog_ptr,
749            );
750            if Self::dialog_is_game_managed(bounds, &items) {
751                self.dialog_visible_snapshots.remove(&dialog_ptr);
752            } else {
753                let pixels = self.save_dialog_pixels(bus, bounds);
754                self.dialog_visible_snapshots
755                    .insert(dialog_ptr, PersistentDialogSnapshot { bounds, pixels });
756            }
757            self.dialog_items.insert(dialog_ptr, items);
758        }
759    }
760
761    pub(crate) fn queue_modeless_dialog_draw_procs(
762        &mut self,
763        bus: &mut MacMemoryBus,
764        dialog_ptr: u32,
765    ) {
766        self.queue_modeless_dialog_draw_procs_intersecting(bus, dialog_ptr, None);
767    }
768
769    fn update_dialog_window_contents<C: CpuOps>(
770        &mut self,
771        bus: &mut MacMemoryBus,
772        cpu: &mut C,
773        dialog_ptr: u32,
774        update_rect: Option<(i16, i16, i16, i16)>,
775    ) -> bool {
776        let Some(mut items) = self.dialog_items.get(&dialog_ptr).cloned() else {
777            return false;
778        };
779        Self::refresh_ditl_proc_ptrs(bus, dialog_ptr, &mut items);
780        let bounds = Self::dialog_screen_bounds(bus, dialog_ptr);
781        let update_screen_rect =
782            update_rect.and_then(|rect| Self::dialog_update_screen_rect(bounds, rect));
783        let proc_id = self.dialog_window_proc_id(bus, dialog_ptr);
784        let (edit_text, edit_item, default_item) = Self::dialog_edit_state(bus, dialog_ptr, &items);
785        // MTE 1992 p. 6-141: DialogSelect handles update events by calling
786        // DrawDialog. MTE 1992 p. 6-143: UpdateDialog uses SetPort before
787        // redrawing the update region. Keep both dialog update paths on the
788        // dialog port before any standard-item drawing or userItem callbacks.
789        self.set_current_port_state(bus, cpu, dialog_ptr, None);
790        let Some(update_screen_rect) = update_screen_rect else {
791            return false;
792        };
793        let before_pixels = self.save_dialog_pixels(bus, bounds);
794        self.dialog_initial_draw_deferred.remove(&dialog_ptr);
795        self.draw_dialog(
796            bus,
797            bounds,
798            proc_id,
799            "",
800            &items,
801            default_item,
802            &edit_text,
803            edit_item,
804            false,
805            dialog_ptr,
806        );
807        // MTE 1992 p. 6-143: UpdateDialog redraws only the supplied update
808        // region. Most Dialog Manager HLE drawing helpers write directly to the
809        // framebuffer, so clip the full redraw by restoring pre-update pixels
810        // outside the update region after standard items have been refreshed.
811        self.restore_dialog_pixels_outside_rect(bus, bounds, &before_pixels, update_screen_rect);
812        self.dialog_items.insert(dialog_ptr, items);
813        // Application-defined userItems are guest-owned. IM:I I-405 says
814        // their item handle is a draw procedure; queue only the callbacks
815        // whose display rectangles intersect the active update region.
816        self.queue_modeless_dialog_draw_procs_intersecting(bus, dialog_ptr, update_rect);
817        true
818    }
819
820    fn dialog_update_screen_rect(
821        bounds: (i16, i16, i16, i16),
822        rect: (i16, i16, i16, i16),
823    ) -> Option<(i16, i16, i16, i16)> {
824        let screen_rect = if Self::rects_intersect(rect, bounds) {
825            rect
826        } else {
827            (
828                bounds.0.saturating_add(rect.0),
829                bounds.1.saturating_add(rect.1),
830                bounds.0.saturating_add(rect.2),
831                bounds.1.saturating_add(rect.3),
832            )
833        };
834        Self::rect_intersection(
835            screen_rect,
836            (
837                bounds.0 - Self::DBOX_FRAME_MARGIN,
838                bounds.1 - Self::DBOX_FRAME_MARGIN,
839                bounds.2 + Self::DBOX_FRAME_MARGIN,
840                bounds.3 + Self::DBOX_FRAME_MARGIN,
841            ),
842        )
843    }
844
845    fn queue_modeless_dialog_draw_procs_intersecting(
846        &mut self,
847        bus: &mut MacMemoryBus,
848        dialog_ptr: u32,
849        update_rect: Option<(i16, i16, i16, i16)>,
850    ) {
851        let Some(mut items) = self.dialog_items.get(&dialog_ptr).cloned() else {
852            return;
853        };
854        Self::refresh_ditl_proc_ptrs(bus, dialog_ptr, &mut items);
855        let bounds = Self::dialog_screen_bounds(bus, dialog_ptr);
856        let effective_update_rect = update_rect.map(|rect| {
857            if Self::rects_intersect(rect, bounds) {
858                rect
859            } else {
860                (
861                    bounds.0.saturating_add(rect.0),
862                    bounds.1.saturating_add(rect.1),
863                    bounds.0.saturating_add(rect.2),
864                    bounds.1.saturating_add(rect.3),
865                )
866            }
867        });
868        for (i, item) in items.iter().enumerate() {
869            let item_rect = Self::dialog_item_screen_rect(bounds, item.rect);
870            if (item.item_type & 0x7F) == 0
871                && item.proc_ptr != 0
872                && Self::rects_intersect(item_rect, bounds)
873                && effective_update_rect
874                    .map(|rect| Self::rects_intersect(item_rect, rect))
875                    .unwrap_or(true)
876            {
877                self.modeless_dialog_draw_proc_queue.push_back((
878                    dialog_ptr,
879                    item.proc_ptr,
880                    (i + 1) as i16,
881                ));
882            }
883        }
884        self.dialog_items.insert(dialog_ptr, items);
885    }
886
887    fn allocate_te_handle(bus: &mut MacMemoryBus) -> u32 {
888        // TENew returns a handle to a zeroed TERec owned by the caller.
889        // Text 1993, 2-85 to 2-86
890        let te_ptr = bus.alloc(Self::TE_REC_MIN_SIZE);
891        if te_ptr == 0 {
892            return 0;
893        }
894        let handle = bus.alloc(4);
895        if handle == 0 {
896            return 0;
897        }
898        bus.write_long(handle, te_ptr);
899        handle
900    }
901
902    fn allocate_handle_with_data(bus: &mut MacMemoryBus, size: u32) -> u32 {
903        let handle = bus.alloc(4);
904        if handle == 0 {
905            return 0;
906        }
907
908        let data_ptr = if size == 0 { 0 } else { bus.alloc(size) };
909        bus.write_long(handle, data_ptr);
910        handle
911    }
912
913    fn ensure_handle_capacity(bus: &mut MacMemoryBus, handle: u32, min_size: u32) -> u32 {
914        if handle == 0 {
915            return 0;
916        }
917
918        let current_ptr = bus.read_long(handle);
919        let current_size = if current_ptr != 0 {
920            bus.get_alloc_size(current_ptr).unwrap_or(0)
921        } else {
922            0
923        };
924        if current_size >= min_size {
925            return current_ptr;
926        }
927
928        let new_ptr = bus.alloc(min_size);
929        if new_ptr == 0 {
930            return current_ptr;
931        }
932        if current_ptr != 0 && current_size != 0 {
933            let existing = bus.read_bytes(current_ptr, current_size as usize);
934            bus.write_bytes(new_ptr, &existing);
935            bus.free(current_ptr);
936        }
937        bus.write_long(handle, new_ptr);
938        new_ptr
939    }
940
941    fn te_record_size_for_line_count(line_count: usize) -> u32 {
942        let line_entries = line_count.saturating_add(4) as u32;
943        Self::TE_REC_MIN_SIZE.max(Self::TE_LINE_STARTS_OFFSET + line_entries * 2)
944    }
945
946    fn ensure_te_record_line_capacity(
947        bus: &mut MacMemoryBus,
948        te_handle: u32,
949        line_count: usize,
950    ) -> u32 {
951        Self::ensure_handle_capacity(
952            bus,
953            te_handle,
954            Self::te_record_size_for_line_count(line_count),
955        )
956    }
957
958    fn te_read_rect(bus: &MacMemoryBus, addr: u32) -> (i16, i16, i16, i16) {
959        (
960            bus.read_word(addr) as i16,
961            bus.read_word(addr + 2) as i16,
962            bus.read_word(addr + 4) as i16,
963            bus.read_word(addr + 6) as i16,
964        )
965    }
966
967    fn te_write_rect_words(bus: &mut MacMemoryBus, addr: u32, rect: (i16, i16, i16, i16)) {
968        bus.write_word(addr, rect.0 as u16);
969        bus.write_word(addr + 2, rect.1 as u16);
970        bus.write_word(addr + 4, rect.2 as u16);
971        bus.write_word(addr + 6, rect.3 as u16);
972    }
973
974    fn te_rect_is_empty(rect: (i16, i16, i16, i16)) -> bool {
975        rect.0 == 0 && rect.1 == 0 && rect.2 == 0 && rect.3 == 0
976    }
977
978    fn te_rect_ptr_candidate(bus: &MacMemoryBus, ptr: u32) -> Option<(i16, i16, i16, i16)> {
979        if ptr == 0 || (ptr & 1) != 0 || ptr < 0x1000 {
980            return None;
981        }
982        let end = ptr.checked_add(8)?;
983        if end > bus.ram_size() {
984            return None;
985        }
986
987        Some(Self::te_read_rect(bus, ptr))
988    }
989
990    /// Decode TENew/TEStyleNew arguments off the Pascal stack, sniffing
991    /// either the modern Universal Headers pointer convention (8 bytes,
992    /// two `const Rect *` ptrs) or the legacy Inside Macintosh by-value
993    /// convention (16 bytes, two Rects pushed by value).
994    ///
995    /// Pascal calling convention pushes left-to-right, so the first
996    /// arg (destRect / destRect_ptr) lands DEEPER on the stack than the
997    /// second arg (viewRect / viewRect_ptr). Concretely:
998    ///   * Pointer convention (8 bytes, MPW Universal Headers):
999    ///       sp+0..3  viewRect_ptr  (last pushed, shallowest)
1000    ///       sp+4..7  destRect_ptr  (first pushed, deepest)
1001    ///   * By-value convention (16 bytes, classic IM:I I-373):
1002    ///       sp+0..7   viewRect bytes
1003    ///       sp+8..15  destRect bytes
1004    ///
1005    /// Returns (destRect, viewRect, stack_pop).
1006    #[allow(clippy::type_complexity)] // (destRect, viewRect, stack_pop) — local return shape, no aliasing benefit
1007    fn te_new_rect_args(
1008        bus: &MacMemoryBus,
1009        sp: u32,
1010    ) -> ((i16, i16, i16, i16), (i16, i16, i16, i16), u32) {
1011        // Pascal first arg (destRect / destRect_ptr) is DEEPEST → sp+4.
1012        // Pascal second arg (viewRect / viewRect_ptr) is SHALLOWEST → sp+0.
1013        let view_ptr_word = bus.read_long(sp);
1014        let dest_ptr_word = bus.read_long(sp + 4);
1015        let view_via_ptr = Self::te_rect_ptr_candidate(bus, view_ptr_word);
1016        let dest_via_ptr = Self::te_rect_ptr_candidate(bus, dest_ptr_word);
1017        if let (Some(dest), Some(view)) = (dest_via_ptr, view_via_ptr) {
1018            // Modern MPW Universal Headers pointer convention (8 bytes).
1019            if Self::te_rect_is_empty(view) && !Self::te_rect_is_empty(dest) {
1020                (dest, dest, 8)
1021            } else if Self::te_rect_is_empty(dest) && !Self::te_rect_is_empty(view) {
1022                (view, view, 8)
1023            } else if Self::te_rect_is_empty(dest) && Self::te_rect_is_empty(view) {
1024                (
1025                    Self::te_read_rect(bus, sp + 8),
1026                    Self::te_read_rect(bus, sp),
1027                    16,
1028                )
1029            } else {
1030                (dest, view, 8)
1031            }
1032        } else {
1033            // Legacy by-value convention (16 bytes).
1034            (
1035                Self::te_read_rect(bus, sp + 8),
1036                Self::te_read_rect(bus, sp),
1037                16,
1038            )
1039        }
1040    }
1041
1042    fn te_record_ptr(bus: &MacMemoryBus, te_handle: u32) -> u32 {
1043        if te_handle == 0 {
1044            0
1045        } else {
1046            bus.read_long(te_handle)
1047        }
1048    }
1049
1050    fn te_is_styled_record(bus: &MacMemoryBus, te_ptr: u32) -> bool {
1051        te_ptr != 0 && bus.read_word(te_ptr + Self::TE_TX_SIZE_OFFSET) == 0xFFFF
1052    }
1053
1054    fn te_text_handle(bus: &MacMemoryBus, te_handle: u32) -> u32 {
1055        let te_ptr = Self::te_record_ptr(bus, te_handle);
1056        if te_ptr == 0 {
1057            0
1058        } else {
1059            bus.read_long(te_ptr + Self::TE_HTEXT_OFFSET)
1060        }
1061    }
1062
1063    fn te_style_handle(bus: &MacMemoryBus, te_handle: u32) -> u32 {
1064        let te_ptr = Self::te_record_ptr(bus, te_handle);
1065        if Self::te_is_styled_record(bus, te_ptr) {
1066            bus.read_long(te_ptr + Self::TE_TX_FONT_OFFSET)
1067        } else {
1068            0
1069        }
1070    }
1071
1072    fn te_write_style_handle(bus: &mut MacMemoryBus, te_handle: u32, style_handle: u32) {
1073        let te_ptr = Self::te_record_ptr(bus, te_handle);
1074        if Self::te_is_styled_record(bus, te_ptr) {
1075            bus.write_long(te_ptr + Self::TE_TX_FONT_OFFSET, style_handle);
1076        }
1077    }
1078
1079    fn te_line_starts(bus: &MacMemoryBus, te_handle: u32) -> Vec<usize> {
1080        let te_ptr = Self::te_record_ptr(bus, te_handle);
1081        if te_ptr == 0 {
1082            return vec![0];
1083        }
1084
1085        let n_lines = bus.read_word(te_ptr + Self::TE_N_LINES_OFFSET) as usize;
1086        let mut starts = Vec::with_capacity(n_lines.saturating_add(1));
1087        for index in 0..=n_lines {
1088            starts.push(
1089                bus.read_word(te_ptr + Self::TE_LINE_STARTS_OFFSET + (index as u32 * 2)) as usize,
1090            );
1091        }
1092        if starts.is_empty() {
1093            starts.push(0);
1094        }
1095        starts
1096    }
1097
1098    fn te_char_to_line_index(bus: &MacMemoryBus, te_handle: u32, offset: usize) -> usize {
1099        let starts = Self::te_line_starts(bus, te_handle);
1100        let n_lines = starts.len().saturating_sub(1);
1101        if n_lines <= 1 {
1102            return 0;
1103        }
1104
1105        let mut low = 0usize;
1106        let mut high = n_lines;
1107        let mut current = (high + low) / 2;
1108        while low < high && starts[current] != offset {
1109            if starts[current] < offset {
1110                low = current + 1;
1111            } else {
1112                high = current.saturating_sub(1);
1113            }
1114            current = (high + low) / 2;
1115        }
1116
1117        if starts[current] > offset || current == n_lines {
1118            current.saturating_sub(1)
1119        } else {
1120            current
1121        }
1122    }
1123
1124    fn font_lookup_size(size: i16) -> i16 {
1125        // QuickDraw grafPorts initialize txSize to 0, which the Font Manager
1126        // interprets as the system font size. Preserve that sentinel for font
1127        // lookup; coercing it to 1 selects the wrong strike/fallback.
1128        // Inside Macintosh Volume I, I-163 and I-178.
1129        if size == 0 {
1130            0
1131        } else {
1132            size.max(1)
1133        }
1134    }
1135
1136    fn te_char_to_point(&self, bus: &MacMemoryBus, te_handle: u32, offset: usize) -> (i16, i16) {
1137        let te_ptr = Self::te_record_ptr(bus, te_handle);
1138        if te_ptr == 0 {
1139            return (0, 0);
1140        }
1141
1142        let dest_rect = Self::te_read_rect(bus, te_ptr + Self::TE_DEST_RECT_OFFSET);
1143        let just = bus.read_word(te_ptr + Self::TE_JUST_OFFSET) as i16;
1144        let text_bytes = Self::te_text_bytes(bus, te_handle);
1145        let text_len = text_bytes.len();
1146        let clamped = offset.min(text_len);
1147        let starts = Self::te_line_starts(bus, te_handle);
1148        let line_index = Self::te_char_to_line_index(bus, te_handle, clamped);
1149        let line_start = starts.get(line_index).copied().unwrap_or(0);
1150        let line_end = starts.get(line_index + 1).copied().unwrap_or(text_len);
1151        let on_break = clamped != 0
1152            && clamped == text_len
1153            && text_bytes.get(clamped - 1).copied() == Some(b'\r');
1154        let styled_runs = if Self::te_is_styled_record(bus, te_ptr) {
1155            self.te_style_runs(bus, te_handle, text_len)
1156        } else {
1157            Vec::new()
1158        };
1159        let uses_styled_runs = !styled_runs.is_empty() && Self::te_is_styled_record(bus, te_ptr);
1160        let (font, _, size, _, _, _) = self.te_primary_style(bus, te_handle);
1161        let rect_width = dest_rect.3.saturating_sub(dest_rect.1);
1162        // Per Inside Macintosh: Text 1993, lines 7320-7323:
1163        //   teJustLeft   =  0 (flush left — system default for LTR)
1164        //   teJustCenter =  1 (centered)
1165        //   teJustRight  = -1 (flush right)
1166        //   teForceLeft  = -2 (force flush left)
1167        let left_offset = match just {
1168            1 | -1 => {
1169                let line_width = if on_break {
1170                    rect_width.saturating_sub(1)
1171                } else {
1172                    let line_width = if uses_styled_runs {
1173                        self.te_measure_text_width_styled(
1174                            &styled_runs,
1175                            &text_bytes,
1176                            line_start,
1177                            line_end,
1178                        )
1179                    } else {
1180                        self.te_measure_text_width(
1181                            font,
1182                            Self::font_lookup_size(size),
1183                            &text_bytes,
1184                            line_start,
1185                            line_end,
1186                        )
1187                    };
1188                    rect_width.saturating_sub(line_width).saturating_sub(1)
1189                };
1190                if just == 1 {
1191                    line_width / 2
1192                } else {
1193                    line_width
1194                }
1195            }
1196            _ => Self::TE_LINE_LEFT_INSET,
1197        };
1198        let x = if on_break {
1199            dest_rect.1.saturating_add(left_offset)
1200        } else {
1201            dest_rect
1202                .1
1203                .saturating_add(left_offset)
1204                .saturating_add(if uses_styled_runs {
1205                    self.te_measure_text_width_styled(
1206                        &styled_runs,
1207                        &text_bytes,
1208                        line_start,
1209                        clamped,
1210                    )
1211                } else {
1212                    self.te_measure_text_width(
1213                        font,
1214                        Self::font_lookup_size(size),
1215                        &text_bytes,
1216                        line_start,
1217                        clamped,
1218                    )
1219                })
1220        };
1221
1222        let mut top = dest_rect.0;
1223        for current_line in 0..line_index {
1224            top = top.saturating_add(Self::te_height_for_line(bus, te_handle, current_line));
1225        }
1226        if on_break {
1227            top = top.saturating_add(Self::te_height_for_line(bus, te_handle, line_index));
1228        }
1229
1230        (top, x)
1231    }
1232
1233    /// Inverse of `te_char_to_point`: locate the character offset whose
1234    /// glyph cell contains `point` (vertical, horizontal). Used by
1235    /// TEGetOffset ($A83C) per Inside Macintosh Volume V, V-172.
1236    fn te_point_to_char(&self, bus: &MacMemoryBus, te_handle: u32, point: (i16, i16)) -> i16 {
1237        let te_ptr = Self::te_record_ptr(bus, te_handle);
1238        if te_ptr == 0 {
1239            return 0;
1240        }
1241
1242        let text_bytes = Self::te_text_bytes(bus, te_handle);
1243        let text_len = text_bytes.len();
1244        let starts = Self::te_line_starts(bus, te_handle);
1245        let n_lines = starts.len().saturating_sub(1);
1246        if text_len == 0 || n_lines == 0 {
1247            return 0;
1248        }
1249
1250        let dest_rect = Self::te_read_rect(bus, te_ptr + Self::TE_DEST_RECT_OFFSET);
1251        let (point_v, point_h) = point;
1252
1253        if point_v < dest_rect.0 {
1254            return 0;
1255        }
1256
1257        let mut top = dest_rect.0;
1258        let mut line_index = n_lines.saturating_sub(1);
1259        let mut found_line = false;
1260        for current_line in 0..n_lines {
1261            let height = Self::te_height_for_line(bus, te_handle, current_line);
1262            let bottom = top.saturating_add(height);
1263            if point_v < bottom {
1264                line_index = current_line;
1265                found_line = true;
1266                break;
1267            }
1268            top = bottom;
1269        }
1270        if !found_line {
1271            return text_len.min(i16::MAX as usize) as i16;
1272        }
1273
1274        let line_start = starts[line_index];
1275        let line_end = starts.get(line_index + 1).copied().unwrap_or(text_len);
1276
1277        let just = bus.read_word(te_ptr + Self::TE_JUST_OFFSET) as i16;
1278        let styled_runs = if Self::te_is_styled_record(bus, te_ptr) {
1279            self.te_style_runs(bus, te_handle, text_len)
1280        } else {
1281            Vec::new()
1282        };
1283        let uses_styled_runs = !styled_runs.is_empty() && Self::te_is_styled_record(bus, te_ptr);
1284        let (font, _, size, _, _, _) = self.te_primary_style(bus, te_handle);
1285        let rect_width = dest_rect.3.saturating_sub(dest_rect.1);
1286        let left_offset = match just {
1287            1 | -1 => {
1288                let measured_width = if uses_styled_runs {
1289                    self.te_measure_text_width_styled(
1290                        &styled_runs,
1291                        &text_bytes,
1292                        line_start,
1293                        line_end,
1294                    )
1295                } else {
1296                    self.te_measure_text_width(
1297                        font,
1298                        Self::font_lookup_size(size),
1299                        &text_bytes,
1300                        line_start,
1301                        line_end,
1302                    )
1303                };
1304                let line_width = rect_width.saturating_sub(measured_width).saturating_sub(1);
1305                if just == 1 {
1306                    line_width / 2
1307                } else {
1308                    line_width
1309                }
1310            }
1311            _ => Self::TE_LINE_LEFT_INSET,
1312        };
1313
1314        let mut x_cursor = dest_rect.1.saturating_add(left_offset);
1315        for (offset, byte) in text_bytes[line_start..line_end].iter().enumerate() {
1316            let index = line_start + offset;
1317            if matches!(*byte, b'\r' | b'\n') {
1318                return index.min(i16::MAX as usize) as i16;
1319            }
1320            let advance = if uses_styled_runs {
1321                let style = Self::te_style_at_offset(&styled_runs, index);
1322                self.te_char_width(style.font, style.size, *byte)
1323            } else {
1324                self.te_char_width(font, Self::font_lookup_size(size), *byte)
1325            };
1326            // Hit-test against the glyph midpoint so a click on the right
1327            // half of a character lands on the next offset, matching the
1328            // semantics described in Inside Macintosh Volume V, V-172.
1329            let mid = x_cursor.saturating_add(advance / 2);
1330            if point_h < mid {
1331                return index.min(i16::MAX as usize) as i16;
1332            }
1333            x_cursor = x_cursor.saturating_add(advance);
1334        }
1335        line_end.min(i16::MAX as usize) as i16
1336    }
1337
1338    fn te_scroll_contents(
1339        &mut self,
1340        cpu: &mut impl CpuOps,
1341        bus: &mut MacMemoryBus,
1342        te_handle: u32,
1343        dh: i16,
1344        dv: i16,
1345    ) {
1346        if dh == 0 && dv == 0 {
1347            return;
1348        }
1349
1350        let te_ptr = Self::te_record_ptr(bus, te_handle);
1351        if te_ptr == 0 {
1352            return;
1353        }
1354
1355        let mut dest_rect = Self::te_read_rect(bus, te_ptr + Self::TE_DEST_RECT_OFFSET);
1356        dest_rect.0 = dest_rect.0.saturating_add(dv);
1357        dest_rect.1 = dest_rect.1.saturating_add(dh);
1358        dest_rect.2 = dest_rect.2.saturating_add(dv);
1359        dest_rect.3 = dest_rect.3.saturating_add(dh);
1360        Self::te_write_rect_words(bus, te_ptr + Self::TE_DEST_RECT_OFFSET, dest_rect);
1361        self.draw_te_contents(cpu, bus, te_handle);
1362        // Refresh rendered_pixels so redraw_chrome restores the scrolled state rather
1363        // than the pre-scroll snapshot captured at dialog creation.
1364        // Text 1993, 2-89 (TEScroll/TEPinScroll modify destRect and redraw).
1365        if let Some(ref tracking) = self.dialog_tracking {
1366            if !tracking.game_managed && tracking.rendered_pixels_final {
1367                let bounds = tracking.bounds;
1368                let new_pixels = self.save_dialog_pixels(bus, bounds);
1369                self.dialog_tracking.as_mut().unwrap().rendered_pixels = new_pixels;
1370            }
1371        }
1372    }
1373
1374    fn te_getdelta(sel_start: i16, sel_stop: i16, view_start: i16, view_stop: i16) -> i16 {
1375        if sel_start < view_start {
1376            view_start.saturating_sub(sel_start)
1377        } else if sel_stop > view_stop {
1378            if sel_stop.saturating_sub(sel_start) > view_stop.saturating_sub(view_start) {
1379                view_start.saturating_sub(sel_start)
1380            } else {
1381                view_stop.saturating_sub(sel_stop)
1382            }
1383        } else {
1384            0
1385        }
1386    }
1387
1388    fn te_height_for_line(bus: &MacMemoryBus, te_handle: u32, line_index: usize) -> i16 {
1389        let te_ptr = Self::te_record_ptr(bus, te_handle);
1390        if te_ptr == 0 {
1391            return 0;
1392        }
1393
1394        if Self::te_is_styled_record(bus, te_ptr) {
1395            let style_handle = Self::te_style_handle(bus, te_handle);
1396            if style_handle != 0 {
1397                let style_ptr = bus.read_long(style_handle);
1398                if style_ptr != 0 {
1399                    let lh_handle = bus.read_long(style_ptr + Self::TE_STYLE_LH_TABLE_OFFSET);
1400                    let lh_ptr = if lh_handle != 0 {
1401                        bus.read_long(lh_handle)
1402                    } else {
1403                        0
1404                    };
1405                    if lh_ptr != 0 {
1406                        return bus.read_word(
1407                            lh_ptr
1408                                + (line_index as u32 * Self::LH_ELEMENT_SIZE)
1409                                + Self::LH_ELEMENT_HEIGHT_OFFSET,
1410                        ) as i16;
1411                    }
1412                }
1413            }
1414        }
1415
1416        bus.read_word(te_ptr + Self::TE_LINE_HEIGHT_OFFSET) as i16
1417    }
1418
1419    fn te_ascent_for_line(bus: &MacMemoryBus, te_handle: u32, line_index: usize) -> i16 {
1420        let te_ptr = Self::te_record_ptr(bus, te_handle);
1421        if te_ptr == 0 {
1422            return 0;
1423        }
1424
1425        if Self::te_is_styled_record(bus, te_ptr) {
1426            let style_handle = Self::te_style_handle(bus, te_handle);
1427            if style_handle != 0 {
1428                let style_ptr = bus.read_long(style_handle);
1429                if style_ptr != 0 {
1430                    let lh_handle = bus.read_long(style_ptr + Self::TE_STYLE_LH_TABLE_OFFSET);
1431                    let lh_ptr = if lh_handle != 0 {
1432                        bus.read_long(lh_handle)
1433                    } else {
1434                        0
1435                    };
1436                    if lh_ptr != 0 {
1437                        return bus.read_word(
1438                            lh_ptr
1439                                + (line_index as u32 * Self::LH_ELEMENT_SIZE)
1440                                + Self::LH_ELEMENT_ASCENT_OFFSET,
1441                        ) as i16;
1442                    }
1443                }
1444            }
1445        }
1446
1447        bus.read_word(te_ptr + Self::TE_FONT_ASCENT_OFFSET) as i16
1448    }
1449
1450    fn te_primary_style(
1451        &self,
1452        bus: &MacMemoryBus,
1453        te_handle: u32,
1454    ) -> (i16, i16, i16, (u16, u16, u16), i16, i16) {
1455        let te_ptr = Self::te_record_ptr(bus, te_handle);
1456        if te_ptr == 0 {
1457            let size = Self::font_lookup_size(self.tx_size);
1458            let metrics = get_font_metrics(self.tx_font, size);
1459            return (
1460                self.tx_font,
1461                self.tx_face,
1462                size,
1463                self.fg_color,
1464                metrics.ascent + metrics.descent + metrics.leading,
1465                metrics.ascent,
1466            );
1467        }
1468
1469        if Self::te_is_styled_record(bus, te_ptr) {
1470            let style_handle = bus.read_long(te_ptr + Self::TE_TX_FONT_OFFSET);
1471            let style_ptr = if style_handle != 0 {
1472                bus.read_long(style_handle)
1473            } else {
1474                0
1475            };
1476            if style_ptr != 0 {
1477                let style_table_handle =
1478                    bus.read_long(style_ptr + Self::TE_STYLE_STYLE_TABLE_OFFSET);
1479                let style_table_ptr = if style_table_handle != 0 {
1480                    bus.read_long(style_table_handle)
1481                } else {
1482                    0
1483                };
1484                if style_table_ptr != 0 {
1485                    return (
1486                        bus.read_word(style_table_ptr + Self::ST_ELEMENT_FONT_OFFSET) as i16,
1487                        i16::from(bus.read_byte(style_table_ptr + Self::ST_ELEMENT_FACE_OFFSET)),
1488                        bus.read_word(style_table_ptr + Self::ST_ELEMENT_SIZE_OFFSET) as i16,
1489                        (
1490                            bus.read_word(style_table_ptr + Self::ST_ELEMENT_COLOR_OFFSET),
1491                            bus.read_word(style_table_ptr + Self::ST_ELEMENT_COLOR_OFFSET + 2),
1492                            bus.read_word(style_table_ptr + Self::ST_ELEMENT_COLOR_OFFSET + 4),
1493                        ),
1494                        bus.read_word(style_table_ptr + Self::ST_ELEMENT_HEIGHT_OFFSET) as i16,
1495                        bus.read_word(style_table_ptr + Self::ST_ELEMENT_ASCENT_OFFSET) as i16,
1496                    );
1497                }
1498            }
1499        }
1500
1501        let tx_font = bus.read_word(te_ptr + Self::TE_TX_FONT_OFFSET) as i16;
1502        let tx_face = i16::from(bus.read_byte(te_ptr + Self::TE_TX_FACE_OFFSET));
1503        let tx_size =
1504            Self::font_lookup_size(bus.read_word(te_ptr + Self::TE_TX_SIZE_OFFSET) as i16);
1505        let metrics = get_font_metrics(tx_font, tx_size);
1506        (
1507            tx_font,
1508            tx_face,
1509            tx_size,
1510            self.fg_color,
1511            metrics.ascent + metrics.descent + metrics.leading,
1512            metrics.ascent,
1513        )
1514    }
1515
1516    fn te_resolved_style_from_parts(
1517        font: i16,
1518        face: i16,
1519        size: i16,
1520        color: (u16, u16, u16),
1521        line_height: i16,
1522        ascent: i16,
1523    ) -> TeResolvedStyle {
1524        let resolved_size = Self::font_lookup_size(size);
1525        let metrics = get_font_metrics(font, resolved_size);
1526        let fallback_height = metrics.ascent + metrics.descent + metrics.leading;
1527        TeResolvedStyle {
1528            font,
1529            face,
1530            size: resolved_size,
1531            color,
1532            line_height: if line_height > 0 {
1533                line_height
1534            } else {
1535                fallback_height
1536            },
1537            ascent: if ascent > 0 { ascent } else { metrics.ascent },
1538        }
1539    }
1540
1541    fn te_primary_resolved_style(&self, bus: &MacMemoryBus, te_handle: u32) -> TeResolvedStyle {
1542        let (font, face, size, color, line_height, ascent) = self.te_primary_style(bus, te_handle);
1543        Self::te_resolved_style_from_parts(font, face, size, color, line_height, ascent)
1544    }
1545
1546    fn te_style_from_table_element(bus: &MacMemoryBus, style_ptr: u32) -> TeResolvedStyle {
1547        Self::te_resolved_style_from_parts(
1548            bus.read_word(style_ptr + Self::ST_ELEMENT_FONT_OFFSET) as i16,
1549            i16::from(bus.read_byte(style_ptr + Self::ST_ELEMENT_FACE_OFFSET)),
1550            bus.read_word(style_ptr + Self::ST_ELEMENT_SIZE_OFFSET) as i16,
1551            (
1552                bus.read_word(style_ptr + Self::ST_ELEMENT_COLOR_OFFSET),
1553                bus.read_word(style_ptr + Self::ST_ELEMENT_COLOR_OFFSET + 2),
1554                bus.read_word(style_ptr + Self::ST_ELEMENT_COLOR_OFFSET + 4),
1555            ),
1556            bus.read_word(style_ptr + Self::ST_ELEMENT_HEIGHT_OFFSET) as i16,
1557            bus.read_word(style_ptr + Self::ST_ELEMENT_ASCENT_OFFSET) as i16,
1558        )
1559    }
1560
1561    fn te_style_from_scrap_element(bus: &MacMemoryBus, scrap_style_ptr: u32) -> TeResolvedStyle {
1562        Self::te_resolved_style_from_parts(
1563            bus.read_word(scrap_style_ptr + Self::SCRAP_STYLE_FONT_OFFSET) as i16,
1564            i16::from(bus.read_byte(scrap_style_ptr + Self::SCRAP_STYLE_FACE_OFFSET)),
1565            bus.read_word(scrap_style_ptr + Self::SCRAP_STYLE_SIZE_OFFSET) as i16,
1566            (
1567                bus.read_word(scrap_style_ptr + Self::SCRAP_STYLE_COLOR_OFFSET),
1568                bus.read_word(scrap_style_ptr + Self::SCRAP_STYLE_COLOR_OFFSET + 2),
1569                bus.read_word(scrap_style_ptr + Self::SCRAP_STYLE_COLOR_OFFSET + 4),
1570            ),
1571            bus.read_word(scrap_style_ptr + Self::SCRAP_STYLE_HEIGHT_OFFSET) as i16,
1572            bus.read_word(scrap_style_ptr + Self::SCRAP_STYLE_ASCENT_OFFSET) as i16,
1573        )
1574    }
1575
1576    fn te_write_style_table_element(
1577        bus: &mut MacMemoryBus,
1578        style_ptr: u32,
1579        style: TeResolvedStyle,
1580    ) {
1581        bus.write_word(style_ptr + Self::ST_ELEMENT_REFCOUNT_OFFSET, 1);
1582        bus.write_word(
1583            style_ptr + Self::ST_ELEMENT_HEIGHT_OFFSET,
1584            style.line_height as u16,
1585        );
1586        bus.write_word(
1587            style_ptr + Self::ST_ELEMENT_ASCENT_OFFSET,
1588            style.ascent as u16,
1589        );
1590        bus.write_word(style_ptr + Self::ST_ELEMENT_FONT_OFFSET, style.font as u16);
1591        bus.write_byte(style_ptr + Self::ST_ELEMENT_FACE_OFFSET, style.face as u8);
1592        bus.write_word(style_ptr + Self::ST_ELEMENT_SIZE_OFFSET, style.size as u16);
1593        bus.write_word(style_ptr + Self::ST_ELEMENT_COLOR_OFFSET, style.color.0);
1594        bus.write_word(style_ptr + Self::ST_ELEMENT_COLOR_OFFSET + 2, style.color.1);
1595        bus.write_word(style_ptr + Self::ST_ELEMENT_COLOR_OFFSET + 4, style.color.2);
1596    }
1597
1598    fn te_set_null_style(
1599        bus: &mut MacMemoryBus,
1600        te_handle: u32,
1601        mode: u16,
1602        text_style_ptr: u32,
1603    ) -> bool {
1604        let te_ptr = Self::te_record_ptr(bus, te_handle);
1605        if !Self::te_is_styled_record(bus, te_ptr) || text_style_ptr == 0 {
1606            return false;
1607        }
1608        let style_handle = Self::te_style_handle(bus, te_handle);
1609        let style_ptr = if style_handle != 0 {
1610            bus.read_long(style_handle)
1611        } else {
1612            0
1613        };
1614        let null_style_handle = if style_ptr != 0 {
1615            bus.read_long(style_ptr + Self::TE_STYLE_NULL_STYLE_OFFSET)
1616        } else {
1617            0
1618        };
1619        let null_style_ptr = if null_style_handle != 0 {
1620            bus.read_long(null_style_handle)
1621        } else {
1622            0
1623        };
1624        let null_scrap_handle = if null_style_ptr != 0 {
1625            bus.read_long(null_style_ptr + Self::NULL_STYLE_SCRAP_OFFSET)
1626        } else {
1627            0
1628        };
1629        let scrap_ptr = if null_scrap_handle != 0 {
1630            bus.read_long(null_scrap_handle)
1631        } else {
1632            0
1633        };
1634        if scrap_ptr == 0 {
1635            return false;
1636        }
1637
1638        let element = scrap_ptr + Self::SCRAP_STYLE_TAB_OFFSET;
1639        bus.write_word(scrap_ptr + Self::SCRAP_N_STYLES_OFFSET, 1);
1640        if (mode & 0x0001) != 0 {
1641            bus.write_word(
1642                element + Self::SCRAP_STYLE_FONT_OFFSET,
1643                bus.read_word(text_style_ptr),
1644            );
1645        }
1646        if (mode & 0x0002) != 0 {
1647            bus.write_byte(
1648                element + Self::SCRAP_STYLE_FACE_OFFSET,
1649                bus.read_byte(text_style_ptr + 2),
1650            );
1651        }
1652        if (mode & 0x0004) != 0 {
1653            bus.write_word(
1654                element + Self::SCRAP_STYLE_SIZE_OFFSET,
1655                bus.read_word(text_style_ptr + 4),
1656            );
1657        }
1658        if (mode & 0x0008) != 0 {
1659            for offset in [0u32, 2, 4] {
1660                bus.write_word(
1661                    element + Self::SCRAP_STYLE_COLOR_OFFSET + offset,
1662                    bus.read_word(text_style_ptr + 6 + offset),
1663                );
1664            }
1665        }
1666        let font = bus.read_word(element + Self::SCRAP_STYLE_FONT_OFFSET) as i16;
1667        let size =
1668            Self::font_lookup_size(bus.read_word(element + Self::SCRAP_STYLE_SIZE_OFFSET) as i16);
1669        let metrics = get_font_metrics(font, size);
1670        bus.write_word(
1671            element + Self::SCRAP_STYLE_HEIGHT_OFFSET,
1672            (metrics.ascent + metrics.descent + metrics.leading) as u16,
1673        );
1674        bus.write_word(
1675            element + Self::SCRAP_STYLE_ASCENT_OFFSET,
1676            metrics.ascent as u16,
1677        );
1678        true
1679    }
1680
1681    fn te_null_style_scrap_handle(bus: &MacMemoryBus, te_handle: u32) -> u32 {
1682        let style_handle = Self::te_style_handle(bus, te_handle);
1683        let style_ptr = if style_handle != 0 {
1684            bus.read_long(style_handle)
1685        } else {
1686            0
1687        };
1688        let null_style_handle = if style_ptr != 0 {
1689            bus.read_long(style_ptr + Self::TE_STYLE_NULL_STYLE_OFFSET)
1690        } else {
1691            0
1692        };
1693        let null_style_ptr = if null_style_handle != 0 {
1694            bus.read_long(null_style_handle)
1695        } else {
1696            0
1697        };
1698        if null_style_ptr != 0 {
1699            bus.read_long(null_style_ptr + Self::NULL_STYLE_SCRAP_OFFSET)
1700        } else {
1701            0
1702        }
1703    }
1704
1705    fn te_style_runs(
1706        &self,
1707        bus: &MacMemoryBus,
1708        te_handle: u32,
1709        text_len: usize,
1710    ) -> Vec<TeStyleRun> {
1711        let fallback = self.te_primary_resolved_style(bus, te_handle);
1712        let te_ptr = Self::te_record_ptr(bus, te_handle);
1713        if !Self::te_is_styled_record(bus, te_ptr) {
1714            return vec![TeStyleRun {
1715                start: 0,
1716                style_index: 0,
1717                style: fallback,
1718            }];
1719        }
1720
1721        let style_handle = Self::te_style_handle(bus, te_handle);
1722        let style_ptr = if style_handle != 0 {
1723            bus.read_long(style_handle)
1724        } else {
1725            0
1726        };
1727        if style_ptr == 0 {
1728            return vec![TeStyleRun {
1729                start: 0,
1730                style_index: 0,
1731                style: fallback,
1732            }];
1733        }
1734
1735        let style_table_handle = bus.read_long(style_ptr + Self::TE_STYLE_STYLE_TABLE_OFFSET);
1736        let style_table_ptr = if style_table_handle != 0 {
1737            bus.read_long(style_table_handle)
1738        } else {
1739            0
1740        };
1741        if style_table_ptr == 0 {
1742            return vec![TeStyleRun {
1743                start: 0,
1744                style_index: 0,
1745                style: fallback,
1746            }];
1747        }
1748
1749        let n_runs = bus.read_word(style_ptr + Self::TE_STYLE_N_RUNS_OFFSET) as usize;
1750        let n_styles = bus.read_word(style_ptr + Self::TE_STYLE_N_STYLES_OFFSET) as usize;
1751        let style_record_size = bus.get_alloc_size(style_ptr).unwrap_or(0);
1752        let style_table_size = bus.get_alloc_size(style_table_ptr).unwrap_or(0);
1753        let max_runs = style_record_size
1754            .saturating_sub(Self::TE_STYLE_RUNS_OFFSET)
1755            .checked_div(4)
1756            .unwrap_or(0) as usize;
1757        let max_styles = style_table_size
1758            .checked_div(Self::ST_ELEMENT_SIZE)
1759            .unwrap_or(0) as usize;
1760        let run_count = n_runs.min(max_runs);
1761        let style_count = n_styles.min(max_styles);
1762        if run_count == 0 || style_count == 0 {
1763            return vec![TeStyleRun {
1764                start: 0,
1765                style_index: 0,
1766                style: fallback,
1767            }];
1768        }
1769
1770        let mut runs = Vec::with_capacity(run_count);
1771        for run_index in 0..run_count {
1772            let run_ptr = style_ptr + Self::TE_STYLE_RUNS_OFFSET + (run_index as u32 * 4);
1773            let style_index_word = bus.read_word(run_ptr + 2);
1774            if style_index_word == 0xFFFF {
1775                break;
1776            }
1777            let style_index = style_index_word as usize;
1778            if style_index >= style_count {
1779                continue;
1780            }
1781            let style_element_ptr = style_table_ptr + (style_index as u32 * Self::ST_ELEMENT_SIZE);
1782            runs.push(TeStyleRun {
1783                start: (bus.read_word(run_ptr) as usize).min(text_len),
1784                style_index,
1785                style: Self::te_style_from_table_element(bus, style_element_ptr),
1786            });
1787        }
1788
1789        if runs.is_empty() {
1790            return vec![TeStyleRun {
1791                start: 0,
1792                style_index: 0,
1793                style: fallback,
1794            }];
1795        }
1796
1797        runs.sort_by_key(|run| run.start);
1798        let mut folded: Vec<TeStyleRun> = Vec::with_capacity(runs.len() + 1);
1799        for run in runs {
1800            if let Some(last) = folded.last_mut() {
1801                if last.start == run.start {
1802                    *last = run;
1803                    continue;
1804                }
1805                if last.style == run.style {
1806                    continue;
1807                }
1808            }
1809            folded.push(run);
1810        }
1811
1812        if folded.first().is_none_or(|run| run.start != 0) {
1813            folded.insert(
1814                0,
1815                TeStyleRun {
1816                    start: 0,
1817                    style_index: 0,
1818                    style: fallback,
1819                },
1820            );
1821        }
1822        folded
1823    }
1824
1825    fn te_style_at_offset(runs: &[TeStyleRun], offset: usize) -> TeResolvedStyle {
1826        let mut style = runs
1827            .first()
1828            .map(|run| run.style)
1829            .unwrap_or_else(|| Self::te_resolved_style_from_parts(0, 0, 0, (0, 0, 0), 0, 0));
1830        for run in runs {
1831            if run.start > offset {
1832                break;
1833            }
1834            style = run.style;
1835        }
1836        style
1837    }
1838
1839    fn te_measure_text_width_styled(
1840        &self,
1841        runs: &[TeStyleRun],
1842        text_bytes: &[u8],
1843        start: usize,
1844        end: usize,
1845    ) -> i16 {
1846        let start = start.min(text_bytes.len());
1847        let end = end.min(text_bytes.len());
1848        let mut width = 0i16;
1849        for (offset, &byte) in text_bytes[start..end].iter().enumerate() {
1850            let style = Self::te_style_at_offset(runs, start + offset);
1851            width = width.saturating_add(self.te_char_width(style.font, style.size, byte));
1852        }
1853        width
1854    }
1855
1856    fn te_next_break_styled(
1857        &self,
1858        runs: &[TeStyleRun],
1859        text_bytes: &[u8],
1860        offset: usize,
1861        max_width: i16,
1862    ) -> usize {
1863        let len = text_bytes.len();
1864        if offset >= len {
1865            return len;
1866        }
1867
1868        let mut width = 0i16;
1869        let mut index = offset;
1870        while width <= max_width && index < len && !matches!(text_bytes[index], b'\r' | b'\n') {
1871            let style = Self::te_style_at_offset(runs, index);
1872            width =
1873                width.saturating_add(self.te_char_width(style.font, style.size, text_bytes[index]));
1874            index += 1;
1875        }
1876
1877        let mut next = if width > max_width {
1878            let max_index = index.saturating_sub(1);
1879            if text_bytes[max_index] == b' ' {
1880                let mut scan = index;
1881                while scan < len && text_bytes[scan] == b' ' {
1882                    scan += 1;
1883                }
1884                scan
1885            } else {
1886                let mut scan = max_index;
1887                while scan > offset && !Self::te_word_break_byte(text_bytes[scan - 1]) {
1888                    scan -= 1;
1889                }
1890                if scan == offset {
1891                    max_index
1892                } else {
1893                    scan
1894                }
1895            }
1896        } else if index == len {
1897            len
1898        } else {
1899            index + 1
1900        };
1901
1902        if next == offset && offset < len {
1903            next += 1;
1904        }
1905        next
1906    }
1907
1908    fn te_wrap_lines_styled(
1909        &self,
1910        runs: &[TeStyleRun],
1911        text_bytes: &[u8],
1912        box_width: i16,
1913    ) -> Vec<(usize, usize)> {
1914        let mut lines = Vec::new();
1915        let mut line_start = 0usize;
1916        while line_start < text_bytes.len() {
1917            let next = self.te_next_break_styled(runs, text_bytes, line_start, box_width);
1918            lines.push((line_start, next.min(text_bytes.len())));
1919            line_start = next;
1920        }
1921        lines
1922    }
1923
1924    fn te_line_metrics_for_range(runs: &[TeStyleRun], start: usize, end: usize) -> (i16, i16) {
1925        if runs.is_empty() {
1926            return (0, 0);
1927        }
1928
1929        let mut line_height = 0i16;
1930        let mut ascent = 0i16;
1931        if start >= end {
1932            let style = Self::te_style_at_offset(runs, start);
1933            return (style.line_height, style.ascent);
1934        }
1935
1936        for offset in start..end {
1937            let style = Self::te_style_at_offset(runs, offset);
1938            line_height = line_height.max(style.line_height);
1939            ascent = ascent.max(style.ascent);
1940        }
1941        (line_height.max(1), ascent.max(0))
1942    }
1943
1944    fn te_update_styled_run_sentinel(bus: &mut MacMemoryBus, te_handle: u32, text_len: usize) {
1945        let style_handle = Self::te_style_handle(bus, te_handle);
1946        if style_handle == 0 {
1947            return;
1948        }
1949        let style_ptr = bus.read_long(style_handle);
1950        if style_ptr == 0 {
1951            return;
1952        }
1953        let n_runs = bus.read_word(style_ptr + Self::TE_STYLE_N_RUNS_OFFSET) as usize;
1954        let needed = Self::TE_STYLE_RUNS_OFFSET + ((n_runs as u32 + 1) * 4);
1955        let style_ptr = Self::ensure_handle_capacity(bus, style_handle, needed);
1956        if style_ptr == 0 {
1957            return;
1958        }
1959        let sentinel = style_ptr + Self::TE_STYLE_RUNS_OFFSET + (n_runs as u32 * 4);
1960        bus.write_word(
1961            sentinel,
1962            text_len.saturating_add(1).min(u16::MAX as usize) as u16,
1963        );
1964        bus.write_word(sentinel + 2, 0xFFFF);
1965    }
1966
1967    fn te_apply_style_scrap_to_range(
1968        &mut self,
1969        bus: &mut MacMemoryBus,
1970        te_handle: u32,
1971        style_scrap: u32,
1972        range_start: usize,
1973        range_len: usize,
1974    ) -> bool {
1975        let te_ptr = Self::te_record_ptr(bus, te_handle);
1976        if !Self::te_is_styled_record(bus, te_ptr) || style_scrap == 0 || range_len == 0 {
1977            return false;
1978        }
1979
1980        let text_len = Self::te_text_length(bus, te_handle);
1981        let range_start = range_start.min(text_len);
1982        let range_end = range_start.saturating_add(range_len).min(text_len);
1983        if range_start >= range_end {
1984            return false;
1985        }
1986
1987        let scrap_ptr = bus.read_long(style_scrap);
1988        if scrap_ptr == 0 {
1989            return false;
1990        }
1991        let scrap_size = bus.get_alloc_size(scrap_ptr).unwrap_or(0);
1992        if scrap_size < Self::SCRAP_STYLE_TAB_OFFSET {
1993            return false;
1994        }
1995        let style_count = (bus.read_word(scrap_ptr + Self::SCRAP_N_STYLES_OFFSET) as usize).min(
1996            scrap_size
1997                .saturating_sub(Self::SCRAP_STYLE_TAB_OFFSET)
1998                .checked_div(Self::SCRAP_STYLE_ELEMENT_SIZE)
1999                .unwrap_or(0) as usize,
2000        );
2001        if style_count == 0 {
2002            return false;
2003        }
2004        let existing_runs = self.te_style_runs(bus, te_handle, text_len);
2005        let mut candidates: Vec<(usize, u8, TeResolvedStyle)> =
2006            Vec::with_capacity(existing_runs.len() + style_count + 2);
2007        for run in &existing_runs {
2008            if run.start < range_start || run.start > range_end {
2009                candidates.push((run.start.min(text_len), 0, run.style));
2010            }
2011        }
2012
2013        for style_index in 0..style_count {
2014            let scrap_style_ptr = scrap_ptr
2015                + Self::SCRAP_STYLE_TAB_OFFSET
2016                + (style_index as u32 * Self::SCRAP_STYLE_ELEMENT_SIZE);
2017            let local_start =
2018                bus.read_long(scrap_style_ptr + Self::SCRAP_STYLE_START_CHAR_OFFSET) as usize;
2019            let run_start = range_start.saturating_add(local_start.min(range_end - range_start));
2020            candidates.push((
2021                run_start,
2022                1,
2023                Self::te_style_from_scrap_element(bus, scrap_style_ptr),
2024            ));
2025        }
2026
2027        if range_end < text_len {
2028            candidates.push((
2029                range_end,
2030                2,
2031                Self::te_style_at_offset(&existing_runs, range_end),
2032            ));
2033        }
2034        if !candidates.iter().any(|(start, _, _)| *start == 0) {
2035            candidates.push((0, 0, Self::te_style_at_offset(&existing_runs, 0)));
2036        }
2037
2038        candidates.sort_by_key(|(start, order, _)| (*start, *order));
2039        let mut merged: Vec<(usize, TeResolvedStyle)> = Vec::with_capacity(candidates.len());
2040        for (start, _, style) in candidates {
2041            let start = start.min(text_len);
2042            if let Some((last_start, last_style)) = merged.last_mut() {
2043                if *last_start == start {
2044                    *last_style = style;
2045                    continue;
2046                }
2047                if *last_style == style {
2048                    continue;
2049                }
2050            }
2051            merged.push((start, style));
2052        }
2053
2054        if merged.is_empty() {
2055            merged.push((0, self.te_primary_resolved_style(bus, te_handle)));
2056        }
2057        if merged[0].0 != 0 {
2058            merged.insert(0, (0, Self::te_style_at_offset(&existing_runs, 0)));
2059        }
2060
2061        let run_count = merged.len().min(u16::MAX as usize);
2062        let style_handle = Self::te_style_handle(bus, te_handle);
2063        if style_handle == 0 {
2064            return false;
2065        }
2066        let style_ptr = Self::ensure_handle_capacity(
2067            bus,
2068            style_handle,
2069            Self::TE_STYLE_RUNS_OFFSET + ((run_count as u32 + 1) * 4),
2070        );
2071        if style_ptr == 0 {
2072            return false;
2073        }
2074
2075        let mut style_table_handle = bus.read_long(style_ptr + Self::TE_STYLE_STYLE_TABLE_OFFSET);
2076        if style_table_handle == 0 {
2077            style_table_handle = Self::allocate_handle_with_data(bus, 0);
2078            bus.write_long(
2079                style_ptr + Self::TE_STYLE_STYLE_TABLE_OFFSET,
2080                style_table_handle,
2081            );
2082        }
2083        let style_table_ptr = Self::ensure_handle_capacity(
2084            bus,
2085            style_table_handle,
2086            (run_count as u32) * Self::ST_ELEMENT_SIZE,
2087        );
2088        if style_table_ptr == 0 {
2089            return false;
2090        }
2091
2092        bus.write_word(style_ptr + Self::TE_STYLE_N_RUNS_OFFSET, run_count as u16);
2093        bus.write_word(style_ptr + Self::TE_STYLE_N_STYLES_OFFSET, run_count as u16);
2094        for (index, (start, style)) in merged.iter().take(run_count).enumerate() {
2095            let style_element_ptr = style_table_ptr + (index as u32 * Self::ST_ELEMENT_SIZE);
2096            Self::te_write_style_table_element(bus, style_element_ptr, *style);
2097            let run_ptr = style_ptr + Self::TE_STYLE_RUNS_OFFSET + (index as u32 * 4);
2098            bus.write_word(run_ptr, (*start).min(u16::MAX as usize) as u16);
2099            bus.write_word(run_ptr + 2, index.min(u16::MAX as usize) as u16);
2100        }
2101        Self::te_update_styled_run_sentinel(bus, te_handle, text_len);
2102        true
2103    }
2104
2105    #[allow(dead_code)]
2106    fn te_line_metrics(&self, bus: &MacMemoryBus, te_handle: u32) -> (i16, i16) {
2107        let te_ptr = Self::te_record_ptr(bus, te_handle);
2108        if te_ptr == 0 {
2109            let metrics = get_font_metrics(self.tx_font, Self::font_lookup_size(self.tx_size));
2110            return (
2111                metrics.ascent + metrics.descent + metrics.leading,
2112                metrics.ascent,
2113            );
2114        }
2115
2116        let line_height = bus.read_word(te_ptr + Self::TE_LINE_HEIGHT_OFFSET) as i16;
2117        let font_ascent = bus.read_word(te_ptr + Self::TE_FONT_ASCENT_OFFSET) as i16;
2118        if line_height > 0 && font_ascent >= 0 {
2119            return (line_height, font_ascent);
2120        }
2121
2122        let (_, _, _, _, resolved_line_height, resolved_ascent) =
2123            self.te_primary_style(bus, te_handle);
2124        (resolved_line_height, resolved_ascent)
2125    }
2126
2127    fn initialize_te_record(
2128        &mut self,
2129        bus: &mut MacMemoryBus,
2130        te_handle: u32,
2131        dest_rect: (i16, i16, i16, i16),
2132        view_rect: (i16, i16, i16, i16),
2133    ) {
2134        let te_ptr = Self::te_record_ptr(bus, te_handle);
2135        if te_ptr == 0 {
2136            return;
2137        }
2138
2139        let metrics = get_font_metrics(self.tx_font, Self::font_lookup_size(self.tx_size));
2140        let line_height = metrics.ascent + metrics.descent + metrics.leading;
2141        let h_text = bus.alloc(4);
2142        if h_text != 0 {
2143            bus.write_long(h_text, 0);
2144        }
2145
2146        Self::te_write_rect_words(bus, te_ptr + Self::TE_DEST_RECT_OFFSET, dest_rect);
2147        Self::te_write_rect_words(bus, te_ptr + Self::TE_VIEW_RECT_OFFSET, view_rect);
2148        Self::te_write_rect_words(bus, te_ptr + Self::TE_SEL_RECT_OFFSET, dest_rect);
2149        bus.write_word(te_ptr + Self::TE_LINE_HEIGHT_OFFSET, line_height as u16);
2150        bus.write_word(te_ptr + Self::TE_FONT_ASCENT_OFFSET, metrics.ascent as u16);
2151        bus.write_word(te_ptr + Self::TE_SEL_POINT_OFFSET, dest_rect.0 as u16);
2152        bus.write_word(te_ptr + Self::TE_SEL_POINT_OFFSET + 2, dest_rect.1 as u16);
2153        bus.write_word(te_ptr + Self::TE_SEL_START_OFFSET, 0);
2154        bus.write_word(te_ptr + Self::TE_SEL_END_OFFSET, 0);
2155        bus.write_long(te_ptr + Self::TE_CARET_TIME_OFFSET, self.tick_count);
2156        bus.write_word(te_ptr + Self::TE_CARET_STATE_OFFSET, 0);
2157        bus.write_word(te_ptr + Self::TE_LENGTH_OFFSET, 0);
2158        bus.write_long(te_ptr + Self::TE_HTEXT_OFFSET, h_text);
2159        bus.write_word(te_ptr + Self::TE_TX_FONT_OFFSET, self.tx_font as u16);
2160        bus.write_byte(te_ptr + Self::TE_TX_FACE_OFFSET, self.tx_face as u8);
2161        bus.write_word(te_ptr + Self::TE_TX_MODE_OFFSET, self.tx_mode as u16);
2162        bus.write_word(te_ptr + Self::TE_TX_SIZE_OFFSET, self.tx_size as u16);
2163        bus.write_long(te_ptr + Self::TE_IN_PORT_OFFSET, self.current_port);
2164        self.textedit_states.remove(&te_handle);
2165    }
2166
2167    fn initialize_styled_te_record(
2168        &mut self,
2169        bus: &mut MacMemoryBus,
2170        te_handle: u32,
2171        dest_rect: (i16, i16, i16, i16),
2172        view_rect: (i16, i16, i16, i16),
2173    ) {
2174        let te_ptr = Self::te_record_ptr(bus, te_handle);
2175        if te_ptr == 0 {
2176            return;
2177        }
2178
2179        let style_size = Self::font_lookup_size(self.tx_size);
2180        let metrics = get_font_metrics(self.tx_font, style_size);
2181        let line_height = metrics.ascent + metrics.descent + metrics.leading;
2182
2183        let h_text = Self::allocate_handle_with_data(bus, 0);
2184        let style_table = Self::allocate_handle_with_data(bus, Self::ST_ELEMENT_SIZE);
2185        let lh_table = Self::allocate_handle_with_data(bus, Self::LH_ELEMENT_SIZE);
2186        let null_scrap = Self::allocate_handle_with_data(bus, Self::STYLE_SCRAP_REC_SIZE);
2187        let null_style = Self::allocate_handle_with_data(bus, Self::NULL_STYLE_REC_SIZE);
2188        let style_handle = Self::allocate_handle_with_data(bus, 0x1C);
2189
2190        let style_table_ptr = if style_table != 0 {
2191            bus.read_long(style_table)
2192        } else {
2193            0
2194        };
2195        if style_table_ptr != 0 {
2196            bus.write_word(style_table_ptr + Self::ST_ELEMENT_REFCOUNT_OFFSET, 1);
2197            bus.write_word(
2198                style_table_ptr + Self::ST_ELEMENT_HEIGHT_OFFSET,
2199                line_height as u16,
2200            );
2201            bus.write_word(
2202                style_table_ptr + Self::ST_ELEMENT_ASCENT_OFFSET,
2203                metrics.ascent as u16,
2204            );
2205            bus.write_word(
2206                style_table_ptr + Self::ST_ELEMENT_FONT_OFFSET,
2207                self.tx_font as u16,
2208            );
2209            bus.write_byte(
2210                style_table_ptr + Self::ST_ELEMENT_FACE_OFFSET,
2211                self.tx_face as u8,
2212            );
2213            bus.write_word(
2214                style_table_ptr + Self::ST_ELEMENT_SIZE_OFFSET,
2215                style_size as u16,
2216            );
2217            bus.write_word(
2218                style_table_ptr + Self::ST_ELEMENT_COLOR_OFFSET,
2219                self.fg_color.0,
2220            );
2221            bus.write_word(
2222                style_table_ptr + Self::ST_ELEMENT_COLOR_OFFSET + 2,
2223                self.fg_color.1,
2224            );
2225            bus.write_word(
2226                style_table_ptr + Self::ST_ELEMENT_COLOR_OFFSET + 4,
2227                self.fg_color.2,
2228            );
2229        }
2230
2231        let lh_table_ptr = if lh_table != 0 {
2232            bus.read_long(lh_table)
2233        } else {
2234            0
2235        };
2236        if lh_table_ptr != 0 {
2237            bus.write_word(
2238                lh_table_ptr + Self::LH_ELEMENT_HEIGHT_OFFSET,
2239                line_height as u16,
2240            );
2241            bus.write_word(
2242                lh_table_ptr + Self::LH_ELEMENT_ASCENT_OFFSET,
2243                metrics.ascent as u16,
2244            );
2245        }
2246
2247        let null_scrap_ptr = if null_scrap != 0 {
2248            bus.read_long(null_scrap)
2249        } else {
2250            0
2251        };
2252        if null_scrap_ptr != 0 {
2253            bus.write_word(null_scrap_ptr + Self::SCRAP_N_STYLES_OFFSET, 0);
2254            bus.write_long(
2255                null_scrap_ptr + Self::SCRAP_STYLE_TAB_OFFSET + Self::SCRAP_STYLE_START_CHAR_OFFSET,
2256                0,
2257            );
2258            bus.write_word(
2259                null_scrap_ptr + Self::SCRAP_STYLE_TAB_OFFSET + Self::SCRAP_STYLE_HEIGHT_OFFSET,
2260                line_height as u16,
2261            );
2262            bus.write_word(
2263                null_scrap_ptr + Self::SCRAP_STYLE_TAB_OFFSET + Self::SCRAP_STYLE_ASCENT_OFFSET,
2264                metrics.ascent as u16,
2265            );
2266            bus.write_word(
2267                null_scrap_ptr + Self::SCRAP_STYLE_TAB_OFFSET + Self::SCRAP_STYLE_FONT_OFFSET,
2268                self.tx_font as u16,
2269            );
2270            bus.write_byte(
2271                null_scrap_ptr + Self::SCRAP_STYLE_TAB_OFFSET + Self::SCRAP_STYLE_FACE_OFFSET,
2272                self.tx_face as u8,
2273            );
2274            bus.write_word(
2275                null_scrap_ptr + Self::SCRAP_STYLE_TAB_OFFSET + Self::SCRAP_STYLE_SIZE_OFFSET,
2276                style_size as u16,
2277            );
2278            bus.write_word(
2279                null_scrap_ptr + Self::SCRAP_STYLE_TAB_OFFSET + Self::SCRAP_STYLE_COLOR_OFFSET,
2280                self.fg_color.0,
2281            );
2282            bus.write_word(
2283                null_scrap_ptr + Self::SCRAP_STYLE_TAB_OFFSET + Self::SCRAP_STYLE_COLOR_OFFSET + 2,
2284                self.fg_color.1,
2285            );
2286            bus.write_word(
2287                null_scrap_ptr + Self::SCRAP_STYLE_TAB_OFFSET + Self::SCRAP_STYLE_COLOR_OFFSET + 4,
2288                self.fg_color.2,
2289            );
2290        }
2291
2292        let null_style_ptr = if null_style != 0 {
2293            bus.read_long(null_style)
2294        } else {
2295            0
2296        };
2297        if null_style_ptr != 0 {
2298            bus.write_long(null_style_ptr + Self::NULL_STYLE_SCRAP_OFFSET, null_scrap);
2299        }
2300
2301        let style_ptr = if style_handle != 0 {
2302            bus.read_long(style_handle)
2303        } else {
2304            0
2305        };
2306        if style_ptr != 0 {
2307            bus.write_word(style_ptr + Self::TE_STYLE_N_RUNS_OFFSET, 1);
2308            bus.write_word(style_ptr + Self::TE_STYLE_N_STYLES_OFFSET, 1);
2309            bus.write_long(style_ptr + Self::TE_STYLE_STYLE_TABLE_OFFSET, style_table);
2310            bus.write_long(style_ptr + Self::TE_STYLE_LH_TABLE_OFFSET, lh_table);
2311            bus.write_long(style_ptr + Self::TE_STYLE_NULL_STYLE_OFFSET, null_style);
2312            bus.write_word(style_ptr + Self::TE_STYLE_RUNS_OFFSET, 0);
2313            bus.write_word(style_ptr + Self::TE_STYLE_RUNS_OFFSET + 2, 0);
2314            bus.write_word(style_ptr + Self::TE_STYLE_RUNS_OFFSET + 4, 1);
2315            bus.write_word(style_ptr + Self::TE_STYLE_RUNS_OFFSET + 6, 0xFFFF);
2316        }
2317
2318        Self::te_write_rect_words(bus, te_ptr + Self::TE_DEST_RECT_OFFSET, dest_rect);
2319        Self::te_write_rect_words(bus, te_ptr + Self::TE_VIEW_RECT_OFFSET, view_rect);
2320        Self::te_write_rect_words(bus, te_ptr + Self::TE_SEL_RECT_OFFSET, dest_rect);
2321        bus.write_word(te_ptr + Self::TE_LINE_HEIGHT_OFFSET, 0xFFFF);
2322        bus.write_word(te_ptr + Self::TE_FONT_ASCENT_OFFSET, 0xFFFF);
2323        bus.write_word(te_ptr + Self::TE_SEL_POINT_OFFSET, dest_rect.0 as u16);
2324        bus.write_word(te_ptr + Self::TE_SEL_POINT_OFFSET + 2, dest_rect.1 as u16);
2325        bus.write_word(te_ptr + Self::TE_SEL_START_OFFSET, 0);
2326        bus.write_word(te_ptr + Self::TE_SEL_END_OFFSET, 0);
2327        bus.write_long(te_ptr + Self::TE_CARET_TIME_OFFSET, self.tick_count);
2328        bus.write_word(te_ptr + Self::TE_CARET_STATE_OFFSET, 0);
2329        bus.write_word(te_ptr + Self::TE_JUST_OFFSET, 0);
2330        bus.write_word(te_ptr + Self::TE_LENGTH_OFFSET, 0);
2331        bus.write_long(te_ptr + Self::TE_HTEXT_OFFSET, h_text);
2332        bus.write_long(te_ptr + Self::TE_TX_FONT_OFFSET, style_handle);
2333        bus.write_word(te_ptr + Self::TE_TX_MODE_OFFSET, self.tx_mode as u16);
2334        bus.write_word(te_ptr + Self::TE_TX_SIZE_OFFSET, 0xFFFF);
2335        bus.write_long(te_ptr + Self::TE_IN_PORT_OFFSET, self.current_port);
2336        bus.write_word(te_ptr + Self::TE_N_LINES_OFFSET, 0);
2337        bus.write_word(te_ptr + Self::TE_LINE_STARTS_OFFSET, 0);
2338        self.textedit_states.remove(&te_handle);
2339    }
2340
2341    fn te_text_length(bus: &MacMemoryBus, te_handle: u32) -> usize {
2342        let te_ptr = Self::te_record_ptr(bus, te_handle);
2343        if te_ptr == 0 {
2344            return 0;
2345        }
2346
2347        let stored = bus.read_word(te_ptr + Self::TE_LENGTH_OFFSET) as usize;
2348        if stored != 0 {
2349            return stored;
2350        }
2351
2352        let h_text = bus.read_long(te_ptr + Self::TE_HTEXT_OFFSET);
2353        if h_text == 0 {
2354            return 0;
2355        }
2356        let text_ptr = bus.read_long(h_text);
2357        if text_ptr == 0 {
2358            0
2359        } else {
2360            bus.get_alloc_size(text_ptr).unwrap_or(0) as usize
2361        }
2362    }
2363
2364    fn te_text_bytes(bus: &MacMemoryBus, te_handle: u32) -> Vec<u8> {
2365        let len = Self::te_text_length(bus, te_handle);
2366        if len == 0 {
2367            return Vec::new();
2368        }
2369
2370        let h_text = Self::te_text_handle(bus, te_handle);
2371        if h_text == 0 {
2372            return Vec::new();
2373        }
2374        let text_ptr = bus.read_long(h_text);
2375        if text_ptr == 0 {
2376            return Vec::new();
2377        }
2378        bus.read_bytes(text_ptr, len)
2379    }
2380
2381    pub(crate) fn te_find_word_bounds(
2382        &self,
2383        bus: &MacMemoryBus,
2384        te_handle: u32,
2385        current_pos: usize,
2386    ) -> (u16, u16) {
2387        let text_bytes = Self::te_text_bytes(bus, te_handle);
2388        let len = text_bytes.len();
2389        if len == 0 {
2390            return (0, 0);
2391        }
2392
2393        let pos = current_pos.min(len);
2394        if pos < len && Self::te_word_break_byte(text_bytes[pos]) {
2395            return (pos as u16, pos as u16);
2396        }
2397
2398        let mut start = pos;
2399        while start > 0 && !Self::te_word_break_byte(text_bytes[start - 1]) {
2400            start -= 1;
2401        }
2402
2403        let mut end = pos;
2404        while end < len && !Self::te_word_break_byte(text_bytes[end]) {
2405            end += 1;
2406        }
2407
2408        (start as u16, end as u16)
2409    }
2410
2411    #[allow(dead_code)]
2412    fn te_reset_styled_metadata(
2413        &mut self,
2414        bus: &mut MacMemoryBus,
2415        te_handle: u32,
2416        text_len: usize,
2417    ) {
2418        let style_handle = Self::te_style_handle(bus, te_handle);
2419        if style_handle == 0 {
2420            return;
2421        }
2422
2423        let (font, face, size, color, line_height, ascent) = self.te_primary_style(bus, te_handle);
2424        let style_ptr = bus.read_long(style_handle);
2425        if style_ptr == 0 {
2426            return;
2427        }
2428
2429        let style_table_handle = bus.read_long(style_ptr + Self::TE_STYLE_STYLE_TABLE_OFFSET);
2430        let style_table_ptr = if style_table_handle != 0 {
2431            bus.read_long(style_table_handle)
2432        } else {
2433            0
2434        };
2435        if style_table_ptr != 0 {
2436            bus.write_word(style_table_ptr + Self::ST_ELEMENT_REFCOUNT_OFFSET, 1);
2437            bus.write_word(
2438                style_table_ptr + Self::ST_ELEMENT_HEIGHT_OFFSET,
2439                line_height as u16,
2440            );
2441            bus.write_word(
2442                style_table_ptr + Self::ST_ELEMENT_ASCENT_OFFSET,
2443                ascent as u16,
2444            );
2445            bus.write_word(style_table_ptr + Self::ST_ELEMENT_FONT_OFFSET, font as u16);
2446            bus.write_byte(style_table_ptr + Self::ST_ELEMENT_FACE_OFFSET, face as u8);
2447            bus.write_word(style_table_ptr + Self::ST_ELEMENT_SIZE_OFFSET, size as u16);
2448            bus.write_word(style_table_ptr + Self::ST_ELEMENT_COLOR_OFFSET, color.0);
2449            bus.write_word(style_table_ptr + Self::ST_ELEMENT_COLOR_OFFSET + 2, color.1);
2450            bus.write_word(style_table_ptr + Self::ST_ELEMENT_COLOR_OFFSET + 4, color.2);
2451        }
2452
2453        let lh_handle = bus.read_long(style_ptr + Self::TE_STYLE_LH_TABLE_OFFSET);
2454        let lh_ptr = if lh_handle != 0 {
2455            bus.read_long(lh_handle)
2456        } else {
2457            0
2458        };
2459        if lh_ptr != 0 {
2460            bus.write_word(lh_ptr + Self::LH_ELEMENT_HEIGHT_OFFSET, line_height as u16);
2461            bus.write_word(lh_ptr + Self::LH_ELEMENT_ASCENT_OFFSET, ascent as u16);
2462        }
2463
2464        bus.write_word(style_ptr + Self::TE_STYLE_N_RUNS_OFFSET, 1);
2465        bus.write_word(style_ptr + Self::TE_STYLE_N_STYLES_OFFSET, 1);
2466        bus.write_word(style_ptr + Self::TE_STYLE_RUNS_OFFSET, 0);
2467        bus.write_word(style_ptr + Self::TE_STYLE_RUNS_OFFSET + 2, 0);
2468        bus.write_word(
2469            style_ptr + Self::TE_STYLE_RUNS_OFFSET + 4,
2470            text_len.saturating_add(1).min(u16::MAX as usize) as u16,
2471        );
2472        bus.write_word(style_ptr + Self::TE_STYLE_RUNS_OFFSET + 6, 0xFFFF);
2473    }
2474
2475    fn te_set_text_contents(&mut self, bus: &mut MacMemoryBus, te_handle: u32, text: &[u8]) {
2476        let te_ptr = Self::te_record_ptr(bus, te_handle);
2477        if te_ptr == 0 {
2478            return;
2479        }
2480
2481        let mut h_text = bus.read_long(te_ptr + Self::TE_HTEXT_OFFSET);
2482        if h_text == 0 {
2483            h_text = Self::allocate_handle_with_data(bus, 0);
2484            bus.write_long(te_ptr + Self::TE_HTEXT_OFFSET, h_text);
2485        }
2486
2487        let text_ptr = Self::ensure_text_handle_size(bus, h_text, text.len());
2488        if !text.is_empty() && text_ptr != 0 {
2489            bus.write_bytes(text_ptr, text);
2490        }
2491
2492        let clamped_len = text.len().min(u16::MAX as usize) as u16;
2493        bus.write_word(te_ptr + Self::TE_LENGTH_OFFSET, clamped_len);
2494        bus.write_word(te_ptr + Self::TE_SEL_START_OFFSET, clamped_len);
2495        bus.write_word(te_ptr + Self::TE_SEL_END_OFFSET, clamped_len);
2496        self.te_recalculate_layout(bus, te_handle);
2497    }
2498
2499    fn textedit_idle(&mut self, cpu: &mut impl CpuOps, bus: &mut MacMemoryBus, te_handle: u32) {
2500        if trace_textedit_enabled() {
2501            eprintln!("[TE] TEIdle hTE=${te_handle:08X}");
2502        }
2503        let te_ptr = Self::te_record_ptr(bus, te_handle);
2504        if te_ptr == 0 {
2505            return;
2506        }
2507        if bus.read_word(te_ptr + Self::TE_ACTIVE_OFFSET) == 0 {
2508            return;
2509        }
2510
2511        let sel_start = bus.read_word(te_ptr + Self::TE_SEL_START_OFFSET);
2512        let sel_end = bus.read_word(te_ptr + Self::TE_SEL_END_OFFSET);
2513        if sel_start != sel_end {
2514            return;
2515        }
2516
2517        let last_toggle = bus.read_long(te_ptr + Self::TE_CARET_TIME_OFFSET);
2518        if self.tick_count.wrapping_sub(last_toggle) < Self::TE_CARET_BLINK_TICKS {
2519            return;
2520        }
2521
2522        let caret_state = bus.read_word(te_ptr + Self::TE_CARET_STATE_OFFSET);
2523        bus.write_word(
2524            te_ptr + Self::TE_CARET_STATE_OFFSET,
2525            if caret_state == 0 { 1 } else { 0 },
2526        );
2527        bus.write_long(te_ptr + Self::TE_CARET_TIME_OFFSET, self.tick_count);
2528        self.draw_te_contents(cpu, bus, te_handle);
2529    }
2530
2531    fn te_insert_text(&mut self, bus: &mut MacMemoryBus, te_handle: u32, text: &[u8]) {
2532        let te_ptr = Self::te_record_ptr(bus, te_handle);
2533        if te_ptr == 0 {
2534            return;
2535        }
2536
2537        let existing = Self::te_text_bytes(bus, te_handle);
2538        let mut sel_start = bus.read_word(te_ptr + Self::TE_SEL_START_OFFSET) as usize;
2539        let mut sel_end = bus.read_word(te_ptr + Self::TE_SEL_END_OFFSET) as usize;
2540        let text_len = existing.len();
2541        sel_start = sel_start.min(text_len);
2542        sel_end = sel_end.min(text_len);
2543        if sel_end < sel_start {
2544            std::mem::swap(&mut sel_start, &mut sel_end);
2545        }
2546
2547        let mut merged =
2548            Vec::with_capacity(sel_start + text.len() + text_len.saturating_sub(sel_end));
2549        merged.extend_from_slice(&existing[..sel_start]);
2550        merged.extend_from_slice(text);
2551        merged.extend_from_slice(&existing[sel_end..]);
2552        self.te_set_text_contents(bus, te_handle, &merged);
2553
2554        let insertion_end = (sel_start + text.len()).min(u16::MAX as usize) as u16;
2555        bus.write_word(te_ptr + Self::TE_SEL_START_OFFSET, insertion_end);
2556        bus.write_word(te_ptr + Self::TE_SEL_END_OFFSET, insertion_end);
2557    }
2558
2559    fn te_char_width(&self, font: i16, size: i16, byte: u8) -> i16 {
2560        let size = Self::font_lookup_size(size);
2561        let (_face, scale) = get_font_face_scaled(font, size);
2562        let ch = byte as char;
2563        if let Some((glyph, _)) = crate::quickdraw::text::get_glyph(font, size, ch) {
2564            self.glyph_advance(glyph) * scale
2565        } else {
2566            self.missing_glyph_advance() * scale
2567        }
2568    }
2569
2570    fn te_measure_text_width(
2571        &self,
2572        font: i16,
2573        size: i16,
2574        text_bytes: &[u8],
2575        start: usize,
2576        end: usize,
2577    ) -> i16 {
2578        let mut width = 0i16;
2579        for &b in &text_bytes[start..end] {
2580            width += self.te_char_width(font, size, b);
2581        }
2582        width
2583    }
2584
2585    fn te_word_break_byte(byte: u8) -> bool {
2586        byte <= 0x20
2587    }
2588
2589    fn te_next_break(
2590        &self,
2591        font: i16,
2592        size: i16,
2593        text_bytes: &[u8],
2594        offset: usize,
2595        max_width: i16,
2596    ) -> usize {
2597        let len = text_bytes.len();
2598        if offset >= len {
2599            return len;
2600        }
2601
2602        let mut width = 0i16;
2603        let mut index = offset;
2604        while width <= max_width && index < len && !matches!(text_bytes[index], b'\r' | b'\n') {
2605            width = width.saturating_add(self.te_char_width(font, size, text_bytes[index]));
2606            index += 1;
2607        }
2608
2609        let mut next = if width > max_width {
2610            let max_index = index.saturating_sub(1);
2611            if text_bytes[max_index] == b' ' {
2612                let mut scan = index;
2613                while scan < len && text_bytes[scan] == b' ' {
2614                    scan += 1;
2615                }
2616                scan
2617            } else {
2618                let mut scan = max_index;
2619                while scan > offset && !Self::te_word_break_byte(text_bytes[scan - 1]) {
2620                    scan -= 1;
2621                }
2622                if scan == offset {
2623                    max_index
2624                } else {
2625                    scan
2626                }
2627            }
2628        } else if index == len {
2629            len
2630        } else {
2631            index + 1
2632        };
2633
2634        if next == offset && offset < len {
2635            next += 1;
2636        }
2637        next
2638    }
2639
2640    fn te_wrap_lines(
2641        &self,
2642        font: i16,
2643        size: i16,
2644        text_bytes: &[u8],
2645        box_width: i16,
2646    ) -> Vec<(usize, usize)> {
2647        let mut lines: Vec<(usize, usize)> = Vec::new();
2648        let mut line_start = 0usize;
2649        while line_start < text_bytes.len() {
2650            let next = self.te_next_break(font, size, text_bytes, line_start, box_width);
2651            lines.push((line_start, next.min(text_bytes.len())));
2652            line_start = next;
2653        }
2654
2655        lines
2656    }
2657
2658    fn te_recalculate_layout(&mut self, bus: &mut MacMemoryBus, te_handle: u32) {
2659        let te_ptr = Self::te_record_ptr(bus, te_handle);
2660        if te_ptr == 0 {
2661            return;
2662        }
2663
2664        let text_bytes = Self::te_text_bytes(bus, te_handle);
2665        let text_len = text_bytes.len();
2666        let dest_rect = Self::te_read_rect(bus, te_ptr + Self::TE_DEST_RECT_OFFSET);
2667        let (font, _, size, _, line_height, font_ascent) = self.te_primary_style(bus, te_handle);
2668        let styled = Self::te_is_styled_record(bus, te_ptr);
2669        let style_runs = if styled {
2670            self.te_style_runs(bus, te_handle, text_len)
2671        } else {
2672            Vec::new()
2673        };
2674        let uses_styled_runs = styled && !style_runs.is_empty();
2675        let lines = if text_bytes.is_empty() {
2676            Vec::new()
2677        } else if uses_styled_runs {
2678            self.te_wrap_lines_styled(&style_runs, &text_bytes, (dest_rect.3 - dest_rect.1).max(0))
2679        } else {
2680            self.te_wrap_lines(font, size, &text_bytes, (dest_rect.3 - dest_rect.1).max(0))
2681        };
2682        let line_count = lines.len();
2683        let te_ptr = Self::ensure_te_record_line_capacity(bus, te_handle, line_count);
2684        if te_ptr == 0 {
2685            return;
2686        }
2687
2688        bus.write_word(
2689            te_ptr + Self::TE_N_LINES_OFFSET,
2690            line_count.min(u16::MAX as usize) as u16,
2691        );
2692        if styled {
2693            bus.write_word(te_ptr + Self::TE_LINE_HEIGHT_OFFSET, 0xFFFF);
2694            bus.write_word(te_ptr + Self::TE_FONT_ASCENT_OFFSET, 0xFFFF);
2695        } else {
2696            bus.write_word(te_ptr + Self::TE_LINE_HEIGHT_OFFSET, line_height as u16);
2697            bus.write_word(te_ptr + Self::TE_FONT_ASCENT_OFFSET, font_ascent as u16);
2698        }
2699
2700        for index in 0..=line_count.saturating_add(1) {
2701            bus.write_word(te_ptr + Self::TE_LINE_STARTS_OFFSET + (index as u32 * 2), 0);
2702        }
2703        for (index, (start, _)) in lines.iter().enumerate() {
2704            bus.write_word(
2705                te_ptr + Self::TE_LINE_STARTS_OFFSET + (index as u32 * 2),
2706                (*start).min(u16::MAX as usize) as u16,
2707            );
2708        }
2709        bus.write_word(
2710            te_ptr + Self::TE_LINE_STARTS_OFFSET + (line_count as u32 * 2),
2711            text_len.min(u16::MAX as usize) as u16,
2712        );
2713        bus.write_word(
2714            te_ptr + Self::TE_LINE_STARTS_OFFSET + ((line_count as u32 + 1) * 2),
2715            0,
2716        );
2717
2718        if styled {
2719            let style_handle = Self::te_style_handle(bus, te_handle);
2720            if style_handle != 0 {
2721                let style_ptr = bus.read_long(style_handle);
2722                if style_ptr != 0 {
2723                    let lh_handle = bus.read_long(style_ptr + Self::TE_STYLE_LH_TABLE_OFFSET);
2724                    if lh_handle != 0 {
2725                        let lh_ptr = Self::ensure_handle_capacity(
2726                            bus,
2727                            lh_handle,
2728                            ((line_count + 1) as u32) * Self::LH_ELEMENT_SIZE,
2729                        );
2730                        if lh_ptr != 0 {
2731                            for index in 0..=line_count {
2732                                let (line_height, font_ascent) = if uses_styled_runs
2733                                    && index < lines.len()
2734                                {
2735                                    let (start, end) = lines[index];
2736                                    Self::te_line_metrics_for_range(&style_runs, start, end)
2737                                } else if uses_styled_runs {
2738                                    Self::te_line_metrics_for_range(&style_runs, text_len, text_len)
2739                                } else {
2740                                    (line_height, font_ascent)
2741                                };
2742                                let element = lh_ptr + (index as u32 * Self::LH_ELEMENT_SIZE);
2743                                bus.write_word(
2744                                    element + Self::LH_ELEMENT_HEIGHT_OFFSET,
2745                                    line_height as u16,
2746                                );
2747                                bus.write_word(
2748                                    element + Self::LH_ELEMENT_ASCENT_OFFSET,
2749                                    font_ascent as u16,
2750                                );
2751                            }
2752                        }
2753                    }
2754                }
2755            }
2756        }
2757
2758        if styled {
2759            Self::te_update_styled_run_sentinel(bus, te_handle, text_len);
2760        }
2761    }
2762
2763    fn te_line_origin_x(just: i16, box_left: i16, box_right: i16, line_width: i16) -> i16 {
2764        // Per Inside Macintosh: Text 1993, p. 7320-7323 (and MPW
2765        // Universal Headers `TextEdit.h`):
2766        //   teJustLeft   =  0  (flush left — system default for LTR)
2767        //   teJustCenter =  1  (centered)
2768        //   teJustRight  = -1  (flush right)
2769        //   teForceLeft  = -2  (force flush left, overrides localised right-to-left)
2770        match just {
2771            1 => box_left + ((box_right - box_left) - line_width) / 2,
2772            -1 => box_right - line_width,
2773            _ => box_left.saturating_add(Self::TE_LINE_LEFT_INSET), // 0 / -2 / any other → flush left inside destRect
2774        }
2775    }
2776
2777    fn port_has_drawable_bitmap(bus: &MacMemoryBus, port: u32) -> bool {
2778        if port == 0 {
2779            return false;
2780        }
2781
2782        let port_version = bus.read_word(port + 6);
2783        let is_color = (port_version & 0xC000) != 0;
2784        let (base, row_bytes, top, left, bottom, right) = if is_color {
2785            let pix_map_handle = bus.read_long(port + 2);
2786            if pix_map_handle == 0 {
2787                return false;
2788            }
2789            let pix_map_ptr = bus.read_long(pix_map_handle);
2790            if pix_map_ptr == 0 {
2791                return false;
2792            }
2793            (
2794                bus.read_long(pix_map_ptr),
2795                bus.read_word(pix_map_ptr + 4) & 0x3FFF,
2796                bus.read_word(pix_map_ptr + 6) as i16,
2797                bus.read_word(pix_map_ptr + 8) as i16,
2798                bus.read_word(pix_map_ptr + 10) as i16,
2799                bus.read_word(pix_map_ptr + 12) as i16,
2800            )
2801        } else {
2802            (
2803                bus.read_long(port + 2),
2804                bus.read_word(port + 6) & 0x3FFF,
2805                bus.read_word(port + 8) as i16,
2806                bus.read_word(port + 10) as i16,
2807                bus.read_word(port + 12) as i16,
2808                bus.read_word(port + 14) as i16,
2809            )
2810        };
2811
2812        base != 0 && row_bytes != 0 && top < bottom && left < right
2813    }
2814
2815    fn draw_te_contents(&mut self, cpu: &mut impl CpuOps, bus: &mut MacMemoryBus, te_handle: u32) {
2816        let te_ptr = Self::te_record_ptr(bus, te_handle);
2817        if te_ptr == 0 {
2818            return;
2819        }
2820
2821        let text_bytes = Self::te_text_bytes(bus, te_handle);
2822        let dest_rect = Self::te_read_rect(bus, te_ptr + Self::TE_DEST_RECT_OFFSET);
2823        let view_rect = Self::te_read_rect(bus, te_ptr + Self::TE_VIEW_RECT_OFFSET);
2824        let te_port = bus.read_long(te_ptr + Self::TE_IN_PORT_OFFSET);
2825        if !Self::port_has_drawable_bitmap(bus, te_port) {
2826            return;
2827        }
2828        let just = bus.read_word(te_ptr + Self::TE_JUST_OFFSET) as i16;
2829        let (font, face, size, color, _, _) = self.te_primary_style(bus, te_handle);
2830        let styled_runs = if Self::te_is_styled_record(bus, te_ptr) {
2831            self.te_style_runs(bus, te_handle, text_bytes.len())
2832        } else {
2833            Vec::new()
2834        };
2835        let uses_styled_runs = !styled_runs.is_empty() && Self::te_is_styled_record(bus, te_ptr);
2836        let old_font = self.tx_font;
2837        let old_face = self.tx_face;
2838        let old_size = self.tx_size;
2839        let old_fg = self.fg_color;
2840        let old_loc = self.pn_loc;
2841        let previous_port = self.current_port;
2842        let previous_gdevice = self.current_gdevice;
2843        let switched_port = te_port != 0 && te_port != previous_port;
2844
2845        // Always sync A5 globals to te_port before drawing, even if port didn't change.
2846        // draw_generic_shape reads from A5 globals, and untrack_window can set self.current_port
2847        // without updating A5 globals, causing a divergence that sends drawing to the wrong port.
2848        if te_port != 0 {
2849            self.set_current_port_state(bus, cpu, te_port, None);
2850        }
2851        if trace_textedit_enabled() {
2852            let (vis_top, vis_left, vis_bottom, vis_right) = {
2853                let vis_handle = bus.read_long(self.current_port + 24);
2854                let vis_ptr = if vis_handle != 0 {
2855                    bus.read_long(vis_handle)
2856                } else {
2857                    0
2858                };
2859                if vis_ptr != 0 {
2860                    (
2861                        bus.read_word(vis_ptr + 2) as i16,
2862                        bus.read_word(vis_ptr + 4) as i16,
2863                        bus.read_word(vis_ptr + 6) as i16,
2864                        bus.read_word(vis_ptr + 8) as i16,
2865                    )
2866                } else {
2867                    (0, 0, 0, 0)
2868                }
2869            };
2870            let (clip_top, clip_left, clip_bottom, clip_right) = {
2871                let clip_handle = bus.read_long(self.current_port + 28);
2872                let clip_ptr = if clip_handle != 0 {
2873                    bus.read_long(clip_handle)
2874                } else {
2875                    0
2876                };
2877                if clip_ptr != 0 {
2878                    (
2879                        bus.read_word(clip_ptr + 2) as i16,
2880                        bus.read_word(clip_ptr + 4) as i16,
2881                        bus.read_word(clip_ptr + 6) as i16,
2882                        bus.read_word(clip_ptr + 8) as i16,
2883                    )
2884                } else {
2885                    (0, 0, 0, 0)
2886                }
2887            };
2888            eprintln!(
2889                "[TE] draw_te_contents hTE=${:08X} te_port=${:08X} prev_port=${:08X} current_port=${:08X} switched={} gworld={} dest=({},{},{},{}) view=({},{},{},{}) vis=({},{},{},{}) clip=({},{},{},{})",
2890                te_handle,
2891                te_port,
2892                previous_port,
2893                self.current_port,
2894                switched_port,
2895                self.gworld_devices.contains_key(&te_port),
2896                dest_rect.0,
2897                dest_rect.1,
2898                dest_rect.2,
2899                dest_rect.3,
2900                view_rect.0,
2901                view_rect.1,
2902                view_rect.2,
2903                view_rect.3,
2904                vis_top,
2905                vis_left,
2906                vis_bottom,
2907                vis_right,
2908                clip_top,
2909                clip_left,
2910                clip_bottom,
2911                clip_right,
2912            );
2913        }
2914
2915        let checksum_view_rect =
2916            |bus: &MacMemoryBus, port: u32, rect: (i16, i16, i16, i16)| -> u64 {
2917                if port == 0 {
2918                    return 0;
2919                }
2920                let port_version = bus.read_word(port + 6);
2921                let is_color = (port_version & 0xC000) != 0;
2922                let (pix_base, row_bytes, bounds_top, bounds_left, bounds_bottom, bounds_right) =
2923                    if is_color {
2924                        let pix_map_handle = bus.read_long(port + 2);
2925                        if pix_map_handle == 0 {
2926                            return 0;
2927                        }
2928                        let pix_map_ptr = bus.read_long(pix_map_handle);
2929                        if pix_map_ptr == 0 {
2930                            return 0;
2931                        }
2932                        (
2933                            bus.read_long(pix_map_ptr),
2934                            (bus.read_word(pix_map_ptr + 4) & 0x3FFF) as u32,
2935                            bus.read_word(pix_map_ptr + 6) as i16,
2936                            bus.read_word(pix_map_ptr + 8) as i16,
2937                            bus.read_word(pix_map_ptr + 10) as i16,
2938                            bus.read_word(pix_map_ptr + 12) as i16,
2939                        )
2940                    } else {
2941                        (
2942                            bus.read_long(port + 2),
2943                            (bus.read_word(port + 6) & 0x3FFF) as u32,
2944                            bus.read_word(port + 8) as i16,
2945                            bus.read_word(port + 10) as i16,
2946                            bus.read_word(port + 12) as i16,
2947                            bus.read_word(port + 14) as i16,
2948                        )
2949                    };
2950                let top = rect.0.max(bounds_top);
2951                let left = rect.1.max(bounds_left);
2952                let bottom = rect.2.min(bounds_bottom);
2953                let right = rect.3.min(bounds_right);
2954                if top >= bottom || left >= right || pix_base == 0 {
2955                    return 0;
2956                }
2957                let mut sum = 0u64;
2958                for y in top..bottom {
2959                    let dy = (y - bounds_top) as u32;
2960                    for x in left..right {
2961                        let dx = (x - bounds_left) as u32;
2962                        sum =
2963                            sum.wrapping_add(bus.read_byte(pix_base + dy * row_bytes + dx) as u64);
2964                    }
2965                }
2966                sum
2967            };
2968        let checksum_before = if trace_textedit_enabled() {
2969            checksum_view_rect(bus, self.current_port, view_rect)
2970        } else {
2971            0
2972        };
2973
2974        self.tx_font = font;
2975        self.tx_face = face;
2976        self.tx_size = size;
2977        self.fg_color = color;
2978
2979        let clip_top = view_rect.0;
2980        let clip_left = view_rect.1;
2981        let clip_bottom = view_rect.2;
2982        let clip_right = view_rect.3;
2983        let box_width = (dest_rect.3 - dest_rect.1).max(0);
2984
2985        self.draw_rect(
2986            cpu,
2987            bus,
2988            &Rect {
2989                top: clip_top,
2990                left: clip_left,
2991                bottom: clip_bottom,
2992                right: clip_right,
2993            },
2994            ShapeOp::Erase,
2995        );
2996        let lines = if text_bytes.is_empty() {
2997            vec![(0usize, 0usize)]
2998        } else if uses_styled_runs {
2999            self.te_wrap_lines_styled(&styled_runs, &text_bytes, box_width)
3000        } else {
3001            self.te_wrap_lines(font, size, &text_bytes, box_width)
3002        };
3003
3004        let mut top = dest_rect.0;
3005        let mut visible_lines = Vec::new();
3006        let mut visual_lines = Vec::new();
3007        for (index, (start, end)) in lines.iter().enumerate() {
3008            let line_height = Self::te_height_for_line(bus, te_handle, index);
3009            let line_ascent = Self::te_ascent_for_line(bus, te_handle, index);
3010            let line_bottom = top.saturating_add(line_height);
3011            if line_bottom <= clip_top {
3012                top = line_bottom;
3013                continue;
3014            }
3015            if top >= clip_bottom {
3016                break;
3017            }
3018            visible_lines.push(index);
3019            let mut trimmed_end = *end;
3020            while trimmed_end > *start
3021                && matches!(text_bytes[trimmed_end - 1], b' ' | b'\r' | b'\n')
3022            {
3023                trimmed_end -= 1;
3024            }
3025            let line_width = if uses_styled_runs {
3026                self.te_measure_text_width_styled(&styled_runs, &text_bytes, *start, trimmed_end)
3027            } else {
3028                self.te_measure_text_width(font, size, &text_bytes, *start, trimmed_end)
3029            };
3030            let x = Self::te_line_origin_x(just, dest_rect.1, dest_rect.3, line_width);
3031            visual_lines.push((*start, *end, top, line_bottom, x));
3032            self.pn_loc = (top.saturating_add(line_ascent), x);
3033            for (offset, &byte) in text_bytes[*start..trimmed_end].iter().enumerate() {
3034                if uses_styled_runs {
3035                    let style = Self::te_style_at_offset(&styled_runs, *start + offset);
3036                    self.tx_font = style.font;
3037                    self.tx_face = style.face;
3038                    self.tx_size = style.size;
3039                    self.fg_color = style.color;
3040                }
3041                self.draw_char(cpu, bus, byte as char);
3042            }
3043            top = line_bottom;
3044        }
3045
3046        let te_active = bus.read_word(te_ptr + Self::TE_ACTIVE_OFFSET) != 0;
3047        let caret_visible = bus.read_word(te_ptr + Self::TE_CARET_STATE_OFFSET) == 0;
3048        let outline_hilite = self.te_feature_bit(te_handle, Self::TE_FEATURE_OUTLINE_HILITE);
3049        let sel_start = bus.read_word(te_ptr + Self::TE_SEL_START_OFFSET) as usize;
3050        let sel_end = bus.read_word(te_ptr + Self::TE_SEL_END_OFFSET) as usize;
3051        let (selection_start, selection_end) = if sel_start <= sel_end {
3052            (sel_start, sel_end)
3053        } else {
3054            (sel_end, sel_start)
3055        };
3056        if selection_start == selection_end {
3057            if te_active && caret_visible {
3058                if let Some(&(line_start, line_end, line_top, line_bottom, line_x)) = visual_lines
3059                    .iter()
3060                    .find(|&&(start, end, _, _, _)| {
3061                        selection_start >= start && selection_start <= end
3062                    })
3063                    .or_else(|| visual_lines.last())
3064                {
3065                    let caret_offset = selection_start.min(line_end).max(line_start);
3066                    let measured_width = if uses_styled_runs {
3067                        self.te_measure_text_width_styled(
3068                            &styled_runs,
3069                            &text_bytes,
3070                            line_start,
3071                            caret_offset,
3072                        )
3073                    } else {
3074                        self.te_measure_text_width(
3075                            font,
3076                            size,
3077                            &text_bytes,
3078                            line_start,
3079                            caret_offset,
3080                        )
3081                    };
3082                    let mut caret_x = line_x + measured_width;
3083                    if caret_offset > line_start {
3084                        caret_x = caret_x.saturating_sub(1);
3085                    }
3086                    let caret_width = self.ui_theme().text_theme().caret_width.max(1);
3087                    if !self.draw_theme_caret(
3088                        bus,
3089                        line_top,
3090                        caret_x,
3091                        line_bottom,
3092                        caret_x.saturating_add(caret_width),
3093                    ) {
3094                        self.draw_rect(
3095                            cpu,
3096                            bus,
3097                            &Rect {
3098                                top: line_top,
3099                                left: caret_x,
3100                                bottom: line_bottom,
3101                                right: caret_x.saturating_add(1),
3102                            },
3103                            ShapeOp::Paint,
3104                        );
3105                    }
3106                }
3107            }
3108        } else if te_active || outline_hilite {
3109            // IM:I I-375/I-385 and Text 1993 p. 2-80: an active edit
3110            // record displays its selection range by highlighting the
3111            // selected characters. Classic black-and-white TextEdit uses
3112            // inverse video; non-classic themes own their selection chrome.
3113            for &(line_start, line_end, line_top, line_bottom, line_x) in &visual_lines {
3114                let start = selection_start.max(line_start);
3115                let end = selection_end.min(line_end);
3116                if start >= end {
3117                    continue;
3118                }
3119                let mut selection_left = line_x
3120                    + if uses_styled_runs {
3121                        self.te_measure_text_width_styled(
3122                            &styled_runs,
3123                            &text_bytes,
3124                            line_start,
3125                            start,
3126                        )
3127                    } else {
3128                        self.te_measure_text_width(font, size, &text_bytes, line_start, start)
3129                    };
3130                let selection_right = line_x
3131                    + if uses_styled_runs {
3132                        self.te_measure_text_width_styled(
3133                            &styled_runs,
3134                            &text_bytes,
3135                            line_start,
3136                            end,
3137                        )
3138                    } else {
3139                        self.te_measure_text_width(font, size, &text_bytes, line_start, end)
3140                    };
3141                if start == line_start && matches!(just, 0 | -2) {
3142                    // BasiliskII/System 7.5.3 `a9d3_teupdate_selection_visual`
3143                    // shows a selection beginning at a left-justified visual
3144                    // line includes TextEdit's one-pixel left inset; glyphs
3145                    // still draw at destRect.left + TE_LINE_LEFT_INSET.
3146                    selection_left = selection_left.saturating_sub(Self::TE_LINE_LEFT_INSET);
3147                }
3148                if self.ui_theme_id() == UiThemeId::ClassicSystem7 {
3149                    if te_active {
3150                        self.draw_rect(
3151                            cpu,
3152                            bus,
3153                            &Rect {
3154                                top: line_top,
3155                                left: selection_left,
3156                                bottom: line_bottom,
3157                                right: selection_right,
3158                            },
3159                            ShapeOp::Invert,
3160                        );
3161                    }
3162                } else {
3163                    // Text 1993, "Outline Highlighting" and TEFeatureFlag
3164                    // teFOutlineHilite: inactive selection ranges are framed,
3165                    // while active ranges keep normal highlighted selection chrome.
3166                    self.draw_theme_text_selection(
3167                        bus,
3168                        line_top,
3169                        selection_left,
3170                        line_bottom,
3171                        selection_right,
3172                        te_active,
3173                    );
3174                }
3175            }
3176        }
3177
3178        self.tx_font = old_font;
3179        self.tx_face = old_face;
3180        self.tx_size = old_size;
3181        self.fg_color = old_fg;
3182        self.pn_loc = old_loc;
3183        if trace_textedit_enabled() {
3184            eprintln!(
3185                "[TE] draw_te_contents visible_lines hTE=${:08X} {:?}",
3186                te_handle, visible_lines
3187            );
3188            eprintln!(
3189                "[TE] draw_te_contents checksum hTE=${:08X} before={} after={}",
3190                te_handle,
3191                checksum_before,
3192                checksum_view_rect(bus, self.current_port, view_rect)
3193            );
3194        }
3195
3196        if switched_port {
3197            self.set_current_port_state(bus, cpu, previous_port, Some(previous_gdevice));
3198        }
3199    }
3200
3201    fn te_feature_bit(&self, te_handle: u32, feature: u16) -> bool {
3202        let mask = 1u16.checked_shl(feature as u32).unwrap_or(0);
3203        self.textedit_states
3204            .get(&te_handle)
3205            .map(|state| (state.feature_bits & mask) != 0)
3206            .unwrap_or(false)
3207    }
3208
3209    /// Whether auto-scroll is enabled on the TextEdit record at
3210    /// `te_handle` (set by `TEAutoView` per IM:V V-173). Public
3211    /// projection over `textedit_states[te].feature_bits` so
3212    /// integration tests can observe TEAutoView's effect without
3213    /// access to the private feature-bit constants.
3214    pub fn te_auto_scroll_enabled(&self, te_handle: u32) -> bool {
3215        self.te_feature_bit(te_handle, Self::TE_FEATURE_AUTO_SCROLL)
3216    }
3217
3218    fn set_te_feature_bit(&mut self, te_handle: u32, feature: u16, enabled: bool) {
3219        let mask = 1u16.checked_shl(feature as u32).unwrap_or(0);
3220        let state = self.textedit_states.entry(te_handle).or_default();
3221        if enabled {
3222            state.feature_bits |= mask;
3223        } else {
3224            state.feature_bits &= !mask;
3225        }
3226    }
3227
3228    fn initialize_dialog_item_handles(
3229        &mut self,
3230        bus: &mut MacMemoryBus,
3231        dialog_ptr: u32,
3232        items: &[DialogItem],
3233    ) {
3234        self.initialize_dialog_item_handles_from(bus, dialog_ptr, items, 0);
3235    }
3236
3237    fn initialize_dialog_item_handles_from(
3238        &mut self,
3239        bus: &mut MacMemoryBus,
3240        dialog_ptr: u32,
3241        items: &[DialogItem],
3242        start_index: usize,
3243    ) {
3244        // Item lists in memory hold live item handles or userItem ProcPtrs.
3245        // GetNewDialog reaches here with a copy of the DITL resource; NewDialog
3246        // initializes the caller-supplied item-list handle.
3247        // Inside Macintosh Volume I, I-405, I-412; MTE 1992 p. 6-114.
3248        for (index, item) in items.iter().enumerate().skip(start_index) {
3249            let item_no = index as i16 + 1;
3250            let Some(item_handle_addr) = Self::dialog_item_handle_addr(bus, dialog_ptr, item_no)
3251            else {
3252                continue;
3253            };
3254
3255            let base_type = item.item_type & 0x7F;
3256            let item_handle = match base_type {
3257                8 | 16 => {
3258                    let text_bytes = item.text.as_bytes();
3259                    let handle = bus.alloc(4);
3260                    let text_ptr = if text_bytes.is_empty() {
3261                        0
3262                    } else {
3263                        let ptr = bus.alloc(text_bytes.len() as u32);
3264                        bus.write_bytes(ptr, text_bytes);
3265                        ptr
3266                    };
3267                    bus.write_long(handle, text_ptr);
3268                    self.dialog_item_handles.insert(handle, (dialog_ptr, index));
3269                    handle
3270                }
3271                32 => self
3272                    .find_resource_any(*b"ICON", item.resource_id)
3273                    .map(|(_, ptr)| {
3274                        self.get_or_create_resource_handle(bus, *b"ICON", item.resource_id, ptr)
3275                    })
3276                    .unwrap_or(0),
3277                4..=6 => {
3278                    let existing = bus.read_long(item_handle_addr);
3279                    if existing != 0 {
3280                        existing
3281                    } else {
3282                        self.create_standard_dialog_control_handle(bus, dialog_ptr, item_no, item)
3283                    }
3284                }
3285                7 => self
3286                    .find_resource_any(*b"CNTL", item.resource_id)
3287                    .map(|(_, cntl_ptr)| {
3288                        let value = bus.read_word(cntl_ptr + 8) as i16;
3289                        let vis_word = bus.read_word(cntl_ptr + 10);
3290                        let max = bus.read_word(cntl_ptr + 12) as i16;
3291                        let min = bus.read_word(cntl_ptr + 14) as i16;
3292                        let proc_id = bus.read_word(cntl_ptr + 16) as i16;
3293                        let ref_con = bus.read_long(cntl_ptr + 18);
3294                        let title_len = bus.read_byte(cntl_ptr + 22) as usize;
3295                        let title = bus.read_bytes(cntl_ptr + 23, title_len.min(255));
3296                        let ctrl_ptr = bus.alloc(296);
3297                        let handle = bus.alloc(4);
3298                        bus.write_long(handle, ctrl_ptr);
3299                        self.initialize_control_record(
3300                            bus,
3301                            ctrl_ptr,
3302                            dialog_ptr,
3303                            item.rect,
3304                            &title,
3305                            vis_word != 0,
3306                            value,
3307                            min,
3308                            max,
3309                            proc_id,
3310                            ref_con,
3311                        );
3312                        self.ensure_control_aux_record(bus, handle);
3313                        let old_head = bus.read_long(dialog_ptr + 140);
3314                        bus.write_long(ctrl_ptr, old_head);
3315                        bus.write_long(dialog_ptr + 140, handle);
3316                        self.dialog_control_handles
3317                            .insert(handle, (dialog_ptr, item_no));
3318                        self.dialog_control_values
3319                            .insert((dialog_ptr, item_no), value);
3320                        handle
3321                    })
3322                    .unwrap_or(0),
3323                64 => self
3324                    .find_resource_any(*b"PICT", item.resource_id)
3325                    .map(|(_, ptr)| {
3326                        self.get_or_create_resource_handle(bus, *b"PICT", item.resource_id, ptr)
3327                    })
3328                    .unwrap_or(0),
3329                0 => item.proc_ptr,
3330                _ => 0,
3331            };
3332
3333            bus.write_long(item_handle_addr, item_handle);
3334        }
3335    }
3336
3337    fn create_standard_dialog_control_handle(
3338        &mut self,
3339        bus: &mut MacMemoryBus,
3340        dialog_ptr: u32,
3341        item_no: i16,
3342        item: &DialogItem,
3343    ) -> u32 {
3344        let proc_id = match item.item_type & 0x7F {
3345            4 => 0, // btnCtrl -> pushButProc
3346            5 => 1, // chkCtrl -> checkBoxProc
3347            6 => 2, // radCtrl -> radioButProc
3348            _ => return 0,
3349        };
3350        let value = self
3351            .dialog_control_values
3352            .get(&(dialog_ptr, item_no))
3353            .copied()
3354            .unwrap_or(0);
3355        let ctrl_ptr = bus.alloc(296);
3356        let handle = bus.alloc(4);
3357        if ctrl_ptr == 0 || handle == 0 {
3358            return 0;
3359        }
3360
3361        bus.write_long(handle, ctrl_ptr);
3362        self.initialize_control_record(
3363            bus,
3364            ctrl_ptr,
3365            dialog_ptr,
3366            item.rect,
3367            item.text.as_bytes(),
3368            true,
3369            value,
3370            0,
3371            1,
3372            proc_id,
3373            0,
3374        );
3375        self.ensure_control_aux_record(bus, handle);
3376
3377        let old_head = bus.read_long(dialog_ptr + 140);
3378        bus.write_long(ctrl_ptr, old_head);
3379        bus.write_long(dialog_ptr + 140, handle);
3380        self.dialog_control_handles
3381            .insert(handle, (dialog_ptr, item_no));
3382        self.dialog_control_values
3383            .insert((dialog_ptr, item_no), value);
3384        handle
3385    }
3386
3387    fn ditl_item_record_len_at(
3388        bus: &MacMemoryBus,
3389        ptr: u32,
3390        data_len: u32,
3391        record_offset: u32,
3392    ) -> Option<u32> {
3393        if record_offset + 14 > data_len {
3394            return None;
3395        }
3396
3397        let item_type = bus.read_byte(ptr + record_offset + 12);
3398        let data_len_byte = bus.read_byte(ptr + record_offset + 13);
3399        let base_type = item_type & 0x7F;
3400        let payload_offset = record_offset + 14;
3401        let remaining = data_len.saturating_sub(payload_offset);
3402        let payload_len = Self::ditl_item_payload_len(base_type, data_len_byte, remaining)?;
3403        let padded = (payload_len + 1) & !1;
3404        if padded > remaining {
3405            return None;
3406        }
3407
3408        Some(14 + padded)
3409    }
3410
3411    fn ditl_used_len(bus: &MacMemoryBus, ptr: u32, data_len: u32) -> u32 {
3412        if ptr == 0 || data_len < 2 {
3413            return data_len;
3414        }
3415
3416        let max_index = bus.read_word(ptr) as i16;
3417        let count = if max_index < 0 {
3418            0
3419        } else {
3420            max_index as usize + 1
3421        };
3422        let mut offset = 2u32;
3423        for _ in 0..count {
3424            let Some(record_len) = Self::ditl_item_record_len_at(bus, ptr, data_len, offset) else {
3425                break;
3426            };
3427            offset = offset.saturating_add(record_len);
3428        }
3429
3430        offset.min(data_len)
3431    }
3432
3433    fn dialog_local_size(bus: &MacMemoryBus, dialog_ptr: u32) -> (i16, i16) {
3434        let height = (bus.read_word(dialog_ptr + 20) as i16)
3435            .saturating_sub(bus.read_word(dialog_ptr + 16) as i16);
3436        let width = (bus.read_word(dialog_ptr + 22) as i16)
3437            .saturating_sub(bus.read_word(dialog_ptr + 18) as i16);
3438        (height, width)
3439    }
3440
3441    fn append_ditl_offset(&self, bus: &MacMemoryBus, dialog_ptr: u32, method: i16) -> (i16, i16) {
3442        match method {
3443            // overlayDITL: appended rectangles are already dialog-local.
3444            // Macintosh Toolbox Essentials 1992, pp. 6-108, 6-153.
3445            0 => (0, 0),
3446            // appendDITLRight / appendDITLBottom position relative to the
3447            // dialog's upper-right or lower-left coordinate. The HLE updates
3448            // item rectangles here; full window resizing is handled by callers
3449            // that subsequently show/redraw the dialog.
3450            1 => {
3451                let (_, width) = Self::dialog_local_size(bus, dialog_ptr);
3452                (0, width)
3453            }
3454            2 => {
3455                let (height, _) = Self::dialog_local_size(bus, dialog_ptr);
3456                (height, 0)
3457            }
3458            item_method if item_method < 0 => {
3459                let item_no = item_method.checked_neg().unwrap_or(i16::MAX) as usize;
3460                self.dialog_items
3461                    .get(&dialog_ptr)
3462                    .and_then(|items| item_no.checked_sub(1).and_then(|index| items.get(index)))
3463                    .map(|item| (item.rect.0, item.rect.1))
3464                    .unwrap_or((0, 0))
3465            }
3466            _ => (0, 0),
3467        }
3468    }
3469
3470    fn offset_dialog_item_rect(item: &mut DialogItem, v_delta: i16, h_delta: i16) {
3471        item.rect = (
3472            item.rect.0.saturating_add(v_delta),
3473            item.rect.1.saturating_add(h_delta),
3474            item.rect.2.saturating_add(v_delta),
3475            item.rect.3.saturating_add(h_delta),
3476        );
3477    }
3478
3479    fn erase_retained_dialog_items_after_ditl_shorten(
3480        &mut self,
3481        bus: &mut MacMemoryBus,
3482        dialog_ptr: u32,
3483        removed_items: &[DialogItem],
3484    ) {
3485        if removed_items.is_empty() || dialog_ptr == 0 {
3486            return;
3487        }
3488        let bounds = Self::dialog_screen_bounds(bus, dialog_ptr);
3489        for item in removed_items {
3490            let (top, left, bottom, right) = Self::dialog_item_screen_rect(bounds, item.rect);
3491            self.fill_rect_clipped_to_dialog(
3492                bus,
3493                bounds,
3494                (top - 2, left - 2, bottom + 2, right + 2),
3495                false,
3496            );
3497        }
3498        if self.dialog_visible_snapshots.contains_key(&dialog_ptr) {
3499            let pixels = self.save_dialog_pixels(bus, bounds);
3500            self.dialog_visible_snapshots
3501                .insert(dialog_ptr, PersistentDialogSnapshot { bounds, pixels });
3502        }
3503        let tracking_bounds = self
3504            .dialog_tracking
3505            .as_ref()
3506            .filter(|tracking| tracking.dialog_ptr == dialog_ptr)
3507            .map(|tracking| tracking.bounds);
3508        if let Some(bounds) = tracking_bounds {
3509            let rendered = self.save_dialog_pixels(bus, bounds);
3510            if let Some(tracking) = self.dialog_tracking.as_mut() {
3511                if tracking.dialog_ptr == dialog_ptr {
3512                    tracking.rendered_pixels = rendered;
3513                    tracking.rendered_pixels_final = true;
3514                }
3515            }
3516        }
3517    }
3518
3519    pub(crate) fn append_ditl_to_dialog(
3520        &mut self,
3521        bus: &mut MacMemoryBus,
3522        dialog_ptr: u32,
3523        ditl_handle: u32,
3524        method: i16,
3525    ) -> u16 {
3526        if dialog_ptr == 0 || ditl_handle == 0 {
3527            return self
3528                .dialog_items
3529                .get(&dialog_ptr)
3530                .map(|items| items.len() as u16)
3531                .unwrap_or(0);
3532        }
3533
3534        let source_ptr = bus.read_long(ditl_handle);
3535        let Some(source_alloc_len) = bus.get_alloc_size(source_ptr) else {
3536            return self
3537                .dialog_items
3538                .get(&dialog_ptr)
3539                .map(|items| items.len() as u16)
3540                .unwrap_or(0);
3541        };
3542        let source_len = Self::ditl_used_len(bus, source_ptr, source_alloc_len);
3543        if source_len <= 2 {
3544            return self
3545                .dialog_items
3546                .get(&dialog_ptr)
3547                .map(|items| items.len() as u16)
3548                .unwrap_or(0);
3549        }
3550
3551        let mut appended_items = Self::parse_ditl(bus, source_ptr, source_len);
3552        if appended_items.is_empty() {
3553            return self
3554                .dialog_items
3555                .get(&dialog_ptr)
3556                .map(|items| items.len() as u16)
3557                .unwrap_or(0);
3558        }
3559
3560        let mut items_handle = bus.read_long(dialog_ptr + 156);
3561        if items_handle == 0 {
3562            items_handle = bus.alloc(4);
3563            let empty_ditl_ptr = bus.alloc(2);
3564            bus.write_word(empty_ditl_ptr, 0xFFFF);
3565            bus.write_long(items_handle, empty_ditl_ptr);
3566            bus.write_long(dialog_ptr + 156, items_handle);
3567        }
3568
3569        let old_ptr = bus.read_long(items_handle);
3570        let old_alloc_len = bus.get_alloc_size(old_ptr).unwrap_or(0);
3571        let old_len = if old_ptr != 0 && old_alloc_len >= 2 {
3572            Self::ditl_used_len(bus, old_ptr, old_alloc_len).max(2)
3573        } else {
3574            2
3575        };
3576        let raw_existing_items = if old_ptr != 0 && old_alloc_len >= 2 {
3577            Self::parse_ditl(bus, old_ptr, old_len)
3578        } else {
3579            Vec::new()
3580        };
3581        let mut all_items = self
3582            .dialog_items
3583            .get(&dialog_ptr)
3584            .cloned()
3585            .unwrap_or_else(|| raw_existing_items.clone());
3586        if all_items.len() != raw_existing_items.len() && !raw_existing_items.is_empty() {
3587            all_items = raw_existing_items;
3588        }
3589
3590        let start_index = all_items.len();
3591        let source_body_len = source_len.saturating_sub(2);
3592        let new_len = old_len.saturating_add(source_body_len);
3593        let new_ptr = bus.alloc(new_len);
3594        if new_ptr == 0 {
3595            return all_items.len() as u16;
3596        }
3597
3598        if old_ptr != 0 && old_alloc_len >= 2 {
3599            let old_bytes = bus.read_bytes(old_ptr, old_len as usize);
3600            bus.write_bytes(new_ptr, &old_bytes);
3601        } else {
3602            bus.write_word(new_ptr, 0xFFFF);
3603        }
3604        let source_body = bus.read_bytes(source_ptr + 2, source_body_len as usize);
3605        bus.write_bytes(new_ptr + old_len, &source_body);
3606
3607        let (v_delta, h_delta) = self.append_ditl_offset(bus, dialog_ptr, method);
3608        let mut source_offset = 2u32;
3609        let mut dest_offset = old_len;
3610        for item in &mut appended_items {
3611            if v_delta != 0 || h_delta != 0 {
3612                Self::offset_dialog_item_rect(item, v_delta, h_delta);
3613                bus.write_word(new_ptr + dest_offset + 4, item.rect.0 as u16);
3614                bus.write_word(new_ptr + dest_offset + 6, item.rect.1 as u16);
3615                bus.write_word(new_ptr + dest_offset + 8, item.rect.2 as u16);
3616                bus.write_word(new_ptr + dest_offset + 10, item.rect.3 as u16);
3617            }
3618
3619            let Some(record_len) =
3620                Self::ditl_item_record_len_at(bus, source_ptr, source_len, source_offset)
3621            else {
3622                break;
3623            };
3624            source_offset = source_offset.saturating_add(record_len);
3625            dest_offset = dest_offset.saturating_add(record_len);
3626        }
3627
3628        all_items.extend(appended_items);
3629        let new_count = all_items.len();
3630        let max_index = if new_count == 0 {
3631            0xFFFF
3632        } else {
3633            (new_count as u16).saturating_sub(1)
3634        };
3635        bus.write_word(new_ptr, max_index);
3636        bus.write_long(items_handle, new_ptr);
3637
3638        self.dialog_items.insert(dialog_ptr, all_items.clone());
3639        self.initialize_dialog_item_handles_from(bus, dialog_ptr, &all_items, start_index);
3640
3641        new_count as u16
3642    }
3643
3644    pub(crate) fn shorten_ditl_in_dialog(
3645        &mut self,
3646        bus: &mut MacMemoryBus,
3647        dialog_ptr: u32,
3648        number_items: u16,
3649    ) -> u16 {
3650        if dialog_ptr == 0 {
3651            return 0;
3652        }
3653
3654        let items_handle = bus.read_long(dialog_ptr + 156);
3655        let ditl_ptr = if items_handle != 0 {
3656            bus.read_long(items_handle)
3657        } else {
3658            0
3659        };
3660        let ditl_len = bus.get_alloc_size(ditl_ptr).unwrap_or(0);
3661        let raw_items = if ditl_ptr != 0 && ditl_len >= 2 {
3662            Self::parse_ditl(bus, ditl_ptr, Self::ditl_used_len(bus, ditl_ptr, ditl_len))
3663        } else {
3664            Vec::new()
3665        };
3666        let mut items = self
3667            .dialog_items
3668            .get(&dialog_ptr)
3669            .cloned()
3670            .unwrap_or(raw_items);
3671        let remove_count = (number_items as usize).min(items.len());
3672        let keep_count = items.len().saturating_sub(remove_count);
3673
3674        for item_no in (keep_count + 1)..=items.len() {
3675            let item_no = item_no as i16;
3676            let handle = Self::dialog_item_handle(bus, dialog_ptr, item_no);
3677            self.dialog_control_values.remove(&(dialog_ptr, item_no));
3678            self.dialog_control_handles.remove(&handle);
3679            self.dialog_item_handles.remove(&handle);
3680        }
3681
3682        let removed_items = items[keep_count..].to_vec();
3683        items.truncate(keep_count);
3684        if ditl_ptr != 0 && ditl_len >= 2 {
3685            let max_index = if keep_count == 0 {
3686                0xFFFF
3687            } else {
3688                (keep_count as u16).saturating_sub(1)
3689            };
3690            bus.write_word(ditl_ptr, max_index);
3691        }
3692        self.dialog_items.insert(dialog_ptr, items);
3693        self.erase_retained_dialog_items_after_ditl_shorten(bus, dialog_ptr, &removed_items);
3694
3695        keep_count as u16
3696    }
3697
3698    // ========== DLOG / DITL Resource Parsing ==========
3699
3700    /// Parse a DLOG resource from guest memory.
3701    /// Returns (bounds, procID, visible, itemsID, title, position).
3702    /// Inside Macintosh Volume I, I-437
3703    /// Macintosh Toolbox Essentials 1992, p. 6-148
3704    fn parse_dlog(
3705        bus: &MacMemoryBus,
3706        ptr: u32,
3707        data_len: u32,
3708    ) -> ((i16, i16, i16, i16), i16, bool, i16, String, u16) {
3709        let top = bus.read_word(ptr) as i16;
3710        let left = bus.read_word(ptr + 2) as i16;
3711        let bottom = bus.read_word(ptr + 4) as i16;
3712        let right = bus.read_word(ptr + 6) as i16;
3713        let proc_id = bus.read_word(ptr + 8) as i16;
3714        let visible = bus.read_byte(ptr + 10) != 0;
3715        // +11: filler
3716        // +12: goAwayFlag (1 byte)
3717        // +13: filler
3718        let _ref_con = bus.read_long(ptr + 14);
3719        let items_id = bus.read_word(ptr + 18) as i16;
3720        // +20: title as Pascal string
3721        let title_len = bus.read_byte(ptr + 20) as usize;
3722        let mut title_bytes = vec![0u8; title_len];
3723        for (i, byte) in title_bytes.iter_mut().enumerate() {
3724            *byte = bus.read_byte(ptr + 21 + i as u32);
3725        }
3726        let title = decode_mac_roman_for_render(&title_bytes);
3727
3728        // Read positioning constant after the title Pascal string.
3729        // Macintosh Toolbox Essentials 1992, pp. 4-125 to 4-126
3730        // The position word follows the title, padded to an even boundary.
3731        let title_end = 21 + title_len as u32;
3732        let padded_end = (title_end + 1) & !1;
3733        let position = if padded_end + 2 <= data_len {
3734            bus.read_word(ptr + padded_end)
3735        } else {
3736            0
3737        };
3738
3739        (
3740            (top, left, bottom, right),
3741            proc_id,
3742            visible,
3743            items_id,
3744            title,
3745            position,
3746        )
3747    }
3748
3749    /// Parse an ALRT resource from guest memory.
3750    /// Returns (bounds, itemsID, stages, position).
3751    /// Inside Macintosh Volume I, I-425 to I-426
3752    /// Macintosh Toolbox Essentials 1992, p. 6-150
3753    fn parse_alrt(
3754        bus: &MacMemoryBus,
3755        ptr: u32,
3756        data_len: u32,
3757    ) -> ((i16, i16, i16, i16), i16, u16, u16) {
3758        let bounds = if data_len >= 8 {
3759            (
3760                bus.read_word(ptr) as i16,
3761                bus.read_word(ptr + 2) as i16,
3762                bus.read_word(ptr + 4) as i16,
3763                bus.read_word(ptr + 6) as i16,
3764            )
3765        } else {
3766            (0, 0, 0, 0)
3767        };
3768        let items_id = if data_len >= 10 {
3769            bus.read_word(ptr + 8) as i16
3770        } else {
3771            0
3772        };
3773        let stages = if data_len >= 12 {
3774            bus.read_word(ptr + 10)
3775        } else {
3776            0
3777        };
3778        // System 7 compiled ALRT resources append the same positioning
3779        // constants as DLOG resources after the 12-byte classic template.
3780        // Macintosh Toolbox Essentials 1992, p. 6-150
3781        let position = if data_len >= 14 {
3782            bus.read_word(ptr + 12)
3783        } else {
3784            0
3785        };
3786        (bounds, items_id, stages, position)
3787    }
3788
3789    fn positioned_dialog_bounds(
3790        &self,
3791        mut bounds: (i16, i16, i16, i16),
3792        position: u16,
3793    ) -> (i16, i16, i16, i16) {
3794        match position {
3795            // alertPositionMainScreen / ParentWindow / ParentWindowScreen:
3796            // MTE 1992 p. 4-126 defines alert position as about one-fifth
3797            // of the unused screen/window space above the new window.
3798            0x300A | 0x700A | 0xB00A => {
3799                let (_, _, screen_w, screen_h, _) = self.get_screen_params();
3800                let dialog_w = bounds.3 - bounds.1;
3801                let dialog_h = bounds.2 - bounds.0;
3802                let new_left = (screen_w - dialog_w) / 2;
3803                let new_top = (screen_h - dialog_h) / 5;
3804                bounds = (new_top, new_left, new_top + dialog_h, new_left + dialog_w);
3805            }
3806            // centerMainScreen / centerParentWindow: true vertical center
3807            // Macintosh Toolbox Essentials 1992, p. 4-126
3808            0x280A | 0x680A | 0xA80A | 0x380A => {
3809                let (_, _, screen_w, screen_h, _) = self.get_screen_params();
3810                let dialog_w = bounds.3 - bounds.1;
3811                let dialog_h = bounds.2 - bounds.0;
3812                let new_left = (screen_w - dialog_w) / 2;
3813                let new_top = (screen_h - dialog_h) / 2;
3814                bounds = (new_top, new_left, new_top + dialog_h, new_left + dialog_w);
3815            }
3816            _ => {} // noAutoCenter (0x0000) or unknown: use raw bounds
3817        }
3818        bounds
3819    }
3820
3821    fn alert_stage_default_item(stages: u16, stage_word: u16) -> (u32, u32, Option<i16>) {
3822        let stage_idx = (stage_word as u32).min(3);
3823        // Each stage occupies 4 bits, stage 1 in the low nibble.
3824        let nibble = ((stages as u32) >> (stage_idx * 4)) & 0xF;
3825        let box_drawn = (nibble & 0x04) != 0;
3826        let default_item = if (nibble & 0x08) == 0 { 1 } else { 2 };
3827        (stage_idx, nibble, box_drawn.then_some(default_item))
3828    }
3829
3830    fn enabled_button_count(items: &[DialogItem]) -> usize {
3831        items
3832            .iter()
3833            .filter(|item| (item.item_type & 0x7F) == 4 && (item.item_type & 0x80) == 0)
3834            .count()
3835    }
3836
3837    fn begin_interactive_alert<C: CpuOps>(
3838        &mut self,
3839        cpu: &mut C,
3840        bus: &mut MacMemoryBus,
3841        sp: u32,
3842        alert_id: i16,
3843        _filter_proc: u32,
3844        bounds: (i16, i16, i16, i16),
3845        items_id: i16,
3846        position: u16,
3847        default_item: i16,
3848    ) -> bool {
3849        let Some((_, ditl_data)) = self.find_resource_any(*b"DITL", items_id) else {
3850            return false;
3851        };
3852        let ditl_len = bus.get_alloc_size(ditl_data).unwrap_or(0);
3853        let items = Self::parse_ditl(bus, ditl_data, ditl_len);
3854        if Self::enabled_button_count(&items) <= 1 {
3855            return false;
3856        }
3857
3858        let items_handle = {
3859            let handle = bus.alloc(4);
3860            bus.write_long(handle, ditl_data);
3861            Self::duplicate_handle_data(bus, handle)
3862        };
3863        let bounds = self.positioned_dialog_bounds(bounds, position);
3864        let dialog_ptr = self.finish_dialog_creation(
3865            bus,
3866            cpu,
3867            0,
3868            bounds,
3869            "",
3870            true,
3871            1,
3872            false,
3873            alert_id as u32,
3874            items_handle,
3875            items.clone(),
3876        );
3877        if dialog_ptr == 0 {
3878            return false;
3879        }
3880        bus.write_word(dialog_ptr + 168, default_item as u16);
3881
3882        let saved_pixels = self
3883            .dialog_saved_pixels
3884            .get(&dialog_ptr)
3885            .cloned()
3886            .unwrap_or_else(|| self.save_dialog_pixels(bus, bounds));
3887        let rendered_pixels = self.save_dialog_pixels(bus, bounds);
3888        bus.write_word(sp + 6, 0);
3889        self.dialog_modal_entered.insert(dialog_ptr);
3890        self.dialog_tracking = Some(DialogTrackingState {
3891            dialog_ptr,
3892            bounds,
3893            title: String::new(),
3894            proc_id: 1,
3895            items,
3896            default_item,
3897            cancel_item: 0,
3898            edit_text: String::new(),
3899            edit_item: 0,
3900            saved_pixels,
3901            // Alert is a Pascal FUNCTION with a 6-byte argument frame
3902            // (filterProc + alertID) and a 2-byte result slot at SP+6. The
3903            // shared dialog completion code writes A7 as stack_ptr + 8, so
3904            // store SP-2 for alerts.
3905            stack_ptr: sp.wrapping_sub(2),
3906            item_hit_ptr: sp + 6,
3907            rendered_pixels,
3908            flash_remaining: 0,
3909            flash_delay: 0,
3910            flash_item: 0,
3911            edit_text_modified: false,
3912            draw_proc_queue: VecDeque::new(),
3913            draw_procs_done: true,
3914            rendered_pixels_final: true,
3915            filter_proc: 0,
3916            game_managed: false,
3917            last_filter_event: None,
3918            popup_draws: Vec::new(),
3919            active_popup: None,
3920            active_button: None,
3921            active_user_item: None,
3922        });
3923        true
3924    }
3925
3926    fn finish_interactive_alert<C: CpuOps>(
3927        &mut self,
3928        cpu: &mut C,
3929        bus: &mut MacMemoryBus,
3930        hit: i16,
3931    ) {
3932        let Some(saved) = self.dialog_tracking.take() else {
3933            return;
3934        };
3935        if saved.item_hit_ptr != 0 {
3936            bus.write_word(saved.item_hit_ptr, hit as u16);
3937        }
3938        let stack_after = saved.stack_ptr + 8;
3939        let dialog_ptr = saved.dialog_ptr;
3940        self.dialog_saved_pixels
3941            .insert(dialog_ptr, saved.saved_pixels.clone());
3942        self.close_dialog_window(bus, cpu, dialog_ptr, true);
3943        cpu.write_reg(Register::A7, stack_after);
3944    }
3945
3946    fn handle_interactive_alert_refire<C: CpuOps>(&mut self, cpu: &mut C, bus: &mut MacMemoryBus) {
3947        if self.dialog_tracking.is_none() {
3948            return;
3949        }
3950
3951        if self
3952            .dialog_tracking
3953            .as_ref()
3954            .is_some_and(|tracking| tracking.active_button.is_some())
3955        {
3956            self.handle_dialog_button_tracking(bus);
3957            return;
3958        }
3959
3960        if self
3961            .dialog_tracking
3962            .as_ref()
3963            .is_some_and(|tracking| tracking.flash_remaining > 0)
3964        {
3965            let (remaining, button_draw, finished_hit) = {
3966                let t = self.dialog_tracking.as_mut().unwrap();
3967                if t.flash_delay > 0 {
3968                    t.flash_delay -= 1;
3969                    return;
3970                }
3971                t.flash_remaining -= 1;
3972                t.flash_delay = 3;
3973                let remaining = t.flash_remaining;
3974                let flash_item = t.flash_item;
3975                let button_draw =
3976                    if remaining > 0 && flash_item > 0 && (flash_item as usize) <= t.items.len() {
3977                        let item = &t.items[(flash_item - 1) as usize];
3978                        Some((
3979                            Self::dialog_item_screen_rect(t.bounds, item.rect),
3980                            item.text.clone(),
3981                            flash_item == t.default_item,
3982                            remaining % 2 == 0,
3983                        ))
3984                    } else {
3985                        None
3986                    };
3987                (remaining, button_draw, flash_item)
3988            };
3989
3990            if let Some((screen_rect, title, is_default, highlighted)) = button_draw {
3991                self.draw_dialog_button_highlight_state(
3992                    bus,
3993                    screen_rect,
3994                    &title,
3995                    is_default,
3996                    highlighted,
3997                );
3998            }
3999            if remaining == 0 {
4000                self.finish_interactive_alert(cpu, bus, finished_hit);
4001            }
4002            return;
4003        }
4004
4005        let event = if !self.event_queue.is_empty() {
4006            let mut event = None;
4007            while let Some(e) = self.event_queue.pop_front() {
4008                match e.what {
4009                    1 | 2 | 3 | 6 => {
4010                        event = Some(e);
4011                        break;
4012                    }
4013                    _ => {}
4014                }
4015            }
4016            event
4017        } else {
4018            None
4019        };
4020
4021        let Some(event) = event else {
4022            return;
4023        };
4024
4025        match event.what {
4026            1 => {
4027                let (dialog_ptr, bounds, items, default_item) = {
4028                    let tracking = self.dialog_tracking.as_ref().unwrap();
4029                    (
4030                        tracking.dialog_ptr,
4031                        tracking.bounds,
4032                        tracking.items.clone(),
4033                        tracking.default_item,
4034                    )
4035                };
4036                let mut hit = self.dialog_item_hit_test(
4037                    bus,
4038                    &items,
4039                    bounds,
4040                    event.where_v,
4041                    event.where_h,
4042                    &self.dialog_popup_original_rects,
4043                    dialog_ptr,
4044                );
4045                if hit <= 0 {
4046                    hit =
4047                        Self::dialog_button_hit_test(&items, bounds, event.where_v, event.where_h);
4048                }
4049                if hit <= 0 {
4050                    return;
4051                }
4052                let item = &items[(hit - 1) as usize];
4053                let base_type = item.item_type & 0x7F;
4054                let is_disabled = (item.item_type & 0x80) != 0;
4055                if base_type != 4 || is_disabled {
4056                    return;
4057                }
4058                let rect = Self::dialog_item_screen_rect(bounds, item.rect);
4059                let is_default = hit == default_item;
4060                self.draw_dialog_button_highlight_state(bus, rect, &item.text, is_default, true);
4061                if self.mouse_button {
4062                    let tracking = self.dialog_tracking.as_mut().unwrap();
4063                    tracking.active_button = Some(super::dispatch::DialogButtonTrackingState {
4064                        item_no: hit,
4065                        rect: item.rect,
4066                        title: item.text.clone(),
4067                        is_default,
4068                        highlighted: true,
4069                    });
4070                } else {
4071                    self.start_dialog_button_flash(
4072                        bus, bounds, hit, item.rect, &item.text, is_default, true,
4073                    );
4074                }
4075            }
4076            3 => {
4077                let (default_item, cancel_item) = self
4078                    .dialog_tracking
4079                    .as_ref()
4080                    .map(|tracking| (tracking.default_item, tracking.cancel_item))
4081                    .unwrap_or((0, 0));
4082                let key_code = (event.message >> 8) as u16;
4083                let char_code = (event.message & 0xFF) as u8;
4084                let hit = if char_code == b'\r' || char_code == 0x03 {
4085                    default_item
4086                } else if char_code == 0x1B || key_code == 0x35 {
4087                    cancel_item
4088                } else {
4089                    0
4090                };
4091                if hit > 0 {
4092                    self.finish_interactive_alert(cpu, bus, hit);
4093                }
4094            }
4095            6 => {
4096                let Some(tracking) = self.dialog_tracking.as_ref() else {
4097                    return;
4098                };
4099                let dialog_ptr = tracking.dialog_ptr;
4100                let bounds = tracking.bounds;
4101                let items = tracking.items.clone();
4102                let default_item = tracking.default_item;
4103                self.begin_update_window(bus, dialog_ptr);
4104                self.set_current_port_state(bus, cpu, dialog_ptr, None);
4105                self.redraw_standard_dialog_items(
4106                    bus,
4107                    bounds,
4108                    &items,
4109                    default_item,
4110                    "",
4111                    0,
4112                    dialog_ptr,
4113                );
4114                let rendered = self.save_dialog_pixels(bus, bounds);
4115                if let Some(tracking) = self.dialog_tracking.as_mut() {
4116                    tracking.rendered_pixels = rendered;
4117                    tracking.rendered_pixels_final = true;
4118                }
4119                self.end_update_window(bus, dialog_ptr);
4120            }
4121            _ => {}
4122        }
4123    }
4124
4125    /// Parse a DITL resource from guest memory into a list of DialogItems.
4126    /// Inside Macintosh Volume I, I-439
4127    fn parse_ditl(bus: &MacMemoryBus, ptr: u32, data_len: u32) -> Vec<DialogItem> {
4128        if data_len < 2 {
4129            return Vec::new();
4130        }
4131        let max_index = bus.read_word(ptr) as i16; // number of items minus 1
4132        let count = if max_index < 0 {
4133            0
4134        } else {
4135            max_index as usize + 1
4136        };
4137        let mut items = Vec::with_capacity(count);
4138        let mut offset = 2u32; // skip dlgMaxIndex
4139
4140        for _ in 0..count {
4141            if offset + 14 > data_len {
4142                break;
4143            }
4144            // Read 4-byte handle/procPtr field.
4145            // For userItem types, the game may write a procedure pointer here
4146            // via SetDItem or direct memory manipulation.
4147            // Inside Macintosh Volume I, I-427
4148            let item_handle = bus.read_long(ptr + offset);
4149            offset += 4;
4150            // Read display rectangle
4151            let top = bus.read_word(ptr + offset) as i16;
4152            let left = bus.read_word(ptr + offset + 2) as i16;
4153            let bottom = bus.read_word(ptr + offset + 4) as i16;
4154            let right = bus.read_word(ptr + offset + 6) as i16;
4155            offset += 8;
4156            // Read type byte and data length
4157            let item_type = bus.read_byte(ptr + offset);
4158            let data_len_byte = bus.read_byte(ptr + offset + 1);
4159            offset += 2;
4160
4161            let base_type = item_type & 0x7F; // strip itemDisable bit
4162
4163            let mut text = String::new();
4164            let mut resource_id: i16 = 0;
4165            let remaining = data_len - offset;
4166            let Some(payload_len) =
4167                Self::ditl_item_payload_len(base_type, data_len_byte, remaining)
4168            else {
4169                break;
4170            };
4171            if matches!(base_type, 7 | 32 | 64) {
4172                resource_id = bus.read_word(ptr + offset) as i16;
4173            }
4174
4175            let padded = (payload_len + 1) & !1;
4176            if padded > remaining {
4177                break;
4178            }
4179
4180            if payload_len > 0 {
4181                match base_type {
4182                    // button (4), checkbox (5), radio (6), statText (8),
4183                    // editText (16): title/text data. IM:I I-427.
4184                    4 | 5 | 6 | 8 | 16 => {
4185                        let bytes = bus.read_bytes(ptr + offset, payload_len as usize);
4186                        text = decode_mac_roman_for_render(&bytes);
4187                    }
4188                    _ => {}
4189                }
4190            }
4191
4192            // Advance past data, padded to even boundary
4193            offset += padded;
4194
4195            // For userItems, itmhand is a ProcPtr, not a relocatable handle.
4196            // Dialog Manager preserves it in the duplicated DITL and calls it
4197            // during update/redraw. Some apps replace it later via SetDItem or
4198            // by writing the duplicated DITL directly.
4199            // Inside Macintosh Volume I, I-405, I-426 to I-427.
4200            let proc_ptr = if base_type == 0 { item_handle } else { 0 };
4201
4202            items.push(DialogItem {
4203                item_type,
4204                rect: (top, left, bottom, right),
4205                text,
4206                resource_id,
4207                proc_ptr,
4208                sel_start: 0,
4209                sel_end: 0,
4210            });
4211        }
4212        items
4213    }
4214
4215    fn ditl_item_payload_len(base_type: u8, data_len_byte: u8, remaining: u32) -> Option<u32> {
4216        let payload_len = match base_type {
4217            // userItem has only the byte after itmtype (reserved/empty),
4218            // not a following payload.
4219            0 => 0,
4220            // Help items use the byte after itmtype as a sized payload.
4221            // Macintosh Toolbox Essentials 1992, p. 6-154
4222            1 => u32::from(data_len_byte),
4223            // icon, picture, resCtrl: 2-byte resource ID. IM:I I-427
4224            // describes the byte after itmtype as length=2; MTE 1992
4225            // p. 6-153 documents the same compiled records as a
4226            // reserved byte plus the two-byte resource ID. Accept both.
4227            7 | 32 | 64 => {
4228                if remaining < 2 {
4229                    return None;
4230                }
4231                if data_len_byte >= 2 {
4232                    u32::from(data_len_byte)
4233                } else {
4234                    2
4235                }
4236            }
4237            _ => u32::from(data_len_byte),
4238        };
4239        Some(payload_len)
4240    }
4241
4242    /// Re-read userItem proc pointers from the DITL data in guest memory.
4243    /// The game may write proc pointers directly to the DITL handle data
4244    /// after GetNewDialog returns, bypassing SetDItem.
4245    /// Inside Macintosh Volume I, I-427
4246    fn refresh_ditl_proc_ptrs(bus: &MacMemoryBus, dialog_ptr: u32, items: &mut [DialogItem]) {
4247        // Read the items handle from the DialogRecord (offset 156)
4248        let items_handle = bus.read_long(dialog_ptr + 156);
4249        if items_handle == 0 {
4250            return;
4251        }
4252        let ditl_ptr = bus.read_long(items_handle);
4253        if ditl_ptr == 0 {
4254            return;
4255        }
4256
4257        // Walk the DITL data structure, reading the 4-byte itmhand field
4258        // for each item. Format: 2-byte count-1, then per item:
4259        //   4 bytes: itmhand (handle or proc pointer)
4260        //   8 bytes: itmr (Rect)
4261        //   1 byte: itmtype
4262        //   1 byte: itmlen
4263        //   itmlen bytes: data (padded to even)
4264        let mut offset = 2u32; // skip count word
4265        for item in items.iter_mut() {
4266            let handle = bus.read_long(ditl_ptr + offset);
4267            offset += 4; // itmhand
4268            offset += 8; // itmr (Rect)
4269            let _item_type = bus.read_byte(ditl_ptr + offset);
4270            let data_len = bus.read_byte(ditl_ptr + offset + 1) as u32;
4271            offset += 2; // itmtype + itmlen
4272            let padded = (data_len + 1) & !1;
4273            offset += padded;
4274
4275            let base_type = item.item_type & 0x7F;
4276            if base_type == 0 {
4277                item.proc_ptr = handle;
4278            }
4279        }
4280    }
4281
4282    fn duplicate_handle_data(bus: &mut MacMemoryBus, handle: u32) -> u32 {
4283        if handle == 0 {
4284            return 0;
4285        }
4286
4287        let data_ptr = bus.read_long(handle);
4288        let new_handle = bus.alloc(4);
4289        if data_ptr == 0 {
4290            bus.write_long(new_handle, 0);
4291            return new_handle;
4292        }
4293
4294        let size = bus.get_alloc_size(data_ptr).unwrap_or(0);
4295        let new_data_ptr = bus.alloc(size);
4296        for offset in 0..size {
4297            bus.write_byte(new_data_ptr + offset, bus.read_byte(data_ptr + offset));
4298        }
4299        bus.write_long(new_handle, new_data_ptr);
4300        new_handle
4301    }
4302
4303    fn dispose_dialog_handle_storage(&mut self, bus: &mut MacMemoryBus, handle: u32) {
4304        if handle == 0 {
4305            return;
4306        }
4307
4308        let resource_backing = self.loaded_handles.get(&handle).copied();
4309        self.detached_handles.remove(&handle);
4310        self.forget_resource_handle_index_for_handle(handle);
4311        self.loaded_handles.remove(&handle);
4312        self.resource_handle_files.remove(&handle);
4313        self.detached_handle_files.remove(&handle);
4314        self.handle_state_bits.remove(&handle);
4315
4316        let data_ptr = bus.read_long(handle);
4317        if resource_backing.is_none() {
4318            bus.free(data_ptr);
4319        }
4320        bus.free(handle);
4321    }
4322
4323    fn dispose_dialog_te_storage(&mut self, bus: &mut MacMemoryBus, te_handle: u32) {
4324        if te_handle == 0 {
4325            return;
4326        }
4327
4328        let te_ptr = bus.read_long(te_handle);
4329        if te_ptr != 0 {
4330            let h_text = bus.read_long(te_ptr + Self::TE_HTEXT_OFFSET);
4331            self.dispose_dialog_handle_storage(bus, h_text);
4332
4333            if Self::te_is_styled_record(bus, te_ptr) {
4334                let style_handle = bus.read_long(te_ptr + Self::TE_TX_FONT_OFFSET);
4335                self.dispose_dialog_handle_storage(bus, style_handle);
4336            }
4337            bus.free(te_ptr);
4338        }
4339        bus.free(te_handle);
4340        self.textedit_states.remove(&te_handle);
4341    }
4342
4343    fn dispose_dialog_control_storage(&mut self, bus: &mut MacMemoryBus, ctrl_handle: u32) {
4344        if ctrl_handle == 0 {
4345            return;
4346        }
4347
4348        let ctrl_ptr = bus.read_long(ctrl_handle);
4349        if ctrl_ptr != 0 {
4350            let owner = bus.read_long(ctrl_ptr + 4);
4351            if owner != 0 {
4352                let mut prev_handle = 0u32;
4353                let mut cur_handle = bus.read_long(owner + 140);
4354                while cur_handle != 0 {
4355                    let cur_ptr = bus.read_long(cur_handle);
4356                    if cur_ptr == 0 {
4357                        break;
4358                    }
4359                    if cur_handle == ctrl_handle {
4360                        let next = bus.read_long(cur_ptr);
4361                        if prev_handle == 0 {
4362                            bus.write_long(owner + 140, next);
4363                        } else {
4364                            let prev_ptr = bus.read_long(prev_handle);
4365                            if prev_ptr != 0 {
4366                                bus.write_long(prev_ptr, next);
4367                            }
4368                        }
4369                        break;
4370                    }
4371                    prev_handle = cur_handle;
4372                    cur_handle = bus.read_long(cur_ptr);
4373                }
4374            }
4375            self.release_control_aux_record(bus, ctrl_handle);
4376            self.control_proc_ids.remove(&ctrl_ptr);
4377            bus.free(ctrl_ptr);
4378        }
4379        bus.free(ctrl_handle);
4380    }
4381
4382    fn dialog_item_storage_handles_from_ditl(
4383        bus: &MacMemoryBus,
4384        dialog_ptr: u32,
4385    ) -> Vec<(u8, u32)> {
4386        let items_handle = bus.read_long(dialog_ptr + 156);
4387        if items_handle == 0 {
4388            return Vec::new();
4389        }
4390        let ditl_ptr = bus.read_long(items_handle);
4391        if ditl_ptr == 0 {
4392            return Vec::new();
4393        }
4394        let data_len = bus.get_alloc_size(ditl_ptr).unwrap_or(0);
4395        if data_len < 2 {
4396            return Vec::new();
4397        }
4398
4399        let max_index = bus.read_word(ditl_ptr) as i16;
4400        if max_index < 0 {
4401            return Vec::new();
4402        }
4403
4404        let mut handles = Vec::new();
4405        let mut offset = 2u32;
4406        for _ in 0..=max_index {
4407            if offset + 14 > data_len {
4408                break;
4409            }
4410
4411            let item_handle = bus.read_long(ditl_ptr + offset);
4412            offset += 12;
4413            let item_type = bus.read_byte(ditl_ptr + offset);
4414            let data_len_byte = bus.read_byte(ditl_ptr + offset + 1);
4415            offset += 2;
4416
4417            let base_type = item_type & 0x7F;
4418            handles.push((base_type, item_handle));
4419
4420            let payload_len = match base_type {
4421                0 => 0,
4422                7 | 32 | 64 if data_len_byte < 2 => 2,
4423                _ => u32::from(data_len_byte),
4424            };
4425            let padded = (payload_len + 1) & !1;
4426            if padded > data_len.saturating_sub(offset) {
4427                break;
4428            }
4429            offset += padded;
4430        }
4431
4432        handles
4433    }
4434
4435    fn clear_dialog_scoped_item_state(&mut self, dialog_ptr: u32) {
4436        self.dialog_items.remove(&dialog_ptr);
4437        self.dialog_item_handles
4438            .retain(|_, (dlg, _)| *dlg != dialog_ptr);
4439        self.dialog_control_handles
4440            .retain(|_, (dlg, _)| *dlg != dialog_ptr);
4441        self.dialog_control_values
4442            .retain(|(dlg, _), _| *dlg != dialog_ptr);
4443        self.dialog_edit_text_modified_items
4444            .retain(|(dlg, _)| *dlg != dialog_ptr);
4445        self.hidden_dialog_item_rects
4446            .retain(|(dlg, _), _| *dlg != dialog_ptr);
4447        self.dialog_item_popup_menus
4448            .retain(|(dlg, _), _| *dlg != dialog_ptr);
4449        self.dialog_popup_original_rects
4450            .retain(|(dlg, _), _| *dlg != dialog_ptr);
4451        self.dialog_popup_candidate_items
4452            .retain(|(dlg, _)| *dlg != dialog_ptr);
4453        if self
4454            .pending_dialog_popup_menu
4455            .is_some_and(|pending| pending.dialog_ptr == dialog_ptr)
4456        {
4457            self.pending_dialog_popup_menu = None;
4458        }
4459        self.dialog_cancel_items.remove(&dialog_ptr);
4460    }
4461
4462    fn dispose_dialog_owned_items(&mut self, bus: &mut MacMemoryBus, dialog_ptr: u32) {
4463        let can_read_dialog_record =
4464            self.dialog_items.contains_key(&dialog_ptr) || self.window_list.contains(&dialog_ptr);
4465        let mut text_handles = Vec::new();
4466        let mut control_handles = Vec::new();
4467
4468        if can_read_dialog_record {
4469            for (base_type, handle) in Self::dialog_item_storage_handles_from_ditl(bus, dialog_ptr)
4470            {
4471                match base_type {
4472                    8 | 16 => text_handles.push(handle),
4473                    4..=7 => control_handles.push(handle),
4474                    _ => {}
4475                }
4476            }
4477        }
4478
4479        text_handles.extend(
4480            self.dialog_item_handles
4481                .iter()
4482                .filter_map(|(&handle, &(dlg, _))| {
4483                    if dlg == dialog_ptr {
4484                        Some(handle)
4485                    } else {
4486                        None
4487                    }
4488                }),
4489        );
4490        control_handles.extend(self.dialog_control_handles.iter().filter_map(
4491            |(&handle, &(dlg, _))| {
4492                if dlg == dialog_ptr {
4493                    Some(handle)
4494                } else {
4495                    None
4496                }
4497            },
4498        ));
4499
4500        text_handles.sort_unstable();
4501        text_handles.dedup();
4502        control_handles.sort_unstable();
4503        control_handles.dedup();
4504
4505        for handle in text_handles {
4506            self.dispose_dialog_handle_storage(bus, handle);
4507        }
4508        for handle in control_handles {
4509            self.dispose_dialog_control_storage(bus, handle);
4510        }
4511
4512        if can_read_dialog_record {
4513            let text_h = bus.read_long(dialog_ptr + 160);
4514            self.dispose_dialog_te_storage(bus, text_h);
4515            bus.write_long(dialog_ptr + 160, 0);
4516        }
4517
4518        self.clear_dialog_scoped_item_state(dialog_ptr);
4519    }
4520
4521    fn dispose_dialog_record_and_item_list(&mut self, bus: &mut MacMemoryBus, dialog_ptr: u32) {
4522        if !self.dialog_items.contains_key(&dialog_ptr) && !self.window_list.contains(&dialog_ptr) {
4523            return;
4524        }
4525
4526        let items_handle = bus.read_long(dialog_ptr + 156);
4527        self.dispose_dialog_handle_storage(bus, items_handle);
4528        bus.free(dialog_ptr);
4529    }
4530
4531    pub(crate) fn dialog_screen_bounds(
4532        bus: &MacMemoryBus,
4533        dialog_ptr: u32,
4534    ) -> (i16, i16, i16, i16) {
4535        let port_height =
4536            bus.read_word(dialog_ptr + 20) as i16 - bus.read_word(dialog_ptr + 16) as i16;
4537        let port_width =
4538            bus.read_word(dialog_ptr + 22) as i16 - bus.read_word(dialog_ptr + 18) as i16;
4539
4540        let port_version = bus.read_word(dialog_ptr + 6);
4541        let is_cgraf = (port_version & 0xC000) == 0xC000;
4542        let (top, left) = if is_cgraf {
4543            let pixmap_handle = bus.read_long(dialog_ptr + 2);
4544            let pixmap_ptr = if pixmap_handle != 0 {
4545                bus.read_long(pixmap_handle)
4546            } else {
4547                0
4548            };
4549            if pixmap_ptr != 0 {
4550                (
4551                    -(bus.read_word(pixmap_ptr + 6) as i16),
4552                    -(bus.read_word(pixmap_ptr + 8) as i16),
4553                )
4554            } else {
4555                (0, 0)
4556            }
4557        } else {
4558            (
4559                -(bus.read_word(dialog_ptr + 8) as i16),
4560                -(bus.read_word(dialog_ptr + 10) as i16),
4561            )
4562        };
4563
4564        (top, left, top + port_height, left + port_width)
4565    }
4566
4567    fn dialog_edit_state(
4568        bus: &MacMemoryBus,
4569        dialog_ptr: u32,
4570        items: &[DialogItem],
4571    ) -> (String, i16, i16) {
4572        let default_item = match bus.read_word(dialog_ptr + 168) as i16 {
4573            value if value > 0 => value,
4574            _ => 1,
4575        };
4576
4577        let edit_field = bus.read_word(dialog_ptr + 164) as i16;
4578        let stored_edit_item = if edit_field >= 0 { edit_field + 1 } else { 0 };
4579        let stored_is_valid = stored_edit_item > 0
4580            && items
4581                .get((stored_edit_item - 1) as usize)
4582                .is_some_and(|item| (item.item_type & 0x7F) == 16 && (item.item_type & 0x80) == 0);
4583        let edit_item = if stored_is_valid {
4584            stored_edit_item
4585        } else {
4586            items
4587                .iter()
4588                .position(|item| (item.item_type & 0x7F) == 16 && (item.item_type & 0x80) == 0)
4589                .map(|idx| (idx + 1) as i16)
4590                .unwrap_or(0)
4591        };
4592        if edit_item <= 0 {
4593            return (String::new(), 0, default_item);
4594        }
4595
4596        let edit_text = Self::text_item_string_from_handle_if_present(
4597            bus,
4598            Self::dialog_item_handle(bus, dialog_ptr, edit_item),
4599        );
4600        if let Some(edit_text) = edit_text {
4601            return (edit_text, edit_item, default_item);
4602        }
4603
4604        let fallback = items
4605            .get((edit_item - 1) as usize)
4606            .map(|item| item.text.clone())
4607            .unwrap_or_default();
4608        (fallback, edit_item, default_item)
4609    }
4610
4611    fn sync_tracking_active_edit_item(tracking: &mut DialogTrackingState) {
4612        let edit_item = tracking.edit_item;
4613        if edit_item <= 0 {
4614            return;
4615        }
4616        if let Some(item) = tracking.items.get_mut((edit_item - 1) as usize) {
4617            if (item.item_type & 0x7F) == 16 {
4618                item.text = tracking.edit_text.clone();
4619            }
4620        }
4621    }
4622
4623    fn set_tracking_active_edit_selection(
4624        tracking: &mut DialogTrackingState,
4625        sel_start: usize,
4626        sel_end: usize,
4627    ) {
4628        let edit_item = tracking.edit_item;
4629        if edit_item <= 0 {
4630            return;
4631        }
4632        if let Some(item) = tracking.items.get_mut((edit_item - 1) as usize) {
4633            if (item.item_type & 0x7F) == 16 {
4634                let text_len = tracking.edit_text.len();
4635                item.sel_start = sel_start.min(text_len).min(i16::MAX as usize) as i16;
4636                item.sel_end = sel_end.min(text_len).min(i16::MAX as usize) as i16;
4637            }
4638        }
4639    }
4640
4641    fn textedit_key_result(
4642        existing: &[u8],
4643        sel_start: usize,
4644        sel_end: usize,
4645        key: u8,
4646    ) -> (Vec<u8>, usize) {
4647        let text_len = existing.len();
4648        let s = sel_start.min(text_len);
4649        let e = sel_end.min(text_len);
4650        let (s, e) = if s > e { (e, s) } else { (s, e) };
4651
4652        if key == 0x08 {
4653            if s != e {
4654                let mut merged = Vec::with_capacity(text_len - (e - s));
4655                merged.extend_from_slice(&existing[..s]);
4656                merged.extend_from_slice(&existing[e..]);
4657                return (merged, s);
4658            }
4659            if s > 0 {
4660                let mut merged = Vec::with_capacity(text_len - 1);
4661                merged.extend_from_slice(&existing[..s - 1]);
4662                merged.extend_from_slice(&existing[s..]);
4663                return (merged, s - 1);
4664            }
4665            return (existing.to_vec(), s);
4666        }
4667
4668        let mut merged = Vec::with_capacity(s + 1 + text_len.saturating_sub(e));
4669        merged.extend_from_slice(&existing[..s]);
4670        merged.push(key);
4671        merged.extend_from_slice(&existing[e..]);
4672        (merged, s + 1)
4673    }
4674
4675    fn apply_dialog_select_key_to_edit_item(
4676        &mut self,
4677        bus: &mut MacMemoryBus,
4678        dialog_ptr: u32,
4679        items: &mut [DialogItem],
4680        edit_item: i16,
4681        key: u8,
4682    ) -> bool {
4683        if edit_item <= 0 {
4684            return false;
4685        }
4686        let Some(item) = items.get_mut((edit_item - 1) as usize) else {
4687            return false;
4688        };
4689        let base_type = item.item_type & 0x7F;
4690        let is_disabled = (item.item_type & 0x80) != 0;
4691        if base_type != 16 || is_disabled {
4692            return false;
4693        }
4694
4695        let item_handle = Self::dialog_item_handle(bus, dialog_ptr, edit_item);
4696        let existing = Self::text_item_bytes_from_handle_if_present(bus, item_handle)
4697            .unwrap_or_else(|| item.text.as_bytes().to_vec());
4698        let text_handle = bus.read_long(dialog_ptr + 160);
4699        let te_ptr = Self::te_record_ptr(bus, text_handle);
4700        let (sel_start, sel_end) = if te_ptr != 0 {
4701            (
4702                bus.read_word(te_ptr + Self::TE_SEL_START_OFFSET) as usize,
4703                bus.read_word(te_ptr + Self::TE_SEL_END_OFFSET) as usize,
4704            )
4705        } else {
4706            (item.sel_start.max(0) as usize, item.sel_end.max(0) as usize)
4707        };
4708
4709        let (updated, insertion_point) =
4710            Self::textedit_key_result(&existing, sel_start, sel_end, key);
4711        let clamped_insertion = insertion_point.min(u16::MAX as usize) as u16;
4712        item.text = decode_mac_roman_for_render(&updated);
4713        item.sel_start = clamped_insertion as i16;
4714        item.sel_end = clamped_insertion as i16;
4715
4716        if item_handle != 0 {
4717            let len = updated.len().min(255);
4718            let data_ptr = Self::ensure_text_handle_size(bus, item_handle, len);
4719            if data_ptr != 0 && len > 0 {
4720                bus.write_bytes(data_ptr, &updated[..len]);
4721            }
4722        }
4723
4724        if te_ptr != 0 {
4725            self.te_set_text_contents(bus, text_handle, &updated);
4726            bus.write_word(te_ptr + Self::TE_SEL_START_OFFSET, clamped_insertion);
4727            bus.write_word(te_ptr + Self::TE_SEL_END_OFFSET, clamped_insertion);
4728        }
4729
4730        true
4731    }
4732
4733    fn activate_dialog_edit_item<C: CpuOps>(
4734        &mut self,
4735        bus: &mut MacMemoryBus,
4736        cpu: &mut C,
4737        dialog_ptr: u32,
4738        items: &[DialogItem],
4739        edit_item: i16,
4740    ) -> bool {
4741        if edit_item <= 0 {
4742            return false;
4743        }
4744        let Some(item) = items.get((edit_item - 1) as usize) else {
4745            return false;
4746        };
4747        if (item.item_type & 0x7F) != 16 || (item.item_type & 0x80) != 0 {
4748            return false;
4749        }
4750
4751        bus.write_word(dialog_ptr + 164, (edit_item - 1) as u16);
4752
4753        let text_handle = bus.read_long(dialog_ptr + 160);
4754        let te_ptr = Self::te_record_ptr(bus, text_handle);
4755        if te_ptr == 0 {
4756            return true;
4757        }
4758
4759        let item_handle = Self::dialog_item_handle(bus, dialog_ptr, edit_item);
4760        let text = Self::text_item_bytes_from_handle_if_present(bus, item_handle)
4761            .unwrap_or_else(|| item.text.as_bytes().to_vec());
4762        self.te_set_text_contents(bus, text_handle, &text);
4763
4764        let text_len = text.len().min(u16::MAX as usize);
4765        let sel_start = item.sel_start.max(0) as usize;
4766        let sel_end = item.sel_end.max(0) as usize;
4767        bus.write_word(
4768            te_ptr + Self::TE_SEL_START_OFFSET,
4769            sel_start.min(text_len) as u16,
4770        );
4771        bus.write_word(
4772            te_ptr + Self::TE_SEL_END_OFFSET,
4773            sel_end.min(text_len) as u16,
4774        );
4775        // MTE 1992 p. 6-139 and IM:I I-417: DialogSelect mouse-down in an
4776        // enabled editText item displays the insertion point or selection.
4777        // The Dialog Manager shares one TERecord across editText items, so
4778        // activating an item must make that record active before later null
4779        // events call TEIdle for caret blinking.
4780        bus.write_word(te_ptr + Self::TE_ACTIVE_OFFSET, 1);
4781        bus.write_long(te_ptr + Self::TE_CARET_TIME_OFFSET, self.tick_count);
4782        bus.write_word(te_ptr + Self::TE_CARET_STATE_OFFSET, 0);
4783        self.draw_te_contents(cpu, bus, text_handle);
4784        true
4785    }
4786
4787    fn dialog_item_selection_range(item: &DialogItem, text_len: usize) -> Option<(usize, usize)> {
4788        let start = (item.sel_start.max(0) as usize).min(text_len);
4789        let end = (item.sel_end.max(0) as usize).min(text_len);
4790        let (start, end) = if start <= end {
4791            (start, end)
4792        } else {
4793            (end, start)
4794        };
4795        (start < end).then_some((start, end))
4796    }
4797
4798    fn redraw_dialog_text_item(&mut self, bus: &mut MacMemoryBus, dialog_ptr: u32, item_no: i16) {
4799        if !self.window_visible(bus, dialog_ptr) {
4800            return;
4801        }
4802        let Some(items) = self.dialog_items.get(&dialog_ptr).cloned() else {
4803            return;
4804        };
4805        let Some(item) = items.get((item_no - 1) as usize) else {
4806            return;
4807        };
4808        let base_type = item.item_type & 0x7F;
4809        if base_type != 8 && base_type != 16 {
4810            return;
4811        }
4812
4813        let bounds = Self::dialog_screen_bounds(bus, dialog_ptr);
4814        let (top, left, bottom, right) = bounds;
4815        let (it, il, ib, ir) = item.rect;
4816        let abs_top = top + it;
4817        let abs_left = left + il;
4818        let abs_bottom = top + ib;
4819        let abs_right = left + ir;
4820        if abs_top >= bottom || abs_bottom <= top || abs_left >= right || abs_right <= left {
4821            return;
4822        }
4823
4824        let (edit_text, edit_item, default_item) = Self::dialog_edit_state(bus, dialog_ptr, &items);
4825        self.redraw_standard_dialog_items(
4826            bus,
4827            bounds,
4828            &items,
4829            default_item,
4830            &edit_text,
4831            edit_item,
4832            dialog_ptr,
4833        );
4834
4835        if self.dialog_visible_snapshots.contains_key(&dialog_ptr) {
4836            let pixels = self.save_dialog_pixels(bus, bounds);
4837            self.dialog_visible_snapshots
4838                .insert(dialog_ptr, PersistentDialogSnapshot { bounds, pixels });
4839        }
4840        self.capture_gui_frame(
4841            bus,
4842            &format!("set_dialog_item_text_{:08X}_{}", dialog_ptr, item_no),
4843        );
4844    }
4845
4846    fn flush_dialog_edit_item_texts(
4847        &mut self,
4848        bus: &mut MacMemoryBus,
4849        dialog_ptr: u32,
4850        items: &[DialogItem],
4851        active_edit_item: i16,
4852        active_edit_text: &str,
4853    ) {
4854        for (idx, item) in items.iter().enumerate() {
4855            if (item.item_type & 0x7F) != 16 {
4856                continue;
4857            }
4858            let item_no = (idx + 1) as i16;
4859            let text = if item_no == active_edit_item {
4860                active_edit_text
4861            } else {
4862                &item.text
4863            };
4864            let bytes = text.as_bytes();
4865            let len = bytes.len().min(255);
4866            let item_handle = Self::dialog_item_handle(bus, dialog_ptr, item_no);
4867            if item_handle != 0 {
4868                let data_ptr = Self::ensure_text_handle_size(bus, item_handle, len);
4869                if data_ptr != 0 && len > 0 {
4870                    bus.write_bytes(data_ptr, &bytes[..len]);
4871                }
4872            }
4873            if let Some(cached_items) = self.dialog_items.get_mut(&dialog_ptr) {
4874                if idx < cached_items.len() {
4875                    cached_items[idx].text = text.to_string();
4876                    cached_items[idx].sel_start = item.sel_start;
4877                    cached_items[idx].sel_end = item.sel_end;
4878                }
4879            }
4880        }
4881    }
4882
4883    fn finish_dialog_creation<C: CpuOps>(
4884        &mut self,
4885        bus: &mut MacMemoryBus,
4886        cpu: &mut C,
4887        storage_ptr: u32,
4888        bounds: (i16, i16, i16, i16),
4889        title: &str,
4890        visible: bool,
4891        proc_id: i16,
4892        go_away_flag: bool,
4893        ref_con: u32,
4894        items_handle: u32,
4895        items: Vec<DialogItem>,
4896    ) -> u32 {
4897        let dlg_ptr = if storage_ptr != 0 {
4898            storage_ptr
4899        } else {
4900            // GetNewDialog/NewDialog allocate storage when dStorage is NIL
4901            // (IM:I I-424, I-412). System 7's Memory Manager zone layout
4902            // gives manager-owned records stable low-byte placement that
4903            // some 68K apps accidentally depend on; keep that shape local
4904            // to Dialog Manager records instead of changing all heap blocks.
4905            bus.alloc_aligned(170, MANAGER_DIALOG_RECORD_ALIGNMENT)
4906        };
4907
4908        self.window_stack.push((
4909            self.front_window,
4910            self.window_bounds,
4911            self.window_proc_id,
4912            self.window_title.clone(),
4913        ));
4914
4915        let screen_base: u32 = bus.read_long(0x0824);
4916        self.init_cgraf_window(
4917            bus,
4918            cpu,
4919            dlg_ptr,
4920            screen_base,
4921            bounds.0,
4922            bounds.1,
4923            bounds.2,
4924            bounds.3,
4925            title,
4926            proc_id,
4927            visible,
4928            false,
4929            go_away_flag,
4930            ref_con,
4931        );
4932        self.set_current_port_state(bus, cpu, dlg_ptr, None);
4933
4934        // DialogRecord starts with a WindowRecord; +108 is windowKind,
4935        // not the WDEF procID. Dialog boxes and alerts must use
4936        // dialogKind=2. The WDEF procID is retained in window_proc_ids.
4937        // Inside Macintosh Volume I, I-407..I-408, I-273; Volume VI, 11-42.
4938        bus.write_word(dlg_ptr + 108, 2);
4939        bus.write_long(dlg_ptr + 156, items_handle);
4940
4941        // SetDAFont / SetDialogFont affect subsequently created dialog and
4942        // alert grafPorts; assembly callers may set DlgFont directly.
4943        // Inside Macintosh Volume I, I-412; MTE 1992 p. 6-104.
4944        let dialog_font = bus.read_word(crate::memory::globals::addr::DLG_FONT) as i16;
4945        bus.write_word(dlg_ptr + 68, dialog_font as u16);
4946        self.tx_font = dialog_font;
4947        if let Some(state) = self.port_draw_states.get_mut(&dlg_ptr) {
4948            state.tx_font = dialog_font;
4949        }
4950
4951        let text_h = Self::allocate_te_handle(bus);
4952        bus.write_long(dlg_ptr + 160, text_h);
4953        bus.write_word(dlg_ptr + 164, 0xFFFF); // editField = -1
4954        bus.write_word(dlg_ptr + 166, 0); // editOpen
4955        bus.write_word(dlg_ptr + 168, 1); // aDefItem
4956
4957        self.initialize_dialog_item_handles(bus, dlg_ptr, &items);
4958        self.dialog_items.insert(dlg_ptr, items.clone());
4959
4960        if visible {
4961            // NewDialog/GetNewDialog create and display visible dialogs
4962            // immediately. userItem contents remain app-owned and are skipped
4963            // until DrawDialog/ModalDialog/update callbacks can run, but the
4964            // standard shell, controls, text, and resource items must still be
4965            // visible. Inside Macintosh Volume I, I-405, I-412, I-421.
4966            let (edit_text, edit_item, default_item) =
4967                Self::dialog_edit_state(bus, dlg_ptr, &items);
4968            self.draw_dialog(
4969                bus,
4970                bounds,
4971                proc_id,
4972                title,
4973                &items,
4974                default_item,
4975                &edit_text,
4976                edit_item,
4977                false,
4978                dlg_ptr,
4979            );
4980            if !Self::dialog_is_game_managed(bounds, &items) {
4981                let pixels = self.save_dialog_pixels(bus, bounds);
4982                self.dialog_visible_snapshots
4983                    .insert(dlg_ptr, PersistentDialogSnapshot { bounds, pixels });
4984            }
4985            for item in &items {
4986                if Self::dialog_item_intersects_bounds(bounds, item) {
4987                    self.validate_window_rect(bus, dlg_ptr, item.rect);
4988                }
4989            }
4990            self.queue_window_update_event(dlg_ptr);
4991            self.capture_gui_frame(bus, &format!("get_new_dialog_initial_{:08X}", dlg_ptr));
4992        } else {
4993            self.dialog_initial_draw_deferred.remove(&dlg_ptr);
4994        }
4995
4996        dlg_ptr
4997    }
4998
4999    // ========== Dialog Drawing Helpers ==========
5000
5001    /// Save framebuffer pixels under a dialog region (including shadow).
5002    /// Supports both 1bpp and 8bpp modes.
5003    pub(crate) fn save_dialog_pixels(
5004        &self,
5005        bus: &MacMemoryBus,
5006        rect: (i16, i16, i16, i16),
5007    ) -> Vec<u8> {
5008        let (screen_base, row_bytes, _, screen_h, pixel_size) = self.get_screen_params();
5009        let (save_top, save_left, save_bottom, save_right) = Self::dialog_saved_pixel_rect(rect);
5010        // Guard against negative y (top < 5) and y >= screen_h. `y as u32`
5011        // sign-extends a negative i16 to a huge value that overflows when
5012        // multiplied by row_bytes. Off-screen rows contribute zeros.
5013        let row_width = (save_right - save_left) as usize;
5014        let row_count = (save_bottom - save_top) as usize;
5015        let mut saved = Vec::new();
5016        let screen_h_i16 = screen_h;
5017        for y in save_top..save_bottom {
5018            let y_on_screen = y >= 0 && y < screen_h_i16;
5019            if pixel_size == 8 {
5020                let on_screen_row =
5021                    y_on_screen && save_left >= 0 && (save_right as u32) <= row_bytes;
5022                if on_screen_row {
5023                    // Pre-allocate capacity on first iteration.
5024                    if saved.capacity() == 0 {
5025                        saved.reserve(row_count * row_width);
5026                    }
5027                    let start = saved.len();
5028                    saved.resize(start + row_width, 0);
5029                    let row_addr = screen_base + (y as u32) * row_bytes + (save_left as u32);
5030                    bus.read_bytes_into(row_addr, &mut saved[start..start + row_width]);
5031                } else if y_on_screen {
5032                    for x in save_left..save_right {
5033                        if x < 0 || (x as u32) >= row_bytes {
5034                            saved.push(0);
5035                        } else {
5036                            let addr = screen_base + (y as u32) * row_bytes + (x as u32);
5037                            saved.push(bus.read_byte(addr));
5038                        }
5039                    }
5040                } else {
5041                    // Entire row off-screen — pad with zeros.
5042                    saved.resize(saved.len() + row_width, 0);
5043                }
5044            } else if y_on_screen {
5045                let byte_left = (save_left.max(0) as u32) / 8;
5046                let byte_right = (save_right as u32).div_ceil(8);
5047                let bx_end = byte_right.min(row_bytes);
5048                if byte_left < bx_end {
5049                    let len = (bx_end - byte_left) as usize;
5050                    let start = saved.len();
5051                    saved.resize(start + len, 0);
5052                    let row_addr = screen_base + (y as u32) * row_bytes + byte_left;
5053                    bus.read_bytes_into(row_addr, &mut saved[start..start + len]);
5054                }
5055            }
5056            // 1bpp off-screen row: silently produces nothing — the
5057            // byte_left < bx_end check + row_count-based saved.len tracking
5058            // tolerates short rows.
5059        }
5060        saved
5061    }
5062
5063    pub(crate) fn ensure_dialog_background_saved(
5064        &mut self,
5065        bus: &MacMemoryBus,
5066        dialog_ptr: u32,
5067        bounds: (i16, i16, i16, i16),
5068    ) {
5069        if dialog_ptr == 0
5070            || !self.dialog_items.contains_key(&dialog_ptr)
5071            || self.dialog_saved_pixels.contains_key(&dialog_ptr)
5072        {
5073            return;
5074        }
5075        let background = self.save_dialog_pixels(bus, bounds);
5076        self.dialog_saved_pixels.insert(dialog_ptr, background);
5077    }
5078
5079    pub(crate) fn ensure_dialog_background_saved_for_screen_port(
5080        &mut self,
5081        bus: &MacMemoryBus,
5082        port: u32,
5083    ) {
5084        if port == 0 || !self.dialog_items.contains_key(&port) {
5085            return;
5086        }
5087        let bounds = Self::dialog_screen_bounds(bus, port);
5088        self.ensure_dialog_background_saved(bus, port, bounds);
5089    }
5090
5091    fn dialog_saved_pixel_rect(rect: (i16, i16, i16, i16)) -> (i16, i16, i16, i16) {
5092        let (top, left, bottom, right) = rect;
5093        let margin = Self::DBOX_FRAME_MARGIN;
5094        (top - margin, left - margin, bottom + margin, right + margin)
5095    }
5096
5097    pub(crate) fn refresh_dialog_saved_pixels_after_screen_draw(
5098        &mut self,
5099        bus: &MacMemoryBus,
5100        drawing_port: u32,
5101        screen_rect: (i16, i16, i16, i16),
5102    ) {
5103        if screen_rect.0 >= screen_rect.2 || screen_rect.1 >= screen_rect.3 {
5104            return;
5105        }
5106        let mut dialogs: Vec<(u32, (i16, i16, i16, i16))> = self
5107            .dialog_visible_snapshots
5108            .iter()
5109            .map(|(&dialog_ptr, snapshot)| (dialog_ptr, snapshot.bounds))
5110            .collect();
5111        if let Some(tracking) = self.dialog_tracking.as_ref() {
5112            if !dialogs
5113                .iter()
5114                .any(|(dialog_ptr, _)| *dialog_ptr == tracking.dialog_ptr)
5115            {
5116                dialogs.push((tracking.dialog_ptr, tracking.bounds));
5117            }
5118        }
5119        for (dialog_ptr, bounds) in dialogs {
5120            let drawing_active_modal_dialog =
5121                self.dialog_tracking.as_ref().is_some_and(|tracking| {
5122                    tracking.dialog_ptr == dialog_ptr && dialog_ptr == drawing_port
5123                });
5124            let drawing_premodal_dialog =
5125                dialog_ptr == drawing_port && !self.dialog_modal_entered.contains(&dialog_ptr);
5126            if drawing_active_modal_dialog
5127                || drawing_premodal_dialog
5128                || self.active_modeless_dialog_draw_proc == Some(dialog_ptr)
5129            {
5130                continue;
5131            }
5132            self.refresh_single_dialog_saved_pixels_after_screen_draw(
5133                bus,
5134                dialog_ptr,
5135                bounds,
5136                screen_rect,
5137            );
5138        }
5139    }
5140
5141    fn refresh_single_dialog_saved_pixels_after_screen_draw(
5142        &mut self,
5143        bus: &MacMemoryBus,
5144        dialog_ptr: u32,
5145        bounds: (i16, i16, i16, i16),
5146        screen_rect: (i16, i16, i16, i16),
5147    ) {
5148        let save_rect = Self::dialog_saved_pixel_rect(bounds);
5149        let Some(intersection) = Self::rect_intersection(save_rect, screen_rect) else {
5150            return;
5151        };
5152
5153        let (screen_base, row_bytes, screen_w, screen_h, pixel_size) = self.get_screen_params();
5154        let Some(saved) = self.dialog_saved_pixels.get_mut(&dialog_ptr) else {
5155            return;
5156        };
5157        let (save_top, save_left, save_bottom, save_right) = save_rect;
5158        let row_width = save_right.saturating_sub(save_left) as usize;
5159        let row_count = save_bottom.saturating_sub(save_top) as usize;
5160        if row_width == 0 || row_count == 0 {
5161            return;
5162        }
5163
5164        match pixel_size {
5165            8 => {
5166                let expected = row_width.saturating_mul(row_count);
5167                if saved.len() < expected {
5168                    return;
5169                }
5170                let top = intersection.0.max(0);
5171                let left = intersection.1.max(0);
5172                let bottom = intersection.2.min(screen_h);
5173                let right = intersection.3.min(screen_w).min(row_bytes as i16);
5174                if top >= bottom || left >= right {
5175                    return;
5176                }
5177                for y in top..bottom {
5178                    let saved_offset =
5179                        (y - save_top) as usize * row_width + (left - save_left) as usize;
5180                    let len = (right - left) as usize;
5181                    let row_addr = screen_base + (y as u32) * row_bytes + (left as u32);
5182                    let row = bus.read_bytes(row_addr, len);
5183                    saved[saved_offset..saved_offset + len].copy_from_slice(&row);
5184                }
5185            }
5186            1 => {
5187                if save_right <= 0 {
5188                    return;
5189                }
5190                let byte_left = (save_left.max(0) as u32) / 8;
5191                let byte_right = (save_right as u32).div_ceil(8);
5192                let byte_end = byte_right.min(row_bytes);
5193                if byte_left >= byte_end {
5194                    return;
5195                }
5196                let row_len = (byte_end - byte_left) as usize;
5197                let first_saved_row = save_top.max(0);
5198                let top = intersection.0.max(0);
5199                let left = intersection.1.max(0);
5200                let bottom = intersection.2.min(screen_h);
5201                let right = intersection
5202                    .3
5203                    .min(screen_w)
5204                    .min((row_bytes.saturating_mul(8)) as i16);
5205                if top >= bottom || left >= right {
5206                    return;
5207                }
5208                for y in top..bottom {
5209                    let row_offset = (y - first_saved_row) as usize * row_len;
5210                    if row_offset + row_len > saved.len() {
5211                        return;
5212                    }
5213                    for x in left..right {
5214                        let byte_x = (x as u32) / 8;
5215                        if byte_x < byte_left || byte_x >= byte_end {
5216                            continue;
5217                        }
5218                        let bit = 7 - ((x as u32) & 7);
5219                        let mask = 1u8 << bit;
5220                        let screen_byte =
5221                            bus.read_byte(screen_base + (y as u32) * row_bytes + byte_x);
5222                        let saved_idx = row_offset + (byte_x - byte_left) as usize;
5223                        if (screen_byte & mask) != 0 {
5224                            saved[saved_idx] |= mask;
5225                        } else {
5226                            saved[saved_idx] &= !mask;
5227                        }
5228                    }
5229                }
5230            }
5231            _ => {}
5232        }
5233    }
5234
5235    /// Restore previously saved framebuffer pixels under a dialog.
5236    pub(crate) fn restore_dialog_pixels(
5237        &self,
5238        bus: &mut MacMemoryBus,
5239        rect: (i16, i16, i16, i16),
5240        saved: &[u8],
5241    ) {
5242        let (screen_base, row_bytes, _, screen_h, pixel_size) = self.get_screen_params();
5243        let (save_top, save_left, save_bottom, save_right) = Self::dialog_saved_pixel_rect(rect);
5244        // Mirror save_dialog_pixels y-bounds guard — saved bytes for off-screen
5245        // rows are skipped so write position stays aligned with the packed input buffer.
5246        let row_width = (save_right - save_left) as usize;
5247        let mut idx = 0;
5248        let screen_h_i16 = screen_h;
5249        for y in save_top..save_bottom {
5250            let y_on_screen = y >= 0 && y < screen_h_i16;
5251            if pixel_size == 8 {
5252                let on_screen_row =
5253                    y_on_screen && save_left >= 0 && (save_right as u32) <= row_bytes;
5254                if on_screen_row && idx + row_width <= saved.len() {
5255                    let row_addr = screen_base + (y as u32) * row_bytes + (save_left as u32);
5256                    bus.write_bytes(row_addr, &saved[idx..idx + row_width]);
5257                    idx += row_width;
5258                } else if y_on_screen {
5259                    for x in save_left..save_right {
5260                        if idx < saved.len() {
5261                            if x >= 0 && (x as u32) < row_bytes {
5262                                let addr = screen_base + (y as u32) * row_bytes + (x as u32);
5263                                bus.write_byte(addr, saved[idx]);
5264                            }
5265                            idx += 1;
5266                        }
5267                    }
5268                } else {
5269                    // Off-screen row: skip the packed bytes that
5270                    // save_dialog_pixels padded in.
5271                    idx += row_width.min(saved.len() - idx);
5272                }
5273            } else if y_on_screen {
5274                let byte_left = (save_left.max(0) as u32) / 8;
5275                let byte_right = (save_right as u32).div_ceil(8);
5276                let bx_end = byte_right.min(row_bytes);
5277                if byte_left < bx_end {
5278                    let len = (bx_end - byte_left) as usize;
5279                    if idx + len <= saved.len() {
5280                        let row_addr = screen_base + (y as u32) * row_bytes + byte_left;
5281                        bus.write_bytes(row_addr, &saved[idx..idx + len]);
5282                        idx += len;
5283                    } else {
5284                        for bx in byte_left..bx_end {
5285                            if idx < saved.len() {
5286                                bus.write_byte(
5287                                    screen_base + (y as u32) * row_bytes + bx,
5288                                    saved[idx],
5289                                );
5290                                idx += 1;
5291                            }
5292                        }
5293                    }
5294                }
5295            }
5296            // 1bpp off-screen: save produced no bytes for this row,
5297            // so there's nothing to advance idx over here.
5298        }
5299    }
5300
5301    fn restore_dialog_pixels_outside_rect(
5302        &self,
5303        bus: &mut MacMemoryBus,
5304        rect: (i16, i16, i16, i16),
5305        saved: &[u8],
5306        keep_rect: (i16, i16, i16, i16),
5307    ) {
5308        let (screen_base, row_bytes, _, screen_h, pixel_size) = self.get_screen_params();
5309        let (top, left, bottom, right) = rect;
5310        let margin = Self::DBOX_FRAME_MARGIN;
5311        let save_top = top - margin;
5312        let save_left = left - margin;
5313        let save_bottom = bottom + margin;
5314        let save_right = right + margin;
5315        let row_width = (save_right - save_left) as usize;
5316        let mut idx = 0;
5317        let screen_h_i16 = screen_h;
5318        for y in save_top..save_bottom {
5319            let y_on_screen = y >= 0 && y < screen_h_i16;
5320            if pixel_size == 8 {
5321                if y_on_screen {
5322                    for x in save_left..save_right {
5323                        if idx < saved.len() {
5324                            if (y < keep_rect.0
5325                                || y >= keep_rect.2
5326                                || x < keep_rect.1
5327                                || x >= keep_rect.3)
5328                                && x >= 0
5329                                && (x as u32) < row_bytes
5330                            {
5331                                let addr = screen_base + (y as u32) * row_bytes + (x as u32);
5332                                bus.write_byte(addr, saved[idx]);
5333                            }
5334                            idx += 1;
5335                        }
5336                    }
5337                } else {
5338                    idx += row_width.min(saved.len().saturating_sub(idx));
5339                }
5340            } else if y_on_screen {
5341                let byte_left = (save_left.max(0) as u32) / 8;
5342                let byte_right = (save_right as u32).div_ceil(8);
5343                let bx_end = byte_right.min(row_bytes);
5344                if byte_left < bx_end {
5345                    for bx in byte_left..bx_end {
5346                        if idx >= saved.len() {
5347                            break;
5348                        }
5349                        let mut restore_mask = 0u8;
5350                        for bit in 0..8u32 {
5351                            let x = (bx * 8 + bit) as i16;
5352                            if x >= save_left
5353                                && x < save_right
5354                                && (y < keep_rect.0
5355                                    || y >= keep_rect.2
5356                                    || x < keep_rect.1
5357                                    || x >= keep_rect.3)
5358                            {
5359                                restore_mask |= 0x80 >> bit;
5360                            }
5361                        }
5362                        if restore_mask != 0 {
5363                            let addr = screen_base + (y as u32) * row_bytes + bx;
5364                            let current = bus.read_byte(addr);
5365                            bus.write_byte(
5366                                addr,
5367                                (current & !restore_mask) | (saved[idx] & restore_mask),
5368                            );
5369                        }
5370                        idx += 1;
5371                    }
5372                }
5373            }
5374        }
5375    }
5376
5377    /// Save framebuffer pixels for an exact rectangle (no margin).
5378    /// Guards off-screen y (y < 0 or y >= screen_h) from sign-extend overflow.
5379    /// Off-screen rows pad 8bpp output with zeros; 1bpp output is short by that row.
5380    fn save_rect_pixels(&self, bus: &MacMemoryBus, rect: (i16, i16, i16, i16)) -> Vec<u8> {
5381        let (screen_base, row_bytes, _, screen_h, pixel_size) = self.get_screen_params();
5382        let (top, left, bottom, right) = rect;
5383        let row_width = (right - left).max(0) as usize;
5384        let mut saved = Vec::with_capacity((bottom - top).max(0) as usize * row_width);
5385        let screen_h_i16 = screen_h;
5386        for y in top..bottom {
5387            let y_on_screen = y >= 0 && y < screen_h_i16;
5388            if pixel_size == 8 {
5389                let on_screen_row = y_on_screen && left >= 0 && (right as u32) <= row_bytes;
5390                if on_screen_row {
5391                    let row_addr = screen_base + (y as u32) * row_bytes + (left as u32);
5392                    saved.extend_from_slice(&bus.read_bytes(row_addr, row_width));
5393                } else if y_on_screen {
5394                    for x in left..right {
5395                        if x < 0 || (x as u32) >= row_bytes {
5396                            saved.push(0);
5397                        } else {
5398                            saved.push(
5399                                bus.read_byte(screen_base + (y as u32) * row_bytes + (x as u32)),
5400                            );
5401                        }
5402                    }
5403                } else {
5404                    saved.resize(saved.len() + row_width, 0);
5405                }
5406            } else if y_on_screen {
5407                let byte_left = (left.max(0) as u32) / 8;
5408                let byte_right = (right.max(0) as u32).div_ceil(8);
5409                let bx_end = byte_right.min(row_bytes);
5410                if byte_left < bx_end {
5411                    let len = (bx_end - byte_left) as usize;
5412                    let row_addr = screen_base + (y as u32) * row_bytes + byte_left;
5413                    saved.extend_from_slice(&bus.read_bytes(row_addr, len));
5414                }
5415            }
5416        }
5417        saved
5418    }
5419
5420    fn saved_rect_has_non_background_content(&self, saved: &[u8]) -> bool {
5421        saved.iter().any(|&byte| byte != 0)
5422    }
5423
5424    fn user_item_preserve_rects(
5425        &self,
5426        dialog_ptr: u32,
5427        bounds: (i16, i16, i16, i16),
5428        items: &[DialogItem],
5429        skip_popup_user_items: bool,
5430        skip_disabled_placeholders: bool,
5431    ) -> Vec<((i16, i16, i16, i16), bool)> {
5432        items
5433            .iter()
5434            .enumerate()
5435            .filter(|(i, it)| {
5436                (it.item_type & 0x7F) == 0
5437                    && (!skip_disabled_placeholders
5438                        || (it.item_type & 0x80) == 0
5439                        || it.proc_ptr != 0)
5440                    && (!skip_popup_user_items
5441                        || !self
5442                            .dialog_item_popup_menus
5443                            .contains_key(&(dialog_ptr, (i + 1) as i16)))
5444            })
5445            .map(|(_, it)| {
5446                let (it_t, it_l, it_b, it_r) = it.rect;
5447                let rect = (
5448                    bounds.0 + it_t,
5449                    bounds.1 + it_l,
5450                    bounds.0 + it_b,
5451                    bounds.1 + it_r,
5452                );
5453                (rect, it.item_type == 0)
5454            })
5455            .collect()
5456    }
5457
5458    fn restore_user_item_preserved_pixels(
5459        &self,
5460        bus: &mut MacMemoryBus,
5461        rects: &[((i16, i16, i16, i16), bool)],
5462        backups: &[Vec<u8>],
5463    ) {
5464        for ((rect, always_restore), pixels) in rects.iter().zip(backups.iter()) {
5465            if *always_restore || self.saved_rect_has_non_background_content(pixels) {
5466                self.restore_rect_pixels(bus, *rect, pixels);
5467            }
5468        }
5469    }
5470
5471    fn draw_dialog_preserving_user_items(
5472        &mut self,
5473        bus: &mut MacMemoryBus,
5474        bounds: (i16, i16, i16, i16),
5475        proc_id: i16,
5476        title: &str,
5477        items: &[DialogItem],
5478        default_item: i16,
5479        edit_text: &str,
5480        edit_item: i16,
5481        skip_pictures: bool,
5482        dialog_ptr: u32,
5483        preserve_user_items: bool,
5484        skip_popup_user_items: bool,
5485        skip_disabled_placeholders: bool,
5486    ) {
5487        let user_item_rects = if preserve_user_items {
5488            self.user_item_preserve_rects(
5489                dialog_ptr,
5490                bounds,
5491                items,
5492                skip_popup_user_items,
5493                skip_disabled_placeholders,
5494            )
5495        } else {
5496            Vec::new()
5497        };
5498        let user_item_backups: Vec<Vec<u8>> = user_item_rects
5499            .iter()
5500            .map(|&(r, _)| self.save_rect_pixels(bus, r))
5501            .collect();
5502
5503        self.draw_dialog(
5504            bus,
5505            bounds,
5506            proc_id,
5507            title,
5508            items,
5509            default_item,
5510            edit_text,
5511            edit_item,
5512            skip_pictures,
5513            dialog_ptr,
5514        );
5515
5516        self.restore_user_item_preserved_pixels(bus, &user_item_rects, &user_item_backups);
5517        self.capture_gui_frame(bus, &format!("draw_dialog_preserved_{:08X}", dialog_ptr));
5518    }
5519
5520    fn fill_rect_clipped_to_dialog(
5521        &self,
5522        bus: &mut MacMemoryBus,
5523        bounds: (i16, i16, i16, i16),
5524        rect: (i16, i16, i16, i16),
5525        value: bool,
5526    ) {
5527        let top = rect.0.max(bounds.0);
5528        let left = rect.1.max(bounds.1);
5529        let bottom = rect.2.min(bounds.2);
5530        let right = rect.3.min(bounds.3);
5531        if top >= bottom || left >= right {
5532            return;
5533        }
5534
5535        let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
5536            self.get_screen_params();
5537        Self::fb_fill_rect(
5538            bus,
5539            screen_base,
5540            row_bytes,
5541            pixel_size,
5542            screen_width,
5543            screen_height,
5544            top,
5545            left,
5546            bottom,
5547            right,
5548            value,
5549        );
5550    }
5551
5552    fn dialog_item_enclosing_local_rect(
5553        item_type: u8,
5554        rect: (i16, i16, i16, i16),
5555    ) -> (i16, i16, i16, i16) {
5556        match item_type & 0x7F {
5557            // IM:IV IV-59 notes that Dialog Manager drawing can extend
5558            // outside an editText item's display rectangle by 3 pixels.
5559            16 => (rect.0 - 3, rect.1 - 3, rect.2 + 3, rect.3 + 3),
5560            _ => rect,
5561        }
5562    }
5563
5564    fn erase_dialog_item_enclosing_rect(
5565        &self,
5566        bus: &mut MacMemoryBus,
5567        dialog_ptr: u32,
5568        local_rect: (i16, i16, i16, i16),
5569    ) {
5570        let bounds = Self::dialog_screen_bounds(bus, dialog_ptr);
5571        let screen_rect = (
5572            bounds.0 + local_rect.0,
5573            bounds.1 + local_rect.1,
5574            bounds.0 + local_rect.2,
5575            bounds.1 + local_rect.3,
5576        );
5577        self.fill_rect_clipped_to_dialog(bus, bounds, screen_rect, false);
5578    }
5579
5580    /// Restore framebuffer pixels to an exact rectangle (no margin).
5581    ///
5582    /// Mirrors save_rect_pixels off-screen guards so the idx position stays
5583    /// aligned with the packed input buffer.
5584    fn restore_rect_pixels(
5585        &self,
5586        bus: &mut MacMemoryBus,
5587        rect: (i16, i16, i16, i16),
5588        saved: &[u8],
5589    ) {
5590        let (screen_base, row_bytes, _, screen_h, pixel_size) = self.get_screen_params();
5591        let (top, left, bottom, right) = rect;
5592        let row_width = (right - left).max(0) as usize;
5593        let mut idx = 0;
5594        let screen_h_i16 = screen_h;
5595        for y in top..bottom {
5596            let y_on_screen = y >= 0 && y < screen_h_i16;
5597            if pixel_size == 8 {
5598                let on_screen_row = y_on_screen && left >= 0 && (right as u32) <= row_bytes;
5599                if on_screen_row && idx + row_width <= saved.len() {
5600                    let row_addr = screen_base + (y as u32) * row_bytes + (left as u32);
5601                    bus.write_bytes(row_addr, &saved[idx..idx + row_width]);
5602                    idx += row_width;
5603                } else if y_on_screen {
5604                    for x in left..right {
5605                        if idx < saved.len() {
5606                            if x >= 0 && (x as u32) < row_bytes {
5607                                bus.write_byte(
5608                                    screen_base + (y as u32) * row_bytes + (x as u32),
5609                                    saved[idx],
5610                                );
5611                            }
5612                            idx += 1;
5613                        }
5614                    }
5615                } else {
5616                    idx = (idx + row_width).min(saved.len());
5617                }
5618            } else if y_on_screen {
5619                let byte_left = (left.max(0) as u32) / 8;
5620                let byte_right = (right.max(0) as u32).div_ceil(8);
5621                let bx_end = byte_right.min(row_bytes);
5622                if byte_left < bx_end {
5623                    let len = (bx_end - byte_left) as usize;
5624                    if idx + len <= saved.len() {
5625                        let row_addr = screen_base + (y as u32) * row_bytes + byte_left;
5626                        bus.write_bytes(row_addr, &saved[idx..idx + len]);
5627                        idx += len;
5628                    } else {
5629                        for bx in byte_left..bx_end {
5630                            if idx < saved.len() {
5631                                bus.write_byte(
5632                                    screen_base + (y as u32) * row_bytes + bx,
5633                                    saved[idx],
5634                                );
5635                                idx += 1;
5636                            }
5637                        }
5638                    }
5639                }
5640            }
5641        }
5642    }
5643
5644    /// Draw a complete dialog box with frame and all items.
5645    /// When `skip_pictures` is true, icon and picture items are skipped
5646    /// (used during redraw_chrome to avoid re-parsing PICTs every frame).
5647    pub(crate) fn draw_dialog(
5648        &mut self,
5649        bus: &mut MacMemoryBus,
5650        bounds: (i16, i16, i16, i16),
5651        proc_id: i16,
5652        title: &str,
5653        items: &[DialogItem],
5654        default_item: i16,
5655        edit_text: &str,
5656        edit_item: i16,
5657        skip_pictures: bool,
5658        dialog_ptr: u32,
5659    ) {
5660        self.ensure_dialog_background_saved(bus, dialog_ptr, bounds);
5661        self.dialog_initial_draw_deferred.remove(&dialog_ptr);
5662
5663        let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
5664            self.get_screen_params();
5665        let (top, left, bottom, right) = bounds;
5666
5667        // Fill dialog background with white — but skip for game-managed
5668        // dialogs whose in-bounds items are userItems, since the game draws
5669        // its own background and the white fill would overwrite that content.
5670        let game_managed = Self::dialog_is_game_managed(bounds, items);
5671        if game_managed {
5672            eprintln!(
5673                "[DIALOG] Skipping white fill for game-managed dialog at ({},{},{},{}), {} items",
5674                top,
5675                left,
5676                bottom,
5677                right,
5678                items.len()
5679            );
5680        }
5681        let themed_frame = match proc_id {
5682            1 => (
5683                top - Self::DBOX_FRAME_MARGIN,
5684                left - Self::DBOX_FRAME_MARGIN,
5685                bottom + Self::DBOX_FRAME_MARGIN,
5686                right + Self::DBOX_FRAME_MARGIN,
5687            ),
5688            2 => (top - 1, left - 1, bottom + 1, right + 1),
5689            3 => (top - 1, left - 1, bottom + 3, right + 3),
5690            _ => (top, left, bottom + 2, right + 2),
5691        };
5692        if !self.draw_theme_dialog_frame(
5693            bus,
5694            (top, left, bottom, right),
5695            themed_frame,
5696            proc_id,
5697            true,
5698            !game_managed,
5699        ) {
5700            if !game_managed {
5701                Self::fb_fill_rect(
5702                    bus,
5703                    screen_base,
5704                    row_bytes,
5705                    pixel_size,
5706                    screen_width,
5707                    screen_height,
5708                    top,
5709                    left,
5710                    bottom,
5711                    right,
5712                    false,
5713                );
5714            }
5715
5716            // Draw border
5717            // Inside Macintosh Volume I, I-299:
5718            // procID 0 = documentProc (title bar + border)
5719            // procID 1 = dBoxProc (double border, no title)
5720            // procID 2 = plainDBox (single border, no title)
5721            // procID 3 = altDBoxProc (shadow border, no title)
5722            // procID 4 = noGrowDocProc (title bar + border, no grow)
5723            match proc_id {
5724                1 => {
5725                    self.draw_classic_dbox_frame(bus, top, left, bottom, right);
5726                }
5727                2 => {
5728                    // plainDBox: single-pixel WDEF border outside the content
5729                    // bounds, matching the Window Manager procID 2 path.
5730                    // Inside Macintosh Volume I, I-275.
5731                    self.draw_rect_border(bus, top - 1, left - 1, bottom + 1, right + 1);
5732                }
5733                3 => {
5734                    // altDBoxProc: single border + shadow
5735                    self.draw_rect_border(bus, top - 1, left - 1, bottom + 1, right + 1);
5736                    self.draw_shadow(bus, top - 1, left - 1, bottom + 1, right + 1);
5737                }
5738                0 | 4 => {
5739                    // documentProc/noGrowDocProc use the Window Manager's
5740                    // document WDEF title-bar chrome. DrawDialog is allowed
5741                    // on modeless dialogs too (IM:I I-417; MTE 1992 p. 6-142),
5742                    // so route through the same WDEF frame path used by
5743                    // visible window creation instead of the modal-box fallback.
5744                    let saved_bounds = self.window_bounds;
5745                    let saved_proc_id = self.window_proc_id;
5746                    let saved_title = self.window_title.clone();
5747                    let saved_go_away = self.go_away_flag;
5748                    self.window_bounds = bounds;
5749                    self.window_proc_id = proc_id;
5750                    self.window_title = if title.is_empty() {
5751                        Self::dialog_window_title(bus, dialog_ptr)
5752                    } else {
5753                        title.to_string()
5754                    };
5755                    self.go_away_flag = dialog_ptr != 0 && bus.read_byte(dialog_ptr + 112) != 0;
5756                    self.draw_window_frame(bus);
5757                    self.window_bounds = saved_bounds;
5758                    self.window_proc_id = saved_proc_id;
5759                    self.window_title = saved_title;
5760                    self.go_away_flag = saved_go_away;
5761                }
5762                _ => {
5763                    // Default: single border + shadow
5764                    self.draw_rect_border(bus, top, left, bottom, right);
5765                    self.draw_shadow(bus, top, left, bottom, right);
5766                }
5767            }
5768        }
5769        // Optional per-iteration trace: gate SYSTEMLESS_TRACE_DIALOG_ITEMS=1
5770        // logs each item's type + relative rect + computed absolute rect.
5771        // Use to localize artifact-producing items in modal dialog rendering.
5772        let trace_items = trace_dialog_items_enabled();
5773        if trace_items {
5774            eprintln!(
5775                "[DLG] draw_dialog bounds=({},{},{},{}) proc_id={} edit_item={} default_item={} items={}",
5776                top,
5777                left,
5778                bottom,
5779                right,
5780                proc_id,
5781                edit_item,
5782                default_item,
5783                items.len()
5784            );
5785        }
5786
5787        // IM:I I-407 and MTE 1992 p. 6-56: aDefItem/first-item default
5788        // controls Return/Enter semantics for modal dialogs, but ordinary
5789        // dialogs draw any bold default-button outline with an application
5790        // userItem. MTE 1992 glossary confirms Dialog Manager auto-draws
5791        // the bold outline for alerts, while applications draw it for
5792        // dialogs. DrawDialog therefore draws standard buttons without
5793        // synthesizing default-button chrome.
5794        let auto_default_outline = false;
5795
5796        // Draw each item
5797        for (i, item) in items.iter().enumerate() {
5798            let item_num = (i + 1) as i16; // 1-based
5799            let (it, il, ib, ir) = item.rect;
5800            // Offset by dialog origin
5801            let abs_top = top + it;
5802            let abs_left = left + il;
5803            let abs_bottom = top + ib;
5804            let abs_right = left + ir;
5805
5806            let base_type = item.item_type & 0x7F;
5807            if trace_items {
5808                eprintln!(
5809                    "[DLG]   item #{} type=0x{:02X} (base={}) rel=({},{},{},{}) abs=({},{},{},{}) rsrc={} text={:?}",
5810                    item_num,
5811                    item.item_type,
5812                    base_type,
5813                    it,
5814                    il,
5815                    ib,
5816                    ir,
5817                    abs_top,
5818                    abs_left,
5819                    abs_bottom,
5820                    abs_right,
5821                    item.resource_id,
5822                    item.text,
5823                );
5824            }
5825
5826            // Generic "fully outside dialog" clip. Inside Macintosh
5827            // Volume I, I-309: dialog items are drawn through the dialog
5828            // window's port whose visRgn/clipRgn restrict drawing to the
5829            // dialog bounds. Real Mac OS QuickDraw clips item draws
5830            // automatically; Systemless's per-type handlers write to the
5831            // framebuffer directly with no clip. Items whose rect partially
5832            // extends beyond bounds are NOT clipped here — the per-type
5833            // handlers can refine if needed.
5834            if abs_top >= bottom || abs_bottom <= top || abs_left >= right || abs_right <= left {
5835                if trace_items {
5836                    eprintln!("[DLG]     -> skipped (fully outside dialog rect)");
5837                }
5838                continue;
5839            }
5840
5841            // Buttons, checkboxes, radios, and resource-backed controls are
5842            // Control Manager controls. Draw them in a second pass below so
5843            // their z-order matches DrawControls: reverse creation order,
5844            // with the earliest-created control frontmost (IM:I I-322; MTE
5845            // 1992 pp. 5-87..5-88).
5846            if matches!(base_type, 4..=7) {
5847                continue;
5848            }
5849
5850            let enabled = (item.item_type & 0x80) == 0;
5851            match base_type {
5852                // Button (ctrlItem + btnCtrl = 4)
5853                4 => {
5854                    // IM:I I-405: itemDisable stops Dialog Manager event
5855                    // reporting, while the standard System 7 control
5856                    // appearance is unchanged. Route the semantic state to
5857                    // non-classic theme chrome without changing metrics or
5858                    // existing hit handling.
5859                    self.draw_button_with_enabled(
5860                        bus,
5861                        abs_top,
5862                        abs_left,
5863                        abs_bottom,
5864                        abs_right,
5865                        &item.text,
5866                        auto_default_outline && item_num == default_item,
5867                        enabled,
5868                    );
5869                }
5870                // Checkbox (ctrlItem + chkCtrl = 5)
5871                5 => {
5872                    let checked = self
5873                        .dialog_control_values
5874                        .get(&(dialog_ptr, item_num))
5875                        .copied()
5876                        .unwrap_or(0)
5877                        != 0;
5878                    self.draw_checkbox_with_enabled(
5879                        bus, abs_top, abs_left, abs_bottom, abs_right, &item.text, checked, enabled,
5880                    );
5881                }
5882                // Radio button (ctrlItem + radCtrl = 6)
5883                6 => {
5884                    let selected = self
5885                        .dialog_control_values
5886                        .get(&(dialog_ptr, item_num))
5887                        .copied()
5888                        .unwrap_or(0)
5889                        != 0;
5890                    self.draw_radio_with_enabled(
5891                        bus, abs_top, abs_left, abs_bottom, abs_right, &item.text, selected,
5892                        enabled,
5893                    );
5894                }
5895                // Static text (8)
5896                8 => {
5897                    self.draw_static_text(
5898                        bus, abs_top, abs_left, abs_bottom, abs_right, &item.text,
5899                    );
5900                }
5901                // Edit text (16)
5902                16 => {
5903                    let display_text = if item_num == edit_item {
5904                        edit_text
5905                    } else {
5906                        &item.text
5907                    };
5908                    // MTE 1992 glossary "disabled item": disabled dialog
5909                    // items do not report user events. Route that semantic
5910                    // state to themed field chrome while leaving TextEdit
5911                    // metrics and existing hit handling unchanged.
5912                    let selection_range = if item_num == edit_item {
5913                        Self::dialog_item_selection_range(item, display_text.len())
5914                    } else {
5915                        None
5916                    };
5917                    self.draw_edit_text_with_cursor(
5918                        bus,
5919                        abs_top,
5920                        abs_left,
5921                        abs_bottom,
5922                        abs_right,
5923                        display_text,
5924                        selection_range,
5925                        false,
5926                        enabled,
5927                    );
5928                }
5929                // Icon (32)
5930                32 => {
5931                    if !skip_pictures && item.resource_id != 0 {
5932                        let drew_cicn = self
5933                            .find_resource_any(*b"cicn", item.resource_id)
5934                            .is_some_and(|(_, icon_ptr)| {
5935                                self.draw_cicn_icon(
5936                                    bus, abs_top, abs_left, abs_bottom, abs_right, icon_ptr,
5937                                )
5938                            });
5939                        if !drew_cicn {
5940                            if let Some((_, icon_ptr)) =
5941                                self.find_resource_any(*b"ICON", item.resource_id)
5942                            {
5943                                // ICON resource: 32x32 1-bit bitmap = 128 bytes
5944                                // Inside Macintosh Volume I, I-205
5945                                self.draw_icon(
5946                                    bus, abs_top, abs_left, abs_bottom, abs_right, icon_ptr,
5947                                );
5948                            }
5949                        }
5950                    }
5951                }
5952                // Picture (64)
5953                64 => {
5954                    // Items reaching here overlap the dialog rect (the
5955                    // fully-outside clip happens above the match).
5956                    if !skip_pictures && item.resource_id != 0 {
5957                        if let Some((_, pic_ptr)) =
5958                            self.find_resource_any(*b"PICT", item.resource_id)
5959                        {
5960                            // Draw PICT into the item's display rectangle
5961                            let device_ct_seed =
5962                                Self::ctab_seed(bus, self.current_gdevice_ctab_handle(bus))
5963                                    .unwrap_or(0);
5964                            super::pict::draw_picture(
5965                                bus,
5966                                pic_ptr,
5967                                abs_top,
5968                                abs_left,
5969                                abs_bottom,
5970                                abs_right,
5971                                self.screen_mode,
5972                                &self.device_clut,
5973                                device_ct_seed,
5974                                None,
5975                            );
5976                        }
5977                    }
5978                }
5979                // resCtrl — DITL item backed by a live CNTL resource.
5980                // Macintosh Toolbox Essentials 1992, 3-31.
5981                7 => {
5982                    if let Some(ctrl_handle) =
5983                        self.dialog_control_handle_for_item(dialog_ptr, item_num)
5984                    {
5985                        let ctrl_ptr = bus.read_long(ctrl_handle);
5986                        let proc_id_ctrl =
5987                            self.control_proc_ids.get(&ctrl_ptr).copied().unwrap_or(0);
5988                        let value = self
5989                            .dialog_control_values
5990                            .get(&(dialog_ptr, item_num))
5991                            .copied()
5992                            .unwrap_or_else(|| bus.read_word(ctrl_ptr + 18) as i16);
5993                        let min = bus.read_word(ctrl_ptr + 20) as i16;
5994                        let max = bus.read_word(ctrl_ptr + 22) as i16;
5995                        let hilite = bus.read_byte(ctrl_ptr + 17);
5996                        let title =
5997                            decode_mac_roman_for_render(&Self::control_title_bytes(bus, ctrl_ptr));
5998
5999                        match proc_id_ctrl {
6000                            0 => self.draw_button_with_enabled(
6001                                bus,
6002                                abs_top,
6003                                abs_left,
6004                                abs_bottom,
6005                                abs_right,
6006                                &title,
6007                                auto_default_outline && item_num == default_item,
6008                                enabled,
6009                            ),
6010                            1 => self.draw_checkbox_with_enabled(
6011                                bus,
6012                                abs_top,
6013                                abs_left,
6014                                abs_bottom,
6015                                abs_right,
6016                                &title,
6017                                value != 0,
6018                                enabled,
6019                            ),
6020                            2 => self.draw_radio_with_enabled(
6021                                bus,
6022                                abs_top,
6023                                abs_left,
6024                                abs_bottom,
6025                                abs_right,
6026                                &title,
6027                                value != 0,
6028                                enabled,
6029                            ),
6030                            16 => self.draw_scroll_bar(
6031                                bus, abs_top, abs_left, abs_bottom, abs_right, value, min, max,
6032                                hilite,
6033                            ),
6034                            proc_id if Self::is_popup_menu_proc_id(proc_id) => {
6035                                let menu_id = self.popup_control_menu_id(bus, ctrl_ptr, min);
6036                                let selected = value.max(1) as usize;
6037                                let item_title = self.popup_menu_item_title(bus, menu_id, selected);
6038                                let (draw_top, draw_left, draw_bottom, draw_right) = self
6039                                    .popup_control_box_rect(
6040                                        bus, abs_top, abs_left, abs_bottom, abs_right, menu_id,
6041                                        max, proc_id,
6042                                    );
6043                                self.draw_popup_control_label(
6044                                    bus,
6045                                    abs_top,
6046                                    abs_left,
6047                                    abs_bottom,
6048                                    draw_left,
6049                                    &title,
6050                                    enabled && hilite != 255,
6051                                );
6052                                self.draw_popup_control_with_state(
6053                                    bus,
6054                                    draw_top,
6055                                    draw_left,
6056                                    draw_bottom,
6057                                    draw_right,
6058                                    &item_title.unwrap_or_default(),
6059                                    enabled && hilite != 255,
6060                                    hilite == 1,
6061                                );
6062                            }
6063                            _ => {
6064                                self.draw_button_with_enabled(
6065                                    bus,
6066                                    abs_top,
6067                                    abs_left,
6068                                    abs_bottom,
6069                                    abs_right,
6070                                    &title,
6071                                    auto_default_outline && item_num == default_item,
6072                                    enabled,
6073                                );
6074                            }
6075                        }
6076                    } else {
6077                        self.draw_button_with_enabled(
6078                            bus,
6079                            abs_top,
6080                            abs_left,
6081                            abs_bottom,
6082                            abs_right,
6083                            "",
6084                            auto_default_outline && item_num == default_item,
6085                            enabled,
6086                        );
6087                    }
6088                }
6089                // userItem (0) and unknown: skip
6090                _ => {}
6091            }
6092        }
6093
6094        // Draw standard controls in reverse DITL/control creation order.
6095        // NewControl adds each control to the beginning of the window's
6096        // control list, and DrawControls draws that list in reverse so the
6097        // earliest-created controls appear frontmost on overlap (IM:I
6098        // I-319/I-322). Dialog Manager standard DITL controls use the same
6099        // Control Manager semantics.
6100        for (i, item) in items.iter().enumerate().rev() {
6101            let item_num = (i + 1) as i16;
6102            let (it, il, ib, ir) = item.rect;
6103            let abs_top = top + it;
6104            let abs_left = left + il;
6105            let abs_bottom = top + ib;
6106            let abs_right = left + ir;
6107            let base_type = item.item_type & 0x7F;
6108            if !matches!(base_type, 4..=7) {
6109                continue;
6110            }
6111            if abs_top >= bottom || abs_bottom <= top || abs_left >= right || abs_right <= left {
6112                continue;
6113            }
6114
6115            let enabled = (item.item_type & 0x80) == 0;
6116            if trace_items {
6117                if let Some(ctrl_handle) = self.dialog_control_handle_for_item(dialog_ptr, item_num)
6118                {
6119                    let ctrl_ptr = bus.read_long(ctrl_handle);
6120                    let proc_id_ctrl = self.control_proc_ids.get(&ctrl_ptr).copied().unwrap_or(0);
6121                    eprintln!(
6122                        "[DLG]     item={} control handle=${:08X} ptr=${:08X} visible={} hilite={} value={} min={} max={} proc_id={}",
6123                        item_num,
6124                        ctrl_handle,
6125                        ctrl_ptr,
6126                        ctrl_ptr != 0 && bus.read_byte(ctrl_ptr + 16) != 0,
6127                        if ctrl_ptr == 0 {
6128                            -1
6129                        } else {
6130                            bus.read_byte(ctrl_ptr + 17) as i16
6131                        },
6132                        if ctrl_ptr == 0 {
6133                            0
6134                        } else {
6135                            bus.read_word(ctrl_ptr + 18) as i16
6136                        },
6137                        if ctrl_ptr == 0 {
6138                            0
6139                        } else {
6140                            bus.read_word(ctrl_ptr + 20) as i16
6141                        },
6142                        if ctrl_ptr == 0 {
6143                            0
6144                        } else {
6145                            bus.read_word(ctrl_ptr + 22) as i16
6146                        },
6147                        proc_id_ctrl,
6148                    );
6149                } else {
6150                    eprintln!("[DLG]     item={} control handle=<none>", item_num);
6151                }
6152            }
6153            match base_type {
6154                4 => {
6155                    self.draw_button_with_enabled(
6156                        bus,
6157                        abs_top,
6158                        abs_left,
6159                        abs_bottom,
6160                        abs_right,
6161                        &item.text,
6162                        auto_default_outline && item_num == default_item,
6163                        enabled,
6164                    );
6165                }
6166                5 => {
6167                    let inactive = self.dialog_control_inactive(bus, dialog_ptr, item_num);
6168                    let checked = self
6169                        .dialog_control_values
6170                        .get(&(dialog_ptr, item_num))
6171                        .copied()
6172                        .unwrap_or(0)
6173                        != 0;
6174                    self.draw_checkbox_with_enabled_and_inactive(
6175                        bus, abs_top, abs_left, abs_bottom, abs_right, &item.text, checked,
6176                        enabled, inactive,
6177                    );
6178                }
6179                6 => {
6180                    let inactive = self.dialog_control_inactive(bus, dialog_ptr, item_num);
6181                    let selected = self
6182                        .dialog_control_values
6183                        .get(&(dialog_ptr, item_num))
6184                        .copied()
6185                        .unwrap_or(0)
6186                        != 0;
6187                    self.draw_radio_with_enabled_and_inactive(
6188                        bus, abs_top, abs_left, abs_bottom, abs_right, &item.text, selected,
6189                        enabled, inactive,
6190                    );
6191                }
6192                7 => {
6193                    if let Some(ctrl_handle) =
6194                        self.dialog_control_handle_for_item(dialog_ptr, item_num)
6195                    {
6196                        let ctrl_ptr = bus.read_long(ctrl_handle);
6197                        let proc_id_ctrl =
6198                            self.control_proc_ids.get(&ctrl_ptr).copied().unwrap_or(0);
6199                        let value = self
6200                            .dialog_control_values
6201                            .get(&(dialog_ptr, item_num))
6202                            .copied()
6203                            .unwrap_or_else(|| bus.read_word(ctrl_ptr + 18) as i16);
6204                        let min = bus.read_word(ctrl_ptr + 20) as i16;
6205                        let max = bus.read_word(ctrl_ptr + 22) as i16;
6206                        let hilite = bus.read_byte(ctrl_ptr + 17);
6207                        let title =
6208                            decode_mac_roman_for_render(&Self::control_title_bytes(bus, ctrl_ptr));
6209
6210                        match proc_id_ctrl {
6211                            0 => self.draw_button_with_enabled(
6212                                bus,
6213                                abs_top,
6214                                abs_left,
6215                                abs_bottom,
6216                                abs_right,
6217                                &title,
6218                                auto_default_outline && item_num == default_item,
6219                                enabled,
6220                            ),
6221                            1 => self.draw_checkbox_with_enabled_and_inactive(
6222                                bus,
6223                                abs_top,
6224                                abs_left,
6225                                abs_bottom,
6226                                abs_right,
6227                                &title,
6228                                value != 0,
6229                                enabled,
6230                                hilite == 255,
6231                            ),
6232                            2 => self.draw_radio_with_enabled_and_inactive(
6233                                bus,
6234                                abs_top,
6235                                abs_left,
6236                                abs_bottom,
6237                                abs_right,
6238                                &title,
6239                                value != 0,
6240                                enabled,
6241                                hilite == 255,
6242                            ),
6243                            16 => self.draw_scroll_bar(
6244                                bus, abs_top, abs_left, abs_bottom, abs_right, value, min, max,
6245                                hilite,
6246                            ),
6247                            proc_id if Self::is_popup_menu_proc_id(proc_id) => {
6248                                let menu_id = self.popup_control_menu_id(bus, ctrl_ptr, min);
6249                                let selected = value.max(1) as usize;
6250                                let item_title = self.popup_menu_item_title(bus, menu_id, selected);
6251                                let (draw_top, draw_left, draw_bottom, draw_right) = self
6252                                    .popup_control_box_rect(
6253                                        bus, abs_top, abs_left, abs_bottom, abs_right, menu_id,
6254                                        max, proc_id,
6255                                    );
6256                                self.draw_popup_control_label(
6257                                    bus,
6258                                    abs_top,
6259                                    abs_left,
6260                                    abs_bottom,
6261                                    draw_left,
6262                                    &title,
6263                                    enabled && hilite != 255,
6264                                );
6265                                self.draw_popup_control_with_state(
6266                                    bus,
6267                                    draw_top,
6268                                    draw_left,
6269                                    draw_bottom,
6270                                    draw_right,
6271                                    &item_title.unwrap_or_default(),
6272                                    enabled && hilite != 255,
6273                                    hilite == 1,
6274                                );
6275                            }
6276                            _ => {
6277                                self.draw_button_with_enabled(
6278                                    bus,
6279                                    abs_top,
6280                                    abs_left,
6281                                    abs_bottom,
6282                                    abs_right,
6283                                    &title,
6284                                    auto_default_outline && item_num == default_item,
6285                                    enabled,
6286                                );
6287                            }
6288                        }
6289                    } else {
6290                        self.draw_button_with_enabled(
6291                            bus,
6292                            abs_top,
6293                            abs_left,
6294                            abs_bottom,
6295                            abs_right,
6296                            "",
6297                            auto_default_outline && item_num == default_item,
6298                            enabled,
6299                        );
6300                    }
6301                }
6302                _ => {}
6303            }
6304        }
6305        self.capture_gui_frame(bus, &format!("draw_dialog_{:08X}", dialog_ptr));
6306    }
6307
6308    fn redraw_standard_dialog_items(
6309        &self,
6310        bus: &mut MacMemoryBus,
6311        bounds: (i16, i16, i16, i16),
6312        items: &[DialogItem],
6313        default_item: i16,
6314        edit_text: &str,
6315        edit_item: i16,
6316        dialog_ptr: u32,
6317    ) {
6318        let (top, left, bottom, right) = bounds;
6319        // Ordinary dialog default outlines are application-owned userItem
6320        // drawing, not synthesized by DrawDialog. See the DrawDialog path
6321        // above for the IM:I/MTE references.
6322        let auto_default_outline = false;
6323        for (i, item) in items.iter().enumerate() {
6324            let item_num = (i + 1) as i16;
6325            let (it, il, ib, ir) = item.rect;
6326            let abs_top = top + it;
6327            let abs_left = left + il;
6328            let abs_bottom = top + ib;
6329            let abs_right = left + ir;
6330            if abs_top >= bottom || abs_bottom <= top || abs_left >= right || abs_right <= left {
6331                continue;
6332            }
6333
6334            let enabled = (item.item_type & 0x80) == 0;
6335            match item.item_type & 0x7F {
6336                4 => self.draw_button_with_enabled(
6337                    bus,
6338                    abs_top,
6339                    abs_left,
6340                    abs_bottom,
6341                    abs_right,
6342                    &item.text,
6343                    auto_default_outline && item_num == default_item,
6344                    enabled,
6345                ),
6346                5 => {
6347                    let inactive = self.dialog_control_inactive(bus, dialog_ptr, item_num);
6348                    let checked = self
6349                        .dialog_control_values
6350                        .get(&(dialog_ptr, item_num))
6351                        .copied()
6352                        .unwrap_or(0)
6353                        != 0;
6354                    self.fill_rect_clipped_to_dialog(
6355                        bus,
6356                        bounds,
6357                        (abs_top, abs_left, abs_bottom, abs_right),
6358                        false,
6359                    );
6360                    self.draw_checkbox_with_enabled_and_inactive(
6361                        bus, abs_top, abs_left, abs_bottom, abs_right, &item.text, checked,
6362                        enabled, inactive,
6363                    );
6364                }
6365                6 => {
6366                    let inactive = self.dialog_control_inactive(bus, dialog_ptr, item_num);
6367                    self.fill_rect_clipped_to_dialog(
6368                        bus,
6369                        bounds,
6370                        (abs_top, abs_left, abs_bottom, abs_right),
6371                        false,
6372                    );
6373                    self.draw_radio_with_enabled_and_inactive(
6374                        bus, abs_top, abs_left, abs_bottom, abs_right, &item.text, false, enabled,
6375                        inactive,
6376                    );
6377                }
6378                7 => {
6379                    if let Some(ctrl_handle) =
6380                        self.dialog_control_handle_for_item(dialog_ptr, item_num)
6381                    {
6382                        let ctrl_ptr = bus.read_long(ctrl_handle);
6383                        let proc_id_ctrl =
6384                            self.control_proc_ids.get(&ctrl_ptr).copied().unwrap_or(0);
6385                        let value = self
6386                            .dialog_control_values
6387                            .get(&(dialog_ptr, item_num))
6388                            .copied()
6389                            .unwrap_or_else(|| bus.read_word(ctrl_ptr + 18) as i16);
6390                        let min = bus.read_word(ctrl_ptr + 20) as i16;
6391                        let max = bus.read_word(ctrl_ptr + 22) as i16;
6392                        let hilite = bus.read_byte(ctrl_ptr + 17);
6393                        let title =
6394                            decode_mac_roman_for_render(&Self::control_title_bytes(bus, ctrl_ptr));
6395                        match proc_id_ctrl {
6396                            0 => self.draw_button_with_enabled(
6397                                bus,
6398                                abs_top,
6399                                abs_left,
6400                                abs_bottom,
6401                                abs_right,
6402                                &title,
6403                                auto_default_outline && item_num == default_item,
6404                                enabled,
6405                            ),
6406                            1 => {
6407                                self.fill_rect_clipped_to_dialog(
6408                                    bus,
6409                                    bounds,
6410                                    (abs_top, abs_left, abs_bottom, abs_right),
6411                                    false,
6412                                );
6413                                self.draw_checkbox_with_enabled_and_inactive(
6414                                    bus,
6415                                    abs_top,
6416                                    abs_left,
6417                                    abs_bottom,
6418                                    abs_right,
6419                                    &title,
6420                                    value != 0,
6421                                    enabled,
6422                                    hilite == 255,
6423                                )
6424                            }
6425                            2 => {
6426                                self.fill_rect_clipped_to_dialog(
6427                                    bus,
6428                                    bounds,
6429                                    (abs_top, abs_left, abs_bottom, abs_right),
6430                                    false,
6431                                );
6432                                self.draw_radio_with_enabled_and_inactive(
6433                                    bus,
6434                                    abs_top,
6435                                    abs_left,
6436                                    abs_bottom,
6437                                    abs_right,
6438                                    &title,
6439                                    value != 0,
6440                                    enabled,
6441                                    hilite == 255,
6442                                )
6443                            }
6444                            16 => self.draw_scroll_bar(
6445                                bus, abs_top, abs_left, abs_bottom, abs_right, value, min, max,
6446                                hilite,
6447                            ),
6448                            proc_id if Self::is_popup_menu_proc_id(proc_id) => {
6449                                let menu_id = self.popup_control_menu_id(bus, ctrl_ptr, min);
6450                                let selected = value.max(1) as usize;
6451                                let item_title = self.popup_menu_item_title(bus, menu_id, selected);
6452                                let (draw_top, draw_left, draw_bottom, draw_right) = self
6453                                    .popup_control_box_rect(
6454                                        bus, abs_top, abs_left, abs_bottom, abs_right, menu_id,
6455                                        max, proc_id,
6456                                    );
6457                                self.draw_popup_control_label(
6458                                    bus,
6459                                    abs_top,
6460                                    abs_left,
6461                                    abs_bottom,
6462                                    draw_left,
6463                                    &title,
6464                                    enabled && hilite != 255,
6465                                );
6466                                self.draw_popup_control_with_state(
6467                                    bus,
6468                                    draw_top,
6469                                    draw_left,
6470                                    draw_bottom,
6471                                    draw_right,
6472                                    &item_title.unwrap_or_default(),
6473                                    enabled && hilite != 255,
6474                                    hilite == 1,
6475                                );
6476                            }
6477                            _ => self.draw_button_with_enabled(
6478                                bus,
6479                                abs_top,
6480                                abs_left,
6481                                abs_bottom,
6482                                abs_right,
6483                                &title,
6484                                auto_default_outline && item_num == default_item,
6485                                enabled,
6486                            ),
6487                        }
6488                    } else {
6489                        self.draw_button_with_enabled(
6490                            bus,
6491                            abs_top,
6492                            abs_left,
6493                            abs_bottom,
6494                            abs_right,
6495                            "",
6496                            auto_default_outline && item_num == default_item,
6497                            enabled,
6498                        );
6499                    }
6500                }
6501                8 => {
6502                    self.fill_rect_clipped_to_dialog(
6503                        bus,
6504                        bounds,
6505                        (abs_top, abs_left, abs_bottom, abs_right),
6506                        false,
6507                    );
6508                    self.draw_static_text(bus, abs_top, abs_left, abs_bottom, abs_right, &item.text)
6509                }
6510                16 => {
6511                    let display_text = if item_num == edit_item {
6512                        edit_text
6513                    } else {
6514                        &item.text
6515                    };
6516                    let selection_range = if item_num == edit_item {
6517                        Self::dialog_item_selection_range(item, display_text.len())
6518                    } else {
6519                        None
6520                    };
6521                    self.draw_edit_text_with_cursor(
6522                        bus,
6523                        abs_top,
6524                        abs_left,
6525                        abs_bottom,
6526                        abs_right,
6527                        display_text,
6528                        selection_range,
6529                        false,
6530                        enabled,
6531                    );
6532                }
6533                _ => {}
6534            }
6535        }
6536    }
6537
6538    fn refresh_dialog_tracking_snapshot(&mut self, bus: &mut MacMemoryBus) {
6539        let Some(tracking) = self.dialog_tracking.as_ref() else {
6540            return;
6541        };
6542        if tracking.game_managed || !tracking.rendered_pixels_final {
6543            return;
6544        }
6545        let bounds = tracking.bounds;
6546        let items = tracking.items.clone();
6547        let default_item = tracking.default_item;
6548        let edit_text = tracking.edit_text.clone();
6549        let edit_item = tracking.edit_item;
6550        let dialog_ptr = tracking.dialog_ptr;
6551        let popup_draws = tracking.popup_draws.clone();
6552        let rendered_pixels = tracking.rendered_pixels.clone();
6553        if !rendered_pixels.is_empty() {
6554            self.restore_dialog_pixels(bus, bounds, &rendered_pixels);
6555        }
6556        self.redraw_standard_dialog_items(
6557            bus,
6558            bounds,
6559            &items,
6560            default_item,
6561            &edit_text,
6562            edit_item,
6563            dialog_ptr,
6564        );
6565        self.redraw_dialog_popup_controls(bus, &popup_draws);
6566        let rendered = self.save_dialog_pixels(bus, bounds);
6567        if let Some(tracking) = self.dialog_tracking.as_mut() {
6568            tracking.rendered_pixels = rendered;
6569        }
6570    }
6571
6572    fn persist_visible_dialog_snapshot(
6573        &mut self,
6574        bus: &MacMemoryBus,
6575        tracking: &DialogTrackingState,
6576    ) {
6577        let pixels = if tracking.rendered_pixels.is_empty() {
6578            self.save_dialog_pixels(bus, tracking.bounds)
6579        } else {
6580            tracking.rendered_pixels.clone()
6581        };
6582        self.dialog_visible_snapshots.insert(
6583            tracking.dialog_ptr,
6584            PersistentDialogSnapshot {
6585                bounds: tracking.bounds,
6586                pixels,
6587            },
6588        );
6589    }
6590
6591    pub(crate) fn refresh_visible_dialog_snapshot_for_port(
6592        &mut self,
6593        bus: &MacMemoryBus,
6594        port: u32,
6595    ) {
6596        if port == 0 {
6597            return;
6598        }
6599        let Some(bounds) = self
6600            .dialog_visible_snapshots
6601            .get(&port)
6602            .map(|snapshot| snapshot.bounds)
6603            .or_else(|| {
6604                if self.dialog_items.contains_key(&port) && self.window_visible(bus, port) {
6605                    Some(Self::dialog_screen_bounds(bus, port))
6606                } else {
6607                    None
6608                }
6609            })
6610        else {
6611            return;
6612        };
6613        let pixels = self.save_dialog_pixels(bus, bounds);
6614        self.dialog_visible_snapshots
6615            .insert(port, PersistentDialogSnapshot { bounds, pixels });
6616    }
6617
6618    pub(crate) fn refresh_visible_dialog_snapshot_after_bulk_port_draw(
6619        &mut self,
6620        bus: &MacMemoryBus,
6621        port: u32,
6622    ) {
6623        if port == 0 {
6624            return;
6625        }
6626        let game_managed_visible = self
6627            .dialog_items
6628            .get(&port)
6629            .filter(|_| self.window_visible(bus, port))
6630            .is_some_and(|items| {
6631                let bounds = Self::dialog_screen_bounds(bus, port);
6632                Self::dialog_is_game_managed(bounds, items)
6633            });
6634        if game_managed_visible {
6635            self.refresh_visible_dialog_snapshot_for_port(bus, port);
6636            return;
6637        }
6638        if !self.dialog_visible_snapshots.contains_key(&port) {
6639            return;
6640        }
6641        // `port` is the port that was just drawn into (callers pass the
6642        // current graphics port). Reaching here means it is a visible dialog
6643        // window with a retained snapshot, so the drawing landed inside that
6644        // dialog and is authoritative content — refresh the retained snapshot
6645        // unconditionally. Applications often render dialog content directly
6646        // into the window before entering ModalDialog (e.g. EV Override's Game
6647        // Speed dialog blits an offscreen slider into a userItem rect via
6648        // CopyBits), which happens before the Dialog Manager has begun modal
6649        // tracking, so gating this on modal-entry lost that drawing.
6650        // Inside Macintosh Volume I, I-405 (userItem contents are
6651        // application-owned and must be preserved across dialog redraws).
6652        let modal_tracking_matches = self
6653            .dialog_tracking
6654            .as_ref()
6655            .is_some_and(|tracking| tracking.dialog_ptr == port);
6656        self.refresh_visible_dialog_snapshot_for_port(bus, port);
6657        // ModalDialog's re-fire restores `rendered_pixels` over the dialog on
6658        // every call, so when the drawing target is the active modal dialog,
6659        // fold the fresh content into that snapshot too or the next re-fire
6660        // immediately erases it.
6661        if modal_tracking_matches {
6662            let bounds = self.dialog_tracking.as_ref().map(|t| t.bounds);
6663            if let Some(bounds) = bounds {
6664                let rendered = self.save_dialog_pixels(bus, bounds);
6665                if let Some(tracking) = self.dialog_tracking.as_mut() {
6666                    tracking.rendered_pixels = rendered;
6667                    tracking.rendered_pixels_final = true;
6668                }
6669            }
6670        }
6671    }
6672
6673    pub(crate) fn redraw_retained_modal_dialog_click(&self, bus: &mut MacMemoryBus) {
6674        let Some(click) = self.retained_modal_dialog_click.as_ref() else {
6675            return;
6676        };
6677        if !click.highlighted || click.item_no <= 0 || self.front_window != click.dialog_ptr {
6678            return;
6679        }
6680        let bounds = self
6681            .dialog_visible_snapshots
6682            .get(&click.dialog_ptr)
6683            .map(|snapshot| snapshot.bounds)
6684            .unwrap_or_else(|| Self::dialog_screen_bounds(bus, click.dialog_ptr));
6685        let rect = Self::dialog_item_screen_rect(bounds, click.rect);
6686        self.draw_dialog_button_highlight_state(bus, rect, &click.title, click.is_default, true);
6687    }
6688
6689    pub(crate) fn finalize_dialog_draw_procs_if_idle(&mut self, bus: &mut MacMemoryBus) {
6690        let Some(tracking) = self.dialog_tracking.as_ref() else {
6691            if let Some(dialog_ptr) = self.active_modeless_dialog_draw_proc.take() {
6692                self.refresh_visible_dialog_snapshot_for_port(bus, dialog_ptr);
6693                self.capture_gui_frame(
6694                    bus,
6695                    &format!("modeless_dialog_draw_proc_{:08X}", dialog_ptr),
6696                );
6697            }
6698            return;
6699        };
6700        if tracking.draw_procs_done || !tracking.draw_proc_queue.is_empty() {
6701            return;
6702        }
6703
6704        let bounds = tracking.bounds;
6705        let items = tracking.items.clone();
6706        let default_item = tracking.default_item;
6707        let edit_text = tracking.edit_text.clone();
6708        let edit_item = tracking.edit_item;
6709        let dialog_ptr = tracking.dialog_ptr;
6710        let popup_draws = tracking.popup_draws.clone();
6711        let game_managed = tracking.game_managed;
6712
6713        if !game_managed {
6714            if self.front_window == dialog_ptr {
6715                self.blit_window_to_screen(bus);
6716            }
6717            self.redraw_standard_dialog_items(
6718                bus,
6719                bounds,
6720                &items,
6721                default_item,
6722                &edit_text,
6723                edit_item,
6724                dialog_ptr,
6725            );
6726        }
6727        self.redraw_dialog_popup_controls(bus, &popup_draws);
6728        let rendered = self.save_dialog_pixels(bus, bounds);
6729
6730        if let Some(tracking) = self.dialog_tracking.as_mut() {
6731            tracking.rendered_pixels = rendered;
6732            tracking.rendered_pixels_final = true;
6733            tracking.draw_procs_done = true;
6734        }
6735    }
6736
6737    /// Draw a 1-pixel black rectangle border.
6738    pub(crate) fn draw_rect_border(
6739        &self,
6740        bus: &mut MacMemoryBus,
6741        top: i16,
6742        left: i16,
6743        bottom: i16,
6744        right: i16,
6745    ) {
6746        let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
6747            self.get_screen_params();
6748        // Top edge
6749        Self::fb_hline(
6750            bus,
6751            screen_base,
6752            row_bytes,
6753            pixel_size,
6754            screen_width,
6755            screen_height,
6756            top,
6757            left,
6758            right - 1,
6759            true,
6760        );
6761        // Bottom edge
6762        Self::fb_hline(
6763            bus,
6764            screen_base,
6765            row_bytes,
6766            pixel_size,
6767            screen_width,
6768            screen_height,
6769            bottom - 1,
6770            left,
6771            right - 1,
6772            true,
6773        );
6774        // Left edge
6775        for y in top..bottom {
6776            Self::fb_set_pixel(
6777                bus,
6778                screen_base,
6779                row_bytes,
6780                pixel_size,
6781                screen_width,
6782                screen_height,
6783                left,
6784                y,
6785                true,
6786            );
6787        }
6788        // Right edge
6789        for y in top..bottom {
6790            Self::fb_set_pixel(
6791                bus,
6792                screen_base,
6793                row_bytes,
6794                pixel_size,
6795                screen_width,
6796                screen_height,
6797                right - 1,
6798                y,
6799                true,
6800            );
6801        }
6802    }
6803
6804    fn fill_dialog_rect(
6805        &self,
6806        bus: &mut MacMemoryBus,
6807        top: i16,
6808        left: i16,
6809        bottom: i16,
6810        right: i16,
6811        black: bool,
6812    ) {
6813        let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
6814            self.get_screen_params();
6815        Self::fb_fill_rect(
6816            bus,
6817            screen_base,
6818            row_bytes,
6819            pixel_size,
6820            screen_width,
6821            screen_height,
6822            top,
6823            left,
6824            bottom,
6825            right,
6826            black,
6827        );
6828    }
6829
6830    fn draw_classic_dbox_frame(
6831        &self,
6832        bus: &mut MacMemoryBus,
6833        top: i16,
6834        left: i16,
6835        bottom: i16,
6836        right: i16,
6837    ) {
6838        let margin = Self::DBOX_FRAME_MARGIN;
6839        // dBoxProc is a Window Manager WDEF variant, not a content-edge
6840        // dialog item border. IM:I I-273 describes a modal dialog box as a
6841        // rectangular window with its border inside the structure edge; the
6842        // boundsRect remains the grafPort content region (IM:I I-282).
6843        self.fill_dialog_rect(
6844            bus,
6845            top - margin,
6846            left - margin,
6847            bottom + margin,
6848            right + margin,
6849            false,
6850        );
6851
6852        // System 7.5.3's standard WDEF draws an asymmetric black structure
6853        // edge: one pixel on top/left and a two-pixel shadow edge on
6854        // bottom/right, with a two-pixel inner band inset from the outer edge.
6855        self.fill_dialog_rect(bus, top - 8, left - 8, top - 7, right + 8, true);
6856        self.fill_dialog_rect(bus, top - 8, left - 8, bottom + 8, left - 7, true);
6857        self.fill_dialog_rect(bus, top - 8, right + 6, bottom + 8, right + 8, true);
6858        self.fill_dialog_rect(bus, bottom + 6, left - 8, bottom + 8, right + 8, true);
6859
6860        self.fill_dialog_rect(bus, top - 5, left - 5, top - 4, right + 5, true);
6861        self.fill_dialog_rect(bus, top - 4, left - 5, top - 3, right + 4, true);
6862        self.fill_dialog_rect(bus, top - 5, left - 5, bottom + 5, left - 4, true);
6863        self.fill_dialog_rect(bus, top - 5, left - 4, bottom + 4, left - 3, true);
6864        self.fill_dialog_rect(bus, top - 5, right + 3, bottom + 4, right + 4, true);
6865        self.fill_dialog_rect(bus, bottom + 3, left - 5, bottom + 4, right + 4, true);
6866    }
6867
6868    /// Draw a 2-pixel shadow on bottom and right edges.
6869    pub(crate) fn draw_shadow(
6870        &self,
6871        bus: &mut MacMemoryBus,
6872        top: i16,
6873        left: i16,
6874        bottom: i16,
6875        right: i16,
6876    ) {
6877        let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
6878            self.get_screen_params();
6879        // Right shadow (2px wide)
6880        for dx in 0..2i16 {
6881            for y in (top + 2)..=(bottom + dx) {
6882                Self::fb_set_pixel(
6883                    bus,
6884                    screen_base,
6885                    row_bytes,
6886                    pixel_size,
6887                    screen_width,
6888                    screen_height,
6889                    right + dx,
6890                    y,
6891                    true,
6892                );
6893            }
6894        }
6895        // Bottom shadow (2px tall)
6896        for dy in 0..2i16 {
6897            Self::fb_hline(
6898                bus,
6899                screen_base,
6900                row_bytes,
6901                pixel_size,
6902                screen_width,
6903                screen_height,
6904                bottom + dy,
6905                left + 2,
6906                right + 2,
6907                true,
6908            );
6909        }
6910    }
6911
6912    /// Draw a popup menu control button (procID 1008 / popupMenuProc).
6913    /// Shows a bordered rectangle with a 1-px drop shadow, a downward-pointing
6914    /// triangle on the right, and the currently-selected item title.
6915    /// Macintosh Toolbox Essentials 1992, 3-31
6916    pub(crate) fn draw_popup_control(
6917        &self,
6918        bus: &mut MacMemoryBus,
6919        top: i16,
6920        left: i16,
6921        bottom: i16,
6922        right: i16,
6923        title: &str,
6924    ) {
6925        self.draw_popup_control_with_state(bus, top, left, bottom, right, title, true, false);
6926    }
6927
6928    pub(crate) fn popup_control_box_rect(
6929        &self,
6930        bus: &MacMemoryBus,
6931        top: i16,
6932        left: i16,
6933        bottom: i16,
6934        right: i16,
6935        menu_id: i16,
6936        title_width: i16,
6937        proc_id: i16,
6938    ) -> (i16, i16, i16, i16) {
6939        let (_, _, screen_width, _, _) = self.get_screen_params();
6940        let box_top = top + 1;
6941        let box_left = left + title_width.max(0);
6942        let box_bottom = bottom - 2;
6943        let fixed_width = ((proc_id - 1008) & 0x0001) != 0;
6944        let box_right = if fixed_width {
6945            right - 1
6946        } else {
6947            let text_width = self.popup_menu_max_item_width(bus, menu_id);
6948            let auto_width = (text_width + 40).max(80);
6949            (box_left + auto_width).min(right - 1)
6950        };
6951        (
6952            box_top,
6953            box_left.min(screen_width),
6954            box_bottom,
6955            box_right.min(screen_width),
6956        )
6957    }
6958
6959    fn popup_menu_max_item_width(&self, bus: &MacMemoryBus, menu_id: i16) -> i16 {
6960        let font_id = 0i16;
6961        let font_size = 12i16;
6962        let mut max_width = 0;
6963        for item_no in 1..=255usize {
6964            let Some(title) = self.popup_menu_item_title(bus, menu_id, item_no) else {
6965                break;
6966            };
6967            max_width = max_width.max(Self::fb_measure_string(&title, font_id, font_size));
6968        }
6969        max_width
6970    }
6971
6972    pub(crate) fn draw_popup_control_label(
6973        &self,
6974        bus: &mut MacMemoryBus,
6975        top: i16,
6976        left: i16,
6977        bottom: i16,
6978        popup_left: i16,
6979        title: &str,
6980        enabled: bool,
6981    ) {
6982        if title.is_empty() || popup_left <= left + 4 {
6983            return;
6984        }
6985
6986        let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
6987            self.get_screen_params();
6988        let font_id = 0i16;
6989        let font_size = 12i16;
6990        let metrics = get_font_metrics(font_id, font_size);
6991        let text_width = Self::fb_measure_string(title, font_id, font_size);
6992        let text_right = popup_left - 6;
6993        let text_x = (text_right - text_width).max(left);
6994        let text_y = top + ((bottom - top) + metrics.ascent - metrics.descent) / 2;
6995
6996        Self::fb_draw_string(
6997            bus,
6998            screen_base,
6999            row_bytes,
7000            pixel_size,
7001            screen_width,
7002            screen_height,
7003            text_x,
7004            text_y,
7005            title,
7006            font_id,
7007            font_size,
7008        );
7009        if !enabled {
7010            self.dim_rect(bus, top, left, bottom, popup_left);
7011        }
7012    }
7013
7014    pub(crate) fn draw_popup_control_with_state(
7015        &self,
7016        bus: &mut MacMemoryBus,
7017        top: i16,
7018        left: i16,
7019        bottom: i16,
7020        right: i16,
7021        title: &str,
7022        enabled: bool,
7023        pressed: bool,
7024    ) {
7025        let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
7026            self.get_screen_params();
7027
7028        let font_id = 0i16;
7029        let font_size = 12i16;
7030
7031        if !self.draw_theme_control_chrome(
7032            bus,
7033            ControlKind::PopupButton,
7034            top,
7035            left,
7036            bottom,
7037            right,
7038            enabled,
7039            pressed,
7040            false,
7041            false,
7042        ) {
7043            // Standard popup menu button appearance.
7044            // Macintosh Toolbox Essentials 1992, 5-26 to 5-27.
7045            Self::fb_fill_rect(
7046                bus,
7047                screen_base,
7048                row_bytes,
7049                pixel_size,
7050                screen_width,
7051                screen_height,
7052                top,
7053                left,
7054                bottom,
7055                right + 1,
7056                false,
7057            );
7058            if enabled {
7059                Self::fb_hline(
7060                    bus,
7061                    screen_base,
7062                    row_bytes,
7063                    pixel_size,
7064                    screen_width,
7065                    screen_height,
7066                    top,
7067                    left,
7068                    right,
7069                    true,
7070                );
7071                Self::fb_hline(
7072                    bus,
7073                    screen_base,
7074                    row_bytes,
7075                    pixel_size,
7076                    screen_width,
7077                    screen_height,
7078                    bottom - 2,
7079                    left,
7080                    right,
7081                    true,
7082                );
7083                for y in top..(bottom - 1) {
7084                    Self::fb_set_pixel(
7085                        bus,
7086                        screen_base,
7087                        row_bytes,
7088                        pixel_size,
7089                        screen_width,
7090                        screen_height,
7091                        left,
7092                        y,
7093                        true,
7094                    );
7095                    Self::fb_set_pixel(
7096                        bus,
7097                        screen_base,
7098                        row_bytes,
7099                        pixel_size,
7100                        screen_width,
7101                        screen_height,
7102                        right - 1,
7103                        y,
7104                        true,
7105                    );
7106                }
7107                for y in (top + 3)..(bottom - 1) {
7108                    Self::fb_set_pixel(
7109                        bus,
7110                        screen_base,
7111                        row_bytes,
7112                        pixel_size,
7113                        screen_width,
7114                        screen_height,
7115                        right,
7116                        y,
7117                        true,
7118                    );
7119                }
7120                Self::fb_hline(
7121                    bus,
7122                    screen_base,
7123                    row_bytes,
7124                    pixel_size,
7125                    screen_width,
7126                    screen_height,
7127                    bottom - 1,
7128                    left + 3,
7129                    right + 1,
7130                    true,
7131                );
7132            } else {
7133                for y in (top + 3)..(bottom - 2) {
7134                    if (y - top) % 2 == 1 {
7135                        Self::fb_set_pixel(
7136                            bus,
7137                            screen_base,
7138                            row_bytes,
7139                            pixel_size,
7140                            screen_width,
7141                            screen_height,
7142                            right,
7143                            y,
7144                            true,
7145                        );
7146                    }
7147                }
7148                for x in (left + 1)..=right {
7149                    if (x - left) % 2 == 1 {
7150                        Self::fb_set_pixel(
7151                            bus,
7152                            screen_base,
7153                            row_bytes,
7154                            pixel_size,
7155                            screen_width,
7156                            screen_height,
7157                            x,
7158                            bottom - 2,
7159                            true,
7160                        );
7161                    }
7162                }
7163            }
7164
7165            // Downward-pointing triangle on right side (popup indicator)
7166            // Macintosh Toolbox Essentials 1992, 5-26
7167            let tri_x = right - 12;
7168            if enabled {
7169                for row in 0..6i16 {
7170                    Self::fb_hline(
7171                        bus,
7172                        screen_base,
7173                        row_bytes,
7174                        pixel_size,
7175                        screen_width,
7176                        screen_height,
7177                        top + 6 + row,
7178                        tri_x - 5 + row,
7179                        tri_x + 6 - row,
7180                        true,
7181                    );
7182                }
7183            } else {
7184                for row in [0i16, 2, 4] {
7185                    let start = tri_x - 4 + row;
7186                    let end = tri_x + 5 - row;
7187                    for x in start..end {
7188                        if (x - start) % 2 == 0 {
7189                            Self::fb_set_pixel(
7190                                bus,
7191                                screen_base,
7192                                row_bytes,
7193                                pixel_size,
7194                                screen_width,
7195                                screen_height,
7196                                x,
7197                                top + 7 + row,
7198                                true,
7199                            );
7200                        }
7201                    }
7202                }
7203            }
7204        }
7205
7206        // Selected item text inside the box
7207        // Macintosh Toolbox Essentials 1992, 5-26
7208        if enabled && !title.is_empty() {
7209            let metrics = get_font_metrics(font_id, font_size);
7210            let text_x = left + 15;
7211            let text_y =
7212                top + (bottom - top - (metrics.ascent + metrics.descent)) / 2 + metrics.ascent - 1;
7213            Self::fb_draw_string_styled(
7214                bus,
7215                screen_base,
7216                row_bytes,
7217                pixel_size,
7218                screen_width,
7219                screen_height,
7220                text_x,
7221                text_y,
7222                title,
7223                font_id,
7224                font_size,
7225                0,
7226            );
7227        }
7228    }
7229
7230    fn redraw_dialog_popup_controls(
7231        &self,
7232        bus: &mut MacMemoryBus,
7233        popup_draws: &[DialogPopupDraw],
7234    ) {
7235        for draw in popup_draws {
7236            let (top, left, bottom, right) = draw.rect;
7237            if draw.enabled && !draw.pressed {
7238                self.draw_popup_control(bus, top, left, bottom, right, &draw.title);
7239                continue;
7240            }
7241            self.draw_popup_control_with_state(
7242                bus,
7243                top,
7244                left,
7245                bottom,
7246                right,
7247                &draw.title,
7248                draw.enabled,
7249                draw.pressed,
7250            );
7251        }
7252    }
7253
7254    /// Draw a framed round-rect directly to the framebuffer.
7255    /// Used for dialog default button outlines where no CpuOps/port is available.
7256    /// Macintosh Toolbox Essentials 1992, Listing 6-17
7257    fn fb_frame_round_rect(
7258        &self,
7259        bus: &mut MacMemoryBus,
7260        top: i16,
7261        left: i16,
7262        bottom: i16,
7263        right: i16,
7264        oval_width: i16,
7265        oval_height: i16,
7266        pen_size: i16,
7267    ) {
7268        let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
7269            self.get_screen_params();
7270        let r = Rect {
7271            top,
7272            left,
7273            bottom,
7274            right,
7275        };
7276        let outer_spans = self.compute_rrect_spans(&r, oval_width, oval_height);
7277
7278        let r_inset = Rect {
7279            top: top + pen_size,
7280            left: left + pen_size,
7281            bottom: bottom - pen_size,
7282            right: right - pen_size,
7283        };
7284        let inner_spans = self.compute_rrect_spans(
7285            &r_inset,
7286            (oval_width - 2 * pen_size).max(0),
7287            (oval_height - 2 * pen_size).max(0),
7288        );
7289
7290        for y in top..bottom {
7291            let outer_idx = (y - top) as usize;
7292            if outer_idx >= outer_spans.len() {
7293                continue;
7294            }
7295            let (ol, or) = outer_spans[outer_idx];
7296
7297            let inner_idx = (y - r_inset.top) as usize;
7298            let (il, ir) =
7299                if y >= r_inset.top && y < r_inset.bottom && inner_idx < inner_spans.len() {
7300                    inner_spans[inner_idx]
7301                } else {
7302                    (or, ol) // no inner = draw full outer span
7303                };
7304
7305            // Left border segment
7306            for x in ol..il.min(or) {
7307                Self::fb_set_pixel(
7308                    bus,
7309                    screen_base,
7310                    row_bytes,
7311                    pixel_size,
7312                    screen_width,
7313                    screen_height,
7314                    x,
7315                    y,
7316                    true,
7317                );
7318            }
7319            // Right border segment
7320            for x in ir.max(ol)..or {
7321                Self::fb_set_pixel(
7322                    bus,
7323                    screen_base,
7324                    row_bytes,
7325                    pixel_size,
7326                    screen_width,
7327                    screen_height,
7328                    x,
7329                    y,
7330                    true,
7331                );
7332            }
7333        }
7334    }
7335
7336    /// Draw a button with optional default (thick) border.
7337    /// Inside Macintosh Volume I, I-405
7338    pub(crate) fn draw_button(
7339        &self,
7340        bus: &mut MacMemoryBus,
7341        top: i16,
7342        left: i16,
7343        bottom: i16,
7344        right: i16,
7345        title: &str,
7346        is_default: bool,
7347    ) {
7348        self.draw_button_state(bus, top, left, bottom, right, title, is_default, true);
7349    }
7350
7351    fn draw_button_with_enabled(
7352        &self,
7353        bus: &mut MacMemoryBus,
7354        top: i16,
7355        left: i16,
7356        bottom: i16,
7357        right: i16,
7358        title: &str,
7359        is_default: bool,
7360        enabled: bool,
7361    ) {
7362        if enabled {
7363            self.draw_button(bus, top, left, bottom, right, title, is_default);
7364            return;
7365        }
7366        self.draw_button_state(bus, top, left, bottom, right, title, is_default, enabled);
7367    }
7368
7369    fn draw_button_state(
7370        &self,
7371        bus: &mut MacMemoryBus,
7372        top: i16,
7373        left: i16,
7374        bottom: i16,
7375        right: i16,
7376        title: &str,
7377        is_default: bool,
7378        enabled: bool,
7379    ) {
7380        if !self.draw_theme_push_button_chrome(
7381            bus, top, left, bottom, right, enabled, false, is_default,
7382        ) {
7383            self.fill_classic_button_shape(bus, top, left, bottom, right);
7384            self.draw_classic_button_outline(bus, top, left, bottom, right);
7385
7386            // Default button: rounded bold outline (3px thick)
7387            // Macintosh Toolbox Essentials 1992, Listing 6-17
7388            // references/executor/src/error/system_error.cpp
7389            if is_default {
7390                let hilite_top = top - 4;
7391                let hilite_left = left - 4;
7392                let hilite_bottom = bottom + 4;
7393                let hilite_right = right + 4;
7394                let hilite_height = hilite_bottom - hilite_top;
7395                let oval = (hilite_height / 2 - 4).max(4);
7396                self.fb_frame_round_rect(
7397                    bus,
7398                    hilite_top,
7399                    hilite_left,
7400                    hilite_bottom,
7401                    hilite_right,
7402                    oval,
7403                    oval,
7404                    3,
7405                );
7406            }
7407        }
7408
7409        self.draw_button_label(bus, top, left, bottom, right, title);
7410    }
7411
7412    fn fill_classic_button_shape(
7413        &self,
7414        bus: &mut MacMemoryBus,
7415        top: i16,
7416        left: i16,
7417        bottom: i16,
7418        right: i16,
7419    ) {
7420        if bottom - top < 8 || right - left < 8 {
7421            let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
7422                self.get_screen_params();
7423            Self::fb_fill_rect(
7424                bus,
7425                screen_base,
7426                row_bytes,
7427                pixel_size,
7428                screen_width,
7429                screen_height,
7430                top,
7431                left,
7432                bottom,
7433                right,
7434                false,
7435            );
7436            return;
7437        }
7438
7439        // Classic push buttons are rounded controls inside the DITL display
7440        // rectangle. Fill the same rounded shape the CDEF frames so pixels in
7441        // the rectangular corner cutouts remain whatever the dialog port had
7442        // underneath.
7443        if bottom - top >= 24 {
7444            self.fill_dialog_rect(bus, top, left + 4, top + 1, right - 4, false);
7445            self.fill_dialog_rect(bus, top + 1, left + 2, top + 2, right - 2, false);
7446            self.fill_dialog_rect(bus, top + 2, left + 1, top + 4, right - 1, false);
7447            self.fill_dialog_rect(bus, top + 4, left, bottom - 4, right, false);
7448            self.fill_dialog_rect(bus, bottom - 4, left + 1, bottom - 2, right - 1, false);
7449            self.fill_dialog_rect(bus, bottom - 2, left + 2, bottom - 1, right - 2, false);
7450            self.fill_dialog_rect(bus, bottom - 1, left + 4, bottom, right - 4, false);
7451            return;
7452        }
7453
7454        self.fill_dialog_rect(bus, top, left + 3, top + 1, right - 3, false);
7455        self.fill_dialog_rect(bus, top + 1, left + 1, top + 3, right - 1, false);
7456        self.fill_dialog_rect(bus, top + 3, left, bottom - 3, right, false);
7457        self.fill_dialog_rect(bus, bottom - 3, left + 1, bottom - 1, right - 1, false);
7458        self.fill_dialog_rect(bus, bottom - 1, left + 3, bottom, right - 3, false);
7459    }
7460
7461    fn draw_button_label(
7462        &self,
7463        bus: &mut MacMemoryBus,
7464        top: i16,
7465        left: i16,
7466        bottom: i16,
7467        right: i16,
7468        title: &str,
7469    ) {
7470        let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
7471            self.get_screen_params();
7472        let font_id = 0i16; // Chicago
7473        let font_size = 12i16;
7474        let metrics = get_font_metrics(font_id, font_size);
7475        let text_w = Self::fb_measure_string(title, font_id, font_size);
7476        let text_x = left + (right - left - text_w) / 2;
7477        let text_y = top + (bottom - top - (metrics.ascent + metrics.descent)) / 2 + metrics.ascent;
7478        Self::fb_draw_string(
7479            bus,
7480            screen_base,
7481            row_bytes,
7482            pixel_size,
7483            screen_width,
7484            screen_height,
7485            text_x,
7486            text_y,
7487            title,
7488            font_id,
7489            font_size,
7490        );
7491    }
7492
7493    fn draw_classic_button_outline(
7494        &self,
7495        bus: &mut MacMemoryBus,
7496        top: i16,
7497        left: i16,
7498        bottom: i16,
7499        right: i16,
7500    ) {
7501        if bottom - top < 8 || right - left < 8 {
7502            self.draw_rect_border(bus, top, left, bottom, right);
7503            return;
7504        }
7505
7506        // Dialog Manager buttons are standard controls whose display
7507        // rectangle becomes the control's enclosing rectangle (IM:I I-405).
7508        // The classic System 7 control definition draws the push button as a
7509        // one-pixel rounded rectangle inside that enclosing rectangle.
7510        if bottom - top >= 24 {
7511            self.fill_dialog_rect(bus, top, left + 4, top + 1, right - 4, true);
7512            self.fill_dialog_rect(bus, top + 1, left + 2, top + 2, left + 4, true);
7513            self.fill_dialog_rect(bus, top + 1, right - 4, top + 2, right - 2, true);
7514            self.fill_dialog_rect(bus, top + 2, left + 1, top + 3, left + 2, true);
7515            self.fill_dialog_rect(bus, top + 2, right - 2, top + 3, right - 1, true);
7516            self.fill_dialog_rect(bus, top + 3, left + 1, top + 4, left + 2, true);
7517            self.fill_dialog_rect(bus, top + 3, right - 2, top + 4, right - 1, true);
7518            self.fill_dialog_rect(bus, top + 4, left, bottom - 4, left + 1, true);
7519            self.fill_dialog_rect(bus, top + 4, right - 1, bottom - 4, right, true);
7520            self.fill_dialog_rect(bus, bottom - 4, left + 1, bottom - 3, left + 2, true);
7521            self.fill_dialog_rect(bus, bottom - 4, right - 2, bottom - 3, right - 1, true);
7522            self.fill_dialog_rect(bus, bottom - 3, left + 1, bottom - 2, left + 2, true);
7523            self.fill_dialog_rect(bus, bottom - 3, right - 2, bottom - 2, right - 1, true);
7524            self.fill_dialog_rect(bus, bottom - 2, left + 2, bottom - 1, left + 4, true);
7525            self.fill_dialog_rect(bus, bottom - 2, right - 4, bottom - 1, right - 2, true);
7526            self.fill_dialog_rect(bus, bottom - 1, left + 4, bottom, right - 4, true);
7527            return;
7528        }
7529
7530        self.fill_dialog_rect(bus, top, left + 3, top + 1, right - 3, true);
7531        self.fill_dialog_rect(bus, top + 1, left + 1, top + 2, left + 3, true);
7532        self.fill_dialog_rect(bus, top + 1, right - 3, top + 2, right - 1, true);
7533        self.fill_dialog_rect(bus, top + 2, left + 1, top + 3, left + 2, true);
7534        self.fill_dialog_rect(bus, top + 2, right - 2, top + 3, right - 1, true);
7535        self.fill_dialog_rect(bus, top + 3, left, bottom - 3, left + 1, true);
7536        self.fill_dialog_rect(bus, top + 3, right - 1, bottom - 3, right, true);
7537        self.fill_dialog_rect(bus, bottom - 3, left + 1, bottom - 2, left + 2, true);
7538        self.fill_dialog_rect(bus, bottom - 3, right - 2, bottom - 2, right - 1, true);
7539        self.fill_dialog_rect(bus, bottom - 2, left + 1, bottom - 1, left + 3, true);
7540        self.fill_dialog_rect(bus, bottom - 2, right - 3, bottom - 1, right - 1, true);
7541        self.fill_dialog_rect(bus, bottom - 1, left + 3, bottom, right - 3, true);
7542    }
7543
7544    fn draw_dialog_button_highlight_state(
7545        &self,
7546        bus: &mut MacMemoryBus,
7547        rect: (i16, i16, i16, i16),
7548        title: &str,
7549        is_default: bool,
7550        highlighted: bool,
7551    ) {
7552        let (top, left, bottom, right) = rect;
7553        if self.draw_theme_push_button_chrome(
7554            bus,
7555            top,
7556            left,
7557            bottom,
7558            right,
7559            true,
7560            highlighted,
7561            is_default,
7562        ) {
7563            self.draw_button_label(bus, top, left, bottom, right, title);
7564            return;
7565        }
7566        self.invert_button_rect(bus, top, left, bottom, right);
7567    }
7568
7569    /// Draw a checkbox item.
7570    pub(crate) fn draw_checkbox(
7571        &self,
7572        bus: &mut MacMemoryBus,
7573        top: i16,
7574        left: i16,
7575        bottom: i16,
7576        _right: i16,
7577        title: &str,
7578        checked: bool,
7579    ) {
7580        self.draw_checkbox_state(bus, top, left, bottom, _right, title, checked, true, false);
7581    }
7582
7583    fn draw_checkbox_with_enabled(
7584        &self,
7585        bus: &mut MacMemoryBus,
7586        top: i16,
7587        left: i16,
7588        bottom: i16,
7589        _right: i16,
7590        title: &str,
7591        checked: bool,
7592        enabled: bool,
7593    ) {
7594        self.draw_checkbox_with_enabled_and_inactive(
7595            bus, top, left, bottom, _right, title, checked, enabled, false,
7596        );
7597    }
7598
7599    fn draw_checkbox_with_enabled_and_inactive(
7600        &self,
7601        bus: &mut MacMemoryBus,
7602        top: i16,
7603        left: i16,
7604        bottom: i16,
7605        _right: i16,
7606        title: &str,
7607        checked: bool,
7608        enabled: bool,
7609        inactive: bool,
7610    ) {
7611        if enabled && !inactive {
7612            self.draw_checkbox(bus, top, left, bottom, _right, title, checked);
7613            return;
7614        }
7615        self.draw_checkbox_state(
7616            bus, top, left, bottom, _right, title, checked, enabled, inactive,
7617        );
7618    }
7619
7620    fn draw_checkbox_state(
7621        &self,
7622        bus: &mut MacMemoryBus,
7623        top: i16,
7624        left: i16,
7625        bottom: i16,
7626        _right: i16,
7627        title: &str,
7628        checked: bool,
7629        enabled: bool,
7630        inactive: bool,
7631    ) {
7632        let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
7633            self.get_screen_params();
7634        let box_size = Self::STANDARD_CONTROL_MARK_SIZE;
7635        // Standard dialog checkbox/radio controls are drawn by the System
7636        // CDEF as a 12-pixel mark plus title inside the item rectangle
7637        // (MTE 1992 pp. 5-4..5-5; HIG 1992 pp. 209..212).
7638        let height = bottom - top;
7639        let box_top = top + (height - box_size) / 2;
7640        let box_left = left + Self::STANDARD_CONTROL_MARK_LEFT_INSET;
7641        if !self.draw_theme_control_chrome(
7642            bus,
7643            ControlKind::Checkbox,
7644            box_top,
7645            box_left,
7646            box_top + box_size,
7647            box_left + box_size,
7648            enabled && !inactive,
7649            false,
7650            checked,
7651            false,
7652        ) {
7653            // Draw checkbox box
7654            Self::fb_fill_rect(
7655                bus,
7656                screen_base,
7657                row_bytes,
7658                pixel_size,
7659                screen_width,
7660                screen_height,
7661                box_top,
7662                box_left,
7663                box_top + box_size,
7664                box_left + box_size,
7665                false,
7666            );
7667            self.draw_rect_border(
7668                bus,
7669                box_top,
7670                box_left,
7671                box_top + box_size,
7672                box_left + box_size,
7673            );
7674            if checked {
7675                // Draw X inside
7676                for i in 1..box_size - 1 {
7677                    Self::fb_set_pixel(
7678                        bus,
7679                        screen_base,
7680                        row_bytes,
7681                        pixel_size,
7682                        screen_width,
7683                        screen_height,
7684                        box_left + i,
7685                        box_top + i,
7686                        true,
7687                    );
7688                    Self::fb_set_pixel(
7689                        bus,
7690                        screen_base,
7691                        row_bytes,
7692                        pixel_size,
7693                        screen_width,
7694                        screen_height,
7695                        box_left + box_size - 1 - i,
7696                        box_top + i,
7697                        true,
7698                    );
7699                }
7700            }
7701        }
7702        // Draw label text to the right of the box. Checkbox/radio items are
7703        // controls, and SetDialogFont/SetDAFont does not affect control
7704        // titles (IM:I I-412; MTE 1992 p. 6-105).
7705        let font_id = 0i16;
7706        let font_size = 12i16;
7707        let metrics = get_font_metrics(font_id, font_size);
7708        let text_x = box_left + box_size + Self::STANDARD_CONTROL_TITLE_GAP;
7709        let text_y = top + (height + metrics.ascent - metrics.descent) / 2;
7710        let label_left = box_left + box_size + 1;
7711        let dim_disabled_title = !enabled && self.ui_theme_id() != UiThemeId::ClassicSystem7;
7712        self.draw_standard_control_title_text(
7713            bus,
7714            top,
7715            label_left,
7716            bottom,
7717            _right,
7718            text_x,
7719            text_y,
7720            title,
7721            font_id,
7722            font_size,
7723            inactive,
7724            dim_disabled_title,
7725        );
7726    }
7727
7728    /// Draw a radio button item.
7729    pub(crate) fn draw_radio(
7730        &self,
7731        bus: &mut MacMemoryBus,
7732        top: i16,
7733        left: i16,
7734        bottom: i16,
7735        _right: i16,
7736        title: &str,
7737        selected: bool,
7738    ) {
7739        self.draw_radio_state(bus, top, left, bottom, _right, title, selected, true, false);
7740    }
7741
7742    fn draw_radio_with_enabled(
7743        &self,
7744        bus: &mut MacMemoryBus,
7745        top: i16,
7746        left: i16,
7747        bottom: i16,
7748        _right: i16,
7749        title: &str,
7750        selected: bool,
7751        enabled: bool,
7752    ) {
7753        self.draw_radio_with_enabled_and_inactive(
7754            bus, top, left, bottom, _right, title, selected, enabled, false,
7755        );
7756    }
7757
7758    fn draw_radio_with_enabled_and_inactive(
7759        &self,
7760        bus: &mut MacMemoryBus,
7761        top: i16,
7762        left: i16,
7763        bottom: i16,
7764        _right: i16,
7765        title: &str,
7766        selected: bool,
7767        enabled: bool,
7768        inactive: bool,
7769    ) {
7770        if enabled && !inactive {
7771            self.draw_radio(bus, top, left, bottom, _right, title, selected);
7772            return;
7773        }
7774        self.draw_radio_state(
7775            bus, top, left, bottom, _right, title, selected, enabled, inactive,
7776        );
7777    }
7778
7779    fn draw_radio_state(
7780        &self,
7781        bus: &mut MacMemoryBus,
7782        top: i16,
7783        left: i16,
7784        bottom: i16,
7785        _right: i16,
7786        title: &str,
7787        selected: bool,
7788        enabled: bool,
7789        inactive: bool,
7790    ) {
7791        let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
7792            self.get_screen_params();
7793        let mark_size = Self::STANDARD_CONTROL_MARK_SIZE;
7794        let height = bottom - top;
7795        let mark_top = top + (height - mark_size) / 2;
7796        let mark_left = left + Self::STANDARD_CONTROL_MARK_LEFT_INSET;
7797        if !self.draw_theme_control_chrome(
7798            bus,
7799            ControlKind::RadioButton,
7800            mark_top,
7801            mark_left,
7802            mark_top + mark_size,
7803            mark_left + mark_size,
7804            enabled && !inactive,
7805            false,
7806            selected,
7807            false,
7808        ) {
7809            Self::fb_fill_rect(
7810                bus,
7811                screen_base,
7812                row_bytes,
7813                pixel_size,
7814                screen_width,
7815                screen_height,
7816                mark_top,
7817                mark_left,
7818                mark_top + mark_size,
7819                mark_left + mark_size,
7820                false,
7821            );
7822            // Radio buttons are small circles, with a dot when selected
7823            // (IM:I I-312; MTE 1992 pp. 5-5..5-6; HIG 1992 pp. 209..210).
7824            self.draw_control_mask_12(
7825                bus,
7826                screen_base,
7827                row_bytes,
7828                pixel_size,
7829                screen_width,
7830                screen_height,
7831                mark_left,
7832                mark_top,
7833                &Self::CLASSIC_RADIO_OUTLINE_12,
7834            );
7835            if selected {
7836                self.draw_control_mask_12(
7837                    bus,
7838                    screen_base,
7839                    row_bytes,
7840                    pixel_size,
7841                    screen_width,
7842                    screen_height,
7843                    mark_left,
7844                    mark_top,
7845                    &Self::CLASSIC_RADIO_DOT_12,
7846                );
7847            }
7848        }
7849        // Label
7850        // SetDialogFont/SetDAFont affects statText and editText items but
7851        // not control titles (IM:I I-412; MTE 1992 p. 6-105).
7852        let font_id = 0i16;
7853        let font_size = 12i16;
7854        let metrics = get_font_metrics(font_id, font_size);
7855        let text_x = mark_left + mark_size + Self::STANDARD_CONTROL_TITLE_GAP;
7856        let text_y = top + (height + metrics.ascent - metrics.descent) / 2;
7857        let label_left = mark_left + mark_size + 1;
7858        let dim_disabled_title = !enabled && self.ui_theme_id() != UiThemeId::ClassicSystem7;
7859        self.draw_standard_control_title_text(
7860            bus,
7861            top,
7862            label_left,
7863            bottom,
7864            _right,
7865            text_x,
7866            text_y,
7867            title,
7868            font_id,
7869            font_size,
7870            inactive,
7871            dim_disabled_title,
7872        );
7873    }
7874
7875    fn draw_standard_control_title_text(
7876        &self,
7877        bus: &mut MacMemoryBus,
7878        label_top: i16,
7879        label_left: i16,
7880        label_bottom: i16,
7881        label_right: i16,
7882        text_x: i16,
7883        text_y: i16,
7884        title: &str,
7885        font_id: i16,
7886        font_size: i16,
7887        inactive: bool,
7888        dim_disabled_title: bool,
7889    ) {
7890        let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
7891            self.get_screen_params();
7892        if inactive && pixel_size == 8 {
7893            // Standard DITL controls are backed by ControlRecords. When a
7894            // game calls HiliteControl(255), their titles follow inactive
7895            // Control Manager rendering; itemDisable alone stays visually
7896            // active in classic dialogs.
7897            let [r, g, b] = Self::INACTIVE_STANDARD_CONTROL_TITLE_RGB;
7898            let gray_index = super::pict::closest_clut_index(r, g, b, &self.device_clut);
7899            Self::fb_draw_string_styled_index(
7900                bus,
7901                screen_base,
7902                row_bytes,
7903                pixel_size,
7904                screen_width,
7905                screen_height,
7906                text_x,
7907                text_y,
7908                title,
7909                font_id,
7910                font_size,
7911                0,
7912                gray_index,
7913            );
7914            return;
7915        }
7916
7917        Self::fb_draw_string(
7918            bus,
7919            screen_base,
7920            row_bytes,
7921            pixel_size,
7922            screen_width,
7923            screen_height,
7924            text_x,
7925            text_y,
7926            title,
7927            font_id,
7928            font_size,
7929        );
7930        if inactive || dim_disabled_title {
7931            self.dim_rect(bus, label_top, label_left, label_bottom, label_right);
7932        }
7933    }
7934
7935    fn draw_control_mask_12(
7936        &self,
7937        bus: &mut MacMemoryBus,
7938        screen_base: u32,
7939        row_bytes: u32,
7940        pixel_size: u16,
7941        screen_width: i16,
7942        screen_height: i16,
7943        left: i16,
7944        top: i16,
7945        mask: &[&str; 12],
7946    ) {
7947        for (dy, row) in mask.iter().enumerate() {
7948            for (dx, pixel) in row.as_bytes().iter().enumerate() {
7949                if *pixel == b'#' {
7950                    Self::fb_set_pixel(
7951                        bus,
7952                        screen_base,
7953                        row_bytes,
7954                        pixel_size,
7955                        screen_width,
7956                        screen_height,
7957                        left + dx as i16,
7958                        top + dy as i16,
7959                        true,
7960                    );
7961                }
7962            }
7963        }
7964    }
7965
7966    /// Replace `^0`..`^3` in dialog/alert text with the strings most
7967    /// recently passed to `ParamText`. Returns a borrowed reference to
7968    /// the original `text` when no placeholders are present (the common
7969    /// case — most DITL items don't use ParamText), avoiding an
7970    /// allocation per draw_static_text call.
7971    /// Inside Macintosh Volume I, I-422.
7972    pub(crate) fn apply_param_text<'a>(&self, text: &'a str) -> std::borrow::Cow<'a, str> {
7973        if !text.contains('^') {
7974            return std::borrow::Cow::Borrowed(text);
7975        }
7976        let mut out = String::with_capacity(text.len());
7977        let mut chars = text.chars().peekable();
7978        while let Some(ch) = chars.next() {
7979            if ch == '^' {
7980                if let Some(&next) = chars.peek() {
7981                    if let Some(idx) = next.to_digit(10) {
7982                        if (idx as usize) < self.param_text.len() {
7983                            chars.next();
7984                            out.push_str(&decode_mac_roman_for_render(
7985                                &self.param_text[idx as usize],
7986                            ));
7987                            continue;
7988                        }
7989                    }
7990                }
7991            }
7992            out.push(ch);
7993        }
7994        std::borrow::Cow::Owned(out)
7995    }
7996
7997    fn draw_static_text(
7998        &self,
7999        bus: &mut MacMemoryBus,
8000        top: i16,
8001        left: i16,
8002        bottom: i16,
8003        right: i16,
8004        text: &str,
8005    ) {
8006        let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
8007            self.get_screen_params();
8008        // SetDialogFont/SetDAFont affects statText and editText items but
8009        // not control titles (IM:I I-412; MTE 1992 p. 6-105).
8010        let font_id = self.tx_font;
8011        let font_size = Self::font_lookup_size(self.tx_size);
8012        let metrics = get_font_metrics(font_id, font_size);
8013        let line_height = metrics.ascent + metrics.descent + metrics.leading;
8014        let max_width = (right - left).saturating_sub(Self::TE_LINE_LEFT_INSET);
8015        let mut y = top + metrics.ascent;
8016        if y >= bottom && bottom > top {
8017            y = bottom - 1;
8018        }
8019
8020        let substituted = self.apply_param_text(text);
8021        let text_bytes = substituted.as_bytes();
8022        let lines = self.te_wrap_lines(font_id, self.tx_size, text_bytes, max_width);
8023
8024        for (start, end) in lines {
8025            let mut trimmed_end = end;
8026            while trimmed_end > start && matches!(text_bytes[trimmed_end - 1], b' ' | b'\r' | b'\n')
8027            {
8028                trimmed_end -= 1;
8029            }
8030            if trimmed_end > start && y <= bottom {
8031                // IM:I I-405 to I-406: statText draws like editText text
8032                // inside the display rectangle, including wrap and clipping,
8033                // but without the editText frame.
8034                let line: String = text_bytes[start..trimmed_end]
8035                    .iter()
8036                    .map(|&byte| byte as char)
8037                    .collect();
8038                Self::fb_draw_string(
8039                    bus,
8040                    screen_base,
8041                    row_bytes,
8042                    pixel_size,
8043                    screen_width,
8044                    screen_height,
8045                    left + Self::TE_LINE_LEFT_INSET,
8046                    y,
8047                    &line,
8048                    font_id,
8049                    font_size,
8050                );
8051            }
8052            y += line_height;
8053            if y > bottom {
8054                break;
8055            }
8056        }
8057    }
8058
8059    /// Draw an editable text field with border and text.
8060    pub(crate) fn draw_edit_text(
8061        &self,
8062        bus: &mut MacMemoryBus,
8063        top: i16,
8064        left: i16,
8065        bottom: i16,
8066        right: i16,
8067        text: &str,
8068        selected: bool,
8069    ) {
8070        let selection_range = selected.then_some((0, text.len()));
8071        self.draw_edit_text_with_cursor(
8072            bus,
8073            top,
8074            left,
8075            bottom,
8076            right,
8077            text,
8078            selection_range,
8079            !selected,
8080            true,
8081        );
8082    }
8083
8084    fn draw_edit_text_with_cursor(
8085        &self,
8086        bus: &mut MacMemoryBus,
8087        top: i16,
8088        left: i16,
8089        bottom: i16,
8090        right: i16,
8091        text: &str,
8092        selection_range: Option<(usize, usize)>,
8093        show_cursor: bool,
8094        enabled: bool,
8095    ) {
8096        let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
8097            self.get_screen_params();
8098        // IM:I I-414 and HIG 1992 p. 184: the active editText item is a
8099        // text-entry field with visible focus feedback. The theme provider owns
8100        // that field chrome only; TextEdit metrics, text drawing, selection,
8101        // and caret positioning remain classic guest-visible behavior.
8102        let frame_top = top - Self::EDIT_TEXT_FRAME_OUTSET;
8103        let frame_left = left - Self::EDIT_TEXT_FRAME_OUTSET;
8104        let frame_bottom = bottom + Self::EDIT_TEXT_FRAME_OUTSET;
8105        let frame_right = right + Self::EDIT_TEXT_FRAME_OUTSET;
8106
8107        if !self.draw_theme_text_field(
8108            bus,
8109            frame_top,
8110            frame_left,
8111            frame_bottom,
8112            frame_right,
8113            enabled,
8114            selection_range.is_some() || show_cursor,
8115        ) {
8116            // White fill
8117            Self::fb_fill_rect(
8118                bus,
8119                screen_base,
8120                row_bytes,
8121                pixel_size,
8122                screen_width,
8123                screen_height,
8124                frame_top,
8125                frame_left,
8126                frame_bottom,
8127                frame_right,
8128                false,
8129            );
8130            // IM:I I-405: editText's display rectangle becomes the TextEdit
8131            // view/dest rectangle, and Dialog Manager frames it three pixels
8132            // outside that display rectangle.
8133            self.draw_rect_border(bus, frame_top, frame_left, frame_bottom, frame_right);
8134        }
8135        let font_id = self.tx_font;
8136        let font_size = Self::font_lookup_size(self.tx_size);
8137        let metrics = get_font_metrics(font_id, font_size);
8138        let text_y = top + metrics.ascent;
8139        Self::fb_draw_string(
8140            bus,
8141            screen_base,
8142            row_bytes,
8143            pixel_size,
8144            screen_width,
8145            screen_height,
8146            left + 1,
8147            text_y,
8148            text,
8149            font_id,
8150            font_size,
8151        );
8152        if let Some((selection_start, selection_end)) = selection_range {
8153            // IM:I I-422 and MTE 1992 p. 6-131: SelIText /
8154            // SelectDialogItemText displays the selected range by inverting
8155            // the selected characters. Match TextEdit's left-justified
8156            // selection rectangle: glyphs draw at destRect.left+1, but a
8157            // selection beginning at character 0 includes the one-pixel inset.
8158            if !text.is_empty() {
8159                let text_bytes = text.as_bytes();
8160                let start = selection_start.min(text_bytes.len());
8161                let end = selection_end.min(text_bytes.len());
8162                if start < end {
8163                    let line_height = metrics.ascent + metrics.descent + metrics.leading;
8164                    let selection_top = top;
8165                    let selection_bottom = top.saturating_add(line_height).min(bottom);
8166                    let selection_left = if start == 0 {
8167                        left
8168                    } else {
8169                        left + Self::TE_LINE_LEFT_INSET
8170                            + self.te_measure_text_width(
8171                                font_id,
8172                                self.tx_size,
8173                                text_bytes,
8174                                0,
8175                                start,
8176                            )
8177                    };
8178                    let selection_right = if end >= text_bytes.len() {
8179                        right
8180                    } else {
8181                        left + Self::TE_LINE_LEFT_INSET
8182                            + self.te_measure_text_width(font_id, self.tx_size, text_bytes, 0, end)
8183                    };
8184                    if selection_left < selection_right && selection_top < selection_bottom {
8185                        if self.ui_theme_id() == UiThemeId::ClassicSystem7 {
8186                            self.invert_rect_exact(
8187                                bus,
8188                                selection_top,
8189                                selection_left,
8190                                selection_bottom,
8191                                selection_right,
8192                            );
8193                        } else {
8194                            self.draw_theme_text_selection(
8195                                bus,
8196                                selection_top,
8197                                selection_left,
8198                                selection_bottom,
8199                                selection_right,
8200                                true,
8201                            );
8202                        }
8203                    }
8204                }
8205            }
8206        } else if show_cursor {
8207            // Draw cursor bar at end of text
8208            let text_width = Self::fb_measure_string(text, font_id, font_size);
8209            let cursor_x = left + 3 + text_width;
8210            if cursor_x < right - 1
8211                && !self.draw_theme_caret(bus, top + 2, cursor_x, bottom - 1, cursor_x + 1)
8212            {
8213                for y in (top + 2)..=(bottom - 2) {
8214                    Self::fb_set_pixel(
8215                        bus,
8216                        screen_base,
8217                        row_bytes,
8218                        pixel_size,
8219                        screen_width,
8220                        screen_height,
8221                        cursor_x,
8222                        y,
8223                        true,
8224                    );
8225                }
8226            }
8227        }
8228    }
8229
8230    fn dialog_cicn_layout(bus: &MacMemoryBus, icon_ptr: u32) -> Option<DialogCIconLayout> {
8231        if bus.get_alloc_size(icon_ptr).is_some_and(|size| size < 82) {
8232            return None;
8233        }
8234
8235        // Imaging With QuickDraw 1994 p. 4-106: a compiled 'cicn'
8236        // resource starts with a 50-byte PixMap, 14-byte mask BitMap,
8237        // 14-byte 1-bit fallback BitMap, 4-byte iconData handle, then
8238        // mask bits, fallback bitmap bits, ColorTable, and PixMap data.
8239        let pm_row_bytes = (bus.read_word(icon_ptr + 4) & 0x3FFF) as u32;
8240        let pm_top = bus.read_word(icon_ptr + 6) as i16;
8241        let pm_left = bus.read_word(icon_ptr + 8) as i16;
8242        let pm_bottom = bus.read_word(icon_ptr + 10) as i16;
8243        let pm_right = bus.read_word(icon_ptr + 12) as i16;
8244        let pixel_size = bus.read_word(icon_ptr + 32);
8245        let mask_row_bytes = (bus.read_word(icon_ptr + 54) & 0x3FFF) as u32;
8246        let bmap_row_bytes = (bus.read_word(icon_ptr + 68) & 0x3FFF) as u32;
8247
8248        let width = pm_right - pm_left;
8249        let height = pm_bottom - pm_top;
8250        if width <= 0 || height <= 0 || pm_row_bytes == 0 || mask_row_bytes == 0 {
8251            return None;
8252        }
8253
8254        let height_u32 = height as u32;
8255        let mask_data_size = mask_row_bytes.checked_mul(height_u32)?;
8256        let bmap_data_size = bmap_row_bytes.checked_mul(height_u32)?;
8257        let mask_data_ptr = icon_ptr + 82;
8258        let bmap_data_ptr = mask_data_ptr.checked_add(mask_data_size)?;
8259        let ctab_ptr = bmap_data_ptr.checked_add(bmap_data_size)?;
8260
8261        if let Some(resource_size) = bus.get_alloc_size(icon_ptr) {
8262            let resource_end = icon_ptr.checked_add(resource_size)?;
8263            if ctab_ptr.checked_add(8)? > resource_end {
8264                return None;
8265            }
8266        }
8267
8268        let ct_size = bus.read_word(ctab_ptr + 6) as u32;
8269        let ctab_total_bytes = 8u32.checked_add((ct_size + 1).checked_mul(8)?)?;
8270        let pixel_data_ptr = ctab_ptr.checked_add(ctab_total_bytes)?;
8271
8272        if let Some(resource_size) = bus.get_alloc_size(icon_ptr) {
8273            let resource_end = icon_ptr.checked_add(resource_size)?;
8274            let pixel_data_size = pm_row_bytes.checked_mul(height_u32)?;
8275            if pixel_data_ptr.checked_add(pixel_data_size)? > resource_end {
8276                return None;
8277            }
8278        }
8279
8280        Some(DialogCIconLayout {
8281            width,
8282            height,
8283            pm_row_bytes,
8284            mask_row_bytes,
8285            bmap_row_bytes,
8286            pixel_size,
8287            mask_data_ptr,
8288            bmap_data_ptr,
8289            pixel_data_ptr,
8290        })
8291    }
8292
8293    fn dialog_cicn_pixel_index(
8294        bus: &MacMemoryBus,
8295        data_ptr: u32,
8296        row_bytes: u32,
8297        pixel_size: u16,
8298        x: u32,
8299        y: u32,
8300    ) -> Option<u8> {
8301        let row_ptr = data_ptr.checked_add(y.checked_mul(row_bytes)?)?;
8302        match pixel_size {
8303            1 => {
8304                let byte = bus.read_byte(row_ptr.checked_add(x / 8)?);
8305                Some(((byte >> (7 - (x % 8))) & 1) as u8)
8306            }
8307            2 => {
8308                let byte = bus.read_byte(row_ptr.checked_add(x / 4)?);
8309                Some((byte >> (6 - 2 * (x % 4))) & 0x03)
8310            }
8311            4 => {
8312                let byte = bus.read_byte(row_ptr.checked_add(x / 2)?);
8313                Some(if x % 2 == 0 {
8314                    (byte >> 4) & 0x0F
8315                } else {
8316                    byte & 0x0F
8317                })
8318            }
8319            8 => Some(bus.read_byte(row_ptr.checked_add(x)?)),
8320            size if size > 8 && size % 8 == 0 => {
8321                let bytes_per_pixel = u32::from(size / 8);
8322                Some(bus.read_byte(row_ptr.checked_add(x.checked_mul(bytes_per_pixel)?)?))
8323            }
8324            _ => None,
8325        }
8326    }
8327
8328    fn draw_cicn_icon(
8329        &self,
8330        bus: &mut MacMemoryBus,
8331        top: i16,
8332        left: i16,
8333        bottom: i16,
8334        right: i16,
8335        icon_ptr: u32,
8336    ) -> bool {
8337        let Some(layout) = Self::dialog_cicn_layout(bus, icon_ptr) else {
8338            return false;
8339        };
8340        let (screen_base, row_bytes, screen_width, screen_height, screen_pixel_size) =
8341            self.get_screen_params();
8342        let dst_w = right - left;
8343        let dst_h = bottom - top;
8344        if dst_w <= 0 || dst_h <= 0 {
8345            return false;
8346        }
8347
8348        for dy in 0..dst_h {
8349            let sy = (dy as i32 * i32::from(layout.height) / i32::from(dst_h)) as u32;
8350            let dst_y = top + dy;
8351            for dx in 0..dst_w {
8352                let sx = (dx as i32 * i32::from(layout.width) / i32::from(dst_w)) as u32;
8353                let dst_x = left + dx;
8354                let mask_byte =
8355                    bus.read_byte(layout.mask_data_ptr + sy * layout.mask_row_bytes + sx / 8);
8356                if (mask_byte & (0x80 >> (sx % 8))) == 0 {
8357                    continue;
8358                }
8359
8360                if screen_pixel_size == 8 && layout.pixel_size >= 2 {
8361                    let Some(pixel_index) = Self::dialog_cicn_pixel_index(
8362                        bus,
8363                        layout.pixel_data_ptr,
8364                        layout.pm_row_bytes,
8365                        layout.pixel_size,
8366                        sx,
8367                        sy,
8368                    ) else {
8369                        continue;
8370                    };
8371                    if dst_x >= 0 && dst_y >= 0 && dst_x < screen_width && dst_y < screen_height {
8372                        bus.write_byte(
8373                            screen_base + (dst_y as u32) * row_bytes + dst_x as u32,
8374                            pixel_index,
8375                        );
8376                    }
8377                    continue;
8378                }
8379
8380                let source_data_ptr = if layout.bmap_row_bytes != 0 {
8381                    layout.bmap_data_ptr
8382                } else {
8383                    layout.pixel_data_ptr
8384                };
8385                let source_row_bytes = if layout.bmap_row_bytes != 0 {
8386                    layout.bmap_row_bytes
8387                } else {
8388                    layout.pm_row_bytes
8389                };
8390                let source_pixel_size = if layout.bmap_row_bytes != 0 {
8391                    1
8392                } else {
8393                    layout.pixel_size
8394                };
8395                let Some(pixel_index) = Self::dialog_cicn_pixel_index(
8396                    bus,
8397                    source_data_ptr,
8398                    source_row_bytes,
8399                    source_pixel_size,
8400                    sx,
8401                    sy,
8402                ) else {
8403                    continue;
8404                };
8405                Self::fb_set_pixel(
8406                    bus,
8407                    screen_base,
8408                    row_bytes,
8409                    screen_pixel_size,
8410                    screen_width,
8411                    screen_height,
8412                    dst_x,
8413                    dst_y,
8414                    pixel_index != 0,
8415                );
8416            }
8417        }
8418        true
8419    }
8420
8421    /// Draw a 32x32 1-bit ICON resource, scaled to fit the display rect.
8422    /// Inside Macintosh Volume I, I-205
8423    fn draw_icon(
8424        &self,
8425        bus: &mut MacMemoryBus,
8426        top: i16,
8427        left: i16,
8428        bottom: i16,
8429        right: i16,
8430        icon_ptr: u32,
8431    ) {
8432        let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
8433            self.get_screen_params();
8434        let dst_w = right - left;
8435        let dst_h = bottom - top;
8436        // ICON is 32x32 pixels, 1 bit per pixel, 4 bytes per row = 128 bytes
8437        for row in 0..32i16 {
8438            let row_data = bus.read_long(icon_ptr + (row as u32) * 4);
8439            for col in 0..32i16 {
8440                let bit = (row_data >> (31 - col)) & 1;
8441                if bit != 0 {
8442                    // Scale to destination rect
8443                    let dx = left + col * dst_w / 32;
8444                    let dy = top + row * dst_h / 32;
8445                    Self::fb_set_pixel(
8446                        bus,
8447                        screen_base,
8448                        row_bytes,
8449                        pixel_size,
8450                        screen_width,
8451                        screen_height,
8452                        dx,
8453                        dy,
8454                        true,
8455                    );
8456                }
8457            }
8458        }
8459    }
8460
8461    /// Invert the pixels within a button rect (for flash animation).
8462    pub(crate) fn invert_button_rect(
8463        &self,
8464        bus: &mut MacMemoryBus,
8465        top: i16,
8466        left: i16,
8467        bottom: i16,
8468        right: i16,
8469    ) {
8470        let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
8471            self.get_screen_params();
8472
8473        let y_start = (top as i32 + 1).clamp(0, screen_height as i32);
8474        let y_end = (bottom as i32 - 1).clamp(0, screen_height as i32);
8475        let x_start = (left as i32 + 1).clamp(0, screen_width as i32);
8476        let x_end = (right as i32 - 1).clamp(0, screen_width as i32);
8477        if y_start >= y_end || x_start >= x_end {
8478            return;
8479        }
8480
8481        for y in y_start..y_end {
8482            for x in x_start..x_end {
8483                if pixel_size == 8 {
8484                    let addr = screen_base + (y as u32) * row_bytes + (x as u32);
8485                    let b = bus.read_byte(addr);
8486                    bus.write_byte(addr, 255 - b);
8487                } else {
8488                    let byte_offset = (y as u32) * row_bytes + (x as u32 / 8);
8489                    let bit = 7 - (x as u32 % 8);
8490                    let addr = screen_base + byte_offset;
8491                    let b = bus.read_byte(addr);
8492                    bus.write_byte(addr, b ^ (1 << bit));
8493                }
8494            }
8495        }
8496    }
8497
8498    fn invert_rect_exact(
8499        &self,
8500        bus: &mut MacMemoryBus,
8501        top: i16,
8502        left: i16,
8503        bottom: i16,
8504        right: i16,
8505    ) {
8506        let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
8507            self.get_screen_params();
8508
8509        let y_start = (top as i32).clamp(0, screen_height as i32);
8510        let y_end = (bottom as i32).clamp(0, screen_height as i32);
8511        let x_start = (left as i32).clamp(0, screen_width as i32);
8512        let x_end = (right as i32).clamp(0, screen_width as i32);
8513        if y_start >= y_end || x_start >= x_end {
8514            return;
8515        }
8516
8517        for y in y_start..y_end {
8518            for x in x_start..x_end {
8519                if pixel_size == 8 {
8520                    let addr = screen_base + (y as u32) * row_bytes + (x as u32);
8521                    let b = bus.read_byte(addr);
8522                    bus.write_byte(addr, 255 - b);
8523                } else {
8524                    let byte_offset = (y as u32) * row_bytes + (x as u32 / 8);
8525                    let bit = 7 - (x as u32 % 8);
8526                    let addr = screen_base + byte_offset;
8527                    let b = bus.read_byte(addr);
8528                    bus.write_byte(addr, b ^ (1 << bit));
8529                }
8530            }
8531        }
8532    }
8533
8534    /// Hit-test a point against dialog items. Returns 1-based item number or 0.
8535    fn dialog_item_hit_test(
8536        &self,
8537        bus: &MacMemoryBus,
8538        items: &[DialogItem],
8539        bounds: (i16, i16, i16, i16),
8540        screen_v: i16,
8541        screen_h: i16,
8542        popup_original_rects: &std::collections::HashMap<(u32, i16), (i16, i16, i16, i16)>,
8543        dialog_ptr: u32,
8544    ) -> i16 {
8545        let (top, left, _, _) = bounds;
8546        let local_v = screen_v - top;
8547        let local_h = screen_h - left;
8548        for (i, item) in items.iter().enumerate() {
8549            let item_no = (i + 1) as i16;
8550            // For popup userItems, use the original DITL rect (before SetDItem
8551            // narrowed it) so clicks on the full popup area register.
8552            let mut rect = popup_original_rects
8553                .get(&(dialog_ptr, item_no))
8554                .copied()
8555                .unwrap_or(item.rect);
8556            let base_type = item.item_type & 0x7F;
8557            if (4..=7).contains(&base_type) {
8558                if let Some(ctrl_handle) = self.dialog_control_handle_for_item(dialog_ptr, item_no)
8559                {
8560                    let ctrl_ptr = bus.read_long(ctrl_handle);
8561                    if ctrl_ptr != 0 && bus.read_byte(ctrl_ptr + 16) != 0 {
8562                        rect = (
8563                            bus.read_word(ctrl_ptr + 8) as i16,
8564                            bus.read_word(ctrl_ptr + 10) as i16,
8565                            bus.read_word(ctrl_ptr + 12) as i16,
8566                            bus.read_word(ctrl_ptr + 14) as i16,
8567                        );
8568                    }
8569                }
8570            }
8571            let (it, il, ib, ir) = rect;
8572            if local_v >= it && local_v < ib && local_h >= il && local_h < ir {
8573                return item_no;
8574            }
8575        }
8576        0
8577    }
8578
8579    fn dialog_button_hit_test(
8580        items: &[DialogItem],
8581        bounds: (i16, i16, i16, i16),
8582        screen_v: i16,
8583        screen_h: i16,
8584    ) -> i16 {
8585        for (i, item) in items.iter().enumerate() {
8586            let base_type = item.item_type & 0x7F;
8587            let is_disabled = (item.item_type & 0x80) != 0;
8588            if base_type != 4 || is_disabled {
8589                continue;
8590            }
8591            let (top, left, bottom, right) = Self::dialog_item_screen_rect(bounds, item.rect);
8592            if screen_v >= top && screen_v < bottom && screen_h >= left && screen_h < right {
8593                return (i + 1) as i16;
8594            }
8595        }
8596        0
8597    }
8598
8599    fn is_plain_modal_user_item(
8600        &self,
8601        tracking: &DialogTrackingState,
8602        item_no: i16,
8603        item: &DialogItem,
8604    ) -> bool {
8605        let base_type = item.item_type & 0x7F;
8606        let is_disabled = (item.item_type & 0x80) != 0;
8607        base_type == 0
8608            && !is_disabled
8609            && item.proc_ptr == 0
8610            && !tracking.game_managed
8611            && !self
8612                .dialog_item_popup_menus
8613                .contains_key(&(tracking.dialog_ptr, item_no))
8614            && !self
8615                .dialog_popup_candidate_items
8616                .contains(&(tracking.dialog_ptr, item_no))
8617    }
8618
8619    fn dialog_plain_user_item_hit_test(
8620        &self,
8621        tracking: &DialogTrackingState,
8622        screen_v: i16,
8623        screen_h: i16,
8624    ) -> i16 {
8625        for (i, item) in tracking.items.iter().enumerate() {
8626            let item_no = (i + 1) as i16;
8627            if !self.is_plain_modal_user_item(tracking, item_no, item) {
8628                continue;
8629            }
8630            let (top, left, bottom, right) =
8631                Self::dialog_item_screen_rect(tracking.bounds, item.rect);
8632            if screen_v >= top && screen_v < bottom && screen_h >= left && screen_h < right {
8633                return item_no;
8634            }
8635        }
8636        0
8637    }
8638
8639    pub(crate) fn pending_dialog_plain_user_item_mouse_down(&self) -> bool {
8640        let Some(tracking) = self.dialog_tracking.as_ref() else {
8641            return false;
8642        };
8643        if tracking.active_user_item.is_some() {
8644            return true;
8645        }
8646        let Some(event) = self
8647            .event_queue
8648            .iter()
8649            .find(|event| matches!(event.what, 1 | 2 | 3 | 4 | 6))
8650        else {
8651            return false;
8652        };
8653        event.what == 1
8654            && self.dialog_plain_user_item_hit_test(tracking, event.where_v, event.where_h) > 0
8655    }
8656
8657    pub(crate) fn mouse_down_over_dialog_button(&self) -> bool {
8658        if !self.mouse_button {
8659            return false;
8660        }
8661        let Some(tracking) = self.dialog_tracking.as_ref() else {
8662            return false;
8663        };
8664        Self::dialog_button_hit_test(
8665            &tracking.items,
8666            tracking.bounds,
8667            self.mouse_pos.0,
8668            self.mouse_pos.1,
8669        ) > 0
8670    }
8671
8672    pub(crate) fn mouse_down_over_dialog_plain_user_item(&self) -> bool {
8673        if !self.mouse_button {
8674            return false;
8675        }
8676        let Some(tracking) = self.dialog_tracking.as_ref() else {
8677            return false;
8678        };
8679        tracking.active_user_item.is_some()
8680            || self.dialog_plain_user_item_hit_test(tracking, self.mouse_pos.0, self.mouse_pos.1)
8681                > 0
8682    }
8683
8684    fn read_guest_event_record(bus: &MacMemoryBus, event_ptr: u32) -> (u16, u32, i16, i16, u16) {
8685        if event_ptr == 0 {
8686            return (0, 0, 0, 0, 0);
8687        }
8688        (
8689            bus.read_word(event_ptr),
8690            bus.read_long(event_ptr + 2),
8691            bus.read_word(event_ptr + 10) as i16,
8692            bus.read_word(event_ptr + 12) as i16,
8693            bus.read_word(event_ptr + 14),
8694        )
8695    }
8696
8697    fn front_dialog_ptr(&self) -> Option<u32> {
8698        let dialog_ptr = self.front_window;
8699        if dialog_ptr != 0 && self.dialog_items.contains_key(&dialog_ptr) {
8700            Some(dialog_ptr)
8701        } else {
8702            None
8703        }
8704    }
8705
8706    fn front_retained_modal_dialog(
8707        &self,
8708        bus: &MacMemoryBus,
8709    ) -> Option<(u32, (i16, i16, i16, i16))> {
8710        if self.dialog_tracking.is_some() {
8711            return None;
8712        }
8713        let dialog_ptr = self.front_dialog_ptr()?;
8714        // This retained-click path is only for dialogs whose event handling
8715        // has already entered our ModalDialog HLE and returned with the
8716        // dialog still visible. Dialogs created by GetNewDialog/NewDialog
8717        // but handled by the application through WaitNextEvent, DialogSelect,
8718        // or custom Control/TextEdit/Window Manager code must keep receiving
8719        // their queued events so the app can decide how to respond.
8720        // Macintosh Toolbox Essentials 1992, pp. 6-136, 6-138..6-141.
8721        if !self.dialog_modal_entered.contains(&dialog_ptr) {
8722            return None;
8723        }
8724        if !self.window_visible(bus, dialog_ptr) {
8725            return None;
8726        }
8727        let proc_id = self.dialog_window_proc_id(bus, dialog_ptr);
8728        // Dialog-box WDEFs 1/2/3 are modal-box variants; 5 is the
8729        // movable modal dialog. noGrowDocProc (4) is modeless and must
8730        // continue to pass events to the application.
8731        // Inside Macintosh Volume I, I-273; Macintosh Toolbox Essentials
8732        // 1992, p. 6-137.
8733        if !matches!(proc_id, 1 | 2 | 3 | 5) {
8734            return None;
8735        }
8736        Some((dialog_ptr, Self::dialog_screen_bounds(bus, dialog_ptr)))
8737    }
8738
8739    fn front_app_owned_modal_dialog(
8740        &self,
8741        bus: &MacMemoryBus,
8742    ) -> Option<(u32, (i16, i16, i16, i16))> {
8743        if self.dialog_tracking.is_some() || self.retained_modal_dialog_click.is_some() {
8744            return None;
8745        }
8746        let dialog_ptr = self.front_dialog_ptr()?;
8747        if self.dialog_modal_entered.contains(&dialog_ptr) || !self.window_visible(bus, dialog_ptr)
8748        {
8749            return None;
8750        }
8751        let proc_id = self.dialog_window_proc_id(bus, dialog_ptr);
8752        if !matches!(proc_id, 1 | 2 | 3 | 5) {
8753            return None;
8754        }
8755        Some((dialog_ptr, Self::dialog_screen_bounds(bus, dialog_ptr)))
8756    }
8757
8758    pub(crate) fn begin_app_owned_modal_dialog_button_tracking(
8759        &mut self,
8760        bus: &mut MacMemoryBus,
8761        event: &QueuedEvent,
8762    ) {
8763        if event.what != 1 {
8764            return;
8765        }
8766        let Some((dialog_ptr, bounds)) = self.front_app_owned_modal_dialog(bus) else {
8767            return;
8768        };
8769        if !Self::dialog_contains_screen_point(bounds, event.where_v, event.where_h) {
8770            return;
8771        }
8772
8773        let Some(mut items) = self.dialog_items.get(&dialog_ptr).cloned() else {
8774            return;
8775        };
8776        Self::refresh_ditl_proc_ptrs(bus, dialog_ptr, &mut items);
8777        let mut hit = self.dialog_item_hit_test(
8778            bus,
8779            &items,
8780            bounds,
8781            event.where_v,
8782            event.where_h,
8783            &self.dialog_popup_original_rects,
8784            dialog_ptr,
8785        );
8786        if hit <= 0 {
8787            hit = Self::dialog_button_hit_test(&items, bounds, event.where_v, event.where_h);
8788        }
8789        self.dialog_items.insert(dialog_ptr, items.clone());
8790        if hit <= 0 {
8791            return;
8792        }
8793
8794        let item = &items[(hit - 1) as usize];
8795        let base_type = item.item_type & 0x7F;
8796        let is_disabled = (item.item_type & 0x80) != 0;
8797        if base_type != 4 || is_disabled {
8798            return;
8799        }
8800
8801        // Some games create a modal DLOG and run their own WaitNextEvent
8802        // loop. They still expect the Dialog Manager's standard push-button
8803        // press/release affordance for modal WDEFs, but the mouseDown must
8804        // remain deliverable to app code. Track only enabled DITL buttons and
8805        // finish the press on mouseUp; DialogSelect cancels this state if the
8806        // app explicitly asks the Dialog Manager to handle the event.
8807        // Macintosh Toolbox Essentials 1992, pp. 6-136, 6-138..6-141.
8808        let rect = Self::dialog_item_screen_rect(bounds, item.rect);
8809        self.invert_button_rect(bus, rect.0, rect.1, rect.2, rect.3);
8810        self.retained_modal_dialog_click = Some(RetainedModalDialogClickState {
8811            dialog_ptr,
8812            item_no: hit,
8813            rect: item.rect,
8814            title: item.text.clone(),
8815            is_default: false,
8816            highlighted: true,
8817            delivered_to_app: true,
8818        });
8819    }
8820
8821    fn cancel_app_owned_modal_dialog_button_tracking(
8822        &mut self,
8823        bus: &mut MacMemoryBus,
8824        dialog_ptr: u32,
8825    ) {
8826        let should_cancel = self
8827            .retained_modal_dialog_click
8828            .as_ref()
8829            .is_some_and(|click| click.dialog_ptr == dialog_ptr && click.delivered_to_app);
8830        if !should_cancel {
8831            return;
8832        }
8833        let Some(click) = self.retained_modal_dialog_click.take() else {
8834            return;
8835        };
8836        if click.item_no > 0 && click.highlighted {
8837            let bounds = Self::dialog_screen_bounds(bus, click.dialog_ptr);
8838            let rect = Self::dialog_item_screen_rect(bounds, click.rect);
8839            self.invert_button_rect(bus, rect.0, rect.1, rect.2, rect.3);
8840        }
8841    }
8842
8843    fn dialog_from_window_event(&self, what: u16, message: u32) -> Option<u32> {
8844        match what {
8845            6 | 8 if self.dialog_items.contains_key(&message) => Some(message),
8846            _ => self.front_dialog_ptr(),
8847        }
8848    }
8849
8850    fn dialog_contains_screen_point(bounds: (i16, i16, i16, i16), v: i16, h: i16) -> bool {
8851        v >= bounds.0 && v < bounds.2 && h >= bounds.1 && h < bounds.3
8852    }
8853
8854    fn point_in_screen_rect(v: i16, h: i16, rect: (i16, i16, i16, i16)) -> bool {
8855        v >= rect.0 && v < rect.2 && h >= rect.1 && h < rect.3
8856    }
8857
8858    fn close_dialog_window<C: CpuOps>(
8859        &mut self,
8860        bus: &mut MacMemoryBus,
8861        cpu: &mut C,
8862        dialog_ptr: u32,
8863        dispose_storage: bool,
8864    ) {
8865        let was_front = self.front_window == dialog_ptr;
8866        let retained_visible_bounds = self
8867            .dialog_visible_snapshots
8868            .get(&dialog_ptr)
8869            .map(|snapshot| snapshot.bounds);
8870        let dialog_record_bounds = if dialog_ptr != 0 && self.dialog_items.contains_key(&dialog_ptr)
8871        {
8872            Self::dialog_screen_bounds(bus, dialog_ptr)
8873        } else {
8874            self.window_bounds
8875        };
8876        let exposed_rect = if was_front {
8877            self.window_bounds
8878        } else {
8879            retained_visible_bounds.unwrap_or(dialog_record_bounds)
8880        };
8881        let initial_draw_deferred = self.dialog_initial_draw_deferred.remove(&dialog_ptr);
8882        let retained_stale_saved_under = self
8883            .dialog_saved_pixels
8884            .get(&dialog_ptr)
8885            .is_some_and(|saved| self.saved_pixels_are_mostly_logical_white(saved));
8886        let mut restored_stale_saved_under = false;
8887
8888        if was_front && !initial_draw_deferred {
8889            if let Some(saved) = self.dialog_saved_pixels.remove(&dialog_ptr) {
8890                restored_stale_saved_under = self.saved_pixels_are_mostly_logical_white(&saved);
8891                self.restore_dialog_pixels(bus, self.window_bounds, &saved);
8892            }
8893        } else {
8894            self.dialog_saved_pixels.remove(&dialog_ptr);
8895        }
8896
8897        self.dialog_visible_snapshots.remove(&dialog_ptr);
8898        self.dialog_modal_entered.remove(&dialog_ptr);
8899        if !was_front && retained_stale_saved_under && self.screen_is_hidden_menu_game_surface(bus)
8900        {
8901            let _ = self.restore_dialog_exposure_from_fullscreen_offscreen_port(bus, exposed_rect);
8902        }
8903        if self.pending_modal_button_dispose_dialog == Some(dialog_ptr) {
8904            self.pending_modal_button_dispose_dialog = None;
8905        }
8906        if self
8907            .retained_modal_dialog_click
8908            .as_ref()
8909            .is_some_and(|click| click.dialog_ptr == dialog_ptr)
8910        {
8911            self.retained_modal_dialog_click = None;
8912        }
8913
8914        self.dispose_dialog_owned_items(bus, dialog_ptr);
8915        if dispose_storage {
8916            self.dispose_dialog_record_and_item_list(bus, dialog_ptr);
8917        }
8918
8919        self.untrack_window(bus, dialog_ptr);
8920
8921        if was_front {
8922            if let Some((prev_window, prev_bounds, prev_proc_id, prev_title)) =
8923                self.window_stack.pop()
8924            {
8925                self.set_current_port_state(bus, cpu, prev_window, None);
8926                self.front_window = prev_window;
8927                self.window_bounds = prev_bounds;
8928                self.window_proc_id = prev_proc_id;
8929                self.window_title = prev_title;
8930                if prev_window != 0 {
8931                    bus.write_byte(prev_window + 111, 0xFF);
8932                    if self.window_visible(bus, prev_window) {
8933                        self.blit_window_to_screen(bus);
8934                        self.blit_large_manual_cport_to_screen(bus);
8935                    }
8936                    if !self.event_queue.iter().any(|event| {
8937                        event.what == 8
8938                            && event.message == prev_window
8939                            && (event.modifiers & 1) != 0
8940                    }) {
8941                        self.event_queue
8942                            .push_back(crate::trap::dispatch::QueuedEvent {
8943                                what: 8,
8944                                message: prev_window,
8945                                where_v: 0,
8946                                where_h: 0,
8947                                modifiers: 1,
8948                            });
8949                    }
8950                    self.draw_single_window_chrome_inline(bus, prev_window, true);
8951                    let exposed_local = (
8952                        exposed_rect.0.saturating_sub(prev_bounds.0),
8953                        exposed_rect.1.saturating_sub(prev_bounds.1),
8954                        exposed_rect.2.saturating_sub(prev_bounds.0),
8955                        exposed_rect.3.saturating_sub(prev_bounds.1),
8956                    );
8957                    self.invalidate_window_rect(bus, prev_window, exposed_local);
8958                    self.queue_window_update_event(prev_window);
8959                }
8960                let should_repair_stale_exposure = restored_stale_saved_under
8961                    && if prev_window != 0 {
8962                        self.promoted_window_is_fullscreen_game_surface(bus, prev_bounds)
8963                    } else {
8964                        self.screen_is_hidden_menu_game_surface(bus)
8965                    };
8966                if should_repair_stale_exposure {
8967                    let _ = self
8968                        .restore_dialog_exposure_from_fullscreen_offscreen_port(bus, exposed_rect);
8969                }
8970            }
8971        }
8972    }
8973
8974    fn dialog_saved_white_like_count(&self, saved: &[u8]) -> usize {
8975        let (_, _, _, _, pixel_size) = self.get_screen_params();
8976        match pixel_size {
8977            8 => saved
8978                .iter()
8979                .filter(|&&idx| {
8980                    let [r, g, b] = self.device_clut[idx as usize];
8981                    r >= 0xEEEE && g >= 0xEEEE && b >= 0xEEEE
8982                })
8983                .count(),
8984            _ => saved.iter().filter(|&&byte| byte == 0).count(),
8985        }
8986    }
8987
8988    fn saved_pixels_are_mostly_logical_white(&self, saved: &[u8]) -> bool {
8989        let (_, _, _, _, pixel_size) = self.get_screen_params();
8990        pixel_size == 8
8991            && !saved.is_empty()
8992            && self
8993                .dialog_saved_white_like_count(saved)
8994                .saturating_mul(100)
8995                >= saved.len().saturating_mul(80)
8996    }
8997
8998    fn promoted_window_is_fullscreen_game_surface(
8999        &self,
9000        bus: &MacMemoryBus,
9001        bounds: (i16, i16, i16, i16),
9002    ) -> bool {
9003        let (_, _, screen_w, screen_h, _) = self.get_screen_params();
9004        let menu_bar_height = bus.read_word(crate::memory::globals::addr::MBAR_HEIGHT) as i16;
9005        bounds.0 <= 0
9006            && bounds.1 <= 0
9007            && bounds.2 >= screen_h
9008            && bounds.3 >= screen_w
9009            && (self.menu_bar_hidden || self.fullscreen_locked || menu_bar_height == 0)
9010    }
9011
9012    fn screen_is_hidden_menu_game_surface(&self, bus: &MacMemoryBus) -> bool {
9013        let menu_bar_height = bus.read_word(crate::memory::globals::addr::MBAR_HEIGHT) as i16;
9014        self.menu_bar_hidden || self.fullscreen_locked || menu_bar_height == 0
9015    }
9016
9017    fn restore_dialog_exposure_from_fullscreen_offscreen_port(
9018        &self,
9019        bus: &mut MacMemoryBus,
9020        bounds: (i16, i16, i16, i16),
9021    ) -> bool {
9022        let (screen_base, screen_row_bytes, screen_w, screen_h, pixel_size) =
9023            self.get_screen_params();
9024        if pixel_size != 8 || screen_w == 0 || screen_h == 0 {
9025            return false;
9026        }
9027        let (top, left, bottom, right) = Self::dialog_saved_pixel_rect(bounds);
9028        let top = top.max(0).min(screen_h);
9029        let left = left.max(0).min(screen_w);
9030        let bottom = bottom.max(0).min(screen_h);
9031        let right = right.max(0).min(screen_w);
9032        if top >= bottom || left >= right {
9033            return false;
9034        }
9035
9036        let mut ports = Vec::new();
9037        for port in self
9038            .cport_ports
9039            .iter()
9040            .copied()
9041            .chain(self.gworld_devices.keys().copied())
9042        {
9043            if !ports.contains(&port) {
9044                ports.push(port);
9045            }
9046        }
9047
9048        let mut best: Option<FullscreenOffscreenDialogRestoreCandidate> = None;
9049        for port in ports {
9050            let Some(mut candidate) =
9051                self.fullscreen_offscreen_dialog_restore_candidate(bus, port, screen_base)
9052            else {
9053                continue;
9054            };
9055            if candidate.top > 0
9056                || candidate.left > 0
9057                || candidate.bottom < screen_h
9058                || candidate.right < screen_w
9059            {
9060                continue;
9061            }
9062            let Some(score) = self.score_offscreen_dialog_restore_candidate(
9063                bus,
9064                candidate,
9065                (top, left, bottom, right),
9066            ) else {
9067                continue;
9068            };
9069            candidate.score = score;
9070            if best.map(|current| score > current.score).unwrap_or(true) {
9071                best = Some(candidate);
9072            }
9073        }
9074
9075        let Some(candidate) = best else {
9076            return false;
9077        };
9078
9079        let screen_ctab_handle = Self::gdevice_ctab_handle(bus, self.main_gdevice_handle);
9080        let src_ctab_seed = Self::ctab_seed(bus, candidate.ctab_handle);
9081        let dst_ctab_seed = Self::ctab_seed(bus, screen_ctab_handle);
9082        let src_clut = self.read_port_clut(bus, candidate.ctab_handle);
9083        let dst_clut = self.read_port_clut(bus, screen_ctab_handle);
9084        let hardware_palette_active = self.device_clut != self.color_manager_clut;
9085        let skip_canonical_to_screen = Self::uses_canonical_system_8bpp_clut(&src_clut);
9086        let translation = if candidate.ctab_handle != screen_ctab_handle
9087            && matches!(src_ctab_seed, Some(src_seed) if src_seed != 0)
9088            && src_ctab_seed != dst_ctab_seed
9089            && !skip_canonical_to_screen
9090            && !hardware_palette_active
9091        {
9092            Some(self.build_palette_translation(bus, &src_clut, &dst_clut, screen_ctab_handle))
9093        } else {
9094            None
9095        };
9096
9097        let width = (right - left) as u32;
9098        for y in top..bottom {
9099            let src_y = (y - candidate.top) as u32;
9100            let src_x = (left - candidate.left) as u32;
9101            let dst_addr = screen_base + (y as u32) * screen_row_bytes + left as u32;
9102            let src_addr = candidate.base + src_y * candidate.row_bytes + src_x;
9103            if let Some(translation) = translation.as_ref() {
9104                for col in 0..width {
9105                    let src_idx = bus.read_byte(src_addr + col);
9106                    bus.write_byte(dst_addr + col, translation[src_idx as usize]);
9107                }
9108            } else {
9109                let row = bus.read_bytes(src_addr, width as usize);
9110                bus.write_bytes(dst_addr, &row);
9111            }
9112        }
9113
9114        true
9115    }
9116
9117    fn fullscreen_offscreen_dialog_restore_candidate(
9118        &self,
9119        bus: &MacMemoryBus,
9120        port: u32,
9121        screen_base: u32,
9122    ) -> Option<FullscreenOffscreenDialogRestoreCandidate> {
9123        if port == 0 || (bus.read_word(port + 6) & 0xC000) != 0xC000 {
9124            return None;
9125        }
9126        let pm_handle = bus.read_long(port + 2);
9127        if pm_handle == 0 {
9128            return None;
9129        }
9130        let pm_ptr = bus.read_long(pm_handle);
9131        if pm_ptr == 0 {
9132            return None;
9133        }
9134        let base = bus.read_long(pm_ptr) & 0x3FFF_FFFF;
9135        let row_bytes = (bus.read_word(pm_ptr + 4) & 0x3FFF) as u32;
9136        let top = bus.read_word(pm_ptr + 6) as i16;
9137        let left = bus.read_word(pm_ptr + 8) as i16;
9138        let bottom = bus.read_word(pm_ptr + 10) as i16;
9139        let right = bus.read_word(pm_ptr + 12) as i16;
9140        let pixel_size = bus.read_word(pm_ptr + 32);
9141        if base == 0
9142            || base == screen_base
9143            || pixel_size != 8
9144            || bottom <= top
9145            || right <= left
9146            || row_bytes < (right - left) as u32
9147        {
9148            return None;
9149        }
9150        Some(FullscreenOffscreenDialogRestoreCandidate {
9151            base,
9152            row_bytes,
9153            top,
9154            left,
9155            bottom,
9156            right,
9157            ctab_handle: bus.read_long(pm_ptr + 42),
9158            score: 0,
9159        })
9160    }
9161
9162    fn score_offscreen_dialog_restore_candidate(
9163        &self,
9164        bus: &MacMemoryBus,
9165        candidate: FullscreenOffscreenDialogRestoreCandidate,
9166        rect: (i16, i16, i16, i16),
9167    ) -> Option<u32> {
9168        let (top, left, bottom, right) = rect;
9169        let width = right - left;
9170        let height = bottom - top;
9171        if width <= 0 || height <= 0 {
9172            return None;
9173        }
9174        let step_x = (width as u32 / 32).max(1);
9175        let step_y = (height as u32 / 24).max(1);
9176        let mut total = 0u32;
9177        let mut visible = 0u32;
9178        let mut white_like = 0u32;
9179        let mut black_like = 0u32;
9180        let mut y = top as u32 + step_y / 2;
9181        while y < bottom as u32 {
9182            let mut x = left as u32 + step_x / 2;
9183            while x < right as u32 {
9184                let src_y = y as i16 - candidate.top;
9185                let src_x = x as i16 - candidate.left;
9186                if src_y >= 0 && src_x >= 0 {
9187                    let idx = bus.read_byte(
9188                        candidate.base + src_y as u32 * candidate.row_bytes + src_x as u32,
9189                    ) as usize;
9190                    let [r, g, b] = self.device_clut[idx];
9191                    total += 1;
9192                    if r > 0x1111 || g > 0x1111 || b > 0x1111 {
9193                        visible += 1;
9194                    }
9195                    if r >= 0xEEEE && g >= 0xEEEE && b >= 0xEEEE {
9196                        white_like += 1;
9197                    }
9198                    if r <= 0x1111 && g <= 0x1111 && b <= 0x1111 {
9199                        black_like += 1;
9200                    }
9201                }
9202                x += step_x;
9203            }
9204            y += step_y;
9205        }
9206        if total == 0
9207            || visible < 8
9208            || white_like.saturating_mul(100) >= total.saturating_mul(70)
9209            || black_like.saturating_mul(100) >= total.saturating_mul(98)
9210        {
9211            return None;
9212        }
9213        Some(visible.saturating_mul(2).saturating_sub(white_like))
9214    }
9215
9216    fn resolve_dispos_dialog_ptr_after_modal_button_hit(
9217        &mut self,
9218        requested_dialog_ptr: u32,
9219    ) -> u32 {
9220        let Some(dialog_ptr) = self.pending_modal_button_dispose_dialog.take() else {
9221            return requested_dialog_ptr;
9222        };
9223
9224        if requested_dialog_ptr != 0
9225            && requested_dialog_ptr != dialog_ptr
9226            && !self.dialog_items.contains_key(&requested_dialog_ptr)
9227            && !self.window_list.contains(&requested_dialog_ptr)
9228            && self.front_window == dialog_ptr
9229            && self.dialog_modal_entered.contains(&dialog_ptr)
9230            && self.dialog_items.contains_key(&dialog_ptr)
9231        {
9232            eprintln!(
9233                "[TRAP] DisposDialog(${:08X}) recovered front ModalDialog button target ${:08X}",
9234                requested_dialog_ptr, dialog_ptr
9235            );
9236            dialog_ptr
9237        } else {
9238            requested_dialog_ptr
9239        }
9240    }
9241
9242    pub(crate) fn consume_retained_modal_dialog_event<C: CpuOps>(
9243        &mut self,
9244        cpu: &mut C,
9245        bus: &mut MacMemoryBus,
9246        event: &QueuedEvent,
9247    ) -> bool {
9248        match event.what {
9249            1 => {
9250                let Some((dialog_ptr, bounds)) = self.front_retained_modal_dialog(bus) else {
9251                    return false;
9252                };
9253
9254                if !Self::dialog_contains_screen_point(bounds, event.where_v, event.where_h) {
9255                    // Modal dialogs own the mouse while visible. A real Dialog
9256                    // Manager click outside the box beeps and does not pass
9257                    // through to windows behind it.
9258                    // Inside Macintosh Volume I, I-418; Macintosh Toolbox
9259                    // Essentials 1992, p. 6-136.
9260                    self.retained_modal_dialog_click = Some(RetainedModalDialogClickState {
9261                        dialog_ptr,
9262                        item_no: 0,
9263                        rect: (0, 0, 0, 0),
9264                        title: String::new(),
9265                        is_default: false,
9266                        highlighted: false,
9267                        delivered_to_app: false,
9268                    });
9269                    return true;
9270                }
9271
9272                let Some(mut items) = self.dialog_items.get(&dialog_ptr).cloned() else {
9273                    return false;
9274                };
9275                Self::refresh_ditl_proc_ptrs(bus, dialog_ptr, &mut items);
9276                let mut hit = self.dialog_item_hit_test(
9277                    bus,
9278                    &items,
9279                    bounds,
9280                    event.where_v,
9281                    event.where_h,
9282                    &self.dialog_popup_original_rects,
9283                    dialog_ptr,
9284                );
9285                if hit <= 0 {
9286                    hit =
9287                        Self::dialog_button_hit_test(&items, bounds, event.where_v, event.where_h);
9288                }
9289                self.dialog_items.insert(dialog_ptr, items.clone());
9290
9291                if hit <= 0 {
9292                    self.retained_modal_dialog_click = Some(RetainedModalDialogClickState {
9293                        dialog_ptr,
9294                        item_no: 0,
9295                        rect: (0, 0, 0, 0),
9296                        title: String::new(),
9297                        is_default: false,
9298                        highlighted: false,
9299                        delivered_to_app: false,
9300                    });
9301                    return true;
9302                }
9303
9304                let item = &items[(hit - 1) as usize];
9305                let base_type = item.item_type & 0x7F;
9306                let is_disabled = (item.item_type & 0x80) != 0;
9307                if base_type == 4 && !is_disabled {
9308                    let rect = Self::dialog_item_screen_rect(bounds, item.rect);
9309                    let is_default = hit == bus.read_word(dialog_ptr + 168) as i16;
9310                    self.draw_dialog_button_highlight_state(
9311                        bus, rect, &item.text, is_default, true,
9312                    );
9313                    self.retained_modal_dialog_click = Some(RetainedModalDialogClickState {
9314                        dialog_ptr,
9315                        item_no: hit,
9316                        rect: item.rect,
9317                        title: item.text.clone(),
9318                        is_default,
9319                        highlighted: true,
9320                        delivered_to_app: false,
9321                    });
9322                } else {
9323                    self.retained_modal_dialog_click = Some(RetainedModalDialogClickState {
9324                        dialog_ptr,
9325                        item_no: 0,
9326                        rect: (0, 0, 0, 0),
9327                        title: String::new(),
9328                        is_default: false,
9329                        highlighted: false,
9330                        delivered_to_app: false,
9331                    });
9332                }
9333                true
9334            }
9335            2 => {
9336                let Some(click) = self.retained_modal_dialog_click.take() else {
9337                    return false;
9338                };
9339                if click.item_no > 0 {
9340                    let bounds = Self::dialog_screen_bounds(bus, click.dialog_ptr);
9341                    let rect = Self::dialog_item_screen_rect(bounds, click.rect);
9342                    if click.highlighted {
9343                        self.draw_dialog_button_highlight_state(
9344                            bus,
9345                            rect,
9346                            &click.title,
9347                            click.is_default,
9348                            false,
9349                        );
9350                    }
9351                    if self.front_window == click.dialog_ptr
9352                        && Self::point_in_screen_rect(event.where_v, event.where_h, rect)
9353                    {
9354                        self.close_dialog_window(bus, cpu, click.dialog_ptr, true);
9355                        self.capture_gui_frame(
9356                            bus,
9357                            &format!("retained_modal_dialog_button_{}", click.item_no),
9358                        );
9359                    }
9360                }
9361                !click.delivered_to_app
9362            }
9363            _ => false,
9364        }
9365    }
9366
9367    fn consume_dialog_mouse_up(&mut self) {
9368        if let Some(idx) = self.event_queue.iter().position(|e| e.what == 2) {
9369            self.event_queue.remove(idx);
9370        }
9371    }
9372
9373    fn dialog_item_screen_rect(
9374        bounds: (i16, i16, i16, i16),
9375        rect: (i16, i16, i16, i16),
9376    ) -> (i16, i16, i16, i16) {
9377        let (it, il, ib, ir) = rect;
9378        (bounds.0 + it, bounds.1 + il, bounds.0 + ib, bounds.1 + ir)
9379    }
9380
9381    fn rects_intersect(a: (i16, i16, i16, i16), b: (i16, i16, i16, i16)) -> bool {
9382        a.0 < b.2 && a.2 > b.0 && a.1 < b.3 && a.3 > b.1
9383    }
9384
9385    fn dialog_item_intersects_bounds(bounds: (i16, i16, i16, i16), item: &DialogItem) -> bool {
9386        Self::rects_intersect(Self::dialog_item_screen_rect(bounds, item.rect), bounds)
9387    }
9388
9389    fn dialog_is_game_managed(bounds: (i16, i16, i16, i16), items: &[DialogItem]) -> bool {
9390        let mut has_visible_item = false;
9391        for item in items {
9392            if !Self::dialog_item_intersects_bounds(bounds, item) {
9393                continue;
9394            }
9395            has_visible_item = true;
9396            if (item.item_type & 0x7F) != 0 {
9397                return false;
9398            }
9399        }
9400        has_visible_item
9401    }
9402
9403    fn start_dialog_button_flash(
9404        &mut self,
9405        bus: &mut MacMemoryBus,
9406        bounds: (i16, i16, i16, i16),
9407        item_no: i16,
9408        rect: (i16, i16, i16, i16),
9409        title: &str,
9410        is_default: bool,
9411        highlighted: bool,
9412    ) {
9413        if !highlighted {
9414            let screen_rect = Self::dialog_item_screen_rect(bounds, rect);
9415            self.draw_dialog_button_highlight_state(bus, screen_rect, title, is_default, true);
9416        }
9417        if let Some(t) = self.dialog_tracking.as_mut() {
9418            t.flash_remaining = 6;
9419            t.flash_delay = 3;
9420            t.flash_item = item_no;
9421        }
9422    }
9423
9424    fn dialog_control_handle_for_item(&self, dialog_ptr: u32, item_no: i16) -> Option<u32> {
9425        self.dialog_control_handles
9426            .iter()
9427            .find_map(|(&handle, &(dlg, item))| {
9428                if dlg == dialog_ptr && item == item_no {
9429                    Some(handle)
9430                } else {
9431                    None
9432                }
9433            })
9434    }
9435
9436    fn dialog_control_inactive(&self, bus: &MacMemoryBus, dialog_ptr: u32, item_no: i16) -> bool {
9437        self.dialog_control_handle_for_item(dialog_ptr, item_no)
9438            .map(|handle| bus.read_long(handle))
9439            .is_some_and(|ctrl_ptr| ctrl_ptr != 0 && bus.read_byte(ctrl_ptr + 17) == 255)
9440    }
9441
9442    fn begin_dialog_popup_tracking(
9443        &mut self,
9444        bus: &mut MacMemoryBus,
9445        dialog_ptr: u32,
9446        item_no: i16,
9447    ) -> bool {
9448        let Some(ctrl_handle) = self.dialog_control_handle_for_item(dialog_ptr, item_no) else {
9449            return false;
9450        };
9451        let ctrl_ptr = bus.read_long(ctrl_handle);
9452        if ctrl_ptr == 0 {
9453            return false;
9454        }
9455        let proc_id = self.control_proc_ids.get(&ctrl_ptr).copied().unwrap_or(0);
9456        if !Self::is_popup_menu_proc_id(proc_id) {
9457            return false;
9458        }
9459        let menu_id =
9460            self.popup_control_menu_id(bus, ctrl_ptr, bus.read_word(ctrl_ptr + 20) as i16);
9461        let Some(menu_idx) = self.menus.iter().rposition(|menu| menu.id == menu_id) else {
9462            return false;
9463        };
9464
9465        let dropdown_rect = self.popup_control_dropdown_rect(bus, ctrl_ptr, menu_idx);
9466        let saved_pixels = self.save_dropdown_pixels(bus, dropdown_rect);
9467        self.draw_menu_dropdown(bus, menu_idx, dropdown_rect);
9468
9469        if let Some(tracking) = self.dialog_tracking.as_mut() {
9470            tracking.active_popup = Some(DialogPopupTrackingState {
9471                item_no,
9472                ctrl_handle,
9473                ctrl_ptr,
9474                active_menu: menu_idx,
9475                highlighted_item: 0,
9476                saved_pixels,
9477                dropdown_rect,
9478            });
9479            true
9480        } else {
9481            false
9482        }
9483    }
9484
9485    fn dialog_popup_item_at_point(&self, bus: &MacMemoryBus, mouse_x: i16, mouse_y: i16) -> i16 {
9486        let Some(popup) = self
9487            .dialog_tracking
9488            .as_ref()
9489            .and_then(|tracking| tracking.active_popup.as_ref())
9490        else {
9491            return 0;
9492        };
9493        let (top, left, bottom, right) = popup.dropdown_rect;
9494        if mouse_x < left || mouse_x >= right || mouse_y < top || mouse_y >= bottom {
9495            return 0;
9496        }
9497        let Some(menu) = self.menus.get(popup.active_menu) else {
9498            return 0;
9499        };
9500        let mut item_top = top + 1;
9501        for (item_idx, item) in menu.items.iter().enumerate() {
9502            let item_bottom = item_top + self.menu_item_height(bus, item);
9503            if mouse_y >= item_top && mouse_y < item_bottom {
9504                if item.text == "-" || !item.enabled {
9505                    return 0;
9506                }
9507                return item_idx as i16 + 1;
9508            }
9509            item_top = item_bottom;
9510        }
9511        0
9512    }
9513
9514    fn handle_dialog_popup_tracking<C: CpuOps>(&mut self, cpu: &mut C, bus: &mut MacMemoryBus) {
9515        if self.mouse_button {
9516            let (mv, mh) = self.mouse_pos;
9517            let new_item = self.dialog_popup_item_at_point(bus, mh, mv);
9518            let old_item = self
9519                .dialog_tracking
9520                .as_ref()
9521                .and_then(|tracking| tracking.active_popup.as_ref())
9522                .map(|popup| popup.highlighted_item)
9523                .unwrap_or(0);
9524            if new_item != old_item {
9525                let popup_state = self
9526                    .dialog_tracking
9527                    .as_ref()
9528                    .and_then(|tracking| tracking.active_popup.as_ref())
9529                    .map(|popup| (popup.active_menu, popup.dropdown_rect));
9530                if let Some((active_menu, dropdown_rect)) = popup_state {
9531                    if old_item > 0 {
9532                        self.invert_dropdown_item_rect(bus, active_menu, dropdown_rect, old_item);
9533                    }
9534                    if let Some(popup) = self
9535                        .dialog_tracking
9536                        .as_mut()
9537                        .and_then(|tracking| tracking.active_popup.as_mut())
9538                    {
9539                        popup.highlighted_item = new_item;
9540                    }
9541                    if new_item > 0 {
9542                        self.invert_dropdown_item_rect(bus, active_menu, dropdown_rect, new_item);
9543                    }
9544                }
9545            }
9546            return;
9547        }
9548
9549        let Some(popup) = self
9550            .dialog_tracking
9551            .as_mut()
9552            .and_then(|tracking| tracking.active_popup.take())
9553        else {
9554            return;
9555        };
9556        self.restore_dropdown_pixels(bus, popup.dropdown_rect, &popup.saved_pixels);
9557        self.consume_dialog_mouse_up();
9558
9559        let selected_item = popup.highlighted_item;
9560        if selected_item <= 0 {
9561            return;
9562        }
9563
9564        self.write_control_value(bus, popup.ctrl_handle, selected_item);
9565        self.draw_control(cpu, bus, popup.ctrl_ptr);
9566
9567        if self.dialog_tracking.is_some() {
9568            let (dialog_ptr, edit_item, edit_text, items) = {
9569                let tracking = self.dialog_tracking.as_mut().unwrap();
9570                Self::sync_tracking_active_edit_item(tracking);
9571                (
9572                    tracking.dialog_ptr,
9573                    tracking.edit_item,
9574                    tracking.edit_text.clone(),
9575                    tracking.items.clone(),
9576                )
9577            };
9578            self.flush_dialog_edit_item_texts(bus, dialog_ptr, &items, edit_item, &edit_text);
9579        }
9580        if let Some((bounds, game_managed)) = self
9581            .dialog_tracking
9582            .as_ref()
9583            .map(|tracking| (tracking.bounds, tracking.game_managed))
9584        {
9585            if !game_managed {
9586                let rendered = self.save_dialog_pixels(bus, bounds);
9587                if let Some(tracking) = self.dialog_tracking.as_mut() {
9588                    tracking.rendered_pixels = rendered;
9589                    tracking.rendered_pixels_final = true;
9590                }
9591            }
9592        }
9593
9594        let Some(saved) = self.dialog_tracking.take() else {
9595            return;
9596        };
9597        self.persist_visible_dialog_snapshot(bus, &saved);
9598        self.dialog_saved_pixels
9599            .insert(saved.dialog_ptr, saved.saved_pixels);
9600        if saved.item_hit_ptr != 0 {
9601            bus.write_word(saved.item_hit_ptr, popup.item_no as u16);
9602        }
9603        cpu.write_reg(Register::A7, saved.stack_ptr + 8);
9604    }
9605
9606    fn handle_dialog_button_tracking(&mut self, bus: &mut MacMemoryBus) {
9607        let Some((dialog_ptr, bounds, item_no, rect, title, is_default, highlighted, item_type)) =
9608            self.dialog_tracking.as_ref().and_then(|tracking| {
9609                tracking.active_button.as_ref().map(|button| {
9610                    (
9611                        tracking.dialog_ptr,
9612                        tracking.bounds,
9613                        button.item_no,
9614                        button.rect,
9615                        button.title.clone(),
9616                        button.is_default,
9617                        button.highlighted,
9618                        tracking
9619                            .items
9620                            .get(button.item_no.saturating_sub(1) as usize)
9621                            .map(|item| item.item_type)
9622                            .unwrap_or(4),
9623                    )
9624                })
9625            })
9626        else {
9627            return;
9628        };
9629
9630        let (top, left, bottom, right) = Self::dialog_item_screen_rect(bounds, rect);
9631        let (mouse_v, mouse_h) = self.mouse_pos;
9632        let inside = mouse_v >= top && mouse_v < bottom && mouse_h >= left && mouse_h < right;
9633
9634        if self.mouse_button {
9635            if inside != highlighted {
9636                self.draw_dialog_button_highlight_state(
9637                    bus,
9638                    (top, left, bottom, right),
9639                    &title,
9640                    is_default,
9641                    inside,
9642                );
9643                if let Some(button) = self
9644                    .dialog_tracking
9645                    .as_mut()
9646                    .and_then(|tracking| tracking.active_button.as_mut())
9647                {
9648                    button.highlighted = inside;
9649                }
9650                self.record_modal_dialog_input_trace(
9651                    "tracking_update",
9652                    dialog_ptr,
9653                    bounds,
9654                    item_no,
9655                    Some(item_type),
9656                    Some(inside),
9657                    "pending",
9658                    if inside {
9659                        "button_highlighted"
9660                    } else {
9661                        "button_unhighlighted"
9662                    },
9663                );
9664            }
9665            return;
9666        }
9667
9668        self.consume_dialog_mouse_up();
9669        if let Some(t) = self.dialog_tracking.as_mut() {
9670            t.active_button = None;
9671        }
9672
9673        if inside {
9674            self.record_modal_dialog_input_trace(
9675                "release",
9676                dialog_ptr,
9677                bounds,
9678                item_no,
9679                Some(item_type),
9680                Some(true),
9681                "pending",
9682                "start_flash",
9683            );
9684            self.start_dialog_button_flash(
9685                bus,
9686                bounds,
9687                item_no,
9688                rect,
9689                &title,
9690                is_default,
9691                highlighted,
9692            );
9693        } else if highlighted {
9694            self.draw_dialog_button_highlight_state(
9695                bus,
9696                (top, left, bottom, right),
9697                &title,
9698                is_default,
9699                false,
9700            );
9701            self.record_modal_dialog_input_trace(
9702                "release",
9703                dialog_ptr,
9704                bounds,
9705                item_no,
9706                Some(item_type),
9707                Some(false),
9708                "pending",
9709                "button_no_selection",
9710            );
9711        } else {
9712            self.record_modal_dialog_input_trace(
9713                "release",
9714                dialog_ptr,
9715                bounds,
9716                item_no,
9717                Some(item_type),
9718                Some(false),
9719                "pending",
9720                "button_no_selection",
9721            );
9722        }
9723    }
9724
9725    fn handle_dialog_user_item_tracking<C: CpuOps>(&mut self, cpu: &mut C, bus: &mut MacMemoryBus) {
9726        let Some((bounds, item_no, rect)) = self.dialog_tracking.as_ref().and_then(|tracking| {
9727            tracking
9728                .active_user_item
9729                .as_ref()
9730                .map(|user_item| (tracking.bounds, user_item.item_no, user_item.rect))
9731        }) else {
9732            return;
9733        };
9734
9735        if self.mouse_button {
9736            return;
9737        }
9738
9739        self.consume_dialog_mouse_up();
9740        if let Some(t) = self.dialog_tracking.as_mut() {
9741            t.active_user_item = None;
9742        }
9743
9744        let (top, left, bottom, right) = Self::dialog_item_screen_rect(bounds, rect);
9745        let (mouse_v, mouse_h) = self.mouse_pos;
9746        let inside = mouse_v >= top && mouse_v < bottom && mouse_h >= left && mouse_h < right;
9747        if !inside {
9748            return;
9749        }
9750
9751        let (dlg_ptr, edit_item, edit_text, items) = {
9752            let tracking = self.dialog_tracking.as_mut().unwrap();
9753            Self::sync_tracking_active_edit_item(tracking);
9754            (
9755                tracking.dialog_ptr,
9756                tracking.edit_item,
9757                tracking.edit_text.clone(),
9758                tracking.items.clone(),
9759            )
9760        };
9761        self.flush_dialog_edit_item_texts(bus, dlg_ptr, &items, edit_item, &edit_text);
9762        let saved = self.dialog_tracking.take().unwrap();
9763        self.persist_visible_dialog_snapshot(bus, &saved);
9764        self.dialog_saved_pixels
9765            .insert(saved.dialog_ptr, saved.saved_pixels);
9766        if saved.item_hit_ptr != 0 {
9767            bus.write_word(saved.item_hit_ptr, item_no as u16);
9768        }
9769        cpu.write_reg(Register::A7, saved.stack_ptr + 8);
9770    }
9771
9772    pub(crate) fn dispatch_dialog<C: CpuOps>(
9773        &mut self,
9774        is_tool: bool,
9775        trap_num: u16,
9776        cpu: &mut C,
9777        bus: &mut MacMemoryBus,
9778    ) -> Option<Result<()>> {
9779        Some(match (is_tool, trap_num) {
9780            // ========== Dialog Manager ==========
9781
9782            // InitDialogs ($A97B)
9783            // PROCEDURE InitDialogs(resumeProc: ProcPtr);
9784            // Inside Macintosh Volume I, I-411.
9785            //
9786            // Performs the 3 documented Dialog Manager init steps:
9787            //   1. Stores resumeProc into the ResumeProc low-mem
9788            //      global at $0A8C, for later access by the System
9789            //      Error Handler when a fatal system error occurs
9790            //      (NIL is the documented default — no resume).
9791            //   2. Installs the standard sound procedure (we store
9792            //      NIL into DABeeper at $0A9C since Systemless's HLE
9793            //      doesn't model menu-bar-blink sound; ErrorSound
9794            //      ($A98C) can override later).
9795            //   3. Passes empty strings to ParamText — implemented
9796            //      as zeroing the 4-handle DAStrings array at
9797            //      $0AA0 (16 bytes = 4 × Handle, all NIL meaning
9798            //      "no substitution" so dialog/alert text rendered
9799            //      with `^0`..`^3` escapes prints the raw escape).
9800            //
9801            // Also resets AlertStage at $0A9A to 0 so the first
9802            // Alert*-family call starts at stage 1 (per IM:I I-417
9803            // — InitDialogs is the canonical place to ensure
9804            // alert state is fresh per app launch). ANumber at
9805            // $0A98 is left untouched since IM:I I-423 only
9806            // documents it as "the resource ID of the last alert
9807            // that occurred" with no init contract.
9808            //
9809            // Pop = 4 bytes (resumeProc ProcPtr).
9810            // InitDialogs ($A97B): Per IM:I I-411: stores resumeProc at $0A8C (ResumeProc global), zeros DABeeper at $0A9C ("no sound" default per HLE), zeros AlertStage at $0A9A (so first Alert call starts at stage 1), zeros 16-byte DAStrings array at $0AA0..$0AAF (4 NIL ParamText handles); pops 4 bytes
9811            (true, 0x17B) => {
9812                let sp = cpu.read_reg(Register::A7);
9813                let resume_proc = bus.read_long(sp);
9814                use crate::memory::globals::addr;
9815                bus.write_long(addr::RESUME_PROC, resume_proc);
9816                bus.write_long(addr::DA_BEEPER, 0);
9817                bus.write_word(addr::ALERT_STAGE, 0);
9818                // Zero the 4-handle DAStrings array (16 bytes).
9819                for i in 0..4u32 {
9820                    bus.write_long(addr::DA_STRINGS + i * 4, 0);
9821                }
9822                cpu.write_reg(Register::A7, sp + 4);
9823                Ok(())
9824            }
9825
9826            // GetNewDialog ($A97C)
9827            // Creates a dialog from DLOG and DITL resources.
9828            // FUNCTION GetNewDialog (dialogID: INTEGER; dStorage: Ptr;
9829            //     behind: WindowPtr) : DialogPtr;
9830            // Inside Macintosh Volume I, I-424
9831            //
9832            // Stack: SP+0 behind(4), SP+4 dStorage(4), SP+8 dialogID(2),
9833            //        SP+10 result(4).
9834            // Honors `behind` at SP+0 via finish_dialog_creation —
9835            // see NewDialog/NewCDialog and window.rs apply_behind_parameter.
9836            // GetNewDialog ($A97C): Parses DLOG + DITL resources, creates DialogRecord, stores parsed items for ModalDialog/GetDItem; saves background pixels before drawing so ModalDialog can restore clean background on dismiss
9837            (true, 0x17C) => {
9838                let sp = cpu.read_reg(Register::A7);
9839                let behind = bus.read_long(sp);
9840                let storage_ptr = bus.read_long(sp + 4);
9841                let dialog_id = bus.read_word(sp + 8) as i16;
9842                eprintln!("[TRAP] GetNewDialog({})", dialog_id);
9843
9844                // Look up DLOG resource
9845                let dlog_ptr = self
9846                    .find_resource_any(*b"DLOG", dialog_id)
9847                    .map(|(_, ptr)| ptr);
9848
9849                if let Some(dlog_data) = dlog_ptr {
9850                    let dlog_len = bus.get_alloc_size(dlog_data).unwrap_or(0);
9851                    let (raw_bounds, proc_id, visible, items_id, title, position) =
9852                        Self::parse_dlog(bus, dlog_data, dlog_len);
9853                    let mut bounds = raw_bounds;
9854
9855                    // Look up DITL resource
9856                    let ditl_info = self
9857                        .find_resource_any(*b"DITL", items_id)
9858                        .map(|(_, ptr)| ptr);
9859
9860                    let Some(ditl_data) = ditl_info else {
9861                        eprintln!(
9862                            "[TRAP] GetNewDialog({}): DITL {} not found",
9863                            dialog_id, items_id
9864                        );
9865                        // MTE 1992 p. 6-114: GetNewDialog returns NIL if
9866                        // either the DLOG or item-list resource cannot be
9867                        // read. Resource Manager ResError reports
9868                        // resNotFound (-192) for missing resources.
9869                        bus.write_word(0x0A60, Self::RES_NOT_FOUND as u16);
9870                        bus.write_long(sp + 10, 0);
9871                        cpu.write_reg(Register::A7, sp + 10);
9872                        return Some(Ok(()));
9873                    };
9874                    bus.write_word(0x0A60, 0);
9875                    let ditl_len = bus.get_alloc_size(ditl_data).unwrap_or(0);
9876                    let items = Self::parse_ditl(bus, ditl_data, ditl_len);
9877
9878                    // Apply System 7 DLOG positioning constants.
9879                    // Macintosh Toolbox Essentials 1992, pp. 4-125 to 4-126.
9880                    bounds = self.positioned_dialog_bounds(bounds, position);
9881
9882                    eprintln!(
9883                        "[TRAP] GetNewDialog({}) bounds=({},{},{},{}) raw_bounds=({},{},{},{}) position=${:04X} len={} procID={} items={} title=\"{}\"",
9884                        dialog_id, bounds.0, bounds.1, bounds.2, bounds.3,
9885                        raw_bounds.0, raw_bounds.1, raw_bounds.2, raw_bounds.3,
9886                        position,
9887                        dlog_len,
9888                        proc_id, items.len(), title
9889                    );
9890                    for (i, item) in items.iter().enumerate() {
9891                        let base_type = item.item_type & 0x7F;
9892                        let type_name = match base_type {
9893                            4 => "button",
9894                            5 => "checkbox",
9895                            6 => "radio",
9896                            7 => "resCtrl",
9897                            8 => "statText",
9898                            16 => "editText",
9899                            32 => "icon",
9900                            64 => "picture",
9901                            0 => "userItem",
9902                            _ => "unknown",
9903                        };
9904                        if base_type == 32 || base_type == 64 {
9905                            eprintln!(
9906                                "[TRAP]   item {}: type={}({}) rect=({},{},{},{}) resID={}",
9907                                i + 1,
9908                                type_name,
9909                                item.item_type,
9910                                item.rect.0,
9911                                item.rect.1,
9912                                item.rect.2,
9913                                item.rect.3,
9914                                item.resource_id
9915                            );
9916                        } else {
9917                            eprintln!(
9918                                "[TRAP]   item {}: type={}({}) rect=({},{},{},{}) text=\"{}\"",
9919                                i + 1,
9920                                type_name,
9921                                item.item_type,
9922                                item.rect.0,
9923                                item.rect.1,
9924                                item.rect.2,
9925                                item.rect.3,
9926                                item.text
9927                            );
9928                        }
9929                    }
9930
9931                    let items_handle = if let Some((_, ditl_handle_ptr)) =
9932                        self.find_resource_any(*b"DITL", items_id)
9933                    {
9934                        let handle = bus.alloc(4);
9935                        bus.write_long(handle, ditl_handle_ptr);
9936                        Self::duplicate_handle_data(bus, handle)
9937                    } else {
9938                        0
9939                    };
9940                    // Honor the DLOG resource's visible flag per IM:I I-424.
9941                    let dlg_ptr = self.finish_dialog_creation(
9942                        bus,
9943                        cpu,
9944                        storage_ptr,
9945                        bounds,
9946                        &title,
9947                        visible,
9948                        proc_id,
9949                        false,
9950                        0,
9951                        items_handle,
9952                        items,
9953                    );
9954                    // Install any 'pltt' resource whose id matches the
9955                    // dialog id onto the freshly-created window. This
9956                    // mirrors what GetNewWindow / NewCWindow do in
9957                    // window.rs and is what the real Window Manager
9958                    // does: a Window with an associated palette gets
9959                    // its palette activated when the window is created
9960                    // and made active. Without this hook, a dialog
9961                    // whose PICT items rely on an auto-installed
9962                    // palette (e.g. EV's landing-scene dialog id 1000)
9963                    // renders through whatever canonical CLUT happens
9964                    // to be live, producing palette-mismatched colour
9965                    // noise.  Inside Macintosh Volume VI, 20-12 to
9966                    // 20-13 (palette association and activation).
9967                    if dlg_ptr != 0 {
9968                        let palette = self.copy_palette_resource(bus, dialog_id);
9969                        if palette != 0 {
9970                            self.set_window_palette_association(dlg_ptr, palette, -0x2000);
9971                            self.activate_palette_for_window(bus, dlg_ptr);
9972                        }
9973                        self.apply_behind_parameter(bus, dlg_ptr, behind);
9974                    }
9975                    bus.write_long(sp + 10, dlg_ptr);
9976                } else {
9977                    eprintln!("[TRAP] GetNewDialog({}): DLOG not found -> NIL", dialog_id);
9978                    // MTE 1992 p. 6-114: GetNewDialog returns NIL if the
9979                    // DLOG resource cannot be read. Resource Manager ResError
9980                    // reports resNotFound (-192) for missing resources.
9981                    bus.write_word(0x0A60, Self::RES_NOT_FOUND as u16);
9982                    bus.write_long(sp + 10, 0);
9983                }
9984                cpu.write_reg(Register::A7, sp + 10);
9985                Ok(())
9986            }
9987
9988            // Alert ($A985) / StopAlert ($A986) / NoteAlert ($A987)
9989            // / CautionAlert ($A988)
9990            //
9991            // FUNCTION Alert(alertID: INTEGER; filterProc: ProcPtr): INTEGER;
9992            // (and identical signatures for Stop/Note/Caution Alert)
9993            //
9994            // The four alert variants differ only in the ICON they
9995            // display (none / Stop hand / Note speaker / Caution
9996            // triangle); their dispatch into the ALRT template +
9997            // ALRT stages logic is identical. OK-only alerts collapse
9998            // to "return the bold/default item for the current stage,
9999            // increment AlertStage." Multi-button alerts enter the
10000            // normal dialog tracking loop so a script or user can
10001            // choose a non-default button before the trap returns.
10002            //
10003            // ALRT template per IM:I I-422:
10004            //   +0  boundsRect:   Rect (8 bytes)
10005            //   +8  itemsID:      INTEGER (DITL resource ID)
10006            //   +10 stages:       INTEGER (16-bit, 4 nibbles, 1
10007            //                     per stage 1..4 from low nibble
10008            //                     to high). Each nibble:
10009            //                       bit 3:    boldItmNum flag
10010            //                                 (0 = item 1 bold/
10011            //                                 default, 1 = item 2
10012            //                                 bold/default)
10013            //                       bit 2:    boxDrawn flag
10014            //                                 (alert is shown if
10015            //                                 set; suppressed if
10016            //                                 clear)
10017            //                       bits 0-1: soundNum (0..3 —
10018            //                                 0=silent, 1=note,
10019            //                                 2=caution, 3=stop)
10020            //
10021            // The current alert stage lives in the AlertStage
10022            // low-mem global at $0A9A (1 byte, holds 0..3 mapping
10023            // to stages 1..4). After each Alert*-family call the
10024            // byte is incremented and capped at 3, so the fourth
10025            // and subsequent calls all use the stage-4 nibble.
10026            // ResetAlrtStage ($A98B) zeros the byte; GetAlrtStage
10027            // ($A9B7) returns it.
10028            //
10029            // HLE compromise: filterProc is NOT invoked (no guest-
10030            // fn dispatch infrastructure for the ProcPtr argument
10031            // — same compromise as Pack1 LSearch and other guest-
10032            // ProcPtr-taking traps).
10033            // The missing-ALRT path is a defensive probe guard:
10034            // return -1 but leave AlertStage and ANumber exactly as
10035            // the caller left them.
10036            //
10037            // Inside Macintosh Volume I, I-417..I-422 (Alert
10038            // family + ALRT template); Macintosh Toolbox Essentials
10039            // 1992, 6-105..6-119 (System 7 alert dispatch).
10040            // Alert ($A985): Looks up ALRT, parses stages word at +10 per IM:I I-422, returns bold item (1 or 2) for current visible AlertStage ($0A9A) or -1 if ALRT missing / boxDrwn is clear; multi-button alerts track user/script button hits before returning; increments AlertStage capped at 3; filterProc NOT invoked
10041            // StopAlert ($A986): Same as Alert with Stop icon; identical dispatch path
10042            // NoteAlert ($A987): Same as Alert with Note icon; identical dispatch path
10043            // CautionAlert ($A988): Same as Alert with Caution icon; identical dispatch path
10044            (true, 0x185) | (true, 0x186) | (true, 0x187) | (true, 0x188) => {
10045                const ALERT_MISSING_RESOURCE_RESULT: i16 = -1;
10046                let sp = cpu.read_reg(Register::A7);
10047                if self.dialog_tracking.is_some() {
10048                    self.handle_interactive_alert_refire(cpu, bus);
10049                    return Some(Ok(()));
10050                }
10051                let filter_proc = bus.read_long(sp);
10052                let alert_id = bus.read_word(sp + 4) as i16;
10053                let trap_name = match trap_num {
10054                    0x185 => "Alert",
10055                    0x186 => "StopAlert",
10056                    0x187 => "NoteAlert",
10057                    0x188 => "CautionAlert",
10058                    _ => "Alert?",
10059                };
10060                let alert_stage_before = bus.read_word(crate::memory::globals::addr::ALERT_STAGE);
10061                let anumber_before = bus.read_word(crate::memory::globals::addr::ANUMBER);
10062                let mut alert_trace_stage: Option<(u16, u16, u32, u32)> = None;
10063                // Look up the ALRT resource and, if present, also pull
10064                // the referenced DITL's static-text items so the trap
10065                // log surfaces the alert message before we silently
10066                // dismiss it. Mid-90s titles (Bumbler, Steel Fighters,
10067                // etc.) emit StopAlert+ExitToShell on incompatibility;
10068                // without the message text in the log there's no way
10069                // to know *which* check failed without RE'ing the
10070                // binary. Inside Macintosh Volume I, I-422 (ALRT
10071                // template) and I-426 (DITL).
10072                let alrt_ptr = self
10073                    .find_resource_any(*b"ALRT", alert_id)
10074                    .map(|(_, ptr)| ptr);
10075                let result: i16 = if let Some(alrt_data) = alrt_ptr {
10076                    let alrt_len = bus.get_alloc_size(alrt_data).unwrap_or(0);
10077                    let (bounds, items_id, stages, position) =
10078                        Self::parse_alrt(bus, alrt_data, alrt_len);
10079                    // AlertStage / ACount lives at $0A9A as a
10080                    // 16-bit WORD (NOT a byte) per IM:I I-423 +
10081                    // MTb 1992 22620 `#define GetAlertStage()
10082                    // (* (short*) 0x0A9A)`. On big-endian 68k a
10083                    // word value 0..3 stores byte 0 at $0A9A and
10084                    // value at $0A9B, so reading as a byte at
10085                    // $0A9A would always return 0 — must use
10086                    // read_word/write_word.
10087                    let stage_word = bus.read_word(crate::memory::globals::addr::ALERT_STAGE);
10088                    let (stage_idx, nibble, default_item) =
10089                        Self::alert_stage_default_item(stages, stage_word);
10090                    alert_trace_stage = Some((stages, stage_word, stage_idx, nibble));
10091                    // bit 3 (MSB) of nibble = boldItm per StageList
10092                    // PACKED RECORD layout (IM:I I-422): boldItm,
10093                    // boxDrwn, sound[2]. Assembly mask okDismissal=8
10094                    // and alBit=4 confirm bit masks. MTE 1992 p. 6-106
10095                    // says Alert returns -1 when boxDrwn is clear.
10096                    // Increment AlertStage, capped at 3, so the
10097                    // next call uses the next stage's nibble.
10098                    let next_stage = ((stage_word as u32) + 1).min(3) as u16;
10099                    bus.write_word(crate::memory::globals::addr::ALERT_STAGE, next_stage);
10100                    // ANumber records the resource ID of the last
10101                    // alert that occurred (IM:I I-423). Apps that
10102                    // probe ANumber after a sequence of Alert
10103                    // calls expect this to reflect the most-recent
10104                    // ID — used by some defensive resume logic.
10105                    bus.write_word(crate::memory::globals::addr::ANUMBER, alert_id as u16);
10106
10107                    if let Some(default_item) = default_item {
10108                        // Until Alert filterProc dispatch is implemented, keep
10109                        // non-NIL filterProc calls on the old default-item
10110                        // path. Entering local button tracking would ignore
10111                        // caller-supplied filter decisions.
10112                        if filter_proc == 0
10113                            && self.begin_interactive_alert(
10114                                cpu,
10115                                bus,
10116                                sp,
10117                                alert_id,
10118                                filter_proc,
10119                                bounds,
10120                                items_id,
10121                                position,
10122                                default_item,
10123                            )
10124                        {
10125                            if super::dispatch::trace_dialog_traps_enabled() {
10126                                eprintln!(
10127                                    "[TRAP] {} id={} -> interactive dialog (PC=${:08X}) stages=${:04X} alertStage={} stageIdx={} nibble=${:X} defaultItem={}",
10128                                    trap_name,
10129                                    alert_id,
10130                                    cpu.read_reg(Register::PC),
10131                                    stages,
10132                                    stage_word,
10133                                    stage_idx,
10134                                    nibble,
10135                                    default_item
10136                                );
10137                            }
10138                            return Some(Ok(()));
10139                        }
10140                        default_item
10141                    } else {
10142                        ALERT_MISSING_RESOURCE_RESULT
10143                    }
10144                } else {
10145                    bus.write_word(
10146                        crate::memory::globals::addr::ALERT_STAGE,
10147                        alert_stage_before,
10148                    );
10149                    bus.write_word(crate::memory::globals::addr::ANUMBER, anumber_before);
10150                    ALERT_MISSING_RESOURCE_RESULT
10151                };
10152
10153                if super::dispatch::trace_dialog_traps_enabled() {
10154                    let mut detail = format!(
10155                        "[TRAP] {} id={} -> item {} (PC=${:08X})",
10156                        trap_name,
10157                        alert_id,
10158                        result,
10159                        cpu.read_reg(Register::PC)
10160                    );
10161                    if let Some((stages, stage_word, stage_idx, nibble)) = alert_trace_stage {
10162                        detail.push_str(&format!(
10163                            " stages=${stages:04X} alertStage={stage_word} stageIdx={stage_idx} nibble=${nibble:X}"
10164                        ));
10165                    }
10166                    if let Some(alrt_data) = alrt_ptr {
10167                        // ALRT template: bounds(8) + itemsID(2) + stages(2).
10168                        // Inside Macintosh Volume I, I-422. Some titles
10169                        // (Bumbler Bee-Luxe in particular) ship ALRT data
10170                        // whose first 4 bytes don't decode as a Rect — most
10171                        // likely a non-standard prologue prepended by the
10172                        // build's PowerPC fragment or resource compiler.
10173                        // Detect via implausible bounds (top > bottom or
10174                        // wildly negative coordinates) and suppress the
10175                        // usual itemsID lookup so the trace reflects that
10176                        // the resource isn't in the documented format.
10177                        let alrt_len = bus.get_alloc_size(alrt_data).unwrap_or(0);
10178                        let ((top, left, bottom, right), items_id, _, _) =
10179                            Self::parse_alrt(bus, alrt_data, alrt_len);
10180                        let bounds_plausible = (-32..1024).contains(&top)
10181                            && (-32..2048).contains(&left)
10182                            && bottom > top
10183                            && right > left
10184                            && (bottom - top) < 1024
10185                            && (right - left) < 2048;
10186                        if !bounds_plausible {
10187                            detail.push_str(&format!(
10188                                " bounds=({},{},{},{}) — implausible, skipping DITL lookup",
10189                                top, left, bottom, right
10190                            ));
10191                        } else {
10192                            let ditl_match = self.find_resource_any(*b"DITL", items_id);
10193                            detail.push_str(&format!(
10194                                " bounds=({},{},{},{}) itemsID={} ditl={}",
10195                                top,
10196                                left,
10197                                bottom,
10198                                right,
10199                                items_id,
10200                                if ditl_match.is_some() {
10201                                    "found"
10202                                } else {
10203                                    "missing"
10204                                }
10205                            ));
10206                            if let Some((_, ditl_data)) = ditl_match {
10207                                let ditl_len = bus.get_alloc_size(ditl_data).unwrap_or(0);
10208                                let items = Self::parse_ditl(bus, ditl_data, ditl_len);
10209                                for (i, item) in items.iter().enumerate() {
10210                                    if !item.text.is_empty() {
10211                                        detail.push_str(&format!(
10212                                            "\n[TRAP]   item {} (type=${:02X}): {:?}",
10213                                            i + 1,
10214                                            item.item_type,
10215                                            item.text
10216                                        ));
10217                                    }
10218                                }
10219                            }
10220                        }
10221                    }
10222                    eprintln!("{}", detail);
10223                }
10224                bus.write_word(sp + 6, result as u16);
10225                cpu.write_reg(Register::A7, sp + 6);
10226                Ok(())
10227            }
10228
10229            // IsDialogEvent ($A97F)
10230            // Tests whether an event should be handled as part of an active
10231            // modeless or movable modal dialog.
10232            // FUNCTION IsDialogEvent(theEvent: EventRecord): Boolean;
10233            // Inside Macintosh Volume I, I-416; Macintosh Toolbox Essentials 1992, 6-138
10234            (true, 0x17F) => {
10235                let sp = cpu.read_reg(Register::A7);
10236                let event_ptr = bus.read_long(sp);
10237                let (what, message, where_v, where_h, _modifiers) =
10238                    Self::read_guest_event_record(bus, event_ptr);
10239                let target_dialog = self.dialog_from_window_event(what, message);
10240                let result = if let Some(dialog_ptr) = target_dialog {
10241                    match what {
10242                        6 | 8 => message == dialog_ptr,
10243                        1 => Self::dialog_contains_screen_point(
10244                            Self::dialog_screen_bounds(bus, dialog_ptr),
10245                            where_v,
10246                            where_h,
10247                        ),
10248                        _ => true,
10249                    }
10250                } else {
10251                    false
10252                };
10253                self.record_dialog_input_trace(
10254                    "A97F",
10255                    event_ptr,
10256                    what,
10257                    message,
10258                    where_v,
10259                    where_h,
10260                    _modifiers,
10261                    target_dialog,
10262                    result,
10263                    "outcome=is_dialog_event",
10264                );
10265                bus.write_word(sp + 4, if result { 0xFFFF } else { 0 });
10266                cpu.write_reg(Register::A7, sp + 4);
10267                Ok(())
10268            }
10269
10270            // DialogSelect ($A980)
10271            // Handles events for modeless and movable modal dialogs.
10272            // FUNCTION DialogSelect(theEvent: EventRecord; VAR theDialog: DialogPtr; VAR itemHit: INTEGER): BOOLEAN;
10273            // Inside Macintosh Volume I, I-417; Macintosh Toolbox Essentials 1992, 6-139
10274            (true, 0x180) => {
10275                let sp = cpu.read_reg(Register::A7);
10276                let item_hit_ptr = bus.read_long(sp);
10277                let dialog_out_ptr = bus.read_long(sp + 4);
10278                let event_ptr = bus.read_long(sp + 8);
10279                let (what, message, where_v, where_h, _modifiers) =
10280                    Self::read_guest_event_record(bus, event_ptr);
10281                let mut result = false;
10282                let mut trace_detail = String::from("outcome=no_dialog_target");
10283
10284                let target_dialog = self.dialog_from_window_event(what, message);
10285                if let Some(dialog_ptr) = target_dialog {
10286                    let bounds = Self::dialog_screen_bounds(bus, dialog_ptr);
10287                    trace_detail = format!(
10288                        "bounds=({},{},{},{}) outcome=no_item",
10289                        bounds.0, bounds.1, bounds.2, bounds.3
10290                    );
10291                    if let Some(mut items) = self.dialog_items.get(&dialog_ptr).cloned() {
10292                        Self::refresh_ditl_proc_ptrs(bus, dialog_ptr, &mut items);
10293                        match what {
10294                            6 => {
10295                                // MTE 1992 p. 6-141: DialogSelect wraps the
10296                                // update redraw in BeginUpdate/EndUpdate,
10297                                // calls DrawDialog, and returns FALSE.
10298                                let update_rect =
10299                                    Self::region_handle_rect(bus, bus.read_long(dialog_ptr + 122));
10300                                self.begin_update_window(bus, dialog_ptr);
10301                                self.update_dialog_window_contents(
10302                                    bus,
10303                                    cpu,
10304                                    dialog_ptr,
10305                                    update_rect,
10306                                );
10307                                self.end_update_window(bus, dialog_ptr);
10308                                trace_detail = format!(
10309                                    "bounds=({},{},{},{}) outcome=update_redraw",
10310                                    bounds.0, bounds.1, bounds.2, bounds.3
10311                                );
10312                            }
10313                            1 if Self::dialog_contains_screen_point(bounds, where_v, where_h) => {
10314                                let hit = self.dialog_item_hit_test(
10315                                    bus,
10316                                    &items,
10317                                    bounds,
10318                                    where_v,
10319                                    where_h,
10320                                    &self.dialog_popup_original_rects,
10321                                    dialog_ptr,
10322                                );
10323                                if hit > 0 {
10324                                    let item = &items[(hit - 1) as usize];
10325                                    let is_disabled = (item.item_type & 0x80) != 0;
10326                                    trace_detail = format!(
10327                                        "bounds=({},{},{},{}) item_hit={} item_type=${:02X} disabled={} outcome={}",
10328                                        bounds.0,
10329                                        bounds.1,
10330                                        bounds.2,
10331                                        bounds.3,
10332                                        hit,
10333                                        item.item_type,
10334                                        if is_disabled { "true" } else { "false" },
10335                                        if is_disabled {
10336                                            "disabled_item"
10337                                        } else {
10338                                            "enabled_item"
10339                                        },
10340                                    );
10341                                    if trace_dialog_items_enabled() {
10342                                        eprintln!(
10343                                            "[DIALOG-SELECT] mouseDown dialog=${:08X} where=({},{}) bounds=({},{},{},{}) hit={} type=${:02X} disabled={}",
10344                                            dialog_ptr,
10345                                            where_v,
10346                                            where_h,
10347                                            bounds.0,
10348                                            bounds.1,
10349                                            bounds.2,
10350                                            bounds.3,
10351                                            hit,
10352                                            item.item_type,
10353                                            is_disabled,
10354                                        );
10355                                    }
10356                                    if !is_disabled {
10357                                        self.cancel_app_owned_modal_dialog_button_tracking(
10358                                            bus, dialog_ptr,
10359                                        );
10360                                        if (item.item_type & 0x7F) == 16 {
10361                                            // MTE 1992 p. 6-139 / IM:I I-417:
10362                                            // mouseDown in an enabled editText item makes
10363                                            // that item the active edit field before
10364                                            // reporting the item hit. TEClick's pixel-to-
10365                                            // caret mapping remains the documented HLE
10366                                            // compromise in the TEClick trap.
10367                                            self.activate_dialog_edit_item(
10368                                                bus, cpu, dialog_ptr, &items, hit,
10369                                            );
10370                                        }
10371                                        if dialog_out_ptr != 0 {
10372                                            bus.write_long(dialog_out_ptr, dialog_ptr);
10373                                        }
10374                                        if item_hit_ptr != 0 {
10375                                            bus.write_word(item_hit_ptr, hit as u16);
10376                                        }
10377                                        result = true;
10378                                    }
10379                                } else {
10380                                    trace_detail = format!(
10381                                        "bounds=({},{},{},{}) item_hit=0 outcome=no_item",
10382                                        bounds.0, bounds.1, bounds.2, bounds.3
10383                                    );
10384                                    if trace_dialog_items_enabled() {
10385                                        eprintln!(
10386                                            "[DIALOG-SELECT] mouseDown dialog=${:08X} where=({},{}) bounds=({},{},{},{}) hit=0",
10387                                            dialog_ptr,
10388                                            where_v,
10389                                            where_h,
10390                                            bounds.0,
10391                                            bounds.1,
10392                                            bounds.2,
10393                                            bounds.3,
10394                                        );
10395                                    }
10396                                }
10397                            }
10398                            0 => {
10399                                let (_edit_text, edit_item, _default_item) =
10400                                    Self::dialog_edit_state(bus, dialog_ptr, &items);
10401                                trace_detail = format!(
10402                                    "bounds=({},{},{},{}) edit_item={} outcome=teidle",
10403                                    bounds.0, bounds.1, bounds.2, bounds.3, edit_item
10404                                );
10405                                if edit_item > 0 {
10406                                    // MTE 1992 p. 6-139 / IM:I I-417:
10407                                    // DialogSelect calls TEIdle for null events when an
10408                                    // editText item is present, letting TextEdit advance
10409                                    // the insertion-caret blink without changing text or
10410                                    // selection fields.
10411                                    let text_handle = bus.read_long(dialog_ptr + 160);
10412                                    self.textedit_idle(cpu, bus, text_handle);
10413                                }
10414                            }
10415                            1 => {
10416                                trace_detail = format!(
10417                                    "bounds=({},{},{},{}) item_hit=0 outcome=outside_dialog",
10418                                    bounds.0, bounds.1, bounds.2, bounds.3
10419                                );
10420                                if trace_dialog_items_enabled() {
10421                                    eprintln!(
10422                                        "[DIALOG-SELECT] mouseDown dialog=${:08X} where=({},{}) outside bounds=({},{},{},{})",
10423                                        dialog_ptr,
10424                                        where_v,
10425                                        where_h,
10426                                        bounds.0,
10427                                        bounds.1,
10428                                        bounds.2,
10429                                        bounds.3,
10430                                    );
10431                                }
10432                            }
10433                            3 | 5 => {
10434                                let (_edit_text, edit_item, _default_item) =
10435                                    Self::dialog_edit_state(bus, dialog_ptr, &items);
10436                                trace_detail = format!(
10437                                    "bounds=({},{},{},{}) edit_item={} outcome=no_enabled_edittext",
10438                                    bounds.0, bounds.1, bounds.2, bounds.3, edit_item
10439                                );
10440                                if edit_item > 0 {
10441                                    // IM:I I-417: keyDown/autoKey dialog handling applies to
10442                                    // editable text items. If no enabled editText item is
10443                                    // active, DialogSelect returns FALSE.
10444                                    if let Some(item) = items.get((edit_item - 1) as usize) {
10445                                        let item_type = item.item_type;
10446                                        let base_type = item_type & 0x7F;
10447                                        let is_disabled = (item_type & 0x80) != 0;
10448                                        trace_detail = format!(
10449                                            "bounds=({},{},{},{}) edit_item={} item_type=${:02X} disabled={} outcome={}",
10450                                            bounds.0,
10451                                            bounds.1,
10452                                            bounds.2,
10453                                            bounds.3,
10454                                            edit_item,
10455                                            item_type,
10456                                            if is_disabled { "true" } else { "false" },
10457                                            if base_type == 16 && !is_disabled {
10458                                                "enabled_edittext"
10459                                            } else {
10460                                                "no_enabled_edittext"
10461                                            },
10462                                        );
10463                                        if base_type == 16 && !is_disabled {
10464                                            let char_code = (message & 0xFF) as u8;
10465                                            // MTE 1992 p. 6-139: DialogSelect uses TextEdit
10466                                            // to handle key-down and auto-key events in
10467                                            // editable text items before reporting itemHit.
10468                                            self.apply_dialog_select_key_to_edit_item(
10469                                                bus, dialog_ptr, &mut items, edit_item, char_code,
10470                                            );
10471                                            if dialog_out_ptr != 0 {
10472                                                bus.write_long(dialog_out_ptr, dialog_ptr);
10473                                            }
10474                                            if item_hit_ptr != 0 {
10475                                                bus.write_word(item_hit_ptr, edit_item as u16);
10476                                            }
10477                                            result = true;
10478                                        }
10479                                    }
10480                                }
10481                            }
10482                            _ => {}
10483                        }
10484                        self.dialog_items.insert(dialog_ptr, items);
10485                    } else {
10486                        trace_detail = format!(
10487                            "bounds=({},{},{},{}) outcome=no_dialog_items",
10488                            bounds.0, bounds.1, bounds.2, bounds.3
10489                        );
10490                        if trace_dialog_items_enabled() {
10491                            eprintln!(
10492                                "[DIALOG-SELECT] event what={} dialog=${:08X} has no dialog_items",
10493                                what, dialog_ptr
10494                            );
10495                        }
10496                    }
10497                } else if trace_dialog_items_enabled() {
10498                    eprintln!(
10499                        "[DIALOG-SELECT] event what={} message=${:08X} where=({},{}) has no dialog target front=${:08X}",
10500                        what, message, where_v, where_h, self.front_window
10501                    );
10502                }
10503                self.record_dialog_input_trace(
10504                    "A980",
10505                    event_ptr,
10506                    what,
10507                    message,
10508                    where_v,
10509                    where_h,
10510                    _modifiers,
10511                    target_dialog,
10512                    result,
10513                    &trace_detail,
10514                );
10515
10516                bus.write_word(sp + 12, if result { 0xFFFF } else { 0 });
10517                cpu.write_reg(Register::A7, sp + 12);
10518                Ok(())
10519            }
10520
10521            // DrawDialog ($A981)
10522            // Draws the entire contents of the specified dialog box.
10523            // PROCEDURE DrawDialog (theDialog: DialogPtr);
10524            // Inside Macintosh Volume I, I-417; Macintosh Toolbox Essentials 1992, 6-142
10525            // DrawDialog ($A981): Renders all dialog items with full DITL support
10526            (true, 0x181) => {
10527                let sp = cpu.read_reg(Register::A7);
10528                let dialog_ptr = bus.read_long(sp);
10529                if let Some(mut items) = self.dialog_items.get(&dialog_ptr).cloned() {
10530                    Self::refresh_ditl_proc_ptrs(bus, dialog_ptr, &mut items);
10531                    let bounds = Self::dialog_screen_bounds(bus, dialog_ptr);
10532                    let proc_id = self.dialog_window_proc_id(bus, dialog_ptr);
10533                    let (edit_text, edit_item, default_item) =
10534                        Self::dialog_edit_state(bus, dialog_ptr, &items);
10535                    self.dialog_initial_draw_deferred.remove(&dialog_ptr);
10536                    self.draw_dialog_preserving_user_items(
10537                        bus,
10538                        bounds,
10539                        proc_id,
10540                        "",
10541                        &items,
10542                        default_item,
10543                        &edit_text,
10544                        edit_item,
10545                        false,
10546                        dialog_ptr,
10547                        true,
10548                        false,
10549                        false,
10550                    );
10551                    self.dialog_items.insert(dialog_ptr, items);
10552                    // MTE 1992 p. 6-142: DrawDialog calls
10553                    // application-defined item draw procs for userItem
10554                    // records. Queue them after the HLE redraw so the
10555                    // runner trampoline executes guest drawing before the
10556                    // caller resumes.
10557                    self.queue_modeless_dialog_draw_procs_intersecting(bus, dialog_ptr, None);
10558                }
10559                cpu.write_reg(Register::A7, sp + 4);
10560                Ok(())
10561            }
10562
10563            // DisposDialog ($A983)
10564            // Removes a dialog and frees its storage.
10565            // PROCEDURE DisposDialog (theDialog: DialogPtr);
10566            // Inside Macintosh Volume I, I-425
10567            //
10568            // DisposDialog internally calls CloseWindow, which on real
10569            // Mac OS invalidates the window's frame and the Window
10570            // Manager's PaintBehind / CalcVisBehind restores the
10571            // content beneath via update events to the back windows.
10572            //
10573            // In HLE we emulate PaintBehind by blitting the saved-under
10574            // pixels captured immediately before the dialog first draws to
10575            // the visible screen. Without this restore, any app that skips
10576            // ModalDialog and runs its own event loop leaves a dialog-shaped
10577            // hole over the window behind.
10578            //
10579            // Only restore when `was_front` holds: a non-front dialog
10580            // would require the saved bounds to still be valid, but
10581            // by the time we'd get here the window stack has already
10582            // moved on. In practice modal dialogs are always the front
10583            // window, so this covers every real case.
10584            //
10585            // Inside Macintosh Volume I, I-283 (CloseWindow), I-425 (DisposDialog)
10586            //
10587            // Regression coverage:
10588            //   src/trap/dialog.rs::tests::disposdialog_restores_saved_background_pixels
10589            //   src/trap/dialog.rs::tests::disposdialog_non_front_does_not_restore
10590            // DisposDialog ($A983): Frees dialog, cleans up dialog_items, restores saved background pixels (IM:I I-425 PaintBehind)
10591            (true, 0x183) => {
10592                let sp = cpu.read_reg(Register::A7);
10593                let requested_dialog_ptr = bus.read_long(sp);
10594                let dialog_ptr =
10595                    self.resolve_dispos_dialog_ptr_after_modal_button_hit(requested_dialog_ptr);
10596                eprintln!("[TRAP] DisposDialog(${:08X})", requested_dialog_ptr);
10597                self.close_dialog_window(bus, cpu, dialog_ptr, true);
10598                self.capture_gui_frame(bus, &format!("dispos_dialog_{:08X}", dialog_ptr));
10599                cpu.write_reg(Register::A7, sp + 4);
10600                Ok(())
10601            }
10602
10603            // ParamText ($A98B)
10604            // Saves the four strings for ^0..^3 substitution into any
10605            // subsequently drawn dialog or alert static-text item.
10606            // PROCEDURE ParamText(param0, param1, param2, param3: Str255);
10607            // Inside Macintosh Volume I, I-422
10608            // ParamText ($A98B): Saves all 4 Pascal strings for ^0..^3 dialog/alert text substitution
10609            (true, 0x18B) => {
10610                let sp = cpu.read_reg(Register::A7);
10611                let trap_pc = cpu.read_reg(Register::PC).wrapping_sub(2);
10612                // Stack layout (top-down): SP+0:param3, +4:param2, +8:param1, +12:param0.
10613                // Per Inside Macintosh Volume I, I-422, passing NIL for any
10614                // parameter leaves that slot's previous value unchanged —
10615                // it's a "set this slot, leave others alone" idiom. Clearing
10616                // on NIL would erase ^N output that an earlier ParamText
10617                // had legitimately staged.
10618                let offsets: [u32; 4] = [12, 8, 4, 0];
10619                for (i, &off) in offsets.iter().enumerate() {
10620                    let ptr = bus.read_long(sp + off);
10621                    if ptr == 0 {
10622                        continue;
10623                    }
10624                    let s = bus.read_pstring(ptr);
10625                    self.param_text[i] = s.clone();
10626                    // Write to DAStrings low-memory global ($0AA0 + i*4).
10627                    // The ROM stores each param string as a StringHandle at
10628                    // *(StringHandle*)($0AA0 + i*4).
10629                    // Inside Macintosh Volume I, I-422 (DAStrings global).
10630                    use crate::memory::globals::addr;
10631                    let data_len = 1u32 + s.len() as u32;
10632                    let data_ptr = bus.alloc(data_len);
10633                    if data_ptr != 0 {
10634                        bus.write_byte(data_ptr, s.len() as u8);
10635                        for (j, &b) in s.iter().enumerate() {
10636                            bus.write_byte(data_ptr + 1 + j as u32, b);
10637                        }
10638                        let handle = bus.alloc(4);
10639                        if handle != 0 {
10640                            bus.write_long(handle, data_ptr);
10641                            bus.write_long(addr::DA_STRINGS + i as u32 * 4, handle);
10642                        }
10643                    }
10644                }
10645                eprintln!(
10646                    "[TRAP] ParamText pc=${:08X} ^0=\"{}\" ^1=\"{}\" ^2=\"{}\" ^3=\"{}\"",
10647                    trap_pc,
10648                    String::from_utf8_lossy(&self.param_text[0]),
10649                    String::from_utf8_lossy(&self.param_text[1]),
10650                    String::from_utf8_lossy(&self.param_text[2]),
10651                    String::from_utf8_lossy(&self.param_text[3]),
10652                );
10653                cpu.write_reg(Register::A7, sp + 16);
10654                Ok(())
10655            }
10656
10657            // GetDItem ($A98D)
10658            // Returns information about a dialog item.
10659            // PROCEDURE GetDItem (theDialog: DialogPtr; itemNo: INTEGER;
10660            //     VAR itemType: INTEGER; VAR item: Handle; VAR box: Rect);
10661            // Inside Macintosh Volume I, I-421
10662            // GetDItem ($A98D): Returns real item type, handle (with text for statText/editText, proc_ptr for userItem), and rect from dialog_items
10663            (true, 0x18D) => {
10664                let sp = cpu.read_reg(Register::A7);
10665                let box_ptr = bus.read_long(sp);
10666                let item_handle_ptr = bus.read_long(sp + 4);
10667                let type_ptr = bus.read_long(sp + 8);
10668                let item_no = bus.read_word(sp + 12) as i16;
10669                let dialog_ptr = bus.read_long(sp + 14);
10670
10671                // Look up real item data
10672                let found = self.dialog_items.get(&dialog_ptr).and_then(|items| {
10673                    if item_no > 0 && (item_no as usize) <= items.len() {
10674                        Some(&items[(item_no - 1) as usize])
10675                    } else {
10676                        None
10677                    }
10678                });
10679
10680                if let Some(item) = found {
10681                    if trace_dialog_items_enabled() && (item.item_type & 0x7F) == 0 {
10682                        eprintln!(
10683                            "[DIALOG-ITEM] GetDItem pc=${:08X} dialog=${:08X} item={} type={} proc=${:08X} out_type=${:08X} out_item=${:08X} out_box=${:08X} rect=({},{},{},{})",
10684                            cpu.read_reg(Register::PC),
10685                            dialog_ptr,
10686                            item_no,
10687                            item.item_type,
10688                            item.proc_ptr,
10689                            type_ptr,
10690                            item_handle_ptr,
10691                            box_ptr,
10692                            item.rect.0,
10693                            item.rect.1,
10694                            item.rect.2,
10695                            item.rect.3,
10696                        );
10697                    }
10698                    if type_ptr != 0 {
10699                        bus.write_word(type_ptr, item.item_type as u16);
10700                    }
10701                    if item_handle_ptr != 0 {
10702                        let current_handle = Self::dialog_item_handle(bus, dialog_ptr, item_no);
10703                        let base_type = item.item_type & 0x7F;
10704                        if current_handle != 0 || !(4..=6).contains(&base_type) {
10705                            bus.write_long(item_handle_ptr, current_handle);
10706                        } else {
10707                            // Create a full ControlRecord so draw_control can render it.
10708                            // ControlRecord layout:
10709                            //   +0: nextControl (4)
10710                            //   +4: contrlOwner (4) = window ptr
10711                            //   +8: contrlRect (8) = top, left, bottom, right
10712                            //  +16: contrlVis (1) = 255 (visible)
10713                            //  +17: contrlHilite (1) = 0
10714                            //  +18: contrlValue (2)
10715                            //  +20: contrlMin (2)
10716                            //  +22: contrlMax (2) = 1
10717                            //  +24: contrlDefProc (4) = procID encoding
10718                            //  +40: contrlTitle (pascal string)
10719                            let title_len = item.text.len().min(255);
10720                            let ctrl_rec = bus.alloc(42 + title_len as u32);
10721                            bus.write_long(ctrl_rec, 0); // nextControl
10722                            bus.write_long(ctrl_rec + 4, dialog_ptr); // contrlOwner
10723                                                                      // contrlRect: dialog-local coordinates (draw_control gets
10724                                                                      // screen offset from the owner window's PixMap bounds)
10725                            bus.write_word(ctrl_rec + 8, item.rect.0 as u16);
10726                            bus.write_word(ctrl_rec + 10, item.rect.1 as u16);
10727                            bus.write_word(ctrl_rec + 12, item.rect.2 as u16);
10728                            bus.write_word(ctrl_rec + 14, item.rect.3 as u16);
10729                            bus.write_byte(ctrl_rec + 16, 255); // contrlVis = visible
10730                            bus.write_byte(ctrl_rec + 17, 0); // contrlHilite
10731                            let value = self
10732                                .dialog_control_values
10733                                .get(&(dialog_ptr, item_no))
10734                                .copied()
10735                                .unwrap_or(0);
10736                            bus.write_word(ctrl_rec + 18, value as u16);
10737                            bus.write_word(ctrl_rec + 20, 0); // contrlMin
10738                            bus.write_word(ctrl_rec + 22, 1); // contrlMax
10739                                                              // Write title as pascal string at offset 40
10740                            bus.write_byte(ctrl_rec + 40, title_len as u8);
10741                            for (i, &ch) in item.text.as_bytes().iter().take(title_len).enumerate()
10742                            {
10743                                bus.write_byte(ctrl_rec + 41 + i as u32, ch);
10744                            }
10745                            // Map DITL item type to Control Manager procID
10746                            let proc_id: i16 = match base_type {
10747                                4 => 0, // btnCtrl → pushButProc
10748                                5 => 1, // chkCtrl → checkBoxProc
10749                                6 => 2, // radCtrl → radioButProc
10750                                _ => 0,
10751                            };
10752                            self.control_proc_ids.insert(ctrl_rec, proc_id);
10753                            let handle = bus.alloc(4);
10754                            bus.write_long(handle, ctrl_rec);
10755                            bus.write_long(item_handle_ptr, handle);
10756                            self.dialog_control_handles
10757                                .insert(handle, (dialog_ptr, item_no));
10758                            // Also update the DITL item handle storage
10759                            Self::set_dialog_item_handle(bus, dialog_ptr, item_no, handle);
10760                        }
10761                    }
10762                    if box_ptr != 0 {
10763                        bus.write_word(box_ptr, item.rect.0 as u16);
10764                        bus.write_word(box_ptr + 2, item.rect.1 as u16);
10765                        bus.write_word(box_ptr + 4, item.rect.2 as u16);
10766                        bus.write_word(box_ptr + 6, item.rect.3 as u16);
10767                    }
10768                    // Some apps use InsertMenu -> GetDItem -> SetDItem to
10769                    // build a custom popup control backed by a userItem. Do
10770                    // not promote on GetDItem alone: plain userItem grids also
10771                    // query their item rectangles and must not inherit a stale
10772                    // popup-menu association.
10773                    if let Some(menu_id) = self.last_inserted_menu_id.take() {
10774                        let enabled_user_item =
10775                            (item.item_type & 0x7F) == 0 && (item.item_type & 0x80) == 0;
10776                        if enabled_user_item {
10777                            if item.proc_ptr != 0 {
10778                                self.dialog_item_popup_menus
10779                                    .insert((dialog_ptr, item_no), menu_id);
10780                                self.dialog_popup_original_rects
10781                                    .insert((dialog_ptr, item_no), item.rect);
10782                                self.pending_dialog_popup_menu = None;
10783                            } else {
10784                                self.pending_dialog_popup_menu = Some(PendingDialogPopupMenu {
10785                                    dialog_ptr,
10786                                    item_no,
10787                                    menu_id,
10788                                    rect: item.rect,
10789                                });
10790                            }
10791                        } else {
10792                            self.pending_dialog_popup_menu = None;
10793                        }
10794                    }
10795                } else {
10796                    if type_ptr != 0 {
10797                        bus.write_word(type_ptr, 0);
10798                    }
10799                    if item_handle_ptr != 0 {
10800                        bus.write_long(item_handle_ptr, 0);
10801                    }
10802                    if box_ptr != 0 {
10803                        bus.write_long(box_ptr, 0);
10804                        bus.write_long(box_ptr + 4, 0);
10805                    }
10806                }
10807                cpu.write_reg(Register::A7, sp + 18);
10808                Ok(())
10809            }
10810
10811            // SetDItem ($A98E)
10812            // Sets information about a dialog item.
10813            // PROCEDURE SetDItem (theDialog: DialogPtr; itemNo: INTEGER;
10814            //     itemType: INTEGER; item: Handle; box: Rect);
10815            // Inside Macintosh Volume I, I-421
10816            //
10817            // Stack layout (box passed as pointer-to-Rect, not inline):
10818            //   SP+0..3:  &box   (pointer to Rect — MPW compiler passes large
10819            //                     value params by reference)
10820            //   SP+4..7:  item   (Handle / ProcPtr)
10821            //   SP+8..9:  itemType
10822            //   SP+10..11: itemNo
10823            //   SP+12..15: theDialog
10824            // SetDItem ($A98E): Stores item type, rect, and proc_ptr (for userItem); updates both dialog_items and active tracking state
10825            (true, 0x18E) => {
10826                let sp = cpu.read_reg(Register::A7);
10827                let box_ptr = bus.read_long(sp);
10828                let item_handle = bus.read_long(sp + 4);
10829                let item_type = bus.read_word(sp + 8) as u8;
10830                let item_no = bus.read_word(sp + 10) as i16;
10831                let dialog_ptr = bus.read_long(sp + 12);
10832                let (box_top, box_left, box_bottom, box_right) = if box_ptr != 0 {
10833                    (
10834                        bus.read_word(box_ptr) as i16,
10835                        bus.read_word(box_ptr + 2) as i16,
10836                        bus.read_word(box_ptr + 4) as i16,
10837                        bus.read_word(box_ptr + 6) as i16,
10838                    )
10839                } else {
10840                    (0, 0, 0, 0)
10841                };
10842
10843                let base_type = item_type & 0x7F;
10844                let previous_handle = Self::dialog_item_handle(bus, dialog_ptr, item_no);
10845                let previous_item = self
10846                    .dialog_items
10847                    .get(&dialog_ptr)
10848                    .and_then(|items| {
10849                        (item_no > 0)
10850                            .then(|| items.get((item_no - 1) as usize))
10851                            .flatten()
10852                    })
10853                    .cloned();
10854                if trace_dialog_items_enabled() {
10855                    eprintln!(
10856                        "[DIALOG-ITEM] SetDItem pc=${:08X} sp=${:08X} rawbytes={:02X?} dialog=${:08X} item={} type={} proc=${:08X} rect=({},{},{},{})",
10857                        cpu.read_reg(Register::PC),
10858                        sp,
10859                        (0..24u32).map(|i| bus.read_byte(sp + i)).collect::<Vec<u8>>(),
10860                        dialog_ptr,
10861                        item_no,
10862                        item_type,
10863                        item_handle,
10864                        box_top,
10865                        box_left,
10866                        box_bottom,
10867                        box_right,
10868                    );
10869                }
10870
10871                if previous_handle != 0 {
10872                    self.dialog_item_handles.remove(&previous_handle);
10873                    self.dialog_control_handles.remove(&previous_handle);
10874                }
10875                if let Some(item_handle_addr) =
10876                    Self::dialog_item_handle_addr(bus, dialog_ptr, item_no)
10877                {
10878                    bus.write_long(item_handle_addr, item_handle);
10879                    bus.write_word(item_handle_addr + 4, box_top as u16);
10880                    bus.write_word(item_handle_addr + 6, box_left as u16);
10881                    bus.write_word(item_handle_addr + 8, box_bottom as u16);
10882                    bus.write_word(item_handle_addr + 10, box_right as u16);
10883                    bus.write_byte(item_handle_addr + 12, item_type);
10884                }
10885                if let Some(items) = self.dialog_items.get_mut(&dialog_ptr) {
10886                    if item_no > 0 && (item_no as usize) <= items.len() {
10887                        let item = &mut items[(item_no - 1) as usize];
10888                        item.item_type = item_type;
10889                        item.rect = (box_top, box_left, box_bottom, box_right);
10890                        if base_type == 0 {
10891                            // userItem: the "item" parameter is a ProcPtr
10892                            // Inside Macintosh Volume I, I-405
10893                            item.proc_ptr = item_handle;
10894                        } else if base_type == 8 || base_type == 16 {
10895                            item.text = Self::text_item_string_from_handle(bus, item_handle);
10896                        }
10897                    }
10898                }
10899
10900                if let Some(pending) = self.pending_dialog_popup_menu {
10901                    if pending.dialog_ptr == dialog_ptr && pending.item_no == item_no {
10902                        if base_type == 0 && (item_type & 0x80) == 0 && item_handle != 0 {
10903                            self.dialog_item_popup_menus
10904                                .insert((dialog_ptr, item_no), pending.menu_id);
10905                            self.dialog_popup_original_rects
10906                                .insert((dialog_ptr, item_no), pending.rect);
10907                        }
10908                        self.pending_dialog_popup_menu = None;
10909                    }
10910                }
10911
10912                if let Some(previous_item) = previous_item {
10913                    let key = (dialog_ptr, item_no);
10914                    let previous_base_type = previous_item.item_type & 0x7F;
10915                    let previous_enabled_user_item =
10916                        previous_base_type == 0 && (previous_item.item_type & 0x80) == 0;
10917                    let current_enabled_user_item = base_type == 0 && (item_type & 0x80) == 0;
10918                    let old_width = previous_item.rect.3 - previous_item.rect.1;
10919                    let old_height = previous_item.rect.2 - previous_item.rect.0;
10920                    let new_width = box_right - box_left;
10921                    let new_height = box_bottom - box_top;
10922                    let narrowed_to_popup_indicator = previous_enabled_user_item
10923                        && current_enabled_user_item
10924                        && item_handle == 0
10925                        && old_width >= 40
10926                        && new_width > 0
10927                        && new_width <= 24
10928                        && old_height > 0
10929                        && new_height > 0
10930                        && (old_height - new_height).abs() <= 4;
10931                    if narrowed_to_popup_indicator {
10932                        self.dialog_popup_original_rects
10933                            .entry(key)
10934                            .or_insert(previous_item.rect);
10935                        self.dialog_popup_candidate_items.insert(key);
10936                    } else if !current_enabled_user_item {
10937                        self.dialog_popup_candidate_items.remove(&key);
10938                        if !self.dialog_item_popup_menus.contains_key(&key) {
10939                            self.dialog_popup_original_rects.remove(&key);
10940                        }
10941                    }
10942                }
10943
10944                if base_type == 8 || base_type == 16 {
10945                    if item_handle != 0 {
10946                        self.dialog_item_handles
10947                            .insert(item_handle, (dialog_ptr, (item_no - 1) as usize));
10948                    }
10949                } else if (base_type == 4 || base_type == 5 || base_type == 6 || base_type == 7)
10950                    && item_handle != 0
10951                {
10952                    self.dialog_control_handles
10953                        .insert(item_handle, (dialog_ptr, item_no));
10954                    let ctrl_ptr = bus.read_long(item_handle);
10955                    if ctrl_ptr != 0 {
10956                        bus.write_word(ctrl_ptr + 8, box_top as u16);
10957                        bus.write_word(ctrl_ptr + 10, box_left as u16);
10958                        bus.write_word(ctrl_ptr + 12, box_bottom as u16);
10959                        bus.write_word(ctrl_ptr + 14, box_right as u16);
10960                        self.dialog_control_values
10961                            .insert((dialog_ptr, item_no), bus.read_word(ctrl_ptr + 18) as i16);
10962                    }
10963                }
10964
10965                // Also update tracking state if dialog is currently active
10966                if let Some(ref mut tracking) = self.dialog_tracking {
10967                    if tracking.dialog_ptr == dialog_ptr
10968                        && item_no > 0
10969                        && (item_no as usize) <= tracking.items.len()
10970                    {
10971                        let item = &mut tracking.items[(item_no - 1) as usize];
10972                        item.item_type = item_type;
10973                        item.rect = (box_top, box_left, box_bottom, box_right);
10974                        if base_type == 0 {
10975                            item.proc_ptr = item_handle;
10976                        } else if base_type == 8 || base_type == 16 {
10977                            item.text = Self::text_item_string_from_handle(bus, item_handle);
10978                            if tracking.edit_item == item_no {
10979                                tracking.edit_text = item.text.clone();
10980                            }
10981                        }
10982                    }
10983                }
10984
10985                cpu.write_reg(Register::A7, sp + 16);
10986                Ok(())
10987            }
10988
10989            // ModalDialog ($A991)
10990            // Handles events in a modal dialog until an enabled item is hit.
10991            // PROCEDURE ModalDialog (filterProc: ProcPtr; VAR itemHit: INTEGER);
10992            // Inside Macintosh Volume I, I-415
10993            // ModalDialog ($A991): Re-fire pattern: draws dialog (including resCtrl/popup controls, type 7), handles button clicks, keyboard input (Return/Escape/text), button flash animation, userItem draw proc callbacks
10994            (true, 0x191) => {
10995                // Check if draw procs need to finish before entering event loop.
10996                if let Some(ref tracking) = self.dialog_tracking {
10997                    if !tracking.draw_procs_done {
10998                        return Some(Ok(()));
10999                    }
11000                }
11001                // Re-snapshot rendered_pixels after draw procs or filter proc completes.
11002                // rendered_pixels_final is cleared when either is injected; the snapshot
11003                // here captures whatever they drew before redraw_chrome can restore it.
11004                if let Some(ref tracking) = self.dialog_tracking {
11005                    if !tracking.rendered_pixels_final {
11006                        let bounds = tracking.bounds;
11007                        let items = tracking.items.clone();
11008                        let default_item = tracking.default_item;
11009                        let edit_text = tracking.edit_text.clone();
11010                        let edit_item = tracking.edit_item;
11011                        let dialog_ptr = tracking.dialog_ptr;
11012                        let popup_draws = tracking.popup_draws.clone();
11013                        if !tracking.game_managed {
11014                            if self.front_window == dialog_ptr {
11015                                self.blit_window_to_screen(bus);
11016                            }
11017                            self.redraw_standard_dialog_items(
11018                                bus,
11019                                bounds,
11020                                &items,
11021                                default_item,
11022                                &edit_text,
11023                                edit_item,
11024                                dialog_ptr,
11025                            );
11026                        }
11027                        self.redraw_dialog_popup_controls(bus, &popup_draws);
11028                        let rendered = self.save_dialog_pixels(bus, bounds);
11029                        let t = self.dialog_tracking.as_mut().unwrap();
11030                        t.rendered_pixels = rendered;
11031                        t.rendered_pixels_final = true;
11032                    }
11033                }
11034
11035                if let Some(ref tracking) = self.dialog_tracking {
11036                    if tracking.active_button.is_some() {
11037                        self.handle_dialog_button_tracking(bus);
11038                        return Some(Ok(()));
11039                    }
11040
11041                    if tracking.active_popup.is_some() {
11042                        self.handle_dialog_popup_tracking(cpu, bus);
11043                        return Some(Ok(()));
11044                    }
11045
11046                    if tracking.active_user_item.is_some() {
11047                        self.handle_dialog_user_item_tracking(cpu, bus);
11048                        return Some(Ok(()));
11049                    }
11050
11051                    // Fast path — when nothing can produce an item hit or visible
11052                    // update on this step (no filter proc, no flash animation, no
11053                    // pending event, no queued events), return Ok without running
11054                    // any of the re-fire body. Any of these flags being non-default
11055                    // routes through the full handler below.
11056                    if tracking.filter_proc == 0
11057                        && tracking.flash_remaining == 0
11058                        && tracking.active_button.is_none()
11059                        && tracking.active_popup.is_none()
11060                        && tracking.active_user_item.is_none()
11061                        && self.event_queue.is_empty()
11062                    {
11063                        return Some(Ok(()));
11064                    }
11065                    // Re-fire: dialog tracking is active
11066                    let dialog_ptr = tracking.dialog_ptr;
11067                    let bounds = tracking.bounds;
11068                    let item_hit_ptr = tracking.item_hit_ptr;
11069                    let stack_ptr = tracking.stack_ptr;
11070                    let filter_proc = tracking.filter_proc;
11071                    let flash_remaining = tracking.flash_remaining;
11072                    // Items_clone is built lazily in the mouseDown branch below to
11073                    // avoid cloning Vec<DialogItem> on every ModalDialog refire.
11074                    let mut pending_event = None;
11075
11076                    // For any dialog with a filter proc, check whether the filter
11077                    // handled the most recent event. Per Inside Macintosh Volume I, I-415:
11078                    // TRUE means the filter handled the event and set itemHit;
11079                    // FALSE means ModalDialog should process the event itself.
11080                    let mut waiting_for_filter_proc_event = false;
11081                    if filter_proc != 0 {
11082                        let result_addr = self.dialog_filter_result_addr;
11083                        let tracking_dialog_ptr = tracking.dialog_ptr;
11084                        let filter_result_word = if result_addr != 0 {
11085                            bus.read_word(result_addr)
11086                        } else {
11087                            0
11088                        };
11089                        let filter_returned_true = if result_addr != 0 {
11090                            // Stack-based Pascal BOOLEAN results are encoded
11091                            // in bit 0 of the high-order byte, not as any
11092                            // nonzero word. Inside Macintosh Volume I,
11093                            // "Using Assembly Language", stack-based routines.
11094                            (filter_result_word & 0x0100) != 0
11095                        } else {
11096                            false
11097                        };
11098
11099                        let mut hit = 0i16;
11100                        if filter_returned_true && item_hit_ptr != 0 {
11101                            hit = bus.read_word(item_hit_ptr) as i16;
11102                        }
11103                        let filter_event = self
11104                            .dialog_tracking
11105                            .as_mut()
11106                            .and_then(|t| t.last_filter_event.take());
11107                        if trace_dialog_filter_enabled() {
11108                            eprintln!(
11109                                "[DIALOG-FILTER] result dialog=${:08X} result_word=${:04X} returned_true={} item_hit={} item_hit_ptr=${:08X}",
11110                                tracking_dialog_ptr,
11111                                filter_result_word,
11112                                filter_returned_true,
11113                                hit,
11114                                item_hit_ptr
11115                            );
11116                        }
11117
11118                        if hit > 0 || filter_returned_true {
11119                            let handled_mouse_down =
11120                                filter_event.as_ref().is_some_and(|e| e.what == 1);
11121                            let (dialog_ptr, edit_item, edit_text, items) = {
11122                                let tracking = self.dialog_tracking.as_mut().unwrap();
11123                                Self::sync_tracking_active_edit_item(tracking);
11124                                (
11125                                    tracking.dialog_ptr,
11126                                    tracking.edit_item,
11127                                    tracking.edit_text.clone(),
11128                                    tracking.items.clone(),
11129                                )
11130                            };
11131                            let keep_dialog_visible = items
11132                                .get((hit - 1) as usize)
11133                                .map(|item| (item.item_type & 0x7F) != 4)
11134                                .unwrap_or(true);
11135                            let item_type =
11136                                items.get((hit - 1) as usize).map(|item| item.item_type);
11137                            self.flush_dialog_edit_item_texts(
11138                                bus, dialog_ptr, &items, edit_item, &edit_text,
11139                            );
11140                            // Filter handled the event — end tracking and return.
11141                            let saved = self.dialog_tracking.take().unwrap();
11142                            let saved_dialog_ptr = saved.dialog_ptr;
11143                            let saved_bounds = saved.bounds;
11144                            if keep_dialog_visible {
11145                                self.persist_visible_dialog_snapshot(bus, &saved);
11146                            }
11147                            self.dialog_saved_pixels
11148                                .insert(saved_dialog_ptr, saved.saved_pixels);
11149                            if hit > 0
11150                                && item_type.is_some_and(|ty| (ty & 0x7F) == 4 && (ty & 0x80) == 0)
11151                            {
11152                                self.pending_modal_button_dispose_dialog = Some(saved_dialog_ptr);
11153                            }
11154                            if handled_mouse_down {
11155                                self.consume_dialog_mouse_up();
11156                            }
11157                            if trace_dialog_filter_enabled() {
11158                                let actual_hit = bus.read_word(item_hit_ptr) as i16;
11159                                eprintln!(
11160                                    "[DIALOG-FILTER] ModalDialog returning: filter hit={} actual_item_hit_ptr=${:08X} actual_hit={} handled_mouseDown={} stack_ptr=${:08X} queue_len={}",
11161                                    hit, item_hit_ptr, actual_hit, handled_mouse_down, stack_ptr, self.event_queue.len()
11162                                );
11163                            }
11164                            let outcome = if hit > 0 {
11165                                if keep_dialog_visible {
11166                                    "filter_item_hit_retained"
11167                                } else {
11168                                    "filter_item_hit_dismissed"
11169                                }
11170                            } else {
11171                                "filter_true_zero_hit"
11172                            };
11173                            self.record_modal_dialog_filter_input_trace(
11174                                "filter_result",
11175                                saved_dialog_ptr,
11176                                saved_bounds,
11177                                filter_proc,
11178                                filter_event.as_ref(),
11179                                hit,
11180                                item_type,
11181                                handled_mouse_down,
11182                                keep_dialog_visible,
11183                                "returned",
11184                                outcome,
11185                            );
11186                            cpu.write_reg(Register::A7, stack_ptr + 8);
11187                            return Some(Ok(()));
11188                        }
11189                        pending_event = filter_event;
11190                        if let Some(ref event) = pending_event {
11191                            self.record_modal_dialog_filter_input_trace(
11192                                "filter_result",
11193                                tracking_dialog_ptr,
11194                                bounds,
11195                                filter_proc,
11196                                Some(event),
11197                                0,
11198                                None,
11199                                false,
11200                                true,
11201                                "passed",
11202                                "filter_declined",
11203                            );
11204                        }
11205                        waiting_for_filter_proc_event = pending_event.is_none();
11206                    }
11207
11208                    if flash_remaining > 0 {
11209                        // Button flash animation
11210                        let flash_item = self.dialog_tracking.as_ref().unwrap().flash_item;
11211                        let (remaining, button_draw) = {
11212                            let t = self.dialog_tracking.as_mut().unwrap();
11213                            if t.flash_delay > 0 {
11214                                t.flash_delay -= 1;
11215                                return Some(Ok(()));
11216                            }
11217                            t.flash_remaining -= 1;
11218                            t.flash_delay = 3;
11219                            let remaining = t.flash_remaining;
11220                            let button_draw = if remaining > 0
11221                                && flash_item > 0
11222                                && (flash_item as usize) <= t.items.len()
11223                            {
11224                                let item = &t.items[(flash_item - 1) as usize];
11225                                Some((
11226                                    Self::dialog_item_screen_rect(bounds, item.rect),
11227                                    item.text.clone(),
11228                                    flash_item == t.default_item,
11229                                    remaining % 2 == 0,
11230                                ))
11231                            } else {
11232                                None
11233                            };
11234                            (remaining, button_draw)
11235                        };
11236
11237                        if let Some((screen_rect, title, is_default, highlighted)) = button_draw {
11238                            // Toggle button highlight.
11239                            self.draw_dialog_button_highlight_state(
11240                                bus,
11241                                screen_rect,
11242                                &title,
11243                                is_default,
11244                                highlighted,
11245                            );
11246                        }
11247
11248                        if remaining == 0 {
11249                            // Flash complete — write all editText item handles
11250                            // back before returning the hit item to the app.
11251                            let (dialog_ptr, edit_item, edit_text, items) = {
11252                                let tracking = self.dialog_tracking.as_mut().unwrap();
11253                                Self::sync_tracking_active_edit_item(tracking);
11254                                (
11255                                    tracking.dialog_ptr,
11256                                    tracking.edit_item,
11257                                    tracking.edit_text.clone(),
11258                                    tracking.items.clone(),
11259                                )
11260                            };
11261                            self.flush_dialog_edit_item_texts(
11262                                bus, dialog_ptr, &items, edit_item, &edit_text,
11263                            );
11264
11265                            let saved = self.dialog_tracking.take().unwrap();
11266                            let saved_dialog_ptr = saved.dialog_ptr;
11267                            let saved_bounds = saved.bounds;
11268                            let item_type = saved
11269                                .items
11270                                .get(flash_item.saturating_sub(1) as usize)
11271                                .map(|item| item.item_type);
11272                            self.persist_visible_dialog_snapshot(bus, &saved);
11273                            self.dialog_saved_pixels
11274                                .insert(saved_dialog_ptr, saved.saved_pixels);
11275                            self.pending_modal_button_dispose_dialog = Some(saved_dialog_ptr);
11276
11277                            // ModalDialog button clicks should consume the
11278                            // matching mouseUp before returning to the app.
11279                            // If we leave it queued, the release can leak into
11280                            // the underlying game view after the dialog closes.
11281                            self.consume_dialog_mouse_up();
11282
11283                            if item_hit_ptr != 0 {
11284                                bus.write_word(item_hit_ptr, flash_item as u16);
11285                            }
11286                            self.record_modal_dialog_input_trace(
11287                                "finish",
11288                                saved_dialog_ptr,
11289                                saved_bounds,
11290                                flash_item,
11291                                item_type,
11292                                None,
11293                                "returned",
11294                                "button_item_hit",
11295                            );
11296                            cpu.write_reg(Register::A7, stack_ptr + 8);
11297                        }
11298                        return Some(Ok(()));
11299                    }
11300
11301                    // With a non-NIL filterProc, ModalDialog passes each event
11302                    // to the filter before Dialog Manager default handling.
11303                    // Leave queued events alone here; runner.rs will inject the
11304                    // filter callback, and this handler will process the returned
11305                    // event only if that callback returns FALSE. MTE 1992 6-136.
11306                    if waiting_for_filter_proc_event {
11307                        return Some(Ok(()));
11308                    }
11309
11310                    let event = if let Some(e) = pending_event {
11311                        Some(e)
11312                    } else if !self.event_queue.is_empty() {
11313                        // Drain events looking for actionable ones
11314                        let mut event = None;
11315                        while let Some(e) = self.event_queue.pop_front() {
11316                            match e.what {
11317                                1 | 2 | 3 | 6 => {
11318                                    event = Some(e);
11319                                    break;
11320                                }
11321                                _ => {} // discard other events
11322                            }
11323                        }
11324                        event
11325                    } else {
11326                        None
11327                    };
11328
11329                    if let Some(mut e) = event {
11330                        if e.what == 0 && self.mouse_button {
11331                            e.what = 1;
11332                            e.where_v = self.mouse_pos.0;
11333                            e.where_h = self.mouse_pos.1;
11334                        }
11335                        match e.what {
11336                            // updateEvt — re-snapshot rendered_pixels.
11337                            // Redraw HLE popup controls first, since the game
11338                            // may have drawn narrow indicator boxes that would
11339                            // taint the snapshot.
11340                            6 => {
11341                                // MTE 1992 pp. 6-135 and 6-141: ModalDialog
11342                                // handles dialog update events through the same
11343                                // DialogSelect path that brackets DrawDialog
11344                                // with BeginUpdate/EndUpdate and makes the
11345                                // dialog the current graphics port.
11346                                self.begin_update_window(bus, dialog_ptr);
11347                                self.set_current_port_state(bus, cpu, dialog_ptr, None);
11348                                let previous_rendered = self
11349                                    .dialog_tracking
11350                                    .as_ref()
11351                                    .filter(|t| t.rendered_pixels_final)
11352                                    .map(|t| t.rendered_pixels.clone())
11353                                    .unwrap_or_default();
11354                                if !previous_rendered.is_empty() {
11355                                    self.restore_dialog_pixels(bus, bounds, &previous_rendered);
11356                                }
11357                                if let Some(ref t) = self.dialog_tracking {
11358                                    if !t.game_managed {
11359                                        self.redraw_standard_dialog_items(
11360                                            bus,
11361                                            bounds,
11362                                            &t.items,
11363                                            t.default_item,
11364                                            &t.edit_text,
11365                                            t.edit_item,
11366                                            t.dialog_ptr,
11367                                        );
11368                                    }
11369                                    let popup_draws = t.popup_draws.clone();
11370                                    self.redraw_dialog_popup_controls(bus, &popup_draws);
11371                                }
11372                                let rendered = self.save_dialog_pixels(bus, bounds);
11373                                let t = self.dialog_tracking.as_mut().unwrap();
11374                                t.rendered_pixels = rendered;
11375                                t.rendered_pixels_final = true;
11376                                self.end_update_window(bus, dialog_ptr);
11377                            }
11378                            // mouseDown
11379                            1 => {
11380                                // Clone items lazily — only when actually processing
11381                                // a mouseDown. Hot path (no events) skips this entirely.
11382                                let items_clone: Vec<DialogItem> = self
11383                                    .dialog_tracking
11384                                    .as_ref()
11385                                    .map(|t| t.items.clone())
11386                                    .unwrap_or_default();
11387                                let mut hit = self.dialog_item_hit_test(
11388                                    bus,
11389                                    &items_clone,
11390                                    bounds,
11391                                    e.where_v,
11392                                    e.where_h,
11393                                    &self.dialog_popup_original_rects,
11394                                    dialog_ptr,
11395                                );
11396                                if hit <= 0 {
11397                                    hit = Self::dialog_button_hit_test(
11398                                        &items_clone,
11399                                        bounds,
11400                                        e.where_v,
11401                                        e.where_h,
11402                                    );
11403                                }
11404                                if hit > 0 {
11405                                    let item = &items_clone[(hit - 1) as usize];
11406                                    let base_type = item.item_type & 0x7F;
11407                                    let is_disabled = (item.item_type & 0x80) != 0;
11408
11409                                    if !is_disabled {
11410                                        match base_type {
11411                                            // Button click: start flash
11412                                            4 => {
11413                                                let (abs_top, abs_left, abs_bottom, abs_right) =
11414                                                    Self::dialog_item_screen_rect(
11415                                                        bounds, item.rect,
11416                                                    );
11417                                                let is_default = hit
11418                                                    == self
11419                                                        .dialog_tracking
11420                                                        .as_ref()
11421                                                        .map(|tracking| tracking.default_item)
11422                                                        .unwrap_or(0);
11423                                                self.draw_dialog_button_highlight_state(
11424                                                    bus,
11425                                                    (abs_top, abs_left, abs_bottom, abs_right),
11426                                                    &item.text,
11427                                                    is_default,
11428                                                    true,
11429                                                );
11430                                                if self.mouse_button {
11431                                                    let t = self.dialog_tracking.as_mut().unwrap();
11432                                                    t.active_button = Some(
11433                                                        super::dispatch::DialogButtonTrackingState {
11434                                                            item_no: hit,
11435                                                            rect: item.rect,
11436                                                            title: item.text.clone(),
11437                                                            is_default,
11438                                                            highlighted: true,
11439                                                        },
11440                                                    );
11441                                                    self.record_modal_dialog_input_trace(
11442                                                        "mouse_down",
11443                                                        dialog_ptr,
11444                                                        bounds,
11445                                                        hit,
11446                                                        Some(item.item_type),
11447                                                        Some(true),
11448                                                        "pending",
11449                                                        "button_tracking_started",
11450                                                    );
11451                                                } else {
11452                                                    self.start_dialog_button_flash(
11453                                                        bus, bounds, hit, item.rect, &item.text,
11454                                                        is_default, true,
11455                                                    );
11456                                                    self.record_modal_dialog_input_trace(
11457                                                        "mouse_down",
11458                                                        dialog_ptr,
11459                                                        bounds,
11460                                                        hit,
11461                                                        Some(item.item_type),
11462                                                        Some(true),
11463                                                        "pending",
11464                                                        "start_flash",
11465                                                    );
11466                                                }
11467                                            }
11468                                            // Checkbox click: return item number immediately
11469                                            // The dialog stays on screen — the app toggles
11470                                            // the checkbox value and calls ModalDialog again.
11471                                            // Inside Macintosh Volume I, I-415
11472                                            5 => {
11473                                                let (dlg_ptr, edit_item, edit_text, items) = {
11474                                                    let tracking =
11475                                                        self.dialog_tracking.as_mut().unwrap();
11476                                                    Self::sync_tracking_active_edit_item(tracking);
11477                                                    (
11478                                                        tracking.dialog_ptr,
11479                                                        tracking.edit_item,
11480                                                        tracking.edit_text.clone(),
11481                                                        tracking.items.clone(),
11482                                                    )
11483                                                };
11484                                                self.flush_dialog_edit_item_texts(
11485                                                    bus, dlg_ptr, &items, edit_item, &edit_text,
11486                                                );
11487                                                // Preserve saved background pixels for re-entry
11488                                                let saved = self.dialog_tracking.take().unwrap();
11489                                                let saved_dialog_ptr = saved.dialog_ptr;
11490                                                let saved_bounds = saved.bounds;
11491                                                self.persist_visible_dialog_snapshot(bus, &saved);
11492                                                self.dialog_saved_pixels
11493                                                    .insert(saved_dialog_ptr, saved.saved_pixels);
11494                                                self.consume_dialog_mouse_up();
11495                                                // Don't restore pixels — dialog stays visible
11496                                                if item_hit_ptr != 0 {
11497                                                    bus.write_word(item_hit_ptr, hit as u16);
11498                                                }
11499                                                self.record_modal_dialog_input_trace(
11500                                                    "mouse_down",
11501                                                    saved_dialog_ptr,
11502                                                    saved_bounds,
11503                                                    hit,
11504                                                    Some(item.item_type),
11505                                                    None,
11506                                                    "returned",
11507                                                    "checkbox_item_hit_retained",
11508                                                );
11509                                                cpu.write_reg(Register::A7, stack_ptr + 8);
11510                                            }
11511                                            // EditText click: set as active
11512                                            16 => {
11513                                                let (dlg_ptr, edit_item, edit_text, items) = {
11514                                                    let tracking =
11515                                                        self.dialog_tracking.as_mut().unwrap();
11516                                                    Self::sync_tracking_active_edit_item(tracking);
11517                                                    tracking.edit_item = hit;
11518                                                    tracking.edit_text = tracking
11519                                                        .items
11520                                                        .get((hit - 1) as usize)
11521                                                        .map(|item| item.text.clone())
11522                                                        .unwrap_or_default();
11523                                                    tracking.edit_text_modified = false;
11524                                                    (
11525                                                        tracking.dialog_ptr,
11526                                                        tracking.edit_item,
11527                                                        tracking.edit_text.clone(),
11528                                                        tracking.items.clone(),
11529                                                    )
11530                                                };
11531                                                // IM:I I-415: mouseDown in an enabled editText
11532                                                // item is TextEdit-handled and ModalDialog
11533                                                // returns that item. TEClick's pixel-to-caret
11534                                                // mapping remains the documented HLE compromise,
11535                                                // but the active editField/TERecord mirror is
11536                                                // still guest-visible Dialog Manager state.
11537                                                self.activate_dialog_edit_item(
11538                                                    bus, cpu, dlg_ptr, &items, edit_item,
11539                                                );
11540                                                self.flush_dialog_edit_item_texts(
11541                                                    bus, dlg_ptr, &items, edit_item, &edit_text,
11542                                                );
11543                                                let saved = self.dialog_tracking.take().unwrap();
11544                                                let saved_dialog_ptr = saved.dialog_ptr;
11545                                                let saved_bounds = saved.bounds;
11546                                                self.persist_visible_dialog_snapshot(bus, &saved);
11547                                                self.dialog_saved_pixels
11548                                                    .insert(saved_dialog_ptr, saved.saved_pixels);
11549                                                self.consume_dialog_mouse_up();
11550                                                if item_hit_ptr != 0 {
11551                                                    bus.write_word(item_hit_ptr, hit as u16);
11552                                                }
11553                                                self.record_modal_dialog_input_trace(
11554                                                    "mouse_down",
11555                                                    saved_dialog_ptr,
11556                                                    saved_bounds,
11557                                                    hit,
11558                                                    Some(item.item_type),
11559                                                    None,
11560                                                    "returned",
11561                                                    "edittext_item_hit_retained",
11562                                                );
11563                                                cpu.write_reg(Register::A7, stack_ptr + 8);
11564                                            }
11565                                            // resCtrl popup controls are tracked by
11566                                            // ModalDialog itself, matching the standard
11567                                            // Dialog Manager control-item path.
11568                                            7 if self.begin_dialog_popup_tracking(
11569                                                bus, dialog_ptr, hit,
11570                                            ) => {}
11571                                            // UserItem with popup menu: return item number.
11572                                            // Dialog stays on screen — the app calls
11573                                            // PopUpMenuSelect to show the popup dropdown.
11574                                            // Macintosh Toolbox Essentials 1992, 5-26.
11575                                            // Original Marathon-style compact popup candidates
11576                                            // use the same app-owned tracking path after
11577                                            // InsertMenu/GetDItem/SetDItem narrows the DITL rect.
11578                                            0 if self
11579                                                .dialog_item_popup_menus
11580                                                .contains_key(&(dialog_ptr, hit))
11581                                                || self
11582                                                    .dialog_popup_candidate_items
11583                                                    .contains(&(dialog_ptr, hit)) =>
11584                                            {
11585                                                let (dlg_ptr, edit_item, edit_text, items) = {
11586                                                    let tracking =
11587                                                        self.dialog_tracking.as_mut().unwrap();
11588                                                    Self::sync_tracking_active_edit_item(tracking);
11589                                                    (
11590                                                        tracking.dialog_ptr,
11591                                                        tracking.edit_item,
11592                                                        tracking.edit_text.clone(),
11593                                                        tracking.items.clone(),
11594                                                    )
11595                                                };
11596                                                self.flush_dialog_edit_item_texts(
11597                                                    bus, dlg_ptr, &items, edit_item, &edit_text,
11598                                                );
11599                                                let saved = self.dialog_tracking.take().unwrap();
11600                                                self.persist_visible_dialog_snapshot(bus, &saved);
11601                                                self.dialog_saved_pixels
11602                                                    .insert(saved.dialog_ptr, saved.saved_pixels);
11603                                                self.consume_dialog_mouse_up();
11604                                                if item_hit_ptr != 0 {
11605                                                    bus.write_word(item_hit_ptr, hit as u16);
11606                                                }
11607                                                cpu.write_reg(Register::A7, stack_ptr + 8);
11608                                            }
11609                                            // Plain userItems in standard dialogs are custom
11610                                            // hit targets whose content and mouse tracking are
11611                                            // application-owned (IM:I I-405). Some applications
11612                                            // implement draggable custom controls (e.g. EV
11613                                            // Override's Game Speed slider) by running their own
11614                                            // StillDown()/GetMouse() tracking loop after
11615                                            // ModalDialog returns the item on the initial press,
11616                                            // so the hit must be returned while the button is
11617                                            // still down — holding it until release leaves the
11618                                            // application's tracking loop with nothing to follow.
11619                                            // Return the hit immediately on mouse-down; the
11620                                            // pending mouse-up stays queued so StillDown() and the
11621                                            // application loop still observe the release.
11622                                            0 if self.dialog_tracking.as_ref().is_some_and(
11623                                                |tracking| {
11624                                                    self.is_plain_modal_user_item(
11625                                                        tracking, hit, item,
11626                                                    )
11627                                                },
11628                                            ) && self.mouse_button =>
11629                                            {
11630                                                let (dlg_ptr, edit_item, edit_text, items) = {
11631                                                    let tracking =
11632                                                        self.dialog_tracking.as_mut().unwrap();
11633                                                    Self::sync_tracking_active_edit_item(tracking);
11634                                                    (
11635                                                        tracking.dialog_ptr,
11636                                                        tracking.edit_item,
11637                                                        tracking.edit_text.clone(),
11638                                                        tracking.items.clone(),
11639                                                    )
11640                                                };
11641                                                self.flush_dialog_edit_item_texts(
11642                                                    bus, dlg_ptr, &items, edit_item, &edit_text,
11643                                                );
11644                                                let saved = self.dialog_tracking.take().unwrap();
11645                                                self.persist_visible_dialog_snapshot(bus, &saved);
11646                                                self.dialog_saved_pixels
11647                                                    .insert(saved.dialog_ptr, saved.saved_pixels);
11648                                                if item_hit_ptr != 0 {
11649                                                    bus.write_word(item_hit_ptr, hit as u16);
11650                                                }
11651                                                cpu.write_reg(Register::A7, stack_ptr + 8);
11652                                            }
11653                                            // Any other enabled item: return item number immediately.
11654                                            // Inside Macintosh Volume I, I-428
11655                                            _ => {
11656                                                let (dlg_ptr, edit_item, edit_text, items) = {
11657                                                    let tracking =
11658                                                        self.dialog_tracking.as_mut().unwrap();
11659                                                    Self::sync_tracking_active_edit_item(tracking);
11660                                                    (
11661                                                        tracking.dialog_ptr,
11662                                                        tracking.edit_item,
11663                                                        tracking.edit_text.clone(),
11664                                                        tracking.items.clone(),
11665                                                    )
11666                                                };
11667                                                self.flush_dialog_edit_item_texts(
11668                                                    bus, dlg_ptr, &items, edit_item, &edit_text,
11669                                                );
11670                                                let saved = self.dialog_tracking.take().unwrap();
11671                                                self.persist_visible_dialog_snapshot(bus, &saved);
11672                                                self.dialog_saved_pixels
11673                                                    .insert(saved.dialog_ptr, saved.saved_pixels);
11674                                                self.consume_dialog_mouse_up();
11675                                                if item_hit_ptr != 0 {
11676                                                    bus.write_word(item_hit_ptr, hit as u16);
11677                                                }
11678                                                cpu.write_reg(Register::A7, stack_ptr + 8);
11679                                            }
11680                                        }
11681                                    }
11682                                }
11683                            }
11684                            // keyDown
11685                            3 => {
11686                                let char_code = (e.message & 0xFF) as u8;
11687                                let key_code = ((e.message >> 8) & 0xFF) as u8;
11688                                let command_period =
11689                                    char_code == b'.' && (e.modifiers & 0x0100) != 0;
11690                                let command_select_all = char_code.eq_ignore_ascii_case(&b'a')
11691                                    && (e.modifiers & 0x0100) != 0;
11692                                match char_code {
11693                                    // Return or Enter: trigger default button
11694                                    0x0D | 0x03 => {
11695                                        let target =
11696                                            self.dialog_tracking.as_ref().and_then(|tracking| {
11697                                                let def = tracking.default_item;
11698                                                if def <= 0 {
11699                                                    return None;
11700                                                }
11701                                                tracking
11702                                                    .items
11703                                                    .get(def.saturating_sub(1) as usize)
11704                                                    .map(|item| {
11705                                                        (def, item.rect, item.text.clone(), true)
11706                                                    })
11707                                            });
11708                                        if let Some((def, rect, title, is_default)) = target {
11709                                            let screen_rect =
11710                                                Self::dialog_item_screen_rect(bounds, rect);
11711                                            self.draw_dialog_button_highlight_state(
11712                                                bus,
11713                                                screen_rect,
11714                                                &title,
11715                                                is_default,
11716                                                true,
11717                                            );
11718                                            let t = self.dialog_tracking.as_mut().unwrap();
11719                                            t.flash_remaining = 6;
11720                                            t.flash_delay = 3;
11721                                            t.flash_item = def;
11722                                        }
11723                                    }
11724                                    // Escape or Command-period: trigger cancel button.
11725                                    // MTE 1992 p. 6-138 maps Esc and Command-period
11726                                    // to Cancel before dialog-select style handling.
11727                                    0x1B | b'.' if char_code == 0x1B || command_period => {
11728                                        let target =
11729                                            self.dialog_tracking.as_ref().and_then(|tracking| {
11730                                                let cancel = tracking.cancel_item;
11731                                                if cancel <= 0 {
11732                                                    return None;
11733                                                }
11734                                                tracking
11735                                                    .items
11736                                                    .get(cancel.saturating_sub(1) as usize)
11737                                                    .map(|item| {
11738                                                        (
11739                                                            cancel,
11740                                                            item.rect,
11741                                                            item.text.clone(),
11742                                                            cancel == tracking.default_item,
11743                                                        )
11744                                                    })
11745                                            });
11746                                        if let Some((cancel, rect, title, is_default)) = target {
11747                                            let screen_rect =
11748                                                Self::dialog_item_screen_rect(bounds, rect);
11749                                            self.draw_dialog_button_highlight_state(
11750                                                bus,
11751                                                screen_rect,
11752                                                &title,
11753                                                is_default,
11754                                                true,
11755                                            );
11756                                            let t = self.dialog_tracking.as_mut().unwrap();
11757                                            t.flash_remaining = 6;
11758                                            t.flash_delay = 3;
11759                                            t.flash_item = cancel;
11760                                        }
11761                                    }
11762                                    // Tab: move to the next editText item, wrapping.
11763                                    0x09 => {
11764                                        let mut switched = false;
11765                                        if let Some(tracking) = self.dialog_tracking.as_mut() {
11766                                            if tracking.edit_item > 0 {
11767                                                Self::sync_tracking_active_edit_item(tracking);
11768                                            }
11769                                            if !tracking.items.is_empty() {
11770                                                let start = tracking.edit_item.max(0) as usize;
11771                                                let next =
11772                                                    (0..tracking.items.len()).find_map(|offset| {
11773                                                        let idx =
11774                                                            (start + offset) % tracking.items.len();
11775                                                        if (tracking.items[idx].item_type & 0x7F)
11776                                                            == 16
11777                                                        {
11778                                                            Some(idx)
11779                                                        } else {
11780                                                            None
11781                                                        }
11782                                                    });
11783                                                if let Some(idx) = next {
11784                                                    tracking.edit_item = (idx + 1) as i16;
11785                                                    tracking.edit_text =
11786                                                        tracking.items[idx].text.clone();
11787                                                    tracking.edit_text_modified = false;
11788                                                    bus.write_word(dialog_ptr + 164, idx as u16);
11789                                                    switched = true;
11790                                                }
11791                                            }
11792                                        }
11793                                        if switched {
11794                                            self.refresh_dialog_tracking_snapshot(bus);
11795                                        }
11796                                    }
11797                                    // Backspace/Delete or printable ASCII.
11798                                    _ if command_select_all => {
11799                                        let mut select_trace = None;
11800                                        let mut modified_key_to_clear = None;
11801                                        if let Some(tracking) = self.dialog_tracking.as_mut() {
11802                                            let text_before = tracking.edit_text.clone();
11803                                            if tracking.edit_item > 0 {
11804                                                let text_len = tracking.edit_text.len();
11805                                                tracking.edit_text_modified = false;
11806                                                Self::set_tracking_active_edit_selection(
11807                                                    tracking, 0, text_len,
11808                                                );
11809                                                Self::sync_tracking_active_edit_item(tracking);
11810
11811                                                let edit_item = tracking.edit_item;
11812                                                let item_type = tracking
11813                                                    .items
11814                                                    .get((edit_item - 1) as usize)
11815                                                    .map(|item| item.item_type);
11816                                                let enabled_edit_text = item_type
11817                                                    .map(|ty| (ty & 0x7F) == 16 && (ty & 0x80) == 0)
11818                                                    .unwrap_or(false);
11819                                                let text_after = tracking.edit_text.clone();
11820                                                select_trace = Some((
11821                                                    edit_item,
11822                                                    item_type,
11823                                                    text_before,
11824                                                    text_after,
11825                                                    enabled_edit_text,
11826                                                ));
11827                                                modified_key_to_clear =
11828                                                    Some((tracking.dialog_ptr, edit_item));
11829                                            }
11830                                        }
11831                                        if let Some(key) = modified_key_to_clear {
11832                                            self.dialog_edit_text_modified_items.remove(&key);
11833                                        }
11834                                        if let Some((
11835                                            edit_item,
11836                                            item_type,
11837                                            text_before,
11838                                            text_after,
11839                                            enabled_edit_text,
11840                                        )) = select_trace
11841                                        {
11842                                            self.refresh_dialog_tracking_snapshot(bus);
11843                                            let outcome = if enabled_edit_text {
11844                                                "enabled_edittext_select_all"
11845                                            } else {
11846                                                "edittext_select_all"
11847                                            };
11848                                            if enabled_edit_text {
11849                                                let (dlg_ptr, active_edit_item, edit_text, items) = {
11850                                                    let tracking =
11851                                                        self.dialog_tracking.as_mut().unwrap();
11852                                                    Self::sync_tracking_active_edit_item(tracking);
11853                                                    (
11854                                                        tracking.dialog_ptr,
11855                                                        tracking.edit_item,
11856                                                        tracking.edit_text.clone(),
11857                                                        tracking.items.clone(),
11858                                                    )
11859                                                };
11860                                                self.flush_dialog_edit_item_texts(
11861                                                    bus,
11862                                                    dlg_ptr,
11863                                                    &items,
11864                                                    active_edit_item,
11865                                                    &edit_text,
11866                                                );
11867                                                let saved = self.dialog_tracking.take().unwrap();
11868                                                let saved_dialog_ptr = saved.dialog_ptr;
11869                                                self.persist_visible_dialog_snapshot(bus, &saved);
11870                                                self.dialog_saved_pixels
11871                                                    .insert(saved_dialog_ptr, saved.saved_pixels);
11872                                                if item_hit_ptr != 0 {
11873                                                    bus.write_word(
11874                                                        item_hit_ptr,
11875                                                        active_edit_item as u16,
11876                                                    );
11877                                                }
11878                                                cpu.write_reg(Register::A7, stack_ptr + 8);
11879                                                self.record_modal_dialog_text_input_trace(
11880                                                    "key_down",
11881                                                    dialog_ptr,
11882                                                    bounds,
11883                                                    edit_item,
11884                                                    item_type,
11885                                                    key_code,
11886                                                    char_code,
11887                                                    &text_before,
11888                                                    &text_after,
11889                                                    "returned",
11890                                                    outcome,
11891                                                );
11892                                            } else {
11893                                                self.record_modal_dialog_text_input_trace(
11894                                                    "key_down",
11895                                                    dialog_ptr,
11896                                                    bounds,
11897                                                    edit_item,
11898                                                    item_type,
11899                                                    key_code,
11900                                                    char_code,
11901                                                    &text_before,
11902                                                    &text_after,
11903                                                    "pending",
11904                                                    outcome,
11905                                                );
11906                                            }
11907                                        }
11908                                    }
11909                                    0x08 | 0x20..=0x7E => {
11910                                        let mut text_trace = None;
11911                                        let mut modified_key_to_set = None;
11912                                        if let Some(tracking) = self.dialog_tracking.as_mut() {
11913                                            let text_before = tracking.edit_text.clone();
11914                                            if tracking.edit_item > 0 {
11915                                                if char_code == 0x08 {
11916                                                    if !tracking.edit_text_modified {
11917                                                        // First backspace clears selection
11918                                                        tracking.edit_text.clear();
11919                                                        tracking.edit_text_modified = true;
11920                                                    } else if !tracking.edit_text.is_empty() {
11921                                                        tracking.edit_text.pop();
11922                                                    }
11923                                                } else {
11924                                                    if !tracking.edit_text_modified {
11925                                                        // First keypress replaces selection
11926                                                        tracking.edit_text.clear();
11927                                                        tracking.edit_text_modified = true;
11928                                                    }
11929                                                    tracking.edit_text.push(char_code as char);
11930                                                }
11931                                                let cursor = tracking.edit_text.len();
11932                                                Self::set_tracking_active_edit_selection(
11933                                                    tracking, cursor, cursor,
11934                                                );
11935                                                Self::sync_tracking_active_edit_item(tracking);
11936                                                modified_key_to_set =
11937                                                    Some((tracking.dialog_ptr, tracking.edit_item));
11938
11939                                                let edit_item = tracking.edit_item;
11940                                                let item_type = tracking
11941                                                    .items
11942                                                    .get((edit_item - 1) as usize)
11943                                                    .map(|item| item.item_type);
11944                                                let enabled_edit_text = item_type
11945                                                    .map(|ty| (ty & 0x7F) == 16 && (ty & 0x80) == 0)
11946                                                    .unwrap_or(false);
11947                                                let text_after = tracking.edit_text.clone();
11948                                                text_trace = Some((
11949                                                    edit_item,
11950                                                    item_type,
11951                                                    text_before,
11952                                                    text_after,
11953                                                    enabled_edit_text,
11954                                                ));
11955                                            }
11956                                        }
11957                                        if let Some(key) = modified_key_to_set {
11958                                            self.dialog_edit_text_modified_items.insert(key);
11959                                        }
11960                                        if let Some((
11961                                            edit_item,
11962                                            item_type,
11963                                            text_before,
11964                                            text_after,
11965                                            enabled_edit_text,
11966                                        )) = text_trace
11967                                        {
11968                                            self.refresh_dialog_tracking_snapshot(bus);
11969                                            let outcome = if enabled_edit_text {
11970                                                "enabled_edittext_item_hit"
11971                                            } else {
11972                                                "edittext_updated"
11973                                            };
11974                                            if enabled_edit_text {
11975                                                let (dlg_ptr, active_edit_item, edit_text, items) = {
11976                                                    let tracking =
11977                                                        self.dialog_tracking.as_mut().unwrap();
11978                                                    Self::sync_tracking_active_edit_item(tracking);
11979                                                    (
11980                                                        tracking.dialog_ptr,
11981                                                        tracking.edit_item,
11982                                                        tracking.edit_text.clone(),
11983                                                        tracking.items.clone(),
11984                                                    )
11985                                                };
11986                                                self.flush_dialog_edit_item_texts(
11987                                                    bus,
11988                                                    dlg_ptr,
11989                                                    &items,
11990                                                    active_edit_item,
11991                                                    &edit_text,
11992                                                );
11993                                                let saved = self.dialog_tracking.take().unwrap();
11994                                                let saved_dialog_ptr = saved.dialog_ptr;
11995                                                self.persist_visible_dialog_snapshot(bus, &saved);
11996                                                self.dialog_saved_pixels
11997                                                    .insert(saved_dialog_ptr, saved.saved_pixels);
11998                                                if item_hit_ptr != 0 {
11999                                                    bus.write_word(
12000                                                        item_hit_ptr,
12001                                                        active_edit_item as u16,
12002                                                    );
12003                                                }
12004                                                cpu.write_reg(Register::A7, stack_ptr + 8);
12005                                                self.record_modal_dialog_text_input_trace(
12006                                                    "key_down",
12007                                                    dialog_ptr,
12008                                                    bounds,
12009                                                    edit_item,
12010                                                    item_type,
12011                                                    key_code,
12012                                                    char_code,
12013                                                    &text_before,
12014                                                    &text_after,
12015                                                    "returned",
12016                                                    outcome,
12017                                                );
12018                                            } else {
12019                                                self.record_modal_dialog_text_input_trace(
12020                                                    "key_down",
12021                                                    dialog_ptr,
12022                                                    bounds,
12023                                                    edit_item,
12024                                                    item_type,
12025                                                    key_code,
12026                                                    char_code,
12027                                                    &text_before,
12028                                                    &text_after,
12029                                                    "pending",
12030                                                    outcome,
12031                                                );
12032                                            }
12033                                        }
12034                                    }
12035                                    _ => {}
12036                                }
12037                            }
12038                            _ => {}
12039                        }
12040                    }
12041                } else {
12042                    // First call: initialize dialog tracking
12043                    let sp = cpu.read_reg(Register::A7);
12044                    let item_hit_ptr = bus.read_long(sp);
12045                    let filter_proc = bus.read_long(sp + 4);
12046                    if item_hit_ptr != 0 {
12047                        bus.write_word(item_hit_ptr, 0);
12048                    }
12049
12050                    // Find the modal dialog's items. Most callers keep the
12051                    // dialog as the front window, but games can temporarily
12052                    // select another port/window between ModalDialog returns
12053                    // and re-entry (for example while tracking a popup menu).
12054                    // A real modal dialog remains the Dialog Manager target
12055                    // until DisposDialog, so prefer the retained visible
12056                    // modal snapshot when the current front window is not a
12057                    // known dialog.
12058                    let mut dialog_ptr = self.front_window;
12059                    if !self.dialog_items.contains_key(&dialog_ptr) {
12060                        if let Some((&retained_dialog_ptr, snapshot)) =
12061                            self.dialog_visible_snapshots.iter().next()
12062                        {
12063                            dialog_ptr = retained_dialog_ptr;
12064                            self.front_window = retained_dialog_ptr;
12065                            self.window_bounds = snapshot.bounds;
12066                            self.window_proc_id = self
12067                                .window_proc_ids
12068                                .get(&retained_dialog_ptr)
12069                                .copied()
12070                                .unwrap_or(self.window_proc_id);
12071                            self.window_title.clear();
12072                        }
12073                    }
12074                    if let Some(mut items) = self.dialog_items.get(&dialog_ptr).cloned() {
12075                        if trace_dialog_filter_enabled() {
12076                            eprintln!(
12077                                "[DIALOG-FILTER] init dialog=${:08X} items={} filter_proc=${:08X} item_hit_ptr=${:08X}",
12078                                dialog_ptr,
12079                                items.len(),
12080                                filter_proc,
12081                                item_hit_ptr
12082                            );
12083                        }
12084                        // Re-read userItem proc pointers from guest memory.
12085                        // The game may have written them directly to the DITL
12086                        // handle data after GetNewDialog returned.
12087                        Self::refresh_ditl_proc_ptrs(bus, dialog_ptr, &mut items);
12088                        let bounds = self.window_bounds;
12089                        let proc_id = self.window_proc_id;
12090                        let title = self.window_title.clone();
12091
12092                        let (edit_text, edit_item, default_item) =
12093                            Self::dialog_edit_state(bus, dialog_ptr, &items);
12094                        let cancel_item = self
12095                            .dialog_cancel_items
12096                            .get(&dialog_ptr)
12097                            .copied()
12098                            .unwrap_or(2);
12099                        let edit_text_modified = edit_item > 0
12100                            && self
12101                                .dialog_edit_text_modified_items
12102                                .contains(&(dialog_ptr, edit_item));
12103
12104                        // If in-bounds items are all userItems, the game
12105                        // manages drawing itself; offscreen placeholders do
12106                        // not make this a standard dialog.
12107                        let game_managed = Self::dialog_is_game_managed(bounds, &items);
12108
12109                        if trace_dialog_procs_enabled() {
12110                            for (i, item) in items.iter().enumerate() {
12111                                eprintln!(
12112                                    "[DIALOG-PROC] dialog=${:08X} item={} type={} proc=${:08X} rect=({},{},{},{}) text={:?}",
12113                                    dialog_ptr, i + 1, item.item_type, item.proc_ptr,
12114                                    item.rect.0, item.rect.1, item.rect.2, item.rect.3,
12115                                    item.text,
12116                                );
12117                                if (item.item_type & 0x7F) == 0 {
12118                                    eprintln!(
12119                                        "[DIALOG-PROC] dialog=${:08X} item={} type={} proc=${:08X}",
12120                                        dialog_ptr,
12121                                        i + 1,
12122                                        item.item_type,
12123                                        item.proc_ptr,
12124                                    );
12125                                }
12126                            }
12127                        }
12128
12129                        // Save pixels under dialog area (background to restore on dismiss).
12130                        // If we have preserved pixels from a previous non-dismissing return
12131                        // (e.g., popup click), reuse those instead of capturing the
12132                        // currently visible dialog as "background."
12133                        let is_reentry = self.dialog_modal_entered.contains(&dialog_ptr);
12134                        let preserved_visible_snapshot =
12135                            self.dialog_visible_snapshots.remove(&dialog_ptr);
12136                        let reused_retained_visible_snapshot =
12137                            is_reentry && preserved_visible_snapshot.is_some();
12138                        let preserved_saved_pixels =
12139                            self.dialog_saved_pixels.get(&dialog_ptr).cloned();
12140                        let restored_visible_snapshot = preserved_visible_snapshot.is_some();
12141                        let saved_pixels = preserved_saved_pixels
12142                            .unwrap_or_else(|| self.save_dialog_pixels(bus, bounds));
12143                        if let Some(snapshot) = preserved_visible_snapshot {
12144                            self.restore_dialog_pixels(bus, snapshot.bounds, &snapshot.pixels);
12145                        }
12146                        if !game_managed && !is_reentry {
12147                            // First entry: draw the dialog chrome and controls.
12148                            // Before draw_dialog fills the dialog area white, save the
12149                            // pixel content of every userItem rect when those pixels
12150                            // come from a visible-dialog snapshot. Games (e.g.
12151                            // Marathon) often draw custom controls (popup buttons,
12152                            // sliders) into userItem rects via QuickDraw before
12153                            // calling ModalDialog. Saved-under background pixels are
12154                            // only for dismissal restore and must not be treated as
12155                            // application-owned userItem drawing.
12156                            // Inside Macintosh Volume I, I-405
12157                            self.dialog_initial_draw_deferred.remove(&dialog_ptr);
12158                            self.draw_dialog_preserving_user_items(
12159                                bus,
12160                                bounds,
12161                                proc_id,
12162                                &title,
12163                                &items,
12164                                default_item,
12165                                &edit_text,
12166                                edit_item,
12167                                false,
12168                                dialog_ptr,
12169                                restored_visible_snapshot,
12170                                true,
12171                                true,
12172                            );
12173                        }
12174
12175                        // HLE-draw popup controls for type-0 userItems that were
12176                        // associated with MENU resources via the InsertMenu → GetDItem
12177                        // pattern used by games (e.g. Marathon) to set up popup
12178                        // controls in dialogs.  We find the checked item (mark=0x12)
12179                        // in each menu and draw a standard popup button for it.
12180                        // Inside Macintosh Volume I, I-405 (userItem draw responsibilities)
12181                        let implicit_popup_candidate_count = items
12182                            .iter()
12183                            .enumerate()
12184                            .filter(|(i, item)| {
12185                                let item_no = (*i + 1) as i16;
12186                                (item.item_type & 0x7F) == 0
12187                                    && !self
12188                                        .dialog_item_popup_menus
12189                                        .contains_key(&(dialog_ptr, item_no))
12190                                    && self
12191                                        .dialog_popup_candidate_items
12192                                        .contains(&(dialog_ptr, item_no))
12193                            })
12194                            .count();
12195                        let mut implicit_popup_menu_ids: Vec<i16> = self
12196                            .menus
12197                            .iter()
12198                            .filter(|menu| {
12199                                menu.in_menu_bar
12200                                    && !menu.visible_in_menu_bar
12201                                    && menu.items.iter().any(|item| item.mark == 0x12)
12202                            })
12203                            .map(|menu| menu.id)
12204                            .collect();
12205                        if implicit_popup_menu_ids.len() < implicit_popup_candidate_count {
12206                            implicit_popup_menu_ids = self
12207                                .menus
12208                                .iter()
12209                                .filter(|menu| menu.items.iter().any(|item| item.mark == 0x12))
12210                                .map(|menu| menu.id)
12211                                .collect();
12212                        }
12213                        let mut implicit_popup_menu_index = 0usize;
12214                        let popup_draws: Vec<DialogPopupDraw> = items
12215                            .iter()
12216                            .enumerate()
12217                            .filter_map(|(i, item)| {
12218                                let item_no = (i + 1) as i16;
12219                                if (item.item_type & 0x7F) != 0 {
12220                                    return None;
12221                                }
12222                                let key = (dialog_ptr, item_no);
12223                                let (menu_id, original_rect) = if let Some(&menu_id) =
12224                                    self.dialog_item_popup_menus.get(&key)
12225                                {
12226                                    let rect = self
12227                                        .dialog_popup_original_rects
12228                                        .get(&key)
12229                                        .copied()
12230                                        .unwrap_or(item.rect);
12231                                    (menu_id, rect)
12232                                } else if self.dialog_popup_candidate_items.contains(&key) {
12233                                    let menu_id =
12234                                        *implicit_popup_menu_ids.get(implicit_popup_menu_index)?;
12235                                    implicit_popup_menu_index += 1;
12236                                    let rect = self
12237                                        .dialog_popup_original_rects
12238                                        .get(&key)
12239                                        .copied()
12240                                        .unwrap_or(item.rect);
12241                                    (menu_id, rect)
12242                                } else {
12243                                    return None;
12244                                };
12245                                let checked_text = self
12246                                    .menus
12247                                    .iter()
12248                                    .find(|m| m.id == menu_id)
12249                                    .and_then(|m| {
12250                                        m.items
12251                                            .iter()
12252                                            .find(|mi| mi.mark == 0x12)
12253                                            .map(|mi| mi.text.clone())
12254                                    })
12255                                    .unwrap_or_default();
12256                                // Use the original DITL rect (before SetDItem narrowed it)
12257                                let (it_t, it_l, it_b, it_r) = original_rect;
12258                                Some(DialogPopupDraw {
12259                                    rect: (
12260                                        bounds.0 + it_t,
12261                                        bounds.1 + it_l,
12262                                        bounds.0 + it_b,
12263                                        bounds.1 + it_r,
12264                                    ),
12265                                    title: checked_text,
12266                                    enabled: (item.item_type & 0x80) == 0,
12267                                    pressed: false,
12268                                })
12269                            })
12270                            .collect();
12271                        self.redraw_dialog_popup_controls(bus, &popup_draws);
12272
12273                        // Snapshot the fully rendered dialog (including PICTs) so
12274                        // redraw_chrome can restore it without re-parsing pictures.
12275                        // If there are userItem draw procs, this will be re-snapshotted
12276                        // after they execute.
12277                        let rendered_pixels = self.save_dialog_pixels(bus, bounds);
12278
12279                        // Collect userItem draw procs to call. ShowWindow can
12280                        // create a visible snapshot before the first
12281                        // ModalDialog entry, and that first entry still needs
12282                        // the initial userItem update pass. Only skip the
12283                        // queue when the same retained modal dialog is
12284                        // re-entered after a non-dismissing return: that
12285                        // restored visible snapshot already contains the
12286                        // completed userItem output, and a real ModalDialog
12287                        // re-entry does not manufacture a fresh update pass
12288                        // just because the app called it again.
12289                        // Inside Macintosh Volume I, I-405 and I-415.
12290                        let mut draw_proc_queue = VecDeque::new();
12291                        if !reused_retained_visible_snapshot {
12292                            for (i, item) in items.iter().enumerate() {
12293                                let base_type = item.item_type & 0x7F;
12294                                if base_type == 0
12295                                    && item.proc_ptr != 0
12296                                    && Self::dialog_item_intersects_bounds(bounds, item)
12297                                {
12298                                    draw_proc_queue.push_back((item.proc_ptr, (i + 1) as i16));
12299                                }
12300                            }
12301                        }
12302                        let has_draw_procs = !draw_proc_queue.is_empty();
12303
12304                        self.dialog_modal_entered.insert(dialog_ptr);
12305                        self.dialog_tracking = Some(super::dispatch::DialogTrackingState {
12306                            dialog_ptr,
12307                            bounds,
12308                            title,
12309                            proc_id,
12310                            items,
12311                            default_item,
12312                            cancel_item,
12313                            edit_text,
12314                            edit_item,
12315                            saved_pixels,
12316                            stack_ptr: sp,
12317                            item_hit_ptr,
12318                            rendered_pixels,
12319                            flash_remaining: 0,
12320                            flash_delay: 0,
12321                            flash_item: 0,
12322                            edit_text_modified,
12323                            draw_proc_queue,
12324                            draw_procs_done: !has_draw_procs,
12325                            rendered_pixels_final: !has_draw_procs,
12326                            filter_proc,
12327                            game_managed,
12328                            last_filter_event: None,
12329                            popup_draws,
12330                            active_popup: None,
12331                            active_button: None,
12332                            active_user_item: None,
12333                        });
12334                        self.record_modal_dialog_input_trace(
12335                            "start",
12336                            dialog_ptr,
12337                            bounds,
12338                            0,
12339                            None,
12340                            None,
12341                            "pending",
12342                            "open_modal_tracking",
12343                        );
12344                        // Don't pop stack or advance PC — re-fire pattern
12345                    } else {
12346                        // No items found — fall back to returning item 1
12347                        eprintln!("[TRAP] ModalDialog: no items found, returning 1");
12348                        if item_hit_ptr != 0 {
12349                            bus.write_word(item_hit_ptr, 1);
12350                        }
12351                        cpu.write_reg(Register::A7, sp + 8);
12352                    }
12353                }
12354                Ok(())
12355            }
12356
12357            // ========== TextEdit Manager ==========
12358            // TEInit ($A9CC)
12359            // Initializes TextEdit's internal globals.
12360            // PROCEDURE TEInit;
12361            // Inside Macintosh Volume I, I-376 ("TEInit
12362            // initializes TextEdit by allocating a handle for
12363            // the TextEdit scrap. The scrap is initially empty.
12364            // Call this procedure once and only once at the
12365            // beginning of your program."). Also note from IM:I
12366            // I-376: "You should call TEInit even if your
12367            // application doesn't use TextEdit, so that desk
12368            // accessories and dialog and alert boxes will work
12369            // correctly."
12370            //
12371            // Per IM:I I-389 the scrap globals are TEScrpHandle
12372            // ($0AB4, 4-byte Handle to the empty/cut/copied
12373            // text block) and TEScrpLength ($0AB0, 2-byte
12374            // INTEGER byte count). TEInit must:
12375            //   1. Allocate a zero-length relocatable block
12376            //      and store its handle at TEScrpHandle.
12377            //   2. Set TEScrpLength to 0 (empty scrap).
12378            //
12379            // Idempotent: per IM the routine is documented as
12380            // "call once and only once" but defensive impls
12381            // check for an existing handle and skip the
12382            // re-allocation to avoid leaking the prior one. We
12383            // do the same — apps that violate the IM contract
12384            // and call TEInit twice get a stable handle (no
12385            // double-free).
12386            // TEInit ($A9CC): Per IM:I I-376 allocates a zero-length scrap handle and stores it at TEScrpHandle ($0AB4); zeros TEScrpLength ($0AB0). Idempotent — repeated calls reuse the existing handle to avoid leaking. TECopy / TECut / TEPaste subsequently resize the underlying block as needed. No args, no result.
12387            (true, 0x1CC) => {
12388                use crate::memory::globals::addr;
12389                // Idempotency: skip re-allocation if a prior
12390                // TEInit (or first-touch by TECopy / TECut /
12391                // TEPaste) already populated the handle.
12392                let existing = bus.read_long(addr::TE_SCRP_HANDLE);
12393                if existing == 0 {
12394                    // Allocate a handle whose master ptr is
12395                    // NIL (== empty scrap). Subsequent
12396                    // TECopy / TECut grow the underlying
12397                    // block via ensure_text_handle_size which
12398                    // tolerates the NIL master ptr by lazy-
12399                    // allocating on first non-empty write.
12400                    // This matches the existing
12401                    // TECopy / TECut first-touch pattern that
12402                    // calls allocate_handle_with_data(bus, 0).
12403                    let handle = Self::allocate_handle_with_data(bus, 0);
12404                    bus.write_long(addr::TE_SCRP_HANDLE, handle);
12405                }
12406                bus.write_word(addr::TE_SCRP_LENGTH, 0);
12407                Ok(())
12408            }
12409
12410            // TEPinScroll ($A812)
12411            // Scrolls the text within the view rectangle by the
12412            // requested (dh, dv); stops scrolling when the last line
12413            // of text is scrolled into view.
12414            // PROCEDURE TEPinScroll(dh: INTEGER; dv: INTEGER; hTE: TEHandle);
12415            // Inside Macintosh: Text 1993, p. 2-91.
12416            //
12417            // IM:Text 1993 p. 2-91 verbatim:
12418            //   "The TEPinScroll procedure scrolls the text within the
12419            //    view rectangle of the specified edit record by the
12420            //    designated number of pixels. Scrolling stops when the
12421            //    last line of text is scrolled into view. ... The
12422            //    destination rectangle is offset by the amount
12423            //    scrolled. ... When the edit record is longer than the
12424            //    text it contains, TEPinScroll displays up to the last
12425            //    line of text inclusive, and not beyond it."
12426            //
12427            // Sign convention (IM:Text 1993 p. 2-91):
12428            //   dh > 0: text moves right → destRect.left/right += dh
12429            //   dh < 0: text moves left  → destRect.left/right += dh
12430            //   dv > 0: text moves down  → destRect.top/bottom += dv
12431            //   dv < 0: text moves up    → destRect.top/bottom += dv
12432            //
12433            // Pascal stack frame (args push left-to-right, first
12434            // source arg deepest):
12435            //   sp+0  hTE: TEHandle           (4) — last arg, shallowest
12436            //   sp+4  dv:  INTEGER            (2) — middle arg
12437            //   sp+6  dh:  INTEGER            (2) — first arg, deepest
12438            // Total pop = 8 bytes; no function-result slot.
12439            //
12440            // MPW Universal Headers TextEdit.h:
12441            //   EXTERN_API(void) TEPinScroll(short dh, short dv,
12442            //                                TEHandle hTE)
12443            //                                  ONEWORDINLINE(0xA812);
12444            //
12445            // Pin semantics: the dv arm clamps so dest_rect.top stays
12446            // within [view_top - (text_bottom - view_bottom), view_top]
12447            // — i.e. far enough that the last line of text remains
12448            // visible at the bottom of the view. The dh arm applies a
12449            // symmetric horizontal clamp. For in-range scrolls the
12450            // call behaves exactly like TEScroll ($A9DD) per the IM
12451            // "offset by the amount scrolled" guarantee.
12452            //
12453            // Regression coverage:
12454            //   dialog::tests::te_pin_scroll_reads_handle_from_stack_top
12455            //   dialog::tests::tepinscroll_in_range_negative_dv_offsets_destrect_top_and_bottom_exactly_by_dv
12456            //   dialog::tests::tepinscroll_pascal_lr_stack_layout_reads_dh_dv_and_hte_from_correct_offsets
12457            //   dialog::tests::tepinscroll_clamps_overscroll_when_last_line_is_already_visible
12458            // TEPinScroll ($A812): Offsets `destRect` by the requested delta and pops 8 bytes
12459            (true, 0x012) => {
12460                let sp = cpu.read_reg(Register::A7);
12461                let te_handle = bus.read_long(sp);
12462                let mut dv = bus.read_word(sp + 4) as i16;
12463                let mut dh = bus.read_word(sp + 6) as i16;
12464                cpu.write_reg(Register::A7, sp + 8);
12465
12466                let te_ptr = Self::te_record_ptr(bus, te_handle);
12467                if te_ptr != 0 {
12468                    let view_rect = Self::te_read_rect(bus, te_ptr + Self::TE_VIEW_RECT_OFFSET);
12469                    let dest_rect = Self::te_read_rect(bus, te_ptr + Self::TE_DEST_RECT_OFFSET);
12470
12471                    if dv > 0 {
12472                        let max_down = view_rect.0.saturating_sub(dest_rect.0);
12473                        if dv > max_down {
12474                            dv = max_down;
12475                        }
12476                    } else if dv < 0 {
12477                        // Scrolling up (dv < 0) is bounded by the
12478                        // distance between the text's bottom and
12479                        // the view's bottom — pinning behaviour
12480                        // stops once the last line is visible per
12481                        // Text 1993, 2-91. max_up = view_bottom -
12482                        // text_bottom: if text already fits (value
12483                        // ≥ 0) there's nothing to scroll up to, so
12484                        // dv clamps to 0. Otherwise max_up < 0
12485                        // gives the amount of up-scroll still available; clamp dv
12486                        // upward to max_up so it can't exceed that.
12487                        let text_len = Self::te_text_length(bus, te_handle);
12488                        let (end_top, _) = self.te_char_to_point(bus, te_handle, text_len);
12489                        let end_line = Self::te_char_to_line_index(bus, te_handle, text_len);
12490                        let text_bottom = end_top
12491                            .saturating_add(Self::te_height_for_line(bus, te_handle, end_line));
12492                        let max_up = view_rect.2.saturating_sub(text_bottom);
12493                        dv = if max_up >= 0 {
12494                            0
12495                        } else if dv < max_up {
12496                            max_up
12497                        } else {
12498                            dv
12499                        };
12500                    }
12501
12502                    if dh > 0 {
12503                        let max_right = view_rect.1.saturating_sub(dest_rect.1);
12504                        dh = if max_right > 0 { dh.min(max_right) } else { 0 };
12505                    } else if dh < 0 {
12506                        let max_left = view_rect.1.saturating_sub(dest_rect.1);
12507                        dh = if max_left < 0 { dh.max(max_left) } else { 0 };
12508                    }
12509
12510                    if trace_textedit_enabled() {
12511                        let adjusted = (
12512                            dest_rect.0.saturating_add(dv),
12513                            dest_rect.1.saturating_add(dh),
12514                            dest_rect.2.saturating_add(dv),
12515                            dest_rect.3.saturating_add(dh),
12516                        );
12517                        eprintln!(
12518                            "[TE] TEPinScroll hTE=${:08X} dh={} dv={} dest=({},{},{},{})",
12519                            te_handle, dh, dv, adjusted.0, adjusted.1, adjusted.2, adjusted.3
12520                        );
12521                    }
12522                    self.te_scroll_contents(cpu, bus, te_handle, dh, dv);
12523                }
12524                Ok(())
12525            }
12526
12527            // TEAutoView ($A813)
12528            // Enables or disables automatic scrolling for an edit record.
12529            // PROCEDURE TEAutoView(fAuto: Boolean; hTE: TEHandle);
12530            // Text 1993, 2-92
12531            //
12532            // hTE is the LAST parameter (Pascal left-to-right push), so it
12533            // sits at SP+0 above the BOOLEAN at SP+4.
12534            // TEAutoView ($A813): Tracks the auto-scroll feature bit per TEHandle
12535            (true, 0x013) => {
12536                let sp = cpu.read_reg(Register::A7);
12537                let te_handle = bus.read_long(sp);
12538                // Pascal BOOLEAN in high byte (MPW C convention).
12539                let enabled = bus.read_byte(sp + 4) != 0;
12540                self.set_te_feature_bit(te_handle, Self::TE_FEATURE_AUTO_SCROLL, enabled);
12541                if trace_textedit_enabled() {
12542                    eprintln!("[TE] TEAutoView hTE=${:08X} enabled={}", te_handle, enabled);
12543                }
12544                cpu.write_reg(Register::A7, sp + 6);
12545                Ok(())
12546            }
12547
12548            // TESelView ($A811)
12549            // PROCEDURE TESelView(hTE: TEHandle);
12550            // Inside Macintosh: Text (1993), p. 2-92.
12551            //
12552            // Per IM:Text 1993 p. 2-92 verbatim: "Once automatic scrolling
12553            // has been enabled by a call to the TEAutoView procedure or
12554            // through the TEFeatureFlag function, the TESelView procedure
12555            // ensures that the selection range is visible and scrolls it
12556            // into the view rectangle if necessary. ... The top left part
12557            // of the selection range is scrolled into view. ... If
12558            // automatic scrolling is disabled, TESelView has no effect."
12559            //
12560            // MPW Universal Headers TextEdit.h:
12561            //   EXTERN_API(void) TESelView(TEHandle hTE)
12562            //                              ONEWORDINLINE(0xA811);
12563            //
12564            // Pascal stack frame:
12565            //   sp+0  hTE: TEHandle  (4)
12566            // Total pop = 4 bytes. No function result.
12567            //
12568            // Algorithm (matches Apple's documented contract):
12569            //   1. If TE_FEATURE_AUTO_SCROLL is OFF on this hTE → no-op.
12570            //   2. Read viewRect, destRect, and the current selection
12571            //      range from the TERec.
12572            //   3. Resolve sel_start and sel_end character offsets to
12573            //      pixel coordinates (top_left of selection range and
12574            //      bottom_right via line-height lookup).
12575            //   4. Compute dh, dv via te_getdelta — the per-axis shift
12576            //      that brings the selection rectangle inside viewRect
12577            //      (zero if the selection is already inside).
12578            //   5. Call te_scroll_contents which adds (dh, dv) to
12579            //      destRect.{top,left,bottom,right} and redraws.
12580            //
12581            // BasiliskII-vs-Apple divergence note:
12582            //   BasiliskII System 7.5.3 ROM does NOT scroll destRect when
12583            //   auto-scroll is enabled and the selection lies below viewRect
12584            //   — pre and post destRect coincide at (0,0,200,30). Apple's
12585            //   IM:Text 1993 p. 2-92 says this case must scroll. Systemless
12586            //   implements the Apple-canonical semantic; the divergent rule
12587            //   is pinned by the assertion-bearing tests in this module.
12588            (true, 0x011) => {
12589                let sp = cpu.read_reg(Register::A7);
12590                let te_handle = bus.read_long(sp);
12591                cpu.write_reg(Register::A7, sp + 4);
12592                if self.te_feature_bit(te_handle, Self::TE_FEATURE_AUTO_SCROLL) {
12593                    let te_ptr = Self::te_record_ptr(bus, te_handle);
12594                    if te_ptr != 0 {
12595                        let view_rect = Self::te_read_rect(bus, te_ptr + Self::TE_VIEW_RECT_OFFSET);
12596                        let dest_rect = Self::te_read_rect(bus, te_ptr + Self::TE_DEST_RECT_OFFSET);
12597                        let sel_start = bus.read_word(te_ptr + Self::TE_SEL_START_OFFSET) as usize;
12598                        let sel_end = bus.read_word(te_ptr + Self::TE_SEL_END_OFFSET) as usize;
12599                        let (start_top, start_left) =
12600                            self.te_char_to_point(bus, te_handle, sel_start);
12601                        let (stop_top, stop_left) = self.te_char_to_point(bus, te_handle, sel_end);
12602                        let stop_line = Self::te_char_to_line_index(bus, te_handle, sel_end);
12603                        let stop_bottom = stop_top
12604                            .saturating_add(Self::te_height_for_line(bus, te_handle, stop_line));
12605                        let dv =
12606                            Self::te_getdelta(start_top, stop_bottom, view_rect.0, view_rect.2);
12607                        let dh = Self::te_getdelta(start_left, stop_left, view_rect.1, view_rect.3);
12608                        if trace_textedit_enabled() {
12609                            let adjusted = (
12610                                dest_rect.0.saturating_add(dv),
12611                                dest_rect.1.saturating_add(dh),
12612                                dest_rect.2.saturating_add(dv),
12613                                dest_rect.3.saturating_add(dh),
12614                            );
12615                            eprintln!(
12616                                "[TE] TESelView hTE=${:08X} view=({},{},{},{}) dest=({},{},{},{}) adjusted=({},{},{},{})",
12617                                te_handle,
12618                                view_rect.0,
12619                                view_rect.1,
12620                                view_rect.2,
12621                                view_rect.3,
12622                                dest_rect.0,
12623                                dest_rect.1,
12624                                dest_rect.2,
12625                                dest_rect.3,
12626                                adjusted.0,
12627                                adjusted.1,
12628                                adjusted.2,
12629                                adjusted.3
12630                            );
12631                        }
12632                        self.te_scroll_contents(cpu, bus, te_handle, dh, dv);
12633                    }
12634                }
12635                Ok(())
12636            }
12637
12638            // ========== Cursor Manager ==========
12639
12640            // InitCursor ($A850) - Toolbox version
12641            // Resets to standard arrow cursor
12642            // InitCursor ($A850): Sets arrow cursor, resets cursor level to 0,
12643            // makes visible (IM:I I-167).
12644            (true, 0x050) => {
12645                self.cursor_data = Some(Self::default_arrow_cursor_image());
12646                self.cursor_level = 0;
12647                self.cursor_visible = true;
12648                Ok(())
12649            }
12650
12651            // SetCursor ($A851)
12652            // PROCEDURE SetCursor(crsr: Cursor);
12653            // Cursor record: data[32] + mask[32] + hotSpot.v(2) + hotSpot.h(2) = 68 bytes
12654            // SetCursor ($A851): Reads 68-byte cursor record (16×16 data + mask + hotspot).
12655            // Per IM:I I-167, if the cursor is hidden it stays hidden and only
12656            // changes appearance when uncovered by matching ShowCursor calls.
12657            (true, 0x051) => {
12658                let sp = cpu.read_reg(Register::A7);
12659                let crsr_ptr = bus.read_long(sp);
12660                cpu.write_reg(Register::A7, sp + 4);
12661
12662                // Read cursor bitmap (16x16 = 32 bytes)
12663                let mut data = [0u8; 32];
12664                for (i, byte) in data.iter_mut().enumerate() {
12665                    *byte = bus.read_byte(crsr_ptr + i as u32);
12666                }
12667                // Read cursor mask (16x16 = 32 bytes)
12668                let mut mask = [0u8; 32];
12669                for (i, byte) in mask.iter_mut().enumerate() {
12670                    *byte = bus.read_byte(crsr_ptr + 32 + i as u32);
12671                }
12672                // Read hotspot
12673                let hot_v = bus.read_word(crsr_ptr + 64) as i16;
12674                let hot_h = bus.read_word(crsr_ptr + 66) as i16;
12675
12676                self.cursor_data = Some(CursorImage::mono(data, mask, hot_v, hot_h));
12677                self.cursor_visible = self.cursor_level == 0;
12678                Ok(())
12679            }
12680
12681            // HideCursor ($A852)
12682            // HideCursor ($A852): Decrements cursor level and hides while level < 0
12683            // per IM:I I-168.
12684            (true, 0x052) => {
12685                self.cursor_level = self.cursor_level.saturating_sub(1);
12686                self.cursor_visible = self.cursor_level == 0;
12687                Ok(())
12688            }
12689
12690            // ShowCursor ($A853)
12691            // ShowCursor ($A853): Increments cursor level toward 0; extra calls
12692            // at level 0 are no-op (IM:I I-168).
12693            (true, 0x053) => {
12694                if self.cursor_level < 0 {
12695                    self.cursor_level += 1;
12696                }
12697                self.cursor_visible = self.cursor_level == 0;
12698                Ok(())
12699            }
12700
12701            // ObscureCursor ($A856)
12702            // PROCEDURE ObscureCursor;
12703            // Inside Macintosh Volume I, I-168
12704            // Imaging With QuickDraw 1994, p. 8-29
12705            //
12706            // MPW Universal Headers (Quickdraw.h):
12707            //
12708            //   EXTERN_API(void) ObscureCursor(void) ONEWORDINLINE(0xA856);
12709            //
12710            // Pascal PROCEDURE with no arguments and no result slot:
12711            // caller pushes 0 bytes; trap pops 0 bytes; SP unchanged.
12712            //
12713            // Per IM:I I-168: "ObscureCursor hides the cursor until
12714            // the next time the mouse is moved. It's normally
12715            // called when the user begins to type. Unlike
12716            // HideCursor, it has no effect on the cursor level and
12717            // must not be balanced by a call to ShowCursor."
12718            //
12719            // HLE compromise: Systemless synthesizes mouse-move events
12720            // every frame from the scripted event source (or
12721            // every interactive frame from systemless). Honouring
12722            // the "hide until next mouse move" semantic would keep
12723            // the cursor PERMANENTLY hidden because each
12724            // synthesized mouse-move arrives before any "is the
12725            // mouse stationary?" check can materialise the cursor
12726            // (every frame produces both the obscure-trigger and
12727            // the un-obscure-trigger simultaneously). Treating it
12728            // as a no-op preserves cursor visibility — HideCursor
12729            // ($A852) / ShowCursor ($A853) still operate the
12730            // level-counter hide/show stack for explicit pairs in
12731            // apps that need them. Per IM:I I-168 explicit
12732            // "must not be balanced by a call to ShowCursor"
12733            // means apps universally call ObscureCursor without a
12734            // matching ShowCursor — so the no-op contract leaves
12735            // them in the same observable state (cursor visible,
12736            // level unchanged) regardless of dispatch.
12737            //
12738            // The Apple-canonical "hides until mouse move" and
12739            // "must not be balanced by ShowCursor" rules are pinned
12740            // in-Rust via `obscure_cursor_noop_preserves_cursor_level_visibility_and_stack`.
12741            // BII and Systemless HLE diverge on the LowMem CrsrVis
12742            // side-effect (BII System 7.5.3 ROM writes CrsrVis;
12743            // Systemless HLE keeps cursor state internal).
12744            //
12745            // ObscureCursor ($A856): No args / no result per IM:I I-168 MPW C declaration ObscureCursor(void) ONEWORDINLINE(0xA856) — HLE no-op; SP unchanged across calls.
12746            (true, 0x056) => Ok(()),
12747
12748            // GetCursor ($A9B9)
12749            // FUNCTION GetCursor(cursorID: INTEGER): CursHandle;
12750            // Inside Macintosh Volume I, I-474
12751            //
12752            // "GetCursor returns a handle to the cursor having the
12753            // given resource ID, reading it from the resource file if
12754            // necessary. It calls the Resource Manager function
12755            // GetResource('CURS', cursorID). If the resource can't be
12756            // read, GetCursor returns NIL." — IM:I I-474.
12757            //
12758            // HLE compromise: Systemless doesn't load the System file's
12759            // resource fork, so the four standard system cursor IDs
12760            // documented at IM:I I-475..I-477 are synthesized here
12761            // via [`Self::synthesize_system_cursor`] (cached for
12762            // handle stability — apps cache the GetCursor result at
12763            // boot and pass it to SetCursor every frame). Any other
12764            // ID falls through to the IM-correct NIL miss path.
12765            //
12766            // The previous Stub allocated a fresh 68-byte zero-filled
12767            // block on every miss and returned a handle to it —
12768            // strictly worse than NIL since callers that defensively
12769            // check `if handle = NIL` got a non-NIL pointer to an
12770            // empty/white cursor and SetCursor'd a blank cursor onto
12771            // the screen. Same fallback issue as the GetIcon ($A9BB)
12772            // 128-byte uninitialised-heap stub closed by the
12773            // family-level resource fallback audit.
12774            //
12775            // Pop = 2 (cursorID INTEGER), result CursHandle at new SP+0.
12776            // GetCursor ($A9B9): Per IM:I I-474 calls GetResource('CURS', cursorID); on hit returns stable handle via get_or_create_resource_handle; on miss synthesizes built-in cursor 1..4 (iBeam/cross/plus/watch per IM:I I-475..I-477) via cached synthesize_system_cursor; otherwise returns NIL. Pops 2 bytes (cursorID), 4-byte CursHandle result at new SP+0.
12777            (true, 0x1B9) => {
12778                let sp = cpu.read_reg(Register::A7);
12779                let cursor_id = bus.read_word(sp) as i16;
12780
12781                let handle = if let Some((refnum, ptr)) =
12782                    self.find_or_load_resource_any(bus, *b"CURS", cursor_id)
12783                {
12784                    self.get_or_create_resource_handle_in_file(
12785                        bus, *b"CURS", cursor_id, ptr, refnum,
12786                    )
12787                } else if let Some(ptr) = self.synthesize_system_cursor(bus, cursor_id) {
12788                    // Built-in cursor synthesised + cached. Use the
12789                    // resource-handle helper so subsequent GetCursor
12790                    // calls for the same ID return the same handle.
12791                    self.get_or_create_resource_handle(bus, *b"CURS", cursor_id, ptr)
12792                } else {
12793                    0
12794                };
12795
12796                bus.write_long(sp + 2, handle);
12797                cpu.write_reg(Register::A7, sp + 2);
12798                Ok(())
12799            }
12800
12801            // GetPattern ($A9B8)
12802            // FUNCTION GetPattern(patID: INTEGER): PatHandle;
12803            // Inside Macintosh Volume I, I-473
12804            //
12805            // "GetPattern returns a handle to the pattern having the
12806            // given resource ID, reading it from the resource file if
12807            // necessary. It calls the Resource Manager function
12808            // GetResource('PAT ', patID). If the resource can't be
12809            // read, GetPattern returns NIL." — IM:I I-473.
12810            //
12811            // The previous Stub allocated a fresh 8-byte all-0xFF
12812            // (white) pattern on every miss and returned a handle to
12813            // it — strictly worse than NIL since callers that
12814            // defensively check `if handle = NIL then use_default
12815            // else FillRect(rect, handle^^)` got a non-NIL handle and
12816            // proceeded to FillRect with white instead of taking the
12817            // recovery branch. Same fallback issue as the GetIcon
12818            // ($A9BB) and GetCursor ($A9B9) fallbacks closed in this
12819            // family's audit pass.
12820            //
12821            // Pop = 2 (patID INTEGER), result PatHandle at new SP+0.
12822            // GetPattern ($A9B8): Per IM:I I-473 calls GetResource('PAT ', patID); on hit returns stable handle via get_or_create_resource_handle; on miss returns NIL (previously a fresh all-0xFF white pattern, which made callers branching on `handle = NIL` take the wrong path). Pops 2 bytes (patID), 4-byte PatHandle result at new SP+0.
12823            (true, 0x1B8) => {
12824                let sp = cpu.read_reg(Register::A7);
12825                let pat_id = bus.read_word(sp) as i16;
12826
12827                let handle = if let Some((refnum, ptr)) =
12828                    self.find_or_load_resource_any(bus, *b"PAT ", pat_id)
12829                {
12830                    self.get_or_create_resource_handle_in_file(bus, *b"PAT ", pat_id, ptr, refnum)
12831                } else {
12832                    0
12833                };
12834
12835                bus.write_long(sp + 2, handle);
12836                cpu.write_reg(Register::A7, sp + 2);
12837                Ok(())
12838            }
12839
12840            // GetIcon ($A9BB)
12841            // Returns a handle to the icon stored in the 'ICON' resource
12842            // with the given ID. Equivalent to GetResource('ICON', iconID).
12843            // The resource is a 128-byte black-and-white bitmap (32x32
12844            // pixels at 1 bit each).
12845            // FUNCTION GetIcon(iconID: INTEGER): Handle;
12846            // Inside Macintosh Volume I, I-473
12847            //
12848            // Mirrors GetPicture ($A9BC) exactly: look up the
12849            // resource via the dispatcher's resource search chain;
12850            // on hit, materialise (or reuse) a stable handle that
12851            // points at the loaded resource bytes; on miss, return
12852            // NIL per IM:I I-473 ("If the resource can't be read,
12853            // GetIcon returns NIL").
12854            //
12855            // The previous Stub allocated a fresh 128-byte block of
12856            // UNINITIALISED memory and returned a handle to it on
12857            // every call — strictly worse than NIL since callers
12858            // pass that handle to PlotIcon ($A94B) which CopyBits
12859            // the random bytes onto the framebuffer. Apps with a
12860            // missing 'ICON' that defensively check `if handle =
12861            // NIL` would crash on the dereference path; apps that
12862            // trust the result blindly would render a junk icon.
12863            // The proper Partial impl returns NIL on miss so both
12864            // branches behave correctly.
12865            //
12866            // Pop = 2 (iconID INTEGER), result Handle at new SP+0.
12867            // GetIcon ($A9BB): Per IM:I I-473 calls GetResource('ICON', iconID); returns handle to the loaded resource via get_or_create_resource_handle (stable handle reused across calls), or NIL if the ICON resource is missing. Pops 2 bytes (iconID), 4-byte Handle result at new SP+0. Mirrors GetPicture ($A9BC).
12868            (true, 0x1BB) => {
12869                let sp = cpu.read_reg(Register::A7);
12870                let icon_id = bus.read_word(sp) as i16;
12871
12872                let handle = if let Some((refnum, ptr)) =
12873                    self.find_or_load_resource_any(bus, *b"ICON", icon_id)
12874                {
12875                    let h = self
12876                        .get_or_create_resource_handle_in_file(bus, *b"ICON", icon_id, ptr, refnum);
12877                    eprintln!(
12878                        "[TRAP] GetIcon({}) -> handle=${:08X} ptr=${:08X}",
12879                        icon_id, h, ptr
12880                    );
12881                    h
12882                } else {
12883                    eprintln!("[TRAP] GetIcon({}) -> NIL (not found)", icon_id);
12884                    0
12885                };
12886
12887                bus.write_long(sp + 2, handle);
12888                cpu.write_reg(Register::A7, sp + 2);
12889                Ok(())
12890            }
12891
12892            // GetPicture ($A9BC)
12893            // Returns a handle to the picture stored in the 'PICT' resource
12894            // with the given ID. Equivalent to GetResource('PICT', picID).
12895            // FUNCTION GetPicture(picID: INTEGER): PicHandle;
12896            // Inside Macintosh Volume I, I-475
12897            // GetPicture ($A9BC): Loads PICT resource via GetResource, returns handle
12898            (true, 0x1BC) => {
12899                let sp = cpu.read_reg(Register::A7);
12900                let pic_id = bus.read_word(sp) as i16;
12901
12902                let handle = if let Some((refnum, ptr)) =
12903                    self.find_or_load_resource_any(bus, *b"PICT", pic_id)
12904                {
12905                    let h = self
12906                        .get_or_create_resource_handle_in_file(bus, *b"PICT", pic_id, ptr, refnum);
12907                    eprintln!(
12908                        "[TRAP] GetPicture({}) -> handle=${:08X} ptr=${:08X}",
12909                        pic_id, h, ptr
12910                    );
12911                    h
12912                } else {
12913                    eprintln!("[TRAP] GetPicture({}) -> NIL (not found)", pic_id);
12914                    0
12915                };
12916
12917                bus.write_long(sp + 2, handle);
12918                cpu.write_reg(Register::A7, sp + 2);
12919                Ok(())
12920            }
12921
12922            // GetString ($A9BA)
12923            // Returns a handle to the 'STR ' resource with the given ID.
12924            // FUNCTION GetString (stringID: INTEGER): StringHandle;
12925            // Text 1993, 5-49; Inside Macintosh Volume I, I-468
12926            // GetString ($A9BA): Returns the loaded `'STR '` resource handle or NIL when missing
12927            (true, 0x1BA) => {
12928                let sp = cpu.read_reg(Register::A7);
12929                let string_id = bus.read_word(sp) as i16;
12930                let handle = if let Some((refnum, ptr)) =
12931                    self.find_or_load_resource_any(bus, *b"STR ", string_id)
12932                {
12933                    self.get_or_create_resource_handle_in_file(
12934                        bus, *b"STR ", string_id, ptr, refnum,
12935                    )
12936                } else if let Some(ptr) = self.synthesize_system_str(bus, string_id) {
12937                    self.get_or_create_resource_handle(bus, *b"STR ", string_id, ptr)
12938                } else {
12939                    0
12940                };
12941                bus.write_long(sp + 2, handle);
12942                cpu.write_reg(Register::A7, sp + 2);
12943                Ok(())
12944            }
12945
12946            // ========== TextEdit Manager Stubs ==========
12947
12948            // TENew ($A9D2)
12949            // FUNCTION TENew(destRect, viewRect: Rect): TEHandle;
12950            // Inside Macintosh Volume I (1985), p. I-373..I-374.
12951            // Text 1993, 2-85..2-86.
12952            //
12953            // IM:I I-374: TENew "creates and initializes the necessary
12954            // data structures, allocates an edit record, returns a
12955            // handle to it, and sets that handle's selection range,
12956            // view rectangle, destination rectangle, and other fields."
12957            //
12958            // Fresh TERec state per IM:I I-373:
12959            //   destRect, viewRect = caller-supplied
12960            //   selStart = selEnd = 0
12961            //   teLength = 0
12962            //   hText = handle to empty char buffer (non-NIL)
12963            //   txFont, txFace, txMode, txSize copied from current grafPort
12964            //   inPort = current grafPort
12965            //
12966            // Calling-convention duality. Classic Inside Macintosh
12967            // declares TENew with Pascal by-value Rect parameters
12968            // (16 bytes on the stack). MPW Universal Headers
12969            // (TextEdit.h) modernise it to pointer parameters:
12970            //   EXTERN_API(TEHandle) TENew(const Rect *destRect,
12971            //                              const Rect *viewRect)
12972            //                                  ONEWORDINLINE(0xA9D2);
12973            // BasiliskII System 7.5.3 ROM accepts the pointer-arg
12974            // convention. Systemless's HLE
12975            // sniffs which convention the caller used by inspecting
12976            // whether the first two long words on the stack are valid
12977            // guest pointers and pops either 8 bytes (pointer convention)
12978            // or 16 bytes (by-value convention) accordingly.
12979            //
12980            // Regression coverage (this file):
12981            //   tenew_pointer_arg_convention_initializes_destrect_viewrect_and_returns_non_nil_handle
12982            //   tenew_fresh_terec_has_zero_telength_and_empty_selection_per_im_i_373
12983            //   tenew_function_protocol_consumes_two_pointer_args_and_writes_4_byte_result
12984            //
12985            // TENew ($A9D2): Allocates and initializes a basic monostyled TERec plus empty `hText` handle; supports both pointer-arg and by-value-rect conventions per te_new_rect_args sniffing
12986            (true, 0x1D2) => {
12987                let sp = cpu.read_reg(Register::A7);
12988                let handle = Self::allocate_te_handle(bus);
12989                let (dest_rect, view_rect, stack_pop) = Self::te_new_rect_args(bus, sp);
12990                self.initialize_te_record(bus, handle, dest_rect, view_rect);
12991                bus.write_long(sp + stack_pop, handle);
12992                cpu.write_reg(Register::A7, sp + stack_pop);
12993                Ok(())
12994            }
12995
12996            // TEStyleNew ($A83E)
12997            // Creates a multistyled edit record in the current port.
12998            // FUNCTION TEStyleNew(destRect: Rect; viewRect: Rect): TEHandle;
12999            // Inside Macintosh: Text (1993), p. 2-78.
13000            //
13001            // IM:Text 1993 p. 2-78 verbatim:
13002            //   "The TEStyleNew function creates a multistyled edit
13003            //    record and allocates a handle to it... TEStyleNew
13004            //    sets the txSize, lineHeight, and fontAscent fields
13005            //    of the edit record to -1, allocates a style record,
13006            //    and stores a handle to the style record in the
13007            //    txFont and txFace fields. The TEStyleNew function
13008            //    creates and initializes a null scrap that is used
13009            //    by TextEdit routines throughout the life of the
13010            //    edit record."
13011            //
13012            // MPW Universal Headers (TextEdit.h):
13013            //   EXTERN_API(TEHandle)
13014            //   TEStyleNew(const Rect *destRect,
13015            //              const Rect *viewRect)   ONEWORDINLINE(0xA83E);
13016            //
13017            // Calling convention: identical to TENew. Pascal pushes
13018            // left-to-right, so destRect (first arg) lands deepest
13019            // and viewRect (second arg) lands shallowest:
13020            //   sp+0..3   viewRect_ptr  (last pushed)
13021            //   sp+4..7   destRect_ptr  (first pushed)
13022            // Both pointer (8-byte) and by-value (16-byte) forms are
13023            // accepted via te_new_rect_args sniffing.
13024            //
13025            // Styled-record signature, per IM:Text 1993 p. 2-78
13026            // (initialize_styled_te_record at dialog.rs:843..):
13027            //   txSize     = -1   sentinel at offset 0x50
13028            //   lineHeight = -1   sentinel at offset 0x18
13029            //   fontAscent = -1   sentinel at offset 0x1A
13030            //   txFont/txFace (4-byte overlay at offset 0x4A) holds
13031            //                     the TEStyleHandle.
13032            //
13033            // Regression coverage (this file):
13034            //   testylenew_returns_styled_handle_and_initializes_sentinel_fields
13035            //   testylenew_pointer_arg_convention_initializes_destrect_viewrect_and_styled_sentinels
13036            //   testylenew_function_protocol_consumes_two_pointer_args_and_writes_4_byte_result
13037            //
13038            // TEStyleNew ($A83E): Allocates a TEHandle and initializes a multistyled record (destRect + viewRect + txSize/lineHeight/fontAscent=-1 sentinels + non-NIL TEStyleHandle); style runs and null scrap allocated per Text 1993, 2-78
13039            (true, 0x03E) => {
13040                let sp = cpu.read_reg(Register::A7);
13041                let handle = Self::allocate_te_handle(bus);
13042                let (dest_rect, view_rect, stack_pop) = Self::te_new_rect_args(bus, sp);
13043                self.initialize_styled_te_record(bus, handle, dest_rect, view_rect);
13044                bus.write_long(sp + stack_pop, handle);
13045                cpu.write_reg(Register::A7, sp + stack_pop);
13046                Ok(())
13047            }
13048
13049            // TEGetOffset ($A83C)
13050            // FUNCTION TEGetOffset(pt: Point; hTE: TEHandle): INTEGER;
13051            // Inside Macintosh Volume V (1986), p. V-172.
13052            //
13053            // IM:V V-172 verbatim: "TEGetOffset returns the character
13054            // position closest to the point pt. The point pt is in
13055            // local coordinates relative to the destination rectangle.
13056            // If pt is above the first line, TEGetOffset returns the
13057            // character offset of the start of the first line. If pt
13058            // is below the last line, TEGetOffset returns the
13059            // character offset of the end of the text."
13060            //
13061            // MPW Universal Headers (TextEdit.h):
13062            //   EXTERN_API(short)
13063            //   TEGetOffset(Point pt, TEHandle hTE) ONEWORDINLINE(0xA83C);
13064            //
13065            // Calling convention. `EXTERN_API` expands to `extern
13066            // pascal` on the 68k target, so the Pascal LR push order
13067            // applies: pt (the first arg) is pushed FIRST and lands
13068            // DEEPEST on the stack; hTE (the last arg) is pushed LAST
13069            // and lands SHALLOWEST. Point is a 4-byte record with
13070            // pt.v at the lower address and pt.h at the higher
13071            // address. Pascal FUNCTION pre-allocates the 2-byte
13072            // INTEGER result slot just above the args. Stack layout
13073            // at trap entry:
13074            //   sp+0..3   hTE      (4 bytes, last pushed)
13075            //   sp+4..5   pt.v     (2 bytes, first half of Point)
13076            //   sp+6..7   pt.h     (2 bytes, second half of Point)
13077            //   sp+8..9   function result slot (2 bytes)
13078            //
13079            // Pre-fix (commit ca6a0ebf — A9D2 te_new_rect_args
13080            // Pascal-LR fix only covered TENew + TEStyleNew sharing
13081            // the te_new_rect_args helper): this arm read te_handle
13082            // from sp+2 and pt.v/pt.h from sp+6/sp+8, off-by-2 versus
13083            // the canonical Pascal LR layout. That off-by-2 read placed
13084            // garbage in te_handle so the te_point_to_char helper bailed
13085            // via the NIL TERec branch and returned 0 instead of the
13086            // expected teLength=5. Fixed by reading args at the canonical
13087            // sp+0, sp+4, sp+6 offsets.
13088            //
13089            // Regression coverage (this file):
13090            //   tegetoffset_point_above_destrect_returns_zero
13091            //   tegetoffset_point_below_last_line_returns_telength
13092            //   tegetoffset_function_protocol_consumes_point_and_tehandle_args_writes_integer_result
13093            //
13094            // TEGetOffset ($A83C): Maps a point back to a character offset using the line-starts / per-line heights and primary-run advance widths per IM:V V-172. Pascal LR push order — sp+0 hTE (last pushed), sp+4 pt.v, sp+6 pt.h, sp+8 INTEGER result slot.
13095            (true, 0x03C) => {
13096                let sp = cpu.read_reg(Register::A7);
13097                let te_handle = bus.read_long(sp);
13098                let point_v = bus.read_word(sp + 4) as i16;
13099                let point_h = bus.read_word(sp + 6) as i16;
13100                let offset = self.te_point_to_char(bus, te_handle, (point_v, point_h));
13101                bus.write_word(sp + 8, offset as u16);
13102                cpu.write_reg(Register::A7, sp + 8);
13103                Ok(())
13104            }
13105
13106            // TEFindWord ($A0FE)
13107            // Register-based TextEdit hook:
13108            //   currentPos in D0.W, caller in D2.W, pTE in A3.L, hTE in A4.L.
13109            //   wordStart returns in D0.W and wordEnd in D1.W.
13110            // Inside Macintosh: Text (1993), pp. 2-60..2-61.
13111            (false, 0x0FE) => {
13112                let current_pos = (cpu.read_reg(Register::D0) as u16) as usize;
13113                let _caller = cpu.read_reg(Register::D2);
13114                let _p_te = cpu.read_reg(Register::A3);
13115                let h_te = cpu.read_reg(Register::A4);
13116                let (word_start, word_end) = self.te_find_word_bounds(bus, h_te, current_pos);
13117                cpu.write_reg(Register::D0, u32::from(word_start));
13118                cpu.write_reg(Register::D1, u32::from(word_end));
13119                Ok(())
13120            }
13121
13122            // TEDispatch ($A83D)
13123            // Selector-based dispatcher for styled TextEdit routines.
13124            // FUNCTION/PROCEDURE TEDispatch(...); selector is the first stack word.
13125            // Inside Macintosh Volume VI, 15-22 (TEFeatureFlag),
13126            //                              15-25..15-43 (selector mapping),
13127            //                              15-34 (TEContinuousStyle).
13128            // Inside Macintosh: Text (1993), p. 2-102 (TEContinuousStyle),
13129            //                                p. 2-92 / 2-97 (autoscroll default).
13130            //
13131            // TEDispatch ($A83D) selectors: $0000 TEStylPaste,
13132            //   $0001 TESetStyle, $0002 TEReplaceStyle, $0003 TEGetStyle,
13133            //   $0004 GetStylHandle, $0005 SetStylHandle, $0006 GetStylScrap,
13134            //   $0007 TEStylInsert, $0008 TEGetPoint, $0009 TEGetHeight,
13135            //   $000A TEContinuousStyle, $000B TEUseStyleScrap,
13136            //   $000C TECustomHook, $000D TENumStyles, $000E TEFeatureFlag.
13137            //
13138            // MPW Universal Headers TextEdit.h declares each entry point via
13139            // THREEWORDINLINE(0x3F3C, <selector>, 0xA83D). The 0x3F3C is
13140            // `MOVE.W #imm,-(A7)` which pre-pushes the selector word at the
13141            // call site, immediately before the A-line trap. Pascal LR
13142            // calling convention pushes args left-to-right (first arg
13143            // deepest), so for any N-arg TEDispatch selector the stack
13144            // layout at trap entry is:
13145            //   sp+0           selector word (2 bytes, last pushed)
13146            //   sp+2           arg[N-1] (last Pascal arg, shallowest)
13147            //   ...
13148            //   sp+(2 + S_0+..+S_{N-2})  arg[0] (first Pascal arg, deepest)
13149            //   sp+(2 + Σ S_i) function result slot (for FUNCTION selectors)
13150            //
13151            // Selector $000A TEContinuousStyle is FUNCTION (Boolean result):
13152            //   EXTERN_API(Boolean) TEContinuousStyle(short *mode,
13153            //                                         TextStyle *aStyle,
13154            //                                         TEHandle hTE)
13155            //       THREEWORDINLINE(0x3F3C, 0x000A, 0xA83D);
13156            //   Stack: sp+0 selector, sp+2 hTE (4), sp+6 aStyle* (4),
13157            //          sp+10 mode* (4), sp+14 Boolean result slot.
13158            //   Per IM:Text 1993 p. 2-102: returns TRUE for unstyled edit
13159            //   records and reports the global style attributes for the
13160            //   mode bits requested by *mode (font=1, face=2, size=4,
13161            //   color=8).
13162            //
13163            // Selector $000E TEFeatureFlag is FUNCTION (short result):
13164            //   EXTERN_API(short) TEFeatureFlag(short feature, short action,
13165            //                                   TEHandle hTE)
13166            //       THREEWORDINLINE(0x3F3C, 0x000E, 0xA83D);
13167            //   Stack: sp+0 selector, sp+2 hTE (4), sp+6 action (2),
13168            //          sp+8 feature (2), sp+10 short result slot.
13169            //   Per IM:VI 15-22: turns features on/off or tests them;
13170            //   returns the PRIOR setting of the bit (which, for
13171            //   teBitTest=-1, coincides with the current setting since
13172            //   the bit is not mutated). Action codes are teBitClear=0,
13173            //   teBitSet=1, teBitTest=-1.
13174            //
13175            // Contract test coverage (this module):
13176            //   te_dispatch_feature_flag_test_action_returns_current_state
13177            //   te_dispatch_feature_flag_tracks_auto_scroll_state
13178            //     (teBitSet on default-off feature returns prior 0 and mutates the bit)
13179            //   tefeatureflag_clear_action_returns_prior_one_state_and_clears_bit
13180            //     (teBitClear on previously-set feature returns prior 1 and clears the bit)
13181            //   te_dispatch_continuous_style_returns_unstyled_record_style
13182            //   tedispatch_function_protocol_consumes_threewordinline_stack_frame_for_tefeatureflag
13183            //   teautoview_and_tefeatureflag_observe_shared_autoscroll_state
13184            (true, 0x03D) => {
13185                let sp = cpu.read_reg(Register::A7);
13186                let selector = bus.read_word(sp);
13187                if trace_textedit_enabled() {
13188                    eprintln!("[TE] TEDispatch selector=${:04X} sp=${:08X}", selector, sp);
13189                }
13190                match selector {
13191                    0x0000 => {
13192                        // TEStylePaste ($A83D, selector $0000)
13193                        // PROCEDURE TEStylePaste(hTE: TEHandle);
13194                        cpu.write_reg(Register::A7, sp + 6);
13195                    }
13196                    0x0001 => {
13197                        // TESetStyle ($A83D, selector $0001)
13198                        // Sets the current selection's style in a styled edit record.
13199                        // PROCEDURE TESetStyle(mode: INTEGER; newStyle: TextStyle; redraw: BOOLEAN; hTE: TEHandle);
13200                        // Inside Macintosh Volume VI, 15-32
13201                        let te_handle = bus.read_long(sp + 2);
13202                        // Pascal BOOLEAN in high byte (MPW C convention).
13203                        let redraw = bus.read_byte(sp + 6) != 0;
13204                        let style_ptr = bus.read_long(sp + 8);
13205                        let mode = bus.read_word(sp + 12);
13206                        if trace_textedit_enabled() {
13207                            eprintln!(
13208                                "[TE] TESetStyle hTE=${:08X} mode=${:04X} redraw={} style_ptr=${:08X}",
13209                                te_handle, mode, redraw, style_ptr
13210                            );
13211                            if style_ptr != 0 {
13212                                let te_ptr = Self::te_record_ptr(bus, te_handle);
13213                                eprintln!(
13214                                    "[TE] TESetStyle values sel={}..{} font={} face=${:04X} size={} color=(${:04X},${:04X},${:04X})",
13215                                    if te_ptr != 0 { bus.read_word(te_ptr + Self::TE_SEL_START_OFFSET) } else { 0 },
13216                                    if te_ptr != 0 { bus.read_word(te_ptr + Self::TE_SEL_END_OFFSET) } else { 0 },
13217                                    bus.read_word(style_ptr) as i16,
13218                                    bus.read_byte(style_ptr + 2),
13219                                    bus.read_word(style_ptr + 4) as i16,
13220                                    bus.read_word(style_ptr + 6),
13221                                    bus.read_word(style_ptr + 8),
13222                                    bus.read_word(style_ptr + 10),
13223                                );
13224                            }
13225                        }
13226                        if style_ptr != 0 {
13227                            let te_ptr = Self::te_record_ptr(bus, te_handle);
13228                            if te_ptr != 0 {
13229                                let insertion_point = bus
13230                                    .read_word(te_ptr + Self::TE_SEL_START_OFFSET)
13231                                    == bus.read_word(te_ptr + Self::TE_SEL_END_OFFSET);
13232                                if insertion_point && Self::te_is_styled_record(bus, te_ptr) {
13233                                    Self::te_set_null_style(bus, te_handle, mode, style_ptr);
13234                                } else {
13235                                    let style_handle = Self::te_style_handle(bus, te_handle);
13236                                    if style_handle != 0 {
13237                                        let style_ptr_record = bus.read_long(style_handle);
13238                                        if style_ptr_record != 0 {
13239                                            let table_handle = bus.read_long(
13240                                                style_ptr_record
13241                                                    + Self::TE_STYLE_STYLE_TABLE_OFFSET,
13242                                            );
13243                                            let table_ptr = if table_handle != 0 {
13244                                                bus.read_long(table_handle)
13245                                            } else {
13246                                                0
13247                                            };
13248                                            if table_ptr != 0 {
13249                                                if (mode & 0x0001) != 0 {
13250                                                    bus.write_word(
13251                                                        table_ptr + Self::ST_ELEMENT_FONT_OFFSET,
13252                                                        bus.read_word(style_ptr),
13253                                                    );
13254                                                }
13255                                                if (mode & 0x0002) != 0 {
13256                                                    bus.write_byte(
13257                                                        table_ptr + Self::ST_ELEMENT_FACE_OFFSET,
13258                                                        bus.read_byte(style_ptr + 2),
13259                                                    );
13260                                                }
13261                                                if (mode & 0x0004) != 0 {
13262                                                    bus.write_word(
13263                                                        table_ptr + Self::ST_ELEMENT_SIZE_OFFSET,
13264                                                        bus.read_word(style_ptr + 4),
13265                                                    );
13266                                                }
13267                                                if (mode & 0x0008) != 0 {
13268                                                    bus.write_word(
13269                                                        table_ptr + Self::ST_ELEMENT_COLOR_OFFSET,
13270                                                        bus.read_word(style_ptr + 6),
13271                                                    );
13272                                                    bus.write_word(
13273                                                        table_ptr
13274                                                            + Self::ST_ELEMENT_COLOR_OFFSET
13275                                                            + 2,
13276                                                        bus.read_word(style_ptr + 8),
13277                                                    );
13278                                                    bus.write_word(
13279                                                        table_ptr
13280                                                            + Self::ST_ELEMENT_COLOR_OFFSET
13281                                                            + 4,
13282                                                        bus.read_word(style_ptr + 10),
13283                                                    );
13284                                                }
13285
13286                                                let resolved_font = bus.read_word(
13287                                                    table_ptr + Self::ST_ELEMENT_FONT_OFFSET,
13288                                                )
13289                                                    as i16;
13290                                                let resolved_size = bus.read_word(
13291                                                    table_ptr + Self::ST_ELEMENT_SIZE_OFFSET,
13292                                                )
13293                                                    as i16;
13294                                                let metrics = get_font_metrics(
13295                                                    resolved_font,
13296                                                    Self::font_lookup_size(resolved_size),
13297                                                );
13298                                                let line_height = metrics.ascent
13299                                                    + metrics.descent
13300                                                    + metrics.leading;
13301                                                bus.write_word(
13302                                                    table_ptr + Self::ST_ELEMENT_HEIGHT_OFFSET,
13303                                                    line_height as u16,
13304                                                );
13305                                                bus.write_word(
13306                                                    table_ptr + Self::ST_ELEMENT_ASCENT_OFFSET,
13307                                                    metrics.ascent as u16,
13308                                                );
13309
13310                                                let lh_handle = bus.read_long(
13311                                                    style_ptr_record
13312                                                        + Self::TE_STYLE_LH_TABLE_OFFSET,
13313                                                );
13314                                                let lh_ptr = if lh_handle != 0 {
13315                                                    bus.read_long(lh_handle)
13316                                                } else {
13317                                                    0
13318                                                };
13319                                                if lh_ptr != 0 {
13320                                                    bus.write_word(
13321                                                        lh_ptr + Self::LH_ELEMENT_HEIGHT_OFFSET,
13322                                                        line_height as u16,
13323                                                    );
13324                                                    bus.write_word(
13325                                                        lh_ptr + Self::LH_ELEMENT_ASCENT_OFFSET,
13326                                                        metrics.ascent as u16,
13327                                                    );
13328                                                }
13329                                            }
13330                                        }
13331                                    } else {
13332                                        if (mode & 0x0001) != 0 {
13333                                            bus.write_word(
13334                                                te_ptr + Self::TE_TX_FONT_OFFSET,
13335                                                bus.read_word(style_ptr),
13336                                            );
13337                                        }
13338                                        if (mode & 0x0002) != 0 {
13339                                            bus.write_byte(
13340                                                te_ptr + Self::TE_TX_FACE_OFFSET,
13341                                                bus.read_byte(style_ptr + 2),
13342                                            );
13343                                        }
13344                                        if (mode & 0x0004) != 0 {
13345                                            bus.write_word(
13346                                                te_ptr + Self::TE_TX_SIZE_OFFSET,
13347                                                bus.read_word(style_ptr + 4),
13348                                            );
13349                                        }
13350
13351                                        let resolved_font =
13352                                            bus.read_word(te_ptr + Self::TE_TX_FONT_OFFSET) as i16;
13353                                        let resolved_size =
13354                                            bus.read_word(te_ptr + Self::TE_TX_SIZE_OFFSET) as i16;
13355                                        let metrics = get_font_metrics(
13356                                            resolved_font,
13357                                            Self::font_lookup_size(resolved_size),
13358                                        );
13359                                        bus.write_word(
13360                                            te_ptr + Self::TE_LINE_HEIGHT_OFFSET,
13361                                            (metrics.ascent + metrics.descent + metrics.leading)
13362                                                as u16,
13363                                        );
13364                                        bus.write_word(
13365                                            te_ptr + Self::TE_FONT_ASCENT_OFFSET,
13366                                            metrics.ascent as u16,
13367                                        );
13368                                    }
13369                                }
13370                            }
13371                        }
13372                        let _ = redraw;
13373                        cpu.write_reg(Register::A7, sp + 14);
13374                    }
13375                    0x0002 => {
13376                        // TEReplaceStyle ($A83D, selector $0002)
13377                        // PROCEDURE TEReplaceStyle(mode: INTEGER;
13378                        //     oldStyle, newStyle: TextStyle;
13379                        //     redraw: BOOLEAN; hTE: TEHandle);
13380                        // Inside Macintosh Volume V, V-271..V-272.
13381                        // MPW C glue passes both TextStyle records by
13382                        // pointer (not by value), giving an 18-byte arg
13383                        // frame: selector(2) + hTE(4) + redraw(2) +
13384                        // newStyle ptr(4) + oldStyle ptr(4) + mode(2).
13385                        let te_handle = bus.read_long(sp + 2);
13386                        let _redraw = bus.read_byte(sp + 6) != 0;
13387                        let new_style_ptr = bus.read_long(sp + 8);
13388                        let old_style_ptr = bus.read_long(sp + 12);
13389                        let mode = bus.read_word(sp + 16);
13390                        if old_style_ptr != 0 && new_style_ptr != 0 {
13391                            let style_handle = Self::te_style_handle(bus, te_handle);
13392                            if style_handle != 0 {
13393                                let style_ptr_record = bus.read_long(style_handle);
13394                                if style_ptr_record != 0 {
13395                                    let table_handle = bus.read_long(
13396                                        style_ptr_record + Self::TE_STYLE_STYLE_TABLE_OFFSET,
13397                                    );
13398                                    let table_ptr = if table_handle != 0 {
13399                                        bus.read_long(table_handle)
13400                                    } else {
13401                                        0
13402                                    };
13403                                    if table_ptr != 0 {
13404                                        // Per IM:V V-270, replace only when the
13405                                        // existing style's selected attributes
13406                                        // match oldStyle exactly. With Systemless's
13407                                        // single-element style table this collapses
13408                                        // to one comparison.
13409                                        let mut matches_old = true;
13410                                        if (mode & 0x0001) != 0
13411                                            && bus
13412                                                .read_word(table_ptr + Self::ST_ELEMENT_FONT_OFFSET)
13413                                                != bus.read_word(old_style_ptr)
13414                                        {
13415                                            matches_old = false;
13416                                        }
13417                                        if (mode & 0x0002) != 0
13418                                            && bus
13419                                                .read_byte(table_ptr + Self::ST_ELEMENT_FACE_OFFSET)
13420                                                != bus.read_byte(old_style_ptr + 2)
13421                                        {
13422                                            matches_old = false;
13423                                        }
13424                                        if (mode & 0x0004) != 0
13425                                            && bus
13426                                                .read_word(table_ptr + Self::ST_ELEMENT_SIZE_OFFSET)
13427                                                != bus.read_word(old_style_ptr + 4)
13428                                        {
13429                                            matches_old = false;
13430                                        }
13431                                        if (mode & 0x0008) != 0
13432                                            && (bus.read_word(
13433                                                table_ptr + Self::ST_ELEMENT_COLOR_OFFSET,
13434                                            ) != bus.read_word(old_style_ptr + 6)
13435                                                || bus.read_word(
13436                                                    table_ptr + Self::ST_ELEMENT_COLOR_OFFSET + 2,
13437                                                ) != bus.read_word(old_style_ptr + 8)
13438                                                || bus.read_word(
13439                                                    table_ptr + Self::ST_ELEMENT_COLOR_OFFSET + 4,
13440                                                ) != bus.read_word(old_style_ptr + 10))
13441                                        {
13442                                            matches_old = false;
13443                                        }
13444                                        if matches_old {
13445                                            if (mode & 0x0001) != 0 {
13446                                                bus.write_word(
13447                                                    table_ptr + Self::ST_ELEMENT_FONT_OFFSET,
13448                                                    bus.read_word(new_style_ptr),
13449                                                );
13450                                            }
13451                                            if (mode & 0x0002) != 0 {
13452                                                bus.write_byte(
13453                                                    table_ptr + Self::ST_ELEMENT_FACE_OFFSET,
13454                                                    bus.read_byte(new_style_ptr + 2),
13455                                                );
13456                                            }
13457                                            if (mode & 0x0004) != 0 {
13458                                                bus.write_word(
13459                                                    table_ptr + Self::ST_ELEMENT_SIZE_OFFSET,
13460                                                    bus.read_word(new_style_ptr + 4),
13461                                                );
13462                                            }
13463                                            if (mode & 0x0008) != 0 {
13464                                                bus.write_word(
13465                                                    table_ptr + Self::ST_ELEMENT_COLOR_OFFSET,
13466                                                    bus.read_word(new_style_ptr + 6),
13467                                                );
13468                                                bus.write_word(
13469                                                    table_ptr + Self::ST_ELEMENT_COLOR_OFFSET + 2,
13470                                                    bus.read_word(new_style_ptr + 8),
13471                                                );
13472                                                bus.write_word(
13473                                                    table_ptr + Self::ST_ELEMENT_COLOR_OFFSET + 4,
13474                                                    bus.read_word(new_style_ptr + 10),
13475                                                );
13476                                            }
13477                                        }
13478                                    }
13479                                }
13480                            }
13481                        }
13482                        let _ = mode;
13483                        cpu.write_reg(Register::A7, sp + 18);
13484                    }
13485                    0x0003 => {
13486                        // TEGetStyle ($A83D, selector $0003)
13487                        // PROCEDURE TEGetStyle(sel: INTEGER; VAR attrs: TextStyle;
13488                        //     VAR lineHeight: INTEGER; VAR fontAscent: INTEGER; hTE: TEHandle);
13489                        let te_handle = bus.read_long(sp + 2);
13490                        let font_ascent_ptr = bus.read_long(sp + 6);
13491                        let line_height_ptr = bus.read_long(sp + 10);
13492                        let attrs_ptr = bus.read_long(sp + 14);
13493                        let _sel = bus.read_word(sp + 18) as i16;
13494                        let (font, face, size, color, line_height, font_ascent) =
13495                            self.te_primary_style(bus, te_handle);
13496                        if attrs_ptr != 0 {
13497                            bus.write_word(attrs_ptr, font as u16);
13498                            bus.write_word(attrs_ptr + 2, face as u16);
13499                            bus.write_word(attrs_ptr + 4, size as u16);
13500                            bus.write_word(attrs_ptr + 6, color.0);
13501                            bus.write_word(attrs_ptr + 8, color.1);
13502                            bus.write_word(attrs_ptr + 10, color.2);
13503                        }
13504                        if line_height_ptr != 0 {
13505                            bus.write_word(line_height_ptr, line_height as u16);
13506                        }
13507                        if font_ascent_ptr != 0 {
13508                            bus.write_word(font_ascent_ptr, font_ascent as u16);
13509                        }
13510                        cpu.write_reg(Register::A7, sp + 20);
13511                    }
13512                    0x0004 => {
13513                        // TEGetStyleHandle ($A83D, selector $0004)
13514                        // FUNCTION TEGetStyleHandle(hTE: TEHandle): TEStyleHandle;
13515                        let te_handle = bus.read_long(sp + 2);
13516                        bus.write_long(sp + 6, Self::te_style_handle(bus, te_handle));
13517                        cpu.write_reg(Register::A7, sp + 6);
13518                    }
13519                    0x0005 => {
13520                        // TESetStyleHandle ($A83D, selector $0005)
13521                        // PROCEDURE TESetStyleHandle(theHandle: TEStyleHandle; hTE: TEHandle);
13522                        let te_handle = bus.read_long(sp + 2);
13523                        let style_handle = bus.read_long(sp + 6);
13524                        Self::te_write_style_handle(bus, te_handle, style_handle);
13525                        cpu.write_reg(Register::A7, sp + 10);
13526                    }
13527                    0x0006 => {
13528                        // TEGetStyleScrapHandle ($A83D, selector $0006)
13529                        // FUNCTION TEGetStyleScrapHandle(hTE: TEHandle): STScrpHandle;
13530                        let te_handle = bus.read_long(sp + 2);
13531                        let style_handle = Self::te_style_handle(bus, te_handle);
13532                        let mut result = 0;
13533                        if style_handle != 0 {
13534                            let style_ptr = bus.read_long(style_handle);
13535                            if style_ptr != 0 {
13536                                let null_style_handle =
13537                                    bus.read_long(style_ptr + Self::TE_STYLE_NULL_STYLE_OFFSET);
13538                                if null_style_handle != 0 {
13539                                    let null_style_ptr = bus.read_long(null_style_handle);
13540                                    if null_style_ptr != 0 {
13541                                        let scrap_handle = bus.read_long(
13542                                            null_style_ptr + Self::NULL_STYLE_SCRAP_OFFSET,
13543                                        );
13544                                        result = Self::duplicate_handle_data(bus, scrap_handle);
13545                                    }
13546                                }
13547                            }
13548                        }
13549                        bus.write_long(sp + 6, result);
13550                        cpu.write_reg(Register::A7, sp + 6);
13551                    }
13552                    0x0007 => {
13553                        // TEStyleInsert ($A83D, selector $0007)
13554                        // PROCEDURE TEStyleInsert(text: Ptr; length: LONGINT; hST: StScrpHandle; hTE: TEHandle);
13555                        let te_handle = bus.read_long(sp + 2);
13556                        let style_scrap = bus.read_long(sp + 6);
13557                        let length = bus.read_long(sp + 10) as usize;
13558                        let text_ptr = bus.read_long(sp + 14);
13559                        if text_ptr != 0 && length != 0 {
13560                            let text = bus.read_bytes(text_ptr, length);
13561                            let insert_start = {
13562                                let te_ptr = Self::te_record_ptr(bus, te_handle);
13563                                let text_len = Self::te_text_length(bus, te_handle);
13564                                if te_ptr != 0 {
13565                                    let mut sel_start =
13566                                        bus.read_word(te_ptr + Self::TE_SEL_START_OFFSET) as usize;
13567                                    let mut sel_end =
13568                                        bus.read_word(te_ptr + Self::TE_SEL_END_OFFSET) as usize;
13569                                    sel_start = sel_start.min(text_len);
13570                                    sel_end = sel_end.min(text_len);
13571                                    if sel_end < sel_start {
13572                                        std::mem::swap(&mut sel_start, &mut sel_end);
13573                                    }
13574                                    sel_start
13575                                } else {
13576                                    0
13577                                }
13578                            };
13579                            let preview_len = text.len().min(64);
13580                            if trace_textedit_enabled() {
13581                                eprintln!(
13582                                    "[TE] TEStyleInsert hTE=${:08X} hST=${:08X} len={} text_ptr=${:08X} preview={:?}",
13583                                    te_handle,
13584                                    style_scrap,
13585                                    length,
13586                                    text_ptr,
13587                                    String::from_utf8_lossy(&text[..preview_len])
13588                                );
13589                            }
13590                            self.te_insert_text(bus, te_handle, &text);
13591                            // With NIL hST, TEStyleInsert follows TEInsert;
13592                            // insertion-point attributes previously stored by
13593                            // TESetStyle come from the null scrap. Inside
13594                            // Macintosh Volume V, V-274; Inside Macintosh:
13595                            // Text 1993, pp. 2-61 and 2-82.
13596                            let effective_style_scrap = if style_scrap != 0 {
13597                                style_scrap
13598                            } else {
13599                                Self::te_null_style_scrap_handle(bus, te_handle)
13600                            };
13601                            if self.te_apply_style_scrap_to_range(
13602                                bus,
13603                                te_handle,
13604                                effective_style_scrap,
13605                                insert_start,
13606                                text.len(),
13607                            ) {
13608                                self.te_recalculate_layout(bus, te_handle);
13609                            }
13610                            self.draw_te_contents(cpu, bus, te_handle);
13611                        } else if text_ptr != 0 && bus.read_byte(text_ptr) == 0 {
13612                            // The ROM styled-TextEdit path leaves an exhausted
13613                            // NUL-terminated insertion with no continuation.
13614                            // Marathon keeps that continuation in A3; retaining
13615                            // its stale pointer makes the guest retry this
13616                            // zero-length insertion forever.
13617                            cpu.write_reg(Register::A3, 0);
13618                        }
13619                        cpu.write_reg(Register::A7, sp + 18);
13620                    }
13621                    0x0008 => {
13622                        // TEGetPoint ($A83D, selector $0008)
13623                        // Returns the point for a character offset within the edit record.
13624                        // FUNCTION TEGetPoint(offset: INTEGER; hTE: TEHandle): Point;
13625                        // Inside Macintosh Volume VI, 15-31
13626                        let te_handle = bus.read_long(sp + 2);
13627                        let offset = bus.read_word(sp + 6) as i16;
13628                        let te_ptr = Self::te_record_ptr(bus, te_handle);
13629                        let result_addr = sp + 8;
13630                        if te_ptr != 0 {
13631                            let text_len = Self::te_text_length(bus, te_handle);
13632                            let clamped = i32::from(offset).clamp(0, text_len as i32) as usize;
13633                            let line_index = Self::te_char_to_line_index(bus, te_handle, clamped);
13634                            let line_start = Self::te_line_starts(bus, te_handle)
13635                                .get(line_index)
13636                                .copied()
13637                                .unwrap_or(0);
13638                            let (top, x) = self.te_char_to_point(bus, te_handle, clamped);
13639                            let y = top.saturating_add(Self::te_ascent_for_line(
13640                                bus, te_handle, line_index,
13641                            ));
13642                            if trace_textedit_enabled() {
13643                                let starts = Self::te_line_starts(bus, te_handle);
13644                                let pc = cpu.read_reg(Register::PC);
13645                                eprintln!(
13646                                    "[TE] TEGetPoint hTE=${:08X} offset={} line={} start={} point_offset={} point=({}, {}) pc=${:08X} next=[{:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X}]",
13647                                    te_handle,
13648                                    clamped,
13649                                    line_index,
13650                                    line_start,
13651                                    clamped,
13652                                    y,
13653                                    x,
13654                                    pc,
13655                                    bus.read_word(pc),
13656                                    bus.read_word(pc + 2),
13657                                    bus.read_word(pc + 4),
13658                                    bus.read_word(pc + 6),
13659                                    bus.read_word(pc + 8),
13660                                    bus.read_word(pc + 10),
13661                                    bus.read_word(pc + 12),
13662                                    bus.read_word(pc + 14),
13663                                    bus.read_word(pc + 16),
13664                                    bus.read_word(pc + 18),
13665                                    bus.read_word(pc + 20),
13666                                    bus.read_word(pc + 22),
13667                                    bus.read_word(pc + 24),
13668                                    bus.read_word(pc + 26),
13669                                    bus.read_word(pc + 28),
13670                                    bus.read_word(pc + 30),
13671                                    bus.read_word(pc + 32),
13672                                    bus.read_word(pc + 34),
13673                                    bus.read_word(pc + 36),
13674                                    bus.read_word(pc + 38)
13675                                );
13676                                eprintln!(
13677                                    "[TE] TEGetPoint layout nLines={} teLength={} lineStarts={:?}",
13678                                    starts.len().saturating_sub(1),
13679                                    text_len,
13680                                    starts
13681                                );
13682                                eprintln!(
13683                                    "[TE] TEGetPoint helper@35614=[{:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X}]",
13684                                    bus.read_word(0x0003_5614),
13685                                    bus.read_word(0x0003_5616),
13686                                    bus.read_word(0x0003_5618),
13687                                    bus.read_word(0x0003_561A),
13688                                    bus.read_word(0x0003_561C),
13689                                    bus.read_word(0x0003_561E),
13690                                    bus.read_word(0x0003_5620),
13691                                    bus.read_word(0x0003_5622),
13692                                    bus.read_word(0x0003_5624),
13693                                    bus.read_word(0x0003_5626),
13694                                    bus.read_word(0x0003_5628),
13695                                    bus.read_word(0x0003_562A)
13696                                );
13697                                let table_base = cpu.read_reg(Register::A5).wrapping_sub(0x37B4);
13698                                let rect_table = bus.read_long(table_base);
13699                                let rect_ptr = rect_table.wrapping_add(21 * 8);
13700                                let a6 = cpu.read_reg(Register::A6);
13701                                let ret = bus.read_long(a6 + 4);
13702                                eprintln!(
13703                                    "[TE] TEGetPoint rect21 table=${:08X} rect_ptr=${:08X} rect=({},{},{},{}) a4=${:08X} d5={} d6={} d7={} a6=${:08X} ret=${:08X}",
13704                                    rect_table,
13705                                    rect_ptr,
13706                                    bus.read_word(rect_ptr) as i16,
13707                                    bus.read_word(rect_ptr + 2) as i16,
13708                                    bus.read_word(rect_ptr + 4) as i16,
13709                                    bus.read_word(rect_ptr + 6) as i16,
13710                                    cpu.read_reg(Register::A4),
13711                                    cpu.read_reg(Register::D5) as i32,
13712                                    cpu.read_reg(Register::D6) as i32,
13713                                    cpu.read_reg(Register::D7) as i32,
13714                                    a6,
13715                                    ret
13716                                );
13717                                eprintln!(
13718                                    "[TE] TEGetPoint caller@{:08X}=[{:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X}]",
13719                                    ret,
13720                                    bus.read_word(ret),
13721                                    bus.read_word(ret + 2),
13722                                    bus.read_word(ret + 4),
13723                                    bus.read_word(ret + 6),
13724                                    bus.read_word(ret + 8),
13725                                    bus.read_word(ret + 10),
13726                                    bus.read_word(ret + 12),
13727                                    bus.read_word(ret + 14),
13728                                    bus.read_word(ret + 16),
13729                                    bus.read_word(ret + 18),
13730                                    bus.read_word(ret + 20),
13731                                    bus.read_word(ret + 22)
13732                                );
13733                                let caller_start = ret.wrapping_sub(0x20);
13734                                eprintln!(
13735                                    "[TE] TEGetPoint caller_pre@{:08X}=[{:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X}]",
13736                                    caller_start,
13737                                    bus.read_word(caller_start),
13738                                    bus.read_word(caller_start + 2),
13739                                    bus.read_word(caller_start + 4),
13740                                    bus.read_word(caller_start + 6),
13741                                    bus.read_word(caller_start + 8),
13742                                    bus.read_word(caller_start + 10),
13743                                    bus.read_word(caller_start + 12),
13744                                    bus.read_word(caller_start + 14),
13745                                    bus.read_word(caller_start + 16),
13746                                    bus.read_word(caller_start + 18),
13747                                    bus.read_word(caller_start + 20),
13748                                    bus.read_word(caller_start + 22),
13749                                    bus.read_word(caller_start + 24),
13750                                    bus.read_word(caller_start + 26),
13751                                    bus.read_word(caller_start + 28),
13752                                    bus.read_word(caller_start + 30)
13753                                );
13754                                eprintln!(
13755                                    "[TE] TEGetPoint caller_block@00035F7C=[{:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X}]",
13756                                    bus.read_word(0x0003_5F7C),
13757                                    bus.read_word(0x0003_5F7E),
13758                                    bus.read_word(0x0003_5F80),
13759                                    bus.read_word(0x0003_5F82),
13760                                    bus.read_word(0x0003_5F84),
13761                                    bus.read_word(0x0003_5F86),
13762                                    bus.read_word(0x0003_5F88),
13763                                    bus.read_word(0x0003_5F8A),
13764                                    bus.read_word(0x0003_5F8C),
13765                                    bus.read_word(0x0003_5F8E),
13766                                    bus.read_word(0x0003_5F90),
13767                                    bus.read_word(0x0003_5F92),
13768                                    bus.read_word(0x0003_5F94),
13769                                    bus.read_word(0x0003_5F96),
13770                                    bus.read_word(0x0003_5F98),
13771                                    bus.read_word(0x0003_5F9A),
13772                                    bus.read_word(0x0003_5F9C),
13773                                    bus.read_word(0x0003_5F9E),
13774                                    bus.read_word(0x0003_5FA0),
13775                                    bus.read_word(0x0003_5FA2),
13776                                    bus.read_word(0x0003_5FA4),
13777                                    bus.read_word(0x0003_5FA6),
13778                                    bus.read_word(0x0003_5FA8),
13779                                    bus.read_word(0x0003_5FAA),
13780                                    bus.read_word(0x0003_5FAC),
13781                                    bus.read_word(0x0003_5FAE),
13782                                    bus.read_word(0x0003_5FB0),
13783                                    bus.read_word(0x0003_5FB2),
13784                                    bus.read_word(0x0003_5FB4),
13785                                    bus.read_word(0x0003_5FB6),
13786                                    bus.read_word(0x0003_5FB8),
13787                                    bus.read_word(0x0003_5FBA)
13788                                );
13789                                eprintln!(
13790                                    "[TE] TEGetPoint branch_c6@00035FC6=[{:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X}] branch_e2@00035FE2=[{:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X}]",
13791                                    bus.read_word(0x0003_5FC6),
13792                                    bus.read_word(0x0003_5FC8),
13793                                    bus.read_word(0x0003_5FCA),
13794                                    bus.read_word(0x0003_5FCC),
13795                                    bus.read_word(0x0003_5FCE),
13796                                    bus.read_word(0x0003_5FD0),
13797                                    bus.read_word(0x0003_5FD2),
13798                                    bus.read_word(0x0003_5FD4),
13799                                    bus.read_word(0x0003_5FE2),
13800                                    bus.read_word(0x0003_5FE4),
13801                                    bus.read_word(0x0003_5FE6),
13802                                    bus.read_word(0x0003_5FE8),
13803                                    bus.read_word(0x0003_5FEA),
13804                                    bus.read_word(0x0003_5FEC),
13805                                    bus.read_word(0x0003_5FEE),
13806                                    bus.read_word(0x0003_5FF0)
13807                                );
13808                                eprintln!(
13809                                    "[TE] TEGetPoint helper_fn@00036B9A=[{:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X}]",
13810                                    bus.read_word(0x0003_6B9A),
13811                                    bus.read_word(0x0003_6B9C),
13812                                    bus.read_word(0x0003_6B9E),
13813                                    bus.read_word(0x0003_6BA0),
13814                                    bus.read_word(0x0003_6BA2),
13815                                    bus.read_word(0x0003_6BA4),
13816                                    bus.read_word(0x0003_6BA6),
13817                                    bus.read_word(0x0003_6BA8),
13818                                    bus.read_word(0x0003_6BAA),
13819                                    bus.read_word(0x0003_6BAC),
13820                                    bus.read_word(0x0003_6BAE),
13821                                    bus.read_word(0x0003_6BB0),
13822                                    bus.read_word(0x0003_6BB2),
13823                                    bus.read_word(0x0003_6BB4),
13824                                    bus.read_word(0x0003_6BB6),
13825                                    bus.read_word(0x0003_6BB8),
13826                                    bus.read_word(0x0003_6BBA),
13827                                    bus.read_word(0x0003_6BBC),
13828                                    bus.read_word(0x0003_6BBE),
13829                                    bus.read_word(0x0003_6BC0),
13830                                    bus.read_word(0x0003_6BC2),
13831                                    bus.read_word(0x0003_6BC4),
13832                                    bus.read_word(0x0003_6BC6),
13833                                    bus.read_word(0x0003_6BC8)
13834                                );
13835                            }
13836                            bus.write_word(result_addr, y as u16);
13837                            bus.write_word(result_addr + 2, x as u16);
13838                        } else {
13839                            bus.write_word(result_addr, 0);
13840                            bus.write_word(result_addr + 2, 0);
13841                        }
13842                        cpu.write_reg(Register::A7, result_addr);
13843                    }
13844                    0x0009 => {
13845                        // TEGetHeight ($A83D, selector $0009)
13846                        // Returns the total height of the requested line range.
13847                        // FUNCTION TEGetHeight(endLine: LONGINT; startLine: LONGINT; hTE: TEHandle): LONGINT;
13848                        // Text 1993, 2-90
13849                        let te_handle = bus.read_long(sp + 2);
13850                        let mut start_line = bus.read_long(sp + 6) as i32;
13851                        let mut end_line = bus.read_long(sp + 10) as i32;
13852                        let te_ptr = Self::te_record_ptr(bus, te_handle);
13853                        let n_lines = if te_ptr != 0 {
13854                            bus.read_word(te_ptr + Self::TE_N_LINES_OFFSET) as i32
13855                        } else {
13856                            0
13857                        };
13858                        if start_line > 0 {
13859                            start_line -= 1;
13860                        } else {
13861                            start_line = 0;
13862                        }
13863                        end_line = end_line.min(n_lines);
13864                        if end_line < 0 {
13865                            end_line = 0;
13866                        } else if end_line > 0 {
13867                            end_line -= 1;
13868                        }
13869                        if start_line > end_line {
13870                            std::mem::swap(&mut start_line, &mut end_line);
13871                        }
13872
13873                        let text_bytes = Self::te_text_bytes(bus, te_handle);
13874                        let line_starts = Self::te_line_starts(bus, te_handle);
13875                        if !text_bytes.is_empty() {
13876                            while end_line >= start_line {
13877                                let current = end_line as usize;
13878                                let Some(&line_start) = line_starts.get(current) else {
13879                                    break;
13880                                };
13881                                let line_end = line_starts
13882                                    .get(current + 1)
13883                                    .copied()
13884                                    .unwrap_or(text_bytes.len());
13885                                let blank_trailing_line = current + 1 == line_starts.len() - 1
13886                                    && line_start < line_end
13887                                    && text_bytes[line_start..line_end]
13888                                        .iter()
13889                                        .all(|&b| matches!(b, b'\r' | b'\n'));
13890                                if blank_trailing_line {
13891                                    end_line -= 1;
13892                                } else {
13893                                    break;
13894                                }
13895                            }
13896                        }
13897
13898                        let mut total_height = 0i32;
13899                        if end_line >= start_line {
13900                            for current_line in
13901                                start_line.max(0) as usize..=end_line.max(0) as usize
13902                            {
13903                                total_height += i32::from(Self::te_height_for_line(
13904                                    bus,
13905                                    te_handle,
13906                                    current_line,
13907                                ));
13908                            }
13909                        }
13910                        if trace_textedit_enabled() {
13911                            eprintln!(
13912                                "[TE] TEGetHeight hTE=${:08X} start_line={} end_line={} result={}",
13913                                te_handle, start_line, end_line, total_height
13914                            );
13915                        }
13916                        bus.write_long(sp + 14, total_height as u32);
13917                        cpu.write_reg(Register::A7, sp + 14);
13918                    }
13919                    0x000A => {
13920                        // TEContinuousStyle ($A83D, selector $000A)
13921                        // Returns the common style across the current selection.
13922                        // FUNCTION TEContinuousStyle(VAR mode: INTEGER; VAR aStyle: TextStyle; hTE: TEHandle): BOOLEAN;
13923                        // Inside Macintosh Volume VI, 15-34 to 15-35
13924                        let te_handle = bus.read_long(sp + 2);
13925                        let style_ptr = bus.read_long(sp + 6);
13926                        let mode_ptr = bus.read_long(sp + 10);
13927                        let result_addr = sp + 14;
13928                        let requested_mode = if mode_ptr != 0 {
13929                            bus.read_word(mode_ptr)
13930                        } else {
13931                            0
13932                        };
13933                        if style_ptr != 0 {
13934                            let (tx_font, tx_face, tx_size, color, _, _) =
13935                                self.te_primary_style(bus, te_handle);
13936                            if (requested_mode & 0x0001) != 0 {
13937                                bus.write_word(style_ptr, tx_font as u16);
13938                            }
13939                            if (requested_mode & 0x0002) != 0 {
13940                                bus.write_byte(style_ptr + 2, tx_face as u8);
13941                            }
13942                            if (requested_mode & 0x0004) != 0 {
13943                                bus.write_word(style_ptr + 4, tx_size as u16);
13944                            }
13945                            if (requested_mode & 0x0008) != 0 {
13946                                bus.write_word(style_ptr + 6, color.0);
13947                                bus.write_word(style_ptr + 8, color.1);
13948                                bus.write_word(style_ptr + 10, color.2);
13949                            }
13950                        }
13951                        bus.write_word(result_addr, 0xFFFF);
13952                        cpu.write_reg(Register::A7, result_addr);
13953                    }
13954                    0x000B => {
13955                        // TEUseStyleScrap ($A83D, selector $000B)
13956                        // Sets style data for the specified text range from a style scrap handle.
13957                        // PROCEDURE TEUseStyleScrap(rangeStart: LONGINT; rangeEnd: LONGINT;
13958                        //     newStyles: StScrpHandle; redraw: BOOLEAN; hTE: TEHandle);
13959                        // Inside Macintosh Volume VI, 15-35 to 15-36
13960                        let te_handle = bus.read_long(sp + 2);
13961                        // Pascal BOOLEAN in high byte (MPW C convention).
13962                        let redraw = bus.read_byte(sp + 6) != 0;
13963                        let new_styles = bus.read_long(sp + 8);
13964                        let range_end = bus.read_long(sp + 12) as usize;
13965                        let range_start = bus.read_long(sp + 16) as usize;
13966                        let (range_start, range_end) = if range_end < range_start {
13967                            (range_end, range_start)
13968                        } else {
13969                            (range_start, range_end)
13970                        };
13971                        if self.te_apply_style_scrap_to_range(
13972                            bus,
13973                            te_handle,
13974                            new_styles,
13975                            range_start,
13976                            range_end.saturating_sub(range_start),
13977                        ) {
13978                            self.te_recalculate_layout(bus, te_handle);
13979                            if redraw {
13980                                self.draw_te_contents(cpu, bus, te_handle);
13981                            }
13982                        }
13983                        cpu.write_reg(Register::A7, sp + 20);
13984                    }
13985                    0x000C => {
13986                        // TECustomHook ($A83D, selector $000C)
13987                        // Reads or replaces one of TextEdit's internal hook procedures.
13988                        // PROCEDURE TECustomHook(which: TEIntHook; VAR addr: ProcPtr; hTE: TEHandle);
13989                        // Inside Macintosh Volume VI, 15-25 to 15-26.
13990                        // Per IM:VI 15-26, addr is a VAR ProcPtr — on
13991                        // return it must hold the previous hook. Systemless
13992                        // does not invoke registered hooks (no guest-fn
13993                        // dispatch infrastructure), so we report "no
13994                        // previous hook" by writing 0 into *addr.
13995                        let addr_ptr = bus.read_long(sp + 6);
13996                        if addr_ptr != 0 {
13997                            bus.write_long(addr_ptr, 0);
13998                        }
13999                        cpu.write_reg(Register::A7, sp + 12);
14000                    }
14001                    0x000D => {
14002                        // TENumStyles ($A83D, selector $000D)
14003                        // Returns the number of style changes contained in the
14004                        // given range, counting one for the start of the range.
14005                        // FUNCTION TENumStyles(rangeStart: LONGINT; rangeEnd: LONGINT;
14006                        //                      hTE: TEHandle): LONGINT;
14007                        // Inside Macintosh Volume VI, 15-36.
14008                        // Per IM:VI 15-36, an unstyled record always
14009                        // returns 1. With Systemless's single-run styled
14010                        // record the in-range transitions count is also
14011                        // zero, so the +1 for the start-of-range gives 1.
14012                        let te_handle = bus.read_long(sp + 2);
14013                        let style_handle = Self::te_style_handle(bus, te_handle);
14014                        let mut count: u32 = 1;
14015                        if style_handle != 0 {
14016                            let style_ptr_record = bus.read_long(style_handle);
14017                            if style_ptr_record != 0 {
14018                                let n_runs = bus
14019                                    .read_word(style_ptr_record + Self::TE_STYLE_N_RUNS_OFFSET)
14020                                    as u32;
14021                                count = n_runs.max(1);
14022                            }
14023                        }
14024                        bus.write_long(sp + 14, count);
14025                        cpu.write_reg(Register::A7, sp + 14);
14026                    }
14027                    0x000E => {
14028                        // TEFeatureFlag ($A83D, selector $000E)
14029                        // Tests or changes a TextEdit feature bit and returns the previous state.
14030                        // FUNCTION TEFeatureFlag(feature: INTEGER; action: INTEGER; hTE: TEHandle): INTEGER;
14031                        // Inside Macintosh Volume VI, 15-22 to 15-24
14032                        let te_handle = bus.read_long(sp + 2);
14033                        let action = bus.read_word(sp + 6) as i16;
14034                        let feature = bus.read_word(sp + 8);
14035                        let was_set = self.te_feature_bit(te_handle, feature);
14036                        if matches!(
14037                            feature,
14038                            Self::TE_FEATURE_AUTO_SCROLL
14039                                | Self::TE_FEATURE_TEXT_BUFFERING
14040                                | Self::TE_FEATURE_OUTLINE_HILITE
14041                                | Self::TE_FEATURE_INLINE_INPUT
14042                                | Self::TE_FEATURE_USE_TEXT_SERVICES
14043                        ) {
14044                            match action {
14045                                Self::TE_BIT_CLEAR => {
14046                                    self.set_te_feature_bit(te_handle, feature, false)
14047                                }
14048                                Self::TE_BIT_SET => {
14049                                    self.set_te_feature_bit(te_handle, feature, true)
14050                                }
14051                                Self::TE_BIT_TEST => {}
14052                                _ => {}
14053                            }
14054                        }
14055                        bus.write_word(sp + 10, if was_set { 1 } else { 0 });
14056                        cpu.write_reg(Register::A7, sp + 10);
14057                    }
14058                    _ => return None,
14059                }
14060                Ok(())
14061            }
14062
14063            // TEDispose ($A9CD)
14064            // Disposes of the edit record and releases memory used by
14065            // the text and record structures.
14066            // PROCEDURE TEDispose(hTE: TEHandle);
14067            // Inside Macintosh Volume I, I-383 to I-384; Text 1993, 2-79
14068            //
14069            // Regression coverage:
14070            //   dialog::tests::tedispose_releases_te_record_text_handle_and_pops_arg
14071            // TEDispose ($A9CD): Frees TERec, hText, and style handles per IM:I I-383..I-384
14072            (true, 0x1CD) => {
14073                let sp = cpu.read_reg(Register::A7);
14074                let te_handle = bus.read_long(sp);
14075                if te_handle != 0 {
14076                    let te_ptr = bus.read_long(te_handle);
14077                    if te_ptr != 0 {
14078                        // Free the text handle and its data
14079                        let h_text = bus.read_long(te_ptr + Self::TE_HTEXT_OFFSET);
14080                        if h_text != 0 {
14081                            let text_ptr = bus.read_long(h_text);
14082                            if text_ptr != 0 {
14083                                bus.free(text_ptr);
14084                            }
14085                            bus.free(h_text);
14086                        }
14087                        // For styled records, free the style handle
14088                        if Self::te_is_styled_record(bus, te_ptr) {
14089                            let style_handle = bus.read_long(te_ptr + Self::TE_TX_FONT_OFFSET);
14090                            if style_handle != 0 {
14091                                let style_ptr = bus.read_long(style_handle);
14092                                if style_ptr != 0 {
14093                                    bus.free(style_ptr);
14094                                }
14095                                bus.free(style_handle);
14096                            }
14097                        }
14098                        bus.free(te_ptr);
14099                    }
14100                    bus.free(te_handle);
14101                    self.textedit_states.remove(&te_handle);
14102                }
14103                cpu.write_reg(Register::A7, sp + 4);
14104                Ok(())
14105            }
14106
14107            // TESetText ($A9CF)
14108            // PROCEDURE TESetText(text: Ptr; length: LONGINT; hTE: TEHandle);
14109            // Inside Macintosh Volume I (1985), p. I-378.
14110            //
14111            // IM:I I-378 verbatim: "TESetText sets the current text
14112            // contents of the edit record specified by hTE. The text
14113            // parameter points to the text, and the length parameter
14114            // contains the number of bytes."
14115            //
14116            // MPW Universal Headers `TextEdit.h` declares:
14117            //   EXTERN_API(void) TESetText(const void *text, long length,
14118            //                              TEHandle hTE) ONEWORDINLINE(0xA9CF);
14119            //
14120            // Pascal calling convention pushes args left-to-right (first
14121            // arg deepest). At trap entry the stack layout is therefore:
14122            //   sp+0  TEHandle hTE          (last pushed, shallowest)
14123            //   sp+4  LONGINT  length       (middle, 4 bytes)
14124            //   sp+8  Ptr      text         (first pushed, deepest)
14125            // The trap pops 12 bytes (no result slot — PROCEDURE).
14126            //
14127            // Empty-input semantics: per IM:I I-378 passing zero length
14128            // yields an empty edit record. Systemless additionally defends
14129            // against NIL text pointer (clears regardless of length);
14130            // the real Mac ROM (BasiliskII System 7.5.3) does NOT safely
14131            // handle NIL source pointers.
14132            //
14133            // Contract tests in this file:
14134            //   tesettext_copies_bytes_updates_length_and_pops_arguments
14135            //   tesettext_nil_or_zero_length_input_clears_text
14136            //   tesettext_replaces_prior_contents_via_sequential_call
14137            //   tesettext_balances_stackspace_with_pascal_protocol
14138            (true, 0x1CF) => {
14139                let sp = cpu.read_reg(Register::A7);
14140                let te_handle = bus.read_long(sp);
14141                let length = bus.read_long(sp + 4) as usize;
14142                let text_ptr = bus.read_long(sp + 8);
14143                if text_ptr != 0 && length != 0 {
14144                    let text = bus.read_bytes(text_ptr, length);
14145                    self.te_set_text_contents(bus, te_handle, &text);
14146                } else {
14147                    self.te_set_text_contents(bus, te_handle, &[]);
14148                }
14149                cpu.write_reg(Register::A7, sp + 12);
14150                Ok(())
14151            }
14152
14153            // TEGetText ($A9CB)
14154            // FUNCTION TEGetText(hTE: TEHandle): CharsHandle;
14155            // Inside Macintosh Volume I (1985), p. I-384.
14156            //
14157            // IM:I I-384 verbatim: "TEGetText returns a handle to the text
14158            // of the specified edit record. The result is the same as the
14159            // handle in the hText field of the edit record, but has the
14160            // CharsHandle data type, which is defined as:
14161            //
14162            //   TYPE CharsHandle = ^CharsPtr;
14163            //        CharsPtr    = ^Chars;
14164            //        Chars       = PACKED ARRAY[0..32000] OF CHAR;
14165            //
14166            // You can get the length of the text from the teLength field
14167            // of the edit record."
14168            //
14169            // Pascal FUNCTION calling convention: the caller pre-allocates
14170            // a 4-byte CharsHandle result slot at SP+4, pushes a 4-byte
14171            // TEHandle argument at SP+0. The trap reads the TEHandle,
14172            // looks up TERec.hText (offset TE_HTEXT_OFFSET = +18 per
14173            // IM:I I-379), writes that handle to the result slot, and
14174            // pops the 4-byte argument. The C wrapper then pops the
14175            // 4-byte result slot — net externally-observed SP delta is
14176            // zero.
14177            //
14178            // MPW Universal Headers `TextEdit.h` declares:
14179            //   EXTERN_API(CharsHandle) TEGetText(TEHandle hTE)
14180            //       ONEWORDINLINE(0xA9CB);
14181            (true, 0x1CB) => {
14182                let sp = cpu.read_reg(Register::A7);
14183                let te_handle = bus.read_long(sp);
14184                bus.write_long(sp + 4, Self::te_text_handle(bus, te_handle));
14185                cpu.write_reg(Register::A7, sp + 4);
14186                Ok(())
14187            }
14188
14189            // TETextBox ($A9CE)
14190            //
14191            // PROCEDURE TETextBox(text: Ptr; length: LONGINT;
14192            //                     box: Rect; align: INTEGER);
14193            //
14194            // Inside Macintosh: Text (1993), p. 2-88; alignment
14195            // constants on p. 2-87.
14196            //
14197            // MPW Universal Headers TextEdit.h:
14198            //   EXTERN_API(void) TETextBox(const void *text,
14199            //                              long          length,
14200            //                              const Rect *  box,
14201            //                              short         just)
14202            //                                       ONEWORDINLINE(0xA9CE);
14203            //
14204            // IM:Text 1993 p. 2-88: "TETextBox erases the specified
14205            //   rectangle and then draws the text into it... TETextBox
14206            //   creates a transient edit record, draws the text wrapped
14207            //   to fit the rectangle, and disposes of the record."
14208            // IM:Text 1993 p. 2-87 alignment constants:
14209            //   teJustLeft  =  0  (flush left — system default)
14210            //   teJustCenter =  1  (centered)
14211            //   teJustRight  = -1  (flush right)
14212            //   teForceLeft  = -2  (force flush left for right-to-left)
14213            //
14214            // Calling convention (MPW C canonical, the only one MPW
14215            // emits because the Universal Headers declaration uses
14216            // `const Rect *`):
14217            //   sp+ 0  short        just     (last pushed, shallowest)
14218            //   sp+ 2  Rect *       box      (4-byte pointer)
14219            //   sp+ 6  long         length   (4 bytes)
14220            //   sp+10  const void * text     (4 bytes, deepest)
14221            //   pop = 14 bytes; no result slot (PROCEDURE).
14222            //
14223            // (The Pascal canonical signature inlines the 8-byte Rect
14224            // by value for an 18-byte frame, but MPW C never produces
14225            // that form. Systemless's HLE only supports the MPW C
14226            // canonical layout; the in-Rust contract tests exercise
14227            // this layout exclusively.)
14228            //
14229            // Documented behaviors (BII System 7.5.3 ROM and Systemless
14230            // HLE produce the same boolean predicates):
14231            //   1. Erases the destination box before drawing (probe a
14232            //      pre-blackened pixel far from any glyph is WHITE after
14233            //      the call) — IM:Text 1993 p. 2-88.
14234            //   2. Word-wraps when text exceeds rect width (40x60 box
14235            //      with "A B C D E F G" has black pixels at row 14+;
14236            //      800x16 box with same text has none) — IM:Text 1993
14237            //      p. 2-88 line layout behavior.
14238            //   3. Alignment parameter controls horizontal origin
14239            //      (teJustLeft / teJustCenter / teJustRight produce
14240            //      strictly increasing leftmost-black-pixel columns)
14241            //      — IM:Text 1993 p. 2-87.
14242            //   4. Pascal PROCEDURE pops 14 bytes, no result slot
14243            //      (StackSpace bookends equal).
14244            (true, 0x1CE) => {
14245                let sp = cpu.read_reg(Register::A7);
14246                let align = bus.read_word(sp) as i16;
14247                let box_ptr = bus.read_long(sp + 2);
14248                let box_top = bus.read_word(box_ptr) as i16;
14249                let box_left = bus.read_word(box_ptr + 2) as i16;
14250                let box_bottom = bus.read_word(box_ptr + 4) as i16;
14251                let box_right = bus.read_word(box_ptr + 6) as i16;
14252                let length = bus.read_long(sp + 6) as usize;
14253                let text_ptr = bus.read_long(sp + 10);
14254                cpu.write_reg(Register::A7, sp + 14);
14255
14256                if trace_dialog_text_inline_enabled() {
14257                    let mut stack_bytes = [0u8; 24];
14258                    for (i, byte) in stack_bytes.iter_mut().enumerate() {
14259                        *byte = bus.read_byte(sp + i as u32);
14260                    }
14261                    let preview_len = length.min(64);
14262                    let preview = if text_ptr != 0 {
14263                        bus.read_bytes(text_ptr, preview_len)
14264                    } else {
14265                        Vec::new()
14266                    };
14267                    eprintln!(
14268                        "[DIALOG-TEXT] TETextBox params current_port=${:08X} sp=${:08X} stack={:02X?} text_ptr=${:08X} len={} box=({},{}..{},{} ) align={} preview={:02X?}",
14269                        self.current_port,
14270                        sp,
14271                        stack_bytes,
14272                        text_ptr,
14273                        length,
14274                        box_top,
14275                        box_left,
14276                        box_bottom,
14277                        box_right,
14278                        align,
14279                        preview,
14280                    );
14281                }
14282
14283                if box_right > box_left && box_bottom > box_top {
14284                    // TETextBox creates a transient edit record and clears the
14285                    // destination box before drawing the wrapped text. The erase
14286                    // happens even when length is zero.
14287                    // Text 1993, 2-88; Executor textedit/teDisplay.cpp C_TETextBox
14288                    self.draw_rect(
14289                        cpu,
14290                        bus,
14291                        &Rect {
14292                            top: box_top,
14293                            left: box_left,
14294                            bottom: box_bottom,
14295                            right: box_right,
14296                        },
14297                        ShapeOp::Erase,
14298                    );
14299
14300                    if text_ptr != 0 && length > 0 {
14301                        // Read the text bytes from guest memory
14302                        let text_bytes = bus.read_bytes(text_ptr, length);
14303
14304                        if trace_dialog_text_inline_enabled() {
14305                            let preview_len = text_bytes.len().min(160);
14306                            let first_char =
14307                                text_bytes.first().copied().map(char::from).unwrap_or('\0');
14308                            let first_glyph = crate::quickdraw::text::get_glyph(
14309                                self.tx_font,
14310                                self.tx_size,
14311                                first_char,
14312                            )
14313                            .is_some();
14314                            eprintln!(
14315                            "[DIALOG-TEXT] TETextBox current_port=${:08X} box=({},{}..{},{} ) align={} len={} txFont={} txFace=${:04X} txMode={} txSize={} firstChar={:?} firstGlyph={} text=\"{}\"",
14316                            self.current_port,
14317                            box_top,
14318                            box_left,
14319                            box_bottom,
14320                            box_right,
14321                            align,
14322                            length,
14323                            self.tx_font,
14324                            self.tx_face,
14325                            self.tx_mode,
14326                            self.tx_size,
14327                            first_char,
14328                            first_glyph,
14329                            String::from_utf8_lossy(&text_bytes[..preview_len]),
14330                        );
14331                        }
14332
14333                        // Capture font params to avoid borrowing self in closures
14334                        let font_id = self.tx_font;
14335                        let font_size = self.tx_size;
14336                        let advance_extra = self.advance_extra();
14337                        let missing_advance = self.missing_glyph_advance();
14338
14339                        let metrics = crate::quickdraw::text::get_font_metrics(font_id, font_size);
14340                        let line_height = metrics.ascent + metrics.descent + metrics.leading.max(2);
14341                        let box_width = box_right - box_left;
14342
14343                        // Measure a run of bytes (no &self borrow needed)
14344                        let measure = |start: usize, end: usize| -> i16 {
14345                            let mut w = 0i16;
14346                            for &b in &text_bytes[start..end] {
14347                                let ch = b as char;
14348                                if let Some((g, _)) =
14349                                    crate::quickdraw::text::get_glyph(font_id, font_size, ch)
14350                                {
14351                                    w += g.advance as i16 + advance_extra;
14352                                } else {
14353                                    w += missing_advance;
14354                                }
14355                            }
14356                            w
14357                        };
14358
14359                        // Word-wrap: split into lines that fit within box_width.
14360                        // Break on spaces; if a single word exceeds box_width, break mid-word.
14361                        let mut lines: Vec<(usize, usize)> = Vec::new();
14362                        let mut line_start = 0usize;
14363                        let mut last_break = 0usize;
14364                        let mut line_width = 0i16;
14365
14366                        for (i, &b) in text_bytes.iter().enumerate() {
14367                            if b == b'\r' || b == b'\n' {
14368                                lines.push((line_start, i));
14369                                line_start = i + 1;
14370                                last_break = line_start;
14371                                line_width = 0;
14372                                continue;
14373                            }
14374                            let ch = b as char;
14375                            let char_w = if let Some((g, _)) =
14376                                crate::quickdraw::text::get_glyph(font_id, font_size, ch)
14377                            {
14378                                g.advance as i16 + advance_extra
14379                            } else {
14380                                missing_advance
14381                            };
14382                            line_width += char_w;
14383                            if b == b' ' {
14384                                last_break = i + 1;
14385                            }
14386                            if line_width > box_width && i > line_start {
14387                                if last_break > line_start {
14388                                    lines.push((line_start, last_break - 1));
14389                                    line_start = last_break;
14390                                } else {
14391                                    lines.push((line_start, i));
14392                                    line_start = i;
14393                                }
14394                                last_break = line_start;
14395                                line_width = measure(line_start, i + 1);
14396                            }
14397                        }
14398                        if line_start < text_bytes.len() {
14399                            lines.push((line_start, text_bytes.len()));
14400                        }
14401
14402                        // Draw each line
14403                        let mut y = box_top + metrics.ascent;
14404                        for (start, end) in &lines {
14405                            if y + metrics.descent > box_bottom {
14406                                break;
14407                            }
14408                            let mut trimmed_end = *end;
14409                            while trimmed_end > *start && text_bytes[trimmed_end - 1] == b' ' {
14410                                trimmed_end -= 1;
14411                            }
14412                            let lw = measure(*start, trimmed_end);
14413                            // Per Inside Macintosh: Text 1993, lines 7320-7323:
14414                            //   teJustLeft   =  0 (flush left — system default)
14415                            //   teJustCenter =  1 (centered)
14416                            //   teJustRight  = -1 (flush right)
14417                            //   teForceLeft  = -2 (force flush left)
14418                            let x = Self::te_line_origin_x(align, box_left, box_right, lw);
14419                            self.pn_loc = (y, x);
14420                            for &byte in &text_bytes[*start..trimmed_end] {
14421                                self.draw_char(cpu, bus, byte as char);
14422                            }
14423                            y += line_height;
14424                        }
14425                    }
14426                }
14427                self.refresh_visible_dialog_snapshot_for_port(bus, self.current_port);
14428                Ok(())
14429            }
14430
14431            // TECalText ($A9D0)
14432            // PROCEDURE TECalText(hTE: TEHandle);
14433            // TECalText ($A9D0): Syncs TE_LENGTH and recalculates TE line layout per IM:I I-390
14434            (true, 0x1D0) => {
14435                let sp = cpu.read_reg(Register::A7);
14436                let te_handle = bus.read_long(sp);
14437                let te_ptr = Self::te_record_ptr(bus, te_handle);
14438                if te_ptr != 0 {
14439                    let text_len =
14440                        Self::te_text_length(bus, te_handle).min(u16::MAX as usize) as u16;
14441                    bus.write_word(te_ptr + Self::TE_LENGTH_OFFSET, text_len);
14442                    self.te_recalculate_layout(bus, te_handle);
14443                }
14444                cpu.write_reg(Register::A7, sp + 4);
14445                Ok(())
14446            }
14447
14448            // TESetSelect ($A9D1)
14449            // PROCEDURE TESetSelect(selStart: LONGINT; selEnd: LONGINT; hTE: TEHandle);
14450            // Inside Macintosh Volume I, I-385
14451            // TESetSelect ($A9D1): Clamps selEnd to teLength per IM:I I-385; selStart/selEnd range 0..32767 stored as u16
14452            (true, 0x1D1) => {
14453                let sp = cpu.read_reg(Register::A7);
14454                let te_handle = bus.read_long(sp);
14455                let te_ptr = Self::te_record_ptr(bus, te_handle);
14456                if te_ptr != 0 {
14457                    let te_length = u32::from(bus.read_word(te_ptr + Self::TE_LENGTH_OFFSET));
14458                    // IM:I I-385: "SelEnd and selStart can range from 0 to 32767.
14459                    // If selEnd is anywhere beyond the last character of the text,
14460                    // the position just past the last character is used."
14461                    let sel_end = bus
14462                        .read_long(sp + 4)
14463                        .min(u32::from(u16::MAX))
14464                        .min(te_length);
14465                    let sel_start = bus.read_long(sp + 8).min(u32::from(u16::MAX));
14466                    bus.write_word(te_ptr + Self::TE_SEL_START_OFFSET, sel_start as u16);
14467                    bus.write_word(te_ptr + Self::TE_SEL_END_OFFSET, sel_end as u16);
14468                }
14469                cpu.write_reg(Register::A7, sp + 12);
14470                Ok(())
14471            }
14472
14473            // TEUpdate ($A9D3)
14474            // PROCEDURE TEUpdate(rUpdate: Rect; hTE: TEHandle);
14475            // Inside Macintosh Volume I (1985), p. I-387.
14476            // Inside Macintosh: Text (1993), p. 2-88.
14477            // MPW Universal Headers TextEdit.h:
14478            //   EXTERN_API(void) TEUpdate(const Rect *rUpdate, TEHandle hTE)
14479            //     ONEWORDINLINE(0xA9D3);
14480            //
14481            // IM:I I-387 quote: "TEUpdate draws the text specified by hTE
14482            //   within the rectangle rUpdate of the destination rectangle
14483            //   for the edit record specified by hTE. You normally call
14484            //   this procedure in response to an update event for the
14485            //   window."
14486            // IM:Text 1993 p. 2-88 quote: "TEUpdate redraws the text
14487            //   specified by the edit record within the update rectangle."
14488            //
14489            // TEUpdate has two stack layouts that real Mac ROMs (System
14490            // 6.0+ onward) auto-tolerate:
14491            //
14492            //   * Pascal canonical (per IM:I I-387 PROCEDURE signature):
14493            //       sp+0   hTE       (4 bytes — last pushed, Pascal LR
14494            //                         order: last arg = shallowest)
14495            //       sp+4   rUpdate   (8 bytes — inlined Rect, deepest)
14496            //       Net pop = 12 bytes; no result slot (PROCEDURE).
14497            //
14498            //   * MPW C canonical (Universal Headers TextEdit.h
14499            //     declaration with `const Rect *`):
14500            //       sp+0   hTE          (4 bytes)
14501            //       sp+4   rUpdatePtr   (4 bytes — pointer to Rect)
14502            //       Net pop = 8 bytes; no result slot.
14503            //
14504            // The two forms agree on the contract: TEUpdate is a redraw
14505            // operation that does NOT mutate the underlying edit-record
14506            // state (hText, teLength, selStart, selEnd) — it only paints
14507            // pixels into the TE's inPort within the rUpdate region.
14508            //
14509            // Both the C-form and Pascal-form stack disciplines are
14510            // exercised, and TE record state is preserved across the two
14511            // calls (BasiliskII System 7.5.3 ROM agrees). The heuristic
14512            // below correctly identifies both call shapes.
14513            //
14514            // Contract coverage:
14515            //   dialog::tests::teupdate_pascal_form_redraws_text_and_pops_inline_rect_and_tehandle
14516            //   dialog::tests::teupdate_c_rect_pointer_form_is_accepted_and_pops_eight_bytes
14517            //   dialog::tests::teupdate_preserves_te_record_state_across_redraw
14518            (true, 0x1D3) => {
14519                let sp = cpu.read_reg(Register::A7);
14520                // Both Pascal and C forms put hTE at SP+0 (last argument
14521                // pushed in Pascal left-to-right convention). The forms
14522                // differ at SP+4:
14523                //   * C form (8-byte total push): SP+4..7 is `rUpdatePtr`,
14524                //     a 4-byte pointer to a Rect on the heap or A5 globals.
14525                //   * Pascal form (12-byte total push): SP+4..11 is the
14526                //     8-byte Rect inlined directly on the stack.
14527                //
14528                // The earlier heuristic (`te_record_ptr(first_long) != 0`)
14529                // ALWAYS picked Pascal because first_long is the same handle
14530                // in both forms. POD MARS Master uses the C form; the wrong
14531                // pop-size leaked 4 bytes per TEUpdate call, which compounded
14532                // into 8 bytes per update event (two TEUpdates) and within
14533                // ~50 update cycles drifted A6/A7 enough to corrupt the
14534                // EventRecord pointer the next WaitNextEvent passed in,
14535                // starving the click hit-test. Per d4a8444b / 172f262a.
14536                //
14537                // The reliable test is: does the longword at SP+4 deref to
14538                // a plausible Rect (top<=bottom, left<=right, both in
14539                // -32000..32000)? If yes, C form. If not, fall back to
14540                // Pascal form. Fallback covers the rare case where SP+4..11
14541                // genuinely contains an inlined rect.
14542                //
14543                // Two extra guards beyond the bounds check:
14544                //   * `ptr_candidate` must be word-aligned. A Pascal-form
14545                //     small-coord rect like `(top=1, left=1, …)` produces
14546                //     `(top<<16)|left = 0x0001_0001`, which is odd and
14547                //     therefore NOT a valid 68k pointer (heap and globals
14548                //     are always even-aligned).
14549                //   * The dereffed Rect must have at least one non-zero
14550                //     word. A Pascal form with small even coords like
14551                //     `(top=10, left=10, bottom=40, right=200)` produces
14552                //     ptr_candidate `0x000A_000A`, which derefs into
14553                //     typically-zero low-memory or uninitialized heap —
14554                //     an all-zero "rect" passes the bounds test but isn't
14555                //     a meaningful update region a real C-form caller
14556                //     would pass.
14557                let hte_at_sp = bus.read_long(sp);
14558                let ptr_candidate = bus.read_long(sp + 4);
14559                let looks_like_c_form = ptr_candidate != 0
14560                    && (ptr_candidate & 1) == 0
14561                    && (0x0000_0010..0x0400_0000).contains(&ptr_candidate)
14562                    && {
14563                        let top = bus.read_word(ptr_candidate) as i16;
14564                        let left = bus.read_word(ptr_candidate + 2) as i16;
14565                        let bottom = bus.read_word(ptr_candidate + 4) as i16;
14566                        let right = bus.read_word(ptr_candidate + 6) as i16;
14567                        top <= bottom
14568                            && left <= right
14569                            && (-32000..=32000).contains(&top)
14570                            && (-32000..=32000).contains(&left)
14571                            && (-32000..=32000).contains(&bottom)
14572                            && (-32000..=32000).contains(&right)
14573                            && !(top == 0 && left == 0 && bottom == 0 && right == 0)
14574                    };
14575                let (te_handle, stack_pop) = if looks_like_c_form {
14576                    (hte_at_sp, 8)
14577                } else {
14578                    (hte_at_sp, 12)
14579                };
14580                let te_ptr = Self::te_record_ptr(bus, te_handle);
14581                if te_ptr != 0 {
14582                    if trace_textedit_enabled() {
14583                        let dest_rect = Self::te_read_rect(bus, te_ptr + Self::TE_DEST_RECT_OFFSET);
14584                        let view_rect = Self::te_read_rect(bus, te_ptr + Self::TE_VIEW_RECT_OFFSET);
14585                        eprintln!(
14586                            "[TE] TEUpdate hTE=${:08X} dest=({},{},{},{}) view=({},{},{},{})",
14587                            te_handle,
14588                            dest_rect.0,
14589                            dest_rect.1,
14590                            dest_rect.2,
14591                            dest_rect.3,
14592                            view_rect.0,
14593                            view_rect.1,
14594                            view_rect.2,
14595                            view_rect.3
14596                        );
14597                    }
14598                    self.draw_te_contents(cpu, bus, te_handle);
14599                }
14600                cpu.write_reg(Register::A7, sp + stack_pop);
14601                Ok(())
14602            }
14603
14604            // TEClick ($A9D4)
14605            // PROCEDURE TEClick(pt: Point; extend: BOOLEAN; hTE: TEHandle);
14606            // Inside Macintosh Volume I, I-376; Inside Macintosh: Text 1993, 2-85
14607            //
14608            // MPW Universal Headers TextEdit.h:
14609            //   EXTERN_API(void) TEClick(Point pt, Boolean fExtend, TEHandle hTE)
14610            //       ONEWORDINLINE(0xA9D4);
14611            //
14612            // IM:I I-376 verbatim: "TEClick controls the placement and
14613            // highlighting of the selection range as determined by mouse
14614            // events. ... If the mouse button is down outside of the
14615            // selection range or if the Shift key was not down when the
14616            // mouse button was pressed (depending on the value of
14617            // extend), the selection range is set to an insertion point
14618            // at the offset corresponding to the position of the mouse."
14619            //
14620            // Pascal stack frame (args push left-to-right, first
14621            // source arg deepest):
14622            //   sp+0  hTE: TEHandle           (4) — last arg, shallowest
14623            //   sp+4  extend: BOOLEAN         (2) — middle arg (value
14624            //                                       in high byte per
14625            //                                       MPW C convention)
14626            //   sp+6  pt.v: INTEGER           (2) — first arg, second word
14627            //   sp+8  pt.h: INTEGER           (2) — first arg, first word
14628            // Total pop = 10 bytes; no function-result slot.
14629            //
14630            // HLE compromise: Systemless doesn't model the interactive
14631            // drag-to-select mouse loop that real-Mac TEClick runs.
14632            // Per IM:I I-376 a real TEClick would: (a) hit-test the
14633            // click point against the TE's destRect, (b) compute
14634            // the byte offset in the text corresponding to the
14635            // click position, (c) extend or replace the selection
14636            // range based on the `extend` flag, (d) loop polling
14637            // the mouse for drag-to-select until WaitMouseUp. Steps
14638            // (a)..(c) require a working caret-from-pixel mapping
14639            // that depends on the current font + size + measured
14640            // line widths; step (d) requires interactive mouse
14641            // synthesis from the host event source. Neither is
14642            // wired up in the current scripted event
14643            // model. So the TE record's selStart/selEnd are NOT
14644            // updated to reflect the click — apps that depend on
14645            // click-to-position-cursor will not see the cursor
14646            // move; apps that programmatically set selStart/selEnd
14647            // via TESetSelect ($A9D1, already Complete) work as
14648            // expected.
14649            //
14650            // BasiliskII-vs-Systemless divergence: BII System 7.5.3 ROM
14651            // DOES update selStart/selEnd on a TEClick call (collapsing
14652            // to the byte offset corresponding to the click position
14653            // per IM:I I-376) because it has the real pixel-to-character
14654            // mapping. Systemless leaves them unchanged; the two engines
14655            // still agree on the active flag, teLength preservation, and
14656            // Pascal PROCEDURE stack discipline.
14657            //
14658            // Both engines DO agree that TEClick must not mutate the
14659            // active flag (owned by TEActivate/TEDeactivate per
14660            // IM:I I-385) or teLength (owned by
14661            // TESetText/TEKey/TEDelete/TEInsert).
14662            //
14663            // NIL hTE is a defensive no-op (matches the real-Mac
14664            // safety contract — TEClick on NIL is undefined but
14665            // shouldn't crash the host).
14666            //
14667            // Regression coverage:
14668            //   dialog::tests::teclick_consumes_point_extend_and_tehandle_arguments
14669            //   dialog::tests::teclick_empty_text_keeps_insertion_point_at_zero
14670            //   dialog::tests::teclick_preserves_terec_active_flag_and_telength
14671            //   dialog::tests::teclick_repeated_calls_balance_stack_no_drift
14672            (true, 0x1D4) => {
14673                let sp = cpu.read_reg(Register::A7);
14674                if trace_textedit_enabled() {
14675                    let te_handle = bus.read_long(sp);
14676                    // Pascal BOOLEAN in high byte (MPW C convention).
14677                    let extend = bus.read_byte(sp + 4) != 0;
14678                    let pt_v = bus.read_word(sp + 6) as i16;
14679                    let pt_h = bus.read_word(sp + 8) as i16;
14680                    eprintln!(
14681                        "[TE] TEClick hTE=${:08X} extend={} pt=({}, {})",
14682                        te_handle, extend, pt_v, pt_h
14683                    );
14684                }
14685                cpu.write_reg(Register::A7, sp + 10);
14686                Ok(())
14687            }
14688
14689            // TECopy ($A9D5)
14690            // PROCEDURE TECopy(hTE: TEHandle);
14691            // Copies the currently-selected bytes into the TextEdit
14692            // scrap (TEScrpLength low-mem global at $0AB0 (word) /
14693            // TEScrpHandle at $0AB4 (long)). Empty selection leaves
14694            // the scrap untouched per IM:I I-386.
14695            // Inside Macintosh Volume I, I-386
14696            // TECopy ($A9D5): Writes selected bytes into TextEdit scrap low-mem globals (TEScrpLength $0AB0, TEScrpHandle $0AB4) per IM:I I-386. Empty selection leaves scrap untouched.
14697            (true, 0x1D5) => {
14698                let sp = cpu.read_reg(Register::A7);
14699                let te_handle = bus.read_long(sp);
14700                let te_ptr = Self::te_record_ptr(bus, te_handle);
14701                if te_ptr != 0 {
14702                    let sel_start = bus.read_word(te_ptr + Self::TE_SEL_START_OFFSET) as usize;
14703                    let sel_end = bus.read_word(te_ptr + Self::TE_SEL_END_OFFSET) as usize;
14704                    let (s, e) = if sel_start <= sel_end {
14705                        (sel_start, sel_end)
14706                    } else {
14707                        (sel_end, sel_start)
14708                    };
14709                    if s != e {
14710                        use crate::memory::globals::addr;
14711                        let existing = Self::te_text_bytes(bus, te_handle);
14712                        let s = s.min(existing.len());
14713                        let e = e.min(existing.len());
14714                        let selected = &existing[s..e];
14715                        let mut scrap_handle = bus.read_long(addr::TE_SCRP_HANDLE);
14716                        if scrap_handle == 0 {
14717                            scrap_handle = Self::allocate_handle_with_data(bus, 0);
14718                            bus.write_long(addr::TE_SCRP_HANDLE, scrap_handle);
14719                        }
14720                        let text_ptr =
14721                            Self::ensure_text_handle_size(bus, scrap_handle, selected.len());
14722                        if text_ptr != 0 && !selected.is_empty() {
14723                            bus.write_bytes(text_ptr, selected);
14724                        }
14725                        bus.write_word(
14726                            addr::TE_SCRP_LENGTH,
14727                            selected.len().min(u16::MAX as usize) as u16,
14728                        );
14729                    }
14730                }
14731                cpu.write_reg(Register::A7, sp + 4);
14732                Ok(())
14733            }
14734
14735            // TECut ($A9D6)
14736            // PROCEDURE TECut(hTE: TEHandle);
14737            // Removes the currently-selected text from the edit record
14738            // and places a copy in the TextEdit scrap (TEScrpLength at
14739            // $0AB0 / TEScrpHandle at $0AB4). Any previous scrap
14740            // contents are replaced. Insertion-point selection is a
14741            // no-op per IM:I I-391.
14742            // TECut ($A9D6): Removes selected text from TERec and writes a copy into the TextEdit scrap low-mem globals per IM:I I-391. Empty selection is a no-op.
14743            (true, 0x1D6) => {
14744                let sp = cpu.read_reg(Register::A7);
14745                let te_handle = bus.read_long(sp);
14746                let te_ptr = Self::te_record_ptr(bus, te_handle);
14747                if te_ptr != 0 {
14748                    let sel_start = bus.read_word(te_ptr + Self::TE_SEL_START_OFFSET) as usize;
14749                    let sel_end = bus.read_word(te_ptr + Self::TE_SEL_END_OFFSET) as usize;
14750                    let (s, e) = if sel_start <= sel_end {
14751                        (sel_start, sel_end)
14752                    } else {
14753                        (sel_end, sel_start)
14754                    };
14755                    if s != e {
14756                        let existing = Self::te_text_bytes(bus, te_handle);
14757                        let s = s.min(existing.len());
14758                        let e = e.min(existing.len());
14759                        // Copy selection into the scrap (replace, not
14760                        // append — IM:I I-391).
14761                        {
14762                            use crate::memory::globals::addr;
14763                            let selected = &existing[s..e];
14764                            let mut scrap_handle = bus.read_long(addr::TE_SCRP_HANDLE);
14765                            if scrap_handle == 0 {
14766                                scrap_handle = Self::allocate_handle_with_data(bus, 0);
14767                                bus.write_long(addr::TE_SCRP_HANDLE, scrap_handle);
14768                            }
14769                            let text_ptr =
14770                                Self::ensure_text_handle_size(bus, scrap_handle, selected.len());
14771                            if text_ptr != 0 && !selected.is_empty() {
14772                                bus.write_bytes(text_ptr, selected);
14773                            }
14774                            bus.write_word(
14775                                addr::TE_SCRP_LENGTH,
14776                                selected.len().min(u16::MAX as usize) as u16,
14777                            );
14778                        }
14779                        // Delete selection from the TE text.
14780                        let mut merged = Vec::with_capacity(existing.len() - (e - s));
14781                        merged.extend_from_slice(&existing[..s]);
14782                        merged.extend_from_slice(&existing[e..]);
14783                        self.te_set_text_contents(bus, te_handle, &merged);
14784                        let new_pos = s.min(u16::MAX as usize) as u16;
14785                        bus.write_word(te_ptr + Self::TE_SEL_START_OFFSET, new_pos);
14786                        bus.write_word(te_ptr + Self::TE_SEL_END_OFFSET, new_pos);
14787                    }
14788                }
14789                cpu.write_reg(Register::A7, sp + 4);
14790                Ok(())
14791            }
14792
14793            // TEDelete ($A9D7)
14794            // Removes the currently selected text from the edit record and
14795            // redraws. If selStart == selEnd (insertion point), does nothing.
14796            // Does NOT transfer text to the scrap (unlike TECut).
14797            // PROCEDURE TEDelete(hTE: TEHandle);
14798            // Inside Macintosh Volume I, I-387; Text 1993, 2-93
14799            //
14800            // Regression coverage:
14801            //   dialog::tests::tedelete_removes_selection_without_touching_scrap_and_pops_arg
14802            //   dialog::tests::tedelete_insertion_point_selection_is_noop_and_pops_arg
14803            // TEDelete ($A9D7): Removes selected text, collapses selection; no-op at insertion point. IM:I I-387
14804            (true, 0x1D7) => {
14805                let sp = cpu.read_reg(Register::A7);
14806                let te_handle = bus.read_long(sp);
14807                if trace_textedit_enabled() {
14808                    eprintln!("[TE] TEDelete hTE=${:08X}", te_handle);
14809                }
14810                let te_ptr = Self::te_record_ptr(bus, te_handle);
14811                if te_ptr != 0 {
14812                    let sel_start = bus.read_word(te_ptr + Self::TE_SEL_START_OFFSET) as usize;
14813                    let sel_end = bus.read_word(te_ptr + Self::TE_SEL_END_OFFSET) as usize;
14814                    if sel_start != sel_end {
14815                        let existing = Self::te_text_bytes(bus, te_handle);
14816                        let s = sel_start.min(existing.len());
14817                        let e = sel_end.min(existing.len());
14818                        let (s, e) = if s > e { (e, s) } else { (s, e) };
14819                        let mut merged = Vec::with_capacity(existing.len() - (e - s));
14820                        merged.extend_from_slice(&existing[..s]);
14821                        merged.extend_from_slice(&existing[e..]);
14822                        self.te_set_text_contents(bus, te_handle, &merged);
14823                        let new_pos = s.min(u16::MAX as usize) as u16;
14824                        bus.write_word(te_ptr + Self::TE_SEL_START_OFFSET, new_pos);
14825                        bus.write_word(te_ptr + Self::TE_SEL_END_OFFSET, new_pos);
14826                    }
14827                }
14828                cpu.write_reg(Register::A7, sp + 4);
14829                Ok(())
14830            }
14831
14832            // TEActivate ($A9D8)
14833            // Activates the edit record, highlighting the selection range or
14834            // displaying a blinking caret at the insertion point.
14835            // PROCEDURE TEActivate(hTE: TEHandle);
14836            // Inside Macintosh Volume I, I-385; Text 1993, 2-80
14837            //
14838            // Regression coverage:
14839            //   teactivate_and_tedeactivate_repaint_empty_insertion_caret
14840            // TEActivate ($A9D8): Sets TERec.active flag per IM:I I-385
14841            (true, 0x1D8) => {
14842                let sp = cpu.read_reg(Register::A7);
14843                let te_handle = bus.read_long(sp);
14844                if trace_textedit_enabled() {
14845                    eprintln!("[TE] TEActivate hTE=${:08X}", te_handle);
14846                }
14847                let te_ptr = Self::te_record_ptr(bus, te_handle);
14848                if te_ptr != 0 {
14849                    bus.write_word(te_ptr + Self::TE_ACTIVE_OFFSET, 1);
14850                    bus.write_long(te_ptr + Self::TE_CARET_TIME_OFFSET, self.tick_count);
14851                    bus.write_word(te_ptr + Self::TE_CARET_STATE_OFFSET, 0);
14852                    self.draw_te_contents(cpu, bus, te_handle);
14853                }
14854                cpu.write_reg(Register::A7, sp + 4);
14855                Ok(())
14856            }
14857
14858            // TEDeactivate ($A9D9)
14859            // Deactivates the edit record, unhighlighting the selection range
14860            // or removing the caret. Does not affect selStart/selEnd.
14861            // PROCEDURE TEDeactivate(hTE: TEHandle);
14862            // Inside Macintosh Volume I, I-385; Text 1993, 2-80
14863            //
14864            // Regression coverage:
14865            //   teactivate_and_tedeactivate_repaint_empty_insertion_caret
14866            // TEDeactivate ($A9D9): Clears TERec.active flag per IM:I I-385
14867            (true, 0x1D9) => {
14868                let sp = cpu.read_reg(Register::A7);
14869                let te_handle = bus.read_long(sp);
14870                if trace_textedit_enabled() {
14871                    eprintln!("[TE] TEDeactivate hTE=${:08X}", te_handle);
14872                }
14873                let te_ptr = Self::te_record_ptr(bus, te_handle);
14874                if te_ptr != 0 {
14875                    bus.write_word(te_ptr + Self::TE_ACTIVE_OFFSET, 0);
14876                    self.draw_te_contents(cpu, bus, te_handle);
14877                }
14878                cpu.write_reg(Register::A7, sp + 4);
14879                Ok(())
14880            }
14881
14882            // TEIdle ($A9DA)
14883            // PROCEDURE TEIdle(hTE: TEHandle);
14884            // Inside Macintosh Volume I (1985), p. I-374; Inside
14885            //   Macintosh: Text (1993), p. 2-84.
14886            // MPW Universal Headers TextEdit.h:
14887            //   EXTERN_API(void) TEIdle(TEHandle hTE) ONEWORDINLINE(0xA9DA);
14888            //
14889            // Pascal stack frame:
14890            //   sp+0  hTE: TEHandle  (4)
14891            // Total pop = 4 bytes; no function-result slot. Net
14892            // externally-observed SP delta is zero (StackSpace
14893            // bookends are equal across one call).
14894            //
14895            // IM:I I-374 quote: "Call TEIdle from your event loop...
14896            // TEIdle causes the caret of the edit record to blink at
14897            // the rate specified in caretTime."
14898            //
14899            // IM:Text 1993 p. 2-84 quote: "TEIdle blinks the caret if
14900            // the destination rectangle contains the caret position;
14901            // if the specified edit record is inactive (such as when
14902            // the window is inactive), TEIdle has no effect."
14903            //
14904            // HLE model: TextEdit's TERec layout includes the private
14905            // caretTime and caretState fields before just/teLength. Systemless
14906            // treats caretTime as the last toggle tick and caretState == 0 as
14907            // visible. On an active insertion point, TEIdle waits the
14908            // documented initial 32-tick interval, toggles caretState, and
14909            // redraws the edit record. Non-insertion selections do not blink.
14910            //
14911            // Crucial no-mutation contract: TEIdle MUST NOT mutate TERec.active,
14912            // TERec.selStart, TERec.selEnd, TERec.teLength, or any
14913            // other selection / text fields. Selection updates are
14914            // the exclusive responsibility of TESetSelect /
14915            // TEClick / TEKey / TEDelete; activation toggles are
14916            // handled by TEActivate / TEDeactivate.
14917            //
14918            // NIL hTE is a defensive no-op.
14919            //
14920            // Regression coverage:
14921            //   dialog::tests::teidle_consumes_tehandle_argument
14922            //   dialog::tests::teidle_toggles_visible_insertion_caret_after_blink_interval
14923            //   dialog::tests::teidle_preserves_active_flag_and_selection_offsets
14924            //   dialog::tests::teidle_repeated_calls_balance_stack_and_preserve_terec_state
14925            (true, 0x1DA) => {
14926                let sp = cpu.read_reg(Register::A7);
14927                let te_handle = bus.read_long(sp);
14928                self.textedit_idle(cpu, bus, te_handle);
14929                cpu.write_reg(Register::A7, sp + 4);
14930                Ok(())
14931            }
14932
14933            // TEPaste ($A9DB)
14934            // PROCEDURE TEPaste(hTE: TEHandle);
14935            // Replaces the current selection with the contents of the
14936            // TextEdit scrap (TEScrpLength at $0AB0 word, TEScrpHandle
14937            // at $0AB4 long). Empty scrap leaves the TERec unchanged
14938            // per IM:I I-387.
14939            // Inside Macintosh Volume I, I-387
14940            // TEPaste ($A9DB): Replaces current selection with TextEdit scrap contents (TEScrpLength / TEScrpHandle low-mem globals) per IM:I I-387. Empty scrap is a no-op.
14941            (true, 0x1DB) => {
14942                let sp = cpu.read_reg(Register::A7);
14943                let te_handle = bus.read_long(sp);
14944                if trace_textedit_enabled() {
14945                    eprintln!("[TE] TEPaste hTE=${:08X}", te_handle);
14946                }
14947                use crate::memory::globals::addr;
14948                let scrap_len = bus.read_word(addr::TE_SCRP_LENGTH) as usize;
14949                let scrap_handle = bus.read_long(addr::TE_SCRP_HANDLE);
14950                if scrap_handle != 0 && scrap_len > 0 {
14951                    let scrap_ptr = bus.read_long(scrap_handle);
14952                    if scrap_ptr != 0 {
14953                        let scrap = bus.read_bytes(scrap_ptr, scrap_len);
14954                        self.te_insert_text(bus, te_handle, &scrap);
14955                    }
14956                }
14957                cpu.write_reg(Register::A7, sp + 4);
14958                Ok(())
14959            }
14960
14961            // TEKey ($A9DC)
14962            // Replaces the selection range with the input character and positions
14963            // the insertion point just past it. Backspace ($08) deletes the
14964            // selection or the character before the insertion point.
14965            // PROCEDURE TEKey(key: CHAR; hTE: TEHandle);
14966            // Inside Macintosh Volume I, I-385; Text 1993, 2-81
14967            //
14968            // TEKey ($A9DC): Inserts character at insertion point, replaces selection; backspace deletes. IM:I I-385
14969            (true, 0x1DC) => {
14970                let sp = cpu.read_reg(Register::A7);
14971                let te_handle = bus.read_long(sp);
14972                let key = bus.read_word(sp + 4) as u8;
14973                if trace_textedit_enabled() {
14974                    eprintln!(
14975                        "[TE] TEKey hTE=${:08X} key=${:02X} {:?}",
14976                        te_handle,
14977                        key,
14978                        char::from(key).escape_default().to_string()
14979                    );
14980                }
14981                let te_ptr = Self::te_record_ptr(bus, te_handle);
14982                if te_ptr != 0 {
14983                    let sel_start = bus.read_word(te_ptr + Self::TE_SEL_START_OFFSET) as usize;
14984                    let sel_end = bus.read_word(te_ptr + Self::TE_SEL_END_OFFSET) as usize;
14985                    let existing = Self::te_text_bytes(bus, te_handle);
14986                    let text_len = existing.len();
14987                    let s = sel_start.min(text_len);
14988                    let e = sel_end.min(text_len);
14989                    let (s, e) = if s > e { (e, s) } else { (s, e) };
14990
14991                    if key == 0x08 {
14992                        // Backspace
14993                        if s != e {
14994                            // Delete selected text
14995                            let mut merged = Vec::with_capacity(text_len - (e - s));
14996                            merged.extend_from_slice(&existing[..s]);
14997                            merged.extend_from_slice(&existing[e..]);
14998                            self.te_set_text_contents(bus, te_handle, &merged);
14999                            let new_pos = s.min(u16::MAX as usize) as u16;
15000                            bus.write_word(te_ptr + Self::TE_SEL_START_OFFSET, new_pos);
15001                            bus.write_word(te_ptr + Self::TE_SEL_END_OFFSET, new_pos);
15002                        } else if s > 0 {
15003                            // Delete character before insertion point
15004                            let mut merged = Vec::with_capacity(text_len - 1);
15005                            merged.extend_from_slice(&existing[..s - 1]);
15006                            merged.extend_from_slice(&existing[s..]);
15007                            self.te_set_text_contents(bus, te_handle, &merged);
15008                            let new_pos = (s - 1).min(u16::MAX as usize) as u16;
15009                            bus.write_word(te_ptr + Self::TE_SEL_START_OFFSET, new_pos);
15010                            bus.write_word(te_ptr + Self::TE_SEL_END_OFFSET, new_pos);
15011                        }
15012                        // At start with no selection: do nothing
15013                    } else {
15014                        // Insert printable character (replacing selection if any)
15015                        self.te_insert_text(bus, te_handle, &[key]);
15016                    }
15017                }
15018                cpu.write_reg(Register::A7, sp + 6);
15019                Ok(())
15020            }
15021
15022            // TEScroll ($A9DD)
15023            // PROCEDURE TEScroll(dh: INTEGER; dv: INTEGER; hTE: TEHandle);
15024            // TEScroll ($A9DD): Scrolls destRect by (dh, dv) and redraws per IM:I I-389 / Text 1993 2-89
15025            (true, 0x1DD) => {
15026                let sp = cpu.read_reg(Register::A7);
15027                let te_handle = bus.read_long(sp);
15028                let dv = bus.read_word(sp + 4) as i16;
15029                let dh = bus.read_word(sp + 6) as i16;
15030                if trace_textedit_enabled() {
15031                    eprintln!("[TE] TEScroll hTE=${:08X} dh={} dv={}", te_handle, dh, dv);
15032                }
15033                self.te_scroll_contents(cpu, bus, te_handle, dh, dv);
15034                cpu.write_reg(Register::A7, sp + 8);
15035                Ok(())
15036            }
15037
15038            // TEInsert ($A9DE)
15039            // PROCEDURE TEInsert(text: Ptr; length: LONGINT; hTE: TEHandle);
15040            // Inside Macintosh Volume I, I-387; Inside Macintosh: Text 1993,
15041            // 2-94. TEInsert splices the supplied bytes into hText at the
15042            // selStart offset (inserting JUST BEFORE the selection range, not
15043            // replacing it). The selection range is preserved logically —
15044            // selStart and selEnd both shift forward by `length` so the
15045            // selection continues to span the same original characters.
15046            // TEInsert does not touch the scrap.
15047            (true, 0x1DE) => {
15048                let sp = cpu.read_reg(Register::A7);
15049                let te_handle = bus.read_long(sp);
15050                let length = bus.read_long(sp + 4) as usize;
15051                let text_ptr = bus.read_long(sp + 8);
15052                if text_ptr != 0 && length != 0 {
15053                    let te_ptr = Self::te_record_ptr(bus, te_handle);
15054                    if te_ptr != 0 {
15055                        let existing = Self::te_text_bytes(bus, te_handle);
15056                        let sel_start = (bus.read_word(te_ptr + Self::TE_SEL_START_OFFSET)
15057                            as usize)
15058                            .min(existing.len());
15059                        let sel_end = (bus.read_word(te_ptr + Self::TE_SEL_END_OFFSET) as usize)
15060                            .min(existing.len());
15061                        let text = bus.read_bytes(text_ptr, length);
15062                        let mut merged = Vec::with_capacity(existing.len() + text.len());
15063                        merged.extend_from_slice(&existing[..sel_start]);
15064                        merged.extend_from_slice(&text);
15065                        merged.extend_from_slice(&existing[sel_start..]);
15066                        self.te_set_text_contents(bus, te_handle, &merged);
15067                        let shifted_start = (sel_start + text.len()).min(u16::MAX as usize) as u16;
15068                        let shifted_end = (sel_end + text.len()).min(u16::MAX as usize) as u16;
15069                        bus.write_word(te_ptr + Self::TE_SEL_START_OFFSET, shifted_start);
15070                        bus.write_word(te_ptr + Self::TE_SEL_END_OFFSET, shifted_end);
15071                    }
15072                }
15073                cpu.write_reg(Register::A7, sp + 12);
15074                Ok(())
15075            }
15076
15077            // TESetAlignment ($A9DF)
15078            // Sets the alignment (justification) of the text in the edit record.
15079            // PROCEDURE TESetAlignment(just: INTEGER; hTE: TEHandle);
15080            // Inside Macintosh Volume I, I-387; Text 1993, 2-87
15081            //
15082            // TESetAlignment ($A9DF): Writes just field to TERec per IM:I I-387
15083            (true, 0x1DF) => {
15084                let sp = cpu.read_reg(Register::A7);
15085                let te_handle = bus.read_long(sp);
15086                let just = bus.read_word(sp + 4) as i16;
15087                let te_ptr = Self::te_record_ptr(bus, te_handle);
15088                if te_ptr != 0 {
15089                    bus.write_word(te_ptr + Self::TE_JUST_OFFSET, just as u16);
15090                }
15091                cpu.write_reg(Register::A7, sp + 6);
15092                Ok(())
15093            }
15094
15095            // SelectDialogItemText ($A97E)
15096            // PROCEDURE SelectDialogItemText(theDialog: DialogPtr;
15097            //     itemNo: INTEGER; strtSel: INTEGER; endSel: INTEGER);
15098            // Selects and highlights text in an editable text dialog item.
15099            // Inside Macintosh Volume I, I-414; Macintosh Toolbox Essentials
15100            // 1992, 6-131..6-132
15101            //
15102            // Pascal stack frame:
15103            //   SP+0  endSel: INTEGER     (2) — last arg, shallowest
15104            //   SP+2  strtSel: INTEGER    (2)
15105            //   SP+4  itemNo: INTEGER     (2)
15106            //   SP+6  theDialog: DialogPtr (4) — first arg, deepest
15107            // Total pop = 10 bytes.
15108            //
15109            // Behaviour per IM:I I-414:
15110            //   - "Selects the text from character strtSel through
15111            //     character endSel-1 in the editable text item with
15112            //     item number itemNo of the specified dialog box."
15113            //   - Special case: "If you set strtSel = 0 and
15114            //     endSel = -1, the entire text is selected" — we
15115            //     normalize this to (0, text.len()) at dispatch.
15116            //   - If itemNo is not an editText item (type 16), the
15117            //     procedure is a no-op (real Mac just ignores it).
15118            //   - The named item becomes the dialog's active edit
15119            //     field (DialogRecord.editField at offset 164).
15120            //
15121            // Systemless stores the normalized range in DialogItem and mirrors
15122            // it into DialogRecord.textH's TERecord when present, then redraws
15123            // the visible editText item so the selection highlight is
15124            // immediate like SelIText / SelectDialogItemText.
15125            (true, 0x17E) => {
15126                let sp = cpu.read_reg(Register::A7);
15127                let end_sel = bus.read_word(sp) as i16;
15128                let start_sel = bus.read_word(sp + 2) as i16;
15129                let item_no = bus.read_word(sp + 4) as i16;
15130                let dialog_ptr = bus.read_long(sp + 6);
15131                let mut redraw_item = None;
15132                if dialog_ptr != 0 && item_no > 0 {
15133                    if let Some(items) = self.dialog_items.get_mut(&dialog_ptr) {
15134                        if let Some(item) = items.get_mut((item_no - 1) as usize) {
15135                            if item.item_type & 0x7F == 16 {
15136                                // IM:I I-414 special case: (0, -1)
15137                                // means "select all" — normalize
15138                                // to (0, text.len()).
15139                                let text_len = item.text.len() as i16;
15140                                let (s, e) = if start_sel == 0 && end_sel == -1 {
15141                                    (0, text_len)
15142                                } else {
15143                                    // Clamp to text bounds.
15144                                    let s = start_sel.max(0).min(text_len);
15145                                    let e = end_sel.max(0).min(text_len);
15146                                    // Swap if reversed (defensive
15147                                    // — real Mac also normalizes).
15148                                    if s <= e {
15149                                        (s, e)
15150                                    } else {
15151                                        (e, s)
15152                                    }
15153                                };
15154                                item.sel_start = s;
15155                                item.sel_end = e;
15156                                bus.write_word(dialog_ptr + 164, (item_no - 1) as u16);
15157                                // Mirror selStart/selEnd into the TERecord so
15158                                // callers that read (**textH).selStart via the
15159                                // canonical DialogRecord layout can verify the
15160                                // selection (IM:I I-382, I-414).
15161                                // textH is a TEHandle at dialog_ptr+160
15162                                // (IM:I I-411 DialogRecord layout).
15163                                let text_h_handle = bus.read_long(dialog_ptr + 160);
15164                                if text_h_handle != 0 {
15165                                    let te_ptr = bus.read_long(text_h_handle);
15166                                    if te_ptr != 0 {
15167                                        bus.write_word(
15168                                            te_ptr + Self::TE_SEL_START_OFFSET,
15169                                            s as u16,
15170                                        );
15171                                        bus.write_word(te_ptr + Self::TE_SEL_END_OFFSET, e as u16);
15172                                    }
15173                                }
15174                                redraw_item = Some((dialog_ptr, item_no));
15175                            }
15176                        }
15177                    }
15178                }
15179                if let Some((dialog_ptr, item_no)) = redraw_item {
15180                    self.redraw_dialog_text_item(bus, dialog_ptr, item_no);
15181                }
15182                cpu.write_reg(Register::A7, sp + 10);
15183                Ok(())
15184            }
15185
15186            // NewCDialog ($AA4B)
15187            // Color-aware variant of NewDialog. Identical parameters and
15188            // semantics; internally uses a CGrafPort instead of GrafPort
15189            // so the dialog supports color drawing. For our HLE this is
15190            // the same code path as NewDialog.
15191            // FUNCTION NewCDialog(dStorage: Ptr; boundsRect: Rect;
15192            //     title: Str255; visible: BOOLEAN; procID: INTEGER;
15193            //     behind: WindowPtr; goAwayFlag: BOOLEAN;
15194            //     refCon: LongInt; items: Handle): CDialogPtr;
15195            // Inside Macintosh Volume V, V-243
15196            //
15197            // NewDialog ($A97D)
15198            // Creates a dialog from a caller-supplied item list handle.
15199            // FUNCTION NewDialog(dStorage: Ptr; boundsRect: Rect; title: Str255;
15200            //     visible: BOOLEAN; procID: INTEGER; behind: WindowPtr;
15201            //     goAwayFlag: BOOLEAN; refCon: LONGINT; items: Handle): DialogPtr;
15202            // Inside Macintosh Volume I, I-412
15203            // NewDialog ($A97D): Allocates DialogRecord, processes DITL items, pops 32 bytes
15204            // NewCDialog ($AA4B): Color-aware NewDialog; same code path, CGrafPort variant per IM:V V-243
15205            (true, 0x17D) | (true, 0x24B) => {
15206                let sp = cpu.read_reg(Register::A7);
15207                // 68K Pascal stack:
15208                //   SP+0:  items(4)
15209                //   SP+4:  refCon(4)
15210                //   SP+8:  goAwayFlag(2)
15211                //   SP+10: behind(4)
15212                //   SP+14: procID(2)
15213                //   SP+16: visible(2)
15214                //   SP+18: title(4)
15215                //   SP+22: boundsRect(4)
15216                //   SP+26: dStorage(4)
15217                //   SP+30: result(4)
15218                let items_handle = bus.read_long(sp);
15219                let ref_con = bus.read_long(sp + 4);
15220                // Pascal BOOLEAN as the HIGH byte of its 2-byte stack slot
15221                // (MPW C convention).
15222                let go_away_flag = bus.read_byte(sp + 8) != 0;
15223                let behind = bus.read_long(sp + 10);
15224                let proc_id = bus.read_word(sp + 14) as i16;
15225                let visible = bus.read_byte(sp + 16) != 0;
15226                let title_ptr = bus.read_long(sp + 18);
15227                let bounds_rect_ptr = bus.read_long(sp + 22);
15228                let storage_ptr = bus.read_long(sp + 26);
15229
15230                let bounds = if bounds_rect_ptr != 0 {
15231                    (
15232                        bus.read_word(bounds_rect_ptr) as i16,
15233                        bus.read_word(bounds_rect_ptr + 2) as i16,
15234                        bus.read_word(bounds_rect_ptr + 4) as i16,
15235                        bus.read_word(bounds_rect_ptr + 6) as i16,
15236                    )
15237                } else {
15238                    (0, 0, 0, 0)
15239                };
15240                let title = if title_ptr != 0 {
15241                    decode_mac_roman_for_render(&bus.read_pstring(title_ptr))
15242                } else {
15243                    String::new()
15244                };
15245
15246                let items_ptr = if items_handle != 0 {
15247                    bus.read_long(items_handle)
15248                } else {
15249                    0
15250                };
15251                let items_len = if items_ptr != 0 {
15252                    bus.get_alloc_size(items_ptr).unwrap_or(0)
15253                } else {
15254                    0
15255                };
15256                let items = if items_ptr != 0 && items_len != 0 {
15257                    Self::parse_ditl(bus, items_ptr, items_len)
15258                } else {
15259                    Vec::new()
15260                };
15261
15262                let dlg_ptr = self.finish_dialog_creation(
15263                    bus,
15264                    cpu,
15265                    storage_ptr,
15266                    bounds,
15267                    &title,
15268                    visible,
15269                    proc_id,
15270                    go_away_flag,
15271                    ref_con,
15272                    items_handle,
15273                    items,
15274                );
15275                // Honor Pascal `behind` param at SP+10 per IM:I I-412.
15276                self.apply_behind_parameter(bus, dlg_ptr, behind);
15277                bus.write_long(sp + 30, dlg_ptr);
15278                cpu.write_reg(Register::A7, sp + 30);
15279                Ok(())
15280            }
15281
15282            // CloseDialog ($A982)
15283            // PROCEDURE CloseDialog(theDialog: DialogPtr);
15284            // Inside Macintosh Volume I, I-413
15285            // CloseDialog ($A982): Removes the dialog from the window list
15286            // and restores previous window state (bounds, procID, title)
15287            // when the closed dialog was frontmost. IM:I I-413 explicitly
15288            // states "deletes the dialog window from the window list" —
15289            // routed through untrack_window so FrontWindow sees the
15290            // updated list, matching DisposDialog's pattern.
15291            (true, 0x182) => {
15292                let sp = cpu.read_reg(Register::A7);
15293                let dialog_ptr = bus.read_long(sp);
15294                self.close_dialog_window(bus, cpu, dialog_ptr, false);
15295                cpu.write_reg(Register::A7, sp + 4);
15296                Ok(())
15297            }
15298
15299            // UpdtDialog ($A978)
15300            // Redraws the dialog items in the specified update region.
15301            // PROCEDURE UpdtDialog(theDialog: DialogPtr; updateRgn: RgnHandle);
15302            // Inside Macintosh Volume I, I-415; Macintosh Toolbox Essentials 1992, 6-142..6-143
15303            (true, 0x178) => {
15304                let sp = cpu.read_reg(Register::A7);
15305                let update_rgn = bus.read_long(sp);
15306                let dialog_ptr = bus.read_long(sp + 4);
15307                cpu.write_reg(Register::A7, sp + 8);
15308                if update_rgn == 0 {
15309                    return Some(Ok(()));
15310                }
15311                let update_rect = Self::region_handle_rect(bus, update_rgn);
15312                self.update_dialog_window_contents(bus, cpu, dialog_ptr, update_rect);
15313                Ok(())
15314            }
15315
15316            // CouldDialog ($A979) / FreeDialog ($A97A)
15317            // PROCEDURE CouldDialog(dialogID: INTEGER);
15318            // PROCEDURE FreeDialog (dialogID: INTEGER);
15319            // Inside Macintosh Volume I, I-415
15320            //
15321            // Per IM:I I-415, CouldDialog makes the DLOG, its DITL, and
15322            // resource-backed DITL items unpurgeable, loading them first if
15323            // needed; FreeDialog makes the same already-loaded resources
15324            // purgeable again. Systemless models that visible handle-state
15325            // axis for DLOG/DITL plus CNTL/ICON/PICT DITL resources. WDEF
15326            // cascade is intentionally omitted: HLE dialog window procs are
15327            // not loaded or executed as guest WDEF resources.
15328            // CouldDialog ($A979): Loads and HNoPurge-equivalent marks DLOG/DITL/resource-backed items; writes ResErr noErr on both hit and miss in BasiliskII.
15329            // FreeDialog ($A97A): HPurge-equivalent marks the already-loaded DLOG/DITL/resource-backed items; writes ResErr noErr on both hit and miss in BasiliskII.
15330            (true, 0x179) | (true, 0x17A) => {
15331                let sp = cpu.read_reg(Register::A7);
15332                let dialog_id = bus.read_word(sp) as i16;
15333                let load_if_missing = trap_num == 0x179;
15334                let purgeable = trap_num == 0x17A;
15335                self.cascade_dialog_resource_purgeability(
15336                    bus,
15337                    *b"DLOG",
15338                    dialog_id,
15339                    purgeable,
15340                    load_if_missing,
15341                );
15342                let res_err = self.dialog_template_res_err(dialog_id);
15343                bus.write_word(0x0A60, res_err as u16);
15344                cpu.write_reg(Register::A7, sp + 2);
15345                Ok(())
15346            }
15347
15348            // HideDialogItem ($A827)
15349            // Erases the item's enclosing rectangle, then moves the
15350            // item's display rect offscreen so it isn't drawn or hit-tested.
15351            // The original rect is saved so ShowDialogItem can restore it.
15352            // PROCEDURE HideDialogItem(theDialog: DialogPtr; itemNo: INTEGER);
15353            // Inside Macintosh Volume IV, IV-59
15354            // Stack: SP+0=itemNo(2), SP+2=theDialog(4). Pop 6.
15355            // HideDialogItem ($A827): If itemRect.left < 8192, calls EraseRect on
15356            // the item's enclosing rectangle, adds that rectangle to the update
15357            // region, and offsets itemRect.left/right by +16384 so the item
15358            // becomes offscreen; already-hidden items are unchanged
15359            // (Inside Macintosh Volume IV, IV-59; MTE 1992, 6-123).
15360            // Pops 6 bytes per IM:IV IV-59.
15361            (true, 0x027) => {
15362                let sp = cpu.read_reg(Register::A7);
15363                let item_no = bus.read_word(sp) as i16;
15364                let dialog_ptr = bus.read_long(sp + 2);
15365                cpu.write_reg(Register::A7, sp + 6);
15366                let mut updated_control_rect = None;
15367                let mut redraw_local_rect = None;
15368                if dialog_ptr != 0 && item_no > 0 {
15369                    let key = (dialog_ptr, item_no);
15370                    if let Some(items) = self.dialog_items.get_mut(&dialog_ptr) {
15371                        let idx = (item_no as usize).wrapping_sub(1);
15372                        if idx < items.len() {
15373                            let rect = items[idx].rect;
15374                            // MTE 1992, 6-123: already-hidden items (left > 8192) are a no-op.
15375                            if rect.1 < 8192 {
15376                                let enclosing = Self::dialog_item_enclosing_local_rect(
15377                                    items[idx].item_type,
15378                                    rect,
15379                                );
15380                                self.hidden_dialog_item_rects.entry(key).or_insert(rect);
15381                                items[idx].rect.1 = rect.1.wrapping_add(16384);
15382                                items[idx].rect.3 = rect.3.wrapping_add(16384);
15383                                updated_control_rect = Some(items[idx].rect);
15384                                redraw_local_rect = Some(enclosing);
15385                            }
15386                        }
15387                    }
15388                    if let Some(rect) = redraw_local_rect {
15389                        self.erase_dialog_item_enclosing_rect(bus, dialog_ptr, rect);
15390                        self.invalidate_window_rect(bus, dialog_ptr, rect);
15391                    }
15392                    if let Some(rect) = updated_control_rect {
15393                        if let Some(ctrl_handle) =
15394                            self.dialog_control_handle_for_item(dialog_ptr, item_no)
15395                        {
15396                            let ctrl_ptr = bus.read_long(ctrl_handle);
15397                            if ctrl_ptr != 0 {
15398                                bus.write_word(ctrl_ptr + 8, rect.0 as u16);
15399                                bus.write_word(ctrl_ptr + 10, rect.1 as u16);
15400                                bus.write_word(ctrl_ptr + 12, rect.2 as u16);
15401                                bus.write_word(ctrl_ptr + 14, rect.3 as u16);
15402                            }
15403                        }
15404                        if let Some(tracking) = self.dialog_tracking.as_mut() {
15405                            if tracking.dialog_ptr == dialog_ptr {
15406                                let idx = (item_no as usize).wrapping_sub(1);
15407                                if idx < tracking.items.len() {
15408                                    tracking.items[idx].rect = rect;
15409                                }
15410                            }
15411                        }
15412                    }
15413                }
15414                Ok(())
15415            }
15416
15417            // ShowDialogItem ($A828)
15418            // Restores the rect saved by HideDialogItem and invalidates
15419            // the new rect so the dialog redraws the item.
15420            // PROCEDURE ShowDialogItem(theDialog: DialogPtr; itemNo: INTEGER);
15421            // Inside Macintosh Volume IV, IV-59
15422            // ShowDialogItem ($A828): If itemRect.left > 8192, restores visibility by
15423            // subtracting 16384 from itemRect.left/right; already-visible items are unchanged
15424            // (MTE 1992, 6-124). Pops 6 bytes per IM:IV IV-59.
15425            (true, 0x028) => {
15426                let sp = cpu.read_reg(Register::A7);
15427                let item_no = bus.read_word(sp) as i16;
15428                let dialog_ptr = bus.read_long(sp + 2);
15429                cpu.write_reg(Register::A7, sp + 6);
15430                let mut updated_control_rect = None;
15431                let mut redraw_local_rect = None;
15432                if dialog_ptr != 0 && item_no > 0 {
15433                    let key = (dialog_ptr, item_no);
15434                    if let Some(items) = self.dialog_items.get_mut(&dialog_ptr) {
15435                        let idx = (item_no as usize).wrapping_sub(1);
15436                        if idx < items.len() {
15437                            let rect = items[idx].rect;
15438                            // MTE 1992, 6-124: already-visible items (left < 8192) are a no-op.
15439                            if rect.1 > 8192 {
15440                                let restored_rect = if let Some(orig_rect) =
15441                                    self.hidden_dialog_item_rects.remove(&key)
15442                                {
15443                                    orig_rect
15444                                } else {
15445                                    (
15446                                        rect.0,
15447                                        rect.1.wrapping_sub(16384),
15448                                        rect.2,
15449                                        rect.3.wrapping_sub(16384),
15450                                    )
15451                                };
15452                                let enclosing = Self::dialog_item_enclosing_local_rect(
15453                                    items[idx].item_type,
15454                                    restored_rect,
15455                                );
15456                                items[idx].rect = restored_rect;
15457                                updated_control_rect = Some(restored_rect);
15458                                redraw_local_rect = Some(enclosing);
15459                            }
15460                        }
15461                    }
15462                    if let Some(rect) = redraw_local_rect {
15463                        self.invalidate_window_rect(bus, dialog_ptr, rect);
15464                    }
15465                    if let Some(rect) = updated_control_rect {
15466                        if let Some(ctrl_handle) =
15467                            self.dialog_control_handle_for_item(dialog_ptr, item_no)
15468                        {
15469                            let ctrl_ptr = bus.read_long(ctrl_handle);
15470                            if ctrl_ptr != 0 {
15471                                bus.write_word(ctrl_ptr + 8, rect.0 as u16);
15472                                bus.write_word(ctrl_ptr + 10, rect.1 as u16);
15473                                bus.write_word(ctrl_ptr + 12, rect.2 as u16);
15474                                bus.write_word(ctrl_ptr + 14, rect.3 as u16);
15475                            }
15476                        }
15477                        if let Some(tracking) = self.dialog_tracking.as_mut() {
15478                            if tracking.dialog_ptr == dialog_ptr {
15479                                let idx = (item_no as usize).wrapping_sub(1);
15480                                if idx < tracking.items.len() {
15481                                    tracking.items[idx].rect = rect;
15482                                }
15483                            }
15484                        }
15485                    }
15486                }
15487                Ok(())
15488            }
15489
15490            // FindDItem ($A984)
15491            // Returns the 0-indexed item number of the item containing thePt
15492            // (in coordinates local to the dialog box), or –1 if no item
15493            // contains the point. Disabled items are returned per IM:IV-60;
15494            // hidden items naturally fail the rect-contains check because
15495            // HideDialogItem moves their rect to (16384, 16384, 16385, 16385).
15496            // Overlapping items resolve to the first matching item in DITL
15497            // order (Macintosh Toolbox Essentials 1992, 6-125).
15498            // FUNCTION FindDItem(theDialog: DialogPtr; thePt: Point): INTEGER;
15499            // Inside Macintosh Volume IV, IV-60; Macintosh Toolbox Essentials 1992, 6-125
15500            //
15501            // Stack layout (Pascal — args pushed left-to-right, result slot
15502            // pre-pushed by caller):
15503            //   SP+0..3:  thePt (Point — v at +0..1, h at +2..3)
15504            //   SP+4..7:  theDialog (DialogPtr)
15505            //   SP+8..9:  result slot (INTEGER)
15506            //
15507            // FindDItem ($A984): Walks dialog_items in order; first item whose
15508            // local rect contains thePt wins; returns 0-indexed item number or
15509            // -1 per Macintosh Toolbox Essentials 1992, p. 6-125.
15510            (true, 0x184) => {
15511                let sp = cpu.read_reg(Register::A7);
15512                let pt_v = bus.read_word(sp) as i16;
15513                let pt_h = bus.read_word(sp + 2) as i16;
15514                let dialog_ptr = bus.read_long(sp + 4);
15515                let result: i16 = if dialog_ptr == 0 {
15516                    -1
15517                } else if let Some(items) = self.dialog_items.get(&dialog_ptr) {
15518                    let mut hit: i16 = -1;
15519                    for (idx, item) in items.iter().enumerate() {
15520                        let (top, left, bottom, right) = item.rect;
15521                        if pt_v >= top && pt_v < bottom && pt_h >= left && pt_h < right {
15522                            hit = idx as i16;
15523                            break;
15524                        }
15525                    }
15526                    hit
15527                } else {
15528                    -1
15529                };
15530                bus.write_word(sp + 8, result as u16);
15531                cpu.write_reg(Register::A7, sp + 8);
15532                Ok(())
15533            }
15534
15535            // CouldAlert ($A989)
15536            // PROCEDURE CouldAlert(alertID: INTEGER);
15537            // Inside Macintosh Volume I, I-420
15538            //
15539            // FreeAlert ($A98A)
15540            // PROCEDURE FreeAlert(alertID: INTEGER);
15541            // Inside Macintosh Volume I, I-420
15542            //
15543            // Companion of CouldDialog/FreeDialog ($A979/$A97A) for ALRT
15544            // templates. Per IM:I I-420, CouldAlert loads and makes the
15545            // ALRT, DITL, and resource-backed DITL items unpurgeable;
15546            // FreeAlert marks the already-loaded equivalents purgeable.
15547            // BasiliskII treats missing alert IDs as harmless no-ops and
15548            // leaves ResErr at noErr.
15549            (true, 0x189) | (true, 0x18A) => {
15550                let sp = cpu.read_reg(Register::A7);
15551                let alert_id = bus.read_word(sp) as i16;
15552                let load_if_missing = trap_num == 0x189;
15553                let purgeable = trap_num == 0x18A;
15554                self.cascade_dialog_resource_purgeability(
15555                    bus,
15556                    *b"ALRT",
15557                    alert_id,
15558                    purgeable,
15559                    load_if_missing,
15560                );
15561                bus.write_word(0x0A60, 0);
15562                cpu.write_reg(Register::A7, sp + 2);
15563                Ok(())
15564            }
15565
15566            // ErrorSound ($A98C)
15567            // Sets the error-sound procedure for alerts.
15568            // PROCEDURE ErrorSound(soundProc: ProcPtr);
15569            // Inside Macintosh Volume I, I-411
15570            //
15571            // Per IM:I I-411: "The address of the sound procedure
15572            // being used is stored in the global variable DABeeper."
15573            // ErrorSound replaces the Dialog Manager's standard
15574            // sound procedure (installed by InitDialogs $A97B) with
15575            // the caller's `soundProc`. NIL is documented as "no
15576            // sound (or menu bar blinking) at all" — same encoding
15577            // as a NIL InitDialogs default.
15578            //
15579            // HLE compromise: Systemless doesn't invoke DABeeper from
15580            // any Alert family path (no menu-bar-blink emulation),
15581            // so this trap's only observable side effect is the
15582            // DABeeper global itself. Apps that probe DABeeper
15583            // before/after ErrorSound to detect "did the previous
15584            // app's sound proc get installed?" still see the
15585            // correct value. Future iterations that wire up
15586            // sound-proc invocation from Alert dispatch will pick
15587            // up the stored ProcPtr automatically.
15588            //
15589            // Pop = 4 bytes (soundProc ProcPtr).
15590            // ErrorSound ($A98C): Per IM:I I-411 stores soundProc at $0A9C (DABeeper global); NIL is the documented "no sound + no menu-bar-blink" default. Mirrors InitDialogs's "install standard sound procedure" step. HLE does not invoke DABeeper from Alert dispatch (no menu-bar-blink emulation), so this is state-only.
15591            (true, 0x18C) => {
15592                let sp = cpu.read_reg(Register::A7);
15593                let sound_proc = bus.read_long(sp);
15594                bus.write_long(crate::memory::globals::addr::DA_BEEPER, sound_proc);
15595                cpu.write_reg(Register::A7, sp + 4);
15596                Ok(())
15597            }
15598
15599            // SetDAFont ($A98D variant — actually uses DlgFont low-mem global)
15600            // Sets the font for subsequently created dialogs and alerts.
15601            // PROCEDURE SetDAFont(fontNum: INTEGER); [Not in ROM]
15602            // Inside Macintosh Volume I, I-412
15603            // Assembly-language: sets DlgFont ($0AFA) directly.
15604            //
15605            // Note: $A98D is GetDItem; SetDAFont is not a ROM trap but a
15606            // glue routine that writes DlgFont. We handle it via the
15607            // dedicated trap word $A97F (Pack13 slot repurposed).
15608            // If apps call it directly, they just write DlgFont.
15609
15610            // SetDialogItemText ($A98F)
15611            // Sets the text of a dialog item (statText or editText).
15612            // PROCEDURE SetDialogItemText(item: Handle; text: Str255);
15613            // Inside Macintosh Volume I, I-422; Macintosh Toolbox Essentials 1992, 6-131
15614            (true, 0x18F) => {
15615                let sp = cpu.read_reg(Register::A7);
15616                let pc = cpu.read_reg(Register::PC);
15617                let text_str_ptr = bus.read_long(sp);
15618                let item_handle = bus.read_long(sp + 4);
15619                let mut redraw_text_item = None;
15620                if text_str_ptr != 0 {
15621                    let bytes = bus.read_pstring(text_str_ptr);
15622                    let len = bytes.len();
15623                    let text = decode_mac_roman_for_render(&bytes);
15624
15625                    if item_handle != 0 {
15626                        let data_ptr = Self::ensure_text_handle_size(bus, item_handle, len);
15627                        if data_ptr != 0 {
15628                            bus.write_bytes(data_ptr, &bytes);
15629                        }
15630                    }
15631
15632                    // Also update the item in dialog_items so ModalDialog picks it up
15633                    if let Some((dlg_ptr, idx)) =
15634                        self.dialog_item_handles.get(&item_handle).copied()
15635                    {
15636                        let item_no = (idx + 1) as i16;
15637                        if trace_dialog_items_enabled() {
15638                            eprintln!(
15639                                "[DIALOG-ITEM] SetDialogItemText pc=${:08X} dialog=${:08X} item={} handle=${:08X} text={:?}",
15640                                pc,
15641                                dlg_ptr,
15642                                item_no,
15643                                item_handle,
15644                                text
15645                            );
15646                        }
15647                        if let Some(items) = self.dialog_items.get_mut(&dlg_ptr) {
15648                            if idx < items.len() {
15649                                items[idx].text = text.clone();
15650                            }
15651                        }
15652                        let mut refresh_tracking = false;
15653                        if let Some(tracking) = self.dialog_tracking.as_mut() {
15654                            if tracking.dialog_ptr == dlg_ptr && idx < tracking.items.len() {
15655                                tracking.items[idx].text = text.clone();
15656                                if tracking.edit_item == item_no {
15657                                    tracking.edit_text = text;
15658                                    tracking.edit_text_modified = false;
15659                                }
15660                                refresh_tracking = !tracking.game_managed;
15661                            }
15662                        }
15663                        if refresh_tracking {
15664                            self.refresh_dialog_tracking_snapshot(bus);
15665                        } else {
15666                            redraw_text_item = Some((dlg_ptr, item_no));
15667                        }
15668                    } else if trace_dialog_items_enabled() {
15669                        eprintln!(
15670                            "[DIALOG-ITEM] SetDialogItemText pc=${:08X} dialog=<unknown> handle=${:08X} text={:?}",
15671                            pc,
15672                            item_handle,
15673                            text
15674                        );
15675                    }
15676                }
15677
15678                if let Some((dlg_ptr, item_no)) = redraw_text_item {
15679                    self.redraw_dialog_text_item(bus, dlg_ptr, item_no);
15680                }
15681
15682                cpu.write_reg(Register::A7, sp + 8);
15683                Ok(())
15684            }
15685
15686            // GetDialogItemText ($A990)
15687            // Returns the text of a dialog item (statText or editText).
15688            // PROCEDURE GetDialogItemText(item: Handle; VAR text: Str255);
15689            // Inside Macintosh Volume I, I-422
15690            (true, 0x190) => {
15691                let sp = cpu.read_reg(Register::A7);
15692                let text_ptr = bus.read_long(sp);
15693                let item_handle = bus.read_long(sp + 4);
15694
15695                if text_ptr != 0 {
15696                    // If dialog tracking is active, return the current edit text
15697                    let mut wrote = false;
15698                    if let Some(ref tracking) = self.dialog_tracking {
15699                        let current_edit_handle =
15700                            self.dialog_item_handles.get(&item_handle).copied().filter(
15701                                |(dlg_ptr, idx)| {
15702                                    *dlg_ptr == tracking.dialog_ptr
15703                                        && (*idx as i16 + 1) == tracking.edit_item
15704                                },
15705                            );
15706                        if current_edit_handle.is_some() {
15707                            let bytes = tracking.edit_text.as_bytes();
15708                            let len = bytes.len().min(255);
15709                            bus.write_byte(text_ptr, len as u8);
15710                            for (i, byte) in bytes.iter().take(len).enumerate() {
15711                                bus.write_byte(text_ptr + 1 + i as u32, *byte);
15712                            }
15713                            wrote = true;
15714                        }
15715                    }
15716                    if !wrote {
15717                        // Text item handles store raw bytes, not a Pascal-length byte.
15718                        // Inside Macintosh Volume I, I-422; Executor dialManip.cpp
15719                        if item_handle != 0 {
15720                            let master = bus.read_long(item_handle);
15721                            if master != 0 {
15722                                let len = bus.get_alloc_size(master).unwrap_or(0).min(255) as usize;
15723                                bus.write_byte(text_ptr, len as u8);
15724                                for i in 0..len {
15725                                    bus.write_byte(
15726                                        text_ptr + 1 + i as u32,
15727                                        bus.read_byte(master + i as u32),
15728                                    );
15729                                }
15730                                wrote = true;
15731                            }
15732                        }
15733                        if !wrote {
15734                            bus.write_byte(text_ptr, 0);
15735                        }
15736                    }
15737                }
15738                cpu.write_reg(Register::A7, sp + 8);
15739                Ok(())
15740            }
15741
15742            // DialogDispatch ($AA68)
15743            // Selector-based dispatch for extended Dialog Manager routines.
15744            // Selector is in D0 (not on stack) per the MPW THREEWORDINLINE
15745            // sequence: MOVE.W #selector, D0 / _DialogDispatch. The low
15746            // byte is the routine number; the high byte encodes param-
15747            // bytes / 2 which the dispatcher uses to pop args.
15748            // Macintosh Toolbox Essentials 1992, 6-162
15749            // DialogDispatch ($AA68): Selector-based per MTb 1992 6-162..6-167: $03 GetStdFilterProc returns a guest-callable standard-filter shim ProcPtr so apps using the common `GetStdFilterProc(&p); ModalDialog(p, &itemHit);` pattern get a safe non-NIL proc instead of Systemless's old NIL compromise; the shim itself declines events (FALSE) so Systemless's ModalDialog default Return/Escape handling still runs. $04 SetDialogDefaultItem writes newItem to DialogRecord.aDefItem at offset 168 + mirrors into DialogTrackingState.default_item if dialog is being tracked; $05 SetDialogCancelItem stores newItem in DialogTrackingState.cancel_item (no canonical DialogRecord field — System 7 addition); $06 SetDialogTracksCursor is a no-op noErr (HLE has no interactive cursor tracking); $0C NewFeaturesDialog allocates an Appearance-flavored DialogRecord via NewCDialog delegation. All return noErr; pop = 2 (selector encoded in D0) + arg_words*2 from the (arg_words << 8) | routine D0 encoding.
15750            (true, 0x268) => {
15751                let sp = cpu.read_reg(Register::A7);
15752                let d0 = cpu.read_reg(Register::D0) as u16;
15753                let param_bytes = (((d0 >> 8) & 0xFF) as u32) * 2;
15754                let routine = d0 & 0xFF;
15755                match routine {
15756                    // NewFeaturesDialog (selector $0C, param_bytes=34)
15757                    // FUNCTION NewFeaturesDialog(inStorage: Ptr;
15758                    //     inBoundsRect: Rect; inTitle: ConstStr255Param;
15759                    //     inIsVisible: Boolean; inProcID: SInt16;
15760                    //     inBehind: WindowPtr; inGoAwayFlag: Boolean;
15761                    //     inRefCon: SInt32; inItems: Handle;
15762                    //     inFeatures: UInt32): DialogPtr;
15763                    // Mac Toolbox: Appearance Manager (Apple, 1997).
15764                    //
15765                    // Same shape as NewCDialog with one extra LongInt
15766                    // for Appearance feature flags. Mid-90s titles
15767                    // (e.g. Meteor Storm) call this unconditionally —
15768                    // even when Gestalt('appr') reports no features —
15769                    // to allocate their main dialog. Delegate to the
15770                    // existing NewCDialog path so the dialog is created
15771                    // without the Appearance theming, then drop the
15772                    // unused inFeatures word.
15773                    0x0C => {
15774                        // Stack (low to high), 34 bytes of params + 4 result:
15775                        //   SP+0:  inFeatures(4)
15776                        //   SP+4:  inItems(4)
15777                        //   SP+8:  inRefCon(4)
15778                        //   SP+12: inGoAwayFlag(2)
15779                        //   SP+14: inBehind(4)
15780                        //   SP+18: inProcID(2)
15781                        //   SP+20: inIsVisible(2)
15782                        //   SP+22: inTitle(4)
15783                        //   SP+26: inBoundsRect(4)
15784                        //   SP+30: inStorage(4)
15785                        //   SP+34: result DialogPtr(4)
15786                        let _features = bus.read_long(sp);
15787                        let items_handle = bus.read_long(sp + 4);
15788                        let ref_con = bus.read_long(sp + 8);
15789                        let go_away_flag = bus.read_byte(sp + 12) != 0;
15790                        let behind = bus.read_long(sp + 14);
15791                        let proc_id = bus.read_word(sp + 18) as i16;
15792                        let visible = bus.read_byte(sp + 20) != 0;
15793                        let title_ptr = bus.read_long(sp + 22);
15794                        let bounds_rect_ptr = bus.read_long(sp + 26);
15795                        let storage_ptr = bus.read_long(sp + 30);
15796
15797                        let bounds = if bounds_rect_ptr != 0 {
15798                            (
15799                                bus.read_word(bounds_rect_ptr) as i16,
15800                                bus.read_word(bounds_rect_ptr + 2) as i16,
15801                                bus.read_word(bounds_rect_ptr + 4) as i16,
15802                                bus.read_word(bounds_rect_ptr + 6) as i16,
15803                            )
15804                        } else {
15805                            (0, 0, 0, 0)
15806                        };
15807                        let title = if title_ptr != 0 {
15808                            decode_mac_roman_for_render(&bus.read_pstring(title_ptr))
15809                        } else {
15810                            String::new()
15811                        };
15812                        let items_ptr = if items_handle != 0 {
15813                            bus.read_long(items_handle)
15814                        } else {
15815                            0
15816                        };
15817                        let items_len = if items_ptr != 0 {
15818                            bus.get_alloc_size(items_ptr).unwrap_or(0)
15819                        } else {
15820                            0
15821                        };
15822                        let items = if items_ptr != 0 && items_len != 0 {
15823                            Self::parse_ditl(bus, items_ptr, items_len)
15824                        } else {
15825                            Vec::new()
15826                        };
15827
15828                        let dlg_ptr = self.finish_dialog_creation(
15829                            bus,
15830                            cpu,
15831                            storage_ptr,
15832                            bounds,
15833                            &title,
15834                            visible,
15835                            proc_id,
15836                            go_away_flag,
15837                            ref_con,
15838                            items_handle,
15839                            items,
15840                        );
15841                        self.apply_behind_parameter(bus, dlg_ptr, behind);
15842                        bus.write_long(sp + param_bytes, dlg_ptr);
15843                        cpu.write_reg(Register::A7, sp + param_bytes);
15844                    }
15845                    // GetStdFilterProc (selector 3, param_bytes=4)
15846                    // FUNCTION GetStdFilterProc(VAR theProc: ProcPtr): OSErr;
15847                    // Macintosh Toolbox Essentials 1992, 6-163
15848                    //
15849                    // Returns a ProcPtr to the system's standard
15850                    // event filter for modal dialogs. The public
15851                    // Dialogs.h / Dialogs.p declarations expose
15852                    // GetStdFilterProc as returning a ModalFilterUPP
15853                    // through a VAR/out pointer. MTb 1992 documents
15854                    // that NIL filterProc passed to ModalDialog uses
15855                    // the standard filter, and BasiliskII returns a
15856                    // non-NIL callable proc here.
15857                    //
15858                    // HLE compromise: return a tiny guest-resident
15859                    // Pascal FUNCTION shim that always returns FALSE:
15860                    //
15861                    //   JMP    shimBody ; recognizable callback entry for
15862                    //                    runner-side filter-proc firing
15863                    //   MOVEQ  #0,D0    ; Boolean result = FALSE in D0 too
15864                    //   CLR.W  16(SP)   ; Boolean result = FALSE
15865                    //   RTD    #12      ; pop dialog/event/itemHit args
15866                    //
15867                    // This makes the proc SAFE and callable for guest
15868                    // code (better than the older NIL placeholder) while
15869                    // still preserving Systemless's own ModalDialog default
15870                    // handling for Return/Escape when apps pass the proc
15871                    // straight back to ModalDialog.
15872                    //
15873                    // NIL VAR ptr (the caller passed NULL for theProc)
15874                    // is a defensive no-op — the impl skips the write
15875                    // rather than dereffing NIL.
15876                    0x03 => {
15877                        let proc_ptr = bus.read_long(sp);
15878                        let shim = if self.dialog_std_filter_proc != 0 {
15879                            self.dialog_std_filter_proc
15880                        } else {
15881                            let shim = bus.alloc(16);
15882                            bus.write_word(shim, 0x4EF9); // JMP abs.L shimBody
15883                            bus.write_long(shim + 2, shim + 6);
15884                            // Pascal FUNCTION ModalFilterProc(dialog, event, itemHit): Boolean
15885                            // entry stack layout:
15886                            //   +0  return address
15887                            //   +4  itemHit ptr
15888                            //   +8  EventRecord ptr
15889                            //   +12 DialogPtr
15890                            //   +16 Boolean result slot
15891                            bus.write_word(shim + 6, 0x7000); // MOVEQ #0, D0
15892                            bus.write_word(shim + 8, 0x426F); // CLR.W 16(SP)
15893                            bus.write_word(shim + 10, 0x0010);
15894                            bus.write_word(shim + 12, 0x4E74); // RTD #12
15895                            bus.write_word(shim + 14, 0x000C);
15896                            self.dialog_std_filter_proc = shim;
15897                            shim
15898                        };
15899                        if proc_ptr != 0 {
15900                            bus.write_long(proc_ptr, shim);
15901                        }
15902                        // pop params; leave result in place
15903                        bus.write_word(sp + param_bytes, 0); // noErr
15904                        cpu.write_reg(Register::A7, sp + param_bytes);
15905                    }
15906                    // SetDialogDefaultItem (selector 4, param_bytes=6)
15907                    // FUNCTION SetDialogDefaultItem(theDialog: DialogPtr;
15908                    //     newItem: INTEGER): OSErr;
15909                    // Macintosh Toolbox Essentials 1992, 6-164
15910                    //
15911                    // Stack: SP+0=newItem(2), SP+2=theDialog(4),
15912                    //        SP+6=result OSErr slot (caller pre-pushed).
15913                    //
15914                    // Marks `newItem` as the dialog's default
15915                    // button — pressed when user hits Return per
15916                    // IM:MTb 6-164. Stored in DialogRecord.aDefItem
15917                    // at offset 168 so subsequent ModalDialog
15918                    // redraws can outline the default button. If
15919                    // dialog tracking is active for this dialog,
15920                    // mirror the value into DialogTrackingState
15921                    // .default_item so the active redraw path
15922                    // sees it without re-reading guest memory.
15923                    0x04 => {
15924                        let new_item = bus.read_word(sp) as i16;
15925                        let dialog_ptr = bus.read_long(sp + 2);
15926                        if dialog_ptr != 0 {
15927                            // DialogRecord.aDefItem is at offset 168
15928                            // per the existing GetNewDialog impl
15929                            // (dialog.rs:1961 writes "aDefItem" here).
15930                            bus.write_word(dialog_ptr + 168, new_item as u16);
15931                            // Mirror into active tracking state if
15932                            // this dialog is currently being tracked.
15933                            if let Some(tracking) = self.dialog_tracking.as_mut() {
15934                                if tracking.dialog_ptr == dialog_ptr {
15935                                    tracking.default_item = new_item;
15936                                }
15937                            }
15938                        }
15939                        bus.write_word(sp + param_bytes, 0); // noErr
15940                        cpu.write_reg(Register::A7, sp + param_bytes);
15941                    }
15942                    // SetDialogCancelItem (selector 5, param_bytes=6)
15943                    // FUNCTION SetDialogCancelItem(theDialog: DialogPtr;
15944                    //     newItem: INTEGER): OSErr;
15945                    // Macintosh Toolbox Essentials 1992, 6-165
15946                    //
15947                    // System 7 addition (no canonical DialogRecord
15948                    // field — Apple added cancel-item tracking to
15949                    // the Dialog Manager AFTER the original
15950                    // DialogRecord layout was finalized). Stored in
15951                    // a host-side per-dialog map and mirrored into
15952                    // active tracking state when ModalDialog is
15953                    // already running. NIL theDialog is a defensive
15954                    // no-op.
15955                    0x05 => {
15956                        let new_item = bus.read_word(sp) as i16;
15957                        let dialog_ptr = bus.read_long(sp + 2);
15958                        if dialog_ptr != 0 {
15959                            self.dialog_cancel_items.insert(dialog_ptr, new_item);
15960                            if let Some(tracking) = self.dialog_tracking.as_mut() {
15961                                if tracking.dialog_ptr == dialog_ptr {
15962                                    tracking.cancel_item = new_item;
15963                                }
15964                            }
15965                        }
15966                        bus.write_word(sp + param_bytes, 0); // noErr
15967                        cpu.write_reg(Register::A7, sp + param_bytes);
15968                    }
15969                    // SetDialogTracksCursor (selector 6, param_bytes=6)
15970                    // FUNCTION SetDialogTracksCursor(theDialog: DialogPtr;
15971                    //     tracks: BOOLEAN): OSErr;
15972                    // Macintosh Toolbox Essentials 1992, 6-166
15973                    //
15974                    // System 7 addition that controls whether the
15975                    // Dialog Manager auto-changes the cursor (to
15976                    // an I-beam) when the mouse is over an
15977                    // editText item. HLE compromise: Systemless
15978                    // doesn't model interactive cursor tracking
15979                    // (no continuous mouse-poll stream from the
15980                    // scripted event source), so the trap is a
15981                    // no-op noErr. Apps that defensively call this
15982                    // at dialog setup time get noErr and proceed.
15983                    0x06 => {
15984                        bus.write_word(sp + param_bytes, 0); // noErr
15985                        cpu.write_reg(Register::A7, sp + param_bytes);
15986                    }
15987                    _ => {
15988                        eprintln!(
15989                            "[TRAP] DialogDispatch unknown routine=${:02X} d0=${:04X}",
15990                            routine, d0
15991                        );
15992                        cpu.write_reg(Register::A7, sp + param_bytes);
15993                    }
15994                }
15995                Ok(())
15996            }
15997
15998            _ => return None,
15999        })
16000    }
16001}
16002
16003#[cfg(test)]
16004mod tests {
16005    use super::super::test_helpers::{setup, setup_with_port, MockCpu, TEST_SP};
16006    use crate::cpu::{CpuOps, Register};
16007    use crate::memory::{MacMemoryBus, MemoryBus};
16008    use crate::trap::dispatch::{
16009        DialogButtonTrackingState, DialogItem, DialogPopupDraw, DialogTrackingState,
16010        PendingDialogPopupMenu, PersistentDialogSnapshot, RetainedModalDialogClickState,
16011    };
16012    use crate::trap::menu::{Menu, MenuItem};
16013    use crate::trap::TrapDispatcher;
16014    use crate::ui_theme::UiThemeId;
16015    use std::collections::VecDeque;
16016
16017    fn screen_pixel_is_set(bus: &MacMemoryBus, base: u32, row_bytes: u32, x: i16, y: i16) -> bool {
16018        let byte = bus.read_byte(base + (y as u32 * row_bytes) + ((x as u32) / 8));
16019        byte & (0x80u8 >> ((x as u8) & 7)) != 0
16020    }
16021
16022    fn count_set_pixels(
16023        bus: &MacMemoryBus,
16024        base: u32,
16025        row_bytes: u32,
16026        top: i16,
16027        left: i16,
16028        bottom: i16,
16029        right: i16,
16030    ) -> u32 {
16031        let mut count = 0;
16032        for y in top..bottom {
16033            for x in left..right {
16034                if screen_pixel_is_set(bus, base, row_bytes, x, y) {
16035                    count += 1;
16036                }
16037            }
16038        }
16039        count
16040    }
16041
16042    fn count_pixel_index(
16043        bus: &MacMemoryBus,
16044        base: u32,
16045        row_bytes: u32,
16046        top: i16,
16047        left: i16,
16048        bottom: i16,
16049        right: i16,
16050        pixel_index: u8,
16051    ) -> u32 {
16052        let mut count = 0;
16053        for y in top..bottom {
16054            for x in left..right {
16055                if bus.read_byte(base + y as u32 * row_bytes + x as u32) == pixel_index {
16056                    count += 1;
16057                }
16058            }
16059        }
16060        count
16061    }
16062
16063    fn sum_screen_bytes(
16064        bus: &MacMemoryBus,
16065        base: u32,
16066        row_bytes: u32,
16067        top: i16,
16068        left: i16,
16069        bottom: i16,
16070        right: i16,
16071    ) -> u64 {
16072        let mut sum = 0;
16073        for y in top..bottom {
16074            let row = base + (y as u32 * row_bytes);
16075            for x in left..right {
16076                sum += u64::from(bus.read_byte(row + x as u32));
16077            }
16078        }
16079        sum
16080    }
16081
16082    fn make_style_scrap(
16083        bus: &mut MacMemoryBus,
16084        styles: &[(u32, i16, i16, i16, i16, i16, (u16, u16, u16))],
16085    ) -> u32 {
16086        let handle = TrapDispatcher::allocate_handle_with_data(
16087            bus,
16088            TrapDispatcher::SCRAP_STYLE_TAB_OFFSET
16089                + (styles.len() as u32 * TrapDispatcher::SCRAP_STYLE_ELEMENT_SIZE),
16090        );
16091        let ptr = bus.read_long(handle);
16092        bus.write_word(
16093            ptr + TrapDispatcher::SCRAP_N_STYLES_OFFSET,
16094            styles.len() as u16,
16095        );
16096        for (index, (start, height, ascent, font, face, size, color)) in styles.iter().enumerate() {
16097            let base = ptr
16098                + TrapDispatcher::SCRAP_STYLE_TAB_OFFSET
16099                + (index as u32 * TrapDispatcher::SCRAP_STYLE_ELEMENT_SIZE);
16100            bus.write_long(base + TrapDispatcher::SCRAP_STYLE_START_CHAR_OFFSET, *start);
16101            bus.write_word(
16102                base + TrapDispatcher::SCRAP_STYLE_HEIGHT_OFFSET,
16103                *height as u16,
16104            );
16105            bus.write_word(
16106                base + TrapDispatcher::SCRAP_STYLE_ASCENT_OFFSET,
16107                *ascent as u16,
16108            );
16109            bus.write_word(base + TrapDispatcher::SCRAP_STYLE_FONT_OFFSET, *font as u16);
16110            bus.write_byte(base + TrapDispatcher::SCRAP_STYLE_FACE_OFFSET, *face as u8);
16111            bus.write_word(base + TrapDispatcher::SCRAP_STYLE_SIZE_OFFSET, *size as u16);
16112            bus.write_word(base + TrapDispatcher::SCRAP_STYLE_COLOR_OFFSET, color.0);
16113            bus.write_word(base + TrapDispatcher::SCRAP_STYLE_COLOR_OFFSET + 2, color.1);
16114            bus.write_word(base + TrapDispatcher::SCRAP_STYLE_COLOR_OFFSET + 4, color.2);
16115        }
16116        handle
16117    }
16118
16119    // ---- InitDialogs ($A97B) ----
16120
16121    #[test]
16122    fn init_dialogs_pops_4_bytes() {
16123        let (mut disp, mut cpu, mut bus) = setup();
16124        // SP+0: ptr (4 bytes)
16125        bus.write_long(TEST_SP, 0x00000000);
16126
16127        let result = disp.dispatch_dialog(true, 0x17B, &mut cpu, &mut bus);
16128        assert!(result.unwrap().is_ok());
16129        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
16130    }
16131
16132    #[test]
16133    fn dialog_edit_state_falls_back_to_first_valid_edit_text_item() {
16134        let (_disp, _cpu, mut bus) = setup();
16135        let dialog_ptr = bus.alloc(170);
16136        bus.write_word(dialog_ptr + 164, 0); // stale/default editField points at item 1
16137        bus.write_word(dialog_ptr + 168, 1); // default OK button
16138        let items = vec![
16139            DialogItem {
16140                item_type: 4,
16141                text: "OK".to_string(),
16142                ..Default::default()
16143            },
16144            DialogItem {
16145                item_type: 4,
16146                text: "Cancel".to_string(),
16147                ..Default::default()
16148            },
16149            DialogItem {
16150                item_type: 16,
16151                text: "answer".to_string(),
16152                ..Default::default()
16153            },
16154        ];
16155
16156        let (text, edit_item, default_item) =
16157            TrapDispatcher::dialog_edit_state(&bus, dialog_ptr, &items);
16158
16159        assert_eq!(text, "answer");
16160        assert_eq!(edit_item, 3);
16161        assert_eq!(default_item, 1);
16162    }
16163
16164    #[test]
16165    fn dialog_edit_state_empty_live_handle_does_not_fallback_to_template_text() {
16166        let (_disp, _cpu, mut bus) = setup();
16167        let dialog_ptr = bus.alloc(170);
16168        let items_handle = bus.alloc(4);
16169        let ditl_ptr = bus.alloc(16);
16170        let text_handle = bus.alloc(4);
16171
16172        bus.write_long(text_handle, 0); // valid empty text handle
16173        bus.write_long(items_handle, ditl_ptr);
16174        bus.write_long(dialog_ptr + 156, items_handle);
16175        bus.write_word(dialog_ptr + 164, 0); // item 1 is the active edit field
16176        bus.write_word(dialog_ptr + 168, 1);
16177        bus.write_word(ditl_ptr, 0);
16178        bus.write_long(ditl_ptr + 2, text_handle);
16179        bus.write_word(ditl_ptr + 6, 10);
16180        bus.write_word(ditl_ptr + 8, 10);
16181        bus.write_word(ditl_ptr + 10, 26);
16182        bus.write_word(ditl_ptr + 12, 120);
16183        bus.write_byte(ditl_ptr + 14, 16);
16184        bus.write_byte(ditl_ptr + 15, 0);
16185
16186        let items = vec![DialogItem {
16187            item_type: 16,
16188            rect: (10, 10, 26, 120),
16189            text: "Edit Text".to_string(),
16190            resource_id: 0,
16191            proc_ptr: 0,
16192            sel_start: 0,
16193            sel_end: 0,
16194        }];
16195
16196        let (text, edit_item, default_item) =
16197            TrapDispatcher::dialog_edit_state(&bus, dialog_ptr, &items);
16198
16199        assert_eq!(text, "");
16200        assert_eq!(edit_item, 1);
16201        assert_eq!(default_item, 1);
16202    }
16203
16204    #[test]
16205    fn parse_dlog_ignores_position_word_when_resource_is_classic_length() {
16206        let (_disp, _cpu, mut bus) = setup();
16207        let ptr = bus.alloc(24);
16208        bus.write_word(ptr, 228); // top
16209        bus.write_word(ptr + 2, 198); // left
16210        bus.write_word(ptr + 4, 372); // bottom
16211        bus.write_word(ptr + 6, 455); // right
16212        bus.write_word(ptr + 8, 2); // procID
16213        bus.write_byte(ptr + 10, 1); // visible
16214        bus.write_byte(ptr + 12, 0); // goAway
16215        bus.write_long(ptr + 14, 0); // refCon
16216        bus.write_word(ptr + 18, 1013); // itemsID
16217        bus.write_byte(ptr + 20, 0); // empty title
16218        bus.write_byte(ptr + 21, 0); // padding only, not a position field
16219        bus.write_word(ptr + 22, 0x280A); // poison: centerMainScreen if over-read
16220
16221        let (bounds, proc_id, visible, items_id, title, position) =
16222            TrapDispatcher::parse_dlog(&bus, ptr, 22);
16223
16224        assert_eq!(bounds, (228, 198, 372, 455));
16225        assert_eq!(proc_id, 2);
16226        assert!(visible);
16227        assert_eq!(items_id, 1013);
16228        assert_eq!(title, "");
16229        assert_eq!(position, 0);
16230    }
16231
16232    #[test]
16233    fn parse_dlog_reads_position_word_when_resource_contains_it() {
16234        let (_disp, _cpu, mut bus) = setup();
16235        let ptr = bus.alloc(24);
16236        bus.write_word(ptr, 10);
16237        bus.write_word(ptr + 2, 20);
16238        bus.write_word(ptr + 4, 110);
16239        bus.write_word(ptr + 6, 220);
16240        bus.write_word(ptr + 8, 2);
16241        bus.write_byte(ptr + 10, 1);
16242        bus.write_long(ptr + 14, 0);
16243        bus.write_word(ptr + 18, 42);
16244        bus.write_byte(ptr + 20, 0);
16245        bus.write_byte(ptr + 21, 0);
16246        bus.write_word(ptr + 22, 0x280A);
16247
16248        let (_, _, _, _, _, position) = TrapDispatcher::parse_dlog(&bus, ptr, 24);
16249
16250        assert_eq!(position, 0x280A);
16251    }
16252
16253    #[test]
16254    fn dlog_parse_title_alignment_even_and_odd() {
16255        let (_disp, _cpu, mut bus) = setup();
16256        let odd = build_test_dlog_with_title((10, 20, 110, 220), 701, "Odd", 0x300A);
16257        let even = build_test_dlog_with_title((11, 21, 111, 221), 702, "Even", 0x700A);
16258
16259        let odd_ptr = bus.alloc(odd.len() as u32);
16260        bus.write_bytes(odd_ptr, &odd);
16261        let even_ptr = bus.alloc(even.len() as u32);
16262        bus.write_bytes(even_ptr, &even);
16263
16264        // MTE 1992 p. 6-148: the optional position word follows the Pascal
16265        // title string after a 0-or-1-byte alignment pad.
16266        let (odd_bounds, _, _, odd_items, odd_title, odd_position) =
16267            TrapDispatcher::parse_dlog(&bus, odd_ptr, odd.len() as u32);
16268        let (even_bounds, _, _, even_items, even_title, even_position) =
16269            TrapDispatcher::parse_dlog(&bus, even_ptr, even.len() as u32);
16270
16271        assert_eq!(odd_bounds, (10, 20, 110, 220));
16272        assert_eq!(odd_items, 701);
16273        assert_eq!(odd_title, "Odd");
16274        assert_eq!(odd_position, 0x300A);
16275        assert_eq!(odd.len(), 26);
16276
16277        assert_eq!(even_bounds, (11, 21, 111, 221));
16278        assert_eq!(even_items, 702);
16279        assert_eq!(even_title, "Even");
16280        assert_eq!(even_position, 0x700A);
16281        assert_eq!(even.len(), 28);
16282    }
16283
16284    #[test]
16285    fn dlog_parse_position_constants() {
16286        let (_disp, _cpu, mut bus) = setup();
16287        // MTE 1992 p. 6-148 documents the compiled DLOG position constants
16288        // stored after the aligned title string.
16289        for position in [0x0000u16, 0xB00A, 0x300A, 0x700A] {
16290            let dlog = build_test_dlog_with_title((10, 20, 110, 220), 711, "P", position);
16291            let ptr = bus.alloc(dlog.len() as u32);
16292            bus.write_bytes(ptr, &dlog);
16293
16294            let (_, _, _, _, _, parsed_position) =
16295                TrapDispatcher::parse_dlog(&bus, ptr, dlog.len() as u32);
16296
16297            assert_eq!(parsed_position, position);
16298        }
16299    }
16300
16301    #[test]
16302    fn alrt_parse_stage_nibbles_and_position() {
16303        let (_disp, _cpu, mut bus) = setup();
16304        let mut alrt = build_alrt_template(-321, 0xF721);
16305        alrt.extend_from_slice(&0xB00Au16.to_be_bytes());
16306        let ptr = bus.alloc(alrt.len() as u32);
16307        bus.write_bytes(ptr, &alrt);
16308
16309        let (bounds, items_id, stages, position) =
16310            TrapDispatcher::parse_alrt(&bus, ptr, alrt.len() as u32);
16311
16312        assert_eq!(bounds, (0, 0, 80, 200));
16313        assert_eq!(items_id, -321);
16314        assert_eq!(stages, 0xF721);
16315        // IM:I I-425 to I-426: low nibble is stage 1, high nibble is stage 4.
16316        assert_eq!(stages & 0x000F, 0x0001);
16317        assert_eq!((stages >> 4) & 0x000F, 0x0002);
16318        assert_eq!((stages >> 8) & 0x000F, 0x0007);
16319        assert_eq!((stages >> 12) & 0x000F, 0x000F);
16320        assert_eq!(position, 0xB00A);
16321
16322        let classic_alrt = build_alrt_template(123, 0x0008);
16323        let classic_ptr = bus.alloc(classic_alrt.len() as u32);
16324        bus.write_bytes(classic_ptr, &classic_alrt);
16325        let (_, classic_items_id, classic_stages, classic_position) =
16326            TrapDispatcher::parse_alrt(&bus, classic_ptr, classic_alrt.len() as u32);
16327        assert_eq!(classic_items_id, 123);
16328        assert_eq!(classic_stages, 0x0008);
16329        assert_eq!(classic_position, 0);
16330    }
16331
16332    fn push_ditl_count(data: &mut Vec<u8>, count_minus_one: i16) {
16333        data.extend_from_slice(&count_minus_one.to_be_bytes());
16334    }
16335
16336    fn push_ditl_item(
16337        data: &mut Vec<u8>,
16338        item_handle: u32,
16339        rect: (i16, i16, i16, i16),
16340        item_type: u8,
16341        length_or_reserved: u8,
16342        payload: &[u8],
16343    ) {
16344        data.extend_from_slice(&item_handle.to_be_bytes());
16345        data.extend_from_slice(&rect.0.to_be_bytes());
16346        data.extend_from_slice(&rect.1.to_be_bytes());
16347        data.extend_from_slice(&rect.2.to_be_bytes());
16348        data.extend_from_slice(&rect.3.to_be_bytes());
16349        data.push(item_type);
16350        data.push(length_or_reserved);
16351        data.extend_from_slice(payload);
16352        if payload.len() % 2 != 0 {
16353            data.push(0);
16354        }
16355    }
16356
16357    fn parse_ditl_bytes(bus: &mut MacMemoryBus, data: &[u8]) -> Vec<DialogItem> {
16358        let ptr = bus.alloc(data.len() as u32);
16359        bus.write_bytes(ptr, data);
16360        TrapDispatcher::parse_ditl(bus, ptr, data.len() as u32)
16361    }
16362
16363    #[test]
16364    fn ditl_iter_empty_count_minus_one() {
16365        let (_disp, _cpu, mut bus) = setup();
16366        let mut ditl = Vec::new();
16367        // IM:I I-427: the first word stores the number of items minus 1.
16368        push_ditl_count(&mut ditl, -1);
16369
16370        let items = parse_ditl_bytes(&mut bus, &ditl);
16371
16372        assert!(items.is_empty());
16373    }
16374
16375    #[test]
16376    fn ditl_iter_button_checkbox_radio_static_edit_text() {
16377        let (_disp, _cpu, mut bus) = setup();
16378        let mut ditl = Vec::new();
16379        push_ditl_count(&mut ditl, 4);
16380        for (item_type, rect, text) in [
16381            (4, (1, 2, 11, 42), "OK"),
16382            (5, (12, 2, 22, 72), "Check"),
16383            (6, (23, 2, 33, 72), "Radio"),
16384            (8, (34, 2, 44, 92), "Static"),
16385            (16, (45, 2, 57, 122), "Edit"),
16386        ] {
16387            push_ditl_item(
16388                &mut ditl,
16389                0,
16390                rect,
16391                item_type,
16392                text.len() as u8,
16393                text.as_bytes(),
16394            );
16395        }
16396
16397        let items = parse_ditl_bytes(&mut bus, &ditl);
16398
16399        assert_eq!(items.len(), 5);
16400        assert_eq!(items[0].item_type, 4);
16401        assert_eq!(items[0].rect, (1, 2, 11, 42));
16402        assert_eq!(items[0].text, "OK");
16403        assert_eq!(items[1].item_type, 5);
16404        assert_eq!(items[1].text, "Check");
16405        assert_eq!(items[2].item_type, 6);
16406        assert_eq!(items[2].text, "Radio");
16407        assert_eq!(items[3].item_type, 8);
16408        assert_eq!(items[3].text, "Static");
16409        assert_eq!(items[4].item_type, 16);
16410        assert_eq!(items[4].rect, (45, 2, 57, 122));
16411        assert_eq!(items[4].text, "Edit");
16412    }
16413
16414    #[test]
16415    fn ditl_iter_control_icon_picture_resource_id() {
16416        let (_disp, _cpu, mut bus) = setup();
16417        let mut ditl = Vec::new();
16418        push_ditl_count(&mut ditl, 2);
16419        // MTE 1992 p. 6-153 documents resource-backed DITL items as a
16420        // reserved byte followed by a signed 16-bit resource ID.
16421        push_ditl_item(&mut ditl, 0, (1, 2, 11, 42), 7, 0, &128i16.to_be_bytes());
16422        // IM:I I-427 describes the same slot as a length byte of 2.
16423        push_ditl_item(
16424            &mut ditl,
16425            0,
16426            (12, 2, 28, 34),
16427            32,
16428            2,
16429            &(-42i16).to_be_bytes(),
16430        );
16431        push_ditl_item(&mut ditl, 0, (29, 2, 70, 90), 64, 0, &300i16.to_be_bytes());
16432
16433        let items = parse_ditl_bytes(&mut bus, &ditl);
16434
16435        assert_eq!(items.len(), 3);
16436        assert_eq!(items[0].item_type, 7);
16437        assert_eq!(items[0].resource_id, 128);
16438        assert_eq!(items[1].item_type, 32);
16439        assert_eq!(items[1].resource_id, -42);
16440        assert_eq!(items[2].item_type, 64);
16441        assert_eq!(items[2].resource_id, 300);
16442    }
16443
16444    #[test]
16445    fn ditl_iter_user_item_without_data_bytes() {
16446        let (_disp, _cpu, mut bus) = setup();
16447        let mut ditl = Vec::new();
16448        push_ditl_count(&mut ditl, 0);
16449        push_ditl_item(&mut ditl, 0x12345678, (10, 20, 30, 40), 0, 0, &[]);
16450
16451        let items = parse_ditl_bytes(&mut bus, &ditl);
16452
16453        assert_eq!(items.len(), 1);
16454        assert_eq!(items[0].item_type, 0);
16455        assert_eq!(items[0].rect, (10, 20, 30, 40));
16456        assert_eq!(items[0].proc_ptr, 0x12345678);
16457    }
16458
16459    #[test]
16460    fn ditl_iter_help_item_sized_payload() {
16461        let (_disp, _cpu, mut bus) = setup();
16462        let mut ditl = Vec::new();
16463        push_ditl_count(&mut ditl, 1);
16464        // MTE 1992 p. 6-154: help items use a 4- or 6-byte payload.
16465        push_ditl_item(
16466            &mut ditl,
16467            0,
16468            (0, 0, 0, 0),
16469            1,
16470            6,
16471            &[0x00, 0x08, 0x01, 0x2C, 0x00, 0x02],
16472        );
16473        push_ditl_item(&mut ditl, 0, (5, 6, 17, 50), 4, 2, b"OK");
16474
16475        let items = parse_ditl_bytes(&mut bus, &ditl);
16476
16477        assert_eq!(items.len(), 2);
16478        assert_eq!(items[0].item_type, 1);
16479        assert_eq!(items[1].item_type, 4);
16480        assert_eq!(items[1].text, "OK");
16481    }
16482
16483    #[test]
16484    fn ditl_iter_odd_pascal_text_alignment() {
16485        let (_disp, _cpu, mut bus) = setup();
16486        let mut ditl = Vec::new();
16487        push_ditl_count(&mut ditl, 1);
16488        push_ditl_item(&mut ditl, 0, (1, 2, 11, 42), 8, 3, b"Odd");
16489        push_ditl_item(&mut ditl, 0, (12, 2, 22, 42), 4, 2, b"OK");
16490
16491        let items = parse_ditl_bytes(&mut bus, &ditl);
16492
16493        assert_eq!(items.len(), 2);
16494        assert_eq!(items[0].text, "Odd");
16495        assert_eq!(items[1].rect, (12, 2, 22, 42));
16496        assert_eq!(items[1].text, "OK");
16497    }
16498
16499    #[test]
16500    fn ditl_iter_even_pascal_text_alignment() {
16501        let (_disp, _cpu, mut bus) = setup();
16502        let mut ditl = Vec::new();
16503        push_ditl_count(&mut ditl, 1);
16504        push_ditl_item(&mut ditl, 0, (1, 2, 11, 42), 8, 4, b"Even");
16505        push_ditl_item(&mut ditl, 0, (12, 2, 22, 42), 4, 2, b"OK");
16506
16507        let items = parse_ditl_bytes(&mut bus, &ditl);
16508
16509        assert_eq!(items.len(), 2);
16510        assert_eq!(items[0].text, "Even");
16511        assert_eq!(items[1].rect, (12, 2, 22, 42));
16512        assert_eq!(items[1].text, "OK");
16513    }
16514
16515    #[test]
16516    fn ditl_iter_rejects_or_stops_at_truncated_item() {
16517        let (_disp, _cpu, mut bus) = setup();
16518        let mut ditl = Vec::new();
16519        push_ditl_count(&mut ditl, 1);
16520        push_ditl_item(&mut ditl, 0, (1, 2, 11, 42), 4, 2, b"OK");
16521        ditl.extend_from_slice(&0u32.to_be_bytes());
16522        for coord in [12i16, 2, 22, 72] {
16523            ditl.extend_from_slice(&coord.to_be_bytes());
16524        }
16525        ditl.push(8);
16526        ditl.push(6);
16527        ditl.extend_from_slice(b"Bad");
16528
16529        let items = parse_ditl_bytes(&mut bus, &ditl);
16530
16531        assert_eq!(items.len(), 1);
16532        assert_eq!(items[0].text, "OK");
16533    }
16534
16535    fn build_test_dlog(bounds: (i16, i16, i16, i16), items_id: i16, position: u16) -> Vec<u8> {
16536        build_test_dlog_with_title(bounds, items_id, "", position)
16537    }
16538
16539    fn build_test_dlog_with_title(
16540        bounds: (i16, i16, i16, i16),
16541        items_id: i16,
16542        title: &str,
16543        position: u16,
16544    ) -> Vec<u8> {
16545        let mut data = Vec::with_capacity(22 + title.len() + (title.len() & 1));
16546        data.extend_from_slice(&bounds.0.to_be_bytes());
16547        data.extend_from_slice(&bounds.1.to_be_bytes());
16548        data.extend_from_slice(&bounds.2.to_be_bytes());
16549        data.extend_from_slice(&bounds.3.to_be_bytes());
16550        data.extend_from_slice(&2i16.to_be_bytes()); // procID: plainDBox
16551        data.push(0); // invisible; geometry-only tests do not need drawing
16552        data.push(0); // filler
16553        data.push(0); // goAwayFlag
16554        data.push(0); // filler
16555        data.extend_from_slice(&0u32.to_be_bytes()); // refCon
16556        data.extend_from_slice(&items_id.to_be_bytes());
16557        data.push(title.len() as u8);
16558        data.extend_from_slice(title.as_bytes());
16559        if (1 + title.len()) % 2 != 0 {
16560            data.push(0); // title alignment padding
16561        }
16562        data.extend_from_slice(&position.to_be_bytes());
16563        data
16564    }
16565
16566    fn build_test_ditl_item(
16567        item_type: u8,
16568        rect: (i16, i16, i16, i16),
16569        item_data: &[u8],
16570    ) -> Vec<u8> {
16571        build_test_ditl_items(&[(item_type, rect, item_data)])
16572    }
16573
16574    fn build_test_ditl_items(items: &[(u8, (i16, i16, i16, i16), &[u8])]) -> Vec<u8> {
16575        let total_len = items.iter().fold(2usize, |len, (_, _, item_data)| {
16576            len + 14 + item_data.len() + (item_data.len() & 1)
16577        });
16578        let mut data = Vec::with_capacity(total_len);
16579        data.extend_from_slice(&((items.len() as u16).saturating_sub(1)).to_be_bytes());
16580        for (item_type, rect, item_data) in items {
16581            append_test_ditl_item(&mut data, *item_type, *rect, item_data);
16582        }
16583        data
16584    }
16585
16586    fn append_test_ditl_item(
16587        data: &mut Vec<u8>,
16588        item_type: u8,
16589        rect: (i16, i16, i16, i16),
16590        item_data: &[u8],
16591    ) {
16592        data.extend_from_slice(&0u32.to_be_bytes()); // itmHandle / userItem proc ptr
16593        data.extend_from_slice(&rect.0.to_be_bytes());
16594        data.extend_from_slice(&rect.1.to_be_bytes());
16595        data.extend_from_slice(&rect.2.to_be_bytes());
16596        data.extend_from_slice(&rect.3.to_be_bytes());
16597        data.push(item_type);
16598        data.push(item_data.len() as u8);
16599        data.extend_from_slice(item_data);
16600        if item_data.len() % 2 != 0 {
16601            data.push(0);
16602        }
16603    }
16604
16605    fn build_test_alrt(bounds: (i16, i16, i16, i16), items_id: i16, stages: u16) -> Vec<u8> {
16606        let mut data = Vec::with_capacity(12);
16607        data.extend_from_slice(&bounds.0.to_be_bytes());
16608        data.extend_from_slice(&bounds.1.to_be_bytes());
16609        data.extend_from_slice(&bounds.2.to_be_bytes());
16610        data.extend_from_slice(&bounds.3.to_be_bytes());
16611        data.extend_from_slice(&items_id.to_be_bytes());
16612        data.extend_from_slice(&stages.to_be_bytes());
16613        data
16614    }
16615
16616    #[test]
16617    fn multi_button_alert_tracks_click_and_returns_selected_item() {
16618        // Multi-choice Alert resources are real modal dialogs, not merely
16619        // OK-only notices. Keep the Alert function frame pending until a
16620        // button is selected, then write the selected item number into the
16621        // function result slot at SP+6.
16622        let (mut disp, mut cpu, mut bus) = setup();
16623        let alert_id = 3300;
16624        let ditl_id = 3301;
16625        let alrt = build_test_alrt((100, 100, 210, 400), ditl_id, 0x5555);
16626        let ditl = build_test_ditl_items(&[
16627            (4, (70, 230, 90, 290), b"Choice 1"),
16628            (4, (70, 130, 90, 210), b"Choice 2"),
16629            (4, (70, 20, 90, 100), b"Cancel"),
16630            (
16631                8,
16632                (16, 20, 56, 280),
16633                b"Select one of these options before continuing.",
16634            ),
16635        ]);
16636        disp.install_test_resource(&mut bus, *b"ALRT", alert_id, &alrt);
16637        disp.install_test_resource(&mut bus, *b"DITL", ditl_id, &ditl);
16638
16639        bus.write_long(TEST_SP, 0); // filterProc
16640        bus.write_word(TEST_SP + 4, alert_id as u16);
16641        bus.write_word(TEST_SP + 6, 0xCAFE);
16642        let result = disp.dispatch_dialog(true, 0x185, &mut cpu, &mut bus);
16643
16644        assert!(result.unwrap().is_ok());
16645        assert_eq!(cpu.read_reg(Register::A7), TEST_SP);
16646        assert_eq!(bus.read_word(TEST_SP + 6), 0);
16647        assert!(disp.dialog_tracking.is_some());
16648        assert!(disp.is_tracking_refire(0xA985));
16649
16650        disp.push_mouse_down(180, 250);
16651        for _ in 0..4 {
16652            let result = disp.dispatch_dialog(true, 0x185, &mut cpu, &mut bus);
16653            assert!(result.unwrap().is_ok());
16654            if disp
16655                .dialog_tracking
16656                .as_ref()
16657                .and_then(|tracking| tracking.active_button.as_ref())
16658                .is_some()
16659            {
16660                break;
16661            }
16662        }
16663        assert!(disp
16664            .dialog_tracking
16665            .as_ref()
16666            .and_then(|tracking| tracking.active_button.as_ref())
16667            .is_some_and(|button| button.item_no == 2));
16668
16669        disp.push_mouse_up(180, 250);
16670        let result = disp.dispatch_dialog(true, 0x185, &mut cpu, &mut bus);
16671        assert!(result.unwrap().is_ok());
16672
16673        for _ in 0..64 {
16674            if disp.dialog_tracking.is_none() {
16675                break;
16676            }
16677            let result = disp.dispatch_dialog(true, 0x185, &mut cpu, &mut bus);
16678            assert!(result.unwrap().is_ok());
16679        }
16680
16681        assert!(disp.dialog_tracking.is_none());
16682        assert_eq!(bus.read_word(TEST_SP + 6), 2);
16683        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
16684    }
16685
16686    fn loaded_resource_handle_for_test(
16687        disp: &TrapDispatcher,
16688        res_type: [u8; 4],
16689        res_id: i16,
16690    ) -> u32 {
16691        disp.loaded_handles
16692            .iter()
16693            .find_map(|(&handle, &(_, loaded_type, loaded_id))| {
16694                if loaded_type == res_type && loaded_id == res_id {
16695                    Some(handle)
16696                } else {
16697                    None
16698                }
16699            })
16700            .unwrap_or_else(|| {
16701                panic!(
16702                    "expected loaded {} resource {}",
16703                    String::from_utf8_lossy(&res_type),
16704                    res_id
16705                )
16706            })
16707    }
16708
16709    fn assert_resource_nonpurgeable(
16710        disp: &TrapDispatcher,
16711        bus: &MacMemoryBus,
16712        res_type: [u8; 4],
16713        res_id: i16,
16714    ) {
16715        let handle = loaded_resource_handle_for_test(disp, res_type, res_id);
16716        assert_ne!(
16717            bus.read_long(handle),
16718            0,
16719            "{} {} should have a non-NIL master pointer",
16720            String::from_utf8_lossy(&res_type),
16721            res_id
16722        );
16723        assert_eq!(
16724            disp.handle_state_bits.get(&handle).copied().unwrap_or(0) & 0x40,
16725            0,
16726            "{} {} should be nonpurgeable",
16727            String::from_utf8_lossy(&res_type),
16728            res_id
16729        );
16730    }
16731
16732    fn assert_resource_purgeable(disp: &TrapDispatcher, res_type: [u8; 4], res_id: i16) {
16733        let handle = loaded_resource_handle_for_test(disp, res_type, res_id);
16734        assert_ne!(
16735            disp.handle_state_bits.get(&handle).copied().unwrap_or(0) & 0x40,
16736            0,
16737            "{} {} should be purgeable",
16738            String::from_utf8_lossy(&res_type),
16739            res_id
16740        );
16741    }
16742
16743    fn ditl_item_handle_field(bus: &MacMemoryBus, ditl_ptr: u32, item_no: i16) -> u32 {
16744        if item_no <= 0 || ditl_ptr == 0 {
16745            return 0;
16746        }
16747
16748        let max_index = bus.read_word(ditl_ptr) as i16;
16749        if item_no > max_index + 1 {
16750            return 0;
16751        }
16752
16753        let mut offset = 2u32;
16754        for current_item in 1..=max_index + 1 {
16755            let handle_addr = ditl_ptr + offset;
16756            offset += 4; // itmHandle / userItem ProcPtr
16757            offset += 8; // item display rectangle
16758            let data_len = bus.read_byte(ditl_ptr + offset + 1) as u32;
16759            offset += 2; // item type + data length
16760            let padded = (data_len + 1) & !1;
16761            if current_item == item_no {
16762                return bus.read_long(handle_addr);
16763            }
16764            offset += padded;
16765        }
16766
16767        0
16768    }
16769
16770    // ---- DialogDispatch ($AA68) ----
16771    // Macintosh Toolbox Essentials (1992), pp. 6-162 to 6-167.
16772
16773    #[test]
16774    fn dialogdispatch_getstdfilterproc_selector_03_writes_non_nil_shim_and_returns_noerr() {
16775        let (mut disp, mut cpu, mut bus) = setup();
16776        let proc_storage_ptr = 0x300000u32;
16777
16778        bus.write_long(proc_storage_ptr, 0x00AB_CDEF);
16779        bus.write_long(TEST_SP, proc_storage_ptr); // VAR theProc
16780        bus.write_word(TEST_SP + 4, 0xBEEF); // OSErr result slot
16781        cpu.write_reg(Register::D0, 0x0203); // selector 3, 4 param bytes
16782
16783        let result = disp.dispatch_dialog(true, 0x268, &mut cpu, &mut bus);
16784        assert!(result.unwrap().is_ok());
16785
16786        let shim = bus.read_long(proc_storage_ptr);
16787        assert_ne!(shim, 0);
16788        assert_eq!(shim, disp.dialog_std_filter_proc);
16789        assert_eq!(bus.read_word(shim), 0x4EF9); // JMP abs.L shimBody
16790        assert_eq!(bus.read_long(shim + 2), shim + 6);
16791        assert_eq!(bus.read_word(shim + 6), 0x7000); // MOVEQ #0, D0
16792        assert_eq!(bus.read_word(shim + 8), 0x426F); // CLR.W 16(SP)
16793        assert_eq!(bus.read_word(shim + 10), 0x0010);
16794        assert_eq!(bus.read_word(shim + 12), 0x4E74); // RTD #12
16795        assert_eq!(bus.read_word(shim + 14), 0x000C);
16796        assert_eq!(bus.read_word(TEST_SP + 4), 0);
16797        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
16798    }
16799
16800    #[test]
16801    fn dialogdispatch_getstdfilterproc_selector_03_reuses_cached_shim() {
16802        let (mut disp, mut cpu, mut bus) = setup();
16803        let slot_a = 0x300000u32;
16804        let slot_b = 0x300004u32;
16805
16806        bus.write_long(TEST_SP, slot_a);
16807        bus.write_word(TEST_SP + 4, 0xBEEF);
16808        cpu.write_reg(Register::D0, 0x0203);
16809        let first = disp.dispatch_dialog(true, 0x268, &mut cpu, &mut bus);
16810        assert!(first.unwrap().is_ok());
16811        let shim_a = bus.read_long(slot_a);
16812        assert_ne!(shim_a, 0);
16813
16814        cpu.write_reg(Register::A7, TEST_SP);
16815        bus.write_long(TEST_SP, slot_b);
16816        bus.write_word(TEST_SP + 4, 0xCAFE);
16817        cpu.write_reg(Register::D0, 0x0203);
16818        let second = disp.dispatch_dialog(true, 0x268, &mut cpu, &mut bus);
16819        assert!(second.unwrap().is_ok());
16820        let shim_b = bus.read_long(slot_b);
16821
16822        assert_eq!(shim_a, shim_b);
16823        assert_eq!(shim_b, disp.dialog_std_filter_proc);
16824        assert_eq!(bus.read_word(shim_b), 0x4EF9);
16825        assert_eq!(bus.read_word(shim_b + 6), 0x7000);
16826        assert_eq!(bus.read_word(TEST_SP + 4), 0);
16827        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
16828    }
16829
16830    #[test]
16831    fn dialogdispatch_setdialogdefaultitem_selector_04_writes_adefitem_and_updates_tracking() {
16832        let (mut disp, mut cpu, mut bus) = setup();
16833        let dialog_ptr = bus.alloc(256);
16834
16835        disp.dialog_tracking = Some(crate::trap::dispatch::DialogTrackingState {
16836            dialog_ptr,
16837            bounds: (0, 0, 0, 0),
16838            title: String::new(),
16839            proc_id: 0,
16840            items: Vec::new(),
16841            default_item: 1,
16842            cancel_item: 2,
16843            edit_text: String::new(),
16844            edit_item: 0,
16845            saved_pixels: Vec::new(),
16846            stack_ptr: TEST_SP,
16847            item_hit_ptr: 0,
16848            rendered_pixels: Vec::new(),
16849            flash_remaining: 0,
16850            flash_delay: 0,
16851            flash_item: 0,
16852            edit_text_modified: false,
16853            draw_proc_queue: VecDeque::new(),
16854            draw_procs_done: true,
16855            rendered_pixels_final: true,
16856            filter_proc: 0,
16857            game_managed: false,
16858            last_filter_event: None,
16859            popup_draws: Vec::new(),
16860            active_popup: None,
16861            active_button: None,
16862            active_user_item: None,
16863        });
16864
16865        bus.write_word(TEST_SP, 9); // newItem
16866        bus.write_long(TEST_SP + 2, dialog_ptr); // theDialog
16867        bus.write_word(TEST_SP + 6, 0xBEEF); // OSErr result slot
16868        cpu.write_reg(Register::D0, 0x0304); // selector 4, 6 param bytes
16869
16870        let result = disp.dispatch_dialog(true, 0x268, &mut cpu, &mut bus);
16871        assert!(result.unwrap().is_ok());
16872
16873        assert_eq!(bus.read_word(dialog_ptr + 168), 9);
16874        assert_eq!(
16875            disp.dialog_tracking.as_ref().map(|t| t.default_item),
16876            Some(9)
16877        );
16878        assert_eq!(bus.read_word(TEST_SP + 6), 0);
16879        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
16880    }
16881
16882    #[test]
16883    fn dialogdispatch_setdialogcancelitem_selector_05_updates_tracking_cancel_item_and_returns_noerr(
16884    ) {
16885        let (mut disp, mut cpu, mut bus) = setup();
16886        let dialog_ptr = bus.alloc(256);
16887
16888        disp.dialog_tracking = Some(crate::trap::dispatch::DialogTrackingState {
16889            dialog_ptr,
16890            bounds: (0, 0, 0, 0),
16891            title: String::new(),
16892            proc_id: 0,
16893            items: Vec::new(),
16894            default_item: 1,
16895            cancel_item: 2,
16896            edit_text: String::new(),
16897            edit_item: 0,
16898            saved_pixels: Vec::new(),
16899            stack_ptr: TEST_SP,
16900            item_hit_ptr: 0,
16901            rendered_pixels: Vec::new(),
16902            flash_remaining: 0,
16903            flash_delay: 0,
16904            flash_item: 0,
16905            edit_text_modified: false,
16906            draw_proc_queue: VecDeque::new(),
16907            draw_procs_done: true,
16908            rendered_pixels_final: true,
16909            filter_proc: 0,
16910            game_managed: false,
16911            last_filter_event: None,
16912            popup_draws: Vec::new(),
16913            active_popup: None,
16914            active_button: None,
16915            active_user_item: None,
16916        });
16917
16918        bus.write_word(TEST_SP, 7); // newItem
16919        bus.write_long(TEST_SP + 2, dialog_ptr); // theDialog
16920        bus.write_word(TEST_SP + 6, 0xBEEF); // OSErr result slot
16921        cpu.write_reg(Register::D0, 0x0305); // selector 5, 6 param bytes
16922
16923        let result = disp.dispatch_dialog(true, 0x268, &mut cpu, &mut bus);
16924        assert!(result.unwrap().is_ok());
16925
16926        assert_eq!(disp.dialog_cancel_items.get(&dialog_ptr), Some(&7));
16927        assert_eq!(
16928            disp.dialog_tracking.as_ref().map(|t| t.cancel_item),
16929            Some(7)
16930        );
16931        assert_eq!(bus.read_word(TEST_SP + 6), 0);
16932        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
16933    }
16934
16935    #[test]
16936    fn dialogdispatch_modal_dialog_first_entry_honors_preserved_default_and_cancel_items() {
16937        let (mut disp, mut cpu, mut bus) = setup();
16938        let dialog_ptr = 0x200000u32;
16939        let item_hit_ptr = 0x300000u32;
16940
16941        disp.front_window = dialog_ptr;
16942        disp.window_bounds = (92, 95, 240, 320);
16943        disp.window_proc_id = 1;
16944        disp.window_title = "AA68".to_string();
16945        disp.dialog_items.insert(
16946            dialog_ptr,
16947            vec![
16948                DialogItem {
16949                    item_type: 4,
16950                    rect: (20, 20, 40, 90),
16951                    text: "OK".to_string(),
16952                    resource_id: 0,
16953                    proc_ptr: 0,
16954                    sel_start: 0,
16955                    sel_end: 0,
16956                },
16957                DialogItem {
16958                    item_type: 4,
16959                    rect: (20, 110, 40, 200),
16960                    text: "Cancel".to_string(),
16961                    resource_id: 0,
16962                    proc_ptr: 0,
16963                    sel_start: 0,
16964                    sel_end: 0,
16965                },
16966            ],
16967        );
16968        bus.write_word(dialog_ptr + 168, 2);
16969        disp.dialog_cancel_items.insert(dialog_ptr, 1);
16970        bus.write_long(TEST_SP, item_hit_ptr);
16971        bus.write_long(TEST_SP + 4, 0);
16972
16973        let result = disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus);
16974        assert!(result.unwrap().is_ok());
16975        assert_eq!(cpu.read_reg(Register::A7), TEST_SP);
16976
16977        let tracking = disp.dialog_tracking.as_ref().unwrap();
16978        assert_eq!(tracking.default_item, 2);
16979        assert_eq!(tracking.cancel_item, 1);
16980    }
16981
16982    #[test]
16983    fn dialogdispatch_setdialogtrackscursor_selector_06_returns_noerr_and_pops_arguments() {
16984        let (mut disp, mut cpu, mut bus) = setup();
16985        let dialog_ptr = bus.alloc(256);
16986
16987        bus.write_word(TEST_SP, 1); // tracks = TRUE
16988        bus.write_long(TEST_SP + 2, dialog_ptr); // theDialog
16989        bus.write_word(TEST_SP + 6, 0xBEEF); // OSErr result slot
16990        cpu.write_reg(Register::D0, 0x0306); // selector 6, 6 param bytes
16991
16992        let result = disp.dispatch_dialog(true, 0x268, &mut cpu, &mut bus);
16993        assert!(result.unwrap().is_ok());
16994
16995        assert_eq!(bus.read_word(TEST_SP + 6), 0);
16996        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
16997    }
16998
16999    // ---- GetNewDialog ($A97C) ----
17000
17001    #[test]
17002    fn get_new_dialog_missing_dlog_sets_reserr_and_returns_nil() {
17003        // MTE 1992 p. 6-114: GetNewDialog returns NIL when the DLOG
17004        // resource can't be read; the failed resource read surfaces through
17005        // Resource Manager ResErr as resNotFound (-192).
17006        let (mut disp, mut cpu, mut bus) = setup();
17007        bus.write_word(0x0A60, 0);
17008
17009        let result = disp.dispatch_dialog(true, 0x17C, &mut cpu, &mut bus);
17010        assert!(result.unwrap().is_ok());
17011        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 10);
17012        let dlg_ptr = bus.read_long(TEST_SP + 10);
17013        assert_eq!(
17014            dlg_ptr, 0,
17015            "GetNewDialog must return NIL when DLOG is missing"
17016        );
17017        assert_eq!(bus.read_word(0x0A60) as i16, -192);
17018        assert!(disp.window_list.is_empty());
17019    }
17020
17021    #[test]
17022    fn get_new_dialog_missing_ditl_sets_reserr_and_returns_nil() {
17023        // MTE 1992 p. 6-114: GetNewDialog also returns NIL when the item-list
17024        // resource named by the DLOG cannot be read.
17025        let (mut disp, mut cpu, mut bus) = setup();
17026        let dlog = build_test_dlog((10, 20, 110, 220), 1909, 0);
17027        disp.install_test_resource(&mut bus, *b"DLOG", 1908, &dlog);
17028        bus.write_word(0x0A60, 0);
17029        bus.write_word(TEST_SP + 8, 1908);
17030
17031        let result = disp.dispatch_dialog(true, 0x17C, &mut cpu, &mut bus);
17032
17033        assert!(result.unwrap().is_ok());
17034        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 10);
17035        assert_eq!(bus.read_long(TEST_SP + 10), 0);
17036        assert_eq!(bus.read_word(0x0A60) as i16, -192);
17037        assert!(disp.window_list.is_empty());
17038        assert!(disp.dialog_items.is_empty());
17039    }
17040
17041    #[test]
17042    fn get_new_dialog_nil_storage_allocates_low_byte_clean_dialog_record() {
17043        let (mut disp, mut cpu, mut bus) = setup();
17044        let screen_base = bus.alloc((640 * 480) as u32);
17045        bus.write_long(0x0824, screen_base);
17046        disp.screen_mode = (screen_base, 640, 640, 480, 8);
17047
17048        let dlog = build_test_dlog((40, 50, 120, 240), 1911, 0);
17049        let ditl = build_test_ditl_item(4, (50, 80, 70, 140), b"OK");
17050        disp.install_test_resource(&mut bus, *b"DLOG", 1910, &dlog);
17051        disp.install_test_resource(&mut bus, *b"DITL", 1911, &ditl);
17052        let skew = bus.alloc(5);
17053        assert_ne!(
17054            (skew + MacMemoryBus::allocation_bucket_size(5)) & 0xFF,
17055            0,
17056            "test precondition should leave the heap skewed before GetNewDialog"
17057        );
17058
17059        bus.write_long(TEST_SP, 0xFFFF_FFFF); // behind
17060        bus.write_long(TEST_SP + 4, 0); // dStorage = NIL
17061        bus.write_word(TEST_SP + 8, 1910);
17062        bus.write_long(TEST_SP + 10, 0xDEAD_BEEF);
17063
17064        disp.dispatch_dialog(true, 0x17C, &mut cpu, &mut bus)
17065            .unwrap()
17066            .unwrap();
17067
17068        let dialog_ptr = bus.read_long(TEST_SP + 10);
17069        assert_ne!(dialog_ptr, 0);
17070        assert_eq!(
17071            dialog_ptr & 0xFF,
17072            0,
17073            "manager-owned DialogRecord pointers should keep the low byte clear"
17074        );
17075        assert_eq!(
17076            bus.get_alloc_size(dialog_ptr),
17077            Some(170),
17078            "DialogRecord allocation should keep its logical size"
17079        );
17080    }
17081
17082    #[test]
17083    fn get_new_dialog_copies_ditl_not_aliases_resource() {
17084        // IM:I I-403 and MTE 1992 p. 6-114: GetNewDialog reads the DITL
17085        // resource, makes a copy, and uses that copy so several dialogs can
17086        // share identical resource-backed items without aliasing the resource.
17087        let (mut disp, mut cpu, mut bus) = setup();
17088        let screen_base = bus.alloc((640 * 480) as u32);
17089        bus.write_long(0x0824, screen_base);
17090        disp.screen_mode = (screen_base, 640, 640, 480, 8);
17091
17092        let dlog = build_test_dlog((40, 50, 110, 230), 2201, 0);
17093        let ditl = build_test_ditl_item(8, (10, 12, 28, 120), b"Shared");
17094        disp.install_test_resource(&mut bus, *b"DLOG", 2200, &dlog);
17095        let original_ditl_ptr = disp.install_test_resource(&mut bus, *b"DITL", 2201, &ditl);
17096
17097        cpu.write_reg(Register::A7, TEST_SP);
17098        bus.write_long(TEST_SP, 0xFFFF_FFFF); // behind
17099        bus.write_long(TEST_SP + 4, 0); // dStorage
17100        bus.write_word(TEST_SP + 8, 2200); // dialogID
17101        bus.write_long(TEST_SP + 10, 0xDEAD_BEEF);
17102        disp.dispatch_dialog(true, 0x17C, &mut cpu, &mut bus)
17103            .unwrap()
17104            .unwrap();
17105        let first_dialog = bus.read_long(TEST_SP + 10);
17106        let first_ditl_ptr = bus.read_long(bus.read_long(first_dialog + 156));
17107
17108        assert_ne!(first_dialog, 0);
17109        assert_ne!(first_ditl_ptr, 0);
17110        assert_ne!(first_ditl_ptr, original_ditl_ptr);
17111        assert_eq!(ditl_item_handle_field(&bus, original_ditl_ptr, 1), 0);
17112        assert_ne!(ditl_item_handle_field(&bus, first_ditl_ptr, 1), 0);
17113
17114        bus.write_long(first_ditl_ptr + 2, 0xAABB_CCDD);
17115
17116        cpu.write_reg(Register::A7, TEST_SP);
17117        bus.write_long(TEST_SP, 0xFFFF_FFFF); // behind
17118        bus.write_long(TEST_SP + 4, 0); // dStorage
17119        bus.write_word(TEST_SP + 8, 2200); // dialogID
17120        bus.write_long(TEST_SP + 10, 0xDEAD_BEEF);
17121        disp.dispatch_dialog(true, 0x17C, &mut cpu, &mut bus)
17122            .unwrap()
17123            .unwrap();
17124        let second_dialog = bus.read_long(TEST_SP + 10);
17125        let second_ditl_ptr = bus.read_long(bus.read_long(second_dialog + 156));
17126        let second_text_handle = ditl_item_handle_field(&bus, second_ditl_ptr, 1);
17127
17128        assert_ne!(second_dialog, 0);
17129        assert_ne!(second_ditl_ptr, original_ditl_ptr);
17130        assert_ne!(second_ditl_ptr, first_ditl_ptr);
17131        assert_ne!(second_text_handle, 0);
17132        assert_ne!(second_text_handle, 0xAABB_CCDD);
17133        assert_eq!(ditl_item_handle_field(&bus, original_ditl_ptr, 1), 0);
17134    }
17135
17136    #[test]
17137    fn get_new_dialog_rewrites_reserved_item_handles() {
17138        // IM:I I-405: an item list in memory contains item handles for text,
17139        // controls, icons, and pictures. GetNewDialog must rewrite the copied
17140        // DITL's reserved handle fields, not the resource's fields.
17141        let (mut disp, mut cpu, mut bus) = setup();
17142        let screen_base = bus.alloc((640 * 480) as u32);
17143        bus.write_long(0x0824, screen_base);
17144        disp.screen_mode = (screen_base, 640, 640, 480, 8);
17145
17146        let cntl_id = 2301i16.to_be_bytes();
17147        let icon_id = 2302i16.to_be_bytes();
17148        let pict_id = 2303i16.to_be_bytes();
17149        let dlog = build_test_dlog((40, 50, 130, 260), 2300, 0);
17150        let ditl = build_test_ditl_items(&[
17151            (8, (10, 12, 24, 140), b"Static".as_slice()),
17152            (16, (30, 12, 44, 140), b"Edit".as_slice()),
17153            (7, (50, 12, 70, 120), cntl_id.as_slice()),
17154            (32, (10, 150, 42, 182), icon_id.as_slice()),
17155            (64, (48, 150, 80, 220), pict_id.as_slice()),
17156        ]);
17157
17158        let mut cntl = Vec::new();
17159        for word in [0i16, 0, 20, 110, 1, -1, 3, 0, 0] {
17160            cntl.extend_from_slice(&(word as u16).to_be_bytes());
17161        }
17162        cntl.extend_from_slice(&0x1234_5678u32.to_be_bytes());
17163        cntl.push(4);
17164        cntl.extend_from_slice(b"Pick");
17165        disp.install_test_resource(&mut bus, *b"DLOG", 2299, &dlog);
17166        disp.install_test_resource(&mut bus, *b"CNTL", 2301, &cntl);
17167        let icon_ptr = disp.install_test_resource(&mut bus, *b"ICON", 2302, &[0xAA; 128]);
17168        let pict_ptr = disp.install_test_resource(&mut bus, *b"PICT", 2303, &[0x11; 32]);
17169        let original_ditl_ptr = disp.install_test_resource(&mut bus, *b"DITL", 2300, &ditl);
17170
17171        bus.write_long(TEST_SP, 0xFFFF_FFFF); // behind
17172        bus.write_long(TEST_SP + 4, 0); // dStorage
17173        bus.write_word(TEST_SP + 8, 2299); // dialogID
17174        disp.dispatch_dialog(true, 0x17C, &mut cpu, &mut bus)
17175            .unwrap()
17176            .unwrap();
17177
17178        let dialog_ptr = bus.read_long(TEST_SP + 10);
17179        let copied_ditl_ptr = bus.read_long(bus.read_long(dialog_ptr + 156));
17180        assert_ne!(dialog_ptr, 0);
17181        assert_ne!(copied_ditl_ptr, original_ditl_ptr);
17182
17183        for item_no in 1..=5 {
17184            assert_eq!(
17185                ditl_item_handle_field(&bus, original_ditl_ptr, item_no),
17186                0,
17187                "resource DITL item {} handle field must stay reserved",
17188                item_no
17189            );
17190        }
17191
17192        let static_handle = ditl_item_handle_field(&bus, copied_ditl_ptr, 1);
17193        let edit_handle = ditl_item_handle_field(&bus, copied_ditl_ptr, 2);
17194        let control_handle = ditl_item_handle_field(&bus, copied_ditl_ptr, 3);
17195        let icon_handle = ditl_item_handle_field(&bus, copied_ditl_ptr, 4);
17196        let pict_handle = ditl_item_handle_field(&bus, copied_ditl_ptr, 5);
17197
17198        assert_eq!(
17199            bus.read_bytes(bus.read_long(static_handle), 6),
17200            b"Static".to_vec()
17201        );
17202        assert_eq!(
17203            bus.read_bytes(bus.read_long(edit_handle), 4),
17204            b"Edit".to_vec()
17205        );
17206        assert_eq!(bus.read_long(bus.read_long(control_handle) + 4), dialog_ptr);
17207        assert_eq!(
17208            disp.dialog_control_handles.get(&control_handle),
17209            Some(&(dialog_ptr, 3))
17210        );
17211        assert_eq!(bus.read_long(icon_handle), icon_ptr);
17212        assert_eq!(bus.read_long(pict_handle), pict_ptr);
17213    }
17214
17215    #[test]
17216    fn get_new_dialog_centers_standard_dlog_with_position_constant() {
17217        let (mut disp, mut cpu, mut bus) = setup();
17218        let screen_base = bus.alloc((800 * 600) as u32);
17219        bus.write_long(0x0824, screen_base);
17220        disp.screen_mode = (screen_base, 800, 800, 600, 8);
17221
17222        let dlog = build_test_dlog((10, 20, 110, 220), 1503, 0x280A);
17223        let ditl = build_test_ditl_item(4, (20, 20, 40, 80), b"OK");
17224        disp.install_test_resource(&mut bus, *b"DLOG", 1502, &dlog);
17225        disp.install_test_resource(&mut bus, *b"DITL", 1503, &ditl);
17226        bus.write_long(TEST_SP, 0); // behind
17227        bus.write_long(TEST_SP + 4, 0); // dStorage
17228        bus.write_word(TEST_SP + 8, 1502); // dialogID
17229
17230        let result = disp.dispatch_dialog(true, 0x17C, &mut cpu, &mut bus);
17231        assert!(result.unwrap().is_ok());
17232
17233        let dlg_ptr = bus.read_long(TEST_SP + 10);
17234        assert_ne!(dlg_ptr, 0);
17235        assert_eq!(disp.window_bounds, (250, 300, 350, 500));
17236    }
17237
17238    #[test]
17239    fn get_new_dialog_uses_system7_alert_position_near_top() {
17240        // MTE 1992 p. 4-126: alert position leaves about one-fifth of
17241        // the unused vertical screen space above the new window. EVO's
17242        // startup registration DLOG has these dimensions; a one-third
17243        // placement draws it visibly too low compared with BasiliskII.
17244        let (mut disp, mut cpu, mut bus) = setup();
17245        let screen_base = bus.alloc((800 * 600) as u32);
17246        bus.write_long(0x0824, screen_base);
17247        disp.screen_mode = (screen_base, 800, 800, 600, 8);
17248
17249        let dlog = build_test_dlog((40, 40, 310, 530), 1513, 0x300A);
17250        let ditl = build_test_ditl_item(4, (241, 291, 261, 381), b"Not Yet");
17251        disp.install_test_resource(&mut bus, *b"DLOG", 1512, &dlog);
17252        disp.install_test_resource(&mut bus, *b"DITL", 1513, &ditl);
17253        bus.write_long(TEST_SP, 0); // behind
17254        bus.write_long(TEST_SP + 4, 0); // dStorage
17255        bus.write_word(TEST_SP + 8, 1512); // dialogID
17256
17257        let result = disp.dispatch_dialog(true, 0x17C, &mut cpu, &mut bus);
17258        assert!(result.unwrap().is_ok());
17259
17260        let dlg_ptr = bus.read_long(TEST_SP + 10);
17261        assert_ne!(dlg_ptr, 0);
17262        assert_eq!(disp.window_bounds, (66, 155, 336, 645));
17263    }
17264
17265    #[test]
17266    fn get_new_dialog_draws_visible_shell_with_unresolved_user_item_proc() {
17267        // Visible dialogs appear immediately on a real Mac. userItem contents
17268        // are application-owned and may be installed later with SetDItem, but
17269        // the dialog shell and standard items must not be suppressed.
17270        // Inside Macintosh Volume I, I-405, I-412, I-421.
17271        let (mut disp, mut cpu, mut bus) = setup();
17272        let screen_base = bus.alloc((800 * 600) as u32);
17273        bus.write_bytes(screen_base, &vec![0x77; 800 * 600]);
17274        bus.write_long(0x0824, screen_base);
17275        disp.screen_mode = (screen_base, 800, 800, 600, 8);
17276
17277        let mut dlog = build_test_dlog((100, 100, 180, 260), 1701, 0);
17278        dlog[10] = 1; // visible
17279        let ditl = build_test_ditl_items(&[
17280            (0x80, (8, 8, 30, 80), b"".as_slice()),
17281            (4, (44, 20, 64, 90), b"OK".as_slice()),
17282        ]);
17283        disp.install_test_resource(&mut bus, *b"DLOG", 1700, &dlog);
17284        disp.install_test_resource(&mut bus, *b"DITL", 1701, &ditl);
17285        bus.write_long(TEST_SP, 0xFFFF_FFFF); // behind
17286        bus.write_long(TEST_SP + 4, 0); // dStorage
17287        bus.write_word(TEST_SP + 8, 1700); // dialogID
17288
17289        let result = disp.dispatch_dialog(true, 0x17C, &mut cpu, &mut bus);
17290        assert!(result.unwrap().is_ok());
17291
17292        let dlg_ptr = bus.read_long(TEST_SP + 10);
17293        assert_ne!(dlg_ptr, 0);
17294        assert_eq!(bus.read_byte(dlg_ptr + 110), 0xFF);
17295        assert!(disp.dialog_items.contains_key(&dlg_ptr));
17296        assert!(
17297            disp.dialog_saved_pixels.contains_key(&dlg_ptr),
17298            "visible dialogs save the covered pixels before drawing"
17299        );
17300        assert!(
17301            disp.dialog_visible_snapshots.contains_key(&dlg_ptr),
17302            "visible dialogs retain their clean initial shell for first ModalDialog entry"
17303        );
17304        assert!(!disp.dialog_initial_draw_deferred.contains(&dlg_ptr));
17305        assert!(
17306            disp.event_queue
17307                .iter()
17308                .any(|event| event.what == 6 && event.message == dlg_ptr),
17309            "visible dialog should still get an update event"
17310        );
17311
17312        let probe_addr = screen_base + 120 * 800 + 120;
17313        assert_ne!(
17314            bus.read_byte(probe_addr),
17315            0x77,
17316            "visible GetNewDialog should draw the standard dialog background immediately"
17317        );
17318    }
17319
17320    #[test]
17321    fn hidden_dialog_quickdraw_shape_is_clipped_by_empty_vis_region() {
17322        // A real invisible window has an empty visRgn. Drawing through its
17323        // port must be clipped away instead of touching the screen-backed
17324        // PixMap. This covers hidden dialogs whose userItem setup draws into
17325        // the current dialog port before the dialog is ever shown.
17326        let (mut disp, mut cpu, mut bus) = setup();
17327        let screen_base = bus.alloc((800 * 600) as u32);
17328        bus.write_bytes(screen_base, &vec![0x77; 800 * 600]);
17329        bus.write_long(0x0824, screen_base);
17330        disp.screen_mode = (screen_base, 800, 800, 600, 8);
17331
17332        let dlog = build_test_dlog((100, 100, 180, 260), 1705, 0);
17333        let ditl = build_test_ditl_item(8, (8, 8, 24, 90), b"Hidden");
17334        disp.install_test_resource(&mut bus, *b"DLOG", 1704, &dlog);
17335        disp.install_test_resource(&mut bus, *b"DITL", 1705, &ditl);
17336        bus.write_long(TEST_SP, 0xFFFF_FFFF); // behind
17337        bus.write_long(TEST_SP + 4, 0); // dStorage
17338        bus.write_word(TEST_SP + 8, 1704); // dialogID
17339
17340        let result = disp.dispatch_dialog(true, 0x17C, &mut cpu, &mut bus);
17341        assert!(result.unwrap().is_ok());
17342        let dlg_ptr = bus.read_long(TEST_SP + 10);
17343        assert_ne!(dlg_ptr, 0);
17344        assert_eq!(bus.read_byte(dlg_ptr + 110), 0);
17345        assert_eq!(
17346            TrapDispatcher::region_handle_rect(&bus, bus.read_long(dlg_ptr + 24)),
17347            None
17348        );
17349
17350        let rect_ptr = bus.alloc(8);
17351        bus.write_word(rect_ptr, 20);
17352        bus.write_word(rect_ptr + 2, 20);
17353        bus.write_word(rect_ptr + 4, 30);
17354        bus.write_word(rect_ptr + 6, 30);
17355        bus.write_long(TEST_SP, rect_ptr);
17356
17357        let result = disp.dispatch_quickdraw(true, 0x0A2, &mut cpu, &mut bus);
17358        assert!(result.unwrap().is_ok());
17359
17360        let probe_addr = screen_base + 125 * 800 + 125;
17361        assert_eq!(
17362            bus.read_byte(probe_addr),
17363            0x77,
17364            "QuickDraw drawing through a hidden dialog port must be clipped"
17365        );
17366        assert!(
17367            !disp.dialog_saved_pixels.contains_key(&dlg_ptr),
17368            "no visible draw means no saved-under snapshot"
17369        );
17370    }
17371
17372    #[test]
17373    fn dispos_dialog_skips_saved_pixels_for_never_drawn_deferred_dialog() {
17374        // Defensive coverage for the deferred-state guard: if a dialog is
17375        // marked never drawn, DisposDialog must not restore stale saved-under
17376        // pixels. Real visible dialogs normally draw at creation.
17377        // Inside Macintosh Volume I, I-425.
17378        let (mut disp, mut cpu, mut bus) = setup();
17379        let screen_base = bus.alloc((800 * 600) as u32);
17380        bus.write_bytes(screen_base, &vec![0x77; 800 * 600]);
17381        bus.write_long(0x0824, screen_base);
17382        disp.screen_mode = (screen_base, 800, 800, 600, 8);
17383
17384        let mut dlog = build_test_dlog((100, 100, 180, 260), 1703, 0);
17385        dlog[10] = 1; // visible
17386        let ditl = build_test_ditl_items(&[
17387            (0x80, (8, 8, 30, 80), b"".as_slice()),
17388            (4, (44, 20, 64, 90), b"OK".as_slice()),
17389        ]);
17390        disp.install_test_resource(&mut bus, *b"DLOG", 1702, &dlog);
17391        disp.install_test_resource(&mut bus, *b"DITL", 1703, &ditl);
17392        bus.write_long(TEST_SP, 0xFFFF_FFFF); // behind
17393        bus.write_long(TEST_SP + 4, 0); // dStorage
17394        bus.write_word(TEST_SP + 8, 1702); // dialogID
17395
17396        let result = disp.dispatch_dialog(true, 0x17C, &mut cpu, &mut bus);
17397        assert!(result.unwrap().is_ok());
17398        let dlg_ptr = bus.read_long(TEST_SP + 10);
17399        disp.dialog_initial_draw_deferred.insert(dlg_ptr);
17400
17401        // Replace the saved snapshot with a distinct dirty pattern. If
17402        // DisposDialog restores it, the probe byte will change to 0x33.
17403        disp.dialog_saved_pixels
17404            .insert(dlg_ptr, vec![0x33; 90 * 170]);
17405        let probe_addr = screen_base + 120 * 800 + 120;
17406        bus.write_byte(probe_addr, 0x77);
17407
17408        bus.write_long(TEST_SP, dlg_ptr);
17409        let result = disp.dispatch_dialog(true, 0x183, &mut cpu, &mut bus);
17410        assert!(result.unwrap().is_ok());
17411
17412        assert_eq!(
17413            bus.read_byte(probe_addr),
17414            0x77,
17415            "never-drawn deferred dialog must not restore saved background"
17416        );
17417        assert!(!disp.dialog_initial_draw_deferred.contains(&dlg_ptr));
17418        assert!(!disp.dialog_saved_pixels.contains_key(&dlg_ptr));
17419    }
17420
17421    // save_dialog_pixels / restore_dialog_pixels must guard against off-screen y
17422    // (negative after the dBoxProc structure margin, or beyond screen_h).
17423    // Without the guard, (y as u32) sign-extends a negative i16 and
17424    // multiply-with-overflow panics in debug.
17425    #[test]
17426    fn save_dialog_pixels_handles_top_below_dbox_margin_without_overflow() {
17427        let (disp, _cpu, mut bus) = setup();
17428        let screen_base = bus.alloc((800 * 600) as u32);
17429        for i in 0..800u32 * 600 {
17430            bus.write_byte(screen_base + i, 0x42);
17431        }
17432        let mut d = disp;
17433        bus.write_long(0x0824, screen_base);
17434        d.screen_mode = (screen_base, 800, 800, 600, 8);
17435
17436        // Bounds with top=2 → save_top = -6 (negative).
17437        let saved = d.save_dialog_pixels(&bus, (2, 2, 50, 50));
17438        // Row width = (58 - (-6)) = 64; row count = 64.
17439        let row_width = 64usize;
17440        let row_count = 64usize;
17441        assert_eq!(saved.len(), row_width * row_count);
17442
17443        // First 6 rows are off-screen (y = -6..-1) → zero-padded.
17444        for row in 0..6 {
17445            for col in 0..row_width {
17446                assert_eq!(
17447                    saved[row * row_width + col],
17448                    0x00,
17449                    "off-screen row {} col {} must be zero-padded",
17450                    row,
17451                    col
17452                );
17453            }
17454        }
17455        // y=0 row, save_left=-6 so cols 0..6 are off-screen → 0,
17456        // cols 6..64 read from the 0x42-filled framebuffer.
17457        let row0 = &saved[6 * row_width..7 * row_width];
17458        for (col, &px) in row0.iter().enumerate().take(6) {
17459            assert_eq!(
17460                px, 0x00,
17461                "off-screen column {} within on-screen row must be zero",
17462                col
17463            );
17464        }
17465        for (col, &px) in row0.iter().enumerate().skip(6) {
17466            assert_eq!(
17467                px, 0x42,
17468                "on-screen pixel at col {} must round-trip via framebuffer",
17469                col
17470            );
17471        }
17472    }
17473
17474    // save_rect_pixels / restore_rect_pixels guard the same off-screen y
17475    // overflow hazard as save_dialog_pixels.
17476    #[test]
17477    fn save_rect_pixels_handles_negative_top_without_overflow() {
17478        let (disp, _cpu, mut bus) = setup();
17479        let screen_base = bus.alloc((800 * 600) as u32);
17480        for i in 0..800u32 * 600 {
17481            bus.write_byte(screen_base + i, 0x55);
17482        }
17483        let mut d = disp;
17484        bus.write_long(0x0824, screen_base);
17485        d.screen_mode = (screen_base, 800, 800, 600, 8);
17486
17487        // Rect spanning above the screen top.
17488        let saved = d.save_rect_pixels(&bus, (-3, 0, 5, 10));
17489        let row_width = 10usize;
17490        let row_count = (5 - (-3)) as usize;
17491        assert_eq!(saved.len(), row_width * row_count);
17492        // First 3 rows off-screen (y = -3..0) → zero-padded.
17493        for row in 0..3 {
17494            for col in 0..row_width {
17495                assert_eq!(saved[row * row_width + col], 0x00);
17496            }
17497        }
17498        // Next 5 rows on-screen → 0x55.
17499        for row in 3..8 {
17500            for col in 0..row_width {
17501                assert_eq!(saved[row * row_width + col], 0x55);
17502            }
17503        }
17504    }
17505
17506    #[test]
17507    fn save_dialog_pixels_handles_top_above_screen_height_without_overflow() {
17508        let (disp, _cpu, mut bus) = setup();
17509        let screen_base = bus.alloc((800 * 600) as u32);
17510        let mut d = disp;
17511        bus.write_long(0x0824, screen_base);
17512        d.screen_mode = (screen_base, 800, 800, 600, 8);
17513
17514        // Bounds way below screen → save_top = 595+ and save_bottom
17515        // = 700+, many rows off-screen on the bottom side.
17516        let saved = d.save_dialog_pixels(&bus, (600, 0, 700, 50));
17517        let row_count = (700 + TrapDispatcher::DBOX_FRAME_MARGIN
17518            - (600 - TrapDispatcher::DBOX_FRAME_MARGIN)) as usize;
17519        let row_width = (50 + TrapDispatcher::DBOX_FRAME_MARGIN
17520            - (0 - TrapDispatcher::DBOX_FRAME_MARGIN)) as usize;
17521        assert_eq!(saved.len(), row_count * row_width);
17522    }
17523
17524    fn install_new_dialog_test_screen(
17525        disp: &mut TrapDispatcher,
17526        bus: &mut MacMemoryBus,
17527        fill: u8,
17528    ) -> u32 {
17529        let screen_base = bus.alloc((640 * 480) as u32);
17530        bus.write_bytes(screen_base, &vec![fill; 640 * 480]);
17531        bus.write_long(0x0824, screen_base);
17532        disp.screen_mode = (screen_base, 640, 640, 480, 8);
17533        screen_base
17534    }
17535
17536    fn call_new_dialog_for_test(
17537        disp: &mut TrapDispatcher,
17538        cpu: &mut MockCpu,
17539        bus: &mut MacMemoryBus,
17540        storage_ptr: u32,
17541        bounds: (i16, i16, i16, i16),
17542        visible: bool,
17543        proc_id: i16,
17544        behind: u32,
17545        items_handle: u32,
17546    ) -> (u32, u32) {
17547        let title_ptr = bus.alloc(32);
17548        bus.write_pstring(title_ptr, b"Lifecycle");
17549        let bounds_ptr = bus.alloc(8);
17550        bus.write_word(bounds_ptr, bounds.0 as u16);
17551        bus.write_word(bounds_ptr + 2, bounds.1 as u16);
17552        bus.write_word(bounds_ptr + 4, bounds.2 as u16);
17553        bus.write_word(bounds_ptr + 6, bounds.3 as u16);
17554
17555        let sp = TEST_SP - 30;
17556        cpu.write_reg(Register::A7, sp);
17557        for offset in 0..34u32 {
17558            bus.write_byte(sp + offset, 0);
17559        }
17560        bus.write_long(sp, items_handle);
17561        bus.write_long(sp + 4, 0x1234_5678); // refCon
17562        bus.write_byte(sp + 8, 0xFF); // goAwayFlag
17563        bus.write_long(sp + 10, behind);
17564        bus.write_word(sp + 14, proc_id as u16);
17565        bus.write_byte(sp + 16, if visible { 0xFF } else { 0 });
17566        bus.write_long(sp + 18, title_ptr);
17567        bus.write_long(sp + 22, bounds_ptr);
17568        bus.write_long(sp + 26, storage_ptr);
17569        bus.write_long(sp + 30, 0xDEAD_BEEF);
17570
17571        disp.dispatch_dialog(true, 0x17D, cpu, bus)
17572            .unwrap()
17573            .unwrap();
17574        (sp, bus.read_long(sp + 30))
17575    }
17576
17577    fn install_ditl_handle_for_test(bus: &mut MacMemoryBus, ditl: &[u8]) -> (u32, u32) {
17578        let items_ptr = bus.alloc(ditl.len() as u32);
17579        bus.write_bytes(items_ptr, ditl);
17580        let items_handle = bus.alloc(4);
17581        bus.write_long(items_handle, items_ptr);
17582        (items_handle, items_ptr)
17583    }
17584
17585    #[test]
17586    fn new_dialog_with_storage_uses_caller_record() {
17587        // IM:I I-412: dStorage supplies the DialogRecord storage; NIL asks
17588        // the Dialog Manager to allocate the record instead.
17589        let (mut disp, mut cpu, mut bus) = setup();
17590        install_new_dialog_test_screen(&mut disp, &mut bus, 0x77);
17591        let storage_ptr = bus.alloc(170);
17592
17593        let (sp, dialog_ptr) = call_new_dialog_for_test(
17594            &mut disp,
17595            &mut cpu,
17596            &mut bus,
17597            storage_ptr,
17598            (80, 90, 150, 260),
17599            false,
17600            4,
17601            0xFFFF_FFFF,
17602            0,
17603        );
17604
17605        assert_eq!(dialog_ptr, storage_ptr);
17606        assert_eq!(bus.read_long(sp + 30), storage_ptr);
17607        assert_eq!(cpu.read_reg(Register::A7), sp + 30);
17608        assert_eq!(disp.window_list, vec![storage_ptr]);
17609    }
17610
17611    #[test]
17612    fn new_dialog_without_storage_allocates_record() {
17613        // IM:I I-412 and MTE 1992 p. 6-118: passing NIL for dStorage
17614        // allocates the DialogRecord in the heap.
17615        let (mut disp, mut cpu, mut bus) = setup();
17616        install_new_dialog_test_screen(&mut disp, &mut bus, 0x77);
17617
17618        let (_sp, dialog_ptr) = call_new_dialog_for_test(
17619            &mut disp,
17620            &mut cpu,
17621            &mut bus,
17622            0,
17623            (80, 90, 150, 260),
17624            false,
17625            2,
17626            0xFFFF_FFFF,
17627            0,
17628        );
17629
17630        assert_ne!(dialog_ptr, 0);
17631        assert_eq!(bus.get_alloc_size(dialog_ptr), Some(170));
17632        assert_eq!(disp.window_list, vec![dialog_ptr]);
17633    }
17634
17635    #[test]
17636    fn new_dialog_sets_window_kind_dialog_kind() {
17637        // IM:I I-407 and I-412: a DialogPtr is a WindowPtr whose
17638        // WindowRecord.windowKind is dialogKind, independent of the WDEF
17639        // procID used to draw modeless or modal chrome.
17640        let (mut disp, mut cpu, mut bus) = setup();
17641        install_new_dialog_test_screen(&mut disp, &mut bus, 0x77);
17642
17643        let (_sp, dialog_ptr) = call_new_dialog_for_test(
17644            &mut disp,
17645            &mut cpu,
17646            &mut bus,
17647            0,
17648            (80, 90, 150, 260),
17649            false,
17650            4,
17651            0xFFFF_FFFF,
17652            0,
17653        );
17654
17655        assert_eq!(bus.read_word(dialog_ptr + 108) as i16, 2);
17656        assert_eq!(disp.window_proc_ids.get(&dialog_ptr), Some(&4));
17657    }
17658
17659    #[test]
17660    fn new_dialog_sets_dialog_port_font_to_dialog_font() {
17661        // IM:I I-412 and MTE 1992 p. 6-104: SetDAFont/SetDialogFont set
17662        // DlgFont for subsequently created dialog and alert grafPorts.
17663        let (mut disp, mut cpu, mut bus) = setup();
17664        install_new_dialog_test_screen(&mut disp, &mut bus, 0x77);
17665        bus.write_word(crate::memory::globals::addr::DLG_FONT, 3); // Geneva
17666
17667        let (_sp, dialog_ptr) = call_new_dialog_for_test(
17668            &mut disp,
17669            &mut cpu,
17670            &mut bus,
17671            0,
17672            (80, 90, 150, 260),
17673            false,
17674            2,
17675            0xFFFF_FFFF,
17676            0,
17677        );
17678
17679        assert_eq!(bus.read_word(dialog_ptr + 68) as i16, 3);
17680        assert_eq!(disp.tx_font, 3);
17681        assert_eq!(
17682            disp.port_draw_states
17683                .get(&dialog_ptr)
17684                .map(|state| state.tx_font),
17685            Some(3)
17686        );
17687    }
17688
17689    #[test]
17690    fn new_dialog_visible_queues_or_draws_expected_update_path() {
17691        // IM:I I-412 and I-287: visible NewDialog draws the dialog window and
17692        // generates an update event for the contents.
17693        let (mut disp, mut cpu, mut bus) = setup();
17694        let screen_base = install_new_dialog_test_screen(&mut disp, &mut bus, 0x77);
17695
17696        let (_sp, dialog_ptr) = call_new_dialog_for_test(
17697            &mut disp,
17698            &mut cpu,
17699            &mut bus,
17700            0,
17701            (80, 90, 150, 260),
17702            true,
17703            2,
17704            0xFFFF_FFFF,
17705            0,
17706        );
17707
17708        assert_eq!(bus.read_byte(dialog_ptr + 110), 0xFF);
17709        assert!(disp
17710            .event_queue
17711            .iter()
17712            .any(|event| event.what == 6 && event.message == dialog_ptr));
17713        assert!(
17714            TrapDispatcher::region_handle_rect(&bus, bus.read_long(dialog_ptr + 122)).is_some()
17715        );
17716        assert_ne!(bus.read_byte(screen_base + 100 * 640 + 110), 0x77);
17717    }
17718
17719    #[test]
17720    fn new_dialog_invisible_defers_screen_pixels() {
17721        // IM:I I-412: if visible is FALSE, the window is initially invisible
17722        // and may later be shown with ShowWindow.
17723        let (mut disp, mut cpu, mut bus) = setup();
17724        let screen_base = install_new_dialog_test_screen(&mut disp, &mut bus, 0x77);
17725
17726        let (_sp, dialog_ptr) = call_new_dialog_for_test(
17727            &mut disp,
17728            &mut cpu,
17729            &mut bus,
17730            0,
17731            (80, 90, 150, 260),
17732            false,
17733            2,
17734            0xFFFF_FFFF,
17735            0,
17736        );
17737
17738        assert_eq!(bus.read_byte(dialog_ptr + 110), 0);
17739        assert!(!disp
17740            .event_queue
17741            .iter()
17742            .any(|event| event.what == 6 && event.message == dialog_ptr));
17743        assert_eq!(
17744            TrapDispatcher::region_handle_rect(&bus, bus.read_long(dialog_ptr + 122)),
17745            None
17746        );
17747        assert_eq!(bus.read_byte(screen_base + 100 * 640 + 110), 0x77);
17748    }
17749
17750    #[test]
17751    fn new_dialog_honors_behind_nil_and_inserts_at_back() {
17752        // Inside Macintosh Volume I, I-412: NewDialog returns a DialogPtr
17753        // and honors the `behind` parameter for plane order.
17754        let (mut disp, mut cpu, mut bus) = setup();
17755        // Seed an existing window that will stay in front.
17756        let existing = 0x200040u32;
17757        disp.window_list = vec![existing];
17758        disp.front_window = existing;
17759        bus.write_byte(existing + 110u32, 0xFF); // visible
17760
17761        // init_cgraf_window reads screen_mode for bounds math; a
17762        // zero-initialized mode multiplies-with-overflow later.
17763        let screen_base = bus.alloc((800 * 600) as u32);
17764        bus.write_long(0x0824, screen_base);
17765        disp.screen_mode = (screen_base, 800, 800, 600, 8);
17766
17767        // Bounds at the origin are now safe via the off-screen y overflow guard
17768        // in save_dialog_pixels. Keep a non-origin placement for realism.
17769        let bounds_rect_ptr = 0x301200u32;
17770        bus.write_word(bounds_rect_ptr, 100);
17771        bus.write_word(bounds_rect_ptr + 2, 100);
17772        bus.write_word(bounds_rect_ptr + 4, 300);
17773        bus.write_word(bounds_rect_ptr + 6, 400);
17774
17775        let sp = TEST_SP - 30;
17776        cpu.write_reg(Register::A7, sp);
17777        for i in 0..34u32 {
17778            bus.write_byte(sp + i, 0);
17779        }
17780        bus.write_long(sp + 22, bounds_rect_ptr);
17781        bus.write_word(sp + 16, 1); // visible
17782        bus.write_word(sp + 14, 1); // dBoxProc WDEF
17783        bus.write_long(sp + 10, 0); // behind = NIL (backmost)
17784
17785        let pre_a7 = cpu.read_reg(Register::A7);
17786        let result = disp.dispatch_dialog(true, 0x17D, &mut cpu, &mut bus);
17787        assert!(result.unwrap().is_ok());
17788        assert_eq!(
17789            cpu.read_reg(Register::A7),
17790            pre_a7 + 30,
17791            "NewDialog must pop 30 bytes of parameters and leave result at new SP+0"
17792        );
17793        let dlg_ptr = bus.read_long(sp + 30);
17794        assert_ne!(dlg_ptr, 0);
17795        assert_eq!(
17796            bus.read_word(dlg_ptr + 108),
17797            2,
17798            "DialogRecord.window.windowKind must be dialogKind"
17799        );
17800        assert_eq!(
17801            disp.window_proc_ids.get(&dlg_ptr),
17802            Some(&1),
17803            "dialog WDEF procID must be tracked separately from windowKind"
17804        );
17805        assert_eq!(
17806            disp.window_list,
17807            vec![existing, dlg_ptr],
17808            "NewDialog(behind=NIL) must insert dialog at the back"
17809        );
17810        assert_eq!(
17811            disp.front_window, existing,
17812            "front must stay on the pre-existing visible window"
17813        );
17814    }
17815
17816    #[test]
17817    fn new_cdialog_honors_behind_nil_and_inserts_at_back() {
17818        // Inside Macintosh Volume V, V-243: NewCDialog follows the same
17819        // creation path as NewDialog but returns a color dialog pointer.
17820        let (mut disp, mut cpu, mut bus) = setup();
17821        let existing = 0x200040u32;
17822        disp.window_list = vec![existing];
17823        disp.front_window = existing;
17824        bus.write_byte(existing + 110u32, 0xFF); // visible
17825
17826        let screen_base = bus.alloc((800 * 600) as u32);
17827        bus.write_long(0x0824, screen_base);
17828        disp.screen_mode = (screen_base, 800, 800, 600, 8);
17829
17830        let bounds_rect_ptr = 0x301280u32;
17831        bus.write_word(bounds_rect_ptr, 120);
17832        bus.write_word(bounds_rect_ptr + 2, 120);
17833        bus.write_word(bounds_rect_ptr + 4, 320);
17834        bus.write_word(bounds_rect_ptr + 6, 420);
17835
17836        let sp = TEST_SP - 30;
17837        cpu.write_reg(Register::A7, sp);
17838        for i in 0..34u32 {
17839            bus.write_byte(sp + i, 0);
17840        }
17841        bus.write_long(sp + 22, bounds_rect_ptr);
17842        bus.write_word(sp + 16, 1); // visible
17843        bus.write_long(sp + 10, 0); // behind = NIL (backmost)
17844
17845        let pre_a7 = cpu.read_reg(Register::A7);
17846        let result = disp.dispatch_dialog(true, 0x24B, &mut cpu, &mut bus);
17847        assert!(result.unwrap().is_ok());
17848        assert_eq!(
17849            cpu.read_reg(Register::A7),
17850            pre_a7 + 30,
17851            "NewCDialog must pop 30 bytes of parameters and leave result at new SP+0"
17852        );
17853        let dlg_ptr = bus.read_long(sp + 30);
17854        assert_ne!(dlg_ptr, 0);
17855        assert_eq!(
17856            disp.window_list,
17857            vec![existing, dlg_ptr],
17858            "NewCDialog(behind=NIL) must insert dialog at the back"
17859        );
17860        assert_eq!(
17861            disp.front_window, existing,
17862            "front must stay on the pre-existing visible window"
17863        );
17864    }
17865
17866    #[test]
17867    fn get_new_dialog_honors_behind_specific_window() {
17868        // GetNewDialog with no DLOG resource hits the fallback branch
17869        // that does NOT call finish_dialog_creation — so behind can't
17870        // reshuffle a list entry that was never added. This test
17871        // verifies the stack slot is at least READ without panicking.
17872        // The main contract test is new_dialog_honors_behind_nil_
17873        // and_inserts_at_back above, since that exercises the
17874        // primary post-finish_dialog_creation path.
17875        let (mut disp, mut cpu, mut bus) = setup();
17876
17877        let sp = TEST_SP;
17878        // Write a specific non-trivial behind pointer at SP+0.
17879        bus.write_long(sp, 0xDEAD0000);
17880
17881        let result = disp.dispatch_dialog(true, 0x17C, &mut cpu, &mut bus);
17882        assert!(result.unwrap().is_ok());
17883        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 10);
17884    }
17885
17886    // ---- SelectDialogItemText ($A97E) ----
17887
17888    #[test]
17889    fn select_dialog_item_text_zero_to_32767_selects_entire_text_and_sets_editfield() {
17890        // Macintosh Toolbox Essentials 1992, 6-131: selecting the whole
17891        // editable text item uses strtSel=0 and endSel=32767.
17892        let (mut disp, mut cpu, mut bus) = setup();
17893        let dialog_ptr = bus.alloc(170);
17894        bus.write_word(dialog_ptr + 164, 0xFFFF); // editField = -1 (none)
17895
17896        disp.dialog_items.insert(
17897            dialog_ptr,
17898            vec![DialogItem {
17899                item_type: 16,
17900                rect: (10, 20, 30, 40),
17901                text: "ABCDE".to_string(),
17902                resource_id: 0,
17903                proc_ptr: 0,
17904                sel_start: 0,
17905                sel_end: 0,
17906            }],
17907        );
17908
17909        bus.write_word(TEST_SP, 32767u16); // endSel
17910        bus.write_word(TEST_SP + 2, 0); // strtSel
17911        bus.write_word(TEST_SP + 4, 1); // itemNo (1-based)
17912        bus.write_long(TEST_SP + 6, dialog_ptr); // theDialog
17913
17914        let result = disp.dispatch_dialog(true, 0x17E, &mut cpu, &mut bus);
17915        assert!(result.unwrap().is_ok());
17916        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 10);
17917
17918        let item = &disp.dialog_items[&dialog_ptr][0];
17919        assert_eq!(item.sel_start, 0);
17920        assert_eq!(item.sel_end, 5);
17921        assert_eq!(bus.read_word(dialog_ptr + 164), 0);
17922    }
17923
17924    #[test]
17925    fn select_dialog_item_text_clamps_and_normalizes_selection_bounds() {
17926        // IM:I I-414 defines a [strtSel,endSel) range; callers can pass
17927        // out-of-range values, so HLE clamps to text bounds and normalizes
17928        // reversed inputs.
17929        let (mut disp, mut cpu, mut bus) = setup();
17930        let dialog_ptr = bus.alloc(170);
17931
17932        disp.dialog_items.insert(
17933            dialog_ptr,
17934            vec![DialogItem {
17935                item_type: 16,
17936                rect: (10, 20, 30, 40),
17937                text: "ABCDE".to_string(),
17938                resource_id: 0,
17939                proc_ptr: 0,
17940                sel_start: 0,
17941                sel_end: 0,
17942            }],
17943        );
17944
17945        bus.write_word(TEST_SP, 2); // endSel
17946        bus.write_word(TEST_SP + 2, 9); // strtSel (> text length)
17947        bus.write_word(TEST_SP + 4, 1); // itemNo
17948        bus.write_long(TEST_SP + 6, dialog_ptr); // theDialog
17949
17950        let result = disp.dispatch_dialog(true, 0x17E, &mut cpu, &mut bus);
17951        assert!(result.unwrap().is_ok());
17952        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 10);
17953
17954        let item = &disp.dialog_items[&dialog_ptr][0];
17955        assert_eq!(item.sel_start, 2);
17956        assert_eq!(item.sel_end, 5);
17957    }
17958
17959    #[test]
17960    fn select_dialog_item_text_non_edit_item_is_noop() {
17961        // Macintosh Toolbox Essentials 1992, 6-131: selection applies to
17962        // editable text items only.
17963        let (mut disp, mut cpu, mut bus) = setup();
17964        let dialog_ptr = bus.alloc(170);
17965        bus.write_word(dialog_ptr + 164, 0x7FFF); // sentinel
17966
17967        disp.dialog_items.insert(
17968            dialog_ptr,
17969            vec![DialogItem {
17970                item_type: 8, // statText
17971                rect: (10, 20, 30, 40),
17972                text: "STATIC".to_string(),
17973                resource_id: 0,
17974                proc_ptr: 0,
17975                sel_start: 1,
17976                sel_end: 3,
17977            }],
17978        );
17979
17980        bus.write_word(TEST_SP, 4); // endSel
17981        bus.write_word(TEST_SP + 2, 0); // strtSel
17982        bus.write_word(TEST_SP + 4, 1); // itemNo
17983        bus.write_long(TEST_SP + 6, dialog_ptr); // theDialog
17984
17985        let result = disp.dispatch_dialog(true, 0x17E, &mut cpu, &mut bus);
17986        assert!(result.unwrap().is_ok());
17987        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 10);
17988
17989        let item = &disp.dialog_items[&dialog_ptr][0];
17990        assert_eq!(item.sel_start, 1);
17991        assert_eq!(item.sel_end, 3);
17992        assert_eq!(bus.read_word(dialog_ptr + 164), 0x7FFF);
17993    }
17994
17995    // ---- Alert ($A985) ----
17996
17997    // Alert with no matching ALRT resource must return -1 per IM:I-412
17998    // ("Alert returns -1 and does nothing").
17999    #[test]
18000    fn alert_returns_minus_one_when_alrt_resource_missing() {
18001        let (mut disp, mut cpu, mut bus) = setup();
18002
18003        // SP+4: alertID = 128 (no ALRT resource loaded in the test
18004        // dispatcher).
18005        bus.write_word(TEST_SP + 4, 128);
18006        let result = disp.dispatch_dialog(true, 0x185, &mut cpu, &mut bus);
18007        assert!(result.unwrap().is_ok());
18008        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
18009        assert_eq!(bus.read_word(TEST_SP + 6) as i16, -1);
18010    }
18011
18012    // ---- te_line_origin_x (TextEdit alignment) ----
18013    //
18014    // Per IM:Text 1993 lines 7320-7323 and the MPW Universal Headers:
18015    // teJustLeft = 0, teJustCenter = 1, teJustRight = -1, teForceLeft = -2.
18016
18017    #[test]
18018    fn te_line_origin_te_just_left_flush_left() {
18019        // teJustLeft (0): origin = box_left + TextEdit's left inset.
18020        assert_eq!(
18021            TrapDispatcher::te_line_origin_x(0, 20, 200, 80),
18022            21,
18023            "teJustLeft (0) must anchor one pixel inside box_left"
18024        );
18025    }
18026
18027    #[test]
18028    fn te_line_origin_te_just_center_midpoint() {
18029        // teJustCenter (1): origin = box_left + (box_w - line_w) / 2.
18030        // (200-20 - 80) / 2 = 50 → origin = 20 + 50 = 70.
18031        assert_eq!(
18032            TrapDispatcher::te_line_origin_x(1, 20, 200, 80),
18033            70,
18034            "teJustCenter must midpoint the slack"
18035        );
18036    }
18037
18038    #[test]
18039    fn te_line_origin_te_just_right_flush_right() {
18040        // teJustRight (-1): origin = box_right - line_width.
18041        // 200 - 80 = 120.
18042        assert_eq!(
18043            TrapDispatcher::te_line_origin_x(-1, 20, 200, 80),
18044            120,
18045            "teJustRight (-1) must anchor at box_right - line_width"
18046        );
18047    }
18048
18049    #[test]
18050    fn te_line_origin_te_force_left_flush_left() {
18051        // teForceLeft (-2): origin = box_left + TextEdit's left inset
18052        // (overrides any
18053        // localised right-to-left default).
18054        assert_eq!(
18055            TrapDispatcher::te_line_origin_x(-2, 20, 200, 80),
18056            21,
18057            "teForceLeft (-2) must anchor one pixel inside box_left"
18058        );
18059    }
18060
18061    #[test]
18062    fn te_line_origin_te_just_system_defaults_left() {
18063        // teJustSystem (0): localised default = left for LTR, with the
18064        // same TextEdit left inset.
18065        assert_eq!(
18066            TrapDispatcher::te_line_origin_x(0, 20, 200, 80),
18067            21,
18068            "teJustSystem must default to flush left inside box_left"
18069        );
18070    }
18071
18072    // ---- StopAlert ($A986) ----
18073
18074    #[test]
18075    fn stop_alert_returns_minus_one_when_alrt_resource_missing() {
18076        let (mut disp, mut cpu, mut bus) = setup();
18077        bus.write_word(crate::memory::globals::addr::ALERT_STAGE, 2);
18078        bus.write_word(crate::memory::globals::addr::ANUMBER, 0xC0DE);
18079        bus.write_word(TEST_SP + 4, 128);
18080        let result = disp.dispatch_dialog(true, 0x186, &mut cpu, &mut bus);
18081        assert!(result.unwrap().is_ok());
18082        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
18083        assert_eq!(bus.read_word(TEST_SP + 6) as i16, -1);
18084        assert_eq!(bus.read_word(crate::memory::globals::addr::ALERT_STAGE), 2);
18085        assert_eq!(bus.read_word(crate::memory::globals::addr::ANUMBER), 0xC0DE);
18086    }
18087
18088    // ---- NoteAlert ($A987) ----
18089
18090    #[test]
18091    fn note_alert_returns_minus_one_when_alrt_resource_missing() {
18092        let (mut disp, mut cpu, mut bus) = setup();
18093        bus.write_word(crate::memory::globals::addr::ALERT_STAGE, 1);
18094        bus.write_word(crate::memory::globals::addr::ANUMBER, 0xBEEF);
18095        bus.write_word(TEST_SP + 4, 128);
18096        let result = disp.dispatch_dialog(true, 0x187, &mut cpu, &mut bus);
18097        assert!(result.unwrap().is_ok());
18098        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
18099        assert_eq!(bus.read_word(TEST_SP + 6) as i16, -1);
18100        assert_eq!(bus.read_word(crate::memory::globals::addr::ALERT_STAGE), 1);
18101        assert_eq!(bus.read_word(crate::memory::globals::addr::ANUMBER), 0xBEEF);
18102    }
18103
18104    // ---- CautionAlert ($A988) ----
18105
18106    #[test]
18107    fn caution_alert_returns_minus_one_when_alrt_resource_missing() {
18108        let (mut disp, mut cpu, mut bus) = setup();
18109        bus.write_word(crate::memory::globals::addr::ALERT_STAGE, 3);
18110        bus.write_word(crate::memory::globals::addr::ANUMBER, 0xFACE);
18111        bus.write_word(TEST_SP + 4, 128);
18112        let result = disp.dispatch_dialog(true, 0x188, &mut cpu, &mut bus);
18113        assert!(result.unwrap().is_ok());
18114        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
18115        assert_eq!(bus.read_word(TEST_SP + 6) as i16, -1);
18116        assert_eq!(bus.read_word(crate::memory::globals::addr::ALERT_STAGE), 3);
18117        assert_eq!(bus.read_word(crate::memory::globals::addr::ANUMBER), 0xFACE);
18118    }
18119
18120    // ---- Alert family stages-driven default item (IM:I I-417 / I-422) ----
18121    //
18122    // Each test installs an ALRT resource with a controlled
18123    // 16-byte template (8-byte boundsRect + 2-byte itemsID + 2-byte
18124    // stages + padding) and verifies the trap returns the bold
18125    // (default) item for the current AlertStage low-mem byte.
18126    //
18127    // ALRT stages encoding (IM:I I-422): 16-bit word, 4 nibbles
18128    // (low to high = stage 1..4). Within each nibble, bit 3 is
18129    // boldItmNum (`okDismissal = 8`), bit 2 is boxDrwn (`alBit = 4`),
18130    // and bits 0..1 are the sound number.
18131
18132    /// Build a 12-byte ALRT template: bounds=(0,0,80,200) +
18133    /// itemsID + stages + 0 padding. Real ALRTs are typically
18134    /// 12 bytes; we install at least 12.
18135    fn build_alrt_template(items_id: i16, stages: u16) -> Vec<u8> {
18136        let mut v = Vec::with_capacity(12);
18137        v.extend_from_slice(&0u16.to_be_bytes()); // top
18138        v.extend_from_slice(&0u16.to_be_bytes()); // left
18139        v.extend_from_slice(&80u16.to_be_bytes()); // bottom
18140        v.extend_from_slice(&200u16.to_be_bytes()); // right
18141        v.extend_from_slice(&items_id.to_be_bytes()); // itemsID
18142        v.extend_from_slice(&stages.to_be_bytes()); // stages
18143        v
18144    }
18145
18146    // ---- CouldDialog ($A979) / FreeDialog ($A97A) ----
18147
18148    #[test]
18149    fn could_dialog_present_resource_sets_reserr_noerr() {
18150        // Inside Macintosh Volume I, I-415: CouldDialog targets a DLOG by
18151        // resource ID and prepares it for later use. BasiliskII leaves
18152        // ResErr at noErr on the missing-resource path.
18153        let (mut disp, mut cpu, mut bus) = setup();
18154        let dlog = vec![0u8; 20];
18155        disp.install_test_resource(&mut bus, *b"DLOG", 300, &dlog);
18156        bus.write_word(0x0A60, 0x7FFF);
18157        bus.write_word(TEST_SP, 300);
18158
18159        let pre_a7 = cpu.read_reg(Register::A7);
18160        let result = disp.dispatch_dialog(true, 0x179, &mut cpu, &mut bus);
18161        assert!(result.unwrap().is_ok());
18162        assert_eq!(cpu.read_reg(Register::A7), pre_a7 + 2);
18163        assert_eq!(bus.read_word(0x0A60) as i16, 0);
18164    }
18165
18166    #[test]
18167    fn could_dialog_missing_resource_sets_reserr_resnotfound() {
18168        // BasiliskII leaves ResErr at noErr even when the DLOG is missing.
18169        let (mut disp, mut cpu, mut bus) = setup();
18170        bus.write_word(0x0A60, 0);
18171        bus.write_word(TEST_SP, 301);
18172
18173        let pre_a7 = cpu.read_reg(Register::A7);
18174        let result = disp.dispatch_dialog(true, 0x179, &mut cpu, &mut bus);
18175        assert!(result.unwrap().is_ok());
18176        assert_eq!(cpu.read_reg(Register::A7), pre_a7 + 2);
18177        assert_eq!(bus.read_word(0x0A60) as i16, 0);
18178    }
18179
18180    #[test]
18181    fn free_dialog_present_resource_sets_reserr_noerr() {
18182        // Inside Macintosh Volume I, I-415: FreeDialog reverses CouldDialog
18183        // for previously targeted DLOG templates. BasiliskII leaves
18184        // ResErr at noErr on the missing-resource path.
18185        let (mut disp, mut cpu, mut bus) = setup();
18186        let dlog = vec![0u8; 20];
18187        disp.install_test_resource(&mut bus, *b"DLOG", 302, &dlog);
18188        bus.write_word(0x0A60, 0x7FFF);
18189        bus.write_word(TEST_SP, 302);
18190
18191        let pre_a7 = cpu.read_reg(Register::A7);
18192        let result = disp.dispatch_dialog(true, 0x17A, &mut cpu, &mut bus);
18193        assert!(result.unwrap().is_ok());
18194        assert_eq!(cpu.read_reg(Register::A7), pre_a7 + 2);
18195        assert_eq!(bus.read_word(0x0A60) as i16, 0);
18196    }
18197
18198    #[test]
18199    fn free_dialog_missing_resource_sets_reserr_resnotfound() {
18200        // BasiliskII leaves ResErr at noErr even when the DLOG is missing.
18201        let (mut disp, mut cpu, mut bus) = setup();
18202        bus.write_word(0x0A60, 0);
18203        bus.write_word(TEST_SP, 303);
18204
18205        let pre_a7 = cpu.read_reg(Register::A7);
18206        let result = disp.dispatch_dialog(true, 0x17A, &mut cpu, &mut bus);
18207        assert!(result.unwrap().is_ok());
18208        assert_eq!(cpu.read_reg(Register::A7), pre_a7 + 2);
18209        assert_eq!(bus.read_word(0x0A60) as i16, 0);
18210    }
18211
18212    #[test]
18213    fn could_dialog_free_dialog_purgeability() {
18214        // IM:I I-415: CouldDialog reads the dialog resource family into
18215        // memory if needed and makes it unpurgeable; FreeDialog reverses
18216        // the purgeability state for those already-loaded resources.
18217        let (mut disp, mut cpu, mut bus) = setup();
18218        let dlog_id: i16 = 3100;
18219        let ditl_id: i16 = 3101;
18220        let cntl_id: i16 = 3102;
18221        let icon_id: i16 = 3103;
18222        let pict_id: i16 = 3104;
18223        let dlog = build_test_dlog((10, 20, 110, 220), ditl_id, 0);
18224        let ditl = build_test_ditl_items(&[
18225            (7, (1, 2, 12, 82), &cntl_id.to_be_bytes()),
18226            (32, (14, 2, 46, 34), &icon_id.to_be_bytes()),
18227            (64, (48, 2, 88, 82), &pict_id.to_be_bytes()),
18228        ]);
18229        disp.install_test_resource(&mut bus, *b"DLOG", dlog_id, &dlog);
18230        disp.install_test_resource(&mut bus, *b"DITL", ditl_id, &ditl);
18231        disp.install_test_resource(&mut bus, *b"CNTL", cntl_id, &[0u8; 32]);
18232        disp.install_test_resource(&mut bus, *b"ICON", icon_id, &[0xAA; 128]);
18233        disp.install_test_resource(&mut bus, *b"PICT", pict_id, &[0x11; 32]);
18234        disp.res_load = false;
18235
18236        bus.write_word(0x0A60, 0x7FFF);
18237        bus.write_word(TEST_SP, dlog_id as u16);
18238        let pre_a7 = cpu.read_reg(Register::A7);
18239        assert!(disp
18240            .dispatch_dialog(true, 0x179, &mut cpu, &mut bus)
18241            .unwrap()
18242            .is_ok());
18243        assert_eq!(cpu.read_reg(Register::A7), pre_a7 + 2);
18244        assert_eq!(bus.read_word(0x0A60) as i16, 0);
18245        for (res_type, res_id) in [
18246            (*b"DLOG", dlog_id),
18247            (*b"DITL", ditl_id),
18248            (*b"CNTL", cntl_id),
18249            (*b"ICON", icon_id),
18250            (*b"PICT", pict_id),
18251        ] {
18252            assert_resource_nonpurgeable(&disp, &bus, res_type, res_id);
18253        }
18254
18255        cpu.write_reg(Register::A7, TEST_SP);
18256        bus.write_word(TEST_SP, dlog_id as u16);
18257        assert!(disp
18258            .dispatch_dialog(true, 0x17A, &mut cpu, &mut bus)
18259            .unwrap()
18260            .is_ok());
18261        assert_eq!(bus.read_word(0x0A60) as i16, 0);
18262        for (res_type, res_id) in [
18263            (*b"DLOG", dlog_id),
18264            (*b"DITL", ditl_id),
18265            (*b"CNTL", cntl_id),
18266            (*b"ICON", icon_id),
18267            (*b"PICT", pict_id),
18268        ] {
18269            assert_resource_purgeable(&disp, res_type, res_id);
18270        }
18271    }
18272
18273    // ---- CouldAlert ($A989) / FreeAlert ($A98A) ----
18274
18275    #[test]
18276    fn could_alert_present_resource_sets_reserr_noerr() {
18277        // IM:I I-420: CouldAlert targets an ALRT template by ID.
18278        // Systemless's HLE compromise writes Resource Manager error
18279        // state at ResErr ($0A60): noErr for present resources.
18280        let (mut disp, mut cpu, mut bus) = setup();
18281        let alrt = build_alrt_template(200, 0x0000);
18282        disp.install_test_resource(&mut bus, *b"ALRT", 400, &alrt);
18283        bus.write_word(0x0A60, 0x7FFF);
18284        bus.write_word(TEST_SP, 400);
18285
18286        let pre_a7 = cpu.read_reg(Register::A7);
18287        let result = disp.dispatch_dialog(true, 0x189, &mut cpu, &mut bus);
18288        assert!(result.unwrap().is_ok());
18289        assert_eq!(cpu.read_reg(Register::A7), pre_a7 + 2);
18290        assert_eq!(bus.read_word(0x0A60) as i16, 0);
18291    }
18292
18293    #[test]
18294    fn could_alert_missing_resource_sets_reserr_resnotfound() {
18295        // IM:I I-420: missing ALRT IDs are ignored.
18296        let (mut disp, mut cpu, mut bus) = setup();
18297        bus.write_word(0x0A60, 0);
18298        bus.write_word(TEST_SP, 401);
18299
18300        let pre_a7 = cpu.read_reg(Register::A7);
18301        let result = disp.dispatch_dialog(true, 0x189, &mut cpu, &mut bus);
18302        assert!(result.unwrap().is_ok());
18303        assert_eq!(cpu.read_reg(Register::A7), pre_a7 + 2);
18304        assert_eq!(bus.read_word(0x0A60) as i16, 0);
18305    }
18306
18307    #[test]
18308    fn free_alert_present_resource_sets_reserr_noerr() {
18309        // IM:I I-420: FreeAlert undoes a prior CouldAlert target.
18310        // HLE compromise reports success via ResErr for present ALRT.
18311        let (mut disp, mut cpu, mut bus) = setup();
18312        let alrt = build_alrt_template(200, 0x0000);
18313        disp.install_test_resource(&mut bus, *b"ALRT", 402, &alrt);
18314        bus.write_word(0x0A60, 0x7FFF);
18315        bus.write_word(TEST_SP, 402);
18316
18317        let pre_a7 = cpu.read_reg(Register::A7);
18318        let result = disp.dispatch_dialog(true, 0x18A, &mut cpu, &mut bus);
18319        assert!(result.unwrap().is_ok());
18320        assert_eq!(cpu.read_reg(Register::A7), pre_a7 + 2);
18321        assert_eq!(bus.read_word(0x0A60) as i16, 0);
18322    }
18323
18324    #[test]
18325    fn free_alert_missing_resource_sets_reserr_resnotfound() {
18326        // Missing ALRT IDs in FreeAlert are ignored.
18327        let (mut disp, mut cpu, mut bus) = setup();
18328        bus.write_word(0x0A60, 0);
18329        bus.write_word(TEST_SP, 403);
18330
18331        let pre_a7 = cpu.read_reg(Register::A7);
18332        let result = disp.dispatch_dialog(true, 0x18A, &mut cpu, &mut bus);
18333        assert!(result.unwrap().is_ok());
18334        assert_eq!(cpu.read_reg(Register::A7), pre_a7 + 2);
18335        assert_eq!(bus.read_word(0x0A60) as i16, 0);
18336    }
18337
18338    #[test]
18339    fn could_alert_and_free_alert_leave_loaded_alert_attrs_unchanged() {
18340        let (mut disp, mut cpu, mut bus) = setup();
18341        let alrt = build_alrt_template(200, 0x0000);
18342        let data_ptr = disp.install_test_resource(&mut bus, *b"ALRT", 404, &alrt);
18343        let handle = disp.get_or_create_resource_handle(&mut bus, *b"ALRT", 404, data_ptr);
18344        let attrs_before = disp.resource_attributes_for_handle(handle).unwrap();
18345
18346        cpu.write_reg(Register::A7, TEST_SP);
18347        bus.write_word(TEST_SP, 404);
18348        assert!(disp
18349            .dispatch_dialog(true, 0x189, &mut cpu, &mut bus)
18350            .unwrap()
18351            .is_ok());
18352        assert_eq!(bus.read_word(0x0A60) as i16, 0);
18353        assert_eq!(
18354            disp.resource_attributes_for_handle(handle).unwrap(),
18355            attrs_before
18356        );
18357
18358        cpu.write_reg(Register::A7, TEST_SP);
18359        bus.write_word(TEST_SP, 404);
18360        assert!(disp
18361            .dispatch_dialog(true, 0x18A, &mut cpu, &mut bus)
18362            .unwrap()
18363            .is_ok());
18364        assert_eq!(bus.read_word(0x0A60) as i16, 0);
18365        assert_eq!(
18366            disp.resource_attributes_for_handle(handle).unwrap(),
18367            attrs_before
18368        );
18369    }
18370
18371    #[test]
18372    fn could_alert_free_alert_purgeability() {
18373        // IM:I I-420 gives CouldAlert/FreeAlert the same resource-family
18374        // purgeability contract as CouldDialog/FreeDialog, rooted at ALRT.
18375        let (mut disp, mut cpu, mut bus) = setup();
18376        let alrt_id: i16 = 3200;
18377        let ditl_id: i16 = 3201;
18378        let cntl_id: i16 = 3202;
18379        let icon_id: i16 = 3203;
18380        let pict_id: i16 = 3204;
18381        let alrt = build_alrt_template(ditl_id, 0x0000);
18382        let ditl = build_test_ditl_items(&[
18383            (7, (1, 2, 12, 82), &cntl_id.to_be_bytes()),
18384            (32, (14, 2, 46, 34), &icon_id.to_be_bytes()),
18385            (64, (48, 2, 88, 82), &pict_id.to_be_bytes()),
18386        ]);
18387        disp.install_test_resource(&mut bus, *b"ALRT", alrt_id, &alrt);
18388        disp.install_test_resource(&mut bus, *b"DITL", ditl_id, &ditl);
18389        disp.install_test_resource(&mut bus, *b"CNTL", cntl_id, &[0u8; 32]);
18390        disp.install_test_resource(&mut bus, *b"ICON", icon_id, &[0xAA; 128]);
18391        disp.install_test_resource(&mut bus, *b"PICT", pict_id, &[0x11; 32]);
18392        disp.res_load = false;
18393
18394        bus.write_word(0x0A60, 0x7FFF);
18395        bus.write_word(TEST_SP, alrt_id as u16);
18396        let pre_a7 = cpu.read_reg(Register::A7);
18397        assert!(disp
18398            .dispatch_dialog(true, 0x189, &mut cpu, &mut bus)
18399            .unwrap()
18400            .is_ok());
18401        assert_eq!(cpu.read_reg(Register::A7), pre_a7 + 2);
18402        assert_eq!(bus.read_word(0x0A60) as i16, 0);
18403        for (res_type, res_id) in [
18404            (*b"ALRT", alrt_id),
18405            (*b"DITL", ditl_id),
18406            (*b"CNTL", cntl_id),
18407            (*b"ICON", icon_id),
18408            (*b"PICT", pict_id),
18409        ] {
18410            assert_resource_nonpurgeable(&disp, &bus, res_type, res_id);
18411        }
18412
18413        cpu.write_reg(Register::A7, TEST_SP);
18414        bus.write_word(TEST_SP, alrt_id as u16);
18415        assert!(disp
18416            .dispatch_dialog(true, 0x18A, &mut cpu, &mut bus)
18417            .unwrap()
18418            .is_ok());
18419        assert_eq!(bus.read_word(0x0A60) as i16, 0);
18420        for (res_type, res_id) in [
18421            (*b"ALRT", alrt_id),
18422            (*b"DITL", ditl_id),
18423            (*b"CNTL", cntl_id),
18424            (*b"ICON", icon_id),
18425            (*b"PICT", pict_id),
18426        ] {
18427            assert_resource_purgeable(&disp, res_type, res_id);
18428        }
18429    }
18430
18431    #[test]
18432    fn alert_returns_item_1_when_stage_1_bold_flag_clear() {
18433        // stages = 0x4444 → all 4 nibbles have boxDrwn set and
18434        // bold item clear, so item 1 (OK button) is the default.
18435        let (mut disp, mut cpu, mut bus) = setup();
18436        let alrt = build_alrt_template(/*itemsID*/ 200, /*stages*/ 0x4444);
18437        disp.install_test_resource(&mut bus, *b"ALRT", 128, &alrt);
18438        bus.write_word(crate::memory::globals::addr::ALERT_STAGE, 0); // stage 1
18439        bus.write_word(TEST_SP + 4, 128);
18440        let result = disp.dispatch_dialog(true, 0x185, &mut cpu, &mut bus);
18441        assert!(result.unwrap().is_ok());
18442        assert_eq!(
18443            bus.read_word(TEST_SP + 6) as i16,
18444            1,
18445            "Alert with stage 1 + bold flag clear must return item 1 (OK)"
18446        );
18447    }
18448
18449    #[test]
18450    fn alert_returns_item_2_when_stage_1_bold_flag_set() {
18451        // stages = 0xCCCC → all 4 nibbles have boxDrwn and bit 3 set
18452        // (okDismissal mask per IM:I I-424), so item 2 is default.
18453        let (mut disp, mut cpu, mut bus) = setup();
18454        let alrt = build_alrt_template(200, 0xCCCC);
18455        disp.install_test_resource(&mut bus, *b"ALRT", 129, &alrt);
18456        bus.write_word(crate::memory::globals::addr::ALERT_STAGE, 0);
18457        bus.write_word(TEST_SP + 4, 129);
18458        let result = disp.dispatch_dialog(true, 0x185, &mut cpu, &mut bus);
18459        assert!(result.unwrap().is_ok());
18460        assert_eq!(
18461            bus.read_word(TEST_SP + 6) as i16,
18462            2,
18463            "Alert with stage 1 + bold flag set must return item 2"
18464        );
18465    }
18466
18467    #[test]
18468    fn alert_steps_through_stages_and_caps_at_stage_4() {
18469        // stages = 0xC4C4 → boxDrwn set in every nibble; bit 3 of
18470        // stage 1/3 nibble = 0 (item 1), bit 3 of stage 2/4 nibble
18471        // = 1 (item 2). Per IM:I I-422 okDismissal mask = 8.
18472        let (mut disp, mut cpu, mut bus) = setup();
18473        let alrt = build_alrt_template(200, 0xC4C4);
18474        disp.install_test_resource(&mut bus, *b"ALRT", 130, &alrt);
18475        bus.write_word(crate::memory::globals::addr::ALERT_STAGE, 0);
18476        bus.write_word(TEST_SP + 4, 130);
18477
18478        let expected = [1i16, 2, 1, 2, 2, 2];
18479        for (i, want) in expected.iter().enumerate() {
18480            cpu.write_reg(Register::A7, TEST_SP);
18481            let result = disp.dispatch_dialog(true, 0x185, &mut cpu, &mut bus);
18482            assert!(result.unwrap().is_ok());
18483            assert_eq!(
18484                bus.read_word(TEST_SP + 6) as i16,
18485                *want,
18486                "call #{} (stage {}): expected item {}",
18487                i + 1,
18488                (i + 1).min(4),
18489                want
18490            );
18491        }
18492        // After at least 4 calls the AlertStage word is capped at 3.
18493        assert_eq!(
18494            bus.read_word(crate::memory::globals::addr::ALERT_STAGE),
18495            3,
18496            "AlertStage must cap at 3 (stages are 1..4 → word 0..3)"
18497        );
18498    }
18499
18500    #[test]
18501    fn alert_increments_alert_stage_word_after_dispatch() {
18502        // First call from stage 0 must leave AlertStage = 1. Read
18503        // and write as 16-bit WORD per IM:I I-423 + MTb 1992 22620.
18504        let (mut disp, mut cpu, mut bus) = setup();
18505        let alrt = build_alrt_template(200, 0x4444);
18506        disp.install_test_resource(&mut bus, *b"ALRT", 131, &alrt);
18507        bus.write_word(crate::memory::globals::addr::ALERT_STAGE, 0);
18508        bus.write_word(TEST_SP + 4, 131);
18509        let _ = disp.dispatch_dialog(true, 0x185, &mut cpu, &mut bus);
18510        assert_eq!(
18511            bus.read_word(crate::memory::globals::addr::ALERT_STAGE),
18512            1,
18513            "Alert must increment AlertStage 0 → 1 after dispatch"
18514        );
18515    }
18516
18517    #[test]
18518    fn alert_does_not_increment_alert_stage_when_alrt_missing() {
18519        // No ALRT → -1 path must NOT touch AlertStage so the next
18520        // call (with a real ALRT installed) sees the original
18521        // stage. This is critical for apps that defensively call
18522        // Alert(missingID) before Alert(realID).
18523        let (mut disp, mut cpu, mut bus) = setup();
18524        bus.write_word(crate::memory::globals::addr::ALERT_STAGE, 2);
18525        bus.write_word(TEST_SP + 4, 999); // no ALRT 999
18526        let result = disp.dispatch_dialog(true, 0x185, &mut cpu, &mut bus);
18527        assert!(result.unwrap().is_ok());
18528        assert_eq!(bus.read_word(TEST_SP + 6) as i16, -1);
18529        assert_eq!(
18530            bus.read_word(crate::memory::globals::addr::ALERT_STAGE),
18531            2,
18532            "missing-ALRT must NOT increment AlertStage"
18533        );
18534    }
18535
18536    #[test]
18537    fn alert_returns_minus_one_when_stage_box_drawn_clear_but_updates_stage() {
18538        // IM:I I-418 and MTE 1992 p. 6-106: Alert calls the alert
18539        // sound procedure for the stage and returns -1 when boxDrwn
18540        // is clear, but it is still an occurrence of that alert.
18541        let (mut disp, mut cpu, mut bus) = setup();
18542        let alrt = build_alrt_template(200, 0x0000);
18543        disp.install_test_resource(&mut bus, *b"ALRT", 133, &alrt);
18544        bus.write_word(crate::memory::globals::addr::ALERT_STAGE, 0);
18545        bus.write_word(crate::memory::globals::addr::ANUMBER, 0xCAFE);
18546        bus.write_word(TEST_SP + 4, 133);
18547
18548        let result = disp.dispatch_dialog(true, 0x185, &mut cpu, &mut bus);
18549        assert!(result.unwrap().is_ok());
18550        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
18551        assert_eq!(
18552            bus.read_word(TEST_SP + 6) as i16,
18553            -1,
18554            "boxDrwn clear must suppress the alert box and return -1"
18555        );
18556        assert_eq!(
18557            bus.read_word(crate::memory::globals::addr::ALERT_STAGE),
18558            1,
18559            "suppressed present ALRT must still advance ACount"
18560        );
18561        assert_eq!(
18562            bus.read_word(crate::memory::globals::addr::ANUMBER) as i16,
18563            133,
18564            "suppressed present ALRT must still record ANumber"
18565        );
18566    }
18567
18568    #[test]
18569    fn alert_writes_anumber_with_alert_id_after_successful_dispatch() {
18570        // ANumber at $0A98 records the resource ID of the last
18571        // alert that occurred per IM:I I-423.
18572        let (mut disp, mut cpu, mut bus) = setup();
18573        let alrt = build_alrt_template(200, 0x4444);
18574        disp.install_test_resource(&mut bus, *b"ALRT", 250, &alrt);
18575        bus.write_word(crate::memory::globals::addr::ANUMBER, 0xCAFE);
18576        bus.write_word(crate::memory::globals::addr::ALERT_STAGE, 0);
18577        bus.write_word(TEST_SP + 4, 250);
18578        let _ = disp.dispatch_dialog(true, 0x185, &mut cpu, &mut bus);
18579        assert_eq!(
18580            bus.read_word(crate::memory::globals::addr::ANUMBER) as i16,
18581            250,
18582            "Alert must write the alertID to ANumber per IM:I I-423"
18583        );
18584    }
18585
18586    #[test]
18587    fn alert_does_not_overwrite_anumber_when_alrt_missing() {
18588        // Missing-ALRT path returns -1 without overwriting ANumber
18589        // (defensive: callers reading ANumber after a probe call
18590        // must see the prior real value, not the failed probe ID).
18591        let (mut disp, mut cpu, mut bus) = setup();
18592        bus.write_word(crate::memory::globals::addr::ANUMBER, 0xC0DE);
18593        bus.write_word(TEST_SP + 4, 999); // no ALRT 999
18594        let _ = disp.dispatch_dialog(true, 0x185, &mut cpu, &mut bus);
18595        assert_eq!(
18596            bus.read_word(crate::memory::globals::addr::ANUMBER),
18597            0xC0DE,
18598            "missing-ALRT must NOT overwrite ANumber"
18599        );
18600    }
18601
18602    #[test]
18603    fn alert_family_share_dispatch_path_so_stop_note_caution_match_alert() {
18604        // The four icon variants ($A985 Alert / $A986 StopAlert /
18605        // $A987 NoteAlert / $A988 CautionAlert) differ only by
18606        // displayed icon — their dispatch into the ALRT template
18607        // is identical. With the same ALRT installed and the same
18608        // AlertStage all four must return the same item number.
18609        let alrt = build_alrt_template(200, 0x4444);
18610        let trap_words = [
18611            (0x185, "Alert"),
18612            (0x186, "StopAlert"),
18613            (0x187, "NoteAlert"),
18614            (0x188, "CautionAlert"),
18615        ];
18616        for (trap, name) in trap_words.iter() {
18617            let (mut disp, mut cpu, mut bus) = setup();
18618            disp.install_test_resource(&mut bus, *b"ALRT", 132, &alrt);
18619            bus.write_word(crate::memory::globals::addr::ALERT_STAGE, 0);
18620            bus.write_word(TEST_SP + 4, 132);
18621            let result = disp.dispatch_dialog(true, *trap, &mut cpu, &mut bus);
18622            assert!(result.unwrap().is_ok());
18623            assert_eq!(
18624                bus.read_word(TEST_SP + 6) as i16,
18625                1,
18626                "{name} must return item 1 (bold flag clear, stage 1) — \
18627                 same as Alert"
18628            );
18629        }
18630    }
18631
18632    #[test]
18633    fn alert_pops_eight_bytes_per_pascal_signature() {
18634        // FUNCTION Alert(alertID: INTEGER; filterProc: ProcPtr): INTEGER;
18635        // Pascal stack frame: result(2) + filterProc(4) +
18636        // alertID(2) + return PC slot mock at SP+0 (caller-set
18637        // here). Trap pops 6 bytes (filterProc + alertID + return)
18638        // leaving result at new SP+0 = TEST_SP+6.
18639        let (mut disp, mut cpu, mut bus) = setup();
18640        bus.write_word(TEST_SP + 4, 999); // alertID
18641        let pre_a7 = cpu.read_reg(Register::A7);
18642        let _ = disp.dispatch_dialog(true, 0x185, &mut cpu, &mut bus);
18643        let post_a7 = cpu.read_reg(Register::A7);
18644        assert_eq!(
18645            post_a7,
18646            pre_a7 + 6,
18647            "Alert must advance A7 by 6 bytes (filterProc + alertID + return)"
18648        );
18649    }
18650
18651    // ---- InitDialogs ($A97B) — IM:I I-411 init contract ----
18652
18653    #[test]
18654    fn init_dialogs_stores_resume_proc_at_lowmem_a8c() {
18655        // PROCEDURE InitDialogs(resumeProc: ProcPtr);
18656        // The non-NIL ProcPtr argument must land at $0A8C
18657        // (ResumeProc global) per IM:I I-411 + the assembly note.
18658        let (mut disp, mut cpu, mut bus) = setup();
18659        bus.write_long(crate::memory::globals::addr::RESUME_PROC, 0xCAFEBABE);
18660        bus.write_long(TEST_SP, 0x00123456); // resumeProc parameter
18661        let _ = disp.dispatch_dialog(true, 0x17B, &mut cpu, &mut bus);
18662        assert_eq!(
18663            bus.read_long(crate::memory::globals::addr::RESUME_PROC),
18664            0x00123456,
18665            "InitDialogs must store resumeProc at $0A8C"
18666        );
18667    }
18668
18669    #[test]
18670    fn init_dialogs_accepts_nil_resume_proc() {
18671        // The IM-canonical default is NIL ("no resume procedure
18672        // is desired"). Pin that NIL is stored as 0 — not a
18673        // sentinel like -1 — so the System Error Handler's
18674        // `if (resume) call(*resume)` path takes the no-op branch.
18675        let (mut disp, mut cpu, mut bus) = setup();
18676        bus.write_long(crate::memory::globals::addr::RESUME_PROC, 0xDEADBEEF);
18677        bus.write_long(TEST_SP, 0); // NIL
18678        let _ = disp.dispatch_dialog(true, 0x17B, &mut cpu, &mut bus);
18679        assert_eq!(
18680            bus.read_long(crate::memory::globals::addr::RESUME_PROC),
18681            0,
18682            "InitDialogs must store NIL resumeProc as 0 at $0A8C"
18683        );
18684    }
18685
18686    #[test]
18687    fn init_dialogs_zeros_dabeeper_at_lowmem_a9c() {
18688        // IM:I I-411: "It installs the standard sound procedure."
18689        // Systemless's HLE has no menu-bar-blink sound, so we install
18690        // NIL (== silent) per the IM:I I-411 ErrorSound semantic
18691        // ("If you pass NIL for soundProc, there will be no sound
18692        // ... at all"). A subsequent ErrorSound ($A98C) call can
18693        // override.
18694        let (mut disp, mut cpu, mut bus) = setup();
18695        bus.write_long(crate::memory::globals::addr::DA_BEEPER, 0xDEADBEEF);
18696        bus.write_long(TEST_SP, 0);
18697        let _ = disp.dispatch_dialog(true, 0x17B, &mut cpu, &mut bus);
18698        assert_eq!(
18699            bus.read_long(crate::memory::globals::addr::DA_BEEPER),
18700            0,
18701            "InitDialogs must store NIL (silent default) at DABeeper $0A9C"
18702        );
18703    }
18704
18705    #[test]
18706    fn init_dialogs_zeros_alert_stage_at_lowmem_a9a() {
18707        // First call to Alert/StopAlert/NoteAlert/CautionAlert
18708        // after InitDialogs must start at stage 1 (= AlertStage
18709        // word value 0). This is critical for apps that call
18710        // InitDialogs at re-launch but already have a stale stage
18711        // byte from a prior crashed run.
18712        let (mut disp, mut cpu, mut bus) = setup();
18713        bus.write_word(crate::memory::globals::addr::ALERT_STAGE, 3);
18714        bus.write_long(TEST_SP, 0);
18715        let _ = disp.dispatch_dialog(true, 0x17B, &mut cpu, &mut bus);
18716        assert_eq!(
18717            bus.read_word(crate::memory::globals::addr::ALERT_STAGE),
18718            0,
18719            "InitDialogs must zero AlertStage at $0A9A so the next Alert starts at stage 1"
18720        );
18721    }
18722
18723    #[test]
18724    fn init_dialogs_zeros_dastrings_array_at_lowmem_aa0() {
18725        // IM:I I-411: "It passes empty strings to ParamText."
18726        // The DAStrings global at $0AA0 is a 16-byte array of 4
18727        // Handles; zeroing all 4 = ParamText('','','','').
18728        let (mut disp, mut cpu, mut bus) = setup();
18729        for i in 0..4u32 {
18730            bus.write_long(
18731                crate::memory::globals::addr::DA_STRINGS + i * 4,
18732                0xDEAD0000 | i,
18733            );
18734        }
18735        bus.write_long(TEST_SP, 0);
18736        let _ = disp.dispatch_dialog(true, 0x17B, &mut cpu, &mut bus);
18737        for i in 0..4u32 {
18738            assert_eq!(
18739                bus.read_long(crate::memory::globals::addr::DA_STRINGS + i * 4),
18740                0,
18741                "InitDialogs must zero DAStrings[{}] at $0AA0+{}",
18742                i,
18743                i * 4
18744            );
18745        }
18746    }
18747
18748    #[test]
18749    fn init_dialogs_pops_four_bytes_resume_proc_param() {
18750        // PROCEDURE → no result slot, single 4-byte ProcPtr arg.
18751        let (mut disp, mut cpu, mut bus) = setup();
18752        bus.write_long(TEST_SP, 0);
18753        let pre_a7 = cpu.read_reg(Register::A7);
18754        let _ = disp.dispatch_dialog(true, 0x17B, &mut cpu, &mut bus);
18755        assert_eq!(
18756            cpu.read_reg(Register::A7),
18757            pre_a7 + 4,
18758            "InitDialogs must pop 4 bytes (resumeProc ProcPtr)"
18759        );
18760    }
18761
18762    // ---- ErrorSound ($A98C) ----
18763
18764    #[test]
18765    fn error_sound_stores_sound_proc_pointer_in_dabeeper() {
18766        // Inside Macintosh Volume I, I-411: ErrorSound sets the current
18767        // alert sound procedure, and the assembly-language note says this
18768        // pointer is stored in DABeeper.
18769        let (mut disp, mut cpu, mut bus) = setup();
18770        let sound_proc = 0x0012_3456u32;
18771        bus.write_long(crate::memory::globals::addr::DA_BEEPER, 0);
18772        bus.write_long(TEST_SP, sound_proc);
18773
18774        let pre_a7 = cpu.read_reg(Register::A7);
18775        let result = disp.dispatch_dialog(true, 0x18C, &mut cpu, &mut bus);
18776        assert!(result.unwrap().is_ok());
18777        assert_eq!(
18778            bus.read_long(crate::memory::globals::addr::DA_BEEPER),
18779            sound_proc
18780        );
18781        assert_eq!(cpu.read_reg(Register::A7), pre_a7 + 4);
18782    }
18783
18784    #[test]
18785    fn error_sound_nil_clears_dabeeper_to_disable_alert_sound() {
18786        // Inside Macintosh Volume I, I-411: passing NIL for soundProc means
18787        // "no sound (or menu bar blinking) at all".
18788        let (mut disp, mut cpu, mut bus) = setup();
18789        bus.write_long(crate::memory::globals::addr::DA_BEEPER, 0xDEADBEEF);
18790        bus.write_long(TEST_SP, 0);
18791
18792        let result = disp.dispatch_dialog(true, 0x18C, &mut cpu, &mut bus);
18793        assert!(result.unwrap().is_ok());
18794        assert_eq!(bus.read_long(crate::memory::globals::addr::DA_BEEPER), 0);
18795    }
18796
18797    #[test]
18798    fn error_sound_overrides_init_dialogs_default_sound_proc() {
18799        // IM:I I-411: InitDialogs installs the default alert sound procedure,
18800        // and ErrorSound replaces it with the caller's soundProc.
18801        let (mut disp, mut cpu, mut bus) = setup();
18802        let sound_proc = 0x0012_3456u32;
18803
18804        bus.write_long(crate::memory::globals::addr::DA_BEEPER, 0xDEADBEEF);
18805        bus.write_long(TEST_SP, 0);
18806        let result = disp.dispatch_dialog(true, 0x17B, &mut cpu, &mut bus);
18807        assert!(result.unwrap().is_ok());
18808        assert_eq!(bus.read_long(crate::memory::globals::addr::DA_BEEPER), 0);
18809
18810        cpu.write_reg(Register::A7, TEST_SP);
18811        bus.write_long(TEST_SP, sound_proc);
18812        let result = disp.dispatch_dialog(true, 0x18C, &mut cpu, &mut bus);
18813        assert!(result.unwrap().is_ok());
18814        assert_eq!(
18815            bus.read_long(crate::memory::globals::addr::DA_BEEPER),
18816            sound_proc
18817        );
18818
18819        cpu.write_reg(Register::A7, TEST_SP);
18820        bus.write_long(TEST_SP, 0);
18821        let result = disp.dispatch_dialog(true, 0x18C, &mut cpu, &mut bus);
18822        assert!(result.unwrap().is_ok());
18823        assert_eq!(bus.read_long(crate::memory::globals::addr::DA_BEEPER), 0);
18824    }
18825
18826    // ---- IsDialogEvent ($A97F) ----
18827
18828    #[test]
18829    fn is_dialog_event_returns_false() {
18830        let (mut disp, mut cpu, mut bus) = setup();
18831        // SP+0: event_ptr (4 bytes)
18832        bus.write_long(TEST_SP, 0x300000);
18833
18834        let result = disp.dispatch_dialog(true, 0x17F, &mut cpu, &mut bus);
18835        assert!(result.unwrap().is_ok());
18836        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
18837        assert_eq!(bus.read_word(TEST_SP + 4), 0);
18838    }
18839
18840    #[test]
18841    fn is_dialog_event_true_for_mouse_down_in_front_dialog() {
18842        // Inside Macintosh Volume I, I-417: mouse-down in content region of
18843        // active dialog window is a dialog event (TRUE).
18844        let (mut disp, mut cpu, mut bus) = setup();
18845        let dialog_ptr = bus.alloc(170);
18846        let event_ptr = 0x300000u32;
18847
18848        disp.front_window = dialog_ptr;
18849        disp.enable_input_trace_capture();
18850        bus.write_word(dialog_ptr + 8, (-100i16) as u16);
18851        bus.write_word(dialog_ptr + 10, (-200i16) as u16);
18852        bus.write_word(dialog_ptr + 16, 0);
18853        bus.write_word(dialog_ptr + 18, 0);
18854        bus.write_word(dialog_ptr + 20, 80);
18855        bus.write_word(dialog_ptr + 22, 120);
18856        disp.dialog_items.insert(dialog_ptr, Vec::new());
18857
18858        bus.write_word(event_ptr, 1);
18859        bus.write_long(event_ptr + 2, 0);
18860        bus.write_long(event_ptr + 6, 0);
18861        bus.write_word(event_ptr + 10, 120);
18862        bus.write_word(event_ptr + 12, 240);
18863        bus.write_word(event_ptr + 14, 0x0080);
18864        bus.write_long(TEST_SP, event_ptr);
18865
18866        let result = disp.dispatch_dialog(true, 0x17F, &mut cpu, &mut bus);
18867        assert!(result.unwrap().is_ok());
18868        assert_eq!(bus.read_word(TEST_SP + 4), 0xFFFF);
18869        let trace = disp.input_trace_text();
18870        assert!(trace.contains("A97F action=guest_event:mouseDown"));
18871        assert!(trace.contains("tracking=menu:idle dialog:idle control:idle"));
18872        assert!(trace.contains("result=true outcome=is_dialog_event"));
18873    }
18874
18875    #[test]
18876    fn is_dialog_event_false_for_mouse_down_outside_front_dialog() {
18877        // Inside Macintosh Volume I, I-417: only mouse-down events in the
18878        // content region of an active dialog window return TRUE.
18879        let (mut disp, mut cpu, mut bus) = setup();
18880        let dialog_ptr = bus.alloc(170);
18881        let event_ptr = 0x300000u32;
18882
18883        disp.front_window = dialog_ptr;
18884        disp.enable_input_trace_capture();
18885        bus.write_word(dialog_ptr + 8, (-100i16) as u16);
18886        bus.write_word(dialog_ptr + 10, (-200i16) as u16);
18887        bus.write_word(dialog_ptr + 16, 0);
18888        bus.write_word(dialog_ptr + 18, 0);
18889        bus.write_word(dialog_ptr + 20, 80);
18890        bus.write_word(dialog_ptr + 22, 120);
18891        disp.dialog_items.insert(dialog_ptr, Vec::new());
18892
18893        bus.write_word(event_ptr, 1); // mouseDown
18894        bus.write_long(event_ptr + 2, 0);
18895        bus.write_long(event_ptr + 6, 0);
18896        bus.write_word(event_ptr + 10, 40); // well outside dialog content
18897        bus.write_word(event_ptr + 12, 40);
18898        bus.write_word(event_ptr + 14, 0x0080);
18899        bus.write_long(TEST_SP, event_ptr);
18900
18901        let result = disp.dispatch_dialog(true, 0x17F, &mut cpu, &mut bus);
18902        assert!(result.unwrap().is_ok());
18903        assert_eq!(bus.read_word(TEST_SP + 4), 0);
18904    }
18905
18906    #[test]
18907    fn is_dialog_event_true_for_update_event_targeting_dialog_window() {
18908        // Inside Macintosh Volume I, I-417: activate/update events for a
18909        // dialog window are dialog events.
18910        let (mut disp, mut cpu, mut bus) = setup();
18911        let dialog_ptr = bus.alloc(170);
18912        let event_ptr = 0x300000u32;
18913
18914        disp.dialog_items.insert(dialog_ptr, Vec::new());
18915        bus.write_word(event_ptr, 6); // updateEvt
18916        bus.write_long(event_ptr + 2, dialog_ptr);
18917        bus.write_long(TEST_SP, event_ptr);
18918
18919        let result = disp.dispatch_dialog(true, 0x17F, &mut cpu, &mut bus);
18920        assert!(result.unwrap().is_ok());
18921        assert_eq!(bus.read_word(TEST_SP + 4), 0xFFFF);
18922    }
18923
18924    #[test]
18925    fn is_dialog_event_true_for_activate_event_targeting_dialog_window() {
18926        // Inside Macintosh Volume I, I-417: activate events for a dialog
18927        // window are dialog events as well.
18928        let (mut disp, mut cpu, mut bus) = setup();
18929        let dialog_ptr = bus.alloc(170);
18930        let event_ptr = 0x300000u32;
18931
18932        disp.dialog_items.insert(dialog_ptr, Vec::new());
18933        bus.write_word(event_ptr, 8); // activateEvt
18934        bus.write_long(event_ptr + 2, dialog_ptr);
18935        bus.write_long(TEST_SP, event_ptr);
18936
18937        let result = disp.dispatch_dialog(true, 0x17F, &mut cpu, &mut bus);
18938        assert!(result.unwrap().is_ok());
18939        assert_eq!(bus.read_word(TEST_SP + 4), 0xFFFF);
18940    }
18941
18942    // ---- DialogSelect ($A980) ----
18943
18944    #[test]
18945    fn dialog_select_returns_false() {
18946        let (mut disp, mut cpu, mut bus) = setup();
18947
18948        let result = disp.dispatch_dialog(true, 0x180, &mut cpu, &mut bus);
18949        assert!(result.unwrap().is_ok());
18950        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 12);
18951        assert_eq!(bus.read_word(TEST_SP + 12), 0);
18952    }
18953
18954    #[test]
18955    fn dialog_select_returns_hit_for_enabled_user_item() {
18956        let (mut disp, mut cpu, mut bus) = setup();
18957        let dialog_ptr = bus.alloc(170);
18958        let event_ptr = 0x300000u32;
18959        let dialog_out_ptr = 0x300100u32;
18960        let item_hit_ptr = 0x300104u32;
18961
18962        disp.front_window = dialog_ptr;
18963        disp.enable_input_trace_capture();
18964        bus.write_word(dialog_ptr + 8, (-100i16) as u16);
18965        bus.write_word(dialog_ptr + 10, (-200i16) as u16);
18966        bus.write_word(dialog_ptr + 16, 0);
18967        bus.write_word(dialog_ptr + 18, 0);
18968        bus.write_word(dialog_ptr + 20, 100);
18969        bus.write_word(dialog_ptr + 22, 160);
18970        disp.dialog_items.insert(
18971            dialog_ptr,
18972            vec![DialogItem {
18973                item_type: 0,
18974                rect: (20, 30, 60, 110),
18975                text: String::new(),
18976                resource_id: 0,
18977                proc_ptr: 0,
18978                sel_start: 0,
18979                sel_end: 0,
18980            }],
18981        );
18982
18983        bus.write_word(event_ptr, 1);
18984        bus.write_long(event_ptr + 2, 0);
18985        bus.write_long(event_ptr + 6, 0);
18986        bus.write_word(event_ptr + 10, 130);
18987        bus.write_word(event_ptr + 12, 240);
18988        bus.write_word(event_ptr + 14, 0x0080);
18989
18990        bus.write_long(TEST_SP, item_hit_ptr);
18991        bus.write_long(TEST_SP + 4, dialog_out_ptr);
18992        bus.write_long(TEST_SP + 8, event_ptr);
18993
18994        let result = disp.dispatch_dialog(true, 0x180, &mut cpu, &mut bus);
18995        assert!(result.unwrap().is_ok());
18996        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 12);
18997        assert_eq!(bus.read_word(TEST_SP + 12), 0xFFFF);
18998        assert_eq!(bus.read_long(dialog_out_ptr), dialog_ptr);
18999        assert_eq!(bus.read_word(item_hit_ptr), 1);
19000        let trace = disp.input_trace_text();
19001        assert!(trace.contains("A980 action=guest_event:mouseDown"));
19002        assert!(trace.contains("item_hit=1 item_type=$00 disabled=false outcome=enabled_item"));
19003    }
19004
19005    fn dialog_select_enabled_user_item_hit_for_theme(theme_id: UiThemeId) -> (u16, u32, u16, u32) {
19006        let (mut disp, mut cpu, mut bus) = setup();
19007        disp.set_ui_theme_id(theme_id);
19008        let dialog_ptr = bus.alloc(170);
19009        let event_ptr = 0x300000u32;
19010        let dialog_out_ptr = 0x300100u32;
19011        let item_hit_ptr = 0x300104u32;
19012
19013        disp.front_window = dialog_ptr;
19014        bus.write_word(dialog_ptr + 8, (-100i16) as u16);
19015        bus.write_word(dialog_ptr + 10, (-200i16) as u16);
19016        bus.write_word(dialog_ptr + 16, 0);
19017        bus.write_word(dialog_ptr + 18, 0);
19018        bus.write_word(dialog_ptr + 20, 100);
19019        bus.write_word(dialog_ptr + 22, 160);
19020        disp.dialog_items.insert(
19021            dialog_ptr,
19022            vec![DialogItem {
19023                item_type: 0,
19024                rect: (20, 30, 60, 110),
19025                text: String::new(),
19026                resource_id: 0,
19027                proc_ptr: 0,
19028                sel_start: 0,
19029                sel_end: 0,
19030            }],
19031        );
19032
19033        bus.write_word(event_ptr, 1);
19034        bus.write_long(event_ptr + 2, 0);
19035        bus.write_long(event_ptr + 6, 0);
19036        bus.write_word(event_ptr + 10, 130);
19037        bus.write_word(event_ptr + 12, 240);
19038        bus.write_word(event_ptr + 14, 0x0080);
19039        bus.write_long(TEST_SP, item_hit_ptr);
19040        bus.write_long(TEST_SP + 4, dialog_out_ptr);
19041        bus.write_long(TEST_SP + 8, event_ptr);
19042
19043        disp.dispatch_dialog(true, 0x180, &mut cpu, &mut bus)
19044            .unwrap()
19045            .unwrap();
19046
19047        (
19048            bus.read_word(TEST_SP + 12),
19049            bus.read_long(dialog_out_ptr),
19050            bus.read_word(item_hit_ptr),
19051            cpu.read_reg(Register::A7),
19052        )
19053    }
19054
19055    fn dialog_select_enabled_checkbox_hit_for_theme(
19056        theme_id: UiThemeId,
19057    ) -> (u16, u32, u16, u32, u16, u16) {
19058        let (mut disp, mut cpu, mut bus) = setup();
19059        disp.set_ui_theme_id(theme_id);
19060        let dialog_ptr = bus.alloc(170);
19061        let event_ptr = 0x300000u32;
19062        let dialog_out_ptr = 0x300100u32;
19063        let item_hit_ptr = 0x300104u32;
19064        let box_ptr = 0x300108u32;
19065        let item_ptr = 0x300110u32;
19066        let type_ptr = 0x300114u32;
19067
19068        disp.front_window = dialog_ptr;
19069        bus.write_word(dialog_ptr + 8, 0);
19070        bus.write_word(dialog_ptr + 10, 0);
19071        bus.write_word(dialog_ptr + 16, 0);
19072        bus.write_word(dialog_ptr + 18, 0);
19073        bus.write_word(dialog_ptr + 20, 100);
19074        bus.write_word(dialog_ptr + 22, 160);
19075        disp.dialog_items.insert(
19076            dialog_ptr,
19077            vec![DialogItem {
19078                item_type: 5,
19079                rect: (20, 30, 40, 130),
19080                text: "Enabled".to_string(),
19081                resource_id: 0,
19082                proc_ptr: 0,
19083                sel_start: 0,
19084                sel_end: 0,
19085            }],
19086        );
19087        disp.dialog_control_values.insert((dialog_ptr, 1), 1);
19088
19089        bus.write_long(TEST_SP, box_ptr);
19090        bus.write_long(TEST_SP + 4, item_ptr);
19091        bus.write_long(TEST_SP + 8, type_ptr);
19092        bus.write_word(TEST_SP + 12, 1);
19093        bus.write_long(TEST_SP + 14, dialog_ptr);
19094        disp.dispatch_dialog(true, 0x18D, &mut cpu, &mut bus)
19095            .unwrap()
19096            .unwrap();
19097        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 18);
19098        let ctrl_handle = bus.read_long(item_ptr);
19099        let ctrl_ptr = bus.read_long(ctrl_handle);
19100        assert_ne!(ctrl_ptr, 0);
19101        assert_eq!(bus.read_word(type_ptr), 5);
19102        assert_eq!(bus.read_word(ctrl_ptr + 18), 1);
19103
19104        cpu.write_reg(Register::A7, TEST_SP);
19105        bus.write_word(event_ptr, 1);
19106        bus.write_long(event_ptr + 2, 0);
19107        bus.write_long(event_ptr + 6, 0);
19108        bus.write_word(event_ptr + 10, 30);
19109        bus.write_word(event_ptr + 12, 40);
19110        bus.write_word(event_ptr + 14, 0x0080);
19111        bus.write_long(TEST_SP, item_hit_ptr);
19112        bus.write_long(TEST_SP + 4, dialog_out_ptr);
19113        bus.write_long(TEST_SP + 8, event_ptr);
19114
19115        disp.dispatch_dialog(true, 0x180, &mut cpu, &mut bus)
19116            .unwrap()
19117            .unwrap();
19118
19119        (
19120            bus.read_word(TEST_SP + 12),
19121            bus.read_long(dialog_out_ptr),
19122            bus.read_word(item_hit_ptr),
19123            cpu.read_reg(Register::A7),
19124            bus.read_word(ctrl_ptr + 18),
19125            disp.dialog_control_values
19126                .get(&(dialog_ptr, 1))
19127                .copied()
19128                .unwrap_or(-1) as u16,
19129        )
19130    }
19131
19132    fn findditem_results_for_theme(theme_id: UiThemeId) -> Vec<(i16, u32)> {
19133        let (mut disp, mut cpu, mut bus) = setup();
19134        disp.set_ui_theme_id(theme_id);
19135        let dialog_ptr = 0x220400u32;
19136        disp.dialog_items.insert(
19137            dialog_ptr,
19138            vec![
19139                DialogItem {
19140                    item_type: 0x80 | 4,
19141                    rect: (10, 20, 40, 60),
19142                    text: "DisabledFirst".into(),
19143                    resource_id: 0,
19144                    proc_ptr: 0,
19145                    sel_start: 0,
19146                    sel_end: 0,
19147                },
19148                DialogItem {
19149                    item_type: 4,
19150                    rect: (20, 30, 55, 80),
19151                    text: "EnabledSecond".into(),
19152                    resource_id: 0,
19153                    proc_ptr: 0,
19154                    sel_start: 0,
19155                    sel_end: 0,
19156                },
19157                DialogItem {
19158                    item_type: 16,
19159                    rect: (60, 16384 + 20, 80, 16384 + 100),
19160                    text: "HiddenEdit".into(),
19161                    resource_id: 0,
19162                    proc_ptr: 0,
19163                    sel_start: 0,
19164                    sel_end: 0,
19165                },
19166            ],
19167        );
19168
19169        let mut out = Vec::new();
19170        for (pt_v, pt_h) in [(25i16, 35i16), (45, 70), (65, 40), (90, 110)] {
19171            cpu.write_reg(Register::A7, TEST_SP);
19172            bus.write_word(TEST_SP, pt_v as u16);
19173            bus.write_word(TEST_SP + 2, pt_h as u16);
19174            bus.write_long(TEST_SP + 4, dialog_ptr);
19175            bus.write_word(TEST_SP + 8, 0xBEEF);
19176            disp.dispatch_dialog(true, 0x184, &mut cpu, &mut bus)
19177                .unwrap()
19178                .unwrap();
19179            out.push((
19180                bus.read_word(TEST_SP + 8) as i16,
19181                cpu.read_reg(Register::A7),
19182            ));
19183        }
19184        out
19185    }
19186
19187    #[derive(Debug, PartialEq, Eq)]
19188    struct DItemGetSnapshot {
19189        item_type: u16,
19190        item: u32,
19191        rect: (u16, u16, u16, u16),
19192        stack_after: u32,
19193    }
19194
19195    #[derive(Debug, PartialEq, Eq)]
19196    struct DItemUserItemRecordSnapshot {
19197        initial_get: DItemGetSnapshot,
19198        set_stack_after: u32,
19199        stored_type: u8,
19200        stored_proc_ptr: u32,
19201        stored_rect: (i16, i16, i16, i16),
19202        post_set_get: DItemGetSnapshot,
19203    }
19204
19205    #[derive(Debug, PartialEq, Eq)]
19206    struct DItemTextControlRecordSnapshot {
19207        text_initial_get: DItemGetSnapshot,
19208        text_set_stack_after: u32,
19209        text_old_handle_mapped_after: bool,
19210        text_new_handle_map_value_after: Option<(u32, usize)>,
19211        text_ditl_handle_after: u32,
19212        text_ditl_rect_after: (u16, u16, u16, u16),
19213        text_ditl_type_after: u8,
19214        text_stored_type_after: u8,
19215        text_stored_rect_after: (i16, i16, i16, i16),
19216        text_cached_text_after: String,
19217        text_handle_bytes_after: Vec<u8>,
19218        text_post_set_get: DItemGetSnapshot,
19219        control_initial_get: DItemGetSnapshot,
19220        control_set_stack_after: u32,
19221        control_old_handle_mapped_after: bool,
19222        control_new_handle_map_value_after: Option<(u32, i16)>,
19223        control_ditl_handle_after: u32,
19224        control_ditl_rect_after: (u16, u16, u16, u16),
19225        control_ditl_type_after: u8,
19226        control_stored_type_after: u8,
19227        control_stored_rect_after: (i16, i16, i16, i16),
19228        control_record_rect_after: (u16, u16, u16, u16),
19229        control_value_after: Option<i16>,
19230        control_post_set_get: DItemGetSnapshot,
19231    }
19232
19233    #[derive(Debug, PartialEq, Eq)]
19234    struct DItemVisibilitySnapshot {
19235        initial_get: DItemGetSnapshot,
19236        hide_stack_after: u32,
19237        hidden_item_rect: (i16, i16, i16, i16),
19238        hidden_saved_rect: Option<(i16, i16, i16, i16)>,
19239        hidden_control_rect: (u16, u16, u16, u16),
19240        hidden_get: DItemGetSnapshot,
19241        hidden_find: (i16, u32),
19242        show_stack_after: u32,
19243        restored_item_rect: (i16, i16, i16, i16),
19244        restored_saved_rect: Option<(i16, i16, i16, i16)>,
19245        restored_control_rect: (u16, u16, u16, u16),
19246        restored_get: DItemGetSnapshot,
19247        restored_find: (i16, u32),
19248    }
19249
19250    #[derive(Debug, PartialEq, Eq)]
19251    struct ParamTextSnapshot {
19252        initial_slots: [Vec<u8>; 4],
19253        initial_da_handles: [u32; 4],
19254        initial_da_strings: [Vec<u8>; 4],
19255        initial_stack_after: u32,
19256        initial_substitution: String,
19257        nil_slots: [Vec<u8>; 4],
19258        nil_da_handles: [u32; 4],
19259        nil_da_strings: [Vec<u8>; 4],
19260        nil_stack_after: u32,
19261        nil_substitution: String,
19262    }
19263
19264    #[derive(Debug, PartialEq, Eq)]
19265    struct DialogItemTextSnapshot {
19266        initial_get_text: Vec<u8>,
19267        initial_get_stack_after: u32,
19268        set_stack_after: u32,
19269        handle_size_after_set: Option<u32>,
19270        handle_bytes_after_set: Vec<u8>,
19271        cached_text_after_set: String,
19272        post_set_get_text: Vec<u8>,
19273        post_set_get_stack_after: u32,
19274    }
19275
19276    #[derive(Debug, PartialEq, Eq)]
19277    struct DialogItemTextSelectionSnapshot {
19278        whole_selection: (i16, i16),
19279        whole_edit_field: u16,
19280        whole_te_selection: (u16, u16),
19281        whole_stack_after: u32,
19282        normalized_selection: (i16, i16),
19283        normalized_edit_field: u16,
19284        normalized_te_selection: (u16, u16),
19285        normalized_stack_after: u32,
19286        non_edit_selection: (i16, i16),
19287        non_edit_edit_field: u16,
19288        non_edit_te_selection: (u16, u16),
19289        non_edit_stack_after: u32,
19290    }
19291
19292    #[derive(Debug, PartialEq, Eq)]
19293    struct IsDialogEventSnapshot {
19294        null_event: (u16, u32),
19295        key_down: (u16, u32),
19296        mouse_inside: (u16, u32),
19297        mouse_outside: (u16, u32),
19298        update_target: (u16, u32),
19299        activate_target: (u16, u32),
19300    }
19301
19302    #[derive(Debug, PartialEq, Eq)]
19303    struct DialogSelectWindowEventSnapshot {
19304        dialog_ptr: u32,
19305        update_result: u16,
19306        update_dialog_out: u32,
19307        update_item_hit: u16,
19308        update_stack_after: u32,
19309        update_deferred_after: bool,
19310        update_region_after: Option<(i16, i16, i16, i16)>,
19311        update_vis_region_after: Option<(i16, i16, i16, i16)>,
19312        update_the_port_after: u32,
19313        update_current_port_after: u32,
19314        update_saved_vis_after: bool,
19315        update_queued_draw_procs: Vec<(u32, u32, i16)>,
19316        update_item_count_after: usize,
19317        activate_result: u16,
19318        activate_dialog_out: u32,
19319        activate_item_hit: u16,
19320        activate_stack_after: u32,
19321    }
19322
19323    #[derive(Debug, PartialEq, Eq)]
19324    struct DialogSelectEditTextMouseSnapshot {
19325        mouse_result: u16,
19326        mouse_dialog_out: u32,
19327        mouse_item_hit: u16,
19328        mouse_stack_after: u32,
19329        mouse_edit_field: u16,
19330        mouse_item_selection: (i16, i16),
19331        mouse_te_text: Vec<u8>,
19332        mouse_te_length: u16,
19333        mouse_te_selection: (u16, u16),
19334        null_result: u16,
19335        null_dialog_out: u32,
19336        null_item_hit: u16,
19337        null_stack_after: u32,
19338        null_edit_field: u16,
19339        null_te_text: Vec<u8>,
19340        null_te_length: u16,
19341        null_te_selection: (u16, u16),
19342    }
19343
19344    #[derive(Debug, PartialEq, Eq)]
19345    struct ModalDialogEditTextMouseSnapshot {
19346        item_hit: u16,
19347        stack_after: u32,
19348        tracking_finished: bool,
19349        retained_visible_snapshot: bool,
19350        saved_background_retained: bool,
19351        queued_mouse_up_consumed: bool,
19352        edit_field: u16,
19353        item_selection: (i16, i16),
19354        te_text: Vec<u8>,
19355        te_length: u16,
19356        te_selection: (u16, u16),
19357        handle_bytes: Vec<u8>,
19358    }
19359
19360    #[derive(Debug, PartialEq, Eq)]
19361    struct ModalDialogKeyboardButtonSnapshot {
19362        return_key: ModalDialogKeyboardButtonCase,
19363        enter_key: ModalDialogKeyboardButtonCase,
19364        escape_key: ModalDialogKeyboardButtonCase,
19365        command_period: ModalDialogKeyboardButtonCase,
19366    }
19367
19368    #[derive(Debug, PartialEq, Eq)]
19369    struct ModalDialogKeyboardButtonCase {
19370        flash_item_after_key: i16,
19371        stack_after_key: u32,
19372        item_hit_after_key: u16,
19373        item_hit_after_flash: u16,
19374        stack_after_flash: u32,
19375        tracking_finished: bool,
19376    }
19377
19378    #[derive(Debug, PartialEq, Eq)]
19379    struct ModalDialogUpdateEventSnapshot {
19380        dialog_ptr: u32,
19381        item_hit_after: u16,
19382        stack_after: u32,
19383        tracking_finished: bool,
19384        rendered_pixels_final: bool,
19385        retained_pixel_after: u8,
19386        retained_snapshot_pixel_after: u8,
19387        update_region_after: Option<(i16, i16, i16, i16)>,
19388        vis_region_after: Option<(i16, i16, i16, i16)>,
19389        the_port_after: u32,
19390        current_port_after: u32,
19391        saved_vis_after: bool,
19392    }
19393
19394    #[derive(Debug, PartialEq, Eq)]
19395    struct DialogLifecycleCleanupSnapshot {
19396        close_stack_after: u32,
19397        close_tracking_cleared: bool,
19398        close_retained_click_cleared: bool,
19399        close_visible_snapshot_cleared: bool,
19400        close_modal_entered_cleared: bool,
19401        close_saved_pixels_cleared: bool,
19402        close_window_list_contains_dialog: bool,
19403        close_front_window_after: u32,
19404        close_the_port_after: u32,
19405        close_current_port_after: u32,
19406        close_update_event_for_previous_window: bool,
19407        close_dialog_items_present_after: bool,
19408        dispose_stack_after: u32,
19409        dispose_tracking_cleared: bool,
19410        dispose_retained_click_cleared: bool,
19411        dispose_visible_snapshot_cleared: bool,
19412        dispose_modal_entered_cleared: bool,
19413        dispose_saved_pixels_cleared: bool,
19414        dispose_window_list_contains_dialog: bool,
19415        dispose_front_window_after: u32,
19416        dispose_the_port_after: u32,
19417        dispose_current_port_after: u32,
19418        dispose_update_event_for_previous_window: bool,
19419        dispose_dialog_items_present_after: bool,
19420        dispose_other_dialog_items_present_after: bool,
19421        dispose_dialog_item_handle_present_after: bool,
19422        dispose_other_dialog_item_handle_present_after: bool,
19423        dispose_dialog_control_handle_present_after: bool,
19424        dispose_other_dialog_control_handle_present_after: bool,
19425        dispose_dialog_control_value_present_after: bool,
19426        dispose_other_dialog_control_value_present_after: bool,
19427        dispose_hidden_rect_present_after: bool,
19428        dispose_other_hidden_rect_present_after: bool,
19429        dispose_cancel_item_present_after: bool,
19430        dispose_other_cancel_item_present_after: bool,
19431        dispose_pending_popup_cleared: bool,
19432    }
19433
19434    #[derive(Debug, PartialEq, Eq)]
19435    struct DialogCreationSnapshot {
19436        new_dialog_ptr: u32,
19437        new_stack_after: u32,
19438        new_result_slot: u32,
19439        new_window_list: Vec<u32>,
19440        new_front_window: u32,
19441        new_current_port: u32,
19442        new_the_port: u32,
19443        new_visible_byte: u8,
19444        new_goaway_byte: u8,
19445        new_refcon: u32,
19446        new_window_kind: i16,
19447        new_proc_id: i16,
19448        new_port_rect: (i16, i16, i16, i16),
19449        new_items_handle: u32,
19450        new_items_data_ptr: u32,
19451        new_first_item_handle_nonzero: bool,
19452        new_cached_items: Vec<(u8, (i16, i16, i16, i16), String, i16)>,
19453        new_update_region: Option<(i16, i16, i16, i16)>,
19454        new_update_event_queued: bool,
19455        new_edit_field: u16,
19456        new_default_item: u16,
19457        get_dialog_ptr: u32,
19458        get_stack_after: u32,
19459        get_result_slot: u32,
19460        get_window_list: Vec<u32>,
19461        get_front_window: u32,
19462        get_current_port: u32,
19463        get_the_port: u32,
19464        get_visible_byte: u8,
19465        get_refcon: u32,
19466        get_window_kind: i16,
19467        get_proc_id: i16,
19468        get_port_rect: (i16, i16, i16, i16),
19469        get_items_handle: u32,
19470        get_items_data_ptr: u32,
19471        get_uses_distinct_ditl_copy: bool,
19472        get_original_ditl_handle_field: u32,
19473        get_first_item_handle_nonzero: bool,
19474        get_cached_items: Vec<(u8, (i16, i16, i16, i16), String, i16)>,
19475        get_update_region: Option<(i16, i16, i16, i16)>,
19476        get_update_event_queued: bool,
19477        get_edit_field: u16,
19478        get_default_item: u16,
19479    }
19480
19481    #[derive(Debug, PartialEq, Eq)]
19482    struct AlertTrapCallSnapshot {
19483        trap_word: u16,
19484        result: i16,
19485        stack_after: u32,
19486        alert_stage_after: u16,
19487        anumber_after: u16,
19488    }
19489
19490    #[derive(Debug, PartialEq, Eq)]
19491    struct AlertResourcePrepSnapshot {
19492        could_present: (i16, u32),
19493        free_present: (i16, u32),
19494        could_missing: (i16, u32),
19495        free_missing: (i16, u32),
19496    }
19497
19498    #[derive(Debug, PartialEq, Eq)]
19499    struct AlertFamilySnapshot {
19500        staged_calls: Vec<AlertTrapCallSnapshot>,
19501        missing_call: AlertTrapCallSnapshot,
19502        resource_prep: AlertResourcePrepSnapshot,
19503    }
19504
19505    #[derive(Debug, PartialEq, Eq)]
19506    struct DialogInitSoundSnapshot {
19507        init_stack_after: u32,
19508        resume_proc_after_init: u32,
19509        da_beeper_after_init: u32,
19510        alert_stage_after_init: u16,
19511        anumber_after_init: u16,
19512        da_strings_after_init: [u32; 4],
19513        error_sound_stack_after: u32,
19514        da_beeper_after_error_sound: u32,
19515        nil_error_sound_stack_after: u32,
19516        da_beeper_after_nil_error_sound: u32,
19517    }
19518
19519    #[derive(Debug, PartialEq, Eq)]
19520    struct DialogDispatchDefaultCancelSnapshot {
19521        default_result: u16,
19522        default_stack_after: u32,
19523        default_adef_item: u16,
19524        default_tracking_item: i16,
19525        cancel_result: u16,
19526        cancel_stack_after: u32,
19527        cancel_map_item: Option<i16>,
19528        cancel_tracking_item: i16,
19529        tracks_result: u16,
19530        tracks_stack_after: u32,
19531        tracks_adef_item: u16,
19532        tracks_cancel_map_item: Option<i16>,
19533        tracks_tracking_items: (i16, i16),
19534    }
19535
19536    #[derive(Debug, PartialEq, Eq)]
19537    struct UpdtDialogUpdateRegionSnapshot {
19538        stack_after: u32,
19539        the_port_after: u32,
19540        current_port_after: u32,
19541        deferred_after: bool,
19542        queued_draw_procs: Vec<(u32, u32, i16)>,
19543        item_count_after: usize,
19544        inside_update_pixel_after: u8,
19545        outside_update_pixel_after: u8,
19546        outside_item_pixel_after: u8,
19547    }
19548
19549    #[derive(Debug, PartialEq, Eq)]
19550    struct DialogSelectEditTextKeySnapshot {
19551        keydown_result: u16,
19552        keydown_dialog_out: u32,
19553        keydown_item_hit: u16,
19554        keydown_stack_after: u32,
19555        keydown_handle_bytes: Vec<u8>,
19556        keydown_cached_text: String,
19557        keydown_item_selection: (i16, i16),
19558        keydown_te_text: Vec<u8>,
19559        keydown_te_length: u16,
19560        keydown_te_selection: (u16, u16),
19561        autokey_result: u16,
19562        autokey_dialog_out: u32,
19563        autokey_item_hit: u16,
19564        autokey_stack_after: u32,
19565        autokey_handle_bytes: Vec<u8>,
19566        autokey_cached_text: String,
19567        autokey_item_selection: (i16, i16),
19568        autokey_te_text: Vec<u8>,
19569        autokey_te_length: u16,
19570        autokey_te_selection: (u16, u16),
19571    }
19572
19573    #[derive(Debug, PartialEq, Eq)]
19574    struct TextEditScrapEditCase {
19575        text: Vec<u8>,
19576        selection: (u16, u16),
19577        scrap_length: u16,
19578        scrap_bytes: Vec<u8>,
19579        stack_after: u32,
19580    }
19581
19582    #[derive(Debug, PartialEq, Eq)]
19583    struct TextEditPrivateScrapEditingSnapshot {
19584        copy: TextEditScrapEditCase,
19585        cut: TextEditScrapEditCase,
19586        paste: TextEditScrapEditCase,
19587        delete: TextEditScrapEditCase,
19588    }
19589
19590    fn paramtext_da_strings(bus: &MacMemoryBus) -> ([u32; 4], [Vec<u8>; 4]) {
19591        let mut handles = [0u32; 4];
19592        let mut strings: [Vec<u8>; 4] = std::array::from_fn(|_| Vec::new());
19593        for i in 0..4 {
19594            let handle = bus.read_long(crate::memory::globals::addr::DA_STRINGS + i as u32 * 4);
19595            handles[i] = handle;
19596            if handle != 0 {
19597                let data_ptr = bus.read_long(handle);
19598                if data_ptr != 0 {
19599                    strings[i] = bus.read_pstring(data_ptr);
19600                }
19601            }
19602        }
19603        (handles, strings)
19604    }
19605
19606    fn paramtext_results_for_theme(theme_id: UiThemeId) -> ParamTextSnapshot {
19607        let (mut disp, mut cpu, mut bus) = setup();
19608        disp.set_ui_theme_id(theme_id);
19609        let p0 = bus.alloc(32);
19610        let p1 = bus.alloc(32);
19611        let p2 = bus.alloc(32);
19612        let p3 = bus.alloc(32);
19613
19614        bus.write_pstring(p0, b"Doc");
19615        bus.write_pstring(p1, b"42");
19616        bus.write_pstring(p2, b"");
19617        bus.write_pstring(p3, b"Tail");
19618
19619        cpu.write_reg(Register::A7, TEST_SP);
19620        bus.write_long(TEST_SP, p3);
19621        bus.write_long(TEST_SP + 4, p2);
19622        bus.write_long(TEST_SP + 8, p1);
19623        bus.write_long(TEST_SP + 12, p0);
19624        disp.dispatch_dialog(true, 0x18B, &mut cpu, &mut bus)
19625            .unwrap()
19626            .unwrap();
19627
19628        let initial_slots = [
19629            disp.param_text[0].clone(),
19630            disp.param_text[1].clone(),
19631            disp.param_text[2].clone(),
19632            disp.param_text[3].clone(),
19633        ];
19634        let (initial_da_handles, initial_da_strings) = paramtext_da_strings(&bus);
19635        let initial_stack_after = cpu.read_reg(Register::A7);
19636        let initial_substitution = disp
19637            .apply_param_text("Cannot open ^0 (^1)^2^3 ^9")
19638            .into_owned();
19639
19640        let new0 = bus.alloc(32);
19641        bus.write_pstring(new0, b"NewDoc");
19642        cpu.write_reg(Register::A7, TEST_SP);
19643        bus.write_long(TEST_SP, 0); // param3 = NIL; preserve previous slot
19644        bus.write_long(TEST_SP + 4, 0); // param2 = NIL; preserve previous slot
19645        bus.write_long(TEST_SP + 8, 0); // param1 = NIL; preserve previous slot
19646        bus.write_long(TEST_SP + 12, new0);
19647        disp.dispatch_dialog(true, 0x18B, &mut cpu, &mut bus)
19648            .unwrap()
19649            .unwrap();
19650
19651        let nil_slots = [
19652            disp.param_text[0].clone(),
19653            disp.param_text[1].clone(),
19654            disp.param_text[2].clone(),
19655            disp.param_text[3].clone(),
19656        ];
19657        let (nil_da_handles, nil_da_strings) = paramtext_da_strings(&bus);
19658        let nil_stack_after = cpu.read_reg(Register::A7);
19659        let nil_substitution = disp
19660            .apply_param_text("Cannot open ^0 (^1)^2^3 ^9")
19661            .into_owned();
19662
19663        ParamTextSnapshot {
19664            initial_slots,
19665            initial_da_handles,
19666            initial_da_strings,
19667            initial_stack_after,
19668            initial_substitution,
19669            nil_slots,
19670            nil_da_handles,
19671            nil_da_strings,
19672            nil_stack_after,
19673            nil_substitution,
19674        }
19675    }
19676
19677    fn dialog_item_text_results_for_theme(theme_id: UiThemeId) -> DialogItemTextSnapshot {
19678        let (mut disp, mut cpu, mut bus) = setup();
19679        disp.set_ui_theme_id(theme_id);
19680        let dialog_ptr = bus.alloc(170);
19681        let text_handle = bus.alloc(4);
19682        let text_ptr = bus.alloc(3);
19683        let out_ptr = bus.alloc(256);
19684        let new_text_ptr = bus.alloc(32);
19685
19686        bus.write_long(text_handle, text_ptr);
19687        bus.write_bytes(text_ptr, b"Old");
19688        disp.dialog_items.insert(
19689            dialog_ptr,
19690            vec![DialogItem {
19691                item_type: 16,
19692                rect: (10, 20, 30, 120),
19693                text: "Old".to_string(),
19694                resource_id: 0,
19695                proc_ptr: 0,
19696                sel_start: 0,
19697                sel_end: 0,
19698            }],
19699        );
19700        disp.dialog_item_handles
19701            .insert(text_handle, (dialog_ptr, 0));
19702
19703        cpu.write_reg(Register::A7, TEST_SP);
19704        bus.write_long(TEST_SP, out_ptr);
19705        bus.write_long(TEST_SP + 4, text_handle);
19706        disp.dispatch_dialog(true, 0x190, &mut cpu, &mut bus)
19707            .unwrap()
19708            .unwrap();
19709        let initial_get_text = bus.read_pstring(out_ptr);
19710        let initial_get_stack_after = cpu.read_reg(Register::A7);
19711
19712        bus.write_pstring(new_text_ptr, b"Edited Text");
19713        cpu.write_reg(Register::A7, TEST_SP);
19714        bus.write_long(TEST_SP, new_text_ptr);
19715        bus.write_long(TEST_SP + 4, text_handle);
19716        disp.dispatch_dialog(true, 0x18F, &mut cpu, &mut bus)
19717            .unwrap()
19718            .unwrap();
19719        let set_stack_after = cpu.read_reg(Register::A7);
19720        let data_ptr_after_set = bus.read_long(text_handle);
19721        let handle_size_after_set = bus.get_alloc_size(data_ptr_after_set);
19722        let handle_bytes_after_set = bus.read_bytes(
19723            data_ptr_after_set,
19724            handle_size_after_set.unwrap_or(0) as usize,
19725        );
19726        let cached_text_after_set = disp.dialog_items.get(&dialog_ptr).unwrap()[0].text.clone();
19727
19728        cpu.write_reg(Register::A7, TEST_SP);
19729        bus.write_long(TEST_SP, out_ptr);
19730        bus.write_long(TEST_SP + 4, text_handle);
19731        disp.dispatch_dialog(true, 0x190, &mut cpu, &mut bus)
19732            .unwrap()
19733            .unwrap();
19734        let post_set_get_text = bus.read_pstring(out_ptr);
19735        let post_set_get_stack_after = cpu.read_reg(Register::A7);
19736
19737        DialogItemTextSnapshot {
19738            initial_get_text,
19739            initial_get_stack_after,
19740            set_stack_after,
19741            handle_size_after_set,
19742            handle_bytes_after_set,
19743            cached_text_after_set,
19744            post_set_get_text,
19745            post_set_get_stack_after,
19746        }
19747    }
19748
19749    fn select_dialog_item_text_results_for_theme(
19750        theme_id: UiThemeId,
19751    ) -> DialogItemTextSelectionSnapshot {
19752        let (mut disp, mut cpu, mut bus) = setup();
19753        disp.set_ui_theme_id(theme_id);
19754        let dialog_ptr = bus.alloc(170);
19755        let text_handle = bus.alloc(4);
19756        let te_ptr = bus.alloc(0x40);
19757
19758        bus.write_long(dialog_ptr + 160, text_handle);
19759        bus.write_long(text_handle, te_ptr);
19760        bus.write_word(dialog_ptr + 164, 0xFFFF);
19761        disp.dialog_items.insert(
19762            dialog_ptr,
19763            vec![
19764                DialogItem {
19765                    item_type: 16,
19766                    rect: (10, 20, 30, 120),
19767                    text: "ABCDE".to_string(),
19768                    resource_id: 0,
19769                    proc_ptr: 0,
19770                    sel_start: 1,
19771                    sel_end: 1,
19772                },
19773                DialogItem {
19774                    item_type: 8,
19775                    rect: (34, 20, 50, 120),
19776                    text: "STATIC".to_string(),
19777                    resource_id: 0,
19778                    proc_ptr: 0,
19779                    sel_start: 1,
19780                    sel_end: 3,
19781                },
19782            ],
19783        );
19784
19785        cpu.write_reg(Register::A7, TEST_SP);
19786        bus.write_word(TEST_SP, 32767); // endSel
19787        bus.write_word(TEST_SP + 2, 0); // strtSel
19788        bus.write_word(TEST_SP + 4, 1); // itemNo
19789        bus.write_long(TEST_SP + 6, dialog_ptr); // theDialog
19790        disp.dispatch_dialog(true, 0x17E, &mut cpu, &mut bus)
19791            .unwrap()
19792            .unwrap();
19793        let item = &disp.dialog_items[&dialog_ptr][0];
19794        let whole_selection = (item.sel_start, item.sel_end);
19795        let whole_edit_field = bus.read_word(dialog_ptr + 164);
19796        let whole_te_selection = (
19797            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
19798            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET),
19799        );
19800        let whole_stack_after = cpu.read_reg(Register::A7);
19801
19802        cpu.write_reg(Register::A7, TEST_SP);
19803        bus.write_word(TEST_SP, 2); // endSel
19804        bus.write_word(TEST_SP + 2, 9); // strtSel, beyond text length
19805        bus.write_word(TEST_SP + 4, 1); // itemNo
19806        bus.write_long(TEST_SP + 6, dialog_ptr); // theDialog
19807        disp.dispatch_dialog(true, 0x17E, &mut cpu, &mut bus)
19808            .unwrap()
19809            .unwrap();
19810        let item = &disp.dialog_items[&dialog_ptr][0];
19811        let normalized_selection = (item.sel_start, item.sel_end);
19812        let normalized_edit_field = bus.read_word(dialog_ptr + 164);
19813        let normalized_te_selection = (
19814            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
19815            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET),
19816        );
19817        let normalized_stack_after = cpu.read_reg(Register::A7);
19818
19819        cpu.write_reg(Register::A7, TEST_SP);
19820        bus.write_word(TEST_SP, 4); // endSel
19821        bus.write_word(TEST_SP + 2, 0); // strtSel
19822        bus.write_word(TEST_SP + 4, 2); // non-edit itemNo
19823        bus.write_long(TEST_SP + 6, dialog_ptr); // theDialog
19824        disp.dispatch_dialog(true, 0x17E, &mut cpu, &mut bus)
19825            .unwrap()
19826            .unwrap();
19827        let item = &disp.dialog_items[&dialog_ptr][1];
19828        let non_edit_selection = (item.sel_start, item.sel_end);
19829        let non_edit_edit_field = bus.read_word(dialog_ptr + 164);
19830        let non_edit_te_selection = (
19831            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
19832            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET),
19833        );
19834        let non_edit_stack_after = cpu.read_reg(Register::A7);
19835
19836        DialogItemTextSelectionSnapshot {
19837            whole_selection,
19838            whole_edit_field,
19839            whole_te_selection,
19840            whole_stack_after,
19841            normalized_selection,
19842            normalized_edit_field,
19843            normalized_te_selection,
19844            normalized_stack_after,
19845            non_edit_selection,
19846            non_edit_edit_field,
19847            non_edit_te_selection,
19848            non_edit_stack_after,
19849        }
19850    }
19851
19852    fn is_dialog_event_results_for_theme(theme_id: UiThemeId) -> IsDialogEventSnapshot {
19853        fn dispatch_is_dialog_event(
19854            disp: &mut TrapDispatcher,
19855            cpu: &mut impl CpuOps,
19856            bus: &mut MacMemoryBus,
19857            event_ptr: u32,
19858            what: u16,
19859            message: u32,
19860            where_v: i16,
19861            where_h: i16,
19862        ) -> (u16, u32) {
19863            cpu.write_reg(Register::A7, TEST_SP);
19864            bus.write_word(event_ptr, what);
19865            bus.write_long(event_ptr + 2, message);
19866            bus.write_long(event_ptr + 6, 0);
19867            bus.write_word(event_ptr + 10, where_v as u16);
19868            bus.write_word(event_ptr + 12, where_h as u16);
19869            bus.write_word(event_ptr + 14, 0);
19870            bus.write_word(TEST_SP + 4, 0xBEEF);
19871            bus.write_long(TEST_SP, event_ptr);
19872
19873            disp.dispatch_dialog(true, 0x17F, cpu, bus)
19874                .unwrap()
19875                .unwrap();
19876            (bus.read_word(TEST_SP + 4), cpu.read_reg(Register::A7))
19877        }
19878
19879        let (mut disp, mut cpu, mut bus) = setup();
19880        disp.set_ui_theme_id(theme_id);
19881        let dialog_ptr = bus.alloc(170);
19882        let event_ptr = 0x300000u32;
19883
19884        disp.front_window = dialog_ptr;
19885        bus.write_word(dialog_ptr + 8, (-100i16) as u16);
19886        bus.write_word(dialog_ptr + 10, (-200i16) as u16);
19887        bus.write_word(dialog_ptr + 16, 0);
19888        bus.write_word(dialog_ptr + 18, 0);
19889        bus.write_word(dialog_ptr + 20, 80);
19890        bus.write_word(dialog_ptr + 22, 120);
19891        disp.dialog_items.insert(dialog_ptr, Vec::new());
19892
19893        let null_event =
19894            dispatch_is_dialog_event(&mut disp, &mut cpu, &mut bus, event_ptr, 0, 0, 0, 0);
19895        let key_down = dispatch_is_dialog_event(
19896            &mut disp,
19897            &mut cpu,
19898            &mut bus,
19899            event_ptr,
19900            3,
19901            u32::from(b'A'),
19902            0,
19903            0,
19904        );
19905        let mouse_inside =
19906            dispatch_is_dialog_event(&mut disp, &mut cpu, &mut bus, event_ptr, 1, 0, 120, 240);
19907        let mouse_outside =
19908            dispatch_is_dialog_event(&mut disp, &mut cpu, &mut bus, event_ptr, 1, 0, 40, 40);
19909        let update_target = dispatch_is_dialog_event(
19910            &mut disp, &mut cpu, &mut bus, event_ptr, 6, dialog_ptr, 0, 0,
19911        );
19912        let activate_target = dispatch_is_dialog_event(
19913            &mut disp, &mut cpu, &mut bus, event_ptr, 8, dialog_ptr, 0, 0,
19914        );
19915
19916        IsDialogEventSnapshot {
19917            null_event,
19918            key_down,
19919            mouse_inside,
19920            mouse_outside,
19921            update_target,
19922            activate_target,
19923        }
19924    }
19925
19926    fn text_handle_bytes(bus: &MacMemoryBus, handle: u32) -> Vec<u8> {
19927        let ptr = bus.read_long(handle);
19928        if ptr == 0 {
19929            return Vec::new();
19930        }
19931        let len = bus.get_alloc_size(ptr).unwrap_or(0) as usize;
19932        bus.read_bytes(ptr, len)
19933    }
19934
19935    fn textedit_scrap_contents(bus: &MacMemoryBus) -> (u16, Vec<u8>) {
19936        let scrap_length = bus.read_word(crate::memory::globals::addr::TE_SCRP_LENGTH);
19937        let scrap_handle = bus.read_long(crate::memory::globals::addr::TE_SCRP_HANDLE);
19938        if scrap_length == 0 || scrap_handle == 0 {
19939            return (scrap_length, Vec::new());
19940        }
19941        let scrap_ptr = bus.read_long(scrap_handle);
19942        if scrap_ptr == 0 {
19943            return (scrap_length, Vec::new());
19944        }
19945        (
19946            scrap_length,
19947            bus.read_bytes(scrap_ptr, scrap_length as usize),
19948        )
19949    }
19950
19951    fn textedit_scrap_edit_case(
19952        bus: &MacMemoryBus,
19953        te_handle: u32,
19954        stack_after: u32,
19955    ) -> TextEditScrapEditCase {
19956        let te_ptr = bus.read_long(te_handle);
19957        let (scrap_length, scrap_bytes) = textedit_scrap_contents(bus);
19958        TextEditScrapEditCase {
19959            text: TrapDispatcher::te_text_bytes(bus, te_handle),
19960            selection: (
19961                bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
19962                bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET),
19963            ),
19964            scrap_length,
19965            scrap_bytes,
19966            stack_after,
19967        }
19968    }
19969
19970    fn dialog_select_window_event_results_for_theme(
19971        theme_id: UiThemeId,
19972    ) -> DialogSelectWindowEventSnapshot {
19973        fn install_region(
19974            bus: &mut MacMemoryBus,
19975            handle: u32,
19976            data: u32,
19977            rect: (i16, i16, i16, i16),
19978        ) {
19979            bus.write_long(handle, data);
19980            bus.write_word(data, 10);
19981            bus.write_word(data + 2, rect.0 as u16);
19982            bus.write_word(data + 4, rect.1 as u16);
19983            bus.write_word(data + 6, rect.2 as u16);
19984            bus.write_word(data + 8, rect.3 as u16);
19985        }
19986
19987        fn dispatch_dialog_select(
19988            disp: &mut TrapDispatcher,
19989            cpu: &mut impl CpuOps,
19990            bus: &mut MacMemoryBus,
19991            event_ptr: u32,
19992            dialog_out_ptr: u32,
19993            item_hit_ptr: u32,
19994            what: u16,
19995            message: u32,
19996        ) -> (u16, u32, u16, u32) {
19997            cpu.write_reg(Register::A7, TEST_SP);
19998            bus.write_word(event_ptr, what);
19999            bus.write_long(event_ptr + 2, message);
20000            bus.write_long(event_ptr + 6, 0);
20001            bus.write_word(event_ptr + 10, 0);
20002            bus.write_word(event_ptr + 12, 0);
20003            bus.write_word(event_ptr + 14, 0);
20004            bus.write_word(TEST_SP + 12, 0xBEEF);
20005            bus.write_long(dialog_out_ptr, 0xDEAD_BEEF);
20006            bus.write_word(item_hit_ptr, 0xCAFE);
20007            bus.write_long(TEST_SP, item_hit_ptr);
20008            bus.write_long(TEST_SP + 4, dialog_out_ptr);
20009            bus.write_long(TEST_SP + 8, event_ptr);
20010
20011            disp.dispatch_dialog(true, 0x180, cpu, bus)
20012                .unwrap()
20013                .unwrap();
20014
20015            (
20016                bus.read_word(TEST_SP + 12),
20017                bus.read_long(dialog_out_ptr),
20018                bus.read_word(item_hit_ptr),
20019                cpu.read_reg(Register::A7),
20020            )
20021        }
20022
20023        let (mut disp, mut cpu, mut bus) = setup_with_port();
20024        disp.set_ui_theme_id(theme_id);
20025        let initial_port = 0x181000;
20026        disp.set_current_port_state(&mut bus, &mut cpu, initial_port, None);
20027
20028        let screen_base = bus.alloc(100 * 100);
20029        bus.write_long(0x0824, screen_base);
20030        disp.screen_mode = (screen_base, 100, 100, 100, 8);
20031
20032        let dialog_ptr = bus.alloc(256);
20033        let event_ptr = 0x300000u32;
20034        let dialog_out_ptr = 0x300100u32;
20035        let item_hit_ptr = 0x300104u32;
20036
20037        let vis_rgn = bus.alloc(10);
20038        let vis_rgn_handle = bus.alloc(4);
20039        install_region(&mut bus, vis_rgn_handle, vis_rgn, (0, 0, 100, 100));
20040        let clip_rgn = bus.alloc(10);
20041        let clip_rgn_handle = bus.alloc(4);
20042        install_region(&mut bus, clip_rgn_handle, clip_rgn, (0, 0, 100, 100));
20043        let update_rgn = bus.alloc(10);
20044        let update_rgn_handle = bus.alloc(4);
20045        install_region(&mut bus, update_rgn_handle, update_rgn, (0, 0, 40, 40));
20046
20047        disp.front_window = dialog_ptr;
20048        disp.window_list.push(dialog_ptr);
20049        bus.write_word(dialog_ptr, 0);
20050        bus.write_long(dialog_ptr + 2, screen_base);
20051        bus.write_word(dialog_ptr + 6, 100);
20052        bus.write_word(dialog_ptr + 8, 0);
20053        bus.write_word(dialog_ptr + 10, 0);
20054        bus.write_word(dialog_ptr + 12, 100);
20055        bus.write_word(dialog_ptr + 14, 100);
20056        bus.write_word(dialog_ptr + 16, 0);
20057        bus.write_word(dialog_ptr + 18, 0);
20058        bus.write_word(dialog_ptr + 20, 100);
20059        bus.write_word(dialog_ptr + 22, 100);
20060        bus.write_long(dialog_ptr + 24, vis_rgn_handle);
20061        bus.write_long(dialog_ptr + 28, clip_rgn_handle);
20062        bus.write_word(dialog_ptr + 108, 2);
20063        bus.write_long(dialog_ptr + 122, update_rgn_handle);
20064        disp.dialog_items.insert(
20065            dialog_ptr,
20066            vec![
20067                DialogItem {
20068                    item_type: 0,
20069                    rect: (8, 8, 24, 32),
20070                    proc_ptr: 0x500000,
20071                    ..Default::default()
20072                },
20073                DialogItem {
20074                    item_type: 0,
20075                    rect: (60, 60, 80, 80),
20076                    proc_ptr: 0x600000,
20077                    ..Default::default()
20078                },
20079                DialogItem {
20080                    item_type: 8,
20081                    rect: (90, 90, 96, 96),
20082                    text: "x".to_string(),
20083                    ..Default::default()
20084                },
20085            ],
20086        );
20087        disp.dialog_initial_draw_deferred.insert(dialog_ptr);
20088
20089        let (update_result, update_dialog_out, update_item_hit, update_stack_after) =
20090            dispatch_dialog_select(
20091                &mut disp,
20092                &mut cpu,
20093                &mut bus,
20094                event_ptr,
20095                dialog_out_ptr,
20096                item_hit_ptr,
20097                6,
20098                dialog_ptr,
20099            );
20100        let update_deferred_after = disp.dialog_initial_draw_deferred.contains(&dialog_ptr);
20101        let update_region_after =
20102            TrapDispatcher::region_handle_rect(&bus, bus.read_long(dialog_ptr + 122));
20103        let update_vis_region_after =
20104            TrapDispatcher::region_handle_rect(&bus, bus.read_long(dialog_ptr + 24));
20105        let update_the_port_after = bus.read_long(crate::memory::globals::addr::THE_PORT);
20106        let update_current_port_after = disp.current_port;
20107        let update_saved_vis_after = disp.saved_vis_regions.contains_key(&dialog_ptr);
20108        let update_queued_draw_procs = disp
20109            .modeless_dialog_draw_proc_queue
20110            .iter()
20111            .copied()
20112            .collect();
20113        let update_item_count_after = disp
20114            .dialog_items
20115            .get(&dialog_ptr)
20116            .map(Vec::len)
20117            .unwrap_or_default();
20118
20119        let (activate_result, activate_dialog_out, activate_item_hit, activate_stack_after) =
20120            dispatch_dialog_select(
20121                &mut disp,
20122                &mut cpu,
20123                &mut bus,
20124                event_ptr,
20125                dialog_out_ptr,
20126                item_hit_ptr,
20127                8,
20128                dialog_ptr,
20129            );
20130
20131        DialogSelectWindowEventSnapshot {
20132            dialog_ptr,
20133            update_result,
20134            update_dialog_out,
20135            update_item_hit,
20136            update_stack_after,
20137            update_deferred_after,
20138            update_region_after,
20139            update_vis_region_after,
20140            update_the_port_after,
20141            update_current_port_after,
20142            update_saved_vis_after,
20143            update_queued_draw_procs,
20144            update_item_count_after,
20145            activate_result,
20146            activate_dialog_out,
20147            activate_item_hit,
20148            activate_stack_after,
20149        }
20150    }
20151
20152    fn dialog_select_edit_text_mouse_results_for_theme(
20153        theme_id: UiThemeId,
20154    ) -> DialogSelectEditTextMouseSnapshot {
20155        fn dispatch_dialog_select(
20156            disp: &mut TrapDispatcher,
20157            cpu: &mut impl CpuOps,
20158            bus: &mut MacMemoryBus,
20159            event_ptr: u32,
20160            dialog_out_ptr: u32,
20161            item_hit_ptr: u32,
20162            what: u16,
20163            where_v: i16,
20164            where_h: i16,
20165        ) -> (u16, u32, u16, u32) {
20166            cpu.write_reg(Register::A7, TEST_SP);
20167            bus.write_word(event_ptr, what);
20168            bus.write_long(event_ptr + 2, 0);
20169            bus.write_long(event_ptr + 6, 0);
20170            bus.write_word(event_ptr + 10, where_v as u16);
20171            bus.write_word(event_ptr + 12, where_h as u16);
20172            bus.write_word(event_ptr + 14, 0);
20173            bus.write_word(TEST_SP + 12, 0xBEEF);
20174            bus.write_long(dialog_out_ptr, 0xDEAD_BEEF);
20175            bus.write_word(item_hit_ptr, 0xCAFE);
20176            bus.write_long(TEST_SP, item_hit_ptr);
20177            bus.write_long(TEST_SP + 4, dialog_out_ptr);
20178            bus.write_long(TEST_SP + 8, event_ptr);
20179
20180            disp.dispatch_dialog(true, 0x180, cpu, bus)
20181                .unwrap()
20182                .unwrap();
20183
20184            (
20185                bus.read_word(TEST_SP + 12),
20186                bus.read_long(dialog_out_ptr),
20187                bus.read_word(item_hit_ptr),
20188                cpu.read_reg(Register::A7),
20189            )
20190        }
20191
20192        fn write_ditl_item(
20193            bus: &mut MacMemoryBus,
20194            ditl_ptr: u32,
20195            offset: u32,
20196            handle: u32,
20197            rect: (i16, i16, i16, i16),
20198            item_type: u8,
20199        ) {
20200            bus.write_long(ditl_ptr + offset, handle);
20201            bus.write_word(ditl_ptr + offset + 4, rect.0 as u16);
20202            bus.write_word(ditl_ptr + offset + 6, rect.1 as u16);
20203            bus.write_word(ditl_ptr + offset + 8, rect.2 as u16);
20204            bus.write_word(ditl_ptr + offset + 10, rect.3 as u16);
20205            bus.write_byte(ditl_ptr + offset + 12, item_type);
20206            bus.write_byte(ditl_ptr + offset + 13, 0);
20207        }
20208
20209        let (mut disp, mut cpu, mut bus) = setup();
20210        disp.set_ui_theme_id(theme_id);
20211        let dialog_ptr = bus.alloc(170);
20212        let event_ptr = 0x300000u32;
20213        let dialog_out_ptr = 0x300100u32;
20214        let item_hit_ptr = 0x300104u32;
20215        let items_handle = bus.alloc(4);
20216        let ditl_ptr = bus.alloc(30);
20217        let item1_text_handle = bus.alloc(4);
20218        let item1_text_ptr = bus.alloc(5);
20219        let item2_text_handle = bus.alloc(4);
20220        let item2_text_ptr = bus.alloc(6);
20221        let text_h = TrapDispatcher::allocate_te_handle(&mut bus);
20222
20223        disp.front_window = dialog_ptr;
20224        bus.write_word(dialog_ptr + 8, (-100i16) as u16);
20225        bus.write_word(dialog_ptr + 10, (-200i16) as u16);
20226        bus.write_word(dialog_ptr + 16, 0);
20227        bus.write_word(dialog_ptr + 18, 0);
20228        bus.write_word(dialog_ptr + 20, 100);
20229        bus.write_word(dialog_ptr + 22, 160);
20230        bus.write_long(dialog_ptr + 156, items_handle);
20231        bus.write_long(dialog_ptr + 160, text_h);
20232        bus.write_word(dialog_ptr + 164, 0); // editField = first item
20233
20234        bus.write_long(items_handle, ditl_ptr);
20235        bus.write_word(ditl_ptr, 1); // two items
20236        write_ditl_item(
20237            &mut bus,
20238            ditl_ptr,
20239            2,
20240            item1_text_handle,
20241            (20, 20, 38, 120),
20242            16,
20243        );
20244        write_ditl_item(
20245            &mut bus,
20246            ditl_ptr,
20247            16,
20248            item2_text_handle,
20249            (42, 20, 62, 120),
20250            16,
20251        );
20252        bus.write_long(item1_text_handle, item1_text_ptr);
20253        bus.write_bytes(item1_text_ptr, b"First");
20254        bus.write_long(item2_text_handle, item2_text_ptr);
20255        bus.write_bytes(item2_text_ptr, b"Second");
20256
20257        disp.te_set_text_contents(&mut bus, text_h, b"First");
20258        let te_ptr = bus.read_long(text_h);
20259        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 1);
20260        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 1);
20261        disp.dialog_items.insert(
20262            dialog_ptr,
20263            vec![
20264                DialogItem {
20265                    item_type: 16,
20266                    rect: (20, 20, 38, 120),
20267                    text: "First".to_string(),
20268                    resource_id: 0,
20269                    proc_ptr: 0,
20270                    sel_start: 1,
20271                    sel_end: 1,
20272                },
20273                DialogItem {
20274                    item_type: 16,
20275                    rect: (42, 20, 62, 120),
20276                    text: "Second".to_string(),
20277                    resource_id: 0,
20278                    proc_ptr: 0,
20279                    sel_start: 2,
20280                    sel_end: 4,
20281                },
20282            ],
20283        );
20284
20285        let (mouse_result, mouse_dialog_out, mouse_item_hit, mouse_stack_after) =
20286            dispatch_dialog_select(
20287                &mut disp,
20288                &mut cpu,
20289                &mut bus,
20290                event_ptr,
20291                dialog_out_ptr,
20292                item_hit_ptr,
20293                1,
20294                150,
20295                240,
20296            );
20297        let mouse_edit_field = bus.read_word(dialog_ptr + 164);
20298        let mouse_item = &disp.dialog_items[&dialog_ptr][1];
20299        let mouse_item_selection = (mouse_item.sel_start, mouse_item.sel_end);
20300        let mouse_te_text = TrapDispatcher::te_text_bytes(&bus, text_h);
20301        let mouse_te_length = bus.read_word(te_ptr + TrapDispatcher::TE_LENGTH_OFFSET);
20302        let mouse_te_selection = (
20303            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
20304            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET),
20305        );
20306
20307        let (null_result, null_dialog_out, null_item_hit, null_stack_after) =
20308            dispatch_dialog_select(
20309                &mut disp,
20310                &mut cpu,
20311                &mut bus,
20312                event_ptr,
20313                dialog_out_ptr,
20314                item_hit_ptr,
20315                0,
20316                0,
20317                0,
20318            );
20319        let null_edit_field = bus.read_word(dialog_ptr + 164);
20320        let null_te_text = TrapDispatcher::te_text_bytes(&bus, text_h);
20321        let null_te_length = bus.read_word(te_ptr + TrapDispatcher::TE_LENGTH_OFFSET);
20322        let null_te_selection = (
20323            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
20324            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET),
20325        );
20326
20327        DialogSelectEditTextMouseSnapshot {
20328            mouse_result,
20329            mouse_dialog_out,
20330            mouse_item_hit,
20331            mouse_stack_after,
20332            mouse_edit_field,
20333            mouse_item_selection,
20334            mouse_te_text,
20335            mouse_te_length,
20336            mouse_te_selection,
20337            null_result,
20338            null_dialog_out,
20339            null_item_hit,
20340            null_stack_after,
20341            null_edit_field,
20342            null_te_text,
20343            null_te_length,
20344            null_te_selection,
20345        }
20346    }
20347
20348    fn modal_dialog_edit_text_mouse_results_for_theme(
20349        theme_id: UiThemeId,
20350    ) -> ModalDialogEditTextMouseSnapshot {
20351        let (mut disp, mut cpu, mut bus) = setup();
20352        disp.set_ui_theme_id(theme_id);
20353        let dialog_ptr = bus.alloc(170);
20354        let item_hit_ptr = 0x300100u32;
20355        let items_handle = bus.alloc(4);
20356        let ditl_ptr = bus.alloc(30);
20357        let item1_text_handle = bus.alloc(4);
20358        let item1_text_ptr = bus.alloc(5);
20359        let item2_text_handle = bus.alloc(4);
20360        let item2_text_ptr = bus.alloc(6);
20361        let text_h = TrapDispatcher::allocate_te_handle(&mut bus);
20362
20363        bus.write_word(dialog_ptr + 8, (-100i16) as u16);
20364        bus.write_word(dialog_ptr + 10, (-200i16) as u16);
20365        bus.write_word(dialog_ptr + 16, 0);
20366        bus.write_word(dialog_ptr + 18, 0);
20367        bus.write_word(dialog_ptr + 20, 100);
20368        bus.write_word(dialog_ptr + 22, 160);
20369        bus.write_long(dialog_ptr + 156, items_handle);
20370        bus.write_long(dialog_ptr + 160, text_h);
20371        bus.write_word(dialog_ptr + 164, 0); // editField = first item
20372
20373        bus.write_long(items_handle, ditl_ptr);
20374        bus.write_word(ditl_ptr, 1); // two items
20375        bus.write_long(ditl_ptr + 2, item1_text_handle);
20376        bus.write_word(ditl_ptr + 6, 20);
20377        bus.write_word(ditl_ptr + 8, 20);
20378        bus.write_word(ditl_ptr + 10, 38);
20379        bus.write_word(ditl_ptr + 12, 120);
20380        bus.write_byte(ditl_ptr + 14, 16);
20381        bus.write_byte(ditl_ptr + 15, 0);
20382        bus.write_long(ditl_ptr + 16, item2_text_handle);
20383        bus.write_word(ditl_ptr + 20, 42);
20384        bus.write_word(ditl_ptr + 22, 20);
20385        bus.write_word(ditl_ptr + 24, 62);
20386        bus.write_word(ditl_ptr + 26, 120);
20387        bus.write_byte(ditl_ptr + 28, 16);
20388        bus.write_byte(ditl_ptr + 29, 0);
20389
20390        bus.write_long(item1_text_handle, item1_text_ptr);
20391        bus.write_bytes(item1_text_ptr, b"First");
20392        bus.write_long(item2_text_handle, item2_text_ptr);
20393        bus.write_bytes(item2_text_ptr, b"Second");
20394        disp.te_set_text_contents(&mut bus, text_h, b"First");
20395        let te_ptr = bus.read_long(text_h);
20396        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 1);
20397        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 1);
20398
20399        let items = vec![
20400            DialogItem {
20401                item_type: 16,
20402                rect: (20, 20, 38, 120),
20403                text: "First".to_string(),
20404                resource_id: 0,
20405                proc_ptr: 0,
20406                sel_start: 1,
20407                sel_end: 1,
20408            },
20409            DialogItem {
20410                item_type: 16,
20411                rect: (42, 20, 62, 120),
20412                text: "Second".to_string(),
20413                resource_id: 0,
20414                proc_ptr: 0,
20415                sel_start: 2,
20416                sel_end: 4,
20417            },
20418        ];
20419        disp.dialog_items.insert(dialog_ptr, items.clone());
20420        disp.dialog_tracking = Some(crate::trap::dispatch::DialogTrackingState {
20421            dialog_ptr,
20422            bounds: (100, 200, 200, 360),
20423            title: String::new(),
20424            proc_id: 2,
20425            items,
20426            default_item: 0,
20427            cancel_item: 0,
20428            edit_text: "First".to_string(),
20429            edit_item: 1,
20430            saved_pixels: vec![0x11, 0x22, 0x33],
20431            stack_ptr: TEST_SP,
20432            item_hit_ptr,
20433            rendered_pixels: Vec::new(),
20434            flash_remaining: 0,
20435            flash_delay: 0,
20436            flash_item: 0,
20437            edit_text_modified: false,
20438            draw_proc_queue: VecDeque::new(),
20439            draw_procs_done: true,
20440            rendered_pixels_final: true,
20441            filter_proc: 0,
20442            game_managed: false,
20443            last_filter_event: None,
20444            popup_draws: Vec::new(),
20445            active_popup: None,
20446            active_button: None,
20447            active_user_item: None,
20448        });
20449        bus.write_word(item_hit_ptr, 0xCAFE);
20450        cpu.write_reg(Register::A7, TEST_SP);
20451        disp.mouse_button = true;
20452        disp.mouse_pos = (150, 240);
20453        disp.event_queue
20454            .push_back(crate::trap::dispatch::QueuedEvent {
20455                what: 1,
20456                message: 0,
20457                where_v: 150,
20458                where_h: 240,
20459                modifiers: 0,
20460            });
20461        disp.event_queue
20462            .push_back(crate::trap::dispatch::QueuedEvent {
20463                what: 2,
20464                message: 0,
20465                where_v: 150,
20466                where_h: 240,
20467                modifiers: 0,
20468            });
20469
20470        disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus)
20471            .unwrap()
20472            .unwrap();
20473        let item = &disp.dialog_items[&dialog_ptr][1];
20474
20475        ModalDialogEditTextMouseSnapshot {
20476            item_hit: bus.read_word(item_hit_ptr),
20477            stack_after: cpu.read_reg(Register::A7),
20478            tracking_finished: disp.dialog_tracking.is_none(),
20479            retained_visible_snapshot: disp.dialog_visible_snapshots.contains_key(&dialog_ptr),
20480            saved_background_retained: disp.dialog_saved_pixels.contains_key(&dialog_ptr),
20481            queued_mouse_up_consumed: !disp.event_queue.iter().any(|event| event.what == 2),
20482            edit_field: bus.read_word(dialog_ptr + 164),
20483            item_selection: (item.sel_start, item.sel_end),
20484            te_text: TrapDispatcher::te_text_bytes(&bus, text_h),
20485            te_length: bus.read_word(te_ptr + TrapDispatcher::TE_LENGTH_OFFSET),
20486            te_selection: (
20487                bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
20488                bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET),
20489            ),
20490            handle_bytes: text_handle_bytes(&bus, item2_text_handle),
20491        }
20492    }
20493
20494    fn modal_dialog_keyboard_button_results_for_theme(
20495        theme_id: UiThemeId,
20496    ) -> ModalDialogKeyboardButtonSnapshot {
20497        fn run_key(
20498            theme_id: UiThemeId,
20499            key_code: u8,
20500            char_code: u8,
20501            modifiers: u16,
20502        ) -> ModalDialogKeyboardButtonCase {
20503            let (mut disp, mut cpu, mut bus) = setup_with_port();
20504            disp.set_ui_theme_id(theme_id);
20505            let dialog_ptr = 0x200000u32;
20506            let item_hit_ptr = 0x300000u32;
20507            let items = vec![
20508                DialogItem {
20509                    item_type: 4,
20510                    rect: (20, 30, 60, 110),
20511                    text: "OK".to_string(),
20512                    resource_id: 0,
20513                    proc_ptr: 0,
20514                    sel_start: 0,
20515                    sel_end: 0,
20516                },
20517                DialogItem {
20518                    item_type: 4,
20519                    rect: (20, 124, 60, 214),
20520                    text: "Cancel".to_string(),
20521                    resource_id: 0,
20522                    proc_ptr: 0,
20523                    sel_start: 0,
20524                    sel_end: 0,
20525                },
20526            ];
20527            disp.dialog_tracking = Some(crate::trap::dispatch::DialogTrackingState {
20528                dialog_ptr,
20529                bounds: (100, 200, 200, 440),
20530                title: String::new(),
20531                proc_id: 2,
20532                items,
20533                default_item: 1,
20534                cancel_item: 2,
20535                edit_text: String::new(),
20536                edit_item: 0,
20537                saved_pixels: Vec::new(),
20538                stack_ptr: TEST_SP,
20539                item_hit_ptr,
20540                rendered_pixels: Vec::new(),
20541                flash_remaining: 0,
20542                flash_delay: 0,
20543                flash_item: 0,
20544                edit_text_modified: false,
20545                draw_proc_queue: VecDeque::new(),
20546                draw_procs_done: true,
20547                rendered_pixels_final: true,
20548                filter_proc: 0,
20549                game_managed: false,
20550                last_filter_event: None,
20551                popup_draws: Vec::new(),
20552                active_popup: None,
20553                active_button: None,
20554                active_user_item: None,
20555            });
20556            disp.event_queue
20557                .push_back(crate::trap::dispatch::QueuedEvent {
20558                    what: 3,
20559                    message: (u32::from(key_code) << 8) | u32::from(char_code),
20560                    where_v: 0,
20561                    where_h: 0,
20562                    modifiers,
20563                });
20564            bus.write_word(item_hit_ptr, 0xCAFE);
20565            cpu.write_reg(Register::A7, TEST_SP);
20566
20567            disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus)
20568                .unwrap()
20569                .unwrap();
20570            let flash_item_after_key = disp
20571                .dialog_tracking
20572                .as_ref()
20573                .map(|tracking| tracking.flash_item)
20574                .unwrap_or(0);
20575            let stack_after_key = cpu.read_reg(Register::A7);
20576            let item_hit_after_key = bus.read_word(item_hit_ptr);
20577
20578            {
20579                let tracking = disp.dialog_tracking.as_mut().unwrap();
20580                tracking.flash_remaining = 1;
20581                tracking.flash_delay = 0;
20582            }
20583            disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus)
20584                .unwrap()
20585                .unwrap();
20586
20587            ModalDialogKeyboardButtonCase {
20588                flash_item_after_key,
20589                stack_after_key,
20590                item_hit_after_key,
20591                item_hit_after_flash: bus.read_word(item_hit_ptr),
20592                stack_after_flash: cpu.read_reg(Register::A7),
20593                tracking_finished: disp.dialog_tracking.is_none(),
20594            }
20595        }
20596
20597        ModalDialogKeyboardButtonSnapshot {
20598            return_key: run_key(theme_id, 0x24, 0x0D, 0),
20599            enter_key: run_key(theme_id, 0x4C, 0x03, 0),
20600            escape_key: run_key(theme_id, 0x35, 0x1B, 0),
20601            command_period: run_key(theme_id, 0x2F, b'.', 0x0100),
20602        }
20603    }
20604
20605    fn modal_dialog_update_event_results_for_theme(
20606        theme_id: UiThemeId,
20607    ) -> ModalDialogUpdateEventSnapshot {
20608        fn install_region(
20609            bus: &mut MacMemoryBus,
20610            handle: u32,
20611            data: u32,
20612            rect: (i16, i16, i16, i16),
20613        ) {
20614            bus.write_long(handle, data);
20615            bus.write_word(data, 10);
20616            bus.write_word(data + 2, rect.0 as u16);
20617            bus.write_word(data + 4, rect.1 as u16);
20618            bus.write_word(data + 6, rect.2 as u16);
20619            bus.write_word(data + 8, rect.3 as u16);
20620        }
20621
20622        let (mut disp, mut cpu, mut bus) = setup_with_port();
20623        disp.set_ui_theme_id(theme_id);
20624
20625        let initial_port = 0x181000;
20626        disp.set_current_port_state(&mut bus, &mut cpu, initial_port, None);
20627
20628        let screen_base = bus.alloc(80 * 80);
20629        bus.write_long(0x0824, screen_base);
20630        disp.screen_mode = (screen_base, 80, 80, 80, 8);
20631
20632        let dialog_ptr = bus.alloc(256);
20633        let item_hit_ptr = 0x300100u32;
20634        let bounds = (10, 10, 40, 40);
20635        let snapshot_width = (bounds.3 - bounds.1 + TrapDispatcher::DBOX_FRAME_MARGIN * 2) as usize;
20636        let snapshot_height =
20637            (bounds.2 - bounds.0 + TrapDispatcher::DBOX_FRAME_MARGIN * 2) as usize;
20638        let snapshot_index = (17 - (bounds.0 - TrapDispatcher::DBOX_FRAME_MARGIN)) as usize
20639            * snapshot_width
20640            + (19 - (bounds.1 - TrapDispatcher::DBOX_FRAME_MARGIN)) as usize;
20641        let mut rendered_pixels = vec![0x00; snapshot_width * snapshot_height];
20642        rendered_pixels[snapshot_index] = 0x44;
20643
20644        let vis_rgn = bus.alloc(10);
20645        let vis_rgn_handle = bus.alloc(4);
20646        install_region(&mut bus, vis_rgn_handle, vis_rgn, (0, 0, 80, 80));
20647        let clip_rgn = bus.alloc(10);
20648        let clip_rgn_handle = bus.alloc(4);
20649        install_region(&mut bus, clip_rgn_handle, clip_rgn, (0, 0, 80, 80));
20650        let update_rgn = bus.alloc(10);
20651        let update_rgn_handle = bus.alloc(4);
20652        install_region(&mut bus, update_rgn_handle, update_rgn, (0, 0, 40, 40));
20653
20654        bus.write_word(dialog_ptr, 0);
20655        bus.write_long(dialog_ptr + 2, screen_base);
20656        bus.write_word(dialog_ptr + 6, 80);
20657        bus.write_word(dialog_ptr + 8, 0);
20658        bus.write_word(dialog_ptr + 10, 0);
20659        bus.write_word(dialog_ptr + 12, 80);
20660        bus.write_word(dialog_ptr + 14, 80);
20661        bus.write_word(dialog_ptr + 16, 0);
20662        bus.write_word(dialog_ptr + 18, 0);
20663        bus.write_word(dialog_ptr + 20, 80);
20664        bus.write_word(dialog_ptr + 22, 80);
20665        bus.write_long(dialog_ptr + 24, vis_rgn_handle);
20666        bus.write_long(dialog_ptr + 28, clip_rgn_handle);
20667        bus.write_long(dialog_ptr + 122, update_rgn_handle);
20668
20669        bus.write_word(item_hit_ptr, 0xCAFE);
20670        disp.front_window = dialog_ptr;
20671        disp.window_bounds = bounds;
20672        disp.dialog_tracking = Some(crate::trap::dispatch::DialogTrackingState {
20673            dialog_ptr,
20674            bounds,
20675            title: String::new(),
20676            proc_id: 2,
20677            items: Vec::new(),
20678            default_item: 0,
20679            cancel_item: 0,
20680            edit_text: String::new(),
20681            edit_item: 0,
20682            saved_pixels: Vec::new(),
20683            stack_ptr: TEST_SP,
20684            item_hit_ptr,
20685            rendered_pixels,
20686            flash_remaining: 0,
20687            flash_delay: 0,
20688            flash_item: 0,
20689            edit_text_modified: false,
20690            draw_proc_queue: VecDeque::new(),
20691            draw_procs_done: true,
20692            rendered_pixels_final: true,
20693            filter_proc: 0,
20694            game_managed: false,
20695            last_filter_event: None,
20696            popup_draws: Vec::new(),
20697            active_popup: None,
20698            active_button: None,
20699            active_user_item: None,
20700        });
20701        disp.event_queue
20702            .push_back(crate::trap::dispatch::QueuedEvent {
20703                what: 6,
20704                message: dialog_ptr,
20705                where_v: 0,
20706                where_h: 0,
20707                modifiers: 0,
20708            });
20709        cpu.write_reg(Register::A7, TEST_SP);
20710
20711        disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus)
20712            .unwrap()
20713            .unwrap();
20714
20715        let tracking = disp.dialog_tracking.as_ref().unwrap();
20716        ModalDialogUpdateEventSnapshot {
20717            dialog_ptr,
20718            item_hit_after: bus.read_word(item_hit_ptr),
20719            stack_after: cpu.read_reg(Register::A7),
20720            tracking_finished: disp.dialog_tracking.is_none(),
20721            rendered_pixels_final: tracking.rendered_pixels_final,
20722            retained_pixel_after: bus.read_byte(screen_base + 17 * 80 + 19),
20723            retained_snapshot_pixel_after: tracking.rendered_pixels[snapshot_index],
20724            update_region_after: TrapDispatcher::region_handle_rect(
20725                &bus,
20726                bus.read_long(dialog_ptr + 122),
20727            ),
20728            vis_region_after: TrapDispatcher::region_handle_rect(
20729                &bus,
20730                bus.read_long(dialog_ptr + 24),
20731            ),
20732            the_port_after: bus.read_long(crate::memory::globals::addr::THE_PORT),
20733            current_port_after: disp.current_port,
20734            saved_vis_after: disp.saved_vis_regions.contains_key(&dialog_ptr),
20735        }
20736    }
20737
20738    fn dialog_select_edit_text_key_results_for_theme(
20739        theme_id: UiThemeId,
20740    ) -> DialogSelectEditTextKeySnapshot {
20741        let (mut disp, mut cpu, mut bus) = setup();
20742        disp.set_ui_theme_id(theme_id);
20743        let dialog_ptr = bus.alloc(170);
20744        let event_ptr = 0x300000u32;
20745        let dialog_out_ptr = 0x300100u32;
20746        let item_hit_ptr = 0x300104u32;
20747        let items_handle = bus.alloc(4);
20748        let ditl_ptr = bus.alloc(16);
20749        let item_text_handle = bus.alloc(4);
20750        let item_text_ptr = bus.alloc(5);
20751        let text_h = TrapDispatcher::allocate_te_handle(&mut bus);
20752
20753        disp.front_window = dialog_ptr;
20754        bus.write_word(dialog_ptr + 8, 0);
20755        bus.write_word(dialog_ptr + 10, 0);
20756        bus.write_word(dialog_ptr + 16, 0);
20757        bus.write_word(dialog_ptr + 18, 0);
20758        bus.write_word(dialog_ptr + 20, 100);
20759        bus.write_word(dialog_ptr + 22, 160);
20760        bus.write_long(dialog_ptr + 156, items_handle);
20761        bus.write_long(dialog_ptr + 160, text_h);
20762        bus.write_word(dialog_ptr + 164, 0); // editField = first item
20763        bus.write_long(items_handle, ditl_ptr);
20764        bus.write_word(ditl_ptr, 0); // one item
20765        bus.write_long(ditl_ptr + 2, item_text_handle);
20766        bus.write_word(ditl_ptr + 6, 20);
20767        bus.write_word(ditl_ptr + 8, 30);
20768        bus.write_word(ditl_ptr + 10, 60);
20769        bus.write_word(ditl_ptr + 12, 110);
20770        bus.write_byte(ditl_ptr + 14, 16); // editText
20771        bus.write_byte(ditl_ptr + 15, 0);
20772        bus.write_long(item_text_handle, item_text_ptr);
20773        bus.write_bytes(item_text_ptr, b"Hello");
20774
20775        disp.te_set_text_contents(&mut bus, text_h, b"Hello");
20776        let te_ptr = bus.read_long(text_h);
20777        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 1);
20778        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 4);
20779        disp.dialog_items.insert(
20780            dialog_ptr,
20781            vec![DialogItem {
20782                item_type: 16,
20783                rect: (20, 30, 60, 110),
20784                text: "Hello".to_string(),
20785                resource_id: 0,
20786                proc_ptr: 0,
20787                sel_start: 1,
20788                sel_end: 4,
20789            }],
20790        );
20791
20792        cpu.write_reg(Register::A7, TEST_SP);
20793        bus.write_word(event_ptr, 3); // keyDown
20794        bus.write_long(event_ptr + 2, u32::from(b'Y'));
20795        bus.write_long(event_ptr + 6, 0);
20796        bus.write_word(event_ptr + 10, 0);
20797        bus.write_word(event_ptr + 12, 0);
20798        bus.write_word(event_ptr + 14, 0);
20799        bus.write_word(TEST_SP + 12, 0xBEEF);
20800        bus.write_long(dialog_out_ptr, 0xDEAD_BEEF);
20801        bus.write_word(item_hit_ptr, 0xCAFE);
20802        bus.write_long(TEST_SP, item_hit_ptr);
20803        bus.write_long(TEST_SP + 4, dialog_out_ptr);
20804        bus.write_long(TEST_SP + 8, event_ptr);
20805        disp.dispatch_dialog(true, 0x180, &mut cpu, &mut bus)
20806            .unwrap()
20807            .unwrap();
20808        let keydown_result = bus.read_word(TEST_SP + 12);
20809        let keydown_dialog_out = bus.read_long(dialog_out_ptr);
20810        let keydown_item_hit = bus.read_word(item_hit_ptr);
20811        let keydown_stack_after = cpu.read_reg(Register::A7);
20812        let keydown_handle_bytes = text_handle_bytes(&bus, item_text_handle);
20813        let keydown_cached_text = disp.dialog_items[&dialog_ptr][0].text.clone();
20814        let keydown_item_selection = (
20815            disp.dialog_items[&dialog_ptr][0].sel_start,
20816            disp.dialog_items[&dialog_ptr][0].sel_end,
20817        );
20818        let keydown_te_text = TrapDispatcher::te_text_bytes(&bus, text_h);
20819        let keydown_te_length = bus.read_word(te_ptr + TrapDispatcher::TE_LENGTH_OFFSET);
20820        let keydown_te_selection = (
20821            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
20822            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET),
20823        );
20824
20825        cpu.write_reg(Register::A7, TEST_SP);
20826        bus.write_word(event_ptr, 5); // autoKey
20827        bus.write_long(event_ptr + 2, 0x0000_0008); // backspace
20828        bus.write_word(TEST_SP + 12, 0xBEEF);
20829        bus.write_long(dialog_out_ptr, 0xDEAD_BEEF);
20830        bus.write_word(item_hit_ptr, 0xCAFE);
20831        bus.write_long(TEST_SP, item_hit_ptr);
20832        bus.write_long(TEST_SP + 4, dialog_out_ptr);
20833        bus.write_long(TEST_SP + 8, event_ptr);
20834        disp.dispatch_dialog(true, 0x180, &mut cpu, &mut bus)
20835            .unwrap()
20836            .unwrap();
20837        let autokey_result = bus.read_word(TEST_SP + 12);
20838        let autokey_dialog_out = bus.read_long(dialog_out_ptr);
20839        let autokey_item_hit = bus.read_word(item_hit_ptr);
20840        let autokey_stack_after = cpu.read_reg(Register::A7);
20841        let autokey_handle_bytes = text_handle_bytes(&bus, item_text_handle);
20842        let autokey_cached_text = disp.dialog_items[&dialog_ptr][0].text.clone();
20843        let autokey_item_selection = (
20844            disp.dialog_items[&dialog_ptr][0].sel_start,
20845            disp.dialog_items[&dialog_ptr][0].sel_end,
20846        );
20847        let autokey_te_text = TrapDispatcher::te_text_bytes(&bus, text_h);
20848        let autokey_te_length = bus.read_word(te_ptr + TrapDispatcher::TE_LENGTH_OFFSET);
20849        let autokey_te_selection = (
20850            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
20851            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET),
20852        );
20853
20854        DialogSelectEditTextKeySnapshot {
20855            keydown_result,
20856            keydown_dialog_out,
20857            keydown_item_hit,
20858            keydown_stack_after,
20859            keydown_handle_bytes,
20860            keydown_cached_text,
20861            keydown_item_selection,
20862            keydown_te_text,
20863            keydown_te_length,
20864            keydown_te_selection,
20865            autokey_result,
20866            autokey_dialog_out,
20867            autokey_item_hit,
20868            autokey_stack_after,
20869            autokey_handle_bytes,
20870            autokey_cached_text,
20871            autokey_item_selection,
20872            autokey_te_text,
20873            autokey_te_length,
20874            autokey_te_selection,
20875        }
20876    }
20877
20878    fn getsetditem_useritem_record_results_for_theme(
20879        theme_id: UiThemeId,
20880    ) -> DItemUserItemRecordSnapshot {
20881        let (mut disp, mut cpu, mut bus) = setup();
20882        disp.set_ui_theme_id(theme_id);
20883        let dialog_ptr = bus.alloc(170);
20884        let items_handle = bus.alloc(4);
20885        let ditl_ptr = bus.alloc(16);
20886        let initial_proc_ptr = 0x0012_3456u32;
20887        let new_proc_ptr = 0x00AB_CDEFu32;
20888        let set_box_ptr = bus.alloc(8);
20889        let get_box_ptr = bus.alloc(8);
20890        let get_item_ptr = bus.alloc(4);
20891        let get_type_ptr = bus.alloc(2);
20892
20893        bus.write_long(items_handle, ditl_ptr);
20894        bus.write_long(dialog_ptr + 156, items_handle);
20895        bus.write_word(ditl_ptr, 0); // one item
20896        bus.write_long(ditl_ptr + 2, initial_proc_ptr);
20897        bus.write_word(ditl_ptr + 6, 12);
20898        bus.write_word(ditl_ptr + 8, 24);
20899        bus.write_word(ditl_ptr + 10, 36);
20900        bus.write_word(ditl_ptr + 12, 48);
20901        bus.write_byte(ditl_ptr + 14, 0);
20902        bus.write_byte(ditl_ptr + 15, 0);
20903        disp.dialog_items.insert(
20904            dialog_ptr,
20905            vec![DialogItem {
20906                item_type: 0,
20907                rect: (12, 24, 36, 48),
20908                text: String::new(),
20909                resource_id: 0,
20910                proc_ptr: initial_proc_ptr,
20911                sel_start: 0,
20912                sel_end: 0,
20913            }],
20914        );
20915
20916        cpu.write_reg(Register::A7, TEST_SP);
20917        bus.write_long(TEST_SP, get_box_ptr);
20918        bus.write_long(TEST_SP + 4, get_item_ptr);
20919        bus.write_long(TEST_SP + 8, get_type_ptr);
20920        bus.write_word(TEST_SP + 12, 1);
20921        bus.write_long(TEST_SP + 14, dialog_ptr);
20922        disp.dispatch_dialog(true, 0x18D, &mut cpu, &mut bus)
20923            .unwrap()
20924            .unwrap();
20925        let initial_get = DItemGetSnapshot {
20926            item_type: bus.read_word(get_type_ptr),
20927            item: bus.read_long(get_item_ptr),
20928            rect: (
20929                bus.read_word(get_box_ptr),
20930                bus.read_word(get_box_ptr + 2),
20931                bus.read_word(get_box_ptr + 4),
20932                bus.read_word(get_box_ptr + 6),
20933            ),
20934            stack_after: cpu.read_reg(Register::A7),
20935        };
20936
20937        cpu.write_reg(Register::A7, TEST_SP);
20938        bus.write_word(set_box_ptr, 50);
20939        bus.write_word(set_box_ptr + 2, 60);
20940        bus.write_word(set_box_ptr + 4, 70);
20941        bus.write_word(set_box_ptr + 6, 80);
20942        bus.write_long(TEST_SP, set_box_ptr);
20943        bus.write_long(TEST_SP + 4, new_proc_ptr);
20944        bus.write_word(TEST_SP + 8, 0);
20945        bus.write_word(TEST_SP + 10, 1);
20946        bus.write_long(TEST_SP + 12, dialog_ptr);
20947        disp.dispatch_dialog(true, 0x18E, &mut cpu, &mut bus)
20948            .unwrap()
20949            .unwrap();
20950        let set_stack_after = cpu.read_reg(Register::A7);
20951        let stored_item = disp
20952            .dialog_items
20953            .get(&dialog_ptr)
20954            .and_then(|items| items.first())
20955            .cloned()
20956            .unwrap();
20957
20958        cpu.write_reg(Register::A7, TEST_SP);
20959        bus.write_long(TEST_SP, get_box_ptr);
20960        bus.write_long(TEST_SP + 4, get_item_ptr);
20961        bus.write_long(TEST_SP + 8, get_type_ptr);
20962        bus.write_word(TEST_SP + 12, 1);
20963        bus.write_long(TEST_SP + 14, dialog_ptr);
20964        disp.dispatch_dialog(true, 0x18D, &mut cpu, &mut bus)
20965            .unwrap()
20966            .unwrap();
20967        let post_set_get = DItemGetSnapshot {
20968            item_type: bus.read_word(get_type_ptr),
20969            item: bus.read_long(get_item_ptr),
20970            rect: (
20971                bus.read_word(get_box_ptr),
20972                bus.read_word(get_box_ptr + 2),
20973                bus.read_word(get_box_ptr + 4),
20974                bus.read_word(get_box_ptr + 6),
20975            ),
20976            stack_after: cpu.read_reg(Register::A7),
20977        };
20978
20979        DItemUserItemRecordSnapshot {
20980            initial_get,
20981            set_stack_after,
20982            stored_type: stored_item.item_type,
20983            stored_proc_ptr: stored_item.proc_ptr,
20984            stored_rect: stored_item.rect,
20985            post_set_get,
20986        }
20987    }
20988
20989    fn get_ditem_snapshot_for_test(
20990        disp: &mut TrapDispatcher,
20991        cpu: &mut MockCpu,
20992        bus: &mut MacMemoryBus,
20993        dialog_ptr: u32,
20994        item_no: i16,
20995        box_ptr: u32,
20996        item_ptr: u32,
20997        type_ptr: u32,
20998    ) -> DItemGetSnapshot {
20999        cpu.write_reg(Register::A7, TEST_SP);
21000        bus.write_long(TEST_SP, box_ptr);
21001        bus.write_long(TEST_SP + 4, item_ptr);
21002        bus.write_long(TEST_SP + 8, type_ptr);
21003        bus.write_word(TEST_SP + 12, item_no as u16);
21004        bus.write_long(TEST_SP + 14, dialog_ptr);
21005        disp.dispatch_dialog(true, 0x18D, cpu, bus)
21006            .unwrap()
21007            .unwrap();
21008
21009        DItemGetSnapshot {
21010            item_type: bus.read_word(type_ptr),
21011            item: bus.read_long(item_ptr),
21012            rect: (
21013                bus.read_word(box_ptr),
21014                bus.read_word(box_ptr + 2),
21015                bus.read_word(box_ptr + 4),
21016                bus.read_word(box_ptr + 6),
21017            ),
21018            stack_after: cpu.read_reg(Register::A7),
21019        }
21020    }
21021
21022    fn write_control_record_for_ditem_test(
21023        bus: &mut MacMemoryBus,
21024        handle: u32,
21025        ctrl_ptr: u32,
21026        dialog_ptr: u32,
21027        rect: (i16, i16, i16, i16),
21028        value: i16,
21029        title: &[u8],
21030    ) {
21031        bus.write_long(handle, ctrl_ptr);
21032        bus.write_long(ctrl_ptr, 0);
21033        bus.write_long(ctrl_ptr + 4, dialog_ptr);
21034        bus.write_word(ctrl_ptr + 8, rect.0 as u16);
21035        bus.write_word(ctrl_ptr + 10, rect.1 as u16);
21036        bus.write_word(ctrl_ptr + 12, rect.2 as u16);
21037        bus.write_word(ctrl_ptr + 14, rect.3 as u16);
21038        bus.write_byte(ctrl_ptr + 16, 255);
21039        bus.write_byte(ctrl_ptr + 17, 0);
21040        bus.write_word(ctrl_ptr + 18, value as u16);
21041        bus.write_word(ctrl_ptr + 20, 0);
21042        bus.write_word(ctrl_ptr + 22, 1);
21043        bus.write_byte(ctrl_ptr + 40, title.len() as u8);
21044        bus.write_bytes(ctrl_ptr + 41, title);
21045    }
21046
21047    fn getsetditem_text_control_record_results_for_theme(
21048        theme_id: UiThemeId,
21049    ) -> DItemTextControlRecordSnapshot {
21050        let (mut disp, mut cpu, mut bus) = setup();
21051        disp.set_ui_theme_id(theme_id);
21052        let dialog_ptr = bus.alloc(170);
21053        let items_handle = bus.alloc(4);
21054        let ditl_ptr = bus.alloc(32);
21055        let old_text_handle = bus.alloc(4);
21056        let old_text_ptr = bus.alloc(3);
21057        let new_text_handle = bus.alloc(4);
21058        let new_text_ptr = bus.alloc(7);
21059        let old_control_handle = bus.alloc(4);
21060        let old_control_ptr = bus.alloc(44);
21061        let new_control_handle = bus.alloc(4);
21062        let new_control_ptr = bus.alloc(47);
21063        let text_set_box_ptr = bus.alloc(8);
21064        let control_set_box_ptr = bus.alloc(8);
21065        let get_box_ptr = bus.alloc(8);
21066        let get_item_ptr = bus.alloc(4);
21067        let get_type_ptr = bus.alloc(2);
21068        let text_entry = ditl_ptr + 2;
21069        let control_entry = ditl_ptr + 16;
21070
21071        bus.write_bytes(old_text_ptr, b"Old");
21072        bus.write_bytes(new_text_ptr, b"Updated");
21073        bus.write_long(old_text_handle, old_text_ptr);
21074        bus.write_long(new_text_handle, new_text_ptr);
21075        write_control_record_for_ditem_test(
21076            &mut bus,
21077            old_control_handle,
21078            old_control_ptr,
21079            dialog_ptr,
21080            (30, 40, 50, 140),
21081            1,
21082            b"OK",
21083        );
21084        write_control_record_for_ditem_test(
21085            &mut bus,
21086            new_control_handle,
21087            new_control_ptr,
21088            dialog_ptr,
21089            (1, 2, 3, 4),
21090            2,
21091            b"Apply",
21092        );
21093
21094        bus.write_long(items_handle, ditl_ptr);
21095        bus.write_long(dialog_ptr + 156, items_handle);
21096        bus.write_word(ditl_ptr, 1); // two items
21097        bus.write_long(text_entry, old_text_handle);
21098        bus.write_word(text_entry + 4, 10);
21099        bus.write_word(text_entry + 6, 20);
21100        bus.write_word(text_entry + 8, 24);
21101        bus.write_word(text_entry + 10, 160);
21102        bus.write_byte(text_entry + 12, 16);
21103        bus.write_byte(text_entry + 13, 0);
21104        bus.write_long(control_entry, old_control_handle);
21105        bus.write_word(control_entry + 4, 30);
21106        bus.write_word(control_entry + 6, 40);
21107        bus.write_word(control_entry + 8, 50);
21108        bus.write_word(control_entry + 10, 140);
21109        bus.write_byte(control_entry + 12, 4);
21110        bus.write_byte(control_entry + 13, 2);
21111        bus.write_bytes(control_entry + 14, b"OK");
21112
21113        disp.dialog_items.insert(
21114            dialog_ptr,
21115            vec![
21116                DialogItem {
21117                    item_type: 16,
21118                    rect: (10, 20, 24, 160),
21119                    text: "Old".to_string(),
21120                    resource_id: 0,
21121                    proc_ptr: 0,
21122                    sel_start: 0,
21123                    sel_end: 0,
21124                },
21125                DialogItem {
21126                    item_type: 4,
21127                    rect: (30, 40, 50, 140),
21128                    text: "OK".to_string(),
21129                    resource_id: 0,
21130                    proc_ptr: 0,
21131                    sel_start: 0,
21132                    sel_end: 0,
21133                },
21134            ],
21135        );
21136        disp.dialog_item_handles
21137            .insert(old_text_handle, (dialog_ptr, 0));
21138        disp.dialog_control_handles
21139            .insert(old_control_handle, (dialog_ptr, 2));
21140        disp.dialog_control_values.insert((dialog_ptr, 2), 1);
21141
21142        let text_initial_get = get_ditem_snapshot_for_test(
21143            &mut disp,
21144            &mut cpu,
21145            &mut bus,
21146            dialog_ptr,
21147            1,
21148            get_box_ptr,
21149            get_item_ptr,
21150            get_type_ptr,
21151        );
21152        let control_initial_get = get_ditem_snapshot_for_test(
21153            &mut disp,
21154            &mut cpu,
21155            &mut bus,
21156            dialog_ptr,
21157            2,
21158            get_box_ptr,
21159            get_item_ptr,
21160            get_type_ptr,
21161        );
21162
21163        cpu.write_reg(Register::A7, TEST_SP);
21164        bus.write_word(text_set_box_ptr, 12);
21165        bus.write_word(text_set_box_ptr + 2, 22);
21166        bus.write_word(text_set_box_ptr + 4, 28);
21167        bus.write_word(text_set_box_ptr + 6, 168);
21168        bus.write_long(TEST_SP, text_set_box_ptr);
21169        bus.write_long(TEST_SP + 4, new_text_handle);
21170        bus.write_word(TEST_SP + 8, 16);
21171        bus.write_word(TEST_SP + 10, 1);
21172        bus.write_long(TEST_SP + 12, dialog_ptr);
21173        disp.dispatch_dialog(true, 0x18E, &mut cpu, &mut bus)
21174            .unwrap()
21175            .unwrap();
21176        let text_set_stack_after = cpu.read_reg(Register::A7);
21177        let text_stored_item = disp.dialog_items.get(&dialog_ptr).unwrap()[0].clone();
21178        let text_post_set_get = get_ditem_snapshot_for_test(
21179            &mut disp,
21180            &mut cpu,
21181            &mut bus,
21182            dialog_ptr,
21183            1,
21184            get_box_ptr,
21185            get_item_ptr,
21186            get_type_ptr,
21187        );
21188
21189        cpu.write_reg(Register::A7, TEST_SP);
21190        bus.write_word(control_set_box_ptr, 42);
21191        bus.write_word(control_set_box_ptr + 2, 52);
21192        bus.write_word(control_set_box_ptr + 4, 62);
21193        bus.write_word(control_set_box_ptr + 6, 172);
21194        bus.write_long(TEST_SP, control_set_box_ptr);
21195        bus.write_long(TEST_SP + 4, new_control_handle);
21196        bus.write_word(TEST_SP + 8, 5);
21197        bus.write_word(TEST_SP + 10, 2);
21198        bus.write_long(TEST_SP + 12, dialog_ptr);
21199        disp.dispatch_dialog(true, 0x18E, &mut cpu, &mut bus)
21200            .unwrap()
21201            .unwrap();
21202        let control_set_stack_after = cpu.read_reg(Register::A7);
21203        let control_stored_item = disp.dialog_items.get(&dialog_ptr).unwrap()[1].clone();
21204        let control_post_set_get = get_ditem_snapshot_for_test(
21205            &mut disp,
21206            &mut cpu,
21207            &mut bus,
21208            dialog_ptr,
21209            2,
21210            get_box_ptr,
21211            get_item_ptr,
21212            get_type_ptr,
21213        );
21214
21215        DItemTextControlRecordSnapshot {
21216            text_initial_get,
21217            text_set_stack_after,
21218            text_old_handle_mapped_after: disp.dialog_item_handles.contains_key(&old_text_handle),
21219            text_new_handle_map_value_after: disp
21220                .dialog_item_handles
21221                .get(&new_text_handle)
21222                .copied(),
21223            text_ditl_handle_after: bus.read_long(text_entry),
21224            text_ditl_rect_after: (
21225                bus.read_word(text_entry + 4),
21226                bus.read_word(text_entry + 6),
21227                bus.read_word(text_entry + 8),
21228                bus.read_word(text_entry + 10),
21229            ),
21230            text_ditl_type_after: bus.read_byte(text_entry + 12),
21231            text_stored_type_after: text_stored_item.item_type,
21232            text_stored_rect_after: text_stored_item.rect,
21233            text_cached_text_after: text_stored_item.text,
21234            text_handle_bytes_after: text_handle_bytes(&bus, new_text_handle),
21235            text_post_set_get,
21236            control_initial_get,
21237            control_set_stack_after,
21238            control_old_handle_mapped_after: disp
21239                .dialog_control_handles
21240                .contains_key(&old_control_handle),
21241            control_new_handle_map_value_after: disp
21242                .dialog_control_handles
21243                .get(&new_control_handle)
21244                .copied(),
21245            control_ditl_handle_after: bus.read_long(control_entry),
21246            control_ditl_rect_after: (
21247                bus.read_word(control_entry + 4),
21248                bus.read_word(control_entry + 6),
21249                bus.read_word(control_entry + 8),
21250                bus.read_word(control_entry + 10),
21251            ),
21252            control_ditl_type_after: bus.read_byte(control_entry + 12),
21253            control_stored_type_after: control_stored_item.item_type,
21254            control_stored_rect_after: control_stored_item.rect,
21255            control_record_rect_after: (
21256                bus.read_word(new_control_ptr + 8),
21257                bus.read_word(new_control_ptr + 10),
21258                bus.read_word(new_control_ptr + 12),
21259                bus.read_word(new_control_ptr + 14),
21260            ),
21261            control_value_after: disp.dialog_control_values.get(&(dialog_ptr, 2)).copied(),
21262            control_post_set_get,
21263        }
21264    }
21265
21266    fn hide_show_ditem_visibility_results_for_theme(
21267        theme_id: UiThemeId,
21268    ) -> DItemVisibilitySnapshot {
21269        let (mut disp, mut cpu, mut bus) = setup();
21270        disp.set_ui_theme_id(theme_id);
21271        let dialog_ptr = bus.alloc(170);
21272        let items_handle = bus.alloc(4);
21273        let ditl_ptr = bus.alloc(18);
21274        let get_box_ptr = bus.alloc(8);
21275        let get_item_ptr = bus.alloc(4);
21276        let get_type_ptr = bus.alloc(2);
21277
21278        bus.write_long(items_handle, ditl_ptr);
21279        bus.write_long(dialog_ptr + 156, items_handle);
21280        bus.write_word(ditl_ptr, 0); // one item
21281        bus.write_long(ditl_ptr + 2, 0);
21282        bus.write_word(ditl_ptr + 6, 10);
21283        bus.write_word(ditl_ptr + 8, 20);
21284        bus.write_word(ditl_ptr + 10, 30);
21285        bus.write_word(ditl_ptr + 12, 120);
21286        bus.write_byte(ditl_ptr + 14, 4);
21287        bus.write_byte(ditl_ptr + 15, 2);
21288        bus.write_bytes(ditl_ptr + 16, b"OK");
21289        disp.dialog_items.insert(
21290            dialog_ptr,
21291            vec![DialogItem {
21292                item_type: 4,
21293                rect: (10, 20, 30, 120),
21294                text: "OK".into(),
21295                resource_id: 0,
21296                proc_ptr: 0,
21297                sel_start: 0,
21298                sel_end: 0,
21299            }],
21300        );
21301
21302        cpu.write_reg(Register::A7, TEST_SP);
21303        bus.write_long(TEST_SP, get_box_ptr);
21304        bus.write_long(TEST_SP + 4, get_item_ptr);
21305        bus.write_long(TEST_SP + 8, get_type_ptr);
21306        bus.write_word(TEST_SP + 12, 1);
21307        bus.write_long(TEST_SP + 14, dialog_ptr);
21308        disp.dispatch_dialog(true, 0x18D, &mut cpu, &mut bus)
21309            .unwrap()
21310            .unwrap();
21311        let initial_get = DItemGetSnapshot {
21312            item_type: bus.read_word(get_type_ptr),
21313            item: bus.read_long(get_item_ptr),
21314            rect: (
21315                bus.read_word(get_box_ptr),
21316                bus.read_word(get_box_ptr + 2),
21317                bus.read_word(get_box_ptr + 4),
21318                bus.read_word(get_box_ptr + 6),
21319            ),
21320            stack_after: cpu.read_reg(Register::A7),
21321        };
21322        assert_ne!(initial_get.item, 0);
21323        let ctrl_ptr = bus.read_long(initial_get.item);
21324        assert_ne!(ctrl_ptr, 0);
21325
21326        cpu.write_reg(Register::A7, TEST_SP);
21327        bus.write_word(TEST_SP, 1);
21328        bus.write_long(TEST_SP + 2, dialog_ptr);
21329        disp.dispatch_dialog(true, 0x027, &mut cpu, &mut bus)
21330            .unwrap()
21331            .unwrap();
21332        let hide_stack_after = cpu.read_reg(Register::A7);
21333        let hidden_item_rect = disp.dialog_items.get(&dialog_ptr).unwrap()[0].rect;
21334        let hidden_saved_rect = disp.hidden_dialog_item_rects.get(&(dialog_ptr, 1)).copied();
21335        let hidden_control_rect = (
21336            bus.read_word(ctrl_ptr + 8),
21337            bus.read_word(ctrl_ptr + 10),
21338            bus.read_word(ctrl_ptr + 12),
21339            bus.read_word(ctrl_ptr + 14),
21340        );
21341
21342        cpu.write_reg(Register::A7, TEST_SP);
21343        bus.write_long(TEST_SP, get_box_ptr);
21344        bus.write_long(TEST_SP + 4, get_item_ptr);
21345        bus.write_long(TEST_SP + 8, get_type_ptr);
21346        bus.write_word(TEST_SP + 12, 1);
21347        bus.write_long(TEST_SP + 14, dialog_ptr);
21348        disp.dispatch_dialog(true, 0x18D, &mut cpu, &mut bus)
21349            .unwrap()
21350            .unwrap();
21351        let hidden_get = DItemGetSnapshot {
21352            item_type: bus.read_word(get_type_ptr),
21353            item: bus.read_long(get_item_ptr),
21354            rect: (
21355                bus.read_word(get_box_ptr),
21356                bus.read_word(get_box_ptr + 2),
21357                bus.read_word(get_box_ptr + 4),
21358                bus.read_word(get_box_ptr + 6),
21359            ),
21360            stack_after: cpu.read_reg(Register::A7),
21361        };
21362
21363        cpu.write_reg(Register::A7, TEST_SP);
21364        bus.write_word(TEST_SP, 20);
21365        bus.write_word(TEST_SP + 2, 40);
21366        bus.write_long(TEST_SP + 4, dialog_ptr);
21367        bus.write_word(TEST_SP + 8, 0xBEEF);
21368        disp.dispatch_dialog(true, 0x184, &mut cpu, &mut bus)
21369            .unwrap()
21370            .unwrap();
21371        let hidden_find = (
21372            bus.read_word(TEST_SP + 8) as i16,
21373            cpu.read_reg(Register::A7),
21374        );
21375
21376        cpu.write_reg(Register::A7, TEST_SP);
21377        bus.write_word(TEST_SP, 1);
21378        bus.write_long(TEST_SP + 2, dialog_ptr);
21379        disp.dispatch_dialog(true, 0x028, &mut cpu, &mut bus)
21380            .unwrap()
21381            .unwrap();
21382        let show_stack_after = cpu.read_reg(Register::A7);
21383        let restored_item_rect = disp.dialog_items.get(&dialog_ptr).unwrap()[0].rect;
21384        let restored_saved_rect = disp.hidden_dialog_item_rects.get(&(dialog_ptr, 1)).copied();
21385        let restored_control_rect = (
21386            bus.read_word(ctrl_ptr + 8),
21387            bus.read_word(ctrl_ptr + 10),
21388            bus.read_word(ctrl_ptr + 12),
21389            bus.read_word(ctrl_ptr + 14),
21390        );
21391
21392        cpu.write_reg(Register::A7, TEST_SP);
21393        bus.write_long(TEST_SP, get_box_ptr);
21394        bus.write_long(TEST_SP + 4, get_item_ptr);
21395        bus.write_long(TEST_SP + 8, get_type_ptr);
21396        bus.write_word(TEST_SP + 12, 1);
21397        bus.write_long(TEST_SP + 14, dialog_ptr);
21398        disp.dispatch_dialog(true, 0x18D, &mut cpu, &mut bus)
21399            .unwrap()
21400            .unwrap();
21401        let restored_get = DItemGetSnapshot {
21402            item_type: bus.read_word(get_type_ptr),
21403            item: bus.read_long(get_item_ptr),
21404            rect: (
21405                bus.read_word(get_box_ptr),
21406                bus.read_word(get_box_ptr + 2),
21407                bus.read_word(get_box_ptr + 4),
21408                bus.read_word(get_box_ptr + 6),
21409            ),
21410            stack_after: cpu.read_reg(Register::A7),
21411        };
21412
21413        cpu.write_reg(Register::A7, TEST_SP);
21414        bus.write_word(TEST_SP, 20);
21415        bus.write_word(TEST_SP + 2, 40);
21416        bus.write_long(TEST_SP + 4, dialog_ptr);
21417        bus.write_word(TEST_SP + 8, 0xBEEF);
21418        disp.dispatch_dialog(true, 0x184, &mut cpu, &mut bus)
21419            .unwrap()
21420            .unwrap();
21421        let restored_find = (
21422            bus.read_word(TEST_SP + 8) as i16,
21423            cpu.read_reg(Register::A7),
21424        );
21425
21426        DItemVisibilitySnapshot {
21427            initial_get,
21428            hide_stack_after,
21429            hidden_item_rect,
21430            hidden_saved_rect,
21431            hidden_control_rect,
21432            hidden_get,
21433            hidden_find,
21434            show_stack_after,
21435            restored_item_rect,
21436            restored_saved_rect,
21437            restored_control_rect,
21438            restored_get,
21439            restored_find,
21440        }
21441    }
21442
21443    fn drawdialog_useritem_pixel_results_for_theme(theme_id: UiThemeId) -> (u8, u32, u32) {
21444        let (mut disp, mut cpu, mut bus) = setup();
21445        disp.set_ui_theme_id(theme_id);
21446        let dialog_ptr = bus.alloc(170);
21447        let (screen_base, row_bytes, _w, _h, pixel_size) = disp.screen_mode;
21448        assert_eq!(pixel_size, 8);
21449
21450        bus.write_word(dialog_ptr + 8, 0);
21451        bus.write_word(dialog_ptr + 10, 0);
21452        bus.write_word(dialog_ptr + 16, 0);
21453        bus.write_word(dialog_ptr + 18, 0);
21454        bus.write_word(dialog_ptr + 20, 40);
21455        bus.write_word(dialog_ptr + 22, 80);
21456        disp.dialog_items.insert(
21457            dialog_ptr,
21458            vec![
21459                DialogItem {
21460                    item_type: 0, // enabled userItem
21461                    rect: (8, 8, 20, 32),
21462                    text: String::new(),
21463                    resource_id: 0,
21464                    proc_ptr: 0x00C0_FFEE,
21465                    sel_start: 0,
21466                    sel_end: 0,
21467                },
21468                DialogItem {
21469                    item_type: 4,
21470                    rect: (24, 8, 36, 40),
21471                    text: "OK".to_string(),
21472                    resource_id: 0,
21473                    proc_ptr: 0,
21474                    sel_start: 0,
21475                    sel_end: 0,
21476                },
21477            ],
21478        );
21479
21480        let user_x = 10u32;
21481        let user_y = 10u32;
21482        let user_pixel = screen_base + user_y * row_bytes + user_x;
21483        bus.write_byte(user_pixel, 0xA5);
21484        cpu.write_reg(Register::A7, TEST_SP);
21485        bus.write_long(TEST_SP, dialog_ptr);
21486        bus.write_long(TEST_SP + 4, 0xCAFE_BABE);
21487
21488        disp.dispatch_dialog(true, 0x181, &mut cpu, &mut bus)
21489            .unwrap()
21490            .unwrap();
21491
21492        (
21493            bus.read_byte(user_pixel),
21494            cpu.read_reg(Register::A7),
21495            bus.read_long(TEST_SP + 4),
21496        )
21497    }
21498
21499    fn monochrome_icon_resource_with_points(points: &[(u8, u8)]) -> Vec<u8> {
21500        let mut icon = vec![0u8; 128];
21501        for &(x, y) in points {
21502            let byte = y as usize * 4 + x as usize / 8;
21503            icon[byte] |= 0x80 >> (x & 7);
21504        }
21505        icon
21506    }
21507
21508    fn write_be_word(data: &mut [u8], offset: usize, value: u16) {
21509        data[offset] = (value >> 8) as u8;
21510        data[offset + 1] = value as u8;
21511    }
21512
21513    fn cicn_resource_with_points(width: u16, height: u16, points: &[(u8, u8, u8)]) -> Vec<u8> {
21514        let pixel_row_bytes = u32::from(width);
21515        let mask_row_bytes = u32::from(width).div_ceil(8);
21516        let mask_size = mask_row_bytes * u32::from(height);
21517        let bmap_size = mask_row_bytes * u32::from(height);
21518        let ctab_size = 16u32;
21519        let pixel_size = pixel_row_bytes * u32::from(height);
21520        let bmap_offset = 82 + mask_size as usize;
21521        let ctab_offset = bmap_offset + bmap_size as usize;
21522        let pixel_offset = ctab_offset + ctab_size as usize;
21523        let mut data = vec![0u8; pixel_offset + pixel_size as usize];
21524
21525        write_be_word(&mut data, 4, pixel_row_bytes as u16);
21526        write_be_word(&mut data, 10, height);
21527        write_be_word(&mut data, 12, width);
21528        write_be_word(&mut data, 32, 8);
21529
21530        write_be_word(&mut data, 54, mask_row_bytes as u16);
21531        write_be_word(&mut data, 60, height);
21532        write_be_word(&mut data, 62, width);
21533
21534        write_be_word(&mut data, 68, mask_row_bytes as u16);
21535        write_be_word(&mut data, 74, height);
21536        write_be_word(&mut data, 76, width);
21537
21538        write_be_word(&mut data, ctab_offset + 6, 0);
21539
21540        for &(x, y, pixel) in points {
21541            let mask_row = 82 + y as usize * mask_row_bytes as usize;
21542            data[mask_row + x as usize / 8] |= 0x80 >> (x & 7);
21543            let pixel_row = pixel_offset + y as usize * pixel_row_bytes as usize;
21544            data[pixel_row + x as usize] = pixel;
21545        }
21546        data
21547    }
21548
21549    fn drawdialog_icon_item_pixel_results_for_theme(theme_id: UiThemeId) -> ([u8; 3], u32, u32) {
21550        let (mut disp, mut cpu, mut bus) = setup();
21551        disp.set_ui_theme_id(theme_id);
21552        let dialog_ptr = bus.alloc(170);
21553        let (screen_base, row_bytes, _w, _h, pixel_size) = disp.screen_mode;
21554        assert_eq!(pixel_size, 8);
21555
21556        bus.write_word(dialog_ptr + 8, 0);
21557        bus.write_word(dialog_ptr + 10, 0);
21558        bus.write_word(dialog_ptr + 16, 0);
21559        bus.write_word(dialog_ptr + 18, 0);
21560        bus.write_word(dialog_ptr + 20, 56);
21561        bus.write_word(dialog_ptr + 22, 88);
21562        disp.install_test_resource(
21563            &mut bus,
21564            *b"ICON",
21565            421,
21566            &monochrome_icon_resource_with_points(&[(0, 0), (15, 15), (31, 31)]),
21567        );
21568        disp.dialog_items.insert(
21569            dialog_ptr,
21570            vec![
21571                DialogItem {
21572                    item_type: 32, // icon item
21573                    rect: (8, 8, 40, 40),
21574                    text: String::new(),
21575                    resource_id: 421,
21576                    proc_ptr: 0,
21577                    sel_start: 0,
21578                    sel_end: 0,
21579                },
21580                DialogItem {
21581                    item_type: 4,
21582                    rect: (44, 8, 54, 40),
21583                    text: "OK".to_string(),
21584                    resource_id: 0,
21585                    proc_ptr: 0,
21586                    sel_start: 0,
21587                    sel_end: 0,
21588                },
21589            ],
21590        );
21591
21592        for (x, y) in [(8u32, 8u32), (23, 23), (39, 39)] {
21593            bus.write_byte(screen_base + y * row_bytes + x, 0x42);
21594        }
21595        cpu.write_reg(Register::A7, TEST_SP);
21596        bus.write_long(TEST_SP, dialog_ptr);
21597        bus.write_long(TEST_SP + 4, 0xCAFE_BABE);
21598
21599        disp.dispatch_dialog(true, 0x181, &mut cpu, &mut bus)
21600            .unwrap()
21601            .unwrap();
21602
21603        (
21604            [
21605                bus.read_byte(screen_base + 8 * row_bytes + 8),
21606                bus.read_byte(screen_base + 23 * row_bytes + 23),
21607                bus.read_byte(screen_base + 39 * row_bytes + 39),
21608            ],
21609            cpu.read_reg(Register::A7),
21610            bus.read_long(TEST_SP + 4),
21611        )
21612    }
21613
21614    fn drawdialog_cicn_item_pixel_results_for_theme(theme_id: UiThemeId) -> ([u8; 3], u32, u32) {
21615        let (mut disp, mut cpu, mut bus) = setup();
21616        disp.set_ui_theme_id(theme_id);
21617        let dialog_ptr = bus.alloc(170);
21618        let (screen_base, row_bytes, _w, _h, pixel_size) = disp.screen_mode;
21619        assert_eq!(pixel_size, 8);
21620
21621        bus.write_word(dialog_ptr + 8, 0);
21622        bus.write_word(dialog_ptr + 10, 0);
21623        bus.write_word(dialog_ptr + 16, 0);
21624        bus.write_word(dialog_ptr + 18, 0);
21625        bus.write_word(dialog_ptr + 20, 56);
21626        bus.write_word(dialog_ptr + 22, 88);
21627        let sample_points = [(0, 0), (15, 15), (31, 31)];
21628        disp.install_test_resource(
21629            &mut bus,
21630            *b"ICON",
21631            422,
21632            &monochrome_icon_resource_with_points(&sample_points),
21633        );
21634        disp.install_test_resource(
21635            &mut bus,
21636            *b"cicn",
21637            422,
21638            &cicn_resource_with_points(32, 32, &[(0, 0, 0x33), (15, 15, 0x55), (31, 31, 0x77)]),
21639        );
21640        disp.dialog_items.insert(
21641            dialog_ptr,
21642            vec![
21643                DialogItem {
21644                    item_type: 32, // icon item with same-ID cicn override
21645                    rect: (8, 8, 40, 40),
21646                    text: String::new(),
21647                    resource_id: 422,
21648                    proc_ptr: 0,
21649                    sel_start: 0,
21650                    sel_end: 0,
21651                },
21652                DialogItem {
21653                    item_type: 4,
21654                    rect: (44, 8, 54, 40),
21655                    text: "OK".to_string(),
21656                    resource_id: 0,
21657                    proc_ptr: 0,
21658                    sel_start: 0,
21659                    sel_end: 0,
21660                },
21661            ],
21662        );
21663
21664        for (x, y) in [(8u32, 8u32), (23, 23), (39, 39)] {
21665            bus.write_byte(screen_base + y * row_bytes + x, 0x42);
21666        }
21667        cpu.write_reg(Register::A7, TEST_SP);
21668        bus.write_long(TEST_SP, dialog_ptr);
21669        bus.write_long(TEST_SP + 4, 0xCAFE_BABE);
21670
21671        disp.dispatch_dialog(true, 0x181, &mut cpu, &mut bus)
21672            .unwrap()
21673            .unwrap();
21674
21675        (
21676            [
21677                bus.read_byte(screen_base + 8 * row_bytes + 8),
21678                bus.read_byte(screen_base + 23 * row_bytes + 23),
21679                bus.read_byte(screen_base + 39 * row_bytes + 39),
21680            ],
21681            cpu.read_reg(Register::A7),
21682            bus.read_long(TEST_SP + 4),
21683        )
21684    }
21685
21686    fn solid_fill_pict_resource(width: i16, height: i16) -> Vec<u8> {
21687        let mut data = Vec::new();
21688        data.extend_from_slice(&0u16.to_be_bytes()); // picSize placeholder
21689        for value in [0i16, 0, height, width] {
21690            data.extend_from_slice(&(value as u16).to_be_bytes());
21691        }
21692        data.push(0x11); // versionOp
21693        data.push(0x01); // PICT v1
21694        data.push(0x0A); // FillPat
21695        data.extend_from_slice(&[0xFF; 8]);
21696        data.push(0x34); // fillRect
21697        for value in [0i16, 0, height, width] {
21698            data.extend_from_slice(&(value as u16).to_be_bytes());
21699        }
21700        data.push(0xFF); // EndOfPicture
21701        let size = data.len() as u16;
21702        data[0..2].copy_from_slice(&size.to_be_bytes());
21703        data
21704    }
21705
21706    fn drawdialog_picture_item_pixel_results_for_theme(theme_id: UiThemeId) -> ([u8; 3], u32, u32) {
21707        let (mut disp, mut cpu, mut bus) = setup();
21708        disp.set_ui_theme_id(theme_id);
21709        let dialog_ptr = bus.alloc(170);
21710        let (screen_base, row_bytes, _w, _h, pixel_size) = disp.screen_mode;
21711        assert_eq!(pixel_size, 8);
21712
21713        bus.write_word(dialog_ptr + 8, 0);
21714        bus.write_word(dialog_ptr + 10, 0);
21715        bus.write_word(dialog_ptr + 16, 0);
21716        bus.write_word(dialog_ptr + 18, 0);
21717        bus.write_word(dialog_ptr + 20, 48);
21718        bus.write_word(dialog_ptr + 22, 80);
21719        disp.install_test_resource(&mut bus, *b"PICT", 420, &solid_fill_pict_resource(16, 16));
21720        disp.dialog_items.insert(
21721            dialog_ptr,
21722            vec![
21723                DialogItem {
21724                    item_type: 64, // picture item
21725                    rect: (8, 8, 24, 24),
21726                    text: String::new(),
21727                    resource_id: 420,
21728                    proc_ptr: 0,
21729                    sel_start: 0,
21730                    sel_end: 0,
21731                },
21732                DialogItem {
21733                    item_type: 4,
21734                    rect: (30, 8, 42, 40),
21735                    text: "OK".to_string(),
21736                    resource_id: 0,
21737                    proc_ptr: 0,
21738                    sel_start: 0,
21739                    sel_end: 0,
21740                },
21741            ],
21742        );
21743
21744        for (x, y) in [(9u32, 9u32), (16, 16), (22, 22)] {
21745            bus.write_byte(screen_base + y * row_bytes + x, 0x42);
21746        }
21747        cpu.write_reg(Register::A7, TEST_SP);
21748        bus.write_long(TEST_SP, dialog_ptr);
21749        bus.write_long(TEST_SP + 4, 0xCAFE_BABE);
21750
21751        disp.dispatch_dialog(true, 0x181, &mut cpu, &mut bus)
21752            .unwrap()
21753            .unwrap();
21754
21755        (
21756            [
21757                bus.read_byte(screen_base + 9 * row_bytes + 9),
21758                bus.read_byte(screen_base + 16 * row_bytes + 16),
21759                bus.read_byte(screen_base + 22 * row_bytes + 22),
21760            ],
21761            cpu.read_reg(Register::A7),
21762            bus.read_long(TEST_SP + 4),
21763        )
21764    }
21765
21766    fn dialog_tracking_state_for_test(dialog_ptr: u32) -> DialogTrackingState {
21767        DialogTrackingState {
21768            dialog_ptr,
21769            bounds: (0, 0, 0, 0),
21770            title: String::new(),
21771            proc_id: 0,
21772            items: Vec::new(),
21773            default_item: 1,
21774            cancel_item: 2,
21775            edit_text: String::new(),
21776            edit_item: 0,
21777            saved_pixels: Vec::new(),
21778            stack_ptr: TEST_SP,
21779            item_hit_ptr: 0,
21780            rendered_pixels: Vec::new(),
21781            flash_remaining: 0,
21782            flash_delay: 0,
21783            flash_item: 0,
21784            edit_text_modified: false,
21785            draw_proc_queue: VecDeque::new(),
21786            draw_procs_done: true,
21787            rendered_pixels_final: true,
21788            filter_proc: 0,
21789            game_managed: false,
21790            last_filter_event: None,
21791            popup_draws: Vec::new(),
21792            active_popup: None,
21793            active_button: None,
21794            active_user_item: None,
21795        }
21796    }
21797
21798    fn dialog_lifecycle_cleanup_results_for_theme(
21799        theme_id: UiThemeId,
21800    ) -> DialogLifecycleCleanupSnapshot {
21801        let bounds = (100, 100, 150, 200);
21802        let previous_bounds = (0, 0, 342, 512);
21803        let saved_pixels = vec![0x33; 66 * 116];
21804
21805        let (mut close_disp, mut close_cpu, mut close_bus) = setup();
21806        close_disp.set_ui_theme_id(theme_id);
21807        close_disp.set_screen_mode_for_test(0x300000, 640, 640, 480, 8);
21808        let close_dialog_ptr = close_bus.alloc(170);
21809        let close_previous_window = close_bus.alloc(170);
21810        seed_window_regions(&mut close_bus, close_dialog_ptr, bounds);
21811        seed_window_regions(&mut close_bus, close_previous_window, previous_bounds);
21812        close_bus.write_word(close_dialog_ptr + 108, 2);
21813        close_disp.front_window = close_dialog_ptr;
21814        close_disp.current_port = close_dialog_ptr;
21815        close_disp.window_bounds = bounds;
21816        close_disp.window_proc_id = 2;
21817        close_disp.window_list = vec![close_dialog_ptr, close_previous_window];
21818        close_disp.window_stack.push((
21819            close_previous_window,
21820            previous_bounds,
21821            0,
21822            "Previous".to_string(),
21823        ));
21824        close_disp.dialog_items.insert(
21825            close_dialog_ptr,
21826            vec![DialogItem {
21827                item_type: 4,
21828                rect: (20, 40, 40, 100),
21829                text: "OK".to_string(),
21830                ..Default::default()
21831            }],
21832        );
21833        close_disp.dialog_tracking = Some(dialog_tracking_state_for_test(close_dialog_ptr));
21834        close_disp.retained_modal_dialog_click = Some(RetainedModalDialogClickState {
21835            dialog_ptr: close_dialog_ptr,
21836            item_no: 1,
21837            rect: (20, 40, 40, 100),
21838            title: "OK".to_string(),
21839            is_default: true,
21840            highlighted: true,
21841            delivered_to_app: false,
21842        });
21843        close_disp.dialog_visible_snapshots.insert(
21844            close_dialog_ptr,
21845            PersistentDialogSnapshot {
21846                bounds,
21847                pixels: saved_pixels.clone(),
21848            },
21849        );
21850        close_disp.dialog_modal_entered.insert(close_dialog_ptr);
21851        close_disp
21852            .dialog_saved_pixels
21853            .insert(close_dialog_ptr, saved_pixels.clone());
21854        close_bus.write_long(crate::memory::globals::addr::THE_PORT, close_dialog_ptr);
21855        let close_global_ptr = close_bus.read_long(close_cpu.read_reg(Register::A5));
21856        close_bus.write_long(close_global_ptr, close_dialog_ptr);
21857        close_cpu.write_reg(Register::A7, TEST_SP);
21858        close_bus.write_long(TEST_SP, close_dialog_ptr);
21859        close_disp
21860            .dispatch_dialog(true, 0x182, &mut close_cpu, &mut close_bus)
21861            .unwrap()
21862            .unwrap();
21863
21864        let close_stack_after = close_cpu.read_reg(Register::A7);
21865        let close_tracking_cleared = close_disp.dialog_tracking.is_none();
21866        let close_retained_click_cleared = close_disp.retained_modal_dialog_click.is_none();
21867        let close_visible_snapshot_cleared = !close_disp
21868            .dialog_visible_snapshots
21869            .contains_key(&close_dialog_ptr);
21870        let close_modal_entered_cleared =
21871            !close_disp.dialog_modal_entered.contains(&close_dialog_ptr);
21872        let close_saved_pixels_cleared = !close_disp
21873            .dialog_saved_pixels
21874            .contains_key(&close_dialog_ptr);
21875        let close_window_list_contains_dialog = close_disp.window_list.contains(&close_dialog_ptr);
21876        let close_front_window_after = close_disp.front_window;
21877        let close_the_port_after = close_bus.read_long(crate::memory::globals::addr::THE_PORT);
21878        let close_current_port_after = close_disp.current_port;
21879        let close_update_event_for_previous_window = close_disp
21880            .event_queue
21881            .iter()
21882            .any(|event| event.what == 6 && event.message == close_previous_window);
21883        let close_dialog_items_present_after =
21884            close_disp.dialog_items.contains_key(&close_dialog_ptr);
21885
21886        let (mut dispose_disp, mut dispose_cpu, mut dispose_bus) = setup();
21887        dispose_disp.set_ui_theme_id(theme_id);
21888        dispose_disp.set_screen_mode_for_test(0x300000, 640, 640, 480, 8);
21889        let dispose_dialog_ptr = dispose_bus.alloc(170);
21890        let dispose_other_dialog_ptr = dispose_bus.alloc(170);
21891        let dispose_previous_window = dispose_bus.alloc(170);
21892        let dispose_text_handle = dispose_bus.alloc(4);
21893        let dispose_other_text_handle = dispose_bus.alloc(4);
21894        let dispose_ctrl_handle = dispose_bus.alloc(4);
21895        let dispose_other_ctrl_handle = dispose_bus.alloc(4);
21896        seed_window_regions(&mut dispose_bus, dispose_dialog_ptr, bounds);
21897        seed_window_regions(&mut dispose_bus, dispose_previous_window, previous_bounds);
21898        dispose_bus.write_word(dispose_dialog_ptr + 108, 2);
21899        dispose_disp.front_window = dispose_dialog_ptr;
21900        dispose_disp.current_port = dispose_dialog_ptr;
21901        dispose_disp.window_bounds = bounds;
21902        dispose_disp.window_proc_id = 2;
21903        dispose_disp.window_list = vec![
21904            dispose_dialog_ptr,
21905            dispose_previous_window,
21906            dispose_other_dialog_ptr,
21907        ];
21908        dispose_disp.window_stack.push((
21909            dispose_previous_window,
21910            previous_bounds,
21911            0,
21912            "Previous".to_string(),
21913        ));
21914        dispose_disp
21915            .dialog_items
21916            .insert(dispose_dialog_ptr, vec![DialogItem::default()]);
21917        dispose_disp
21918            .dialog_items
21919            .insert(dispose_other_dialog_ptr, vec![DialogItem::default()]);
21920        dispose_disp.dialog_tracking = Some(dialog_tracking_state_for_test(dispose_dialog_ptr));
21921        dispose_disp.retained_modal_dialog_click = Some(RetainedModalDialogClickState {
21922            dialog_ptr: dispose_dialog_ptr,
21923            item_no: 1,
21924            rect: (20, 40, 40, 100),
21925            title: "OK".to_string(),
21926            is_default: true,
21927            highlighted: true,
21928            delivered_to_app: false,
21929        });
21930        dispose_disp.dialog_visible_snapshots.insert(
21931            dispose_dialog_ptr,
21932            PersistentDialogSnapshot {
21933                bounds,
21934                pixels: saved_pixels.clone(),
21935            },
21936        );
21937        dispose_disp.dialog_modal_entered.insert(dispose_dialog_ptr);
21938        dispose_disp
21939            .dialog_saved_pixels
21940            .insert(dispose_dialog_ptr, saved_pixels);
21941        dispose_disp
21942            .dialog_item_handles
21943            .insert(dispose_text_handle, (dispose_dialog_ptr, 0));
21944        dispose_disp
21945            .dialog_item_handles
21946            .insert(dispose_other_text_handle, (dispose_other_dialog_ptr, 0));
21947        dispose_disp
21948            .dialog_control_handles
21949            .insert(dispose_ctrl_handle, (dispose_dialog_ptr, 1));
21950        dispose_disp
21951            .dialog_control_handles
21952            .insert(dispose_other_ctrl_handle, (dispose_other_dialog_ptr, 1));
21953        dispose_disp
21954            .dialog_control_values
21955            .insert((dispose_dialog_ptr, 1), 1);
21956        dispose_disp
21957            .dialog_control_values
21958            .insert((dispose_other_dialog_ptr, 1), 1);
21959        dispose_disp
21960            .hidden_dialog_item_rects
21961            .insert((dispose_dialog_ptr, 1), (10, 20, 30, 40));
21962        dispose_disp
21963            .hidden_dialog_item_rects
21964            .insert((dispose_other_dialog_ptr, 1), (50, 60, 70, 80));
21965        dispose_disp
21966            .dialog_cancel_items
21967            .insert(dispose_dialog_ptr, 2);
21968        dispose_disp
21969            .dialog_cancel_items
21970            .insert(dispose_other_dialog_ptr, 3);
21971        dispose_disp.pending_dialog_popup_menu = Some(PendingDialogPopupMenu {
21972            dialog_ptr: dispose_dialog_ptr,
21973            item_no: 1,
21974            menu_id: 900,
21975            rect: (10, 20, 30, 130),
21976        });
21977        dispose_bus.write_long(crate::memory::globals::addr::THE_PORT, dispose_dialog_ptr);
21978        let dispose_global_ptr = dispose_bus.read_long(dispose_cpu.read_reg(Register::A5));
21979        dispose_bus.write_long(dispose_global_ptr, dispose_dialog_ptr);
21980        dispose_cpu.write_reg(Register::A7, TEST_SP);
21981        dispose_bus.write_long(TEST_SP, dispose_dialog_ptr);
21982        dispose_disp
21983            .dispatch_dialog(true, 0x183, &mut dispose_cpu, &mut dispose_bus)
21984            .unwrap()
21985            .unwrap();
21986
21987        DialogLifecycleCleanupSnapshot {
21988            close_stack_after,
21989            close_tracking_cleared,
21990            close_retained_click_cleared,
21991            close_visible_snapshot_cleared,
21992            close_modal_entered_cleared,
21993            close_saved_pixels_cleared,
21994            close_window_list_contains_dialog,
21995            close_front_window_after,
21996            close_the_port_after,
21997            close_current_port_after,
21998            close_update_event_for_previous_window,
21999            close_dialog_items_present_after,
22000            dispose_stack_after: dispose_cpu.read_reg(Register::A7),
22001            dispose_tracking_cleared: dispose_disp.dialog_tracking.is_none(),
22002            dispose_retained_click_cleared: dispose_disp.retained_modal_dialog_click.is_none(),
22003            dispose_visible_snapshot_cleared: !dispose_disp
22004                .dialog_visible_snapshots
22005                .contains_key(&dispose_dialog_ptr),
22006            dispose_modal_entered_cleared: !dispose_disp
22007                .dialog_modal_entered
22008                .contains(&dispose_dialog_ptr),
22009            dispose_saved_pixels_cleared: !dispose_disp
22010                .dialog_saved_pixels
22011                .contains_key(&dispose_dialog_ptr),
22012            dispose_window_list_contains_dialog: dispose_disp
22013                .window_list
22014                .contains(&dispose_dialog_ptr),
22015            dispose_front_window_after: dispose_disp.front_window,
22016            dispose_the_port_after: dispose_bus.read_long(crate::memory::globals::addr::THE_PORT),
22017            dispose_current_port_after: dispose_disp.current_port,
22018            dispose_update_event_for_previous_window: dispose_disp
22019                .event_queue
22020                .iter()
22021                .any(|event| event.what == 6 && event.message == dispose_previous_window),
22022            dispose_dialog_items_present_after: dispose_disp
22023                .dialog_items
22024                .contains_key(&dispose_dialog_ptr),
22025            dispose_other_dialog_items_present_after: dispose_disp
22026                .dialog_items
22027                .contains_key(&dispose_other_dialog_ptr),
22028            dispose_dialog_item_handle_present_after: dispose_disp
22029                .dialog_item_handles
22030                .contains_key(&dispose_text_handle),
22031            dispose_other_dialog_item_handle_present_after: dispose_disp
22032                .dialog_item_handles
22033                .contains_key(&dispose_other_text_handle),
22034            dispose_dialog_control_handle_present_after: dispose_disp
22035                .dialog_control_handles
22036                .contains_key(&dispose_ctrl_handle),
22037            dispose_other_dialog_control_handle_present_after: dispose_disp
22038                .dialog_control_handles
22039                .contains_key(&dispose_other_ctrl_handle),
22040            dispose_dialog_control_value_present_after: dispose_disp
22041                .dialog_control_values
22042                .contains_key(&(dispose_dialog_ptr, 1)),
22043            dispose_other_dialog_control_value_present_after: dispose_disp
22044                .dialog_control_values
22045                .contains_key(&(dispose_other_dialog_ptr, 1)),
22046            dispose_hidden_rect_present_after: dispose_disp
22047                .hidden_dialog_item_rects
22048                .contains_key(&(dispose_dialog_ptr, 1)),
22049            dispose_other_hidden_rect_present_after: dispose_disp
22050                .hidden_dialog_item_rects
22051                .contains_key(&(dispose_other_dialog_ptr, 1)),
22052            dispose_cancel_item_present_after: dispose_disp
22053                .dialog_cancel_items
22054                .contains_key(&dispose_dialog_ptr),
22055            dispose_other_cancel_item_present_after: dispose_disp
22056                .dialog_cancel_items
22057                .contains_key(&dispose_other_dialog_ptr),
22058            dispose_pending_popup_cleared: dispose_disp.pending_dialog_popup_menu.is_none(),
22059        }
22060    }
22061
22062    fn cached_dialog_items(
22063        disp: &TrapDispatcher,
22064        dialog_ptr: u32,
22065    ) -> Vec<(u8, (i16, i16, i16, i16), String, i16)> {
22066        disp.dialog_items
22067            .get(&dialog_ptr)
22068            .into_iter()
22069            .flatten()
22070            .map(|item| {
22071                (
22072                    item.item_type,
22073                    item.rect,
22074                    item.text.clone(),
22075                    item.resource_id,
22076                )
22077            })
22078            .collect()
22079    }
22080
22081    fn dialog_port_rect(bus: &MacMemoryBus, dialog_ptr: u32) -> (i16, i16, i16, i16) {
22082        (
22083            bus.read_word(dialog_ptr + 16) as i16,
22084            bus.read_word(dialog_ptr + 18) as i16,
22085            bus.read_word(dialog_ptr + 20) as i16,
22086            bus.read_word(dialog_ptr + 22) as i16,
22087        )
22088    }
22089
22090    fn dialog_creation_results_for_theme(theme_id: UiThemeId) -> DialogCreationSnapshot {
22091        let (mut new_disp, mut new_cpu, mut new_bus) = setup();
22092        new_disp.set_ui_theme_id(theme_id);
22093        let new_screen_base = new_bus.alloc((640 * 480) as u32);
22094        new_bus.write_long(0x0824, new_screen_base);
22095        new_disp.screen_mode = (new_screen_base, 640, 640, 480, 8);
22096
22097        let new_ditl = build_test_ditl_items(&[
22098            (16, (10, 12, 28, 120), b"Alpha".as_slice()),
22099            (4, (44, 70, 66, 132), b"OK".as_slice()),
22100        ]);
22101        let new_items_data_ptr = new_bus.alloc(new_ditl.len() as u32);
22102        new_bus.write_bytes(new_items_data_ptr, &new_ditl);
22103        let new_items_handle = new_bus.alloc(4);
22104        new_bus.write_long(new_items_handle, new_items_data_ptr);
22105
22106        let new_title = new_bus.alloc(32);
22107        new_bus.write_pstring(new_title, b"Modeless");
22108        let new_bounds = new_bus.alloc(8);
22109        new_bus.write_word(new_bounds, 60);
22110        new_bus.write_word(new_bounds + 2, 70);
22111        new_bus.write_word(new_bounds + 4, 140);
22112        new_bus.write_word(new_bounds + 6, 220);
22113        let new_storage = new_bus.alloc(170);
22114        let new_sp = TEST_SP - 30;
22115        new_cpu.write_reg(Register::A7, new_sp);
22116        new_bus.write_long(new_sp, new_items_handle);
22117        new_bus.write_long(new_sp + 4, 0x1234_5678);
22118        new_bus.write_byte(new_sp + 8, 0xFF); // goAwayFlag
22119        new_bus.write_long(new_sp + 10, 0xFFFF_FFFF); // behind = front
22120        new_bus.write_word(new_sp + 14, 4); // noGrowDocProc/modeless
22121        new_bus.write_byte(new_sp + 16, 0xFF); // visible
22122        new_bus.write_long(new_sp + 18, new_title);
22123        new_bus.write_long(new_sp + 22, new_bounds);
22124        new_bus.write_long(new_sp + 26, new_storage);
22125        new_bus.write_long(new_sp + 30, 0xDEAD_BEEF);
22126        new_disp
22127            .dispatch_dialog(true, 0x17D, &mut new_cpu, &mut new_bus)
22128            .unwrap()
22129            .unwrap();
22130        let new_dialog_ptr = new_bus.read_long(new_sp + 30);
22131        let new_update_region =
22132            TrapDispatcher::region_handle_rect(&new_bus, new_bus.read_long(new_dialog_ptr + 122));
22133        let new_update_event_queued = new_disp
22134            .event_queue
22135            .iter()
22136            .any(|event| event.what == 6 && event.message == new_dialog_ptr);
22137
22138        let (mut get_disp, mut get_cpu, mut get_bus) = setup();
22139        get_disp.set_ui_theme_id(theme_id);
22140        let get_screen_base = get_bus.alloc((640 * 480) as u32);
22141        get_bus.write_long(0x0824, get_screen_base);
22142        get_disp.screen_mode = (get_screen_base, 640, 640, 480, 8);
22143
22144        let mut dlog = build_test_dlog((80, 90, 160, 240), 1901, 0);
22145        dlog[10] = 1; // visible
22146        let get_ditl = build_test_ditl_items(&[
22147            (16, (12, 18, 30, 128), b"Beta".as_slice()),
22148            (4, (46, 78, 68, 138), b"OK".as_slice()),
22149        ]);
22150        get_disp.install_test_resource(&mut get_bus, *b"DLOG", 1900, &dlog);
22151        let original_ditl_ptr =
22152            get_disp.install_test_resource(&mut get_bus, *b"DITL", 1901, &get_ditl);
22153        let get_storage = get_bus.alloc(170);
22154        get_cpu.write_reg(Register::A7, TEST_SP);
22155        get_bus.write_long(TEST_SP, 0xFFFF_FFFF); // behind = front
22156        get_bus.write_long(TEST_SP + 4, get_storage);
22157        get_bus.write_word(TEST_SP + 8, 1900);
22158        get_bus.write_long(TEST_SP + 10, 0xDEAD_BEEF);
22159        get_disp
22160            .dispatch_dialog(true, 0x17C, &mut get_cpu, &mut get_bus)
22161            .unwrap()
22162            .unwrap();
22163        let get_dialog_ptr = get_bus.read_long(TEST_SP + 10);
22164        let get_items_handle = get_bus.read_long(get_dialog_ptr + 156);
22165        let get_items_data_ptr = get_bus.read_long(get_items_handle);
22166        let get_update_region =
22167            TrapDispatcher::region_handle_rect(&get_bus, get_bus.read_long(get_dialog_ptr + 122));
22168        let get_update_event_queued = get_disp
22169            .event_queue
22170            .iter()
22171            .any(|event| event.what == 6 && event.message == get_dialog_ptr);
22172
22173        DialogCreationSnapshot {
22174            new_dialog_ptr,
22175            new_stack_after: new_cpu.read_reg(Register::A7),
22176            new_result_slot: new_bus.read_long(new_sp + 30),
22177            new_window_list: new_disp.window_list.clone(),
22178            new_front_window: new_disp.front_window,
22179            new_current_port: new_disp.current_port,
22180            new_the_port: new_bus.read_long(crate::memory::globals::addr::THE_PORT),
22181            new_visible_byte: new_bus.read_byte(new_dialog_ptr + 110),
22182            new_goaway_byte: new_bus.read_byte(new_dialog_ptr + 112),
22183            new_refcon: new_bus.read_long(new_dialog_ptr + 152),
22184            new_window_kind: new_bus.read_word(new_dialog_ptr + 108) as i16,
22185            new_proc_id: new_disp
22186                .window_proc_ids
22187                .get(&new_dialog_ptr)
22188                .copied()
22189                .unwrap_or_default(),
22190            new_port_rect: dialog_port_rect(&new_bus, new_dialog_ptr),
22191            new_items_handle: new_bus.read_long(new_dialog_ptr + 156),
22192            new_items_data_ptr: new_bus.read_long(new_items_handle),
22193            new_first_item_handle_nonzero: new_bus.read_long(new_items_data_ptr + 2) != 0,
22194            new_cached_items: cached_dialog_items(&new_disp, new_dialog_ptr),
22195            new_update_region,
22196            new_update_event_queued,
22197            new_edit_field: new_bus.read_word(new_dialog_ptr + 164),
22198            new_default_item: new_bus.read_word(new_dialog_ptr + 168),
22199            get_dialog_ptr,
22200            get_stack_after: get_cpu.read_reg(Register::A7),
22201            get_result_slot: get_bus.read_long(TEST_SP + 10),
22202            get_window_list: get_disp.window_list.clone(),
22203            get_front_window: get_disp.front_window,
22204            get_current_port: get_disp.current_port,
22205            get_the_port: get_bus.read_long(crate::memory::globals::addr::THE_PORT),
22206            get_visible_byte: get_bus.read_byte(get_dialog_ptr + 110),
22207            get_refcon: get_bus.read_long(get_dialog_ptr + 152),
22208            get_window_kind: get_bus.read_word(get_dialog_ptr + 108) as i16,
22209            get_proc_id: get_disp
22210                .window_proc_ids
22211                .get(&get_dialog_ptr)
22212                .copied()
22213                .unwrap_or_default(),
22214            get_port_rect: dialog_port_rect(&get_bus, get_dialog_ptr),
22215            get_items_handle,
22216            get_items_data_ptr,
22217            get_uses_distinct_ditl_copy: get_items_data_ptr != original_ditl_ptr,
22218            get_original_ditl_handle_field: get_bus.read_long(original_ditl_ptr + 2),
22219            get_first_item_handle_nonzero: get_bus.read_long(get_items_data_ptr + 2) != 0,
22220            get_cached_items: cached_dialog_items(&get_disp, get_dialog_ptr),
22221            get_update_region,
22222            get_update_event_queued,
22223            get_edit_field: get_bus.read_word(get_dialog_ptr + 164),
22224            get_default_item: get_bus.read_word(get_dialog_ptr + 168),
22225        }
22226    }
22227
22228    fn alert_family_results_for_theme(theme_id: UiThemeId) -> AlertFamilySnapshot {
22229        let (mut disp, mut cpu, mut bus) = setup();
22230        disp.set_ui_theme_id(theme_id);
22231
22232        let ditl = build_test_ditl_items(&[
22233            (4, (48, 70, 70, 132), b"OK".as_slice()),
22234            // Keep this fixture on the auto-return path while still verifying
22235            // that stage defaults can report item 2.
22236            (0x84, (48, 144, 70, 212), b"Cancel".as_slice()),
22237        ]);
22238        let alrt = build_alrt_template(2100, 0xC4C4);
22239        disp.install_test_resource(&mut bus, *b"DITL", 2100, &ditl);
22240        disp.install_test_resource(&mut bus, *b"ALRT", 2099, &alrt);
22241        bus.write_word(crate::memory::globals::addr::ALERT_STAGE, 0);
22242        bus.write_word(crate::memory::globals::addr::ANUMBER, 0xCAFE);
22243
22244        let mut staged_calls = Vec::new();
22245        for trap_word in [0x185u16, 0x186, 0x187, 0x188, 0x185] {
22246            cpu.write_reg(Register::A7, TEST_SP);
22247            bus.write_word(TEST_SP + 4, 2099);
22248            disp.dispatch_dialog(true, trap_word, &mut cpu, &mut bus)
22249                .unwrap()
22250                .unwrap();
22251            staged_calls.push(AlertTrapCallSnapshot {
22252                trap_word,
22253                result: bus.read_word(TEST_SP + 6) as i16,
22254                stack_after: cpu.read_reg(Register::A7),
22255                alert_stage_after: bus.read_word(crate::memory::globals::addr::ALERT_STAGE),
22256                anumber_after: bus.read_word(crate::memory::globals::addr::ANUMBER),
22257            });
22258        }
22259
22260        cpu.write_reg(Register::A7, TEST_SP);
22261        bus.write_word(crate::memory::globals::addr::ALERT_STAGE, 2);
22262        bus.write_word(crate::memory::globals::addr::ANUMBER, 0xBEEF);
22263        bus.write_word(TEST_SP + 4, 2999);
22264        disp.dispatch_dialog(true, 0x187, &mut cpu, &mut bus)
22265            .unwrap()
22266            .unwrap();
22267        let missing_call = AlertTrapCallSnapshot {
22268            trap_word: 0x187,
22269            result: bus.read_word(TEST_SP + 6) as i16,
22270            stack_after: cpu.read_reg(Register::A7),
22271            alert_stage_after: bus.read_word(crate::memory::globals::addr::ALERT_STAGE),
22272            anumber_after: bus.read_word(crate::memory::globals::addr::ANUMBER),
22273        };
22274
22275        let (mut prep_disp, mut prep_cpu, mut prep_bus) = setup();
22276        prep_disp.set_ui_theme_id(theme_id);
22277        let prep_alrt = build_alrt_template(2101, 0);
22278        prep_disp.install_test_resource(&mut prep_bus, *b"ALRT", 2102, &prep_alrt);
22279        let mut prep_call = |trap_word: u16, alert_id: i16| -> (i16, u32) {
22280            prep_cpu.write_reg(Register::A7, TEST_SP);
22281            prep_bus.write_word(0x0A60, 0x7FFF);
22282            prep_bus.write_word(TEST_SP, alert_id as u16);
22283            prep_disp
22284                .dispatch_dialog(true, trap_word, &mut prep_cpu, &mut prep_bus)
22285                .unwrap()
22286                .unwrap();
22287            (
22288                prep_bus.read_word(0x0A60) as i16,
22289                prep_cpu.read_reg(Register::A7),
22290            )
22291        };
22292        let resource_prep = AlertResourcePrepSnapshot {
22293            could_present: prep_call(0x189, 2102),
22294            free_present: prep_call(0x18A, 2102),
22295            could_missing: prep_call(0x189, 2998),
22296            free_missing: prep_call(0x18A, 2998),
22297        };
22298
22299        AlertFamilySnapshot {
22300            staged_calls,
22301            missing_call,
22302            resource_prep,
22303        }
22304    }
22305
22306    fn dialog_init_sound_results_for_theme(theme_id: UiThemeId) -> DialogInitSoundSnapshot {
22307        let (mut disp, mut cpu, mut bus) = setup();
22308        disp.set_ui_theme_id(theme_id);
22309        use crate::memory::globals::addr;
22310
22311        bus.write_long(addr::RESUME_PROC, 0xDEAD_BEEF);
22312        bus.write_long(addr::DA_BEEPER, 0x00AA_BBCC);
22313        bus.write_word(addr::ALERT_STAGE, 3);
22314        bus.write_word(addr::ANUMBER, 0xCAFE);
22315        for i in 0..4u32 {
22316            bus.write_long(addr::DA_STRINGS + i * 4, 0x00D0_0000 | i);
22317        }
22318
22319        cpu.write_reg(Register::A7, TEST_SP);
22320        bus.write_long(TEST_SP, 0x0012_3456);
22321        disp.dispatch_dialog(true, 0x17B, &mut cpu, &mut bus)
22322            .unwrap()
22323            .unwrap();
22324        let init_stack_after = cpu.read_reg(Register::A7);
22325        let resume_proc_after_init = bus.read_long(addr::RESUME_PROC);
22326        let da_beeper_after_init = bus.read_long(addr::DA_BEEPER);
22327        let alert_stage_after_init = bus.read_word(addr::ALERT_STAGE);
22328        let anumber_after_init = bus.read_word(addr::ANUMBER);
22329        let da_strings_after_init =
22330            std::array::from_fn(|i| bus.read_long(addr::DA_STRINGS + i as u32 * 4));
22331
22332        cpu.write_reg(Register::A7, TEST_SP);
22333        bus.write_long(TEST_SP, 0x00AB_CDEF);
22334        disp.dispatch_dialog(true, 0x18C, &mut cpu, &mut bus)
22335            .unwrap()
22336            .unwrap();
22337        let error_sound_stack_after = cpu.read_reg(Register::A7);
22338        let da_beeper_after_error_sound = bus.read_long(addr::DA_BEEPER);
22339
22340        cpu.write_reg(Register::A7, TEST_SP);
22341        bus.write_long(TEST_SP, 0);
22342        disp.dispatch_dialog(true, 0x18C, &mut cpu, &mut bus)
22343            .unwrap()
22344            .unwrap();
22345        let nil_error_sound_stack_after = cpu.read_reg(Register::A7);
22346        let da_beeper_after_nil_error_sound = bus.read_long(addr::DA_BEEPER);
22347
22348        DialogInitSoundSnapshot {
22349            init_stack_after,
22350            resume_proc_after_init,
22351            da_beeper_after_init,
22352            alert_stage_after_init,
22353            anumber_after_init,
22354            da_strings_after_init,
22355            error_sound_stack_after,
22356            da_beeper_after_error_sound,
22357            nil_error_sound_stack_after,
22358            da_beeper_after_nil_error_sound,
22359        }
22360    }
22361
22362    fn dialogdispatch_default_cancel_results_for_theme(
22363        theme_id: UiThemeId,
22364    ) -> DialogDispatchDefaultCancelSnapshot {
22365        let (mut disp, mut cpu, mut bus) = setup();
22366        disp.set_ui_theme_id(theme_id);
22367        let dialog_ptr = bus.alloc(256);
22368        bus.write_word(dialog_ptr + 168, 1);
22369        disp.dialog_tracking = Some(dialog_tracking_state_for_test(dialog_ptr));
22370
22371        cpu.write_reg(Register::A7, TEST_SP);
22372        bus.write_word(TEST_SP, 9); // newItem
22373        bus.write_long(TEST_SP + 2, dialog_ptr); // theDialog
22374        bus.write_word(TEST_SP + 6, 0xBEEF); // OSErr result slot
22375        cpu.write_reg(Register::D0, 0x0304); // selector 4, 6 param bytes
22376        disp.dispatch_dialog(true, 0x268, &mut cpu, &mut bus)
22377            .unwrap()
22378            .unwrap();
22379        let default_result = bus.read_word(TEST_SP + 6);
22380        let default_stack_after = cpu.read_reg(Register::A7);
22381        let default_adef_item = bus.read_word(dialog_ptr + 168);
22382        let default_tracking_item = disp
22383            .dialog_tracking
22384            .as_ref()
22385            .map(|tracking| tracking.default_item)
22386            .unwrap_or(-1);
22387
22388        cpu.write_reg(Register::A7, TEST_SP);
22389        bus.write_word(TEST_SP, 7); // newItem
22390        bus.write_long(TEST_SP + 2, dialog_ptr); // theDialog
22391        bus.write_word(TEST_SP + 6, 0xCAFE); // OSErr result slot
22392        cpu.write_reg(Register::D0, 0x0305); // selector 5, 6 param bytes
22393        disp.dispatch_dialog(true, 0x268, &mut cpu, &mut bus)
22394            .unwrap()
22395            .unwrap();
22396        let cancel_result = bus.read_word(TEST_SP + 6);
22397        let cancel_stack_after = cpu.read_reg(Register::A7);
22398        let cancel_map_item = disp.dialog_cancel_items.get(&dialog_ptr).copied();
22399        let cancel_tracking_item = disp
22400            .dialog_tracking
22401            .as_ref()
22402            .map(|tracking| tracking.cancel_item)
22403            .unwrap_or(-1);
22404
22405        cpu.write_reg(Register::A7, TEST_SP);
22406        bus.write_word(TEST_SP, 1); // tracks = TRUE
22407        bus.write_long(TEST_SP + 2, dialog_ptr); // theDialog
22408        bus.write_word(TEST_SP + 6, 0xCAFE); // OSErr result slot
22409        cpu.write_reg(Register::D0, 0x0306); // selector 6, 6 param bytes
22410        disp.dispatch_dialog(true, 0x268, &mut cpu, &mut bus)
22411            .unwrap()
22412            .unwrap();
22413        let tracks_result = bus.read_word(TEST_SP + 6);
22414        let tracks_stack_after = cpu.read_reg(Register::A7);
22415        let tracks_adef_item = bus.read_word(dialog_ptr + 168);
22416        let tracks_cancel_map_item = disp.dialog_cancel_items.get(&dialog_ptr).copied();
22417        let tracks_tracking_items = disp
22418            .dialog_tracking
22419            .as_ref()
22420            .map(|tracking| (tracking.default_item, tracking.cancel_item))
22421            .unwrap_or((-1, -1));
22422
22423        DialogDispatchDefaultCancelSnapshot {
22424            default_result,
22425            default_stack_after,
22426            default_adef_item,
22427            default_tracking_item,
22428            cancel_result,
22429            cancel_stack_after,
22430            cancel_map_item,
22431            cancel_tracking_item,
22432            tracks_result,
22433            tracks_stack_after,
22434            tracks_adef_item,
22435            tracks_cancel_map_item,
22436            tracks_tracking_items,
22437        }
22438    }
22439
22440    fn updtdialog_update_region_results_for_theme(
22441        theme_id: UiThemeId,
22442    ) -> UpdtDialogUpdateRegionSnapshot {
22443        let (mut disp, mut cpu, mut bus) = setup_with_port();
22444        disp.set_ui_theme_id(theme_id);
22445
22446        let initial_port = 0x181000;
22447        disp.set_current_port_state(&mut bus, &mut cpu, initial_port, None);
22448
22449        let screen_base = bus.alloc(100 * 100);
22450        bus.write_long(0x0824, screen_base);
22451        disp.screen_mode = (screen_base, 100, 100, 100, 8);
22452
22453        let dialog_ptr = bus.alloc(256);
22454        bus.write_word(dialog_ptr, 0);
22455        bus.write_long(dialog_ptr + 2, screen_base);
22456        bus.write_word(dialog_ptr + 6, 100);
22457        bus.write_word(dialog_ptr + 8, 0);
22458        bus.write_word(dialog_ptr + 10, 0);
22459        bus.write_word(dialog_ptr + 12, 100);
22460        bus.write_word(dialog_ptr + 14, 100);
22461        bus.write_word(dialog_ptr + 16, 0);
22462        bus.write_word(dialog_ptr + 18, 0);
22463        bus.write_word(dialog_ptr + 20, 100);
22464        bus.write_word(dialog_ptr + 22, 100);
22465        bus.write_word(dialog_ptr + 108, 2);
22466        disp.window_list.push(dialog_ptr);
22467        disp.front_window = dialog_ptr;
22468        disp.dialog_initial_draw_deferred.insert(dialog_ptr);
22469        disp.dialog_items.insert(
22470            dialog_ptr,
22471            vec![
22472                DialogItem {
22473                    item_type: 0,
22474                    rect: (8, 8, 24, 32),
22475                    proc_ptr: 0x500000,
22476                    ..Default::default()
22477                },
22478                DialogItem {
22479                    item_type: 0,
22480                    rect: (60, 60, 80, 80),
22481                    proc_ptr: 0x600000,
22482                    ..Default::default()
22483                },
22484                DialogItem {
22485                    item_type: 8,
22486                    rect: (90, 90, 96, 96),
22487                    text: "x".to_string(),
22488                    ..Default::default()
22489                },
22490                DialogItem {
22491                    item_type: 4,
22492                    rect: (52, 8, 72, 44),
22493                    text: "OK".to_string(),
22494                    ..Default::default()
22495                },
22496            ],
22497        );
22498        let inside_update_pixel = screen_base + 4 * 100 + 4;
22499        let outside_update_pixel = screen_base + 70 * 100 + 70;
22500        let outside_item_pixel = screen_base + 52 * 100 + 9;
22501        bus.write_byte(inside_update_pixel, 0xA5);
22502        bus.write_byte(outside_update_pixel, 0x5A);
22503        bus.write_byte(outside_item_pixel, 0x3C);
22504
22505        let update_rgn_ptr = bus.alloc(10);
22506        let update_rgn = bus.alloc(4);
22507        bus.write_long(update_rgn, update_rgn_ptr);
22508        bus.write_word(update_rgn_ptr, 10);
22509        bus.write_word(update_rgn_ptr + 2, 0);
22510        bus.write_word(update_rgn_ptr + 4, 0);
22511        bus.write_word(update_rgn_ptr + 6, 40);
22512        bus.write_word(update_rgn_ptr + 8, 40);
22513
22514        cpu.write_reg(Register::A7, TEST_SP);
22515        bus.write_long(TEST_SP, update_rgn);
22516        bus.write_long(TEST_SP + 4, dialog_ptr);
22517        disp.dispatch_dialog(true, 0x178, &mut cpu, &mut bus)
22518            .unwrap()
22519            .unwrap();
22520
22521        UpdtDialogUpdateRegionSnapshot {
22522            stack_after: cpu.read_reg(Register::A7),
22523            the_port_after: bus.read_long(crate::memory::globals::addr::THE_PORT),
22524            current_port_after: disp.current_port,
22525            deferred_after: disp.dialog_initial_draw_deferred.contains(&dialog_ptr),
22526            queued_draw_procs: disp
22527                .modeless_dialog_draw_proc_queue
22528                .iter()
22529                .copied()
22530                .collect(),
22531            item_count_after: disp
22532                .dialog_items
22533                .get(&dialog_ptr)
22534                .map(Vec::len)
22535                .unwrap_or_default(),
22536            inside_update_pixel_after: bus.read_byte(inside_update_pixel),
22537            outside_update_pixel_after: bus.read_byte(outside_update_pixel),
22538            outside_item_pixel_after: bus.read_byte(outside_item_pixel),
22539        }
22540    }
22541
22542    #[test]
22543    fn systemless_theme_does_not_change_dialogselect_item_hits() {
22544        // IM:I I-417: DialogSelect reports enabled dialog item hits through
22545        // its Boolean result, dialog pointer, and itemHit. Theme rendering is
22546        // outside that guest-visible event contract.
22547        let classic = dialog_select_enabled_user_item_hit_for_theme(UiThemeId::ClassicSystem7);
22548        let themed = dialog_select_enabled_user_item_hit_for_theme(UiThemeId::SystemlessDefault);
22549
22550        assert_eq!(classic.0, 0xFFFF);
22551        assert_eq!(classic.2, 1);
22552        assert_eq!(classic.3, TEST_SP + 12);
22553        assert_eq!(
22554            themed, classic,
22555            "systemless-default must not change DialogSelect item-hit semantics"
22556        );
22557    }
22558
22559    #[test]
22560    fn systemless_theme_does_not_change_dialogselect_control_item_hits() {
22561        // IM:I I-417: for mouseDown in an enabled control, DialogSelect
22562        // returns TRUE and itemHit after Control Manager tracking succeeds.
22563        // The app owns checkbox value changes, so theme chrome must not alter
22564        // the ControlRecord value or dialog-scoped control-value mirror.
22565        let classic = dialog_select_enabled_checkbox_hit_for_theme(UiThemeId::ClassicSystem7);
22566        let themed = dialog_select_enabled_checkbox_hit_for_theme(UiThemeId::SystemlessDefault);
22567
22568        assert_eq!(classic.0, 0xFFFF);
22569        assert_eq!(classic.2, 1);
22570        assert_eq!(classic.3, TEST_SP + 12);
22571        assert_eq!(classic.4, 1);
22572        assert_eq!(classic.5, 1);
22573        assert_eq!(
22574            themed, classic,
22575            "systemless-default must not change DialogSelect control item-hit or value semantics"
22576        );
22577    }
22578
22579    #[test]
22580    fn systemless_theme_does_not_change_dialogselect_window_events() {
22581        // MTE 1992 p. 6-139 and IM:I I-417: activate/update events for a
22582        // dialog window are handled by DialogSelect and return FALSE rather
22583        // than reporting an enabled item through theDialog/itemHit.
22584        // MTE 1992 pp. 6-141 and 6-143 plus IM:I I-291: update handling
22585        // brackets the redraw with BeginUpdate/EndUpdate, uses the dialog
22586        // port, redraws the dialog, and leaves app-owned userItem drawing
22587        // to the guest draw procs.
22588        // Theme rendering can change pixels, but not this function ABI,
22589        // output-slot behavior, stack protocol, or update bookkeeping.
22590        let classic = dialog_select_window_event_results_for_theme(UiThemeId::ClassicSystem7);
22591        let themed = dialog_select_window_event_results_for_theme(UiThemeId::SystemlessDefault);
22592
22593        assert_eq!(classic.update_result, 0);
22594        assert_eq!(classic.update_dialog_out, 0xDEAD_BEEF);
22595        assert_eq!(classic.update_item_hit, 0xCAFE);
22596        assert_eq!(classic.update_stack_after, TEST_SP + 12);
22597        assert!(!classic.update_deferred_after);
22598        assert_eq!(classic.update_region_after, None);
22599        assert_eq!(classic.update_vis_region_after, Some((0, 0, 100, 100)));
22600        assert_eq!(
22601            classic.update_the_port_after,
22602            classic.update_current_port_after
22603        );
22604        assert_eq!(classic.update_current_port_after, classic.dialog_ptr);
22605        assert!(!classic.update_saved_vis_after);
22606        assert_eq!(
22607            classic.update_queued_draw_procs,
22608            vec![(classic.dialog_ptr, 0x500000, 1)]
22609        );
22610        assert_eq!(classic.update_item_count_after, 3);
22611        assert_eq!(classic.activate_result, 0);
22612        assert_eq!(classic.activate_dialog_out, 0xDEAD_BEEF);
22613        assert_eq!(classic.activate_item_hit, 0xCAFE);
22614        assert_eq!(classic.activate_stack_after, TEST_SP + 12);
22615        assert_eq!(
22616            themed, classic,
22617            "systemless-default must not change DialogSelect update/activate handling"
22618        );
22619    }
22620
22621    #[test]
22622    fn systemless_theme_does_not_change_modaldialog_edit_text_mouse_handling() {
22623        // IM:I I-415: ModalDialog handles mouseDown in an editText item using
22624        // TextEdit and returns the item when it is enabled. Theme rendering
22625        // must not change the returned item, retained-dialog state, active
22626        // editField, TERecord mirroring, queued mouse-up consumption, or stack
22627        // protocol.
22628        let classic = modal_dialog_edit_text_mouse_results_for_theme(UiThemeId::ClassicSystem7);
22629        let themed = modal_dialog_edit_text_mouse_results_for_theme(UiThemeId::SystemlessDefault);
22630
22631        assert_eq!(classic.item_hit, 2);
22632        assert_eq!(classic.stack_after, TEST_SP + 8);
22633        assert!(classic.tracking_finished);
22634        assert!(classic.retained_visible_snapshot);
22635        assert!(classic.saved_background_retained);
22636        assert!(classic.queued_mouse_up_consumed);
22637        assert_eq!(classic.edit_field, 1);
22638        assert_eq!(classic.item_selection, (2, 4));
22639        assert_eq!(classic.te_text, b"Second".to_vec());
22640        assert_eq!(classic.te_length, 6);
22641        assert_eq!(classic.te_selection, (2, 4));
22642        assert_eq!(classic.handle_bytes, b"Second".to_vec());
22643        assert_eq!(
22644            themed, classic,
22645            "systemless-default must not change ModalDialog editText mouse handling"
22646        );
22647    }
22648
22649    #[test]
22650    fn systemless_theme_does_not_change_modaldialog_keyboard_default_cancel_items() {
22651        // MTE 1992 p. 6-30 and p. 6-138: Return/Enter activate the default
22652        // button; Esc and Command-period activate the Cancel button. Theme
22653        // chrome must not change the key-to-item mapping, flash lifecycle,
22654        // returned itemHit, or Pascal stack protocol.
22655        let classic = modal_dialog_keyboard_button_results_for_theme(UiThemeId::ClassicSystem7);
22656        let themed = modal_dialog_keyboard_button_results_for_theme(UiThemeId::SystemlessDefault);
22657
22658        for case in [&classic.return_key, &classic.enter_key] {
22659            assert_eq!(case.flash_item_after_key, 1);
22660            assert_eq!(case.stack_after_key, TEST_SP);
22661            assert_eq!(case.item_hit_after_key, 0xCAFE);
22662            assert_eq!(case.item_hit_after_flash, 1);
22663            assert_eq!(case.stack_after_flash, TEST_SP + 8);
22664            assert!(case.tracking_finished);
22665        }
22666        for case in [&classic.escape_key, &classic.command_period] {
22667            assert_eq!(case.flash_item_after_key, 2);
22668            assert_eq!(case.stack_after_key, TEST_SP);
22669            assert_eq!(case.item_hit_after_key, 0xCAFE);
22670            assert_eq!(case.item_hit_after_flash, 2);
22671            assert_eq!(case.stack_after_flash, TEST_SP + 8);
22672            assert!(case.tracking_finished);
22673        }
22674        assert_eq!(
22675            themed, classic,
22676            "systemless-default must not change ModalDialog keyboard default/cancel handling"
22677        );
22678    }
22679
22680    #[test]
22681    fn systemless_theme_does_not_change_modaldialog_update_events() {
22682        // MTE 1992 pp. 6-135 and 6-141: ModalDialog routes update events
22683        // through IsDialogEvent/DialogSelect-style handling. That brackets
22684        // the redraw with BeginUpdate/EndUpdate and makes the dialog the
22685        // current graphics port, while ModalDialog itself does not return
22686        // an item for update events. Theme chrome must not alter this ABI or
22687        // the retained visible dialog snapshot used across modal refires.
22688        let classic = modal_dialog_update_event_results_for_theme(UiThemeId::ClassicSystem7);
22689        let themed = modal_dialog_update_event_results_for_theme(UiThemeId::SystemlessDefault);
22690
22691        assert_eq!(classic.item_hit_after, 0xCAFE);
22692        assert_eq!(classic.stack_after, TEST_SP);
22693        assert!(!classic.tracking_finished);
22694        assert!(classic.rendered_pixels_final);
22695        assert_eq!(classic.retained_pixel_after, 0x44);
22696        assert_eq!(classic.retained_snapshot_pixel_after, 0x44);
22697        assert_eq!(classic.update_region_after, None);
22698        assert_eq!(classic.vis_region_after, Some((0, 0, 80, 80)));
22699        assert_eq!(classic.the_port_after, classic.current_port_after);
22700        assert_eq!(classic.current_port_after, classic.dialog_ptr);
22701        assert!(!classic.saved_vis_after);
22702        assert_eq!(
22703            themed, classic,
22704            "systemless-default must not change ModalDialog update-event handling"
22705        );
22706    }
22707
22708    #[test]
22709    fn systemless_theme_does_not_change_dialog_lifecycle_cleanup() {
22710        // MTE 1992 pp. 6-119..6-120: CloseDialog removes the dialog from
22711        // the screen/window list, while DisposeDialog calls CloseDialog and
22712        // additionally releases dialog-owned item-list/dialog-record storage.
22713        // Theme chrome must not alter Pascal stack discipline, active
22714        // ModalDialog cleanup, retained-click cleanup, saved snapshot
22715        // cleanup, front-window promotion, current-port side effects, or
22716        // dialog-scoped side-map cleanup.
22717        let classic = dialog_lifecycle_cleanup_results_for_theme(UiThemeId::ClassicSystem7);
22718        let themed = dialog_lifecycle_cleanup_results_for_theme(UiThemeId::SystemlessDefault);
22719
22720        assert_eq!(classic.close_stack_after, TEST_SP + 4);
22721        assert!(classic.close_tracking_cleared);
22722        assert!(classic.close_retained_click_cleared);
22723        assert!(classic.close_visible_snapshot_cleared);
22724        assert!(classic.close_modal_entered_cleared);
22725        assert!(classic.close_saved_pixels_cleared);
22726        assert!(!classic.close_window_list_contains_dialog);
22727        assert_eq!(
22728            classic.close_front_window_after,
22729            classic.close_the_port_after
22730        );
22731        assert_eq!(
22732            classic.close_front_window_after,
22733            classic.close_current_port_after
22734        );
22735        assert!(classic.close_update_event_for_previous_window);
22736        assert!(!classic.close_dialog_items_present_after);
22737
22738        assert_eq!(classic.dispose_stack_after, TEST_SP + 4);
22739        assert!(classic.dispose_tracking_cleared);
22740        assert!(classic.dispose_retained_click_cleared);
22741        assert!(classic.dispose_visible_snapshot_cleared);
22742        assert!(classic.dispose_modal_entered_cleared);
22743        assert!(classic.dispose_saved_pixels_cleared);
22744        assert!(!classic.dispose_window_list_contains_dialog);
22745        assert_eq!(
22746            classic.dispose_front_window_after,
22747            classic.dispose_the_port_after
22748        );
22749        assert_eq!(
22750            classic.dispose_front_window_after,
22751            classic.dispose_current_port_after
22752        );
22753        assert!(classic.dispose_update_event_for_previous_window);
22754        assert!(!classic.dispose_dialog_items_present_after);
22755        assert!(classic.dispose_other_dialog_items_present_after);
22756        assert!(!classic.dispose_dialog_item_handle_present_after);
22757        assert!(classic.dispose_other_dialog_item_handle_present_after);
22758        assert!(!classic.dispose_dialog_control_handle_present_after);
22759        assert!(classic.dispose_other_dialog_control_handle_present_after);
22760        assert!(!classic.dispose_dialog_control_value_present_after);
22761        assert!(classic.dispose_other_dialog_control_value_present_after);
22762        assert!(!classic.dispose_hidden_rect_present_after);
22763        assert!(classic.dispose_other_hidden_rect_present_after);
22764        assert!(!classic.dispose_cancel_item_present_after);
22765        assert!(classic.dispose_other_cancel_item_present_after);
22766        assert!(classic.dispose_pending_popup_cleared);
22767        assert_eq!(
22768            themed, classic,
22769            "systemless-default must not change CloseDialog/DisposeDialog lifecycle cleanup"
22770        );
22771    }
22772
22773    #[test]
22774    fn systemless_theme_does_not_change_dialog_creation_records() {
22775        // IM:I I-412 and MTE 1992 pp. 6-118..6-119: NewDialog creates a
22776        // dialog from caller-supplied parameters and an item-list handle,
22777        // sets dialogKind/default dialog fields, and returns a DialogPtr.
22778        // IM:I I-412 and I-424: GetNewDialog reads DLOG/DITL resources and
22779        // uses a copy of the item list. Theme chrome must not alter resource
22780        // parsing, item-list ownership, DialogRecord fields, visible/update
22781        // bookkeeping, current-port side effects, or Pascal stack ABI.
22782        let classic = dialog_creation_results_for_theme(UiThemeId::ClassicSystem7);
22783        let themed = dialog_creation_results_for_theme(UiThemeId::SystemlessDefault);
22784
22785        assert_ne!(classic.new_dialog_ptr, 0);
22786        assert_eq!(classic.new_stack_after, TEST_SP);
22787        assert_eq!(classic.new_result_slot, classic.new_dialog_ptr);
22788        assert_eq!(classic.new_window_list, vec![classic.new_dialog_ptr]);
22789        assert_eq!(classic.new_front_window, classic.new_dialog_ptr);
22790        assert_eq!(classic.new_current_port, classic.new_dialog_ptr);
22791        assert_eq!(classic.new_the_port, classic.new_dialog_ptr);
22792        assert_eq!(classic.new_visible_byte, 0xFF);
22793        assert_eq!(classic.new_goaway_byte, 0xFF);
22794        assert_eq!(classic.new_refcon, 0x1234_5678);
22795        assert_eq!(classic.new_window_kind, 2);
22796        assert_eq!(classic.new_proc_id, 4);
22797        assert_eq!(classic.new_port_rect, (0, 0, 80, 150));
22798        assert_ne!(classic.new_items_handle, 0);
22799        assert_ne!(classic.new_items_data_ptr, 0);
22800        assert!(classic.new_first_item_handle_nonzero);
22801        assert_eq!(
22802            classic.new_cached_items,
22803            vec![
22804                (16, (10, 12, 28, 120), "Alpha".to_string(), 0),
22805                (4, (44, 70, 66, 132), "OK".to_string(), 0),
22806            ]
22807        );
22808        assert!(classic.new_update_region.is_some());
22809        assert!(classic.new_update_event_queued);
22810        assert_eq!(classic.new_edit_field, 0xFFFF);
22811        assert_eq!(classic.new_default_item, 1);
22812
22813        assert_ne!(classic.get_dialog_ptr, 0);
22814        assert_eq!(classic.get_stack_after, TEST_SP + 10);
22815        assert_eq!(classic.get_result_slot, classic.get_dialog_ptr);
22816        assert_eq!(classic.get_window_list, vec![classic.get_dialog_ptr]);
22817        assert_eq!(classic.get_front_window, classic.get_dialog_ptr);
22818        assert_eq!(classic.get_current_port, classic.get_dialog_ptr);
22819        assert_eq!(classic.get_the_port, classic.get_dialog_ptr);
22820        assert_eq!(classic.get_visible_byte, 0xFF);
22821        assert_eq!(classic.get_refcon, 0);
22822        assert_eq!(classic.get_window_kind, 2);
22823        assert_eq!(classic.get_proc_id, 2);
22824        assert_eq!(classic.get_port_rect, (0, 0, 80, 150));
22825        assert_ne!(classic.get_items_handle, 0);
22826        assert_ne!(classic.get_items_data_ptr, 0);
22827        assert!(classic.get_uses_distinct_ditl_copy);
22828        assert_eq!(classic.get_original_ditl_handle_field, 0);
22829        assert!(classic.get_first_item_handle_nonzero);
22830        assert_eq!(
22831            classic.get_cached_items,
22832            vec![
22833                (16, (12, 18, 30, 128), "Beta".to_string(), 0),
22834                (4, (46, 78, 68, 138), "OK".to_string(), 0),
22835            ]
22836        );
22837        assert!(classic.get_update_region.is_some());
22838        assert!(classic.get_update_event_queued);
22839        assert_eq!(classic.get_edit_field, 0xFFFF);
22840        assert_eq!(classic.get_default_item, 1);
22841        assert_eq!(
22842            themed, classic,
22843            "systemless-default must not change NewDialog/GetNewDialog creation semantics"
22844        );
22845    }
22846
22847    #[test]
22848    fn systemless_theme_does_not_change_alert_family_resource_state() {
22849        // IM:I I-417..I-423 and MTE 1992 pp. 6-105..6-112: Alert,
22850        // StopAlert, NoteAlert, and CautionAlert read ALRT/DITL resources,
22851        // use the current alert stage to choose the default item, update
22852        // ACount/ANumber low-memory state, and share behavior apart from
22853        // icon drawing. IM:I I-420 also defines CouldAlert/FreeAlert as
22854        // resource-preparation routines. Theme chrome must not alter those
22855        // resource, low-memory, result-slot, or stack contracts.
22856        let classic = alert_family_results_for_theme(UiThemeId::ClassicSystem7);
22857        let themed = alert_family_results_for_theme(UiThemeId::SystemlessDefault);
22858
22859        assert_eq!(
22860            classic.staged_calls,
22861            vec![
22862                AlertTrapCallSnapshot {
22863                    trap_word: 0x185,
22864                    result: 1,
22865                    stack_after: TEST_SP + 6,
22866                    alert_stage_after: 1,
22867                    anumber_after: 2099,
22868                },
22869                AlertTrapCallSnapshot {
22870                    trap_word: 0x186,
22871                    result: 2,
22872                    stack_after: TEST_SP + 6,
22873                    alert_stage_after: 2,
22874                    anumber_after: 2099,
22875                },
22876                AlertTrapCallSnapshot {
22877                    trap_word: 0x187,
22878                    result: 1,
22879                    stack_after: TEST_SP + 6,
22880                    alert_stage_after: 3,
22881                    anumber_after: 2099,
22882                },
22883                AlertTrapCallSnapshot {
22884                    trap_word: 0x188,
22885                    result: 2,
22886                    stack_after: TEST_SP + 6,
22887                    alert_stage_after: 3,
22888                    anumber_after: 2099,
22889                },
22890                AlertTrapCallSnapshot {
22891                    trap_word: 0x185,
22892                    result: 2,
22893                    stack_after: TEST_SP + 6,
22894                    alert_stage_after: 3,
22895                    anumber_after: 2099,
22896                },
22897            ]
22898        );
22899        assert_eq!(
22900            classic.missing_call,
22901            AlertTrapCallSnapshot {
22902                trap_word: 0x187,
22903                result: -1,
22904                stack_after: TEST_SP + 6,
22905                alert_stage_after: 2,
22906                anumber_after: 0xBEEF,
22907            }
22908        );
22909        assert_eq!(
22910            classic.resource_prep,
22911            AlertResourcePrepSnapshot {
22912                could_present: (0, TEST_SP + 2),
22913                free_present: (0, TEST_SP + 2),
22914                could_missing: (0, TEST_SP + 2),
22915                free_missing: (0, TEST_SP + 2),
22916            }
22917        );
22918        assert_eq!(
22919            themed, classic,
22920            "systemless-default must not change Alert family resource or low-memory semantics"
22921        );
22922    }
22923
22924    #[test]
22925    fn systemless_theme_does_not_change_initdialogs_errorsound_state() {
22926        // IM:I I-411 and MTE 1992 pp. 6-103..6-104: InitDialogs stores
22927        // ResumeProc, initializes alert sound state, and prepares the
22928        // ParamText/DAStrings globals; ErrorSound stores the current alert
22929        // sound ProcPtr in DABeeper, with NIL disabling sound/menu-bar blink.
22930        // Those low-memory globals and procedure stack pops are app-visible
22931        // Dialog Manager state and must not depend on the selected renderer.
22932        let classic = dialog_init_sound_results_for_theme(UiThemeId::ClassicSystem7);
22933        let themed = dialog_init_sound_results_for_theme(UiThemeId::SystemlessDefault);
22934
22935        assert_eq!(classic.init_stack_after, TEST_SP + 4);
22936        assert_eq!(classic.resume_proc_after_init, 0x0012_3456);
22937        assert_eq!(classic.da_beeper_after_init, 0);
22938        assert_eq!(classic.alert_stage_after_init, 0);
22939        assert_eq!(classic.anumber_after_init, 0xCAFE);
22940        assert_eq!(classic.da_strings_after_init, [0, 0, 0, 0]);
22941        assert_eq!(classic.error_sound_stack_after, TEST_SP + 4);
22942        assert_eq!(classic.da_beeper_after_error_sound, 0x00AB_CDEF);
22943        assert_eq!(classic.nil_error_sound_stack_after, TEST_SP + 4);
22944        assert_eq!(classic.da_beeper_after_nil_error_sound, 0);
22945        assert_eq!(
22946            themed, classic,
22947            "systemless-default must not change InitDialogs/ErrorSound low-memory state"
22948        );
22949    }
22950
22951    #[test]
22952    fn systemless_theme_does_not_change_dialogdispatch_default_cancel_selectors() {
22953        // MTE 1992 pp. 6-162..6-166: DialogDispatch selectors extend the
22954        // Dialog Manager with default-item, cancel-item, and cursor-tracking
22955        // state. MTE 1992 p. 6-138 ties Return/Enter and Esc/Command-period
22956        // handling to the default and Cancel items, so theme chrome must not
22957        // alter selector OSErr results, stack ABI, DialogRecord.aDefItem, the
22958        // cancel-item side state, or active ModalDialog tracking mirrors.
22959        let classic = dialogdispatch_default_cancel_results_for_theme(UiThemeId::ClassicSystem7);
22960        let themed = dialogdispatch_default_cancel_results_for_theme(UiThemeId::SystemlessDefault);
22961
22962        assert_eq!(classic.default_result, 0);
22963        assert_eq!(classic.default_stack_after, TEST_SP + 6);
22964        assert_eq!(classic.default_adef_item, 9);
22965        assert_eq!(classic.default_tracking_item, 9);
22966
22967        assert_eq!(classic.cancel_result, 0);
22968        assert_eq!(classic.cancel_stack_after, TEST_SP + 6);
22969        assert_eq!(classic.cancel_map_item, Some(7));
22970        assert_eq!(classic.cancel_tracking_item, 7);
22971
22972        assert_eq!(classic.tracks_result, 0);
22973        assert_eq!(classic.tracks_stack_after, TEST_SP + 6);
22974        assert_eq!(classic.tracks_adef_item, 9);
22975        assert_eq!(classic.tracks_cancel_map_item, Some(7));
22976        assert_eq!(classic.tracks_tracking_items, (9, 7));
22977        assert_eq!(
22978            themed, classic,
22979            "systemless-default must not change DialogDispatch default/cancel selector semantics"
22980        );
22981    }
22982
22983    #[test]
22984    fn systemless_theme_does_not_change_updtdialog_update_region_semantics() {
22985        // MTE 1992 pp. 6-142..6-143: UpdateDialog redraws only items in the
22986        // supplied update region, calls application-defined item draw procs,
22987        // and uses SetPort to make the dialog box current before updating.
22988        // Theme chrome can change pixels but must not change that update-item
22989        // selection, current-port side effect, deferred redraw state, or stack
22990        // ABI.
22991        let classic = updtdialog_update_region_results_for_theme(UiThemeId::ClassicSystem7);
22992        let themed = updtdialog_update_region_results_for_theme(UiThemeId::SystemlessDefault);
22993
22994        assert_eq!(classic.stack_after, TEST_SP + 8);
22995        assert_ne!(classic.the_port_after, 0x181000);
22996        assert_eq!(classic.the_port_after, classic.current_port_after);
22997        assert!(!classic.deferred_after);
22998        assert_eq!(classic.queued_draw_procs.len(), 1);
22999        assert_eq!(classic.queued_draw_procs[0].0, classic.the_port_after);
23000        assert_eq!(classic.queued_draw_procs[0].1, 0x500000);
23001        assert_eq!(classic.queued_draw_procs[0].2, 1);
23002        assert_eq!(classic.item_count_after, 4);
23003        assert_eq!(classic.inside_update_pixel_after, 0);
23004        assert_eq!(classic.outside_update_pixel_after, 0x5A);
23005        assert_eq!(classic.outside_item_pixel_after, 0x3C);
23006        assert_eq!(
23007            themed, classic,
23008            "systemless-default must not change UpdtDialog update-region semantics"
23009        );
23010    }
23011
23012    #[test]
23013    fn systemless_theme_does_not_change_dialogselect_edit_text_mouse_and_null_events() {
23014        // MTE 1992 p. 6-139 and IM:I I-417: mouseDown in an enabled
23015        // editText item is handled through TextEdit and reports the item,
23016        // while null events with an editText item call TEIdle and return
23017        // FALSE. Systemless TEClick/TEIdle preserve the TERec text/selection,
23018        // so theme rendering must not alter the active editField, result slots,
23019        // output-slot behavior, TERecord mirroring, or stack protocol.
23020        let classic = dialog_select_edit_text_mouse_results_for_theme(UiThemeId::ClassicSystem7);
23021        let themed = dialog_select_edit_text_mouse_results_for_theme(UiThemeId::SystemlessDefault);
23022
23023        assert_eq!(classic.mouse_result, 0xFFFF);
23024        assert_ne!(classic.mouse_dialog_out, 0);
23025        assert_eq!(classic.mouse_item_hit, 2);
23026        assert_eq!(classic.mouse_stack_after, TEST_SP + 12);
23027        assert_eq!(classic.mouse_edit_field, 1);
23028        assert_eq!(classic.mouse_item_selection, (2, 4));
23029        assert_eq!(classic.mouse_te_text, b"Second".to_vec());
23030        assert_eq!(classic.mouse_te_length, 6);
23031        assert_eq!(classic.mouse_te_selection, (2, 4));
23032        assert_eq!(classic.null_result, 0);
23033        assert_eq!(classic.null_dialog_out, 0xDEAD_BEEF);
23034        assert_eq!(classic.null_item_hit, 0xCAFE);
23035        assert_eq!(classic.null_stack_after, TEST_SP + 12);
23036        assert_eq!(classic.null_edit_field, 1);
23037        assert_eq!(classic.null_te_text, b"Second".to_vec());
23038        assert_eq!(classic.null_te_length, 6);
23039        assert_eq!(classic.null_te_selection, (2, 4));
23040        assert_eq!(
23041            themed, classic,
23042            "systemless-default must not change DialogSelect editText mouse/null handling"
23043        );
23044    }
23045
23046    #[test]
23047    fn systemless_theme_does_not_change_dialogselect_edit_text_key_handling() {
23048        // MTE 1992 p. 6-139: for keyDown/autoKey events in editable text
23049        // items, DialogSelect uses TextEdit to handle text entry/editing and
23050        // reports the edit item through itemHit. Text mutation, selection
23051        // collapse, shared TERecord state, result slots, and stack ABI are
23052        // guest-visible Dialog Manager behavior, not theme rendering choices.
23053        let classic = dialog_select_edit_text_key_results_for_theme(UiThemeId::ClassicSystem7);
23054        let themed = dialog_select_edit_text_key_results_for_theme(UiThemeId::SystemlessDefault);
23055
23056        assert_eq!(classic.keydown_result, 0xFFFF);
23057        assert_ne!(classic.keydown_dialog_out, 0);
23058        assert_eq!(classic.keydown_item_hit, 1);
23059        assert_eq!(classic.keydown_stack_after, TEST_SP + 12);
23060        assert_eq!(classic.keydown_handle_bytes, b"HYo".to_vec());
23061        assert_eq!(classic.keydown_cached_text, "HYo");
23062        assert_eq!(classic.keydown_item_selection, (2, 2));
23063        assert_eq!(classic.keydown_te_text, b"HYo".to_vec());
23064        assert_eq!(classic.keydown_te_length, 3);
23065        assert_eq!(classic.keydown_te_selection, (2, 2));
23066        assert_eq!(classic.autokey_result, 0xFFFF);
23067        assert_eq!(classic.autokey_dialog_out, classic.keydown_dialog_out);
23068        assert_eq!(classic.autokey_item_hit, 1);
23069        assert_eq!(classic.autokey_stack_after, TEST_SP + 12);
23070        assert_eq!(classic.autokey_handle_bytes, b"Ho".to_vec());
23071        assert_eq!(classic.autokey_cached_text, "Ho");
23072        assert_eq!(classic.autokey_item_selection, (1, 1));
23073        assert_eq!(classic.autokey_te_text, b"Ho".to_vec());
23074        assert_eq!(classic.autokey_te_length, 2);
23075        assert_eq!(classic.autokey_te_selection, (1, 1));
23076        assert_eq!(
23077            themed, classic,
23078            "systemless-default must not change DialogSelect editText key handling"
23079        );
23080    }
23081
23082    #[test]
23083    fn systemless_theme_does_not_change_isdialogevent_active_dialog_routing() {
23084        // MTE 1992 p. 6-138: IsDialogEvent returns TRUE for events that
23085        // belong to an active dialog, including null events; IM:I I-416 also
23086        // calls out active-dialog key events, mouse-downs in the content
23087        // region, and update/activate events targeting a dialog window.
23088        // Themes must not alter that event-routing or function-stack ABI.
23089        let classic = is_dialog_event_results_for_theme(UiThemeId::ClassicSystem7);
23090        let themed = is_dialog_event_results_for_theme(UiThemeId::SystemlessDefault);
23091
23092        assert_eq!(classic.null_event, (0xFFFF, TEST_SP + 4));
23093        assert_eq!(classic.key_down, (0xFFFF, TEST_SP + 4));
23094        assert_eq!(classic.mouse_inside, (0xFFFF, TEST_SP + 4));
23095        assert_eq!(classic.mouse_outside, (0, TEST_SP + 4));
23096        assert_eq!(classic.update_target, (0xFFFF, TEST_SP + 4));
23097        assert_eq!(classic.activate_target, (0xFFFF, TEST_SP + 4));
23098        assert_eq!(
23099            themed, classic,
23100            "systemless-default must not change IsDialogEvent routing"
23101        );
23102    }
23103
23104    #[test]
23105    fn systemless_theme_does_not_change_findditem_hit_testing() {
23106        // MTE 1992 p. 6-125: FindDialogItem/FindDItem returns the first
23107        // item-list index containing the local point, including disabled
23108        // items; themes must not alter this guest-visible hit test.
23109        let classic = findditem_results_for_theme(UiThemeId::ClassicSystem7);
23110        let themed = findditem_results_for_theme(UiThemeId::SystemlessDefault);
23111
23112        assert_eq!(
23113            classic,
23114            vec![
23115                (0, TEST_SP + 8),
23116                (1, TEST_SP + 8),
23117                (-1, TEST_SP + 8),
23118                (-1, TEST_SP + 8),
23119            ]
23120        );
23121        assert_eq!(
23122            themed, classic,
23123            "systemless-default must not change FindDItem hit-test semantics"
23124        );
23125    }
23126
23127    #[test]
23128    fn systemless_theme_does_not_change_getsetditem_useritem_records() {
23129        // MTE 1992 p. 6-121..6-123: GetDialogItem and SetDialogItem expose
23130        // item type, item handle/ProcPtr, and display rectangle. For
23131        // application-defined userItems, `item` is the app's draw ProcPtr;
23132        // theme chrome must not alter that record ABI or stack contract.
23133        let classic = getsetditem_useritem_record_results_for_theme(UiThemeId::ClassicSystem7);
23134        let themed = getsetditem_useritem_record_results_for_theme(UiThemeId::SystemlessDefault);
23135
23136        assert_eq!(classic.initial_get.item_type, 0);
23137        assert_eq!(classic.initial_get.item, 0x0012_3456);
23138        assert_eq!(classic.initial_get.rect, (12, 24, 36, 48));
23139        assert_eq!(classic.initial_get.stack_after, TEST_SP + 18);
23140        assert_eq!(classic.set_stack_after, TEST_SP + 16);
23141        assert_eq!(classic.stored_type, 0);
23142        assert_eq!(classic.stored_proc_ptr, 0x00AB_CDEF);
23143        assert_eq!(classic.stored_rect, (50, 60, 70, 80));
23144        assert_eq!(classic.post_set_get.item_type, 0);
23145        assert_eq!(classic.post_set_get.item, 0x00AB_CDEF);
23146        assert_eq!(classic.post_set_get.rect, (50, 60, 70, 80));
23147        assert_eq!(classic.post_set_get.stack_after, TEST_SP + 18);
23148        assert_eq!(
23149            themed, classic,
23150            "systemless-default must not change GetDItem/SetDItem userItem record semantics"
23151        );
23152    }
23153
23154    #[test]
23155    fn systemless_theme_does_not_change_getsetditem_text_control_records() {
23156        // MTE 1992 p. 6-121..6-123 and IM:I I-421: GetDialogItem and
23157        // SetDialogItem expose item type, item handle, and display rectangle.
23158        // Text items use handles consumed by Get/SetDialogItemText; control
23159        // items use ControlRecord handles. Theme chrome must not alter that
23160        // guest ABI, DITL storage, side-map relinking, or stack contract.
23161        let classic = getsetditem_text_control_record_results_for_theme(UiThemeId::ClassicSystem7);
23162        let themed =
23163            getsetditem_text_control_record_results_for_theme(UiThemeId::SystemlessDefault);
23164
23165        assert_eq!(classic.text_initial_get.item_type, 16);
23166        assert_ne!(classic.text_initial_get.item, 0);
23167        assert_eq!(classic.text_initial_get.rect, (10, 20, 24, 160));
23168        assert_eq!(classic.text_initial_get.stack_after, TEST_SP + 18);
23169        assert_eq!(classic.text_set_stack_after, TEST_SP + 16);
23170        assert!(!classic.text_old_handle_mapped_after);
23171        let mapped_dialog = classic.text_new_handle_map_value_after.unwrap().0;
23172        let text_new_handle = classic.text_post_set_get.item;
23173        assert_ne!(mapped_dialog, 0);
23174        assert_eq!(classic.text_new_handle_map_value_after.unwrap().1, 0);
23175        assert_ne!(text_new_handle, classic.text_initial_get.item);
23176        assert_eq!(classic.text_ditl_handle_after, text_new_handle);
23177        assert_eq!(classic.text_ditl_rect_after, (12, 22, 28, 168));
23178        assert_eq!(classic.text_ditl_type_after, 16);
23179        assert_eq!(classic.text_stored_type_after, 16);
23180        assert_eq!(classic.text_stored_rect_after, (12, 22, 28, 168));
23181        assert_eq!(classic.text_cached_text_after, "Updated");
23182        assert_eq!(classic.text_handle_bytes_after, b"Updated".to_vec());
23183        assert_eq!(classic.text_post_set_get.item_type, 16);
23184        assert_eq!(classic.text_post_set_get.item, text_new_handle);
23185        assert_eq!(classic.text_post_set_get.rect, (12, 22, 28, 168));
23186        assert_eq!(classic.text_post_set_get.stack_after, TEST_SP + 18);
23187
23188        assert_eq!(classic.control_initial_get.item_type, 4);
23189        assert_ne!(classic.control_initial_get.item, 0);
23190        assert_eq!(classic.control_initial_get.rect, (30, 40, 50, 140));
23191        assert_eq!(classic.control_initial_get.stack_after, TEST_SP + 18);
23192        assert_eq!(classic.control_set_stack_after, TEST_SP + 16);
23193        assert!(!classic.control_old_handle_mapped_after);
23194        let control_new_handle = classic.control_post_set_get.item;
23195        assert_eq!(
23196            classic.control_new_handle_map_value_after.unwrap().0,
23197            mapped_dialog
23198        );
23199        assert_eq!(classic.control_new_handle_map_value_after.unwrap().1, 2);
23200        assert_ne!(control_new_handle, classic.control_initial_get.item);
23201        assert_eq!(classic.control_ditl_handle_after, control_new_handle);
23202        assert_eq!(classic.control_ditl_rect_after, (42, 52, 62, 172));
23203        assert_eq!(classic.control_ditl_type_after, 5);
23204        assert_eq!(classic.control_stored_type_after, 5);
23205        assert_eq!(classic.control_stored_rect_after, (42, 52, 62, 172));
23206        assert_eq!(classic.control_record_rect_after, (42, 52, 62, 172));
23207        assert_eq!(classic.control_value_after, Some(2));
23208        assert_eq!(classic.control_post_set_get.item_type, 5);
23209        assert_eq!(classic.control_post_set_get.item, control_new_handle);
23210        assert_eq!(classic.control_post_set_get.rect, (42, 52, 62, 172));
23211        assert_eq!(classic.control_post_set_get.stack_after, TEST_SP + 18);
23212        assert_eq!(
23213            themed, classic,
23214            "systemless-default must not change GetDItem/SetDItem text/control record semantics"
23215        );
23216    }
23217
23218    #[test]
23219    fn systemless_theme_does_not_change_hide_show_ditem_visibility_records() {
23220        // MTE 1992 p. 6-123..6-124: HideDialogItem moves an item's display
23221        // rectangle offscreen by offsetting left/right, while ShowDialogItem
23222        // restores the visible rectangle and queues redraw. Themes must not
23223        // alter that item/control record state, hit testing, or stack ABI.
23224        let classic = hide_show_ditem_visibility_results_for_theme(UiThemeId::ClassicSystem7);
23225        let themed = hide_show_ditem_visibility_results_for_theme(UiThemeId::SystemlessDefault);
23226
23227        assert_eq!(classic.initial_get.item_type, 4);
23228        assert_eq!(classic.initial_get.rect, (10, 20, 30, 120));
23229        assert_eq!(classic.initial_get.stack_after, TEST_SP + 18);
23230        assert_eq!(classic.hide_stack_after, TEST_SP + 6);
23231        assert_eq!(classic.hidden_item_rect, (10, 16404, 30, 16504));
23232        assert_eq!(classic.hidden_saved_rect, Some((10, 20, 30, 120)));
23233        assert_eq!(classic.hidden_control_rect, (10, 16404, 30, 16504));
23234        assert_eq!(classic.hidden_get.item_type, 4);
23235        assert_eq!(classic.hidden_get.item, classic.initial_get.item);
23236        assert_eq!(classic.hidden_get.rect, (10, 16404, 30, 16504));
23237        assert_eq!(classic.hidden_get.stack_after, TEST_SP + 18);
23238        assert_eq!(classic.hidden_find, (-1, TEST_SP + 8));
23239        assert_eq!(classic.show_stack_after, TEST_SP + 6);
23240        assert_eq!(classic.restored_item_rect, (10, 20, 30, 120));
23241        assert_eq!(classic.restored_saved_rect, None);
23242        assert_eq!(classic.restored_control_rect, (10, 20, 30, 120));
23243        assert_eq!(classic.restored_get.item_type, 4);
23244        assert_eq!(classic.restored_get.item, classic.initial_get.item);
23245        assert_eq!(classic.restored_get.rect, (10, 20, 30, 120));
23246        assert_eq!(classic.restored_get.stack_after, TEST_SP + 18);
23247        assert_eq!(classic.restored_find, (0, TEST_SP + 8));
23248        assert_eq!(
23249            themed, classic,
23250            "systemless-default must not change HideDItem/ShowDItem visibility semantics"
23251        );
23252    }
23253
23254    #[test]
23255    fn systemless_theme_does_not_change_paramtext_storage_and_substitution() {
23256        // MTE 1992 p. 6-129..6-130: ParamText stores up to four strings in
23257        // DAStrings and substitutes ^0..^3 in subsequently created dialog or
23258        // alert text. This low-memory/global text contract is app-visible and
23259        // must not depend on the selected rendering provider.
23260        let classic = paramtext_results_for_theme(UiThemeId::ClassicSystem7);
23261        let themed = paramtext_results_for_theme(UiThemeId::SystemlessDefault);
23262
23263        assert_eq!(
23264            classic.initial_slots,
23265            [
23266                b"Doc".to_vec(),
23267                b"42".to_vec(),
23268                Vec::<u8>::new(),
23269                b"Tail".to_vec(),
23270            ]
23271        );
23272        assert!(classic.initial_da_handles.iter().all(|handle| *handle != 0));
23273        assert_eq!(classic.initial_da_strings, classic.initial_slots);
23274        assert_eq!(classic.initial_stack_after, TEST_SP + 16);
23275        assert_eq!(classic.initial_substitution, "Cannot open Doc (42)Tail ^9");
23276        assert_eq!(
23277            classic.nil_slots,
23278            [
23279                b"NewDoc".to_vec(),
23280                b"42".to_vec(),
23281                Vec::<u8>::new(),
23282                b"Tail".to_vec(),
23283            ]
23284        );
23285        assert!(classic.nil_da_handles.iter().all(|handle| *handle != 0));
23286        assert_eq!(classic.nil_da_strings, classic.nil_slots);
23287        assert_eq!(classic.nil_da_handles[1], classic.initial_da_handles[1]);
23288        assert_eq!(classic.nil_da_handles[2], classic.initial_da_handles[2]);
23289        assert_eq!(classic.nil_da_handles[3], classic.initial_da_handles[3]);
23290        assert_eq!(classic.nil_stack_after, TEST_SP + 16);
23291        assert_eq!(classic.nil_substitution, "Cannot open NewDoc (42)Tail ^9");
23292        assert_eq!(
23293            themed, classic,
23294            "systemless-default must not change ParamText storage or substitution semantics"
23295        );
23296    }
23297
23298    #[test]
23299    fn systemless_theme_does_not_change_dialog_item_text_storage() {
23300        // MTE 1992 p. 6-130..6-131: GetDialogItemText/GetIText returns the
23301        // text in a statText/editText item, and SetDialogItemText/SetIText
23302        // replaces that text. Theme rendering must not alter the text handle,
23303        // cached item text, Pascal-string output, or stack ABI.
23304        let classic = dialog_item_text_results_for_theme(UiThemeId::ClassicSystem7);
23305        let themed = dialog_item_text_results_for_theme(UiThemeId::SystemlessDefault);
23306
23307        assert_eq!(classic.initial_get_text, b"Old".to_vec());
23308        assert_eq!(classic.initial_get_stack_after, TEST_SP + 8);
23309        assert_eq!(classic.set_stack_after, TEST_SP + 8);
23310        assert_eq!(classic.handle_size_after_set, Some(11));
23311        assert_eq!(classic.handle_bytes_after_set, b"Edited Text".to_vec());
23312        assert_eq!(classic.cached_text_after_set, "Edited Text");
23313        assert_eq!(classic.post_set_get_text, b"Edited Text".to_vec());
23314        assert_eq!(classic.post_set_get_stack_after, TEST_SP + 8);
23315        assert_eq!(
23316            themed, classic,
23317            "systemless-default must not change Get/SetDialogItemText storage semantics"
23318        );
23319    }
23320
23321    #[test]
23322    fn systemless_theme_does_not_change_select_dialog_item_text_selection() {
23323        // MTE 1992 p. 6-131..6-132: SelectDialogItemText/SelIText sets the
23324        // selection range in an editable text item and supports the 0..32767
23325        // whole-text selection convention. The active editField, TERecord
23326        // mirrored selection, non-edit no-op, and stack ABI are guest-visible
23327        // Dialog Manager behavior, not theme rendering choices.
23328        let classic = select_dialog_item_text_results_for_theme(UiThemeId::ClassicSystem7);
23329        let themed = select_dialog_item_text_results_for_theme(UiThemeId::SystemlessDefault);
23330
23331        assert_eq!(classic.whole_selection, (0, 5));
23332        assert_eq!(classic.whole_edit_field, 0);
23333        assert_eq!(classic.whole_te_selection, (0, 5));
23334        assert_eq!(classic.whole_stack_after, TEST_SP + 10);
23335        assert_eq!(classic.normalized_selection, (2, 5));
23336        assert_eq!(classic.normalized_edit_field, 0);
23337        assert_eq!(classic.normalized_te_selection, (2, 5));
23338        assert_eq!(classic.normalized_stack_after, TEST_SP + 10);
23339        assert_eq!(classic.non_edit_selection, (1, 3));
23340        assert_eq!(classic.non_edit_edit_field, 0);
23341        assert_eq!(classic.non_edit_te_selection, (2, 5));
23342        assert_eq!(classic.non_edit_stack_after, TEST_SP + 10);
23343        assert_eq!(
23344            themed, classic,
23345            "systemless-default must not change SelectDialogItemText selection semantics"
23346        );
23347    }
23348
23349    #[test]
23350    fn systemless_theme_does_not_change_drawdialog_useritem_pixels() {
23351        // MTE 1992 p. 6-142: DrawDialog calls application-defined item
23352        // draw procedures when their rectangles are in the update region.
23353        // MTE 1992 p. 6-155 defines application-defined items as userItem
23354        // records; theme chrome must not erase the app-owned rectangle.
23355        let classic = drawdialog_useritem_pixel_results_for_theme(UiThemeId::ClassicSystem7);
23356        let themed = drawdialog_useritem_pixel_results_for_theme(UiThemeId::SystemlessDefault);
23357
23358        assert_eq!(classic.0, 0xA5);
23359        assert_eq!(classic.1, TEST_SP + 4);
23360        assert_eq!(classic.2, 0xCAFE_BABE);
23361        assert_eq!(
23362            themed, classic,
23363            "systemless-default must not change DrawDialog userItem pixel preservation"
23364        );
23365    }
23366
23367    #[test]
23368    fn systemless_theme_does_not_change_drawdialog_icon_item_pixels() {
23369        // MTE 1992 p. 6-155: an icon item's item-list record names an
23370        // `ICON` resource. MTE 1992 p. 7-63 defines `ICON` as application
23371        // resource content used in dialog boxes; theme chrome must not alter
23372        // those pixels inside the icon item rectangle.
23373        let classic = drawdialog_icon_item_pixel_results_for_theme(UiThemeId::ClassicSystem7);
23374        let themed = drawdialog_icon_item_pixel_results_for_theme(UiThemeId::SystemlessDefault);
23375
23376        assert_eq!(classic.0, [0xFF, 0xFF, 0xFF]);
23377        assert_eq!(classic.1, TEST_SP + 4);
23378        assert_eq!(classic.2, 0xCAFE_BABE);
23379        assert_eq!(
23380            themed, classic,
23381            "systemless-default must not change DrawDialog ICON item pixels"
23382        );
23383    }
23384
23385    #[test]
23386    fn systemless_theme_does_not_change_drawdialog_cicn_item_pixels() {
23387        // MTE 1992 p. 6-155: an icon item's item-list record names an
23388        // `ICON` resource and optionally a same-ID `cicn` resource. MTE 1992
23389        // p. 7-64 specifies that the Dialog Manager displays that color icon
23390        // instead of the black-and-white icon on color displays.
23391        let classic = drawdialog_cicn_item_pixel_results_for_theme(UiThemeId::ClassicSystem7);
23392        let themed = drawdialog_cicn_item_pixel_results_for_theme(UiThemeId::SystemlessDefault);
23393
23394        assert_eq!(classic.0, [0x33, 0x55, 0x77]);
23395        assert_eq!(classic.1, TEST_SP + 4);
23396        assert_eq!(classic.2, 0xCAFE_BABE);
23397        assert_eq!(
23398            themed, classic,
23399            "systemless-default must not change DrawDialog cicn item pixels"
23400        );
23401    }
23402
23403    #[test]
23404    fn systemless_theme_does_not_change_drawdialog_picture_item_pixels() {
23405        // MTE 1992 p. 6-155: a picture item's item-list record names a
23406        // `PICT` resource. That resource is application-supplied picture
23407        // content, so theme chrome must not alter the pixels DrawDialog
23408        // renders inside the picture item rectangle.
23409        let classic = drawdialog_picture_item_pixel_results_for_theme(UiThemeId::ClassicSystem7);
23410        let themed = drawdialog_picture_item_pixel_results_for_theme(UiThemeId::SystemlessDefault);
23411
23412        assert_eq!(classic.0, [0xFF, 0xFF, 0xFF]);
23413        assert_eq!(classic.1, TEST_SP + 4);
23414        assert_eq!(classic.2, 0xCAFE_BABE);
23415        assert_eq!(
23416            themed, classic,
23417            "systemless-default must not change DrawDialog PICT item pixels"
23418        );
23419    }
23420
23421    #[test]
23422    fn dialog_select_disabled_item_hit_returns_false_and_leaves_outputs_unchanged() {
23423        // Inside Macintosh Volume I, I-417: disabled items do nothing and
23424        // DialogSelect returns FALSE.
23425        let (mut disp, mut cpu, mut bus) = setup();
23426        let dialog_ptr = bus.alloc(170);
23427        let event_ptr = 0x300000u32;
23428        let dialog_out_ptr = 0x300100u32;
23429        let item_hit_ptr = 0x300104u32;
23430
23431        disp.front_window = dialog_ptr;
23432        bus.write_word(dialog_ptr + 8, 0);
23433        bus.write_word(dialog_ptr + 10, 0);
23434        bus.write_word(dialog_ptr + 16, 0);
23435        bus.write_word(dialog_ptr + 18, 0);
23436        bus.write_word(dialog_ptr + 20, 100);
23437        bus.write_word(dialog_ptr + 22, 160);
23438        disp.dialog_items.insert(
23439            dialog_ptr,
23440            vec![DialogItem {
23441                item_type: 0x80, // disabled userItem
23442                rect: (20, 30, 60, 110),
23443                text: String::new(),
23444                resource_id: 0,
23445                proc_ptr: 0,
23446                sel_start: 0,
23447                sel_end: 0,
23448            }],
23449        );
23450
23451        bus.write_word(event_ptr, 1); // mouseDown
23452        bus.write_long(event_ptr + 2, 0);
23453        bus.write_long(event_ptr + 6, 0);
23454        bus.write_word(event_ptr + 10, 130);
23455        bus.write_word(event_ptr + 12, 240);
23456        bus.write_word(event_ptr + 14, 0x0080);
23457
23458        bus.write_long(dialog_out_ptr, 0x12345678);
23459        bus.write_word(item_hit_ptr, 0x7F7F);
23460
23461        bus.write_long(TEST_SP, item_hit_ptr);
23462        bus.write_long(TEST_SP + 4, dialog_out_ptr);
23463        bus.write_long(TEST_SP + 8, event_ptr);
23464
23465        let result = disp.dispatch_dialog(true, 0x180, &mut cpu, &mut bus);
23466        assert!(result.unwrap().is_ok());
23467        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 12);
23468        assert_eq!(bus.read_word(TEST_SP + 12), 0);
23469        assert_eq!(bus.read_long(dialog_out_ptr), 0x12345678);
23470        assert_eq!(bus.read_word(item_hit_ptr), 0x7F7F);
23471    }
23472
23473    #[test]
23474    fn dialog_select_keydown_without_edit_text_item_returns_false() {
23475        // Inside Macintosh Volume I, I-417: keyDown handling returns TRUE only
23476        // when an enabled editText item is active.
23477        let (mut disp, mut cpu, mut bus) = setup();
23478        let dialog_ptr = bus.alloc(170);
23479        let event_ptr = 0x300000u32;
23480        let dialog_out_ptr = 0x300100u32;
23481        let item_hit_ptr = 0x300104u32;
23482
23483        disp.front_window = dialog_ptr;
23484        bus.write_word(dialog_ptr + 8, 0);
23485        bus.write_word(dialog_ptr + 10, 0);
23486        bus.write_word(dialog_ptr + 16, 0);
23487        bus.write_word(dialog_ptr + 18, 0);
23488        bus.write_word(dialog_ptr + 20, 100);
23489        bus.write_word(dialog_ptr + 22, 160);
23490        bus.write_word(dialog_ptr + 164, 0); // editField points at item 1
23491        disp.dialog_items.insert(
23492            dialog_ptr,
23493            vec![DialogItem {
23494                item_type: 0, // userItem, not editText
23495                rect: (20, 30, 60, 110),
23496                text: String::new(),
23497                resource_id: 0,
23498                proc_ptr: 0,
23499                sel_start: 0,
23500                sel_end: 0,
23501            }],
23502        );
23503
23504        bus.write_word(event_ptr, 3); // keyDown
23505        bus.write_long(event_ptr + 2, 0x00000041); // 'A'
23506        bus.write_long(event_ptr + 6, 0);
23507        bus.write_word(event_ptr + 10, 0);
23508        bus.write_word(event_ptr + 12, 0);
23509        bus.write_word(event_ptr + 14, 0);
23510
23511        bus.write_long(TEST_SP, item_hit_ptr);
23512        bus.write_long(TEST_SP + 4, dialog_out_ptr);
23513        bus.write_long(TEST_SP + 8, event_ptr);
23514
23515        let result = disp.dispatch_dialog(true, 0x180, &mut cpu, &mut bus);
23516        assert!(result.unwrap().is_ok());
23517        assert_eq!(bus.read_word(TEST_SP + 12), 0);
23518    }
23519
23520    #[test]
23521    fn dialog_select_keydown_with_enabled_edit_text_returns_hit() {
23522        // Inside Macintosh Volume I, I-417: for keyDown/autoKey with an
23523        // enabled editText item, DialogSelect returns TRUE and item number.
23524        let (mut disp, mut cpu, mut bus) = setup();
23525        let dialog_ptr = bus.alloc(170);
23526        let event_ptr = 0x300000u32;
23527        let dialog_out_ptr = 0x300100u32;
23528        let item_hit_ptr = 0x300104u32;
23529
23530        disp.front_window = dialog_ptr;
23531        bus.write_word(dialog_ptr + 8, 0);
23532        bus.write_word(dialog_ptr + 10, 0);
23533        bus.write_word(dialog_ptr + 16, 0);
23534        bus.write_word(dialog_ptr + 18, 0);
23535        bus.write_word(dialog_ptr + 20, 100);
23536        bus.write_word(dialog_ptr + 22, 160);
23537        bus.write_word(dialog_ptr + 164, 0); // editField = first item
23538        disp.dialog_items.insert(
23539            dialog_ptr,
23540            vec![DialogItem {
23541                item_type: 16, // editText
23542                rect: (20, 30, 60, 110),
23543                text: "Hello".to_string(),
23544                resource_id: 0,
23545                proc_ptr: 0,
23546                sel_start: 0,
23547                sel_end: 0,
23548            }],
23549        );
23550
23551        bus.write_word(event_ptr, 3); // keyDown
23552        bus.write_long(event_ptr + 2, 0x00000041); // 'A'
23553        bus.write_long(event_ptr + 6, 0);
23554        bus.write_word(event_ptr + 10, 0);
23555        bus.write_word(event_ptr + 12, 0);
23556        bus.write_word(event_ptr + 14, 0);
23557
23558        bus.write_long(TEST_SP, item_hit_ptr);
23559        bus.write_long(TEST_SP + 4, dialog_out_ptr);
23560        bus.write_long(TEST_SP + 8, event_ptr);
23561
23562        let result = disp.dispatch_dialog(true, 0x180, &mut cpu, &mut bus);
23563        assert!(result.unwrap().is_ok());
23564        assert_eq!(bus.read_word(TEST_SP + 12), 0xFFFF);
23565        assert_eq!(bus.read_long(dialog_out_ptr), dialog_ptr);
23566        assert_eq!(bus.read_word(item_hit_ptr), 1);
23567    }
23568
23569    #[test]
23570    fn dialog_select_null_event_blinks_active_edit_text_insertion_caret() {
23571        // MTE 1992 p. 6-139 and IM:I I-417: DialogSelect mouse-down in an
23572        // enabled editText item displays the insertion point, and later null
23573        // events call TEIdle so the insertion point blinks.
23574        let (mut disp, mut cpu, mut bus) = setup_with_port();
23575        let dialog_ptr = bus.alloc(170);
23576        let event_ptr = bus.alloc(16);
23577        let dialog_out_ptr = bus.alloc(4);
23578        let item_hit_ptr = bus.alloc(2);
23579        let items_handle = bus.alloc(4);
23580        let ditl_ptr = bus.alloc(16);
23581        let text_item_handle = TrapDispatcher::allocate_handle_with_data(&mut bus, 0);
23582        let text_h = make_te_with_text(&mut disp, &mut bus, b"");
23583        let te_ptr = bus.read_long(text_h);
23584        let (screen_base, row_bytes, _screen_w, _screen_h, _pixel_size) = disp.screen_mode;
23585
23586        for i in 0..(row_bytes * 80) {
23587            bus.write_byte(screen_base + i, 0);
23588        }
23589
23590        disp.front_window = dialog_ptr;
23591        bus.write_word(dialog_ptr + 8, 0);
23592        bus.write_word(dialog_ptr + 10, 0);
23593        bus.write_word(dialog_ptr + 16, 0);
23594        bus.write_word(dialog_ptr + 18, 0);
23595        bus.write_word(dialog_ptr + 20, 100);
23596        bus.write_word(dialog_ptr + 22, 160);
23597        bus.write_long(dialog_ptr + 156, items_handle);
23598        bus.write_long(dialog_ptr + 160, text_h);
23599        bus.write_word(dialog_ptr + 164, 0);
23600
23601        bus.write_long(items_handle, ditl_ptr);
23602        bus.write_word(ditl_ptr, 0);
23603        bus.write_long(ditl_ptr + 2, text_item_handle);
23604        bus.write_word(ditl_ptr + 6, 20);
23605        bus.write_word(ditl_ptr + 8, 20);
23606        bus.write_word(ditl_ptr + 10, 38);
23607        bus.write_word(ditl_ptr + 12, 120);
23608        bus.write_byte(ditl_ptr + 14, 16);
23609        bus.write_byte(ditl_ptr + 15, 0);
23610
23611        disp.dialog_items.insert(
23612            dialog_ptr,
23613            vec![DialogItem {
23614                item_type: 16,
23615                rect: (20, 20, 38, 120),
23616                text: String::new(),
23617                resource_id: 0,
23618                proc_ptr: 0,
23619                sel_start: 0,
23620                sel_end: 0,
23621            }],
23622        );
23623
23624        let dispatch_dialog_select_event = |disp: &mut TrapDispatcher,
23625                                            cpu: &mut MockCpu,
23626                                            bus: &mut MacMemoryBus,
23627                                            what: u16,
23628                                            tick: u32| {
23629            disp.tick_count = tick;
23630            bus.write_long(crate::memory::globals::addr::TICKS, tick);
23631            cpu.write_reg(Register::A7, TEST_SP);
23632            bus.write_word(event_ptr, what);
23633            bus.write_long(event_ptr + 2, 0);
23634            bus.write_long(event_ptr + 6, 0);
23635            bus.write_word(event_ptr + 10, 25);
23636            bus.write_word(event_ptr + 12, 25);
23637            bus.write_word(event_ptr + 14, 0);
23638            bus.write_long(dialog_out_ptr, 0xDEAD_BEEF);
23639            bus.write_word(item_hit_ptr, 0xCAFE);
23640            bus.write_long(TEST_SP, item_hit_ptr);
23641            bus.write_long(TEST_SP + 4, dialog_out_ptr);
23642            bus.write_long(TEST_SP + 8, event_ptr);
23643            disp.dispatch_dialog(true, 0x180, cpu, bus)
23644                .unwrap()
23645                .unwrap();
23646        };
23647
23648        dispatch_dialog_select_event(&mut disp, &mut cpu, &mut bus, 1, 100);
23649        assert_eq!(bus.read_word(TEST_SP + 12), 0xFFFF);
23650        assert_eq!(bus.read_long(dialog_out_ptr), dialog_ptr);
23651        assert_eq!(bus.read_word(item_hit_ptr), 1);
23652        assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_ACTIVE_OFFSET), 1);
23653        assert_eq!(
23654            bus.read_long(te_ptr + TrapDispatcher::TE_CARET_TIME_OFFSET),
23655            100
23656        );
23657        assert_eq!(
23658            bus.read_word(te_ptr + TrapDispatcher::TE_CARET_STATE_OFFSET),
23659            0
23660        );
23661        assert!(
23662            screen_pixel_is_set(&bus, screen_base, row_bytes, 1, 0),
23663            "DialogSelect editText mouse-down should display the insertion caret"
23664        );
23665
23666        dispatch_dialog_select_event(&mut disp, &mut cpu, &mut bus, 0, 131);
23667        assert_eq!(bus.read_word(TEST_SP + 12), 0);
23668        assert_eq!(bus.read_long(dialog_out_ptr), 0xDEAD_BEEF);
23669        assert_eq!(bus.read_word(item_hit_ptr), 0xCAFE);
23670        assert_eq!(
23671            bus.read_word(te_ptr + TrapDispatcher::TE_CARET_STATE_OFFSET),
23672            0
23673        );
23674        assert!(
23675            screen_pixel_is_set(&bus, screen_base, row_bytes, 1, 0),
23676            "DialogSelect null event before 32 ticks should keep the caret visible"
23677        );
23678
23679        dispatch_dialog_select_event(&mut disp, &mut cpu, &mut bus, 0, 132);
23680        assert_eq!(
23681            bus.read_long(te_ptr + TrapDispatcher::TE_CARET_TIME_OFFSET),
23682            132
23683        );
23684        assert_eq!(
23685            bus.read_word(te_ptr + TrapDispatcher::TE_CARET_STATE_OFFSET),
23686            1
23687        );
23688        assert!(
23689            !screen_pixel_is_set(&bus, screen_base, row_bytes, 1, 0),
23690            "DialogSelect null event at the 32-tick boundary should hide the caret"
23691        );
23692
23693        dispatch_dialog_select_event(&mut disp, &mut cpu, &mut bus, 0, 164);
23694        assert_eq!(
23695            bus.read_long(te_ptr + TrapDispatcher::TE_CARET_TIME_OFFSET),
23696            164
23697        );
23698        assert_eq!(
23699            bus.read_word(te_ptr + TrapDispatcher::TE_CARET_STATE_OFFSET),
23700            0
23701        );
23702        assert!(
23703            screen_pixel_is_set(&bus, screen_base, row_bytes, 1, 0),
23704            "the next DialogSelect null-event blink interval should show the caret again"
23705        );
23706    }
23707
23708    // ---- DrawDialog ($A981) ----
23709
23710    #[test]
23711    fn draw_dialog_pops_4_bytes() {
23712        let (mut disp, mut cpu, mut bus) = setup();
23713        // SP+0: dialog ptr (4 bytes)
23714        bus.write_long(TEST_SP, 0x200000);
23715
23716        let result = disp.dispatch_dialog(true, 0x181, &mut cpu, &mut bus);
23717        assert!(result.unwrap().is_ok());
23718        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
23719    }
23720
23721    #[test]
23722    fn draw_dialog_repaints_dialog_background_for_known_dialog() {
23723        // Inside Macintosh Volume I, I-417: DrawDialog draws the contents of
23724        // the given dialog box.
23725        let (mut disp, mut cpu, mut bus) = setup();
23726        let dialog_ptr = bus.alloc(170);
23727        let (screen_base, row_bytes, _w, _h, pixel_size) = disp.screen_mode;
23728
23729        bus.write_word(dialog_ptr + 8, 0);
23730        bus.write_word(dialog_ptr + 10, 0);
23731        bus.write_word(dialog_ptr + 16, 0);
23732        bus.write_word(dialog_ptr + 18, 0);
23733        bus.write_word(dialog_ptr + 20, 30);
23734        bus.write_word(dialog_ptr + 22, 40);
23735        disp.dialog_items.insert(
23736            dialog_ptr,
23737            vec![DialogItem {
23738                item_type: 4, // btnCtrl draws a deterministic border and label
23739                rect: (8, 8, 20, 32),
23740                text: "OK".to_string(),
23741                resource_id: 0,
23742                proc_ptr: 0,
23743                sel_start: 0,
23744                sel_end: 0,
23745            }],
23746        );
23747
23748        let probe_x = 4u32;
23749        let probe_y = 4u32;
23750        let probe_addr = if pixel_size == 8 {
23751            screen_base + probe_y * row_bytes + probe_x
23752        } else {
23753            screen_base + probe_y * row_bytes + (probe_x / 8)
23754        };
23755        let probe_bit = 1 << (7 - (probe_x % 8));
23756        if pixel_size == 8 {
23757            bus.write_byte(probe_addr, 0xFF); // black in 8bpp CLUT
23758        } else {
23759            bus.write_byte(probe_addr, bus.read_byte(probe_addr) | probe_bit);
23760        }
23761
23762        bus.write_long(TEST_SP, dialog_ptr);
23763        let result = disp.dispatch_dialog(true, 0x181, &mut cpu, &mut bus);
23764        assert!(result.unwrap().is_ok());
23765        if pixel_size == 8 {
23766            assert_eq!(bus.read_byte(probe_addr), 0); // white in 8bpp CLUT
23767        } else {
23768            assert_eq!(bus.read_byte(probe_addr) & probe_bit, 0);
23769        }
23770    }
23771
23772    #[test]
23773    fn draw_dialog_repaints_document_proc_title_chrome() {
23774        // DrawDialog is also valid for modeless documentProc dialogs. It must
23775        // redraw the WDEF title bar from the WindowRecord title instead of
23776        // falling back to modal-box border chrome (IM:I I-299/I-417).
23777        let screen_base = 0x300000u32;
23778        let row_bytes = 64u32;
23779        let (mut disp, mut cpu, mut bus) = setup();
23780        disp.set_screen_mode_for_test(screen_base, row_bytes, 512, 342, 1);
23781
23782        let dialog_ptr = bus.alloc(170);
23783        bus.write_word(dialog_ptr + 6, 0);
23784        bus.write_word(dialog_ptr + 8, (-40i16) as u16);
23785        bus.write_word(dialog_ptr + 10, (-80i16) as u16);
23786        bus.write_word(dialog_ptr + 16, 0);
23787        bus.write_word(dialog_ptr + 18, 0);
23788        bus.write_word(dialog_ptr + 20, 80);
23789        bus.write_word(dialog_ptr + 22, 160);
23790        bus.write_byte(dialog_ptr + 112, 0);
23791        let title_ptr = bus.alloc(16);
23792        bus.write_pstring(title_ptr, b"Modeless");
23793        let title_handle = bus.alloc(4);
23794        bus.write_long(title_handle, title_ptr);
23795        bus.write_long(dialog_ptr + 134, title_handle);
23796        disp.window_proc_ids.insert(dialog_ptr, 0);
23797        disp.dialog_items.insert(dialog_ptr, Vec::new());
23798
23799        bus.write_long(TEST_SP, dialog_ptr);
23800        let result = disp.dispatch_dialog(true, 0x181, &mut cpu, &mut bus);
23801        assert!(result.unwrap().is_ok());
23802
23803        assert!(
23804            screen_pixel_is_set(&bus, screen_base, row_bytes, 85, 24),
23805            "documentProc DrawDialog should redraw active title-bar stripes"
23806        );
23807        assert!(
23808            screen_pixel_is_set(&bus, screen_base, row_bytes, 79, 21),
23809            "documentProc DrawDialog should redraw the title-bar border"
23810        );
23811    }
23812
23813    #[test]
23814    fn draw_dialog_radio_items_use_control_value_state() {
23815        // SetCtlValue stores value 1 for a selected radio control (IM:I
23816        // I-317/I-328). DrawDialog must honor the dialog control value table
23817        // for radio items, just as it does for checkboxes.
23818        let screen_base = 0x300000u32;
23819        let row_bytes = 64u32;
23820        let bounds = (40, 40, 120, 220);
23821        let dialog_ptr = 0x2000;
23822        let items = vec![DialogItem {
23823            item_type: 6,
23824            rect: (20, 20, 40, 160),
23825            text: "Selected".to_string(),
23826            resource_id: 0,
23827            proc_ptr: 0,
23828            sel_start: 0,
23829            sel_end: 0,
23830        }];
23831        let (mut disp, _cpu, mut bus) = setup();
23832        disp.set_screen_mode_for_test(screen_base, row_bytes, 512, 342, 1);
23833        disp.dialog_control_values.insert((dialog_ptr, 1), 1);
23834
23835        disp.draw_dialog(&mut bus, bounds, 1, "", &items, 0, "", 0, false, dialog_ptr);
23836
23837        assert!(
23838            screen_pixel_is_set(&bus, screen_base, row_bytes, 67, 69),
23839            "selected radio item should draw the central dot"
23840        );
23841    }
23842
23843    #[test]
23844    fn draw_dialog_queues_user_item_draw_proc_for_known_dialog() {
23845        // MTE 1992 p. 6-142: DrawDialog calls application-defined item
23846        // draw procs whose userItem display rectangles are in the dialog.
23847        let (mut disp, mut cpu, mut bus) = setup();
23848        let dialog_ptr = bus.alloc(170);
23849
23850        bus.write_word(dialog_ptr + 16, 0);
23851        bus.write_word(dialog_ptr + 18, 0);
23852        bus.write_word(dialog_ptr + 20, 80);
23853        bus.write_word(dialog_ptr + 22, 120);
23854        disp.dialog_items.insert(
23855            dialog_ptr,
23856            vec![
23857                DialogItem {
23858                    item_type: 0,
23859                    rect: (8, 8, 24, 48),
23860                    proc_ptr: 0x500000,
23861                    ..Default::default()
23862                },
23863                DialogItem {
23864                    item_type: 0,
23865                    rect: (100, 8, 120, 48),
23866                    proc_ptr: 0x600000,
23867                    ..Default::default()
23868                },
23869                DialogItem {
23870                    item_type: 4,
23871                    rect: (40, 8, 60, 48),
23872                    text: "OK".to_string(),
23873                    ..Default::default()
23874                },
23875            ],
23876        );
23877
23878        bus.write_long(TEST_SP, dialog_ptr);
23879        let result = disp.dispatch_dialog(true, 0x181, &mut cpu, &mut bus);
23880        assert!(result.unwrap().is_ok());
23881
23882        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
23883        assert_eq!(
23884            disp.modeless_dialog_draw_proc_queue
23885                .iter()
23886                .copied()
23887                .collect::<Vec<_>>(),
23888            vec![(dialog_ptr, 0x500000, 1)]
23889        );
23890    }
23891
23892    #[test]
23893    fn draw_button_systemless_theme_routes_dialog_chrome_through_provider() {
23894        let screen_base = 0x300000u32;
23895        let row_bytes = 64u32;
23896
23897        let (mut classic, _classic_cpu, mut classic_bus) = setup();
23898        classic.set_screen_mode_for_test(screen_base, row_bytes, 512, 342, 1);
23899        classic.draw_button(&mut classic_bus, 20, 20, 40, 80, "", true);
23900
23901        let (mut themed, _themed_cpu, mut themed_bus) = setup();
23902        themed.set_ui_theme_id(UiThemeId::SystemlessDefault);
23903        themed.set_screen_mode_for_test(screen_base, row_bytes, 512, 342, 1);
23904        themed.draw_button(&mut themed_bus, 20, 20, 40, 80, "", true);
23905
23906        assert!(
23907            !screen_pixel_is_set(&classic_bus, screen_base, row_bytes, 16, 16),
23908            "classic default-button round rect should leave its outer corner as background"
23909        );
23910        assert!(
23911            screen_pixel_is_set(&themed_bus, screen_base, row_bytes, 16, 16),
23912            "systemless-default provider chrome should own the default-button outline corner"
23913        );
23914    }
23915
23916    #[test]
23917    fn draw_checkbox_and_radio_systemless_theme_route_dialog_chrome_through_provider() {
23918        let screen_base = 0x300000u32;
23919        let row_bytes = 64u32;
23920
23921        let (mut classic, _classic_cpu, mut classic_bus) = setup();
23922        classic.set_screen_mode_for_test(screen_base, row_bytes, 512, 342, 1);
23923        classic.draw_checkbox(&mut classic_bus, 20, 20, 40, 120, "", true);
23924        classic.draw_radio(&mut classic_bus, 50, 20, 70, 120, "", true);
23925
23926        let (mut themed, _themed_cpu, mut themed_bus) = setup();
23927        themed.set_ui_theme_id(UiThemeId::SystemlessDefault);
23928        themed.set_screen_mode_for_test(screen_base, row_bytes, 512, 342, 1);
23929        themed.draw_checkbox(&mut themed_bus, 20, 20, 40, 120, "", true);
23930        themed.draw_radio(&mut themed_bus, 50, 20, 70, 120, "", true);
23931
23932        let classic_checkbox_pixels =
23933            count_set_pixels(&classic_bus, screen_base, row_bytes, 21, 20, 33, 32);
23934        let themed_checkbox_pixels =
23935            count_set_pixels(&themed_bus, screen_base, row_bytes, 21, 20, 33, 32);
23936        assert!(
23937            themed_checkbox_pixels > classic_checkbox_pixels,
23938            "systemless-default dialog checkbox provider should draw a denser selected mark ({themed_checkbox_pixels} <= {classic_checkbox_pixels})"
23939        );
23940        let classic_radio_pixels =
23941            count_set_pixels(&classic_bus, screen_base, row_bytes, 51, 20, 63, 32);
23942        let themed_radio_pixels =
23943            count_set_pixels(&themed_bus, screen_base, row_bytes, 51, 20, 63, 32);
23944        assert!(
23945            themed_radio_pixels > classic_radio_pixels,
23946            "systemless-default dialog radio provider should draw a denser selected mark ({themed_radio_pixels} <= {classic_radio_pixels})"
23947        );
23948    }
23949
23950    #[test]
23951    fn draw_popup_control_systemless_theme_routes_dialog_chrome_through_provider() {
23952        let screen_base = 0x300000u32;
23953        let row_bytes = 64u32;
23954
23955        let (mut classic, _classic_cpu, mut classic_bus) = setup();
23956        classic.set_screen_mode_for_test(screen_base, row_bytes, 512, 342, 1);
23957        classic.draw_popup_control(&mut classic_bus, 20, 20, 40, 120, "");
23958
23959        let (mut themed, _themed_cpu, mut themed_bus) = setup();
23960        themed.set_ui_theme_id(UiThemeId::SystemlessDefault);
23961        themed.set_screen_mode_for_test(screen_base, row_bytes, 512, 342, 1);
23962        themed.draw_popup_control(&mut themed_bus, 20, 20, 40, 120, "");
23963
23964        assert!(
23965            screen_pixel_is_set(&classic_bus, screen_base, row_bytes, 120, 39),
23966            "classic popup CDEF should draw its one-pixel offset shadow"
23967        );
23968        assert!(
23969            !screen_pixel_is_set(&themed_bus, screen_base, row_bytes, 120, 39),
23970            "systemless-default popup provider should not draw the classic offset shadow"
23971        );
23972    }
23973
23974    #[test]
23975    fn draw_edit_text_systemless_theme_routes_caret_through_provider() {
23976        let screen_base = 0x300000u32;
23977        let row_bytes = 64u32;
23978
23979        let (mut classic, _classic_cpu, mut classic_bus) = setup();
23980        classic.set_screen_mode_for_test(screen_base, row_bytes, 512, 342, 1);
23981        classic.draw_edit_text(&mut classic_bus, 20, 20, 40, 100, "", false);
23982
23983        let (mut themed, _themed_cpu, mut themed_bus) = setup();
23984        themed.set_ui_theme_id(UiThemeId::SystemlessDefault);
23985        themed.set_screen_mode_for_test(screen_base, row_bytes, 512, 342, 1);
23986        themed.draw_edit_text(&mut themed_bus, 20, 20, 40, 100, "", false);
23987
23988        assert!(
23989            !screen_pixel_is_set(&classic_bus, screen_base, row_bytes, 22, 22),
23990            "classic editText caret should remain a single vertical bar"
23991        );
23992        assert!(
23993            screen_pixel_is_set(&themed_bus, screen_base, row_bytes, 22, 22),
23994            "systemless-default provider should draw the caret cap"
23995        );
23996    }
23997
23998    #[test]
23999    fn draw_edit_text_systemless_theme_routes_field_frame_through_provider() {
24000        let screen_base = 0x300000u32;
24001        let row_bytes = 64u32;
24002
24003        let (mut classic, _classic_cpu, mut classic_bus) = setup();
24004        classic.set_screen_mode_for_test(screen_base, row_bytes, 512, 342, 1);
24005        classic.draw_edit_text(&mut classic_bus, 20, 20, 40, 100, "", false);
24006
24007        let (mut themed, _themed_cpu, mut themed_bus) = setup();
24008        themed.set_ui_theme_id(UiThemeId::SystemlessDefault);
24009        themed.set_screen_mode_for_test(screen_base, row_bytes, 512, 342, 1);
24010        themed.draw_edit_text(&mut themed_bus, 20, 20, 40, 100, "", false);
24011
24012        assert!(
24013            !screen_pixel_is_set(&classic_bus, screen_base, row_bytes, 19, 22),
24014            "classic editText field should keep the upper interior clear"
24015        );
24016        assert!(
24017            screen_pixel_is_set(&themed_bus, screen_base, row_bytes, 19, 22),
24018            "systemless-default provider should draw focused editText field chrome"
24019        );
24020    }
24021
24022    #[test]
24023    fn draw_dialog_systemless_theme_routes_disabled_edit_text_state_through_provider() {
24024        let screen_base = 0x300000u32;
24025        let row_bytes = 64u32;
24026        let bounds = (40, 40, 110, 190);
24027        let items = vec![DialogItem {
24028            item_type: 0x80 | 16,
24029            rect: (20, 20, 44, 130),
24030            text: "Disabled".to_string(),
24031            resource_id: 0,
24032            proc_ptr: 0,
24033            sel_start: 0,
24034            sel_end: 0,
24035        }];
24036
24037        let (mut classic, _classic_cpu, mut classic_bus) = setup();
24038        classic.set_screen_mode_for_test(screen_base, row_bytes, 512, 342, 1);
24039        classic.draw_dialog(
24040            &mut classic_bus,
24041            bounds,
24042            1,
24043            "",
24044            &items,
24045            0,
24046            "",
24047            0,
24048            false,
24049            0x2000,
24050        );
24051
24052        let (mut themed, _themed_cpu, mut themed_bus) = setup();
24053        themed.set_ui_theme_id(UiThemeId::SystemlessDefault);
24054        themed.set_screen_mode_for_test(screen_base, row_bytes, 512, 342, 1);
24055        themed.draw_dialog(
24056            &mut themed_bus,
24057            bounds,
24058            1,
24059            "",
24060            &items,
24061            0,
24062            "",
24063            0,
24064            false,
24065            0x2000,
24066        );
24067
24068        let disabled_inner_x =
24069            bounds.1 + items[0].rect.1 - TrapDispatcher::EDIT_TEXT_FRAME_OUTSET + 2;
24070        let disabled_inner_y = bounds.0 + items[0].rect.0 + 2;
24071        assert!(
24072            !screen_pixel_is_set(
24073                &classic_bus,
24074                screen_base,
24075                row_bytes,
24076                disabled_inner_x,
24077                disabled_inner_y
24078            ),
24079            "classic editText field should keep the disabled-state inner frame pixel clear"
24080        );
24081        assert!(
24082            screen_pixel_is_set(
24083                &themed_bus,
24084                screen_base,
24085                row_bytes,
24086                disabled_inner_x,
24087                disabled_inner_y
24088            ),
24089            "systemless-default provider should receive disabled editText field state"
24090        );
24091    }
24092
24093    #[test]
24094    fn draw_dialog_systemless_theme_routes_disabled_standard_controls_through_provider() {
24095        let screen_base = 0x300000u32;
24096        let row_bytes = 64u32;
24097        let bounds = (40, 40, 140, 210);
24098        let items = vec![
24099            DialogItem {
24100                item_type: 0x80 | 4,
24101                rect: (20, 20, 40, 82),
24102                text: String::new(),
24103                resource_id: 0,
24104                proc_ptr: 0,
24105                sel_start: 0,
24106                sel_end: 0,
24107            },
24108            DialogItem {
24109                item_type: 0x80 | 5,
24110                rect: (52, 20, 72, 120),
24111                text: String::new(),
24112                resource_id: 0,
24113                proc_ptr: 0,
24114                sel_start: 0,
24115                sel_end: 0,
24116            },
24117            DialogItem {
24118                item_type: 0x80 | 6,
24119                rect: (84, 20, 104, 120),
24120                text: String::new(),
24121                resource_id: 0,
24122                proc_ptr: 0,
24123                sel_start: 0,
24124                sel_end: 0,
24125            },
24126        ];
24127
24128        let (mut classic, _classic_cpu, mut classic_bus) = setup();
24129        classic.set_screen_mode_for_test(screen_base, row_bytes, 512, 342, 1);
24130        classic.draw_dialog(
24131            &mut classic_bus,
24132            bounds,
24133            1,
24134            "",
24135            &items,
24136            0,
24137            "",
24138            0,
24139            false,
24140            0x2000,
24141        );
24142
24143        let (mut themed, _themed_cpu, mut themed_bus) = setup();
24144        themed.set_ui_theme_id(UiThemeId::SystemlessDefault);
24145        themed.set_screen_mode_for_test(screen_base, row_bytes, 512, 342, 1);
24146        themed.draw_dialog(
24147            &mut themed_bus,
24148            bounds,
24149            1,
24150            "",
24151            &items,
24152            0,
24153            "",
24154            0,
24155            false,
24156            0x2000,
24157        );
24158
24159        // The unselected disabled checkbox is intentionally pixel-equivalent
24160        // after classic CDEF mark alignment; selected checkbox routing is
24161        // covered by draw_checkbox_and_radio_systemless_theme_route_dialog_chrome_through_provider.
24162        let probes = [
24163            (
24164                bounds.1 + items[0].rect.1 + 2,
24165                bounds.0 + items[0].rect.0 + 2,
24166            ),
24167            (
24168                bounds.1 + items[2].rect.1 + 2,
24169                bounds.0
24170                    + items[2].rect.0
24171                    + (items[2].rect.2
24172                        - items[2].rect.0
24173                        - TrapDispatcher::STANDARD_CONTROL_MARK_SIZE)
24174                        / 2,
24175            ),
24176        ];
24177        for (x, y) in probes {
24178            assert!(
24179                !screen_pixel_is_set(&classic_bus, screen_base, row_bytes, x, y),
24180                "classic disabled DITL controls should keep standard appearance at ({x},{y})"
24181            );
24182            assert!(
24183                screen_pixel_is_set(&themed_bus, screen_base, row_bytes, x, y),
24184                "systemless-default provider should receive disabled DITL control state at ({x},{y})"
24185            );
24186        }
24187    }
24188
24189    #[test]
24190    fn draw_dialog_classic_keeps_disabled_checkbox_and_radio_titles_undimmed() {
24191        let screen_base = 0x300000u32;
24192        let row_bytes = 64u32;
24193        let bounds = (40, 40, 190, 230);
24194        let items = vec![
24195            DialogItem {
24196                item_type: 5,
24197                rect: (20, 20, 40, 150),
24198                text: "Sound".to_string(),
24199                resource_id: 0,
24200                proc_ptr: 0,
24201                sel_start: 0,
24202                sel_end: 0,
24203            },
24204            DialogItem {
24205                item_type: 0x80 | 5,
24206                rect: (52, 20, 72, 150),
24207                text: "Sound".to_string(),
24208                resource_id: 0,
24209                proc_ptr: 0,
24210                sel_start: 0,
24211                sel_end: 0,
24212            },
24213            DialogItem {
24214                item_type: 6,
24215                rect: (84, 20, 104, 150),
24216                text: "Music".to_string(),
24217                resource_id: 0,
24218                proc_ptr: 0,
24219                sel_start: 0,
24220                sel_end: 0,
24221            },
24222            DialogItem {
24223                item_type: 0x80 | 6,
24224                rect: (116, 20, 136, 150),
24225                text: "Music".to_string(),
24226                resource_id: 0,
24227                proc_ptr: 0,
24228                sel_start: 0,
24229                sel_end: 0,
24230            },
24231        ];
24232
24233        let (mut dispatcher, _cpu, mut bus) = setup();
24234        dispatcher.set_screen_mode_for_test(screen_base, row_bytes, 512, 342, 1);
24235        dispatcher.draw_dialog(&mut bus, bounds, 1, "", &items, 0, "", 0, false, 0x2000);
24236
24237        let label_left = bounds.1
24238            + items[0].rect.1
24239            + TrapDispatcher::STANDARD_CONTROL_MARK_LEFT_INSET
24240            + TrapDispatcher::STANDARD_CONTROL_MARK_SIZE
24241            + TrapDispatcher::STANDARD_CONTROL_TITLE_GAP;
24242        let enabled_checkbox = count_set_pixels(
24243            &bus,
24244            screen_base,
24245            row_bytes,
24246            bounds.0 + items[0].rect.0,
24247            label_left,
24248            bounds.0 + items[0].rect.2,
24249            bounds.1 + items[0].rect.3,
24250        );
24251        let disabled_checkbox = count_set_pixels(
24252            &bus,
24253            screen_base,
24254            row_bytes,
24255            bounds.0 + items[1].rect.0,
24256            label_left,
24257            bounds.0 + items[1].rect.2,
24258            bounds.1 + items[1].rect.3,
24259        );
24260        let enabled_radio = count_set_pixels(
24261            &bus,
24262            screen_base,
24263            row_bytes,
24264            bounds.0 + items[2].rect.0,
24265            label_left,
24266            bounds.0 + items[2].rect.2,
24267            bounds.1 + items[2].rect.3,
24268        );
24269        let disabled_radio = count_set_pixels(
24270            &bus,
24271            screen_base,
24272            row_bytes,
24273            bounds.0 + items[3].rect.0,
24274            label_left,
24275            bounds.0 + items[3].rect.2,
24276            bounds.1 + items[3].rect.3,
24277        );
24278
24279        assert!(
24280            enabled_checkbox > 0,
24281            "enabled checkbox title should be visible"
24282        );
24283        assert_eq!(
24284            enabled_checkbox, disabled_checkbox,
24285            "classic itemDisable checkbox title must not be visually dimmed"
24286        );
24287        assert!(enabled_radio > 0, "enabled radio title should be visible");
24288        assert_eq!(
24289            enabled_radio, disabled_radio,
24290            "classic itemDisable radio title must not be visually dimmed"
24291        );
24292    }
24293
24294    #[test]
24295    fn draw_dialog_standard_control_hilite_255_titles_use_gray_device_index() {
24296        let screen_base = 0x300000u32;
24297        let row_bytes = 160u32;
24298        let bounds = (20, 20, 120, 220);
24299        let dialog_ptr = 0x2400u32;
24300        let items = vec![
24301            DialogItem {
24302                item_type: 5,
24303                rect: (20, 20, 40, 150),
24304                text: "Sound".to_string(),
24305                resource_id: 0,
24306                proc_ptr: 0,
24307                sel_start: 0,
24308                sel_end: 0,
24309            },
24310            DialogItem {
24311                item_type: 6,
24312                rect: (52, 20, 72, 150),
24313                text: "Music".to_string(),
24314                resource_id: 0,
24315                proc_ptr: 0,
24316                sel_start: 0,
24317                sel_end: 0,
24318            },
24319        ];
24320
24321        let (mut dispatcher, _cpu, mut bus) = setup();
24322        dispatcher.set_screen_mode_for_test(screen_base, row_bytes, 160, 140, 8);
24323        dispatcher.device_clut = [[0x2020, 0x4040, 0x6060]; 256];
24324        dispatcher.device_clut[0] = [0xFFFF, 0xFFFF, 0xFFFF];
24325        dispatcher.device_clut[42] = [0xA1A1, 0xA1A1, 0xA1A1];
24326        dispatcher.device_clut[255] = [0, 0, 0];
24327        for offset in 0..(row_bytes * 140) {
24328            bus.write_byte(screen_base + offset, 0);
24329        }
24330
24331        for (item_no, proc_id, item) in [(1i16, 1i16, &items[0]), (2, 2, &items[1])] {
24332            let ctrl_ptr = bus.alloc(296);
24333            let ctrl_handle = bus.alloc(4);
24334            bus.write_long(ctrl_handle, ctrl_ptr);
24335            dispatcher.initialize_control_record(
24336                &mut bus,
24337                ctrl_ptr,
24338                dialog_ptr,
24339                item.rect,
24340                item.text.as_bytes(),
24341                true,
24342                0,
24343                0,
24344                1,
24345                proc_id,
24346                0,
24347            );
24348            bus.write_byte(ctrl_ptr + 17, 255);
24349            dispatcher
24350                .dialog_control_handles
24351                .insert(ctrl_handle, (dialog_ptr, item_no));
24352            dispatcher
24353                .dialog_control_values
24354                .insert((dialog_ptr, item_no), 0);
24355        }
24356
24357        dispatcher.draw_dialog(&mut bus, bounds, 1, "", &items, 0, "", 0, false, dialog_ptr);
24358
24359        for (label, item) in [("checkbox", &items[0]), ("radio", &items[1])] {
24360            let label_left = bounds.1
24361                + item.rect.1
24362                + TrapDispatcher::STANDARD_CONTROL_MARK_LEFT_INSET
24363                + TrapDispatcher::STANDARD_CONTROL_MARK_SIZE
24364                + TrapDispatcher::STANDARD_CONTROL_TITLE_GAP;
24365            let top = bounds.0 + item.rect.0;
24366            let bottom = bounds.0 + item.rect.2;
24367            let right = bounds.1 + item.rect.3;
24368            let gray = count_pixel_index(
24369                &bus,
24370                screen_base,
24371                row_bytes,
24372                top,
24373                label_left,
24374                bottom,
24375                right,
24376                42,
24377            );
24378            let black = count_pixel_index(
24379                &bus,
24380                screen_base,
24381                row_bytes,
24382                top,
24383                label_left,
24384                bottom,
24385                right,
24386                255,
24387            );
24388            assert!(
24389                gray > 12,
24390                "inactive standard DITL {label} title should draw with the device gray index"
24391            );
24392            assert_eq!(
24393                black, 0,
24394                "inactive standard DITL {label} title must not leave black glyph pixels"
24395            );
24396        }
24397    }
24398
24399    #[test]
24400    fn draw_dialog_systemless_theme_routes_frame_through_provider() {
24401        let screen_base = 0x300000u32;
24402        let row_bytes = 64u32;
24403        let bounds = (40, 40, 100, 140);
24404
24405        let (mut classic, _classic_cpu, mut classic_bus) = setup();
24406        classic.set_screen_mode_for_test(screen_base, row_bytes, 512, 342, 1);
24407        classic.draw_dialog(
24408            &mut classic_bus,
24409            bounds,
24410            1,
24411            "",
24412            &[],
24413            0,
24414            "",
24415            0,
24416            false,
24417            0x2000,
24418        );
24419
24420        let (mut themed, _themed_cpu, mut themed_bus) = setup();
24421        themed.set_ui_theme_id(UiThemeId::SystemlessDefault);
24422        themed.set_screen_mode_for_test(screen_base, row_bytes, 512, 342, 1);
24423        themed.draw_dialog(&mut themed_bus, bounds, 1, "", &[], 0, "", 0, false, 0x2000);
24424
24425        assert!(
24426            !screen_pixel_is_set(&classic_bus, screen_base, row_bytes, 50, 38),
24427            "classic dBoxProc leaves the gap between outer and inner borders clear"
24428        );
24429        assert!(
24430            screen_pixel_is_set(&themed_bus, screen_base, row_bytes, 50, 38),
24431            "systemless-default provider should own dialog frame accent pixels"
24432        );
24433    }
24434
24435    #[test]
24436    fn draw_window_frame_systemless_theme_routes_border_through_provider() {
24437        let screen_base = 0x300000u32;
24438        let row_bytes = 64u32;
24439
24440        let (mut classic, _classic_cpu, mut classic_bus) = setup();
24441        classic.set_screen_mode_for_test(screen_base, row_bytes, 512, 342, 1);
24442        classic.window_bounds = (40, 40, 100, 140);
24443        classic.window_proc_id = 1;
24444        classic.draw_window_frame(&mut classic_bus);
24445
24446        let (mut themed, _themed_cpu, mut themed_bus) = setup();
24447        themed.set_ui_theme_id(UiThemeId::SystemlessDefault);
24448        themed.set_screen_mode_for_test(screen_base, row_bytes, 512, 342, 1);
24449        themed.window_bounds = (40, 40, 100, 140);
24450        themed.window_proc_id = 1;
24451        themed.draw_window_frame(&mut themed_bus);
24452
24453        assert!(
24454            !screen_pixel_is_set(&classic_bus, screen_base, row_bytes, 50, 34),
24455            "classic dBoxProc window frame keeps the structure gap clear"
24456        );
24457        assert!(
24458            screen_pixel_is_set(&themed_bus, screen_base, row_bytes, 50, 34),
24459            "systemless-default provider should own window frame accent pixels"
24460        );
24461    }
24462
24463    #[test]
24464    fn draw_dialog_preserves_existing_user_item_pixels() {
24465        // UserItems are application-owned drawing areas. Games often draw
24466        // into them before calling DrawDialog; the Dialog Manager redraw
24467        // must not erase that content while refreshing the standard items.
24468        let (mut disp, mut cpu, mut bus) = setup();
24469        let dialog_ptr = bus.alloc(170);
24470        let (screen_base, row_bytes, _w, _h, pixel_size) = disp.screen_mode;
24471
24472        bus.write_word(dialog_ptr + 8, 0);
24473        bus.write_word(dialog_ptr + 10, 0);
24474        bus.write_word(dialog_ptr + 16, 0);
24475        bus.write_word(dialog_ptr + 18, 0);
24476        bus.write_word(dialog_ptr + 20, 40);
24477        bus.write_word(dialog_ptr + 22, 80);
24478        disp.dialog_items.insert(
24479            dialog_ptr,
24480            vec![
24481                DialogItem {
24482                    item_type: 0x80, // disabled userItem
24483                    rect: (8, 8, 20, 32),
24484                    text: String::new(),
24485                    resource_id: 0,
24486                    proc_ptr: 0,
24487                    sel_start: 0,
24488                    sel_end: 0,
24489                },
24490                DialogItem {
24491                    item_type: 4,
24492                    rect: (24, 8, 36, 40),
24493                    text: "OK".to_string(),
24494                    resource_id: 0,
24495                    proc_ptr: 0,
24496                    sel_start: 0,
24497                    sel_end: 0,
24498                },
24499            ],
24500        );
24501
24502        let user_x = 10u32;
24503        let user_y = 10u32;
24504        let background_x = 4u32;
24505        let background_y = 4u32;
24506        if pixel_size == 8 {
24507            bus.write_byte(screen_base + user_y * row_bytes + user_x, 0xFF);
24508            bus.write_byte(screen_base + background_y * row_bytes + background_x, 0xFF);
24509        } else {
24510            for (x, y) in [(user_x, user_y), (background_x, background_y)] {
24511                let addr = screen_base + y * row_bytes + (x / 8);
24512                let bit = 1 << (7 - (x % 8));
24513                bus.write_byte(addr, bus.read_byte(addr) | bit);
24514            }
24515        }
24516
24517        bus.write_long(TEST_SP, dialog_ptr);
24518        let result = disp.dispatch_dialog(true, 0x181, &mut cpu, &mut bus);
24519        assert!(result.unwrap().is_ok());
24520
24521        if pixel_size == 8 {
24522            assert_eq!(
24523                bus.read_byte(screen_base + user_y * row_bytes + user_x),
24524                0xFF,
24525                "DrawDialog should preserve pre-drawn userItem pixels"
24526            );
24527            assert_eq!(
24528                bus.read_byte(screen_base + background_y * row_bytes + background_x),
24529                0,
24530                "DrawDialog should still repaint normal dialog background"
24531            );
24532        } else {
24533            let user_addr = screen_base + user_y * row_bytes + (user_x / 8);
24534            let user_bit = 1 << (7 - (user_x % 8));
24535            let background_addr = screen_base + background_y * row_bytes + (background_x / 8);
24536            let background_bit = 1 << (7 - (background_x % 8));
24537            assert_ne!(bus.read_byte(user_addr) & user_bit, 0);
24538            assert_eq!(bus.read_byte(background_addr) & background_bit, 0);
24539        }
24540    }
24541
24542    #[test]
24543    fn modal_dialog_first_entry_does_not_preserve_invisible_user_item_background() {
24544        // A newly shown modal dialog has no meaningful userItem pixels yet.
24545        // Dialog Manager fills the dialog background; it must not treat the
24546        // previous screen contents as application-owned userItem drawing.
24547        let (mut disp, mut cpu, mut bus) = setup();
24548        let dialog_ptr = bus.alloc(170);
24549        let item_hit_ptr = bus.alloc(2);
24550        let (screen_base, row_bytes, _w, _h, pixel_size) = disp.screen_mode;
24551        assert_eq!(pixel_size, 8);
24552
24553        bus.write_word(dialog_ptr + 8, 0);
24554        bus.write_word(dialog_ptr + 10, 0);
24555        bus.write_word(dialog_ptr + 16, 0);
24556        bus.write_word(dialog_ptr + 18, 0);
24557        bus.write_word(dialog_ptr + 20, 40);
24558        bus.write_word(dialog_ptr + 22, 80);
24559        bus.write_word(dialog_ptr + 108, 2);
24560        disp.front_window = dialog_ptr;
24561        disp.window_bounds = (0, 0, 40, 80);
24562        disp.window_proc_id = 2;
24563        disp.dialog_items.insert(
24564            dialog_ptr,
24565            vec![
24566                DialogItem {
24567                    item_type: 0x80, // disabled userItem placeholder
24568                    rect: (8, 8, 20, 32),
24569                    text: String::new(),
24570                    resource_id: 0,
24571                    proc_ptr: 0,
24572                    sel_start: 0,
24573                    sel_end: 0,
24574                },
24575                DialogItem {
24576                    item_type: 4,
24577                    rect: (24, 8, 36, 40),
24578                    text: "OK".to_string(),
24579                    resource_id: 0,
24580                    proc_ptr: 0,
24581                    sel_start: 0,
24582                    sel_end: 0,
24583                },
24584            ],
24585        );
24586
24587        let user_x = 10u32;
24588        let user_y = 10u32;
24589        bus.write_byte(screen_base + user_y * row_bytes + user_x, 0xFF);
24590
24591        bus.write_long(TEST_SP, item_hit_ptr);
24592        bus.write_long(TEST_SP + 4, 0);
24593        let result = disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus);
24594        assert!(result.unwrap().is_ok());
24595
24596        assert_eq!(
24597            bus.read_byte(screen_base + user_y * row_bytes + user_x),
24598            0,
24599            "ModalDialog should paint a clean dialog background through untouched userItems"
24600        );
24601        assert!(disp.dialog_tracking.is_some());
24602    }
24603
24604    #[test]
24605    fn modal_dialog_first_entry_restores_visible_shell_before_user_item_preservation() {
24606        // Visible GetNewDialog draws a clean shell before the first ModalDialog
24607        // call. If later screen/chrome drawing leaves unrelated pixels in a
24608        // userItem rect, ModalDialog must restore that clean shell before
24609        // preserving application-owned userItem pixels. EVO's startup
24610        // shareware notice exposes this when title/menu art leaks through its
24611        // large userItem body.
24612        let (mut disp, mut cpu, mut bus) = setup();
24613        let screen_base = bus.alloc((800 * 600) as u32);
24614        bus.write_bytes(screen_base, &vec![0x77; 800 * 600]);
24615        bus.write_long(0x0824, screen_base);
24616        disp.screen_mode = (screen_base, 800, 800, 600, 8);
24617
24618        let mut dlog = build_test_dlog((100, 100, 180, 260), 1711, 0);
24619        dlog[10] = 1; // visible
24620        let ditl = build_test_ditl_items(&[
24621            (0, (8, 8, 30, 80), b"".as_slice()),
24622            (4, (44, 20, 64, 90), b"OK".as_slice()),
24623        ]);
24624        disp.install_test_resource(&mut bus, *b"DLOG", 1710, &dlog);
24625        disp.install_test_resource(&mut bus, *b"DITL", 1711, &ditl);
24626
24627        cpu.write_reg(Register::A7, TEST_SP);
24628        bus.write_long(TEST_SP, 0xFFFF_FFFF); // behind
24629        bus.write_long(TEST_SP + 4, 0); // dStorage
24630        bus.write_word(TEST_SP + 8, 1710); // dialogID
24631        disp.dispatch_dialog(true, 0x17C, &mut cpu, &mut bus)
24632            .unwrap()
24633            .unwrap();
24634
24635        let dlg_ptr = bus.read_long(TEST_SP + 10);
24636        assert_ne!(dlg_ptr, 0);
24637        assert!(disp.dialog_visible_snapshots.contains_key(&dlg_ptr));
24638
24639        let probe_v = 120u32;
24640        let probe_h = 120u32;
24641        let probe_addr = screen_base + probe_v * 800 + probe_h;
24642        assert_eq!(
24643            bus.read_byte(probe_addr),
24644            0,
24645            "visible dialog creation should paint the userItem area as dialog background"
24646        );
24647
24648        bus.write_byte(probe_addr, 0x55);
24649        let item_hit_ptr = bus.alloc(2);
24650        cpu.write_reg(Register::A7, TEST_SP);
24651        bus.write_long(TEST_SP, item_hit_ptr);
24652        bus.write_long(TEST_SP + 4, 0);
24653        disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus)
24654            .unwrap()
24655            .unwrap();
24656
24657        assert_eq!(
24658            bus.read_byte(probe_addr),
24659            0,
24660            "first ModalDialog entry should not preserve stale non-dialog pixels in userItems"
24661        );
24662        assert!(disp.dialog_tracking.is_some());
24663        assert!(
24664            !disp.dialog_visible_snapshots.contains_key(&dlg_ptr),
24665            "first ModalDialog entry consumes the clean visible shell snapshot"
24666        );
24667    }
24668
24669    #[test]
24670    fn premodal_dialog_port_draw_refreshes_visible_snapshot() {
24671        // Callers pass the port that was just drawn into (the current graphics
24672        // port). When that port is a visible dialog with a retained snapshot,
24673        // the drawing landed inside the dialog and is application-owned dialog
24674        // content — even before ModalDialog has begun modal tracking. Games
24675        // routinely render dialog content directly into the window before
24676        // entering ModalDialog (EV Override blits its Game Speed slider into a
24677        // userItem rect; Marathon draws custom controls the same way), so the
24678        // retained snapshot must be refreshed to capture it.
24679        let (mut disp, _cpu, mut bus) = setup();
24680        let screen_base = bus.alloc((64 * 64) as u32);
24681        bus.write_bytes(screen_base, &vec![0x00; 64 * 64]);
24682        bus.write_long(0x0824, screen_base);
24683        disp.screen_mode = (screen_base, 64, 64, 64, 8);
24684
24685        let dialog_ptr = 0x200000u32;
24686        let bounds = (10, 10, 30, 30);
24687        let snapshot_width = (bounds.3 - bounds.1 + TrapDispatcher::DBOX_FRAME_MARGIN * 2) as usize;
24688        let snapshot_height =
24689            (bounds.2 - bounds.0 + TrapDispatcher::DBOX_FRAME_MARGIN * 2) as usize;
24690        disp.front_window = dialog_ptr;
24691        disp.dialog_items.insert(dialog_ptr, Vec::new());
24692        disp.dialog_visible_snapshots.insert(
24693            dialog_ptr,
24694            PersistentDialogSnapshot {
24695                bounds,
24696                pixels: vec![0x11; snapshot_width * snapshot_height],
24697            },
24698        );
24699
24700        let probe = screen_base + 20 * 64 + 20;
24701        bus.write_byte(probe, 0x77);
24702        disp.refresh_visible_dialog_snapshot_after_bulk_port_draw(&bus, dialog_ptr);
24703        bus.write_byte(probe, 0x00);
24704        disp.restore_visible_dialog_snapshots(&mut bus);
24705        assert_eq!(
24706            bus.read_byte(probe),
24707            0x77,
24708            "drawing into the dialog's own port refreshes its retained snapshot"
24709        );
24710
24711        disp.dialog_modal_entered.insert(dialog_ptr);
24712        bus.write_byte(probe, 0x88);
24713        disp.refresh_visible_dialog_snapshot_after_bulk_port_draw(&bus, dialog_ptr);
24714        bus.write_byte(probe, 0x00);
24715        disp.restore_visible_dialog_snapshots(&mut bus);
24716        assert_eq!(
24717            bus.read_byte(probe),
24718            0x88,
24719            "retained modal dialogs may refresh snapshots from later bulk drawing"
24720        );
24721
24722        let game_dialog_ptr = 0x200100u32;
24723        let game_bounds = (34, 10, 54, 30);
24724        bus.write_word(game_dialog_ptr + 6, 0xC000);
24725        let pixmap_handle = bus.alloc(4);
24726        let pixmap_ptr = bus.alloc(50);
24727        bus.write_long(pixmap_handle, pixmap_ptr);
24728        bus.write_long(pixmap_ptr, screen_base);
24729        bus.write_word(pixmap_ptr + 4, 0x8000 | 64);
24730        bus.write_word(pixmap_ptr + 6, (-(game_bounds.0)) as u16);
24731        bus.write_word(pixmap_ptr + 8, (-(game_bounds.1)) as u16);
24732        bus.write_word(pixmap_ptr + 32, 8);
24733        bus.write_long(game_dialog_ptr + 2, pixmap_handle);
24734        bus.write_word(game_dialog_ptr + 16, 0);
24735        bus.write_word(game_dialog_ptr + 18, 0);
24736        bus.write_word(game_dialog_ptr + 20, 20);
24737        bus.write_word(game_dialog_ptr + 22, 20);
24738        bus.write_byte(game_dialog_ptr + 110, 0xFF);
24739        disp.dialog_items.insert(
24740            game_dialog_ptr,
24741            vec![DialogItem {
24742                item_type: 0,
24743                rect: (0, 0, 20, 20),
24744                text: String::new(),
24745                resource_id: 0,
24746                proc_ptr: 0,
24747                sel_start: 0,
24748                sel_end: 0,
24749            }],
24750        );
24751        disp.front_window = game_dialog_ptr;
24752        bus.write_byte(screen_base + 40 * 64 + 20, 0x99);
24753        disp.refresh_visible_dialog_snapshot_after_bulk_port_draw(&bus, game_dialog_ptr);
24754        bus.write_byte(screen_base + 40 * 64 + 20, 0x00);
24755        disp.restore_visible_dialog_snapshots(&mut bus);
24756        assert_eq!(
24757            bus.read_byte(screen_base + 40 * 64 + 20),
24758            0x99,
24759            "game-managed dialogs should create snapshots from app-owned bulk drawing"
24760        );
24761    }
24762
24763    #[test]
24764    fn updtdialog_pops_eight_bytes() {
24765        // Inside Macintosh Volume I, I-415: UpdtDialog is a Pascal
24766        // procedure taking theDialog and updateRgn.
24767        let (mut disp, mut cpu, mut bus) = setup();
24768        let sp = TEST_SP;
24769        bus.write_long(sp, 0x300100); // updateRgn
24770        bus.write_long(sp + 4, 0x200000); // theDialog
24771
24772        let result = disp.dispatch_dialog(true, 0x178, &mut cpu, &mut bus);
24773        assert!(result.unwrap().is_ok());
24774        assert_eq!(cpu.read_reg(Register::A7), sp + 8);
24775    }
24776
24777    #[test]
24778    fn updtdialog_nil_update_region_is_a_noop() {
24779        // Inside Macintosh Volume I, I-415 names updateRgn as the dialog's
24780        // update region. A NIL region should not trigger redraw work.
24781        let (mut disp, mut cpu, mut bus) = setup();
24782        let existing = 0x200040u32;
24783        disp.window_list = vec![existing];
24784        disp.front_window = existing;
24785        bus.write_byte(existing + 110, 0xFF);
24786
24787        let screen_base = bus.alloc((800 * 600) as u32);
24788        bus.write_long(0x0824, screen_base);
24789        disp.screen_mode = (screen_base, 800, 800, 600, 8);
24790
24791        let bounds_rect_ptr = 0x301200u32;
24792        bus.write_word(bounds_rect_ptr, 100);
24793        bus.write_word(bounds_rect_ptr + 2, 100);
24794        bus.write_word(bounds_rect_ptr + 4, 300);
24795        bus.write_word(bounds_rect_ptr + 6, 400);
24796
24797        let sp = TEST_SP - 30;
24798        cpu.write_reg(Register::A7, sp);
24799        for i in 0..34u32 {
24800            bus.write_byte(sp + i, 0);
24801        }
24802        bus.write_long(sp + 22, bounds_rect_ptr);
24803        bus.write_word(sp + 16, 1); // visible
24804        bus.write_long(sp + 10, 0); // behind = NIL (backmost)
24805
24806        let result = disp.dispatch_dialog(true, 0x17D, &mut cpu, &mut bus);
24807        assert!(result.unwrap().is_ok());
24808        let dlg_ptr = bus.read_long(sp + 30);
24809        assert_ne!(dlg_ptr, 0);
24810
24811        let probe_x = 120u32;
24812        let probe_y = 120u32;
24813        let probe_addr = screen_base + probe_y * 800 + probe_x;
24814        bus.write_byte(probe_addr, 0xFF);
24815
24816        cpu.write_reg(Register::A7, TEST_SP);
24817        bus.write_long(TEST_SP, 0); // NIL update region
24818        bus.write_long(TEST_SP + 4, dlg_ptr);
24819
24820        let result = disp.dispatch_dialog(true, 0x178, &mut cpu, &mut bus);
24821        assert!(result.unwrap().is_ok());
24822        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
24823        assert_eq!(bus.read_byte(probe_addr), 0xFF);
24824    }
24825
24826    #[test]
24827    fn updtdialog_queues_user_item_draw_proc_for_intersecting_update_region() {
24828        // MTE 1992, 6-142 to 6-143: UpdateDialog redraws only items in
24829        // the supplied update region and calls application-defined item
24830        // draw procedures.
24831        let (mut disp, mut cpu, mut bus) = setup();
24832        let screen_base = bus.alloc(100 * 100);
24833        bus.write_long(0x0824, screen_base);
24834        disp.screen_mode = (screen_base, 100, 100, 100, 8);
24835
24836        let dialog_ptr = bus.alloc(256);
24837        bus.write_word(dialog_ptr + 16, 0);
24838        bus.write_word(dialog_ptr + 18, 0);
24839        bus.write_word(dialog_ptr + 20, 100);
24840        bus.write_word(dialog_ptr + 22, 100);
24841        bus.write_word(dialog_ptr + 108, 2); // plain dialog box
24842        disp.dialog_items.insert(
24843            dialog_ptr,
24844            vec![
24845                DialogItem {
24846                    item_type: 0,
24847                    rect: (8, 8, 24, 32),
24848                    proc_ptr: 0x500000,
24849                    ..Default::default()
24850                },
24851                DialogItem {
24852                    item_type: 0,
24853                    rect: (60, 60, 80, 80),
24854                    proc_ptr: 0x600000,
24855                    ..Default::default()
24856                },
24857            ],
24858        );
24859        let update_rgn_ptr = bus.alloc(10);
24860        let update_rgn = bus.alloc(4);
24861        bus.write_long(update_rgn, update_rgn_ptr);
24862        bus.write_word(update_rgn_ptr, 10);
24863        bus.write_word(update_rgn_ptr + 2, 0);
24864        bus.write_word(update_rgn_ptr + 4, 0);
24865        bus.write_word(update_rgn_ptr + 6, 40);
24866        bus.write_word(update_rgn_ptr + 8, 40);
24867
24868        bus.write_long(TEST_SP, update_rgn);
24869        bus.write_long(TEST_SP + 4, dialog_ptr);
24870        let result = disp.dispatch_dialog(true, 0x178, &mut cpu, &mut bus);
24871        assert!(result.unwrap().is_ok());
24872
24873        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
24874        assert_eq!(
24875            disp.modeless_dialog_draw_proc_queue
24876                .iter()
24877                .copied()
24878                .collect::<Vec<_>>(),
24879            vec![(dialog_ptr, 0x500000, 1)]
24880        );
24881    }
24882
24883    #[test]
24884    fn te_text_box_erases_box_before_drawing_text() {
24885        // Inside Macintosh: Text 1993, p. 2-88: TETextBox erases the box
24886        // and then draws wrapped/aligned text into it.
24887        let (mut disp, mut cpu, mut bus) = setup_with_port();
24888        let port_ptr = 0x181000u32;
24889        let screen_base = bus.read_long(0x0824);
24890
24891        // Fill the target area black so a missing EraseRect is visible.
24892        for y in 0..12u32 {
24893            for byte in 0..3u32 {
24894                bus.write_byte(screen_base + y * 64 + byte, 0xFF);
24895            }
24896        }
24897
24898        // Make sure the text renderer has a usable size and port state.
24899        disp.current_port = port_ptr;
24900        disp.tx_size = 12;
24901        bus.write_word(port_ptr + 74, 12);
24902
24903        let text_ptr = 0x200000u32;
24904        bus.write_byte(text_ptr, b'A');
24905
24906        let box_ptr = 0x200100u32;
24907        bus.write_word(box_ptr, 0); // top
24908        bus.write_word(box_ptr + 2, 0); // left
24909        bus.write_word(box_ptr + 4, 12); // bottom
24910        bus.write_word(box_ptr + 6, 24); // right
24911
24912        let sp = TEST_SP - 14;
24913        cpu.write_reg(Register::A7, sp);
24914        bus.write_word(sp, 0); // teFlushLeft
24915        bus.write_long(sp + 2, box_ptr);
24916        bus.write_long(sp + 6, 1); // length
24917        bus.write_long(sp + 10, text_ptr);
24918
24919        let result = disp.dispatch_dialog(true, 0x1CE, &mut cpu, &mut bus);
24920        assert!(result.unwrap().is_ok());
24921        assert_eq!(cpu.read_reg(Register::A7), TEST_SP);
24922
24923        // Pixel (20, 10) lies inside the box but outside the glyph; it should
24924        // be white after the implicit EraseRect.
24925        let probe_addr = screen_base + 10 * 64 + 2;
24926        assert_eq!(bus.read_byte(probe_addr) & (1 << 3), 0);
24927    }
24928
24929    #[test]
24930    fn te_text_box_erases_box_for_zero_length_text() {
24931        // Inside Macintosh: Text 1993, p. 2-88: the erase precedes text
24932        // drawing and is not conditional on the text length.
24933        let (mut disp, mut cpu, mut bus) = setup_with_port();
24934        let port_ptr = 0x181000u32;
24935        let screen_base = bus.read_long(0x0824);
24936
24937        for y in 0..12u32 {
24938            for byte in 0..3u32 {
24939                bus.write_byte(screen_base + y * 64 + byte, 0xFF);
24940            }
24941        }
24942
24943        disp.current_port = port_ptr;
24944        disp.tx_size = 12;
24945        bus.write_word(port_ptr + 74, 12);
24946
24947        let text_ptr = 0x200000u32;
24948        let box_ptr = 0x200100u32;
24949        bus.write_word(box_ptr, 0);
24950        bus.write_word(box_ptr + 2, 0);
24951        bus.write_word(box_ptr + 4, 12);
24952        bus.write_word(box_ptr + 6, 24);
24953
24954        let sp = TEST_SP - 14;
24955        cpu.write_reg(Register::A7, sp);
24956        bus.write_word(sp, 0);
24957        bus.write_long(sp + 2, box_ptr);
24958        bus.write_long(sp + 6, 0);
24959        bus.write_long(sp + 10, text_ptr);
24960
24961        let result = disp.dispatch_dialog(true, 0x1CE, &mut cpu, &mut bus);
24962        assert!(result.unwrap().is_ok());
24963        assert_eq!(cpu.read_reg(Register::A7), TEST_SP);
24964
24965        let probe_addr = screen_base + 10 * 64 + 2;
24966        assert_eq!(bus.read_byte(probe_addr) & (1 << 3), 0);
24967    }
24968
24969    #[test]
24970    fn te_text_box_consumes_align_box_length_text_arguments() {
24971        // Inside Macintosh: Text 1993, p. 2-88 declares TETextBox as a
24972        // procedure taking text/length/box/align arguments.
24973        let (mut disp, mut cpu, mut bus) = setup_with_port();
24974        let port_ptr = 0x181000u32;
24975        disp.current_port = port_ptr;
24976        disp.tx_size = 12;
24977        bus.write_word(port_ptr + 74, 12);
24978
24979        let text_ptr = 0x200200u32;
24980        bus.write_byte(text_ptr, b'A');
24981
24982        let box_ptr = 0x200240u32;
24983        bus.write_word(box_ptr, 0);
24984        bus.write_word(box_ptr + 2, 0);
24985        bus.write_word(box_ptr + 4, 20);
24986        bus.write_word(box_ptr + 6, 60);
24987
24988        let sp = TEST_SP - 14;
24989        cpu.write_reg(Register::A7, sp);
24990        bus.write_word(sp, 0);
24991        bus.write_long(sp + 2, box_ptr);
24992        bus.write_long(sp + 6, 1);
24993        bus.write_long(sp + 10, text_ptr);
24994
24995        let result = disp.dispatch_dialog(true, 0x1CE, &mut cpu, &mut bus);
24996        assert!(result.unwrap().is_ok());
24997        assert_eq!(cpu.read_reg(Register::A7), TEST_SP);
24998    }
24999
25000    #[test]
25001    fn te_text_box_align_parameter_controls_rendered_line_origin() {
25002        // Inside Macintosh: Text 1993, p. 2-87 defines teJustLeft(0),
25003        // teJustCenter(1), and teJustRight(-1) alignment values.
25004        let (mut disp, mut cpu, mut bus) = setup_with_port();
25005        let port_ptr = 0x181000u32;
25006        disp.current_port = port_ptr;
25007        disp.tx_size = 12;
25008        bus.write_word(port_ptr + 74, 12);
25009
25010        let text = b"ABC";
25011        let text_ptr = 0x200280u32;
25012        bus.write_bytes(text_ptr, text);
25013
25014        let box_ptr = 0x2002C0u32;
25015        let box_top = 0i16;
25016        let box_left = 10i16;
25017        let box_bottom = 40i16;
25018        let box_right = 110i16;
25019        bus.write_word(box_ptr, box_top as u16);
25020        bus.write_word(box_ptr + 2, box_left as u16);
25021        bus.write_word(box_ptr + 4, box_bottom as u16);
25022        bus.write_word(box_ptr + 6, box_right as u16);
25023
25024        let advance_extra = disp.advance_extra();
25025        let missing_advance = disp.missing_glyph_advance();
25026        let mut line_width = 0i16;
25027        for &byte in text {
25028            if let Some((glyph, _)) =
25029                crate::quickdraw::text::get_glyph(disp.tx_font, disp.tx_size, byte as char)
25030            {
25031                line_width += glyph.advance as i16 + advance_extra;
25032            } else {
25033                line_width += missing_advance;
25034            }
25035        }
25036
25037        let mut run_align = |align: i16| -> i16 {
25038            let sp = TEST_SP - 14;
25039            cpu.write_reg(Register::A7, sp);
25040            bus.write_word(sp, align as u16);
25041            bus.write_long(sp + 2, box_ptr);
25042            bus.write_long(sp + 6, text.len() as u32);
25043            bus.write_long(sp + 10, text_ptr);
25044            let result = disp.dispatch_dialog(true, 0x1CE, &mut cpu, &mut bus);
25045            assert!(result.unwrap().is_ok());
25046            assert_eq!(cpu.read_reg(Register::A7), TEST_SP);
25047            disp.pn_loc.1
25048        };
25049
25050        let x_left = run_align(0);
25051        let x_center = run_align(1);
25052        let x_right = run_align(-1);
25053
25054        let expected_left =
25055            TrapDispatcher::te_line_origin_x(0, box_left, box_right, line_width) + line_width;
25056        let expected_center =
25057            TrapDispatcher::te_line_origin_x(1, box_left, box_right, line_width) + line_width;
25058        let expected_right =
25059            TrapDispatcher::te_line_origin_x(-1, box_left, box_right, line_width) + line_width;
25060
25061        assert_eq!(x_left, expected_left);
25062        assert_eq!(x_center, expected_center);
25063        assert_eq!(x_right, expected_right);
25064        assert!(x_left < x_center && x_center < x_right);
25065    }
25066
25067    #[test]
25068    fn te_text_box_pascal_procedure_protocol_does_not_overwrite_past_arg_frame() {
25069        // Per Inside Macintosh: Text 1993, p. 2-88 TETextBox is a
25070        // Pascal PROCEDURE. The MPW Universal Headers C declaration
25071        // uses `const Rect *box`, so the caller pushes exactly 14
25072        // bytes (2-byte just + 4-byte Rect* + 4-byte long + 4-byte
25073        // text Ptr) and the trap must pop exactly 14 bytes with no
25074        // result slot written.
25075        let (mut disp, mut cpu, mut bus) = setup_with_port();
25076        let port_ptr = 0x181000u32;
25077        disp.current_port = port_ptr;
25078        disp.tx_size = 12;
25079        bus.write_word(port_ptr + 74, 12);
25080
25081        let text_ptr = 0x200400u32;
25082        bus.write_byte(text_ptr, b'X');
25083
25084        let box_ptr = 0x200440u32;
25085        bus.write_word(box_ptr, 0);
25086        bus.write_word(box_ptr + 2, 0);
25087        bus.write_word(box_ptr + 4, 12);
25088        bus.write_word(box_ptr + 6, 64);
25089
25090        // Pre-poison memory immediately past the 14-byte arg frame.
25091        // After the trap pops 14 bytes, A7 must equal TEST_SP and the
25092        // sentinel words at TEST_SP, TEST_SP+2 must survive untouched.
25093        let sp = TEST_SP - 14;
25094        bus.write_word(TEST_SP, 0xCAFE);
25095        bus.write_word(TEST_SP + 2, 0xBABE);
25096        cpu.write_reg(Register::A7, sp);
25097        bus.write_word(sp, 0); // align = teJustLeft
25098        bus.write_long(sp + 2, box_ptr);
25099        bus.write_long(sp + 6, 1); // length
25100        bus.write_long(sp + 10, text_ptr);
25101
25102        let result = disp.dispatch_dialog(true, 0x1CE, &mut cpu, &mut bus);
25103        assert!(result.unwrap().is_ok());
25104        assert_eq!(cpu.read_reg(Register::A7), TEST_SP);
25105        assert_eq!(bus.read_word(TEST_SP), 0xCAFE);
25106        assert_eq!(bus.read_word(TEST_SP + 2), 0xBABE);
25107    }
25108
25109    #[test]
25110    fn te_text_box_wraps_when_text_exceeds_box_width() {
25111        // Inside Macintosh: Text 1993, p. 2-88: TETextBox word-wraps text
25112        // in the destination rectangle.
25113        let (mut disp, mut cpu, mut bus) = setup_with_port();
25114        let port_ptr = 0x181000u32;
25115        disp.current_port = port_ptr;
25116        disp.tx_size = 12;
25117        bus.write_word(port_ptr + 74, 12);
25118
25119        let text = b"A A A A";
25120        let text_ptr = 0x200300u32;
25121        bus.write_bytes(text_ptr, text);
25122
25123        let mut glyph_w = disp.missing_glyph_advance();
25124        if let Some((glyph, _)) = crate::quickdraw::text::get_glyph(disp.tx_font, disp.tx_size, 'A')
25125        {
25126            glyph_w = glyph.advance as i16 + disp.advance_extra();
25127        }
25128
25129        let wide_box_ptr = 0x200340u32;
25130        bus.write_word(wide_box_ptr, 0);
25131        bus.write_word(wide_box_ptr + 2, 0);
25132        bus.write_word(wide_box_ptr + 4, 80);
25133        bus.write_word(wide_box_ptr + 6, 140);
25134
25135        let narrow_box_ptr = 0x200380u32;
25136        bus.write_word(narrow_box_ptr, 0);
25137        bus.write_word(narrow_box_ptr + 2, 0);
25138        bus.write_word(narrow_box_ptr + 4, 80);
25139        bus.write_word(narrow_box_ptr + 6, glyph_w as u16);
25140
25141        let mut run_box = |box_ptr: u32| -> i16 {
25142            let sp = TEST_SP - 14;
25143            cpu.write_reg(Register::A7, sp);
25144            bus.write_word(sp, 0);
25145            bus.write_long(sp + 2, box_ptr);
25146            bus.write_long(sp + 6, text.len() as u32);
25147            bus.write_long(sp + 10, text_ptr);
25148            let result = disp.dispatch_dialog(true, 0x1CE, &mut cpu, &mut bus);
25149            assert!(result.unwrap().is_ok());
25150            assert_eq!(cpu.read_reg(Register::A7), TEST_SP);
25151            disp.pn_loc.0
25152        };
25153
25154        let y_wide = run_box(wide_box_ptr);
25155        let y_narrow = run_box(narrow_box_ptr);
25156        let metrics = crate::quickdraw::text::get_font_metrics(disp.tx_font, disp.tx_size);
25157        let line_height = metrics.ascent + metrics.descent + metrics.leading.max(2);
25158
25159        assert!(
25160            y_narrow >= y_wide + line_height,
25161            "narrow box should force at least one additional wrapped line"
25162        );
25163    }
25164
25165    // ---- CloseDialog ($A982) ----
25166
25167    fn alloc_region_handle(bus: &mut MacMemoryBus, rect: Option<(i16, i16, i16, i16)>) -> u32 {
25168        let rgn_ptr = bus.alloc(10);
25169        bus.write_word(rgn_ptr, 10);
25170        if let Some((top, left, bottom, right)) = rect.filter(|r| r.2 > r.0 && r.3 > r.1) {
25171            bus.write_word(rgn_ptr + 2, top as u16);
25172            bus.write_word(rgn_ptr + 4, left as u16);
25173            bus.write_word(rgn_ptr + 6, bottom as u16);
25174            bus.write_word(rgn_ptr + 8, right as u16);
25175        } else {
25176            bus.write_long(rgn_ptr + 2, 0);
25177            bus.write_long(rgn_ptr + 6, 0);
25178        }
25179        let handle = bus.alloc(4);
25180        bus.write_long(handle, rgn_ptr);
25181        handle
25182    }
25183
25184    fn seed_window_regions(
25185        bus: &mut MacMemoryBus,
25186        window_ptr: u32,
25187        content_rect: (i16, i16, i16, i16),
25188    ) {
25189        // Minimal WindowRecord region setup for invalidate_window_rect:
25190        // contRgn @ +118 and updateRgn @ +122.
25191        bus.write_word(window_ptr + 16, content_rect.0 as u16);
25192        bus.write_word(window_ptr + 18, content_rect.1 as u16);
25193        bus.write_word(window_ptr + 20, content_rect.2 as u16);
25194        bus.write_word(window_ptr + 22, content_rect.3 as u16);
25195        let cont_rgn = alloc_region_handle(bus, Some(content_rect));
25196        let update_rgn = alloc_region_handle(bus, None);
25197        bus.write_long(window_ptr + 118, cont_rgn);
25198        bus.write_long(window_ptr + 122, update_rgn);
25199        bus.write_byte(window_ptr + 110, 0xFF);
25200    }
25201
25202    fn seed_app_owned_modal_dialog(
25203        disp: &mut TrapDispatcher,
25204        bus: &mut MacMemoryBus,
25205        dialog_ptr: u32,
25206        prev_window: u32,
25207        proc_id: i16,
25208    ) {
25209        seed_window_regions(bus, dialog_ptr, (0, 0, 100, 220));
25210        seed_window_regions(bus, prev_window, (0, 0, 342, 512));
25211        bus.write_word(dialog_ptr + 8, 0);
25212        bus.write_word(dialog_ptr + 10, 0);
25213        bus.write_word(dialog_ptr + 108, 2);
25214        disp.window_proc_ids.insert(dialog_ptr, proc_id);
25215        disp.front_window = dialog_ptr;
25216        disp.current_port = dialog_ptr;
25217        disp.window_bounds = (0, 0, 100, 220);
25218        disp.window_proc_id = proc_id;
25219        disp.window_list = vec![dialog_ptr, prev_window];
25220        disp.window_stack
25221            .push((prev_window, (0, 0, 342, 512), 0, "Game".to_string()));
25222        disp.dialog_items.insert(
25223            dialog_ptr,
25224            vec![DialogItem {
25225                item_type: 4,
25226                rect: (60, 130, 82, 200),
25227                text: "OK".to_string(),
25228                ..DialogItem::default()
25229            }],
25230        );
25231        disp.sent_open_app_event = true;
25232    }
25233
25234    fn seed_retained_modal_dialog(
25235        disp: &mut TrapDispatcher,
25236        bus: &mut MacMemoryBus,
25237        dialog_ptr: u32,
25238        prev_window: u32,
25239        proc_id: i16,
25240    ) {
25241        seed_app_owned_modal_dialog(disp, bus, dialog_ptr, prev_window, proc_id);
25242        disp.dialog_modal_entered.insert(dialog_ptr);
25243    }
25244
25245    #[test]
25246    fn close_dialog_pops_4_bytes() {
25247        // Inside Macintosh Volume I, I-413: CloseDialog takes one
25248        // DialogPtr argument.
25249        let (mut disp, mut cpu, mut bus) = setup();
25250        bus.write_long(TEST_SP, 0x200000);
25251
25252        let result = disp.dispatch_dialog(true, 0x182, &mut cpu, &mut bus);
25253        assert!(result.unwrap().is_ok());
25254        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
25255    }
25256
25257    #[test]
25258    fn close_dialog_front_dialog_restores_previous_port_state() {
25259        // IM:I I-413 says CloseDialog behaves like CloseWindow; when the
25260        // front dialog closes, the window behind it becomes frontmost.
25261        let (mut disp, mut cpu, mut bus) = setup();
25262        let dialog_ptr = 0x200000u32;
25263        let prev_window = 0x181000u32;
25264        seed_window_regions(&mut bus, prev_window, (0, 0, 342, 512));
25265
25266        disp.front_window = dialog_ptr;
25267        disp.current_port = dialog_ptr;
25268        disp.window_bounds = (100, 120, 220, 320);
25269        bus.write_long(crate::memory::globals::addr::THE_PORT, dialog_ptr);
25270        let a5 = cpu.read_reg(Register::A5);
25271        let global_ptr = bus.read_long(a5);
25272        bus.write_long(global_ptr, dialog_ptr);
25273        disp.window_stack
25274            .push((prev_window, (0, 0, 342, 512), 2, "Prev".to_string()));
25275
25276        bus.write_long(TEST_SP, dialog_ptr);
25277
25278        let result = disp.dispatch_dialog(true, 0x182, &mut cpu, &mut bus);
25279        assert!(result.unwrap().is_ok());
25280        assert_eq!(disp.front_window, prev_window);
25281        assert_eq!(disp.current_port, prev_window);
25282        assert_eq!(
25283            bus.read_long(crate::memory::globals::addr::THE_PORT),
25284            prev_window
25285        );
25286        assert_eq!(bus.read_long(global_ptr), prev_window);
25287        assert_eq!(
25288            TrapDispatcher::region_handle_rect(&bus, bus.read_long(prev_window + 122)),
25289            Some((100, 120, 220, 320)),
25290            "CloseDialog should invalidate the dialog-exposed area on the promoted window"
25291        );
25292        assert!(
25293            disp.event_queue.iter().any(|event| {
25294                event.what == 8 && event.message == prev_window && (event.modifiers & 1) != 0
25295            }),
25296            "CloseDialog should queue activateEvt for the promoted front window"
25297        );
25298        assert!(
25299            disp.event_queue
25300                .iter()
25301                .any(|event| event.what == 6 && event.message == prev_window),
25302            "CloseDialog must queue updateEvt for the newly exposed front window"
25303        );
25304    }
25305
25306    #[test]
25307    fn close_dialog_restores_saved_background_pixels() {
25308        let (mut disp, mut cpu, mut bus) = setup();
25309        let screen_base = 0x300000u32;
25310        let row_bytes: u32 = 640;
25311        disp.set_screen_mode_for_test(screen_base, row_bytes, 640, 480, 8);
25312
25313        let bounds = (100i16, 100i16, 150i16, 200i16);
25314        let dialog_ptr = 0x200000u32;
25315        disp.front_window = dialog_ptr;
25316        disp.window_bounds = bounds;
25317
25318        for y in 92u32..158 {
25319            for x in 92u32..208 {
25320                bus.write_byte(screen_base + y * row_bytes + x, 0xCC);
25321            }
25322        }
25323
25324        disp.dialog_saved_pixels
25325            .insert(dialog_ptr, vec![0x33; 66 * 116]);
25326
25327        bus.write_long(TEST_SP, dialog_ptr);
25328        let result = disp.dispatch_dialog(true, 0x182, &mut cpu, &mut bus);
25329        assert!(result.unwrap().is_ok());
25330
25331        for y in 92u32..158 {
25332            for x in 92u32..208 {
25333                assert_eq!(
25334                    bus.read_byte(screen_base + y * row_bytes + x),
25335                    0x33,
25336                    "CloseDialog should restore saved background at ({x},{y})"
25337                );
25338            }
25339        }
25340        assert!(
25341            !disp.dialog_saved_pixels.contains_key(&dialog_ptr),
25342            "saved pixels must be consumed after CloseDialog"
25343        );
25344    }
25345
25346    #[test]
25347    fn close_dialog_non_front_dialog_leaves_front_window_and_port_unchanged() {
25348        // CloseWindow front-promotion only applies when the closed window
25349        // was frontmost (IM:I I-283); CloseDialog inherits that behavior.
25350        let (mut disp, mut cpu, mut bus) = setup();
25351        let dialog_ptr = 0x200000u32;
25352        let other_front = 0x181000u32;
25353
25354        disp.front_window = other_front;
25355        disp.current_port = other_front;
25356        bus.write_long(crate::memory::globals::addr::THE_PORT, other_front);
25357        let a5 = cpu.read_reg(Register::A5);
25358        let global_ptr = bus.read_long(a5);
25359        bus.write_long(global_ptr, other_front);
25360        disp.window_stack
25361            .push((0x170000, (1, 2, 3, 4), 3, "Prev".to_string()));
25362
25363        bus.write_long(TEST_SP, dialog_ptr);
25364        let result = disp.dispatch_dialog(true, 0x182, &mut cpu, &mut bus);
25365        assert!(result.unwrap().is_ok());
25366
25367        assert_eq!(disp.front_window, other_front);
25368        assert_eq!(disp.current_port, other_front);
25369        assert_eq!(
25370            bus.read_long(crate::memory::globals::addr::THE_PORT),
25371            other_front
25372        );
25373        assert_eq!(bus.read_long(global_ptr), other_front);
25374        assert_eq!(
25375            disp.window_stack.len(),
25376            1,
25377            "non-front CloseDialog should not consume the saved front-window stack entry"
25378        );
25379    }
25380
25381    #[test]
25382    fn close_dialog_removes_window_but_preserves_caller_storage() {
25383        // MTE 1992 pp. 6-119..6-120: CloseDialog releases dialog-owned
25384        // items but does not dispose caller-supplied DialogRecord or item-list
25385        // storage.
25386        let (mut disp, mut cpu, mut bus) = setup();
25387        install_new_dialog_test_screen(&mut disp, &mut bus, 0x77);
25388        let storage_ptr = bus.alloc(170);
25389        let ditl = build_test_ditl_item(8, (10, 12, 24, 140), b"Static");
25390        let (items_handle, items_ptr) = install_ditl_handle_for_test(&mut bus, &ditl);
25391
25392        let (_sp, dialog_ptr) = call_new_dialog_for_test(
25393            &mut disp,
25394            &mut cpu,
25395            &mut bus,
25396            storage_ptr,
25397            (80, 90, 150, 260),
25398            false,
25399            4,
25400            0xFFFF_FFFF,
25401            items_handle,
25402        );
25403
25404        let text_handle = ditl_item_handle_field(&bus, items_ptr, 1);
25405        let text_ptr = bus.read_long(text_handle);
25406        let te_handle = bus.read_long(dialog_ptr + 160);
25407        let te_ptr = bus.read_long(te_handle);
25408        assert_ne!(text_handle, 0);
25409        assert_ne!(text_ptr, 0);
25410        assert_ne!(te_handle, 0);
25411        assert_ne!(te_ptr, 0);
25412
25413        cpu.write_reg(Register::A7, TEST_SP);
25414        bus.write_long(TEST_SP, dialog_ptr);
25415        disp.dispatch_dialog(true, 0x182, &mut cpu, &mut bus)
25416            .unwrap()
25417            .unwrap();
25418
25419        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
25420        assert!(!disp.window_list.contains(&dialog_ptr));
25421        assert_eq!(bus.get_alloc_size(storage_ptr), Some(170));
25422        assert_eq!(bus.get_alloc_size(items_handle), Some(4));
25423        assert_eq!(bus.get_alloc_size(items_ptr), Some(ditl.len() as u32));
25424        assert_eq!(bus.get_alloc_size(text_ptr), None);
25425        assert_eq!(bus.get_alloc_size(text_handle), None);
25426        assert_eq!(bus.get_alloc_size(te_ptr), None);
25427        assert_eq!(bus.get_alloc_size(te_handle), None);
25428        assert!(!disp.dialog_items.contains_key(&dialog_ptr));
25429        assert!(!disp.dialog_item_handles.contains_key(&text_handle));
25430    }
25431
25432    // ---- DisposDialog ($A983) ----
25433
25434    #[test]
25435    fn dispos_dialog_pops_4_bytes() {
25436        let (mut disp, mut cpu, mut bus) = setup();
25437        // SP+0: dialog ptr (4 bytes)
25438        bus.write_long(TEST_SP, 0x200000);
25439
25440        let result = disp.dispatch_dialog(true, 0x183, &mut cpu, &mut bus);
25441        assert!(result.unwrap().is_ok());
25442        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
25443    }
25444
25445    #[test]
25446    fn dispos_dialog_restores_previous_port_state() {
25447        let (mut disp, mut cpu, mut bus) = setup();
25448        let dialog_ptr = 0x200000u32;
25449        let prev_window = 0x181000u32;
25450        seed_window_regions(&mut bus, prev_window, (0, 0, 342, 512));
25451
25452        disp.front_window = dialog_ptr;
25453        disp.current_port = dialog_ptr;
25454        disp.window_bounds = (100, 120, 220, 320);
25455        bus.write_long(crate::memory::globals::addr::THE_PORT, dialog_ptr);
25456        let a5 = cpu.read_reg(Register::A5);
25457        let global_ptr = bus.read_long(a5);
25458        bus.write_long(global_ptr, dialog_ptr);
25459        disp.window_stack
25460            .push((prev_window, (0, 0, 342, 512), 2, "Prev".to_string()));
25461
25462        bus.write_long(TEST_SP, dialog_ptr);
25463
25464        let result = disp.dispatch_dialog(true, 0x183, &mut cpu, &mut bus);
25465        assert!(result.unwrap().is_ok());
25466        assert_eq!(disp.front_window, prev_window);
25467        assert_eq!(disp.current_port, prev_window);
25468        assert_eq!(
25469            bus.read_long(crate::memory::globals::addr::THE_PORT),
25470            prev_window
25471        );
25472        assert_eq!(bus.read_long(global_ptr), prev_window);
25473        assert_eq!(
25474            TrapDispatcher::region_handle_rect(&bus, bus.read_long(prev_window + 122)),
25475            Some((100, 120, 220, 320)),
25476            "DisposDialog should invalidate the dialog-exposed area on the promoted window"
25477        );
25478        assert!(
25479            disp.event_queue.iter().any(|event| {
25480                event.what == 8 && event.message == prev_window && (event.modifiers & 1) != 0
25481            }),
25482            "DisposDialog should queue activateEvt for the promoted front window"
25483        );
25484        assert!(
25485            disp.event_queue
25486                .iter()
25487                .any(|event| event.what == 6 && event.message == prev_window),
25488            "DisposDialog must queue updateEvt for the newly exposed front window"
25489        );
25490    }
25491
25492    #[test]
25493    fn dispos_dialog_removes_dialog_from_window_list() {
25494        // IM:I I-425: DisposDialog closes/disposes the dialog and
25495        // IM:I I-283 CloseWindow semantics remove it from window list.
25496        let (mut disp, mut cpu, mut bus) = setup();
25497        let dialog_ptr = 0x200000u32;
25498        let prev_window = 0x181000u32;
25499
25500        disp.window_list = vec![dialog_ptr, prev_window];
25501        disp.front_window = dialog_ptr;
25502        disp.current_port = dialog_ptr;
25503        bus.write_byte(dialog_ptr + 110, 0xFF);
25504        bus.write_byte(prev_window + 110, 0xFF);
25505        disp.window_stack
25506            .push((prev_window, (0, 0, 342, 512), 2, "Prev".to_string()));
25507
25508        bus.write_long(TEST_SP, dialog_ptr);
25509        let result = disp.dispatch_dialog(true, 0x183, &mut cpu, &mut bus);
25510        assert!(result.unwrap().is_ok());
25511        assert_eq!(disp.window_list, vec![prev_window]);
25512        assert_eq!(disp.front_window, prev_window);
25513    }
25514
25515    #[test]
25516    fn dispos_dialog_with_user_item_proc_argument_does_not_close_dialog() {
25517        // SetDItem stores a ProcPtr for userItem entries. A ProcPtr is not a
25518        // DialogPtr, so DisposDialog must not translate it into the current
25519        // front dialog. Doing so suppresses legitimate visible prompts whose
25520        // app code is installing or calling userItem procedures.
25521        // Inside Macintosh Volume I, I-421, I-425.
25522        let (mut disp, mut cpu, mut bus) = setup();
25523        let dialog_ptr = 0x200000u32;
25524        let prev_window = 0x181000u32;
25525        let user_item_proc = 0x00016178u32;
25526
25527        seed_window_regions(&mut bus, prev_window, (0, 0, 342, 512));
25528        disp.window_list = vec![dialog_ptr, prev_window];
25529        disp.front_window = dialog_ptr;
25530        disp.current_port = dialog_ptr;
25531        disp.window_bounds = (110, 155, 380, 645);
25532        disp.dialog_items.insert(
25533            dialog_ptr,
25534            vec![DialogItem {
25535                item_type: 0x80,
25536                proc_ptr: user_item_proc,
25537                ..DialogItem::default()
25538            }],
25539        );
25540        disp.dialog_saved_pixels
25541            .insert(dialog_ptr, vec![0x33; 66 * 116]);
25542        disp.window_stack
25543            .push((prev_window, (0, 0, 342, 512), 2, "Prev".to_string()));
25544
25545        bus.write_long(TEST_SP, user_item_proc);
25546        let result = disp.dispatch_dialog(true, 0x183, &mut cpu, &mut bus);
25547        assert!(result.unwrap().is_ok());
25548        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
25549        assert_eq!(disp.window_list, vec![dialog_ptr, prev_window]);
25550        assert_eq!(disp.front_window, dialog_ptr);
25551        assert!(disp.dialog_items.contains_key(&dialog_ptr));
25552        assert!(disp.dialog_saved_pixels.contains_key(&dialog_ptr));
25553    }
25554
25555    #[test]
25556    fn dispos_dialog_after_modal_button_hit_recovers_stale_proc_argument() {
25557        // When ModalDialog has just returned an enabled push button, a
25558        // following DisposeDialog is part of the standard modal teardown
25559        // pattern. If the app hands back a stale ProcPtr-shaped value in that
25560        // exact one-shot window, close the retained modal dialog rather than
25561        // leaving the button-accepted dialog visible forever.
25562        let (mut disp, mut cpu, mut bus) = setup();
25563        let dialog_ptr = 0x200000u32;
25564        let prev_window = 0x181000u32;
25565        let stale_proc_arg = 0x00016178u32;
25566
25567        seed_window_regions(&mut bus, prev_window, (0, 0, 342, 512));
25568        disp.window_list = vec![dialog_ptr, prev_window];
25569        disp.front_window = dialog_ptr;
25570        disp.current_port = dialog_ptr;
25571        disp.window_bounds = (110, 155, 380, 645);
25572        disp.dialog_modal_entered.insert(dialog_ptr);
25573        disp.pending_modal_button_dispose_dialog = Some(dialog_ptr);
25574        disp.dialog_items.insert(
25575            dialog_ptr,
25576            vec![DialogItem {
25577                item_type: 4,
25578                text: String::from("OK"),
25579                ..DialogItem::default()
25580            }],
25581        );
25582        disp.window_stack
25583            .push((prev_window, (0, 0, 342, 512), 2, "Prev".to_string()));
25584
25585        bus.write_long(TEST_SP, stale_proc_arg);
25586        let result = disp.dispatch_dialog(true, 0x183, &mut cpu, &mut bus);
25587        assert!(result.unwrap().is_ok());
25588        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
25589        assert_eq!(disp.window_list, vec![prev_window]);
25590        assert_eq!(disp.front_window, prev_window);
25591        assert!(!disp.dialog_items.contains_key(&dialog_ptr));
25592        assert!(disp.pending_modal_button_dispose_dialog.is_none());
25593    }
25594
25595    #[test]
25596    fn dispose_dialog_disposes_allocated_storage_and_items() {
25597        // MTE 1992 p. 6-120: DisposeDialog calls CloseDialog, then releases
25598        // the copied item list and DialogRecord allocated for a NIL dStorage.
25599        let (mut disp, mut cpu, mut bus) = setup();
25600        install_new_dialog_test_screen(&mut disp, &mut bus, 0x77);
25601        let ditl = build_test_ditl_item(8, (10, 12, 24, 140), b"Static");
25602        let (items_handle, items_ptr) = install_ditl_handle_for_test(&mut bus, &ditl);
25603
25604        let (_sp, dialog_ptr) = call_new_dialog_for_test(
25605            &mut disp,
25606            &mut cpu,
25607            &mut bus,
25608            0,
25609            (80, 90, 150, 260),
25610            false,
25611            4,
25612            0xFFFF_FFFF,
25613            items_handle,
25614        );
25615
25616        let text_handle = ditl_item_handle_field(&bus, items_ptr, 1);
25617        let text_ptr = bus.read_long(text_handle);
25618        let te_handle = bus.read_long(dialog_ptr + 160);
25619        let te_ptr = bus.read_long(te_handle);
25620
25621        cpu.write_reg(Register::A7, TEST_SP);
25622        bus.write_long(TEST_SP, dialog_ptr);
25623        disp.dispatch_dialog(true, 0x183, &mut cpu, &mut bus)
25624            .unwrap()
25625            .unwrap();
25626
25627        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
25628        assert_eq!(bus.get_alloc_size(text_ptr), None);
25629        assert_eq!(bus.get_alloc_size(text_handle), None);
25630        assert_eq!(bus.get_alloc_size(te_ptr), None);
25631        assert_eq!(bus.get_alloc_size(te_handle), None);
25632        assert_eq!(bus.get_alloc_size(items_ptr), None);
25633        assert_eq!(bus.get_alloc_size(items_handle), None);
25634        assert_eq!(bus.get_alloc_size(dialog_ptr), None);
25635        assert!(!disp.dialog_items.contains_key(&dialog_ptr));
25636    }
25637
25638    #[test]
25639    fn dispose_dialog_releases_text_handles() {
25640        // IM:I I-413 and I-425: CloseDialog releases standard dialog items;
25641        // DisposeDialog inherits that item cleanup before freeing the record.
25642        let (mut disp, mut cpu, mut bus) = setup();
25643        install_new_dialog_test_screen(&mut disp, &mut bus, 0x77);
25644        let ditl = build_test_ditl_item(16, (10, 12, 24, 140), b"Edit");
25645        let (items_handle, items_ptr) = install_ditl_handle_for_test(&mut bus, &ditl);
25646
25647        let (_sp, dialog_ptr) = call_new_dialog_for_test(
25648            &mut disp,
25649            &mut cpu,
25650            &mut bus,
25651            0,
25652            (80, 90, 150, 260),
25653            false,
25654            4,
25655            0xFFFF_FFFF,
25656            items_handle,
25657        );
25658        let item_text_handle = ditl_item_handle_field(&bus, items_ptr, 1);
25659        let item_text_ptr = bus.read_long(item_text_handle);
25660        let dialog_te_handle = bus.read_long(dialog_ptr + 160);
25661        let dialog_te_ptr = bus.read_long(dialog_te_handle);
25662
25663        cpu.write_reg(Register::A7, TEST_SP);
25664        bus.write_long(TEST_SP, dialog_ptr);
25665        disp.dispatch_dialog(true, 0x183, &mut cpu, &mut bus)
25666            .unwrap()
25667            .unwrap();
25668
25669        assert_eq!(bus.get_alloc_size(item_text_ptr), None);
25670        assert_eq!(bus.get_alloc_size(item_text_handle), None);
25671        assert_eq!(bus.get_alloc_size(dialog_te_ptr), None);
25672        assert_eq!(bus.get_alloc_size(dialog_te_handle), None);
25673        assert!(!disp.dialog_item_handles.contains_key(&item_text_handle));
25674    }
25675
25676    #[test]
25677    fn dispose_dialog_releases_control_mappings() {
25678        // IM:I I-413 and I-425: dialog-owned controls are released with the
25679        // dialog, including dispatcher-side control mappings.
25680        let (mut disp, mut cpu, mut bus) = setup();
25681        install_new_dialog_test_screen(&mut disp, &mut bus, 0x77);
25682        let ditl = build_test_ditl_item(4, (36, 40, 58, 120), b"OK");
25683        let (items_handle, _items_ptr) = install_ditl_handle_for_test(&mut bus, &ditl);
25684
25685        let (_sp, dialog_ptr) = call_new_dialog_for_test(
25686            &mut disp,
25687            &mut cpu,
25688            &mut bus,
25689            0,
25690            (80, 90, 150, 260),
25691            false,
25692            4,
25693            0xFFFF_FFFF,
25694            items_handle,
25695        );
25696        let box_ptr = bus.alloc(8);
25697        let item_ptr = bus.alloc(4);
25698        let type_ptr = bus.alloc(2);
25699        let snapshot = get_ditem_snapshot_for_test(
25700            &mut disp, &mut cpu, &mut bus, dialog_ptr, 1, box_ptr, item_ptr, type_ptr,
25701        );
25702        let control_handle = snapshot.item;
25703        let control_ptr = bus.read_long(control_handle);
25704        assert_ne!(control_handle, 0);
25705        assert_ne!(control_ptr, 0);
25706        assert_eq!(
25707            disp.dialog_control_handles.get(&control_handle),
25708            Some(&(dialog_ptr, 1))
25709        );
25710        assert!(disp.control_proc_ids.contains_key(&control_ptr));
25711        let aux_handle = disp.ensure_control_aux_record(&mut bus, control_handle);
25712        let aux_ptr = bus.read_long(aux_handle);
25713        assert_ne!(aux_handle, 0);
25714        assert_ne!(aux_ptr, 0);
25715
25716        cpu.write_reg(Register::A7, TEST_SP);
25717        bus.write_long(TEST_SP, dialog_ptr);
25718        disp.dispatch_dialog(true, 0x183, &mut cpu, &mut bus)
25719            .unwrap()
25720            .unwrap();
25721
25722        assert_eq!(bus.get_alloc_size(control_ptr), None);
25723        assert_eq!(bus.get_alloc_size(control_handle), None);
25724        assert_eq!(bus.get_alloc_size(aux_ptr), None);
25725        assert_eq!(bus.get_alloc_size(aux_handle), None);
25726        assert!(disp.control_aux_state(control_handle).is_none());
25727        assert!(!disp.dialog_control_handles.contains_key(&control_handle));
25728        assert!(!disp.control_proc_ids.contains_key(&control_ptr));
25729    }
25730
25731    #[test]
25732    fn dispose_dialog_clears_visible_snapshot() {
25733        let (mut disp, mut cpu, mut bus) = setup();
25734        let dialog_ptr = bus.alloc(170);
25735        disp.window_list = vec![dialog_ptr];
25736        disp.dialog_visible_snapshots.insert(
25737            dialog_ptr,
25738            PersistentDialogSnapshot {
25739                bounds: (10, 20, 30, 40),
25740                pixels: vec![0x55; 400],
25741            },
25742        );
25743
25744        bus.write_long(TEST_SP, dialog_ptr);
25745        disp.dispatch_dialog(true, 0x183, &mut cpu, &mut bus)
25746            .unwrap()
25747            .unwrap();
25748
25749        assert!(!disp.dialog_visible_snapshots.contains_key(&dialog_ptr));
25750    }
25751
25752    #[test]
25753    fn dispose_dialog_clears_retained_click_state() {
25754        let (mut disp, mut cpu, mut bus) = setup();
25755        let dialog_ptr = bus.alloc(170);
25756        disp.window_list = vec![dialog_ptr];
25757        disp.retained_modal_dialog_click = Some(RetainedModalDialogClickState {
25758            dialog_ptr,
25759            item_no: 1,
25760            rect: (10, 20, 30, 40),
25761            title: "OK".to_string(),
25762            is_default: true,
25763            highlighted: true,
25764            delivered_to_app: false,
25765        });
25766
25767        bus.write_long(TEST_SP, dialog_ptr);
25768        disp.dispatch_dialog(true, 0x183, &mut cpu, &mut bus)
25769            .unwrap()
25770            .unwrap();
25771
25772        assert!(disp.retained_modal_dialog_click.is_none());
25773    }
25774
25775    #[test]
25776    fn dispose_dialog_with_active_tracking_cancels_tracking() {
25777        let (mut disp, mut cpu, mut bus) = setup();
25778        let dialog_ptr = 0x200000u32;
25779        disp.dialog_tracking = Some(DialogTrackingState {
25780            dialog_ptr,
25781            ..Default::default()
25782        });
25783
25784        bus.write_long(TEST_SP, dialog_ptr);
25785        disp.dispatch_dialog(true, 0x183, &mut cpu, &mut bus)
25786            .unwrap()
25787            .unwrap();
25788
25789        assert!(disp.dialog_tracking.is_none());
25790    }
25791
25792    #[test]
25793    fn retained_modal_dialog_consumes_outside_mouse_down() {
25794        let (mut disp, mut cpu, mut bus) = setup();
25795        let dialog_ptr = bus.alloc(170);
25796        let prev_window = bus.alloc(170);
25797        seed_retained_modal_dialog(&mut disp, &mut bus, dialog_ptr, prev_window, 1);
25798
25799        disp.push_mouse_down(140, 260);
25800        let (what, _message, _where_v, _where_h, _modifiers, has_event) =
25801            disp.dequeue_toolbox_event(&mut cpu, &mut bus, 0xFFFF);
25802
25803        assert!(!has_event);
25804        assert_eq!(what, 0);
25805        assert!(disp.event_queue.is_empty());
25806        assert_eq!(disp.front_window, dialog_ptr);
25807        assert!(
25808            disp.retained_modal_dialog_click.is_some(),
25809            "outside click must be captured until mouseUp so it cannot leak behind the dialog"
25810        );
25811
25812        disp.push_mouse_up(140, 260);
25813        let (_what, _message, _where_v, _where_h, _modifiers, has_event) =
25814            disp.dequeue_toolbox_event(&mut cpu, &mut bus, 0xFFFF);
25815
25816        assert!(!has_event);
25817        assert_eq!(disp.front_window, dialog_ptr);
25818        assert!(disp.retained_modal_dialog_click.is_none());
25819        assert!(
25820            disp.dialog_items.contains_key(&dialog_ptr),
25821            "outside clicks do not dismiss modal dialogs"
25822        );
25823    }
25824
25825    #[test]
25826    fn app_owned_modal_dialog_mouse_down_is_delivered_to_event_loop() {
25827        // A DLOG can be created and shown with GetNewDialog while the
25828        // application owns the WaitNextEvent/DialogSelect loop. The Dialog
25829        // Manager reports enabled item clicks to that application code; it
25830        // does not dismiss the dialog from the Event Manager dequeue path.
25831        // Macintosh Toolbox Essentials 1992, pp. 6-138..6-141.
25832        let (mut disp, mut cpu, mut bus) = setup();
25833        let dialog_ptr = bus.alloc(170);
25834        let prev_window = bus.alloc(170);
25835        let screen_base = 0x300000u32;
25836        let row_bytes = 320u32;
25837        disp.set_screen_mode_for_test(screen_base, row_bytes, 320, 200, 8);
25838        let probe = screen_base + 70 * row_bytes + 150;
25839        bus.write_byte(probe, 17);
25840
25841        seed_app_owned_modal_dialog(&mut disp, &mut bus, dialog_ptr, prev_window, 1);
25842
25843        disp.push_mouse_down(70, 150);
25844        let (what, _message, where_v, where_h, _modifiers, has_event) =
25845            disp.dequeue_toolbox_event(&mut cpu, &mut bus, 0xFFFF);
25846
25847        assert!(has_event);
25848        assert_eq!(what, 1);
25849        assert_eq!((where_v, where_h), (70, 150));
25850        assert_eq!(
25851            disp.retained_modal_dialog_click
25852                .as_ref()
25853                .map(|click| (click.item_no, click.delivered_to_app)),
25854            Some((1, true))
25855        );
25856        assert_eq!(
25857            bus.read_byte(probe),
25858            238,
25859            "app-owned modal button should still show the pressed state"
25860        );
25861        assert_eq!(disp.front_window, dialog_ptr);
25862        assert!(
25863            disp.dialog_items.contains_key(&dialog_ptr),
25864            "application-owned dialog should not be closed by event dequeue"
25865        );
25866    }
25867
25868    #[test]
25869    fn app_owned_modal_dialog_button_mouse_up_dismisses_after_delivery() {
25870        // Some apps drive modal DLOGs with their own WaitNextEvent loop and
25871        // only ask the Window Manager which window was clicked. Keep the
25872        // mouseDown deliverable, but finish the standard modal button press
25873        // on mouseUp if app code has not called DialogSelect.
25874        // Macintosh Toolbox Essentials 1992, pp. 6-136, 6-138..6-141.
25875        let (mut disp, mut cpu, mut bus) = setup();
25876        let dialog_ptr = bus.alloc(170);
25877        let prev_window = bus.alloc(170);
25878        let screen_base = 0x300000u32;
25879        let row_bytes = 320u32;
25880        disp.set_screen_mode_for_test(screen_base, row_bytes, 320, 200, 8);
25881        let probe = screen_base + 70 * row_bytes + 150;
25882        bus.write_byte(probe, 17);
25883
25884        seed_app_owned_modal_dialog(&mut disp, &mut bus, dialog_ptr, prev_window, 1);
25885
25886        disp.push_mouse_down(70, 150);
25887        let (what, _message, where_v, where_h, _modifiers, has_event) =
25888            disp.dequeue_toolbox_event(&mut cpu, &mut bus, 0xFFFF);
25889        assert!(has_event);
25890        assert_eq!(what, 1);
25891        assert_eq!((where_v, where_h), (70, 150));
25892        assert_eq!(disp.front_window, dialog_ptr);
25893
25894        disp.push_mouse_up(70, 150);
25895        let (what, _message, where_v, where_h, _modifiers, has_event) =
25896            disp.dequeue_toolbox_event(&mut cpu, &mut bus, 0xFFFF);
25897
25898        assert!(
25899            has_event,
25900            "app-owned modal mouseUp remains deliverable after compatibility close"
25901        );
25902        assert_eq!(what, 2);
25903        assert_eq!((where_v, where_h), (70, 150));
25904        assert_eq!(bus.read_byte(probe), 17);
25905        assert_eq!(disp.front_window, prev_window);
25906        assert!(!disp.dialog_items.contains_key(&dialog_ptr));
25907        assert!(disp.retained_modal_dialog_click.is_none());
25908        assert!(
25909            disp.event_queue
25910                .iter()
25911                .any(|event| event.what == 6 && event.message == prev_window),
25912            "closing the dialog must invalidate/update the exposed window"
25913        );
25914    }
25915
25916    #[test]
25917    fn retained_modal_dialog_button_click_highlights_then_dismisses() {
25918        let (mut disp, mut cpu, mut bus) = setup();
25919        let dialog_ptr = bus.alloc(170);
25920        let prev_window = bus.alloc(170);
25921        let screen_base = 0x300000u32;
25922        let row_bytes = 320u32;
25923        disp.set_screen_mode_for_test(screen_base, row_bytes, 320, 200, 8);
25924        seed_retained_modal_dialog(&mut disp, &mut bus, dialog_ptr, prev_window, 1);
25925
25926        let probe = screen_base + 70 * row_bytes + 150;
25927        bus.write_byte(probe, 17);
25928        let bounds = TrapDispatcher::dialog_screen_bounds(&bus, dialog_ptr);
25929        assert_eq!(bounds, (0, 0, 100, 220));
25930        let base_pixels = disp.save_dialog_pixels(&bus, bounds);
25931        disp.dialog_visible_snapshots.insert(
25932            dialog_ptr,
25933            PersistentDialogSnapshot {
25934                bounds,
25935                pixels: base_pixels,
25936            },
25937        );
25938        let items = disp.dialog_items.get(&dialog_ptr).cloned().unwrap();
25939        assert_eq!(
25940            disp.dialog_item_hit_test(
25941                &bus,
25942                &items,
25943                bounds,
25944                70,
25945                150,
25946                &disp.dialog_popup_original_rects,
25947                dialog_ptr,
25948            ),
25949            1
25950        );
25951
25952        disp.push_mouse_down(70, 150);
25953        let (_what, _message, _where_v, _where_h, _modifiers, has_event) =
25954            disp.dequeue_toolbox_event(&mut cpu, &mut bus, 0xFFFF);
25955
25956        assert!(!has_event);
25957        assert_eq!(
25958            disp.retained_modal_dialog_click
25959                .as_ref()
25960                .map(|click| click.item_no),
25961            Some(1)
25962        );
25963        assert_eq!(
25964            bus.read_byte(probe),
25965            238,
25966            "button interior should invert on mouseDown"
25967        );
25968        disp.restore_visible_dialog_snapshots(&mut bus);
25969        disp.redraw_retained_modal_dialog_click(&mut bus);
25970        assert_eq!(
25971            bus.read_byte(probe),
25972            238,
25973            "pressed retained-modal button must survive visible-dialog snapshot redraw"
25974        );
25975        assert_eq!(disp.front_window, dialog_ptr);
25976
25977        disp.push_mouse_up(70, 150);
25978        let (_what, _message, _where_v, _where_h, _modifiers, has_event) =
25979            disp.dequeue_toolbox_event(&mut cpu, &mut bus, 0xFFFF);
25980
25981        assert!(!has_event);
25982        assert_eq!(
25983            bus.read_byte(probe),
25984            17,
25985            "button should be unhighlighted before close"
25986        );
25987        assert_eq!(disp.front_window, prev_window);
25988        assert!(!disp.dialog_items.contains_key(&dialog_ptr));
25989        assert!(disp.retained_modal_dialog_click.is_none());
25990        assert!(
25991            !disp
25992                .event_queue
25993                .iter()
25994                .any(|event| matches!(event.what, 1 | 2)),
25995            "consumed dialog click must not leave mouse events for the game loop"
25996        );
25997        assert!(
25998            disp.event_queue
25999                .iter()
26000                .any(|event| event.what == 6 && event.message == prev_window),
26001            "closing the dialog must invalidate/update the exposed window"
26002        );
26003    }
26004
26005    #[test]
26006    fn modal_dialog_button_tracking_systemless_theme_routes_pressed_state_through_provider() {
26007        let (mut disp, _cpu, mut bus) = setup();
26008        let screen_base = 0x300000u32;
26009        let row_bytes = 64u32;
26010        let bounds = (0, 0, 100, 220);
26011        let item_rect = (60, 120, 80, 180);
26012        let screen_rect = TrapDispatcher::dialog_item_screen_rect(bounds, item_rect);
26013
26014        disp.set_ui_theme_id(UiThemeId::SystemlessDefault);
26015        disp.set_screen_mode_for_test(screen_base, row_bytes, 512, 342, 1);
26016        disp.draw_button(
26017            &mut bus,
26018            screen_rect.0,
26019            screen_rect.1,
26020            screen_rect.2,
26021            screen_rect.3,
26022            "",
26023            true,
26024        );
26025
26026        let probe_x = 150;
26027        let probe_y = 70;
26028        assert!(
26029            !screen_pixel_is_set(&bus, screen_base, row_bytes, probe_x, probe_y),
26030            "unpressed provider button interior should start clear"
26031        );
26032
26033        let dialog_ptr = bus.alloc(170);
26034        disp.dialog_tracking = Some(DialogTrackingState {
26035            dialog_ptr,
26036            bounds,
26037            items: vec![DialogItem {
26038                item_type: 4,
26039                rect: item_rect,
26040                text: String::new(),
26041                resource_id: 0,
26042                proc_ptr: 0,
26043                sel_start: 0,
26044                sel_end: 0,
26045            }],
26046            default_item: 1,
26047            active_button: Some(DialogButtonTrackingState {
26048                item_no: 1,
26049                rect: item_rect,
26050                title: String::new(),
26051                is_default: true,
26052                highlighted: false,
26053            }),
26054            ..Default::default()
26055        });
26056
26057        disp.mouse_button = true;
26058        disp.mouse_pos = (probe_y, probe_x);
26059        disp.handle_dialog_button_tracking(&mut bus);
26060
26061        assert!(
26062            screen_pixel_is_set(&bus, screen_base, row_bytes, probe_x, probe_y),
26063            "inside tracking should paint the provider pressed fill"
26064        );
26065        assert_eq!(
26066            disp.dialog_tracking
26067                .as_ref()
26068                .and_then(|tracking| tracking.active_button.as_ref())
26069                .map(|button| button.highlighted),
26070            Some(true)
26071        );
26072
26073        disp.mouse_pos = (40, 50);
26074        disp.handle_dialog_button_tracking(&mut bus);
26075
26076        assert!(
26077            !screen_pixel_is_set(&bus, screen_base, row_bytes, probe_x, probe_y),
26078            "outside tracking should redraw unpressed provider chrome"
26079        );
26080        assert_eq!(
26081            disp.dialog_tracking
26082                .as_ref()
26083                .and_then(|tracking| tracking.active_button.as_ref())
26084                .map(|button| button.highlighted),
26085            Some(false)
26086        );
26087    }
26088
26089    #[test]
26090    fn retained_modal_dialog_capture_does_not_apply_to_modeless_dialogs() {
26091        let (mut disp, mut cpu, mut bus) = setup();
26092        let dialog_ptr = bus.alloc(170);
26093        let prev_window = bus.alloc(170);
26094        seed_retained_modal_dialog(&mut disp, &mut bus, dialog_ptr, prev_window, 4);
26095
26096        disp.push_mouse_down(70, 150);
26097        let (what, _message, where_v, where_h, _modifiers, has_event) =
26098            disp.dequeue_toolbox_event(&mut cpu, &mut bus, 0xFFFF);
26099
26100        assert!(has_event);
26101        assert_eq!(what, 1);
26102        assert_eq!((where_v, where_h), (70, 150));
26103        assert!(disp.retained_modal_dialog_click.is_none());
26104        assert_eq!(disp.front_window, dialog_ptr);
26105    }
26106
26107    #[test]
26108    fn dispos_dialog_clears_tracking_for_disposed_dialog() {
26109        let (mut disp, mut cpu, mut bus) = setup();
26110        let dialog_ptr = 0x200000u32;
26111        disp.dialog_tracking = Some(crate::trap::dispatch::DialogTrackingState {
26112            dialog_ptr,
26113            bounds: (0, 0, 32, 32),
26114            title: String::new(),
26115            proc_id: 1,
26116            items: Vec::new(),
26117            default_item: 0,
26118            cancel_item: 0,
26119            edit_text: String::new(),
26120            edit_item: 0,
26121            saved_pixels: Vec::new(),
26122            stack_ptr: 0,
26123            item_hit_ptr: 0,
26124            rendered_pixels: Vec::new(),
26125            flash_remaining: 0,
26126            flash_delay: 0,
26127            flash_item: 0,
26128            edit_text_modified: false,
26129            draw_proc_queue: VecDeque::new(),
26130            draw_procs_done: true,
26131            rendered_pixels_final: true,
26132            filter_proc: 0,
26133            game_managed: false,
26134            last_filter_event: None,
26135            popup_draws: Vec::new(),
26136            active_popup: None,
26137            active_button: None,
26138            active_user_item: None,
26139        });
26140
26141        bus.write_long(TEST_SP, dialog_ptr);
26142        let result = disp.dispatch_dialog(true, 0x183, &mut cpu, &mut bus);
26143        assert!(result.unwrap().is_ok());
26144        assert!(disp.dialog_tracking.is_none());
26145    }
26146
26147    #[test]
26148    fn dispos_dialog_clears_dialog_scoped_side_maps_for_disposed_dialog() {
26149        let (mut disp, mut cpu, mut bus) = setup();
26150        let dialog_ptr = 0x200000u32;
26151        let other_dialog_ptr = 0x210000u32;
26152        let text_handle = 0x300000u32;
26153        let other_text_handle = 0x300010u32;
26154        let ctrl_handle = 0x300020u32;
26155        let other_ctrl_handle = 0x300030u32;
26156
26157        disp.dialog_items
26158            .insert(dialog_ptr, vec![DialogItem::default()]);
26159        disp.dialog_items
26160            .insert(other_dialog_ptr, vec![DialogItem::default()]);
26161        disp.dialog_item_handles
26162            .insert(text_handle, (dialog_ptr, 0));
26163        disp.dialog_item_handles
26164            .insert(other_text_handle, (other_dialog_ptr, 0));
26165        disp.dialog_control_handles
26166            .insert(ctrl_handle, (dialog_ptr, 1));
26167        disp.dialog_control_handles
26168            .insert(other_ctrl_handle, (other_dialog_ptr, 1));
26169        disp.dialog_control_values.insert((dialog_ptr, 1), 1);
26170        disp.dialog_control_values.insert((other_dialog_ptr, 1), 1);
26171        disp.hidden_dialog_item_rects
26172            .insert((dialog_ptr, 1), (10, 20, 30, 40));
26173        disp.hidden_dialog_item_rects
26174            .insert((other_dialog_ptr, 1), (50, 60, 70, 80));
26175        disp.dialog_item_popup_menus.insert((dialog_ptr, 1), 900);
26176        disp.dialog_item_popup_menus
26177            .insert((other_dialog_ptr, 1), 901);
26178        disp.dialog_popup_original_rects
26179            .insert((dialog_ptr, 1), (10, 20, 30, 130));
26180        disp.dialog_popup_original_rects
26181            .insert((other_dialog_ptr, 1), (50, 60, 70, 180));
26182        disp.dialog_popup_candidate_items.insert((dialog_ptr, 1));
26183        disp.dialog_popup_candidate_items
26184            .insert((other_dialog_ptr, 1));
26185        disp.dialog_cancel_items.insert(dialog_ptr, 2);
26186        disp.dialog_cancel_items.insert(other_dialog_ptr, 3);
26187        disp.pending_dialog_popup_menu = Some(PendingDialogPopupMenu {
26188            dialog_ptr,
26189            item_no: 1,
26190            menu_id: 900,
26191            rect: (10, 20, 30, 130),
26192        });
26193
26194        bus.write_long(TEST_SP, dialog_ptr);
26195        disp.dispatch_dialog(true, 0x183, &mut cpu, &mut bus)
26196            .unwrap()
26197            .unwrap();
26198
26199        assert!(!disp.dialog_items.contains_key(&dialog_ptr));
26200        assert!(disp.dialog_items.contains_key(&other_dialog_ptr));
26201        assert!(!disp.dialog_item_handles.contains_key(&text_handle));
26202        assert!(disp.dialog_item_handles.contains_key(&other_text_handle));
26203        assert!(!disp.dialog_control_handles.contains_key(&ctrl_handle));
26204        assert!(disp.dialog_control_handles.contains_key(&other_ctrl_handle));
26205        assert!(!disp.dialog_control_values.contains_key(&(dialog_ptr, 1)));
26206        assert!(disp
26207            .dialog_control_values
26208            .contains_key(&(other_dialog_ptr, 1)));
26209        assert!(!disp.hidden_dialog_item_rects.contains_key(&(dialog_ptr, 1)));
26210        assert!(disp
26211            .hidden_dialog_item_rects
26212            .contains_key(&(other_dialog_ptr, 1)));
26213        assert!(!disp.dialog_item_popup_menus.contains_key(&(dialog_ptr, 1)));
26214        assert!(disp
26215            .dialog_item_popup_menus
26216            .contains_key(&(other_dialog_ptr, 1)));
26217        assert!(!disp
26218            .dialog_popup_original_rects
26219            .contains_key(&(dialog_ptr, 1)));
26220        assert!(disp
26221            .dialog_popup_original_rects
26222            .contains_key(&(other_dialog_ptr, 1)));
26223        assert!(!disp.dialog_popup_candidate_items.contains(&(dialog_ptr, 1)));
26224        assert!(disp
26225            .dialog_popup_candidate_items
26226            .contains(&(other_dialog_ptr, 1)));
26227        assert!(!disp.dialog_cancel_items.contains_key(&dialog_ptr));
26228        assert!(disp.dialog_cancel_items.contains_key(&other_dialog_ptr));
26229        assert!(disp.pending_dialog_popup_menu.is_none());
26230    }
26231
26232    // Regression: games that run their own event loop (e.g. Escape
26233    // Velocity's "enter pilot/ship name" text dialogs) call
26234    // GetNewDialog → custom event loop → DisposDialog without ever
26235    // invoking ModalDialog. Before the fix, DisposDialog discarded
26236    // the saved-background pixels without blitting them back to the
26237    // screen, leaving a dialog-shaped hole over the window behind.
26238    // IM:I I-425 says DisposDialog internally calls CloseWindow,
26239    // whose PaintBehind/CalcVisBehind is supposed to restore the
26240    // underlying content. These three tests pin that contract.
26241    //
26242    // Save/restore geometry note: save_dialog_pixels adds the dBoxProc
26243    // structure margin around the bounds. The tests use bounds
26244    // (100,100,150,200) → save area (92,92)..(158,208) =
26245    // 66 rows × 116 cols = 7656 bytes.
26246
26247    #[test]
26248    fn disposdialog_restores_saved_background_pixels() {
26249        let (mut disp, mut cpu, mut bus) = setup();
26250
26251        // 8bpp test screen. Must be inside the 4MB test bus.
26252        let screen_base = 0x300000u32;
26253        let row_bytes: u32 = 640;
26254        disp.set_screen_mode_for_test(screen_base, row_bytes, 640, 480, 8);
26255
26256        let bounds = (100i16, 100i16, 150i16, 200i16);
26257        let dialog_ptr = 0x200000u32;
26258        disp.front_window = dialog_ptr;
26259        disp.window_bounds = bounds;
26260
26261        // Paint the save area with 0xCC — the "dialog pixels" that
26262        // should be overwritten on dispose.
26263        for y in 92u32..158 {
26264            for x in 92u32..208 {
26265                bus.write_byte(screen_base + y * row_bytes + x, 0xCC);
26266            }
26267        }
26268
26269        // Install a saved background snapshot filled with 0x33 (the
26270        // "what was behind the dialog" pattern). 66*116=7656 bytes.
26271        disp.dialog_saved_pixels
26272            .insert(dialog_ptr, vec![0x33; 66 * 116]);
26273
26274        bus.write_long(TEST_SP, dialog_ptr);
26275        let result = disp.dispatch_dialog(true, 0x183, &mut cpu, &mut bus);
26276        assert!(result.unwrap().is_ok());
26277
26278        // Every byte in the save area should now be 0x33, not 0xCC.
26279        for y in 92u32..158 {
26280            for x in 92u32..208 {
26281                let addr = screen_base + y * row_bytes + x;
26282                let got = bus.read_byte(addr);
26283                assert_eq!(
26284                    got, 0x33,
26285                    "byte at ({},{}) must be restored background 0x33, got 0x{:02X}",
26286                    x, y, got
26287                );
26288            }
26289        }
26290        assert!(
26291            !disp.dialog_saved_pixels.contains_key(&dialog_ptr),
26292            "saved pixels must be consumed after DisposDialog"
26293        );
26294    }
26295
26296    #[test]
26297    fn visible_dialog_saved_background_tracks_screen_draws_behind_it() {
26298        // Some applications keep animating their screen-backed main window
26299        // while a visible DLOG is frontmost. The Window Manager still closes
26300        // the dialog via CloseWindow/DisposDialog (IM:I I-283, I-425), so the
26301        // saved-under pixels must reflect those later behind-dialog draws.
26302        let (mut disp, mut cpu, mut bus) = setup();
26303        let screen_base = 0x300000u32;
26304        let row_bytes: u32 = 640;
26305        disp.set_screen_mode_for_test(screen_base, row_bytes, 640, 480, 8);
26306
26307        let bounds = (100i16, 100i16, 150i16, 200i16);
26308        let dialog_ptr = bus.alloc(170);
26309        let background_port = bus.alloc(170);
26310        let save_top = 92u32;
26311        let save_left = 92u32;
26312        let save_bottom = 158u32;
26313        let save_right = 208u32;
26314        let row_width = (save_right - save_left) as usize;
26315
26316        for y in save_top..save_bottom {
26317            for x in save_left..save_right {
26318                bus.write_byte(screen_base + y * row_bytes + x, 0xEE);
26319            }
26320        }
26321
26322        disp.dialog_saved_pixels.insert(
26323            dialog_ptr,
26324            vec![0x11; row_width * (save_bottom - save_top) as usize],
26325        );
26326        disp.dialog_visible_snapshots.insert(
26327            dialog_ptr,
26328            PersistentDialogSnapshot {
26329                bounds,
26330                pixels: vec![0xEE; row_width * (save_bottom - save_top) as usize],
26331            },
26332        );
26333
26334        for y in 120u32..123 {
26335            for x in 130u32..139 {
26336                bus.write_byte(screen_base + y * row_bytes + x, 0x44);
26337            }
26338        }
26339        disp.refresh_dialog_saved_pixels_after_screen_draw(
26340            &bus,
26341            background_port,
26342            (120, 130, 123, 139),
26343        );
26344
26345        let saved = disp.dialog_saved_pixels.get(&dialog_ptr).unwrap();
26346        let touched_idx = (120 - save_top) as usize * row_width + (130 - save_left) as usize;
26347        let untouched_idx = (100 - save_top) as usize * row_width + (100 - save_left) as usize;
26348        assert_eq!(saved[touched_idx], 0x44);
26349        assert_eq!(saved[untouched_idx], 0x11);
26350
26351        for y in save_top..save_bottom {
26352            for x in save_left..save_right {
26353                bus.write_byte(screen_base + y * row_bytes + x, 0xEE);
26354            }
26355        }
26356
26357        disp.front_window = dialog_ptr;
26358        disp.current_port = dialog_ptr;
26359        disp.window_bounds = bounds;
26360        disp.window_list = vec![dialog_ptr];
26361        disp.window_stack.push((0, (0, 0, 0, 0), -1, String::new()));
26362
26363        bus.write_long(TEST_SP, dialog_ptr);
26364        disp.dispatch_dialog(true, 0x183, &mut cpu, &mut bus)
26365            .unwrap()
26366            .unwrap();
26367
26368        assert_eq!(bus.read_byte(screen_base + 120 * row_bytes + 130), 0x44);
26369        assert_eq!(bus.read_byte(screen_base + 100 * row_bytes + 100), 0x11);
26370    }
26371
26372    #[test]
26373    fn active_modal_dialog_saved_background_tracks_screen_draws_behind_it() {
26374        // ModalDialog moves the visible dialog snapshot into dialog_tracking
26375        // while the modal loop is active. Background screen draws behind that
26376        // front modal still need to update the saved-under pixels that
26377        // DisposDialog/CloseWindow will restore.
26378        let (mut disp, _cpu, mut bus) = setup();
26379        let screen_base = 0x300000u32;
26380        let row_bytes: u32 = 640;
26381        disp.set_screen_mode_for_test(screen_base, row_bytes, 640, 480, 8);
26382
26383        let bounds = (100i16, 100i16, 150i16, 200i16);
26384        let dialog_ptr = bus.alloc(170);
26385        let background_port = bus.alloc(170);
26386        let save_top = 92u32;
26387        let save_left = 92u32;
26388        let save_bottom = 158u32;
26389        let save_right = 208u32;
26390        let row_width = (save_right - save_left) as usize;
26391
26392        for y in save_top..save_bottom {
26393            for x in save_left..save_right {
26394                bus.write_byte(screen_base + y * row_bytes + x, 0xEE);
26395            }
26396        }
26397
26398        disp.dialog_saved_pixels.insert(
26399            dialog_ptr,
26400            vec![0x11; row_width * (save_bottom - save_top) as usize],
26401        );
26402        let mut tracking = dialog_tracking_state_for_test(dialog_ptr);
26403        tracking.bounds = bounds;
26404        disp.dialog_tracking = Some(tracking);
26405
26406        for y in 120u32..123 {
26407            for x in 130u32..139 {
26408                bus.write_byte(screen_base + y * row_bytes + x, 0x44);
26409            }
26410        }
26411        disp.refresh_dialog_saved_pixels_after_screen_draw(
26412            &bus,
26413            background_port,
26414            (120, 130, 123, 139),
26415        );
26416
26417        let saved = disp.dialog_saved_pixels.get(&dialog_ptr).unwrap();
26418        let touched_idx = (120 - save_top) as usize * row_width + (130 - save_left) as usize;
26419        let untouched_idx = (100 - save_top) as usize * row_width + (100 - save_left) as usize;
26420        assert_eq!(saved[touched_idx], 0x44);
26421        assert_eq!(saved[untouched_idx], 0x11);
26422    }
26423
26424    #[test]
26425    fn retained_modal_dialog_saved_background_tracks_same_port_screen_draws() {
26426        // After ModalDialog returns with a retained visible modal, the app may
26427        // immediately redraw the background before calling DisposDialog while
26428        // the dialog remains the current screen-backed port. Treat those
26429        // same-port screen writes as background, not as dialog content.
26430        let (mut disp, _cpu, mut bus) = setup();
26431        let screen_base = 0x300000u32;
26432        let row_bytes: u32 = 640;
26433        disp.set_screen_mode_for_test(screen_base, row_bytes, 640, 480, 8);
26434
26435        let bounds = (100i16, 100i16, 150i16, 200i16);
26436        let dialog_ptr = bus.alloc(170);
26437        let save_top = 92u32;
26438        let save_left = 92u32;
26439        let save_bottom = 158u32;
26440        let save_right = 208u32;
26441        let row_width = (save_right - save_left) as usize;
26442
26443        disp.dialog_saved_pixels.insert(
26444            dialog_ptr,
26445            vec![0x11; row_width * (save_bottom - save_top) as usize],
26446        );
26447        disp.dialog_visible_snapshots.insert(
26448            dialog_ptr,
26449            PersistentDialogSnapshot {
26450                bounds,
26451                pixels: vec![0xEE; row_width * (save_bottom - save_top) as usize],
26452            },
26453        );
26454
26455        let touched_idx = (120 - save_top) as usize * row_width + (130 - save_left) as usize;
26456        bus.write_byte(screen_base + 120 * row_bytes + 130, 0x22);
26457        disp.refresh_dialog_saved_pixels_after_screen_draw(&bus, dialog_ptr, (120, 130, 121, 131));
26458        assert_eq!(
26459            disp.dialog_saved_pixels.get(&dialog_ptr).unwrap()[touched_idx],
26460            0x11,
26461            "pre-modal same-port dialog drawing must not become saved-under background"
26462        );
26463
26464        disp.dialog_modal_entered.insert(dialog_ptr);
26465        bus.write_byte(screen_base + 120 * row_bytes + 130, 0x44);
26466        disp.refresh_dialog_saved_pixels_after_screen_draw(&bus, dialog_ptr, (120, 130, 121, 131));
26467        assert_eq!(
26468            disp.dialog_saved_pixels.get(&dialog_ptr).unwrap()[touched_idx],
26469            0x44,
26470            "retained modal same-port background drawing should update saved-under pixels"
26471        );
26472    }
26473
26474    #[test]
26475    fn stale_fullscreen_dialog_exposure_restores_from_offscreen_scene_port() {
26476        let (mut disp, _cpu, mut bus) = setup();
26477        let screen_base = bus.alloc(100 * 80);
26478        bus.write_long(0x0824, screen_base);
26479        disp.screen_mode = (screen_base, 100, 100, 80, 8);
26480        disp.device_clut[0x22] = [0x3333, 0x7777, 0x2222];
26481        disp.color_manager_clut = disp.device_clut;
26482
26483        let offscreen_base = bus.alloc(100 * 80);
26484        bus.write_bytes(offscreen_base, &vec![0x22; 100 * 80]);
26485        let port = bus.alloc(170);
26486        let pixmap_handle = bus.alloc(4);
26487        let pixmap_ptr = bus.alloc(50);
26488        bus.write_word(port + 6, 0xC000);
26489        bus.write_long(port + 2, pixmap_handle);
26490        bus.write_long(pixmap_handle, pixmap_ptr);
26491        bus.write_long(pixmap_ptr, offscreen_base);
26492        bus.write_word(pixmap_ptr + 4, 0x8000 | 100);
26493        bus.write_word(pixmap_ptr + 6, 0);
26494        bus.write_word(pixmap_ptr + 8, 0);
26495        bus.write_word(pixmap_ptr + 10, 80);
26496        bus.write_word(pixmap_ptr + 12, 100);
26497        bus.write_word(pixmap_ptr + 32, 8);
26498        disp.cport_ports.insert(port);
26499
26500        bus.write_bytes(screen_base, &vec![0x00; 100 * 80]);
26501        assert!(
26502            disp.restore_dialog_exposure_from_fullscreen_offscreen_port(&mut bus, (20, 20, 50, 70))
26503        );
26504        assert_eq!(bus.read_byte(screen_base + 20 * 100 + 20), 0x22);
26505        assert_eq!(bus.read_byte(screen_base + 49 * 100 + 69), 0x22);
26506        assert_eq!(bus.read_byte(screen_base + 2 * 100 + 2), 0x00);
26507    }
26508
26509    #[test]
26510    fn disposdialog_restore_is_bounded_by_save_margin() {
26511        // Pin that the restore writes exactly the dBoxProc structure-margin
26512        // rectangle and does not stomp adjacent bytes. This catches
26513        // off-by-one errors in the save/restore geometry.
26514        let (mut disp, mut cpu, mut bus) = setup();
26515        let screen_base = 0x300000u32;
26516        let row_bytes: u32 = 640;
26517        disp.set_screen_mode_for_test(screen_base, row_bytes, 640, 480, 8);
26518
26519        let bounds = (100i16, 100i16, 150i16, 200i16);
26520        let dialog_ptr = 0x200000u32;
26521        disp.front_window = dialog_ptr;
26522        disp.window_bounds = bounds;
26523
26524        // Paint the entire screen area of interest (including a
26525        // generous border around the save rect) with 0xAA.
26526        for y in 85u32..165 {
26527            for x in 85u32..215 {
26528                bus.write_byte(screen_base + y * row_bytes + x, 0xAA);
26529            }
26530        }
26531
26532        disp.dialog_saved_pixels
26533            .insert(dialog_ptr, vec![0x33; 66 * 116]);
26534
26535        bus.write_long(TEST_SP, dialog_ptr);
26536        disp.dispatch_dialog(true, 0x183, &mut cpu, &mut bus)
26537            .unwrap()
26538            .unwrap();
26539
26540        // Bytes OUTSIDE the save area must still be 0xAA.
26541        // Above the save rect:
26542        for x in 85u32..215 {
26543            assert_eq!(bus.read_byte(screen_base + 91 * row_bytes + x), 0xAA);
26544        }
26545        // Below the save rect:
26546        for x in 85u32..215 {
26547            assert_eq!(bus.read_byte(screen_base + 158 * row_bytes + x), 0xAA);
26548        }
26549        // Left of the save rect:
26550        for y in 85u32..165 {
26551            assert_eq!(bus.read_byte(screen_base + y * row_bytes + 91), 0xAA);
26552        }
26553        // Right of the save rect:
26554        for y in 85u32..165 {
26555            assert_eq!(bus.read_byte(screen_base + y * row_bytes + 208), 0xAA);
26556        }
26557        // Bytes INSIDE the save area must now be 0x33.
26558        for y in 92u32..158 {
26559            for x in 92u32..208 {
26560                assert_eq!(bus.read_byte(screen_base + y * row_bytes + x), 0x33);
26561            }
26562        }
26563    }
26564
26565    #[test]
26566    fn disposdialog_without_saved_pixels_leaves_screen_untouched() {
26567        // If no saved pixels exist for this dialog (e.g. ModalDialog
26568        // already consumed them on flash completion), DisposDialog
26569        // must be a no-op on the screen — no panic, no accidental
26570        // fill.
26571        let (mut disp, mut cpu, mut bus) = setup();
26572        let screen_base = 0x300000u32;
26573        let row_bytes: u32 = 640;
26574        disp.set_screen_mode_for_test(screen_base, row_bytes, 640, 480, 8);
26575
26576        let bounds = (100i16, 100i16, 150i16, 200i16);
26577        let dialog_ptr = 0x200000u32;
26578        disp.front_window = dialog_ptr;
26579        disp.window_bounds = bounds;
26580
26581        for y in 95u32..155 {
26582            for x in 95u32..205 {
26583                bus.write_byte(screen_base + y * row_bytes + x, 0x77);
26584            }
26585        }
26586        assert!(!disp.dialog_saved_pixels.contains_key(&dialog_ptr));
26587
26588        bus.write_long(TEST_SP, dialog_ptr);
26589        disp.dispatch_dialog(true, 0x183, &mut cpu, &mut bus)
26590            .unwrap()
26591            .unwrap();
26592
26593        for y in 95u32..155 {
26594            for x in 95u32..205 {
26595                let addr = screen_base + y * row_bytes + x;
26596                assert_eq!(
26597                    bus.read_byte(addr),
26598                    0x77,
26599                    "DisposDialog without saved pixels must not write to screen"
26600                );
26601            }
26602        }
26603    }
26604
26605    #[test]
26606    fn disposdialog_non_front_does_not_restore() {
26607        // If the dialog being disposed is NOT the front window, we
26608        // don't know its correct bounds (self.window_bounds belongs
26609        // to whatever is currently front). Restoring at the wrong
26610        // coords would corrupt the screen over the actual front
26611        // window. Expected behavior: discard saved pixels silently.
26612        let (mut disp, mut cpu, mut bus) = setup();
26613        let screen_base = 0x300000u32;
26614        let row_bytes: u32 = 640;
26615        disp.set_screen_mode_for_test(screen_base, row_bytes, 640, 480, 8);
26616
26617        let dialog_ptr = 0x200000u32;
26618        let other_front = 0x280000u32;
26619        disp.front_window = other_front; // dialog_ptr is NOT front
26620        disp.window_bounds = (200, 200, 300, 400); // matches other_front
26621
26622        for y in 95u32..155 {
26623            for x in 95u32..205 {
26624                bus.write_byte(screen_base + y * row_bytes + x, 0x55);
26625            }
26626        }
26627        disp.dialog_saved_pixels
26628            .insert(dialog_ptr, vec![0x99; 66 * 116]);
26629
26630        bus.write_long(TEST_SP, dialog_ptr);
26631        disp.dispatch_dialog(true, 0x183, &mut cpu, &mut bus)
26632            .unwrap()
26633            .unwrap();
26634
26635        for y in 95u32..155 {
26636            for x in 95u32..205 {
26637                let addr = screen_base + y * row_bytes + x;
26638                assert_eq!(
26639                    bus.read_byte(addr),
26640                    0x55,
26641                    "non-front DisposDialog must not restore over current front window"
26642                );
26643            }
26644        }
26645        assert!(
26646            !disp.dialog_saved_pixels.contains_key(&dialog_ptr),
26647            "saved pixels must still be removed for non-front dispose"
26648        );
26649    }
26650
26651    // ---- ParamText ($A98B) ----
26652
26653    fn write_pascal_str(bus: &mut crate::memory::MacMemoryBus, addr: u32, s: &[u8]) {
26654        bus.write_byte(addr, s.len() as u8);
26655        for (i, b) in s.iter().enumerate() {
26656            bus.write_byte(addr + 1 + i as u32, *b);
26657        }
26658    }
26659
26660    #[test]
26661    fn param_text_saves_all_four_strings_and_pops_16_bytes() {
26662        let (mut disp, mut cpu, mut bus) = setup();
26663
26664        let p0 = 0x300000u32;
26665        let p1 = 0x300100u32;
26666        let p2 = 0x300200u32;
26667        let p3 = 0x300300u32;
26668        write_pascal_str(&mut bus, p0, b"alpha");
26669        write_pascal_str(&mut bus, p1, b"bravo");
26670        write_pascal_str(&mut bus, p2, b"chi");
26671        write_pascal_str(&mut bus, p3, b"d");
26672
26673        bus.write_long(TEST_SP, p3);
26674        bus.write_long(TEST_SP + 4, p2);
26675        bus.write_long(TEST_SP + 8, p1);
26676        bus.write_long(TEST_SP + 12, p0);
26677
26678        disp.dispatch_dialog(true, 0x18B, &mut cpu, &mut bus)
26679            .unwrap()
26680            .unwrap();
26681
26682        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 16);
26683        assert_eq!(disp.param_text[0], b"alpha");
26684        assert_eq!(disp.param_text[1], b"bravo");
26685        assert_eq!(disp.param_text[2], b"chi");
26686        assert_eq!(disp.param_text[3], b"d");
26687    }
26688
26689    #[test]
26690    fn param_text_empty_pascal_string_clears_slot() {
26691        // Per Inside Macintosh Volume I, I-422, an empty Pascal string
26692        // (length-byte = 0) IS a valid value — the caret placeholder
26693        // gets replaced by nothing. This is the explicit "clear this
26694        // slot" idiom. Distinguishes from NIL (preserves prior).
26695        let (mut disp, mut cpu, mut bus) = setup();
26696        disp.param_text[0] = b"stale".to_vec();
26697        disp.param_text[1] = b"stale".to_vec();
26698        disp.param_text[2] = b"stale".to_vec();
26699        disp.param_text[3] = b"stale".to_vec();
26700
26701        let empty_ptr = 0x300000u32;
26702        write_pascal_str(&mut bus, empty_ptr, b"");
26703
26704        for off in [0u32, 4, 8, 12] {
26705            bus.write_long(TEST_SP + off, empty_ptr);
26706        }
26707
26708        disp.dispatch_dialog(true, 0x18B, &mut cpu, &mut bus)
26709            .unwrap()
26710            .unwrap();
26711
26712        for i in 0..4 {
26713            assert_eq!(
26714                disp.param_text[i],
26715                Vec::<u8>::new(),
26716                "empty Pascal string must clear slot {}",
26717                i
26718            );
26719        }
26720    }
26721
26722    #[test]
26723    fn apply_param_text_substitutes_caret_placeholders() {
26724        let (mut disp, _cpu, _bus) = setup();
26725        disp.param_text[0] = b"MS UserKey".to_vec();
26726        disp.param_text[1] = b"42".to_vec();
26727
26728        assert_eq!(
26729            disp.apply_param_text("Unable to open the \"^0\" file."),
26730            "Unable to open the \"MS UserKey\" file."
26731        );
26732        assert_eq!(disp.apply_param_text("count: ^1"), "count: 42");
26733        assert_eq!(
26734            disp.apply_param_text("plain text without placeholders"),
26735            "plain text without placeholders"
26736        );
26737        assert_eq!(disp.apply_param_text("^0 ^1 ^2 ^3"), "MS UserKey 42  ");
26738        assert_eq!(
26739            disp.apply_param_text("^A literal caret"),
26740            "^A literal caret"
26741        );
26742
26743        // Edge cases: lone trailing caret, double caret, caret at boundary,
26744        // out-of-range digit (^9 has no slot 9 → kept literal).
26745        assert_eq!(disp.apply_param_text("trailing^"), "trailing^");
26746        assert_eq!(disp.apply_param_text("^^0"), "^MS UserKey");
26747        assert_eq!(disp.apply_param_text("^9 unknown slot"), "^9 unknown slot");
26748        assert_eq!(disp.apply_param_text(""), "");
26749    }
26750
26751    #[test]
26752    fn param_text_nil_pointer_preserves_previous_slot_value() {
26753        // Per Inside Macintosh Volume I, I-422, passing NIL for any
26754        // ParamText slot must leave the prior value unchanged — apps
26755        // commonly stage one parameter at a time before opening an
26756        // alert, expecting the others to retain whatever they were
26757        // last set to.
26758        let (mut disp, mut cpu, mut bus) = setup();
26759        disp.param_text[0] = b"old0".to_vec();
26760        disp.param_text[1] = b"old1".to_vec();
26761        disp.param_text[2] = b"old2".to_vec();
26762        disp.param_text[3] = b"old3".to_vec();
26763
26764        let new0 = 0x300000u32;
26765        write_pascal_str(&mut bus, new0, b"new0");
26766
26767        bus.write_long(TEST_SP, 0); // param3 = NIL
26768        bus.write_long(TEST_SP + 4, 0); // param2 = NIL
26769        bus.write_long(TEST_SP + 8, 0); // param1 = NIL
26770        bus.write_long(TEST_SP + 12, new0);
26771
26772        disp.dispatch_dialog(true, 0x18B, &mut cpu, &mut bus)
26773            .unwrap()
26774            .unwrap();
26775
26776        assert_eq!(disp.param_text[0], b"new0", "param0 should be replaced");
26777        assert_eq!(disp.param_text[1], b"old1", "NIL must preserve param1");
26778        assert_eq!(disp.param_text[2], b"old2", "NIL must preserve param2");
26779        assert_eq!(disp.param_text[3], b"old3", "NIL must preserve param3");
26780    }
26781
26782    #[test]
26783    fn apply_param_text_returns_borrowed_when_no_placeholders() {
26784        // Pin the no-allocation contract: the common case (DITL text
26785        // without any `^N` placeholders) must return Cow::Borrowed so
26786        // draw_static_text doesn't allocate per item.
26787        use std::borrow::Cow;
26788        let (mut disp, _cpu, _bus) = setup();
26789        disp.param_text[0] = b"value".to_vec();
26790
26791        let plain = disp.apply_param_text("hello world");
26792        assert!(
26793            matches!(plain, Cow::Borrowed(_)),
26794            "no-placeholder input must skip the allocation path"
26795        );
26796
26797        let substituted = disp.apply_param_text("hello ^0");
26798        assert!(
26799            matches!(substituted, Cow::Owned(_)),
26800            "with-placeholder input takes the allocation path"
26801        );
26802        assert_eq!(substituted, "hello value");
26803    }
26804
26805    // ---- GetDItem ($A98D) ----
26806
26807    #[test]
26808    fn get_ditem_clears_outputs_and_pops_18_bytes() {
26809        let (mut disp, mut cpu, mut bus) = setup();
26810
26811        // Set up output pointers
26812        let box_ptr = 0x300000u32;
26813        let item_ptr = 0x300100u32;
26814        let type_ptr = 0x300200u32;
26815
26816        // Pre-fill output locations with non-zero to verify they get cleared
26817        bus.write_long(box_ptr, 0xDEADBEEF);
26818        bus.write_long(box_ptr + 4, 0xDEADBEEF);
26819        bus.write_long(item_ptr, 0xDEADBEEF);
26820        bus.write_word(type_ptr, 0xBEEF);
26821
26822        // Stack layout: SP+0: box(4), SP+4: item(4), SP+8: type(4), SP+12: itemNo(2), SP+14: dialog(4)
26823        bus.write_long(TEST_SP, box_ptr);
26824        bus.write_long(TEST_SP + 4, item_ptr);
26825        bus.write_long(TEST_SP + 8, type_ptr);
26826        bus.write_word(TEST_SP + 12, 1); // item number
26827        bus.write_long(TEST_SP + 14, 0x200000); // dialog ptr
26828
26829        let result = disp.dispatch_dialog(true, 0x18D, &mut cpu, &mut bus);
26830        assert!(result.unwrap().is_ok());
26831        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 18);
26832
26833        // Verify outputs were cleared to 0
26834        assert_eq!(bus.read_word(type_ptr), 0);
26835        assert_eq!(bus.read_long(item_ptr), 0);
26836        assert_eq!(bus.read_long(box_ptr), 0);
26837        assert_eq!(bus.read_long(box_ptr + 4), 0);
26838    }
26839
26840    #[test]
26841    fn get_ditem_text_handle_returns_existing_raw_text_handle() {
26842        let (mut disp, mut cpu, mut bus) = setup();
26843        let dialog_ptr = bus.alloc(170);
26844        let box_ptr = 0x300000u32;
26845        let item_ptr = 0x300100u32;
26846        let type_ptr = 0x300200u32;
26847        let text_handle = bus.alloc(4);
26848        let text_ptr = bus.alloc(5);
26849        let items_handle = bus.alloc(4);
26850        let ditl_ptr = bus.alloc(22);
26851
26852        bus.write_bytes(text_ptr, b"Hello");
26853        bus.write_long(text_handle, text_ptr);
26854        bus.write_long(items_handle, ditl_ptr);
26855        bus.write_long(dialog_ptr + 156, items_handle);
26856        bus.write_word(ditl_ptr, 0);
26857        bus.write_long(ditl_ptr + 2, text_handle);
26858        bus.write_word(ditl_ptr + 6, 10);
26859        bus.write_word(ditl_ptr + 8, 20);
26860        bus.write_word(ditl_ptr + 10, 30);
26861        bus.write_word(ditl_ptr + 12, 40);
26862        bus.write_byte(ditl_ptr + 14, 8);
26863        bus.write_byte(ditl_ptr + 15, 5);
26864        bus.write_bytes(ditl_ptr + 16, b"Hello");
26865
26866        disp.dialog_items.insert(
26867            dialog_ptr,
26868            vec![DialogItem {
26869                item_type: 8,
26870                rect: (10, 20, 30, 40),
26871                text: "Hello".to_string(),
26872                resource_id: 0,
26873                proc_ptr: 0,
26874                sel_start: 0,
26875                sel_end: 0,
26876            }],
26877        );
26878
26879        bus.write_long(TEST_SP, box_ptr);
26880        bus.write_long(TEST_SP + 4, item_ptr);
26881        bus.write_long(TEST_SP + 8, type_ptr);
26882        bus.write_word(TEST_SP + 12, 1);
26883        bus.write_long(TEST_SP + 14, dialog_ptr);
26884
26885        let result = disp.dispatch_dialog(true, 0x18D, &mut cpu, &mut bus);
26886        assert!(result.unwrap().is_ok());
26887
26888        assert_eq!(bus.read_long(item_ptr), text_handle);
26889        assert_eq!(bus.read_long(ditl_ptr + 2), text_handle);
26890        assert_eq!(bus.get_alloc_size(text_ptr), Some(5));
26891        assert_eq!(bus.read_bytes(text_ptr, 5), b"Hello".to_vec());
26892    }
26893
26894    #[test]
26895    fn get_ditem_records_pending_popup_candidate_without_associating() {
26896        let (mut disp, mut cpu, mut bus) = setup();
26897        let dialog_ptr = bus.alloc(170);
26898        let box_ptr = bus.alloc(8);
26899        let item_ptr = bus.alloc(4);
26900        let type_ptr = bus.alloc(2);
26901
26902        disp.dialog_items.insert(
26903            dialog_ptr,
26904            vec![DialogItem {
26905                item_type: 0,
26906                rect: (10, 20, 30, 40),
26907                text: String::new(),
26908                resource_id: 0,
26909                proc_ptr: 0,
26910                sel_start: 0,
26911                sel_end: 0,
26912            }],
26913        );
26914        disp.last_inserted_menu_id = Some(1234);
26915
26916        bus.write_long(TEST_SP, box_ptr);
26917        bus.write_long(TEST_SP + 4, item_ptr);
26918        bus.write_long(TEST_SP + 8, type_ptr);
26919        bus.write_word(TEST_SP + 12, 1);
26920        bus.write_long(TEST_SP + 14, dialog_ptr);
26921
26922        disp.dispatch_dialog(true, 0x18D, &mut cpu, &mut bus)
26923            .unwrap()
26924            .unwrap();
26925
26926        assert!(!disp.dialog_item_popup_menus.contains_key(&(dialog_ptr, 1)));
26927        assert!(!disp
26928            .dialog_popup_original_rects
26929            .contains_key(&(dialog_ptr, 1)));
26930        let pending = disp
26931            .pending_dialog_popup_menu
26932            .expect("enabled userItem should leave a pending popup candidate");
26933        assert_eq!(pending.dialog_ptr, dialog_ptr);
26934        assert_eq!(pending.item_no, 1);
26935        assert_eq!(pending.menu_id, 1234);
26936        assert_eq!(pending.rect, (10, 20, 30, 40));
26937        assert_eq!(disp.last_inserted_menu_id, None);
26938    }
26939
26940    #[test]
26941    fn get_ditem_ignores_recent_inserted_menu_for_disabled_user_item() {
26942        let (mut disp, mut cpu, mut bus) = setup();
26943        let dialog_ptr = bus.alloc(170);
26944        let box_ptr = bus.alloc(8);
26945        let item_ptr = bus.alloc(4);
26946        let type_ptr = bus.alloc(2);
26947
26948        disp.dialog_items.insert(
26949            dialog_ptr,
26950            vec![DialogItem {
26951                item_type: 0x80,
26952                rect: (10, 20, 30, 40),
26953                text: String::new(),
26954                resource_id: 0,
26955                proc_ptr: 0,
26956                sel_start: 0,
26957                sel_end: 0,
26958            }],
26959        );
26960        disp.last_inserted_menu_id = Some(1234);
26961
26962        bus.write_long(TEST_SP, box_ptr);
26963        bus.write_long(TEST_SP + 4, item_ptr);
26964        bus.write_long(TEST_SP + 8, type_ptr);
26965        bus.write_word(TEST_SP + 12, 1);
26966        bus.write_long(TEST_SP + 14, dialog_ptr);
26967
26968        disp.dispatch_dialog(true, 0x18D, &mut cpu, &mut bus)
26969            .unwrap()
26970            .unwrap();
26971
26972        assert!(!disp.dialog_item_popup_menus.contains_key(&(dialog_ptr, 1)));
26973        assert!(!disp
26974            .dialog_popup_original_rects
26975            .contains_key(&(dialog_ptr, 1)));
26976        assert!(disp.pending_dialog_popup_menu.is_none());
26977        assert_eq!(disp.last_inserted_menu_id, None);
26978    }
26979
26980    #[test]
26981    fn set_ditem_confirms_pending_popup_user_item_when_proc_installed() {
26982        let (mut disp, mut cpu, mut bus) = setup();
26983        let dialog_ptr = bus.alloc(170);
26984        let get_box_ptr = bus.alloc(8);
26985        let get_item_ptr = bus.alloc(4);
26986        let get_type_ptr = bus.alloc(2);
26987        let set_box_ptr = bus.alloc(8);
26988        let proc_ptr = 0x00AB_CDEF;
26989
26990        disp.dialog_items.insert(
26991            dialog_ptr,
26992            vec![DialogItem {
26993                item_type: 0,
26994                rect: (10, 20, 30, 40),
26995                text: String::new(),
26996                resource_id: 0,
26997                proc_ptr: 0,
26998                sel_start: 0,
26999                sel_end: 0,
27000            }],
27001        );
27002        disp.last_inserted_menu_id = Some(1234);
27003
27004        bus.write_long(TEST_SP, get_box_ptr);
27005        bus.write_long(TEST_SP + 4, get_item_ptr);
27006        bus.write_long(TEST_SP + 8, get_type_ptr);
27007        bus.write_word(TEST_SP + 12, 1);
27008        bus.write_long(TEST_SP + 14, dialog_ptr);
27009        disp.dispatch_dialog(true, 0x18D, &mut cpu, &mut bus)
27010            .unwrap()
27011            .unwrap();
27012
27013        cpu.write_reg(Register::A7, TEST_SP);
27014        bus.write_word(set_box_ptr, 12);
27015        bus.write_word(set_box_ptr + 2, 22);
27016        bus.write_word(set_box_ptr + 4, 32);
27017        bus.write_word(set_box_ptr + 6, 42);
27018        bus.write_long(TEST_SP, set_box_ptr);
27019        bus.write_long(TEST_SP + 4, proc_ptr);
27020        bus.write_word(TEST_SP + 8, 0);
27021        bus.write_word(TEST_SP + 10, 1);
27022        bus.write_long(TEST_SP + 12, dialog_ptr);
27023        disp.dispatch_dialog(true, 0x18E, &mut cpu, &mut bus)
27024            .unwrap()
27025            .unwrap();
27026
27027        assert_eq!(
27028            disp.dialog_item_popup_menus.get(&(dialog_ptr, 1)),
27029            Some(&1234)
27030        );
27031        assert_eq!(
27032            disp.dialog_popup_original_rects.get(&(dialog_ptr, 1)),
27033            Some(&(10, 20, 30, 40))
27034        );
27035        assert!(disp.pending_dialog_popup_menu.is_none());
27036    }
27037
27038    #[test]
27039    fn set_ditem_narrowing_user_item_records_popup_candidate_without_proc() {
27040        let (mut disp, mut cpu, mut bus) = setup();
27041        let dialog_ptr = bus.alloc(170);
27042        let set_box_ptr = bus.alloc(8);
27043
27044        disp.dialog_items.insert(
27045            dialog_ptr,
27046            vec![DialogItem {
27047                item_type: 0,
27048                rect: (10, 20, 30, 150),
27049                text: String::new(),
27050                resource_id: 0,
27051                proc_ptr: 0,
27052                sel_start: 0,
27053                sel_end: 0,
27054            }],
27055        );
27056
27057        bus.write_word(set_box_ptr, 10);
27058        bus.write_word(set_box_ptr + 2, 20);
27059        bus.write_word(set_box_ptr + 4, 30);
27060        bus.write_word(set_box_ptr + 6, 35);
27061        bus.write_long(TEST_SP, set_box_ptr);
27062        bus.write_long(TEST_SP + 4, 0);
27063        bus.write_word(TEST_SP + 8, 0);
27064        bus.write_word(TEST_SP + 10, 1);
27065        bus.write_long(TEST_SP + 12, dialog_ptr);
27066        disp.dispatch_dialog(true, 0x18E, &mut cpu, &mut bus)
27067            .unwrap()
27068            .unwrap();
27069
27070        assert!(!disp.dialog_item_popup_menus.contains_key(&(dialog_ptr, 1)));
27071        assert_eq!(
27072            disp.dialog_popup_original_rects.get(&(dialog_ptr, 1)),
27073            Some(&(10, 20, 30, 150))
27074        );
27075        assert!(disp.dialog_popup_candidate_items.contains(&(dialog_ptr, 1)));
27076        assert_eq!(
27077            disp.dialog_items
27078                .get(&dialog_ptr)
27079                .and_then(|items| items.first())
27080                .map(|item| item.rect),
27081            Some((10, 20, 30, 35))
27082        );
27083    }
27084
27085    // ---- SetDItem ($A98E) ----
27086
27087    #[test]
27088    fn set_ditem_pops_16_bytes() {
27089        let (mut disp, mut cpu, mut bus) = setup();
27090
27091        let result = disp.dispatch_dialog(true, 0x18E, &mut cpu, &mut bus);
27092        assert!(result.unwrap().is_ok());
27093        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 16);
27094    }
27095
27096    #[test]
27097    fn set_ditem_updates_item_fields_visible_through_get_ditem() {
27098        // Inside Macintosh Volume I, I-421: SetDItem changes itemType, item,
27099        // and box for the specified item; GetDItem must then report those
27100        // updated fields.
27101        let (mut disp, mut cpu, mut bus) = setup();
27102        let dialog_ptr = bus.alloc(170);
27103        let items_handle = bus.alloc(4);
27104        let ditl_ptr = bus.alloc(22);
27105        let old_handle = bus.alloc(4);
27106        let old_ptr = bus.alloc(4);
27107        let new_handle = bus.alloc(4);
27108        let new_ptr = bus.alloc(3);
27109        let set_box_ptr = bus.alloc(8);
27110        let get_box_ptr = bus.alloc(8);
27111        let get_item_ptr = bus.alloc(4);
27112        let get_type_ptr = bus.alloc(2);
27113
27114        bus.write_bytes(old_ptr, b"Old!");
27115        bus.write_bytes(new_ptr, b"New");
27116        bus.write_long(old_handle, old_ptr);
27117        bus.write_long(new_handle, new_ptr);
27118        bus.write_long(items_handle, ditl_ptr);
27119        bus.write_long(dialog_ptr + 156, items_handle);
27120        bus.write_word(ditl_ptr, 0); // one item
27121        bus.write_long(ditl_ptr + 2, old_handle);
27122        bus.write_word(ditl_ptr + 6, 10);
27123        bus.write_word(ditl_ptr + 8, 20);
27124        bus.write_word(ditl_ptr + 10, 30);
27125        bus.write_word(ditl_ptr + 12, 40);
27126        bus.write_byte(ditl_ptr + 14, 8);
27127        bus.write_byte(ditl_ptr + 15, 4);
27128        bus.write_bytes(ditl_ptr + 16, b"Old!");
27129
27130        disp.dialog_items.insert(
27131            dialog_ptr,
27132            vec![DialogItem {
27133                item_type: 8,
27134                rect: (10, 20, 30, 40),
27135                text: "Old!".to_string(),
27136                resource_id: 0,
27137                proc_ptr: 0,
27138                sel_start: 0,
27139                sel_end: 0,
27140            }],
27141        );
27142        disp.dialog_item_handles.insert(old_handle, (dialog_ptr, 0));
27143
27144        bus.write_word(set_box_ptr, 111);
27145        bus.write_word(set_box_ptr + 2, 222);
27146        bus.write_word(set_box_ptr + 4, 333);
27147        bus.write_word(set_box_ptr + 6, 444);
27148        bus.write_long(TEST_SP, set_box_ptr);
27149        bus.write_long(TEST_SP + 4, new_handle);
27150        bus.write_word(TEST_SP + 8, 16);
27151        bus.write_word(TEST_SP + 10, 1);
27152        bus.write_long(TEST_SP + 12, dialog_ptr);
27153
27154        disp.dispatch_dialog(true, 0x18E, &mut cpu, &mut bus)
27155            .unwrap()
27156            .unwrap();
27157
27158        cpu.write_reg(Register::A7, TEST_SP);
27159        bus.write_long(TEST_SP, get_box_ptr);
27160        bus.write_long(TEST_SP + 4, get_item_ptr);
27161        bus.write_long(TEST_SP + 8, get_type_ptr);
27162        bus.write_word(TEST_SP + 12, 1);
27163        bus.write_long(TEST_SP + 14, dialog_ptr);
27164        disp.dispatch_dialog(true, 0x18D, &mut cpu, &mut bus)
27165            .unwrap()
27166            .unwrap();
27167
27168        assert_eq!(bus.read_word(get_type_ptr), 16);
27169        assert_eq!(bus.read_long(get_item_ptr), new_handle);
27170        assert_eq!(bus.read_word(get_box_ptr), 111);
27171        assert_eq!(bus.read_word(get_box_ptr + 2), 222);
27172        assert_eq!(bus.read_word(get_box_ptr + 4), 333);
27173        assert_eq!(bus.read_word(get_box_ptr + 6), 444);
27174    }
27175
27176    #[test]
27177    fn set_ditem_user_item_treats_item_parameter_as_proc_ptr() {
27178        // Inside Macintosh Volume I, I-421: for userItem, SetDItem's `item`
27179        // parameter is a draw procedure pointer (ProcPtr), not a text/control
27180        // handle. GetDItem should report the same ProcPtr value.
27181        let (mut disp, mut cpu, mut bus) = setup();
27182        let dialog_ptr = bus.alloc(170);
27183        let items_handle = bus.alloc(4);
27184        let ditl_ptr = bus.alloc(16);
27185        let proc_ptr = 0x00AB_CDEFu32;
27186        let set_box_ptr = bus.alloc(8);
27187        let get_box_ptr = bus.alloc(8);
27188        let get_item_ptr = bus.alloc(4);
27189        let get_type_ptr = bus.alloc(2);
27190
27191        bus.write_long(items_handle, ditl_ptr);
27192        bus.write_long(dialog_ptr + 156, items_handle);
27193        bus.write_word(ditl_ptr, 0); // one item
27194        bus.write_long(ditl_ptr + 2, 0);
27195        bus.write_word(ditl_ptr + 6, 10);
27196        bus.write_word(ditl_ptr + 8, 20);
27197        bus.write_word(ditl_ptr + 10, 30);
27198        bus.write_word(ditl_ptr + 12, 40);
27199        bus.write_byte(ditl_ptr + 14, 0);
27200        bus.write_byte(ditl_ptr + 15, 0);
27201
27202        disp.dialog_items.insert(
27203            dialog_ptr,
27204            vec![DialogItem {
27205                item_type: 0,
27206                rect: (10, 20, 30, 40),
27207                text: String::new(),
27208                resource_id: 0,
27209                proc_ptr: 0,
27210                sel_start: 0,
27211                sel_end: 0,
27212            }],
27213        );
27214
27215        bus.write_word(set_box_ptr, 50);
27216        bus.write_word(set_box_ptr + 2, 60);
27217        bus.write_word(set_box_ptr + 4, 70);
27218        bus.write_word(set_box_ptr + 6, 80);
27219        bus.write_long(TEST_SP, set_box_ptr);
27220        bus.write_long(TEST_SP + 4, proc_ptr);
27221        bus.write_word(TEST_SP + 8, 0);
27222        bus.write_word(TEST_SP + 10, 1);
27223        bus.write_long(TEST_SP + 12, dialog_ptr);
27224        disp.dispatch_dialog(true, 0x18E, &mut cpu, &mut bus)
27225            .unwrap()
27226            .unwrap();
27227
27228        cpu.write_reg(Register::A7, TEST_SP);
27229        bus.write_long(TEST_SP, get_box_ptr);
27230        bus.write_long(TEST_SP + 4, get_item_ptr);
27231        bus.write_long(TEST_SP + 8, get_type_ptr);
27232        bus.write_word(TEST_SP + 12, 1);
27233        bus.write_long(TEST_SP + 14, dialog_ptr);
27234        disp.dispatch_dialog(true, 0x18D, &mut cpu, &mut bus)
27235            .unwrap()
27236            .unwrap();
27237
27238        assert_eq!(bus.read_word(get_type_ptr), 0);
27239        assert_eq!(bus.read_long(get_item_ptr), proc_ptr);
27240        assert_eq!(bus.read_word(get_box_ptr), 50);
27241        assert_eq!(bus.read_word(get_box_ptr + 2), 60);
27242        assert_eq!(bus.read_word(get_box_ptr + 4), 70);
27243        assert_eq!(bus.read_word(get_box_ptr + 6), 80);
27244    }
27245
27246    #[test]
27247    fn set_dialog_item_text_resizes_existing_text_handle() {
27248        let (mut disp, mut cpu, mut bus) = setup();
27249        let dialog_ptr = 0x200000u32;
27250        let text_handle = bus.alloc(4);
27251        let text_ptr = bus.alloc(2);
27252        let new_text_ptr = 0x300000u32;
27253
27254        bus.write_long(text_handle, text_ptr);
27255        bus.write_byte(text_ptr, 1);
27256        bus.write_byte(text_ptr + 1, b'A');
27257        bus.write_byte(new_text_ptr, 5);
27258        bus.write_bytes(new_text_ptr + 1, b"Hello");
27259
27260        disp.dialog_items.insert(
27261            dialog_ptr,
27262            vec![DialogItem {
27263                item_type: 8,
27264                rect: (0, 0, 10, 10),
27265                text: "A".to_string(),
27266                resource_id: 0,
27267                proc_ptr: 0,
27268                sel_start: 0,
27269                sel_end: 0,
27270            }],
27271        );
27272        disp.dialog_item_handles
27273            .insert(text_handle, (dialog_ptr, 0));
27274
27275        bus.write_long(TEST_SP, new_text_ptr);
27276        bus.write_long(TEST_SP + 4, text_handle);
27277
27278        let result = disp.dispatch_dialog(true, 0x18F, &mut cpu, &mut bus);
27279        assert!(result.unwrap().is_ok());
27280
27281        let resized_ptr = bus.read_long(text_handle);
27282        assert_eq!(bus.get_alloc_size(resized_ptr), Some(5));
27283        assert_eq!(bus.read_bytes(resized_ptr, 5), b"Hello".to_vec());
27284    }
27285
27286    #[test]
27287    fn set_dialog_item_text_redraws_visible_text_item() {
27288        let (mut disp, mut cpu, mut bus) = setup();
27289        let screen_base = bus.alloc((200 * 100) as u32);
27290        let row_bytes = 200u32;
27291        disp.set_screen_mode_for_test(screen_base, row_bytes, 200, 100, 8);
27292        bus.write_long(0x0824, screen_base);
27293        TrapDispatcher::fb_fill_rect(
27294            &mut bus,
27295            screen_base,
27296            row_bytes,
27297            8,
27298            200,
27299            100,
27300            0,
27301            0,
27302            100,
27303            200,
27304            false,
27305        );
27306        let white = bus.read_byte(screen_base);
27307
27308        let bounds = (10, 20, 70, 180);
27309        let dialog_ptr = bus.alloc(170);
27310        bus.write_word(dialog_ptr + 6, 0); // GrafPort, not CGrafPort
27311        bus.write_word(dialog_ptr + 8, (-bounds.0) as u16);
27312        bus.write_word(dialog_ptr + 10, (-bounds.1) as u16);
27313        bus.write_word(dialog_ptr + 16, 0);
27314        bus.write_word(dialog_ptr + 18, 0);
27315        bus.write_word(dialog_ptr + 20, (bounds.2 - bounds.0) as u16);
27316        bus.write_word(dialog_ptr + 22, (bounds.3 - bounds.1) as u16);
27317        bus.write_word(dialog_ptr + 108, 2);
27318        bus.write_byte(dialog_ptr + 110, 0xFF);
27319        bus.write_word(dialog_ptr + 164, 0xFFFF);
27320        bus.write_word(dialog_ptr + 168, 1);
27321
27322        let items_handle = bus.alloc(4);
27323        let ditl_ptr = bus.alloc(16);
27324        let text_handle = bus.alloc(4);
27325        let text_ptr = bus.alloc(1);
27326        bus.write_bytes(text_ptr, b"A");
27327        bus.write_long(text_handle, text_ptr);
27328        bus.write_long(items_handle, ditl_ptr);
27329        bus.write_long(dialog_ptr + 156, items_handle);
27330        bus.write_word(ditl_ptr, 0);
27331        bus.write_long(ditl_ptr + 2, text_handle);
27332        bus.write_word(ditl_ptr + 6, 10);
27333        bus.write_word(ditl_ptr + 8, 10);
27334        bus.write_word(ditl_ptr + 10, 26);
27335        bus.write_word(ditl_ptr + 12, 140);
27336        bus.write_byte(ditl_ptr + 14, 8);
27337        bus.write_byte(ditl_ptr + 15, 0);
27338
27339        let items = vec![DialogItem {
27340            item_type: 8,
27341            rect: (10, 10, 26, 140),
27342            text: "A".to_string(),
27343            resource_id: 0,
27344            proc_ptr: 0,
27345            sel_start: 0,
27346            sel_end: 0,
27347        }];
27348        disp.dialog_items.insert(dialog_ptr, items.clone());
27349        disp.dialog_item_handles
27350            .insert(text_handle, (dialog_ptr, 0));
27351        disp.draw_dialog(&mut bus, bounds, 2, "", &items, 1, "", 0, false, dialog_ptr);
27352
27353        let count_nonwhite = |bus: &MacMemoryBus| -> usize {
27354            let mut count = 0;
27355            for y in 20..36u32 {
27356                for x in 30..160u32 {
27357                    if bus.read_byte(screen_base + y * row_bytes + x) != white {
27358                        count += 1;
27359                    }
27360                }
27361            }
27362            count
27363        };
27364        let before = count_nonwhite(&bus);
27365
27366        let pstr = bus.alloc(9);
27367        bus.write_byte(pstr, 8);
27368        bus.write_bytes(pstr + 1, b"WWWWWWWW");
27369        bus.write_long(TEST_SP, pstr);
27370        bus.write_long(TEST_SP + 4, text_handle);
27371
27372        let result = disp.dispatch_dialog(true, 0x18F, &mut cpu, &mut bus);
27373        assert!(result.unwrap().is_ok());
27374
27375        let after = count_nonwhite(&bus);
27376        assert!(
27377            after > before + 20,
27378            "SetDialogItemText must draw the updated text item; before={} after={}",
27379            before,
27380            after
27381        );
27382    }
27383
27384    #[test]
27385    fn set_dialog_item_text_redraw_clips_repaint_to_dialog_bounds() {
27386        let (mut disp, mut cpu, mut bus) = setup();
27387        let screen_base = bus.alloc((200 * 100) as u32);
27388        let row_bytes = 200u32;
27389        disp.set_screen_mode_for_test(screen_base, row_bytes, 200, 100, 8);
27390        bus.write_long(0x0824, screen_base);
27391        TrapDispatcher::fb_fill_rect(
27392            &mut bus,
27393            screen_base,
27394            row_bytes,
27395            8,
27396            200,
27397            100,
27398            0,
27399            0,
27400            100,
27401            200,
27402            true,
27403        );
27404
27405        let bounds = (10, 20, 70, 120);
27406        let dialog_ptr = bus.alloc(170);
27407        bus.write_word(dialog_ptr + 6, 0); // GrafPort, not CGrafPort
27408        bus.write_word(dialog_ptr + 8, (-bounds.0) as u16);
27409        bus.write_word(dialog_ptr + 10, (-bounds.1) as u16);
27410        bus.write_word(dialog_ptr + 16, 0);
27411        bus.write_word(dialog_ptr + 18, 0);
27412        bus.write_word(dialog_ptr + 20, (bounds.2 - bounds.0) as u16);
27413        bus.write_word(dialog_ptr + 22, (bounds.3 - bounds.1) as u16);
27414        bus.write_word(dialog_ptr + 108, 2);
27415        bus.write_byte(dialog_ptr + 110, 0xFF);
27416        bus.write_word(dialog_ptr + 164, 1); // item 2 is the active edit field
27417        bus.write_word(dialog_ptr + 168, 1);
27418
27419        let items_handle = bus.alloc(4);
27420        let ditl_ptr = bus.alloc(30);
27421        let edit_handle = bus.alloc(4);
27422        let edit_ptr = bus.alloc(3);
27423        bus.write_bytes(edit_ptr, b"Old");
27424        bus.write_long(edit_handle, edit_ptr);
27425        bus.write_long(items_handle, ditl_ptr);
27426        bus.write_long(dialog_ptr + 156, items_handle);
27427        bus.write_word(ditl_ptr, 1); // two items
27428        bus.write_long(ditl_ptr + 2, 0);
27429        bus.write_word(ditl_ptr + 6, 5);
27430        bus.write_word(ditl_ptr + 8, 20);
27431        bus.write_word(ditl_ptr + 10, 20);
27432        bus.write_word(ditl_ptr + 12, 160); // extends beyond dialog right
27433        bus.write_byte(ditl_ptr + 14, 8);
27434        bus.write_byte(ditl_ptr + 15, 0);
27435        bus.write_long(ditl_ptr + 16, edit_handle);
27436        bus.write_word(ditl_ptr + 20, 25);
27437        bus.write_word(ditl_ptr + 22, 20);
27438        bus.write_word(ditl_ptr + 24, 41);
27439        bus.write_word(ditl_ptr + 26, 80);
27440        bus.write_byte(ditl_ptr + 28, 16);
27441        bus.write_byte(ditl_ptr + 29, 0);
27442
27443        let items = vec![
27444            DialogItem {
27445                item_type: 8,
27446                rect: (5, 20, 20, 160),
27447                text: String::new(),
27448                resource_id: 0,
27449                proc_ptr: 0,
27450                sel_start: 0,
27451                sel_end: 0,
27452            },
27453            DialogItem {
27454                item_type: 16,
27455                rect: (25, 20, 41, 80),
27456                text: "Old".to_string(),
27457                resource_id: 0,
27458                proc_ptr: 0,
27459                sel_start: 0,
27460                sel_end: 0,
27461            },
27462        ];
27463        disp.dialog_items.insert(dialog_ptr, items.clone());
27464        disp.dialog_item_handles
27465            .insert(edit_handle, (dialog_ptr, 1));
27466        disp.draw_dialog(
27467            &mut bus, bounds, 2, "", &items, 1, "Old", 2, false, dialog_ptr,
27468        );
27469
27470        let sample = screen_base + 17 * row_bytes + 150;
27471        let outside_before = bus.read_byte(sample);
27472
27473        let pstr = bus.alloc(4);
27474        bus.write_byte(pstr, 3);
27475        bus.write_bytes(pstr + 1, b"New");
27476        bus.write_long(TEST_SP, pstr);
27477        bus.write_long(TEST_SP + 4, edit_handle);
27478
27479        let result = disp.dispatch_dialog(true, 0x18F, &mut cpu, &mut bus);
27480        assert!(result.unwrap().is_ok());
27481
27482        assert_eq!(
27483            bus.read_byte(sample),
27484            outside_before,
27485            "SetDialogItemText repaint must not clear outside the dialog port"
27486        );
27487    }
27488
27489    #[test]
27490    fn get_dialog_item_text_returns_pascal_string_from_raw_handle() {
27491        let (mut disp, mut cpu, mut bus) = setup();
27492        let text_handle = bus.alloc(4);
27493        let text_ptr = bus.alloc(5);
27494        let out_ptr = 0x300000u32;
27495
27496        bus.write_long(text_handle, text_ptr);
27497        bus.write_bytes(text_ptr, b"Hello");
27498        bus.write_long(TEST_SP, out_ptr);
27499        bus.write_long(TEST_SP + 4, text_handle);
27500
27501        let result = disp.dispatch_dialog(true, 0x190, &mut cpu, &mut bus);
27502        assert!(result.unwrap().is_ok());
27503        assert_eq!(bus.read_byte(out_ptr), 5);
27504        assert_eq!(bus.read_bytes(out_ptr + 1, 5), b"Hello".to_vec());
27505    }
27506
27507    #[test]
27508    fn initialize_dialog_item_handles_rewrites_ditl_text_item_storage() {
27509        let (mut disp, _cpu, mut bus) = setup();
27510        let dialog_ptr = bus.alloc(170);
27511        let items_handle = bus.alloc(4);
27512        let ditl_ptr = bus.alloc(22);
27513
27514        bus.write_long(items_handle, ditl_ptr);
27515        bus.write_long(dialog_ptr + 156, items_handle);
27516        bus.write_word(ditl_ptr, 0);
27517        bus.write_long(ditl_ptr + 2, 0);
27518        bus.write_word(ditl_ptr + 6, 10);
27519        bus.write_word(ditl_ptr + 8, 20);
27520        bus.write_word(ditl_ptr + 10, 30);
27521        bus.write_word(ditl_ptr + 12, 40);
27522        bus.write_byte(ditl_ptr + 14, 8);
27523        bus.write_byte(ditl_ptr + 15, 5);
27524        bus.write_bytes(ditl_ptr + 16, b"Hello");
27525
27526        let items = vec![DialogItem {
27527            item_type: 8,
27528            rect: (10, 20, 30, 40),
27529            text: "Hello".to_string(),
27530            resource_id: 0,
27531            proc_ptr: 0,
27532            sel_start: 0,
27533            sel_end: 0,
27534        }];
27535
27536        disp.initialize_dialog_item_handles(&mut bus, dialog_ptr, &items);
27537
27538        let created_handle = bus.read_long(ditl_ptr + 2);
27539        let created_ptr = bus.read_long(created_handle);
27540        assert_ne!(created_handle, 0);
27541        assert_eq!(bus.get_alloc_size(created_ptr), Some(5));
27542        assert_eq!(bus.read_bytes(created_ptr, 5), b"Hello".to_vec());
27543        assert_eq!(
27544            disp.dialog_item_handles.get(&created_handle),
27545            Some(&(dialog_ptr, 0))
27546        );
27547    }
27548
27549    #[test]
27550    fn initialize_dialog_item_handles_materializes_icon_and_picture_handles() {
27551        let (mut disp, _cpu, mut bus) = setup();
27552        let dialog_ptr = bus.alloc(170);
27553        let items_handle = bus.alloc(4);
27554        let ditl_ptr = bus.alloc(34);
27555        let icon_ptr = disp.install_test_resource(&mut bus, *b"ICON", 421, &[0xFF; 128]);
27556        let pict_ptr = disp.install_test_resource(&mut bus, *b"PICT", 422, &[0x11; 32]);
27557
27558        bus.write_long(items_handle, ditl_ptr);
27559        bus.write_long(dialog_ptr + 156, items_handle);
27560        bus.write_word(ditl_ptr, 1);
27561        bus.write_long(ditl_ptr + 2, 0);
27562        bus.write_word(ditl_ptr + 6, 8);
27563        bus.write_word(ditl_ptr + 8, 8);
27564        bus.write_word(ditl_ptr + 10, 40);
27565        bus.write_word(ditl_ptr + 12, 40);
27566        bus.write_byte(ditl_ptr + 14, 32);
27567        bus.write_byte(ditl_ptr + 15, 2);
27568        bus.write_word(ditl_ptr + 16, 421);
27569        bus.write_long(ditl_ptr + 18, 0);
27570        bus.write_word(ditl_ptr + 22, 8);
27571        bus.write_word(ditl_ptr + 24, 48);
27572        bus.write_word(ditl_ptr + 26, 40);
27573        bus.write_word(ditl_ptr + 28, 112);
27574        bus.write_byte(ditl_ptr + 30, 64);
27575        bus.write_byte(ditl_ptr + 31, 2);
27576        bus.write_word(ditl_ptr + 32, 422);
27577
27578        let items = vec![
27579            DialogItem {
27580                item_type: 32,
27581                rect: (8, 8, 40, 40),
27582                text: String::new(),
27583                resource_id: 421,
27584                proc_ptr: 0,
27585                sel_start: 0,
27586                sel_end: 0,
27587            },
27588            DialogItem {
27589                item_type: 64,
27590                rect: (8, 48, 40, 112),
27591                text: String::new(),
27592                resource_id: 422,
27593                proc_ptr: 0,
27594                sel_start: 0,
27595                sel_end: 0,
27596            },
27597        ];
27598
27599        disp.initialize_dialog_item_handles(&mut bus, dialog_ptr, &items);
27600
27601        let icon_handle = bus.read_long(ditl_ptr + 2);
27602        let pict_handle = bus.read_long(ditl_ptr + 18);
27603        assert_ne!(icon_handle, 0);
27604        assert_ne!(pict_handle, 0);
27605        assert_eq!(bus.read_long(icon_handle), icon_ptr);
27606        assert_eq!(bus.read_long(pict_handle), pict_ptr);
27607    }
27608
27609    #[test]
27610    fn initialize_dialog_item_handles_clears_user_item_placeholder_proc_ptr() {
27611        let (mut disp, _cpu, mut bus) = setup();
27612        let dialog_ptr = bus.alloc(170);
27613        let items_handle = bus.alloc(4);
27614        let ditl_ptr = bus.alloc(16);
27615
27616        bus.write_long(items_handle, ditl_ptr);
27617        bus.write_long(dialog_ptr + 156, items_handle);
27618        bus.write_word(ditl_ptr, 0);
27619        bus.write_long(ditl_ptr + 2, 0x12345678);
27620        bus.write_word(ditl_ptr + 6, 10);
27621        bus.write_word(ditl_ptr + 8, 20);
27622        bus.write_word(ditl_ptr + 10, 30);
27623        bus.write_word(ditl_ptr + 12, 40);
27624        bus.write_byte(ditl_ptr + 14, 0);
27625        bus.write_byte(ditl_ptr + 15, 0);
27626
27627        let items = vec![DialogItem {
27628            item_type: 0,
27629            rect: (10, 20, 30, 40),
27630            text: String::new(),
27631            resource_id: 0,
27632            proc_ptr: 0,
27633            sel_start: 0,
27634            sel_end: 0,
27635        }];
27636
27637        disp.initialize_dialog_item_handles(&mut bus, dialog_ptr, &items);
27638
27639        assert_eq!(bus.read_long(ditl_ptr + 2), 0);
27640    }
27641
27642    #[test]
27643    fn initialize_dialog_item_handles_creates_standard_control_handle() {
27644        let (mut disp, _cpu, mut bus) = setup();
27645        let dialog_ptr = bus.alloc(170);
27646        let items_handle = bus.alloc(4);
27647        let ditl_ptr = bus.alloc(22);
27648
27649        bus.write_long(items_handle, ditl_ptr);
27650        bus.write_long(dialog_ptr + 156, items_handle);
27651        bus.write_word(ditl_ptr, 0);
27652        bus.write_long(ditl_ptr + 2, 0);
27653        bus.write_word(ditl_ptr + 6, 10);
27654        bus.write_word(ditl_ptr + 8, 20);
27655        bus.write_word(ditl_ptr + 10, 30);
27656        bus.write_word(ditl_ptr + 12, 120);
27657        bus.write_byte(ditl_ptr + 14, 5);
27658        bus.write_byte(ditl_ptr + 15, 5);
27659        bus.write_bytes(ditl_ptr + 16, b"Sound");
27660
27661        let items = vec![DialogItem {
27662            item_type: 5,
27663            rect: (10, 20, 30, 120),
27664            text: "Sound".to_string(),
27665            resource_id: 0,
27666            proc_ptr: 0,
27667            sel_start: 0,
27668            sel_end: 0,
27669        }];
27670
27671        disp.initialize_dialog_item_handles(&mut bus, dialog_ptr, &items);
27672
27673        let control_handle = bus.read_long(ditl_ptr + 2);
27674        let control_ptr = bus.read_long(control_handle);
27675        assert_ne!(control_handle, 0);
27676        assert_ne!(control_ptr, 0);
27677        assert_eq!(bus.read_long(dialog_ptr + 140), control_handle);
27678        assert_eq!(bus.read_long(control_ptr + 4), dialog_ptr);
27679        assert_eq!(bus.read_word(control_ptr + 8) as i16, 10);
27680        assert_eq!(bus.read_word(control_ptr + 10) as i16, 20);
27681        assert_eq!(bus.read_byte(control_ptr + 17), 0);
27682        assert_eq!(
27683            disp.dialog_control_handles.get(&control_handle),
27684            Some(&(dialog_ptr, 1))
27685        );
27686        assert_eq!(disp.dialog_control_values.get(&(dialog_ptr, 1)), Some(&0));
27687        assert_eq!(disp.control_proc_ids.get(&control_ptr), Some(&1));
27688    }
27689
27690    #[test]
27691    fn initialize_dialog_item_handles_creates_resctrl_control_handle() {
27692        let (mut disp, _cpu, mut bus) = setup();
27693        let dialog_ptr = bus.alloc(170);
27694        let items_handle = bus.alloc(4);
27695        let ditl_ptr = bus.alloc(18);
27696
27697        bus.write_long(items_handle, ditl_ptr);
27698        bus.write_long(dialog_ptr + 156, items_handle);
27699        bus.write_word(ditl_ptr, 0);
27700        bus.write_long(ditl_ptr + 2, 0);
27701        bus.write_word(ditl_ptr + 6, 10);
27702        bus.write_word(ditl_ptr + 8, 20);
27703        bus.write_word(ditl_ptr + 10, 30);
27704        bus.write_word(ditl_ptr + 12, 140);
27705        bus.write_byte(ditl_ptr + 14, 7);
27706        bus.write_byte(ditl_ptr + 15, 2);
27707        bus.write_word(ditl_ptr + 16, 128);
27708
27709        let mut cntl = Vec::new();
27710        for word in [0i16, 0, 20, 110, 2, -1, 4, 1300, 1009] {
27711            cntl.extend_from_slice(&(word as u16).to_be_bytes());
27712        }
27713        cntl.extend_from_slice(&0x1234_5678u32.to_be_bytes());
27714        cntl.push(4);
27715        cntl.extend_from_slice(b"Team");
27716        disp.install_test_resource(&mut bus, *b"CNTL", 128, &cntl);
27717
27718        let items = vec![DialogItem {
27719            item_type: 7,
27720            rect: (10, 20, 30, 140),
27721            text: String::new(),
27722            resource_id: 128,
27723            proc_ptr: 0,
27724            sel_start: 0,
27725            sel_end: 0,
27726        }];
27727
27728        disp.initialize_dialog_item_handles(&mut bus, dialog_ptr, &items);
27729
27730        let handle = bus.read_long(ditl_ptr + 2);
27731        let ctrl_ptr = bus.read_long(handle);
27732        assert_ne!(handle, 0);
27733        assert_ne!(ctrl_ptr, 0);
27734        assert_eq!(bus.read_long(dialog_ptr + 140), handle);
27735        assert_eq!(bus.read_long(ctrl_ptr + 4), dialog_ptr);
27736        assert_eq!(bus.read_word(ctrl_ptr + 8) as i16, 10);
27737        assert_eq!(bus.read_word(ctrl_ptr + 10) as i16, 20);
27738        assert_eq!(bus.read_word(ctrl_ptr + 12) as i16, 30);
27739        assert_eq!(bus.read_word(ctrl_ptr + 14) as i16, 140);
27740        assert_eq!(bus.read_word(ctrl_ptr + 18) as i16, 2);
27741        assert_eq!(bus.read_word(ctrl_ptr + 20) as i16, 1300);
27742        assert_eq!(disp.control_proc_ids.get(&ctrl_ptr), Some(&1009));
27743        assert_eq!(
27744            disp.dialog_control_handles.get(&handle),
27745            Some(&(dialog_ptr, 1))
27746        );
27747        assert_eq!(disp.dialog_control_values.get(&(dialog_ptr, 1)), Some(&2));
27748    }
27749
27750    #[test]
27751    fn popup_resctrl_initializes_contrl_data_private_menu_record() {
27752        let (mut disp, _cpu, mut bus) = setup();
27753        let dialog_ptr = bus.alloc(170);
27754        let items_handle = bus.alloc(4);
27755        let ditl_ptr = bus.alloc(18);
27756
27757        bus.write_long(items_handle, ditl_ptr);
27758        bus.write_long(dialog_ptr + 156, items_handle);
27759        bus.write_word(ditl_ptr, 0);
27760        bus.write_long(ditl_ptr + 2, 0);
27761        bus.write_word(ditl_ptr + 6, 10);
27762        bus.write_word(ditl_ptr + 8, 20);
27763        bus.write_word(ditl_ptr + 10, 30);
27764        bus.write_word(ditl_ptr + 12, 140);
27765        bus.write_byte(ditl_ptr + 14, 7);
27766        bus.write_byte(ditl_ptr + 15, 2);
27767        bus.write_word(ditl_ptr + 16, 128);
27768
27769        let mut cntl = Vec::new();
27770        for word in [0i16, 0, 20, 110, 1, -1, 0, 4000, 1009] {
27771            cntl.extend_from_slice(&(word as u16).to_be_bytes());
27772        }
27773        cntl.extend_from_slice(&0u32.to_be_bytes());
27774        cntl.push(0);
27775        disp.install_test_resource(&mut bus, *b"CNTL", 128, &cntl);
27776
27777        let items = vec![DialogItem {
27778            item_type: 7,
27779            rect: (10, 20, 30, 140),
27780            text: String::new(),
27781            resource_id: 128,
27782            proc_ptr: 0,
27783            sel_start: 0,
27784            sel_end: 0,
27785        }];
27786
27787        disp.initialize_dialog_item_handles(&mut bus, dialog_ptr, &items);
27788
27789        let ctrl_handle = bus.read_long(ditl_ptr + 2);
27790        let ctrl_ptr = bus.read_long(ctrl_handle);
27791        let private_handle = bus.read_long(ctrl_ptr + 28);
27792        let private_ptr = bus.read_long(private_handle);
27793        let menu_handle = bus.read_long(private_ptr);
27794        let menu_ptr = bus.read_long(menu_handle);
27795
27796        assert_ne!(private_handle, 0);
27797        assert_ne!(private_ptr, 0);
27798        assert_ne!(menu_handle, 0);
27799        assert_eq!(bus.read_word(private_ptr + 4) as i16, 4000);
27800        assert_eq!(bus.read_word(menu_ptr) as i16, 4000);
27801        assert_eq!(disp.popup_control_menu_id(&bus, ctrl_ptr, -1), 4000);
27802        assert!(disp
27803            .menus
27804            .iter()
27805            .any(|menu| menu.id == 4000 && menu.handle == menu_handle));
27806    }
27807
27808    #[test]
27809    fn append_ditl_overlay_preserves_existing_handles_and_initializes_resctrl() {
27810        let (mut disp, _cpu, mut bus) = setup();
27811        let dialog_ptr = bus.alloc(170);
27812        let items_handle = bus.alloc(4);
27813        let old_ditl_ptr = bus.alloc(18);
27814        let preserved_handle = 0x00A1_B2C3;
27815
27816        bus.write_long(items_handle, old_ditl_ptr);
27817        bus.write_long(dialog_ptr + 156, items_handle);
27818        bus.write_word(old_ditl_ptr, 0);
27819        bus.write_long(old_ditl_ptr + 2, preserved_handle);
27820        bus.write_word(old_ditl_ptr + 6, 10);
27821        bus.write_word(old_ditl_ptr + 8, 20);
27822        bus.write_word(old_ditl_ptr + 10, 30);
27823        bus.write_word(old_ditl_ptr + 12, 60);
27824        bus.write_byte(old_ditl_ptr + 14, 4);
27825        bus.write_byte(old_ditl_ptr + 15, 2);
27826        bus.write_bytes(old_ditl_ptr + 16, b"OK");
27827        disp.dialog_items.insert(
27828            dialog_ptr,
27829            vec![DialogItem {
27830                item_type: 4,
27831                rect: (10, 20, 30, 60),
27832                text: "OK".to_string(),
27833                resource_id: 0,
27834                proc_ptr: 0,
27835                sel_start: 0,
27836                sel_end: 0,
27837            }],
27838        );
27839
27840        let mut cntl = Vec::new();
27841        for word in [0i16, 0, 20, 110, 3, -1, 1, 5, 1008] {
27842            cntl.extend_from_slice(&(word as u16).to_be_bytes());
27843        }
27844        cntl.extend_from_slice(&0x1234_5678u32.to_be_bytes());
27845        cntl.push(4);
27846        cntl.extend_from_slice(b"Pane");
27847        disp.install_test_resource(&mut bus, *b"CNTL", 131, &cntl);
27848
27849        let append_handle = bus.alloc(4);
27850        let append_ditl_ptr = bus.alloc(18);
27851        bus.write_long(append_handle, append_ditl_ptr);
27852        bus.write_word(append_ditl_ptr, 0);
27853        bus.write_long(append_ditl_ptr + 2, 0);
27854        bus.write_word(append_ditl_ptr + 6, 40);
27855        bus.write_word(append_ditl_ptr + 8, 50);
27856        bus.write_word(append_ditl_ptr + 10, 60);
27857        bus.write_word(append_ditl_ptr + 12, 170);
27858        bus.write_byte(append_ditl_ptr + 14, 7);
27859        bus.write_byte(append_ditl_ptr + 15, 2);
27860        bus.write_word(append_ditl_ptr + 16, 131);
27861
27862        let count = disp.append_ditl_to_dialog(&mut bus, dialog_ptr, append_handle, 0);
27863
27864        assert_eq!(count, 2);
27865        let new_ditl_ptr = bus.read_long(items_handle);
27866        assert_ne!(new_ditl_ptr, old_ditl_ptr);
27867        assert_eq!(bus.read_word(new_ditl_ptr), 1);
27868        assert_eq!(
27869            bus.read_long(new_ditl_ptr + 2),
27870            preserved_handle,
27871            "AppendDITL must not recreate existing item handles"
27872        );
27873        let ctrl_handle = bus.read_long(new_ditl_ptr + 18);
27874        let ctrl_ptr = bus.read_long(ctrl_handle);
27875        assert_ne!(ctrl_handle, 0);
27876        assert_ne!(ctrl_ptr, 0);
27877        assert_eq!(bus.read_long(dialog_ptr + 140), ctrl_handle);
27878        assert_eq!(bus.read_word(ctrl_ptr + 8) as i16, 40);
27879        assert_eq!(bus.read_word(ctrl_ptr + 10) as i16, 50);
27880        assert_eq!(bus.read_word(ctrl_ptr + 12) as i16, 60);
27881        assert_eq!(bus.read_word(ctrl_ptr + 14) as i16, 170);
27882        assert_eq!(
27883            disp.dialog_control_handles.get(&ctrl_handle),
27884            Some(&(dialog_ptr, 2))
27885        );
27886        assert_eq!(disp.dialog_control_values.get(&(dialog_ptr, 2)), Some(&3));
27887    }
27888
27889    #[test]
27890    fn shorten_ditl_erases_removed_retained_item_rects_without_wiping_header_pixels() {
27891        let (mut disp, _cpu, mut bus) = setup();
27892        let screen_base = bus.alloc((400 * 300) as u32);
27893        for i in 0..400u32 * 300 {
27894            bus.write_byte(screen_base + i, 0x00);
27895        }
27896        bus.write_long(0x0824, screen_base);
27897        disp.screen_mode = (screen_base, 400, 400, 300, 8);
27898
27899        let dialog_ptr = bus.alloc(200);
27900        bus.write_word(dialog_ptr + 6, 0);
27901        bus.write_word(dialog_ptr + 8, (-100i16) as u16);
27902        bus.write_word(dialog_ptr + 10, (-100i16) as u16);
27903        bus.write_word(dialog_ptr + 16, 0);
27904        bus.write_word(dialog_ptr + 18, 0);
27905        bus.write_word(dialog_ptr + 20, 120);
27906        bus.write_word(dialog_ptr + 22, 200);
27907        let bounds = TrapDispatcher::dialog_screen_bounds(&bus, dialog_ptr);
27908
27909        let items_handle = bus.alloc(4);
27910        let ditl_ptr = bus.alloc(38);
27911        bus.write_long(items_handle, ditl_ptr);
27912        bus.write_long(dialog_ptr + 156, items_handle);
27913        bus.write_word(ditl_ptr, 1);
27914        bus.write_long(ditl_ptr + 2, 0);
27915        bus.write_word(ditl_ptr + 6, 10);
27916        bus.write_word(ditl_ptr + 8, 10);
27917        bus.write_word(ditl_ptr + 10, 30);
27918        bus.write_word(ditl_ptr + 12, 80);
27919        bus.write_byte(ditl_ptr + 14, 8);
27920        bus.write_byte(ditl_ptr + 15, 2);
27921        bus.write_bytes(ditl_ptr + 16, b"OK");
27922        bus.write_long(ditl_ptr + 20, 0);
27923        bus.write_word(ditl_ptr + 24, 40);
27924        bus.write_word(ditl_ptr + 26, 40);
27925        bus.write_word(ditl_ptr + 28, 60);
27926        bus.write_word(ditl_ptr + 30, 160);
27927        bus.write_byte(ditl_ptr + 32, 8);
27928        bus.write_byte(ditl_ptr + 33, 3);
27929        bus.write_bytes(ditl_ptr + 34, b"Old");
27930
27931        disp.dialog_items.insert(
27932            dialog_ptr,
27933            vec![
27934                DialogItem {
27935                    item_type: 8,
27936                    rect: (10, 10, 30, 80),
27937                    text: "OK".to_string(),
27938                    resource_id: 0,
27939                    proc_ptr: 0,
27940                    sel_start: 0,
27941                    sel_end: 0,
27942                },
27943                DialogItem {
27944                    item_type: 8,
27945                    rect: (40, 40, 60, 160),
27946                    text: "Old".to_string(),
27947                    resource_id: 0,
27948                    proc_ptr: 0,
27949                    sel_start: 0,
27950                    sel_end: 0,
27951                },
27952            ],
27953        );
27954
27955        disp.fill_rect_clipped_to_dialog(&mut bus, bounds, (106, 106, 114, 170), true);
27956        disp.fill_rect_clipped_to_dialog(&mut bus, bounds, (140, 140, 160, 260), true);
27957        let pixels = disp.save_dialog_pixels(&bus, bounds);
27958        disp.dialog_visible_snapshots
27959            .insert(dialog_ptr, PersistentDialogSnapshot { bounds, pixels });
27960
27961        let count = disp.shorten_ditl_in_dialog(&mut bus, dialog_ptr, 1);
27962
27963        assert_eq!(count, 1);
27964        assert_eq!(bus.read_word(ditl_ptr), 0);
27965        assert_eq!(bus.read_byte(screen_base + 110 * 400 + 120), 0xFF);
27966        assert_eq!(bus.read_byte(screen_base + 150 * 400 + 150), 0x00);
27967        let retained = disp.dialog_visible_snapshots.get(&dialog_ptr).unwrap();
27968        assert!(
27969            retained.pixels == disp.save_dialog_pixels(&bus, bounds),
27970            "ShortenDITL retained snapshot must match the erased framebuffer"
27971        );
27972    }
27973
27974    #[test]
27975    fn dialog_item_handle_addr_skips_zero_len_resctrl_resource_id() {
27976        let (mut disp, _cpu, mut bus) = setup();
27977        let dialog_ptr = bus.alloc(170);
27978        let items_handle = bus.alloc(4);
27979        let ditl_ptr = bus.alloc(36);
27980
27981        bus.write_long(items_handle, ditl_ptr);
27982        bus.write_long(dialog_ptr + 156, items_handle);
27983        bus.write_word(ditl_ptr, 1);
27984
27985        bus.write_long(ditl_ptr + 2, 0);
27986        bus.write_word(ditl_ptr + 6, 10);
27987        bus.write_word(ditl_ptr + 8, 20);
27988        bus.write_word(ditl_ptr + 10, 30);
27989        bus.write_word(ditl_ptr + 12, 140);
27990        bus.write_byte(ditl_ptr + 14, 7);
27991        bus.write_byte(ditl_ptr + 15, 0);
27992        bus.write_word(ditl_ptr + 16, 128);
27993
27994        bus.write_long(ditl_ptr + 18, 0);
27995        bus.write_word(ditl_ptr + 22, 40);
27996        bus.write_word(ditl_ptr + 24, 20);
27997        bus.write_word(ditl_ptr + 26, 52);
27998        bus.write_word(ditl_ptr + 28, 140);
27999        bus.write_byte(ditl_ptr + 30, 8);
28000        bus.write_byte(ditl_ptr + 31, 3);
28001        bus.write_bytes(ditl_ptr + 32, b"abc");
28002        bus.write_byte(ditl_ptr + 35, 0);
28003
28004        let mut cntl = Vec::new();
28005        for word in [0i16, 0, 20, 110, 2, -1, 4, 1300, 1009] {
28006            cntl.extend_from_slice(&(word as u16).to_be_bytes());
28007        }
28008        cntl.extend_from_slice(&0x1234_5678u32.to_be_bytes());
28009        cntl.push(4);
28010        cntl.extend_from_slice(b"Team");
28011        disp.install_test_resource(&mut bus, *b"CNTL", 128, &cntl);
28012
28013        let items = vec![
28014            DialogItem {
28015                item_type: 7,
28016                rect: (10, 20, 30, 140),
28017                text: String::new(),
28018                resource_id: 128,
28019                proc_ptr: 0,
28020                sel_start: 0,
28021                sel_end: 0,
28022            },
28023            DialogItem {
28024                item_type: 8,
28025                rect: (40, 20, 52, 140),
28026                text: "abc".to_string(),
28027                resource_id: 0,
28028                proc_ptr: 0,
28029                sel_start: 0,
28030                sel_end: 0,
28031            },
28032        ];
28033
28034        disp.initialize_dialog_item_handles(&mut bus, dialog_ptr, &items);
28035
28036        assert_eq!(
28037            bus.read_word(ditl_ptr + 16),
28038            128,
28039            "resCtrl resource ID must not be overwritten by the next item handle"
28040        );
28041        assert_ne!(bus.read_long(ditl_ptr + 2), 0);
28042        assert_ne!(bus.read_long(ditl_ptr + 18), 0);
28043        assert_eq!(
28044            TrapDispatcher::dialog_item_handle(&bus, dialog_ptr, 2),
28045            bus.read_long(ditl_ptr + 18)
28046        );
28047    }
28048
28049    #[test]
28050    fn draw_dialog_resctrl_uses_cntl_proc_id_not_always_popup() {
28051        let (mut disp, _cpu, mut bus) = setup();
28052        let screen_base = 0x300000u32;
28053        let row_bytes: u32 = 640;
28054        disp.set_screen_mode_for_test(screen_base, row_bytes, 640, 480, 8);
28055
28056        let dialog_ptr = bus.alloc(170);
28057        let items_handle = bus.alloc(4);
28058        let ditl_ptr = bus.alloc(18);
28059        bus.write_long(items_handle, ditl_ptr);
28060        bus.write_long(dialog_ptr + 156, items_handle);
28061        bus.write_word(ditl_ptr, 0);
28062        bus.write_long(ditl_ptr + 2, 0);
28063        bus.write_word(ditl_ptr + 6, 20);
28064        bus.write_word(ditl_ptr + 8, 20);
28065        bus.write_word(ditl_ptr + 10, 40);
28066        bus.write_word(ditl_ptr + 12, 120);
28067        bus.write_byte(ditl_ptr + 14, 7);
28068        bus.write_byte(ditl_ptr + 15, 2);
28069        bus.write_word(ditl_ptr + 16, 128);
28070
28071        let mut cntl = Vec::new();
28072        for word in [0i16, 0, 20, 100, 0, -1, 0, 0, 0] {
28073            cntl.extend_from_slice(&(word as u16).to_be_bytes());
28074        }
28075        cntl.extend_from_slice(&0u32.to_be_bytes());
28076        cntl.push(2);
28077        cntl.extend_from_slice(b"OK");
28078        disp.install_test_resource(&mut bus, *b"CNTL", 128, &cntl);
28079
28080        let items = vec![DialogItem {
28081            item_type: 7,
28082            rect: (20, 20, 40, 120),
28083            text: String::new(),
28084            resource_id: 128,
28085            proc_ptr: 0,
28086            sel_start: 0,
28087            sel_end: 0,
28088        }];
28089        disp.initialize_dialog_item_handles(&mut bus, dialog_ptr, &items);
28090
28091        disp.draw_dialog(
28092            &mut bus,
28093            (100, 100, 180, 260),
28094            3,
28095            "",
28096            &items,
28097            0,
28098            "",
28099            0,
28100            false,
28101            dialog_ptr,
28102        );
28103
28104        let old_popup_arrow_pixel = screen_base + 130 * row_bytes + 210;
28105        assert_ne!(
28106            bus.read_byte(old_popup_arrow_pixel),
28107            0xFF,
28108            "push-button resCtrl must not draw popup-menu arrow pixels"
28109        );
28110    }
28111
28112    #[test]
28113    fn show_dialog_item_updates_live_resctrl_control_rect() {
28114        let (mut disp, mut cpu, mut bus) = setup();
28115        let dialog_ptr = bus.alloc(170);
28116        let items_handle = bus.alloc(4);
28117        let ditl_ptr = bus.alloc(18);
28118        let hidden_rect = (20, 20 + 16384, 40, 120 + 16384);
28119        let visible_rect = (20, 20, 40, 120);
28120
28121        bus.write_long(items_handle, ditl_ptr);
28122        bus.write_long(dialog_ptr + 156, items_handle);
28123        bus.write_word(ditl_ptr, 0);
28124        bus.write_long(ditl_ptr + 2, 0);
28125        bus.write_word(ditl_ptr + 6, hidden_rect.0 as u16);
28126        bus.write_word(ditl_ptr + 8, hidden_rect.1 as u16);
28127        bus.write_word(ditl_ptr + 10, hidden_rect.2 as u16);
28128        bus.write_word(ditl_ptr + 12, hidden_rect.3 as u16);
28129        bus.write_byte(ditl_ptr + 14, 7);
28130        bus.write_byte(ditl_ptr + 15, 2);
28131        bus.write_word(ditl_ptr + 16, 128);
28132
28133        let mut cntl = Vec::new();
28134        for word in [0i16, 0, 20, 100, 0, -1, 0, 0, 0] {
28135            cntl.extend_from_slice(&(word as u16).to_be_bytes());
28136        }
28137        cntl.extend_from_slice(&0u32.to_be_bytes());
28138        cntl.push(2);
28139        cntl.extend_from_slice(b"OK");
28140        disp.install_test_resource(&mut bus, *b"CNTL", 128, &cntl);
28141
28142        let items = vec![DialogItem {
28143            item_type: 7,
28144            rect: hidden_rect,
28145            text: String::new(),
28146            resource_id: 128,
28147            proc_ptr: 0,
28148            sel_start: 0,
28149            sel_end: 0,
28150        }];
28151        disp.initialize_dialog_item_handles(&mut bus, dialog_ptr, &items);
28152        disp.dialog_items.insert(dialog_ptr, items);
28153        disp.hidden_dialog_item_rects
28154            .insert((dialog_ptr, 1), visible_rect);
28155
28156        bus.write_word(TEST_SP, 1);
28157        bus.write_long(TEST_SP + 2, dialog_ptr);
28158        disp.dispatch_dialog(true, 0x028, &mut cpu, &mut bus)
28159            .unwrap()
28160            .unwrap();
28161
28162        let ctrl_handle = disp.dialog_control_handle_for_item(dialog_ptr, 1).unwrap();
28163        let ctrl_ptr = bus.read_long(ctrl_handle);
28164        assert_eq!(bus.read_word(ctrl_ptr + 8) as i16, visible_rect.0);
28165        assert_eq!(bus.read_word(ctrl_ptr + 10) as i16, visible_rect.1);
28166        assert_eq!(bus.read_word(ctrl_ptr + 12) as i16, visible_rect.2);
28167        assert_eq!(bus.read_word(ctrl_ptr + 14) as i16, visible_rect.3);
28168    }
28169
28170    #[test]
28171    fn parse_ditl_preserves_user_item_proc_ptr() {
28172        let (_disp, _cpu, mut bus) = setup();
28173        let ditl_ptr = bus.alloc(16);
28174
28175        bus.write_word(ditl_ptr, 0);
28176        bus.write_long(ditl_ptr + 2, 0x12345678);
28177        bus.write_word(ditl_ptr + 6, 10);
28178        bus.write_word(ditl_ptr + 8, 20);
28179        bus.write_word(ditl_ptr + 10, 30);
28180        bus.write_word(ditl_ptr + 12, 40);
28181        bus.write_byte(ditl_ptr + 14, 0);
28182        bus.write_byte(ditl_ptr + 15, 0);
28183
28184        let items = TrapDispatcher::parse_ditl(&bus, ditl_ptr, 16);
28185        assert_eq!(items.len(), 1);
28186        assert_eq!(items[0].proc_ptr, 0x12345678);
28187    }
28188
28189    #[test]
28190    fn initialize_dialog_item_handles_preserves_user_item_proc_ptr() {
28191        let (mut disp, _cpu, mut bus) = setup();
28192        let dialog_ptr = bus.alloc(170);
28193        let items_handle = bus.alloc(4);
28194        let ditl_ptr = bus.alloc(16);
28195
28196        bus.write_long(items_handle, ditl_ptr);
28197        bus.write_long(dialog_ptr + 156, items_handle);
28198        bus.write_word(ditl_ptr, 0);
28199        bus.write_long(ditl_ptr + 2, 0);
28200        bus.write_word(ditl_ptr + 6, 10);
28201        bus.write_word(ditl_ptr + 8, 20);
28202        bus.write_word(ditl_ptr + 10, 30);
28203        bus.write_word(ditl_ptr + 12, 40);
28204        bus.write_byte(ditl_ptr + 14, 0);
28205        bus.write_byte(ditl_ptr + 15, 0);
28206
28207        let items = vec![DialogItem {
28208            item_type: 0,
28209            rect: (10, 20, 30, 40),
28210            text: String::new(),
28211            resource_id: 0,
28212            proc_ptr: 0x12345678,
28213            sel_start: 0,
28214            sel_end: 0,
28215        }];
28216
28217        disp.initialize_dialog_item_handles(&mut bus, dialog_ptr, &items);
28218
28219        assert_eq!(bus.read_long(ditl_ptr + 2), 0x12345678);
28220    }
28221
28222    #[test]
28223    fn refresh_ditl_proc_ptrs_clears_stale_user_item_proc_ptr() {
28224        let (_disp, _cpu, mut bus) = setup();
28225        let dialog_ptr = bus.alloc(170);
28226        let items_handle = bus.alloc(4);
28227        let ditl_ptr = bus.alloc(16);
28228
28229        bus.write_long(items_handle, ditl_ptr);
28230        bus.write_long(dialog_ptr + 156, items_handle);
28231        bus.write_word(ditl_ptr, 0);
28232        bus.write_long(ditl_ptr + 2, 0);
28233        bus.write_word(ditl_ptr + 6, 10);
28234        bus.write_word(ditl_ptr + 8, 20);
28235        bus.write_word(ditl_ptr + 10, 30);
28236        bus.write_word(ditl_ptr + 12, 40);
28237        bus.write_byte(ditl_ptr + 14, 0);
28238        bus.write_byte(ditl_ptr + 15, 0);
28239
28240        let mut items = vec![DialogItem {
28241            item_type: 0,
28242            rect: (10, 20, 30, 40),
28243            text: String::new(),
28244            resource_id: 0,
28245            proc_ptr: 0x12345678,
28246            sel_start: 0,
28247            sel_end: 0,
28248        }];
28249
28250        TrapDispatcher::refresh_ditl_proc_ptrs(&bus, dialog_ptr, &mut items);
28251
28252        assert_eq!(items[0].proc_ptr, 0);
28253    }
28254
28255    // ---- ModalDialog ($A991) ----
28256
28257    #[test]
28258    fn modal_dialog_writes_item_hit_and_pops_8_bytes() {
28259        let (mut disp, mut cpu, mut bus) = setup();
28260
28261        let item_hit_addr = 0x300000u32;
28262        bus.write_word(item_hit_addr, 0); // pre-clear
28263
28264        // SP+0: item_hit_ptr (4), SP+4: filterProc (4)
28265        bus.write_long(TEST_SP, item_hit_addr);
28266        bus.write_long(TEST_SP + 4, 0); // nil filterProc
28267
28268        let result = disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus);
28269        assert!(result.unwrap().is_ok());
28270        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
28271        assert_eq!(bus.read_word(item_hit_addr), 1);
28272    }
28273
28274    #[test]
28275    fn modal_dialog_retained_edit_text_appends_across_reentry_and_command_a_selects() {
28276        let (mut disp, mut cpu, mut bus) = setup_with_port();
28277        let screen_base = bus.alloc(320 * 240);
28278        for offset in 0..320u32 * 240 {
28279            bus.write_byte(screen_base + offset, 0xFF);
28280        }
28281        bus.write_long(0x0824, screen_base);
28282        disp.set_screen_mode_for_test(screen_base, 320, 320, 240, 8);
28283
28284        let dialog_ptr = bus.alloc(256);
28285        let item_hit_addr = 0x300000u32;
28286        let bounds = (40, 40, 130, 280);
28287        disp.front_window = dialog_ptr;
28288        disp.current_port = dialog_ptr;
28289        disp.window_bounds = bounds;
28290        disp.window_proc_id = 2;
28291        disp.window_title.clear();
28292        disp.window_list = vec![dialog_ptr];
28293        bus.write_word(dialog_ptr + 108, 2);
28294        bus.write_word(dialog_ptr + 164, 0);
28295        bus.write_word(dialog_ptr + 168, 0);
28296        disp.dialog_items.insert(
28297            dialog_ptr,
28298            vec![DialogItem {
28299                item_type: 16,
28300                rect: (24, 24, 42, 190),
28301                text: String::new(),
28302                resource_id: 0,
28303                proc_ptr: 0,
28304                sel_start: 0,
28305                sel_end: 0,
28306            }],
28307        );
28308
28309        fn enter_modal(
28310            disp: &mut TrapDispatcher,
28311            cpu: &mut MockCpu,
28312            bus: &mut MacMemoryBus,
28313            item_hit_addr: u32,
28314        ) {
28315            bus.write_word(item_hit_addr, 0xCAFE);
28316            bus.write_long(TEST_SP, item_hit_addr);
28317            bus.write_long(TEST_SP + 4, 0);
28318            cpu.write_reg(Register::A7, TEST_SP);
28319            disp.dispatch_dialog(true, 0x191, cpu, bus)
28320                .unwrap()
28321                .unwrap();
28322            assert!(disp.dialog_tracking.is_some());
28323            assert_eq!(cpu.read_reg(Register::A7), TEST_SP);
28324        }
28325
28326        fn key_down(
28327            disp: &mut TrapDispatcher,
28328            cpu: &mut MockCpu,
28329            bus: &mut MacMemoryBus,
28330            item_hit_addr: u32,
28331            key_code: u8,
28332            char_code: u8,
28333            modifiers: u16,
28334        ) {
28335            disp.event_queue
28336                .push_back(crate::trap::dispatch::QueuedEvent {
28337                    what: 3,
28338                    message: (u32::from(key_code) << 8) | u32::from(char_code),
28339                    where_v: 0,
28340                    where_h: 0,
28341                    modifiers,
28342                });
28343            disp.dispatch_dialog(true, 0x191, cpu, bus)
28344                .unwrap()
28345                .unwrap();
28346            assert!(disp.dialog_tracking.is_none());
28347            assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
28348            assert_eq!(bus.read_word(item_hit_addr), 1);
28349        }
28350
28351        for (key_code, char_code) in [(0x02, b'd'), (0x20, b'u'), (0x02, b'd')] {
28352            enter_modal(&mut disp, &mut cpu, &mut bus, item_hit_addr);
28353            key_down(
28354                &mut disp,
28355                &mut cpu,
28356                &mut bus,
28357                item_hit_addr,
28358                key_code,
28359                char_code,
28360                0,
28361            );
28362        }
28363
28364        let item = &disp.dialog_items[&dialog_ptr][0];
28365        assert_eq!(item.text, "dud");
28366        assert_eq!((item.sel_start, item.sel_end), (3, 3));
28367        assert!(disp
28368            .dialog_edit_text_modified_items
28369            .contains(&(dialog_ptr, 1)));
28370
28371        enter_modal(&mut disp, &mut cpu, &mut bus, item_hit_addr);
28372        key_down(
28373            &mut disp,
28374            &mut cpu,
28375            &mut bus,
28376            item_hit_addr,
28377            0x00,
28378            b'a',
28379            0x0100,
28380        );
28381        let item = &disp.dialog_items[&dialog_ptr][0];
28382        assert_eq!(item.text, "dud");
28383        assert_eq!((item.sel_start, item.sel_end), (0, 3));
28384        assert!(!disp
28385            .dialog_edit_text_modified_items
28386            .contains(&(dialog_ptr, 1)));
28387
28388        enter_modal(&mut disp, &mut cpu, &mut bus, item_hit_addr);
28389        key_down(&mut disp, &mut cpu, &mut bus, item_hit_addr, 0x33, 0x08, 0);
28390        let item = &disp.dialog_items[&dialog_ptr][0];
28391        assert_eq!(item.text, "");
28392        assert_eq!((item.sel_start, item.sel_end), (0, 0));
28393    }
28394
28395    #[test]
28396    fn modal_dialog_popup_resctrl_tracks_selection_and_returns_item() {
28397        let (mut disp, mut cpu, mut bus) = setup();
28398        let screen_base = bus.alloc((800 * 600) as u32);
28399        for i in 0..800u32 * 600 {
28400            bus.write_byte(screen_base + i, 0xFF);
28401        }
28402        bus.write_long(0x0824, screen_base);
28403        disp.screen_mode = (screen_base, 800, 800, 600, 8);
28404
28405        let dialog_ptr = bus.alloc(256);
28406        bus.write_word(dialog_ptr + 6, 0);
28407        bus.write_word(dialog_ptr + 8, (-100i16) as u16);
28408        bus.write_word(dialog_ptr + 10, (-100i16) as u16);
28409        bus.write_word(dialog_ptr + 16, 0);
28410        bus.write_word(dialog_ptr + 18, 0);
28411        bus.write_word(dialog_ptr + 20, 180);
28412        bus.write_word(dialog_ptr + 22, 220);
28413        let dialog_bounds = (100, 100, 280, 320);
28414
28415        let ctrl_ptr = bus.alloc(296);
28416        let ctrl_handle = bus.alloc(4);
28417        bus.write_long(ctrl_handle, ctrl_ptr);
28418        disp.initialize_control_record(
28419            &mut bus,
28420            ctrl_ptr,
28421            dialog_ptr,
28422            (10, 20, 30, 130),
28423            b"",
28424            true,
28425            1,
28426            900,
28427            0,
28428            1009,
28429            0,
28430        );
28431        disp.dialog_control_handles
28432            .insert(ctrl_handle, (dialog_ptr, 1));
28433        disp.dialog_control_values.insert((dialog_ptr, 1), 1);
28434        disp.menus.push(Menu {
28435            id: 900,
28436            title: "Squadies".to_string(),
28437            items: vec![
28438                MenuItem {
28439                    text: "Duke".to_string(),
28440                    icon: 0,
28441                    key_equiv: 0,
28442                    mark: 0,
28443                    style: 0,
28444                    enabled: true,
28445                },
28446                MenuItem {
28447                    text: "Carnage".to_string(),
28448                    icon: 0,
28449                    key_equiv: 0,
28450                    mark: 0,
28451                    style: 0,
28452                    enabled: true,
28453                },
28454            ],
28455            enabled: true,
28456            handle: 0,
28457            in_menu_bar: false,
28458            hierarchical: false,
28459            visible_in_menu_bar: false,
28460        });
28461
28462        let item_hit_addr = 0x300000u32;
28463        bus.write_word(item_hit_addr, 0);
28464        cpu.write_reg(Register::A7, TEST_SP);
28465        disp.dialog_tracking = Some(crate::trap::dispatch::DialogTrackingState {
28466            dialog_ptr,
28467            bounds: dialog_bounds,
28468            title: String::new(),
28469            proc_id: 2,
28470            items: vec![DialogItem {
28471                item_type: 7,
28472                rect: (10, 20, 30, 130),
28473                text: String::new(),
28474                resource_id: 0,
28475                proc_ptr: 0,
28476                sel_start: 0,
28477                sel_end: 0,
28478            }],
28479            default_item: 0,
28480            cancel_item: 0,
28481            edit_text: String::new(),
28482            edit_item: 0,
28483            saved_pixels: Vec::new(),
28484            stack_ptr: TEST_SP,
28485            item_hit_ptr: item_hit_addr,
28486            rendered_pixels: Vec::new(),
28487            flash_remaining: 0,
28488            flash_delay: 0,
28489            flash_item: 0,
28490            edit_text_modified: false,
28491            draw_proc_queue: VecDeque::new(),
28492            draw_procs_done: true,
28493            rendered_pixels_final: true,
28494            filter_proc: 0,
28495            game_managed: false,
28496            last_filter_event: None,
28497            popup_draws: Vec::new(),
28498            active_popup: None,
28499            active_button: None,
28500            active_user_item: None,
28501        });
28502        disp.mouse_button = true;
28503        disp.mouse_pos = (115, 125);
28504        disp.event_queue
28505            .push_back(crate::trap::dispatch::QueuedEvent {
28506                what: 1,
28507                message: 0,
28508                where_v: 115,
28509                where_h: 125,
28510                modifiers: 0,
28511            });
28512
28513        disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus)
28514            .unwrap()
28515            .unwrap();
28516        assert!(disp
28517            .dialog_tracking
28518            .as_ref()
28519            .and_then(|tracking| tracking.active_popup.as_ref())
28520            .is_some());
28521        assert_eq!(cpu.read_reg(Register::A7), TEST_SP);
28522        let stale_open_popup_pixels = disp.save_dialog_pixels(&bus, dialog_bounds);
28523        let tracking = disp.dialog_tracking.as_mut().unwrap();
28524        tracking.rendered_pixels = stale_open_popup_pixels.clone();
28525        tracking.rendered_pixels_final = true;
28526
28527        let (dropdown_top, dropdown_left, _, _) = disp
28528            .dialog_tracking
28529            .as_ref()
28530            .and_then(|tracking| tracking.active_popup.as_ref())
28531            .map(|popup| popup.dropdown_rect)
28532            .expect("popup tracking should expose the live dropdown rect");
28533        disp.mouse_pos = (dropdown_top + 1 + 16 + 1, dropdown_left + 5);
28534        disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus)
28535            .unwrap()
28536            .unwrap();
28537        assert_eq!(
28538            disp.dialog_tracking
28539                .as_ref()
28540                .and_then(|tracking| tracking.active_popup.as_ref())
28541                .map(|popup| popup.highlighted_item),
28542            Some(2)
28543        );
28544
28545        disp.mouse_button = false;
28546        disp.event_queue
28547            .push_back(crate::trap::dispatch::QueuedEvent {
28548                what: 2,
28549                message: 0,
28550                where_v: 148,
28551                where_h: 125,
28552                modifiers: 0,
28553            });
28554        disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus)
28555            .unwrap()
28556            .unwrap();
28557
28558        assert!(disp.dialog_tracking.is_none());
28559        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
28560        assert_eq!(bus.read_word(item_hit_addr), 1);
28561        assert_eq!(bus.read_word(ctrl_ptr + 18) as i16, 2);
28562        assert_eq!(disp.dialog_control_values.get(&(dialog_ptr, 1)), Some(&2));
28563
28564        let retained = disp.dialog_visible_snapshots.get(&dialog_ptr).unwrap();
28565        let current_pixels = disp.save_dialog_pixels(&bus, dialog_bounds);
28566        assert!(
28567            retained.pixels == current_pixels,
28568            "retained snapshot must match the closed popup framebuffer"
28569        );
28570        assert!(
28571            retained.pixels != stale_open_popup_pixels,
28572            "retained snapshot kept stale open-popup pixels"
28573        );
28574    }
28575
28576    #[test]
28577    fn modal_dialog_game_managed_dialog_still_queues_user_item_draw_procs() {
28578        let (mut disp, mut cpu, mut bus) = setup();
28579        let dialog_ptr = 0x200000u32;
28580        let item_hit_addr = 0x300000u32;
28581
28582        disp.front_window = dialog_ptr;
28583        disp.window_bounds = (92, 95, 415, 704);
28584        disp.window_proc_id = 2;
28585        disp.window_title.clear();
28586        disp.dialog_items.insert(
28587            dialog_ptr,
28588            vec![
28589                DialogItem {
28590                    item_type: 0,
28591                    rect: (8, 353, 148, 603),
28592                    text: String::new(),
28593                    resource_id: 0,
28594                    proc_ptr: 0x500000,
28595                    sel_start: 0,
28596                    sel_end: 0,
28597                },
28598                DialogItem {
28599                    item_type: 0x80,
28600                    rect: (159, 381, 184, 581),
28601                    text: String::new(),
28602                    resource_id: 0,
28603                    proc_ptr: 0x500100,
28604                    sel_start: 0,
28605                    sel_end: 0,
28606                },
28607                DialogItem {
28608                    item_type: 0x80,
28609                    rect: (324, 650, 349, 771),
28610                    text: String::new(),
28611                    resource_id: 0,
28612                    proc_ptr: 0x500200,
28613                    sel_start: 0,
28614                    sel_end: 0,
28615                },
28616            ],
28617        );
28618
28619        bus.write_long(TEST_SP, item_hit_addr);
28620        bus.write_long(TEST_SP + 4, 0x149F0);
28621
28622        let result = disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus);
28623        assert!(result.unwrap().is_ok());
28624        assert_eq!(cpu.read_reg(Register::A7), TEST_SP);
28625
28626        let tracking = disp.dialog_tracking.as_ref().unwrap();
28627        assert!(tracking.game_managed);
28628        assert_eq!(tracking.filter_proc, 0x149F0);
28629        assert_eq!(tracking.draw_proc_queue.len(), 2);
28630        assert_eq!(
28631            tracking
28632                .draw_proc_queue
28633                .iter()
28634                .map(|(_, item_no)| *item_no)
28635                .collect::<Vec<_>>(),
28636            vec![1, 2]
28637        );
28638        assert!(!tracking.draw_procs_done);
28639        assert!(!tracking.rendered_pixels_final);
28640    }
28641
28642    #[test]
28643    fn modal_dialog_retained_reentry_reuses_visible_snapshot_without_user_item_redraw() {
28644        let (mut disp, mut cpu, mut bus) = setup();
28645        let dialog_ptr = 0x200000u32;
28646        let item_hit_addr = 0x300000u32;
28647        let screen_base = bus.alloc((64 * 64) as u32);
28648        let row_bytes = 64u32;
28649        for i in 0..row_bytes * 64 {
28650            bus.write_byte(screen_base + i, 0x11);
28651        }
28652        bus.write_long(0x0824, screen_base);
28653        disp.screen_mode = (screen_base, row_bytes, 64, 64, 8);
28654
28655        let bounds = (10, 12, 30, 34);
28656        let snapshot_width = (bounds.3 - bounds.1 + TrapDispatcher::DBOX_FRAME_MARGIN * 2) as usize;
28657        let snapshot_height =
28658            (bounds.2 - bounds.0 + TrapDispatcher::DBOX_FRAME_MARGIN * 2) as usize;
28659        let visible_pixels = vec![0x44; snapshot_width * snapshot_height];
28660        disp.dialog_visible_snapshots.insert(
28661            dialog_ptr,
28662            PersistentDialogSnapshot {
28663                bounds,
28664                pixels: visible_pixels,
28665            },
28666        );
28667        disp.dialog_saved_pixels
28668            .insert(dialog_ptr, vec![0x22; snapshot_width * snapshot_height]);
28669        disp.dialog_modal_entered.insert(dialog_ptr);
28670
28671        disp.front_window = dialog_ptr;
28672        disp.window_bounds = bounds;
28673        disp.window_proc_id = 1;
28674        disp.window_title.clear();
28675        disp.dialog_items.insert(
28676            dialog_ptr,
28677            vec![DialogItem {
28678                item_type: 0x80,
28679                rect: (0, 0, 20, 22),
28680                text: String::new(),
28681                resource_id: 0,
28682                proc_ptr: 0x500000,
28683                sel_start: 0,
28684                sel_end: 0,
28685            }],
28686        );
28687
28688        bus.write_long(TEST_SP, item_hit_addr);
28689        bus.write_long(TEST_SP + 4, 0);
28690
28691        let result = disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus);
28692        assert!(result.unwrap().is_ok());
28693        assert_eq!(cpu.read_reg(Register::A7), TEST_SP);
28694
28695        let tracking = disp.dialog_tracking.as_ref().unwrap();
28696        assert!(tracking.draw_proc_queue.is_empty());
28697        assert!(tracking.draw_procs_done);
28698        assert!(tracking.rendered_pixels_final);
28699        assert_eq!(
28700            bus.read_byte(screen_base + 20 * row_bytes + 20),
28701            0x44,
28702            "retained visible snapshot should be restored on re-entry"
28703        );
28704    }
28705
28706    #[test]
28707    fn modal_dialog_first_entry_with_showwindow_snapshot_still_queues_user_item_draw_proc() {
28708        let (mut disp, mut cpu, mut bus) = setup();
28709        let dialog_ptr = 0x200000u32;
28710        let item_hit_addr = 0x300000u32;
28711        let screen_base = bus.alloc((64 * 64) as u32);
28712        let row_bytes = 64u32;
28713        bus.write_long(0x0824, screen_base);
28714        disp.screen_mode = (screen_base, row_bytes, 64, 64, 8);
28715
28716        let bounds = (10, 12, 30, 34);
28717        let snapshot_width = (bounds.3 - bounds.1 + TrapDispatcher::DBOX_FRAME_MARGIN * 2) as usize;
28718        let snapshot_height =
28719            (bounds.2 - bounds.0 + TrapDispatcher::DBOX_FRAME_MARGIN * 2) as usize;
28720        disp.dialog_visible_snapshots.insert(
28721            dialog_ptr,
28722            PersistentDialogSnapshot {
28723                bounds,
28724                pixels: vec![0x44; snapshot_width * snapshot_height],
28725            },
28726        );
28727
28728        disp.front_window = dialog_ptr;
28729        disp.window_bounds = bounds;
28730        disp.window_proc_id = 1;
28731        disp.window_title.clear();
28732        disp.dialog_items.insert(
28733            dialog_ptr,
28734            vec![DialogItem {
28735                item_type: 0x80,
28736                rect: (0, 0, 20, 22),
28737                text: String::new(),
28738                resource_id: 0,
28739                proc_ptr: 0x500000,
28740                sel_start: 0,
28741                sel_end: 0,
28742            }],
28743        );
28744
28745        bus.write_long(TEST_SP, item_hit_addr);
28746        bus.write_long(TEST_SP + 4, 0);
28747
28748        let result = disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus);
28749        assert!(result.unwrap().is_ok());
28750
28751        let tracking = disp.dialog_tracking.as_ref().unwrap();
28752        assert_eq!(tracking.draw_proc_queue.len(), 1);
28753        assert_eq!(tracking.draw_proc_queue[0], (0x500000, 1));
28754        assert!(!tracking.draw_procs_done);
28755        assert!(!tracking.rendered_pixels_final);
28756    }
28757
28758    #[test]
28759    fn dialog_game_managed_ignores_offscreen_placeholder_items() {
28760        let bounds = (200, 146, 400, 510);
28761        let items = vec![
28762            DialogItem {
28763                item_type: 0,
28764                rect: (167, 279, 192, 357),
28765                text: String::new(),
28766                resource_id: 0,
28767                proc_ptr: 0,
28768                sel_start: 0,
28769                sel_end: 0,
28770            },
28771            DialogItem {
28772                item_type: 0xC0, // disabled picture placeholder outside bounds
28773                rect: (217, 179, 247, 247),
28774                text: String::new(),
28775                resource_id: 1431,
28776                proc_ptr: 0,
28777                sel_start: 0,
28778                sel_end: 0,
28779            },
28780            DialogItem {
28781                item_type: 0x80, // disabled userItem inside bounds
28782                rect: (7, 8, 157, 358),
28783                text: String::new(),
28784                resource_id: 0,
28785                proc_ptr: 0,
28786                sel_start: 0,
28787                sel_end: 0,
28788            },
28789        ];
28790
28791        assert!(TrapDispatcher::dialog_is_game_managed(bounds, &items));
28792
28793        let mut standard_items = items;
28794        standard_items.push(DialogItem {
28795            item_type: 8,
28796            rect: (20, 20, 40, 100),
28797            text: "Standard".to_string(),
28798            resource_id: 0,
28799            proc_ptr: 0,
28800            sel_start: 0,
28801            sel_end: 0,
28802        });
28803        assert!(!TrapDispatcher::dialog_is_game_managed(
28804            bounds,
28805            &standard_items
28806        ));
28807    }
28808
28809    #[test]
28810    fn redraw_dialog_window_contents_does_not_snapshot_game_managed_shell() {
28811        let (mut disp, _cpu, mut bus) = setup();
28812        let dialog_ptr = 0x200000u32;
28813        let screen_base = bus.alloc(64 * 64);
28814        for offset in 0..64u32 * 64 {
28815            bus.write_byte(screen_base + offset, 0x11);
28816        }
28817        bus.write_long(0x0824, screen_base);
28818        disp.screen_mode = (screen_base, 64, 64, 64, 8);
28819
28820        bus.write_long(dialog_ptr + 2, screen_base);
28821        bus.write_word(dialog_ptr + 6, 64);
28822        bus.write_word(dialog_ptr + 8, 0);
28823        bus.write_word(dialog_ptr + 10, 0);
28824        bus.write_word(dialog_ptr + 12, 20);
28825        bus.write_word(dialog_ptr + 14, 20);
28826        bus.write_word(dialog_ptr + 16, 0);
28827        bus.write_word(dialog_ptr + 18, 0);
28828        bus.write_word(dialog_ptr + 20, 20);
28829        bus.write_word(dialog_ptr + 22, 20);
28830        bus.write_word(dialog_ptr + 108, 2);
28831        bus.write_byte(dialog_ptr + 110, 0xFF);
28832        disp.window_proc_ids.insert(dialog_ptr, 2);
28833        disp.dialog_items.insert(
28834            dialog_ptr,
28835            vec![DialogItem {
28836                item_type: 0x80,
28837                rect: (2, 2, 10, 10),
28838                text: String::new(),
28839                resource_id: 0,
28840                proc_ptr: 0,
28841                sel_start: 0,
28842                sel_end: 0,
28843            }],
28844        );
28845        disp.dialog_visible_snapshots.insert(
28846            dialog_ptr,
28847            PersistentDialogSnapshot {
28848                bounds: (0, 0, 20, 20),
28849                pixels: vec![0x44; 30 * 30],
28850            },
28851        );
28852
28853        disp.redraw_dialog_window_contents(&mut bus, dialog_ptr);
28854
28855        assert!(
28856            !disp.dialog_visible_snapshots.contains_key(&dialog_ptr),
28857            "ShowWindow redraw must not retain a stale shell for app-drawn all-userItem dialogs"
28858        );
28859    }
28860
28861    #[test]
28862    fn modal_dialog_resnapshot_redraws_popup_controls_after_user_item_draw_procs() {
28863        let (mut disp, mut cpu, mut bus) = setup();
28864        let screen_base = bus.alloc((800 * 600) as u32);
28865        for i in 0..800u32 * 600 {
28866            bus.write_byte(screen_base + i, 0xFF);
28867        }
28868        bus.write_long(0x0824, screen_base);
28869        disp.screen_mode = (screen_base, 800, 800, 600, 8);
28870
28871        disp.dialog_tracking = Some(crate::trap::dispatch::DialogTrackingState {
28872            dialog_ptr: 0x200000,
28873            bounds: (0, 0, 100, 220),
28874            title: String::new(),
28875            proc_id: 2,
28876            items: Vec::new(),
28877            default_item: 0,
28878            cancel_item: 0,
28879            edit_text: String::new(),
28880            edit_item: 0,
28881            saved_pixels: Vec::new(),
28882            stack_ptr: TEST_SP,
28883            item_hit_ptr: 0,
28884            rendered_pixels: Vec::new(),
28885            flash_remaining: 0,
28886            flash_delay: 0,
28887            flash_item: 0,
28888            edit_text_modified: false,
28889            draw_proc_queue: VecDeque::new(),
28890            draw_procs_done: true,
28891            rendered_pixels_final: false,
28892            filter_proc: 0,
28893            game_managed: false,
28894            last_filter_event: None,
28895            popup_draws: vec![DialogPopupDraw {
28896                rect: (20, 30, 42, 180),
28897                title: String::new(),
28898                enabled: true,
28899                pressed: false,
28900            }],
28901            active_popup: None,
28902            active_button: None,
28903            active_user_item: None,
28904        });
28905
28906        let result = disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus);
28907        assert!(result.unwrap().is_ok());
28908
28909        let interior = screen_base + 30 * 800 + 80;
28910        assert_eq!(
28911            bus.read_byte(interior),
28912            0,
28913            "popup interior must be redrawn over the guest userItem pixels"
28914        );
28915        assert!(
28916            disp.dialog_tracking
28917                .as_ref()
28918                .is_some_and(|tracking| tracking.rendered_pixels_final),
28919            "ModalDialog should re-snapshot after popup redraw"
28920        );
28921    }
28922
28923    #[test]
28924    fn modal_dialog_popup_resnapshot_preserves_theme_pressed_and_inactive_states() {
28925        let (mut disp, mut cpu, mut bus) = setup();
28926        disp.set_ui_theme_id(UiThemeId::SystemlessDefault);
28927
28928        let row_bytes = 64u32;
28929        let screen_base = bus.alloc(row_bytes * 160);
28930        for i in 0..row_bytes * 160 {
28931            bus.write_byte(screen_base + i, 0);
28932        }
28933        disp.set_screen_mode_for_test(screen_base, row_bytes, 512, 160, 1);
28934        cpu.write_reg(Register::A7, TEST_SP);
28935
28936        disp.dialog_tracking = Some(crate::trap::dispatch::DialogTrackingState {
28937            dialog_ptr: 0x200000,
28938            bounds: (0, 0, 120, 220),
28939            title: String::new(),
28940            proc_id: 2,
28941            items: Vec::new(),
28942            default_item: 0,
28943            cancel_item: 0,
28944            edit_text: String::new(),
28945            edit_item: 0,
28946            saved_pixels: Vec::new(),
28947            stack_ptr: TEST_SP,
28948            item_hit_ptr: 0,
28949            rendered_pixels: Vec::new(),
28950            flash_remaining: 0,
28951            flash_delay: 0,
28952            flash_item: 0,
28953            edit_text_modified: false,
28954            draw_proc_queue: VecDeque::new(),
28955            draw_procs_done: true,
28956            rendered_pixels_final: false,
28957            filter_proc: 0,
28958            game_managed: false,
28959            last_filter_event: None,
28960            popup_draws: vec![
28961                DialogPopupDraw {
28962                    rect: (20, 30, 42, 180),
28963                    title: String::new(),
28964                    enabled: true,
28965                    pressed: true,
28966                },
28967                DialogPopupDraw {
28968                    rect: (60, 30, 82, 180),
28969                    title: String::new(),
28970                    enabled: false,
28971                    pressed: false,
28972                },
28973            ],
28974            active_popup: None,
28975            active_button: None,
28976            active_user_item: None,
28977        });
28978
28979        let result = disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus);
28980        assert!(result.unwrap().is_ok());
28981
28982        assert!(
28983            screen_pixel_is_set(&bus, screen_base, row_bytes, 35, 25),
28984            "pressed popup redraw should preserve the provider pressed fill"
28985        );
28986        assert!(
28987            screen_pixel_is_set(&bus, screen_base, row_bytes, 32, 62),
28988            "inactive popup redraw should preserve the provider inactive frame"
28989        );
28990        assert!(
28991            disp.dialog_tracking
28992                .as_ref()
28993                .is_some_and(|tracking| tracking.rendered_pixels_final),
28994            "ModalDialog should re-snapshot after stateful popup redraw"
28995        );
28996    }
28997
28998    #[test]
28999    fn modal_dialog_resnapshot_blits_dialog_port_after_user_item_draw_procs() {
29000        let (mut disp, mut cpu, mut bus) = setup();
29001        let screen_base = bus.alloc((32 * 32) as u32);
29002        for i in 0..32u32 * 32 {
29003            bus.write_byte(screen_base + i, 0xFF);
29004        }
29005        bus.write_long(0x0824, screen_base);
29006        disp.screen_mode = (screen_base, 32, 32, 32, 8);
29007
29008        let dialog_ptr = bus.alloc(64);
29009        let offscreen_base = bus.alloc(20 * 20);
29010        for i in 0..20u32 * 20 {
29011            bus.write_byte(offscreen_base + i, 0x00);
29012        }
29013        let pixmap_handle = bus.alloc(4);
29014        let pixmap_ptr = bus.alloc(50);
29015        bus.write_long(pixmap_handle, pixmap_ptr);
29016        bus.write_long(pixmap_ptr, offscreen_base);
29017        bus.write_word(pixmap_ptr + 4, 0x8000 | 20);
29018        bus.write_word(pixmap_ptr + 32, 8);
29019        bus.write_long(dialog_ptr + 2, pixmap_handle);
29020        bus.write_word(dialog_ptr + 6, 0xC000);
29021        bus.write_word(dialog_ptr + 16, 0);
29022        bus.write_word(dialog_ptr + 18, 0);
29023        bus.write_word(dialog_ptr + 20, 20);
29024        bus.write_word(dialog_ptr + 22, 20);
29025
29026        bus.write_byte(offscreen_base + 7 * 20 + 9, 0x44);
29027        disp.front_window = dialog_ptr;
29028        disp.window_bounds = (0, 0, 20, 20);
29029        disp.dialog_tracking = Some(crate::trap::dispatch::DialogTrackingState {
29030            dialog_ptr,
29031            bounds: (0, 0, 20, 20),
29032            title: String::new(),
29033            proc_id: 2,
29034            items: Vec::new(),
29035            default_item: 0,
29036            cancel_item: 0,
29037            edit_text: String::new(),
29038            edit_item: 0,
29039            saved_pixels: Vec::new(),
29040            stack_ptr: TEST_SP,
29041            item_hit_ptr: 0,
29042            rendered_pixels: Vec::new(),
29043            flash_remaining: 0,
29044            flash_delay: 0,
29045            flash_item: 0,
29046            edit_text_modified: false,
29047            draw_proc_queue: VecDeque::new(),
29048            draw_procs_done: true,
29049            rendered_pixels_final: false,
29050            filter_proc: 0,
29051            game_managed: false,
29052            last_filter_event: None,
29053            popup_draws: Vec::new(),
29054            active_popup: None,
29055            active_button: None,
29056            active_user_item: None,
29057        });
29058
29059        let result = disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus);
29060        assert!(result.unwrap().is_ok());
29061
29062        let screen_pixel = screen_base + 7 * 32 + 9;
29063        assert_eq!(
29064            bus.read_byte(screen_pixel),
29065            0x44,
29066            "ModalDialog should composite dialog-port userItem pixels before snapshot"
29067        );
29068        let tracking = disp.dialog_tracking.as_ref().unwrap();
29069        assert!(tracking.rendered_pixels_final);
29070        let snapshot_width = 20 + (TrapDispatcher::DBOX_FRAME_MARGIN as usize * 2);
29071        let snapshot_height = 20 + (TrapDispatcher::DBOX_FRAME_MARGIN as usize * 2);
29072        assert_eq!(
29073            tracking.rendered_pixels.len(),
29074            snapshot_width * snapshot_height
29075        );
29076        let snapshot_index = (7 + TrapDispatcher::DBOX_FRAME_MARGIN as usize) * snapshot_width
29077            + (9 + TrapDispatcher::DBOX_FRAME_MARGIN as usize);
29078        assert_eq!(tracking.rendered_pixels[snapshot_index], 0x44);
29079    }
29080
29081    #[test]
29082    fn modal_dialog_update_event_restores_existing_snapshot_before_resnapshot() {
29083        let (mut disp, mut cpu, mut bus) = setup();
29084        let screen_base = bus.alloc((32 * 32) as u32);
29085        for i in 0..32u32 * 32 {
29086            bus.write_byte(screen_base + i, 0x00);
29087        }
29088        bus.write_long(0x0824, screen_base);
29089        disp.screen_mode = (screen_base, 32, 32, 32, 8);
29090
29091        let dialog_ptr = bus.alloc(64);
29092        disp.front_window = dialog_ptr;
29093        disp.window_bounds = (0, 0, 20, 20);
29094
29095        let snapshot_width = 20 + (TrapDispatcher::DBOX_FRAME_MARGIN as usize * 2);
29096        let snapshot_height = 20 + (TrapDispatcher::DBOX_FRAME_MARGIN as usize * 2);
29097        let mut rendered_pixels = vec![0x00; snapshot_width * snapshot_height];
29098        let snapshot_index = (7 + TrapDispatcher::DBOX_FRAME_MARGIN as usize) * snapshot_width
29099            + (9 + TrapDispatcher::DBOX_FRAME_MARGIN as usize);
29100        rendered_pixels[snapshot_index] = 0x44;
29101
29102        disp.dialog_tracking = Some(crate::trap::dispatch::DialogTrackingState {
29103            dialog_ptr,
29104            bounds: (0, 0, 20, 20),
29105            title: String::new(),
29106            proc_id: 2,
29107            items: Vec::new(),
29108            default_item: 0,
29109            cancel_item: 0,
29110            edit_text: String::new(),
29111            edit_item: 0,
29112            saved_pixels: Vec::new(),
29113            stack_ptr: TEST_SP,
29114            item_hit_ptr: 0,
29115            rendered_pixels,
29116            flash_remaining: 0,
29117            flash_delay: 0,
29118            flash_item: 0,
29119            edit_text_modified: false,
29120            draw_proc_queue: VecDeque::new(),
29121            draw_procs_done: true,
29122            rendered_pixels_final: true,
29123            filter_proc: 0,
29124            game_managed: false,
29125            last_filter_event: None,
29126            popup_draws: Vec::new(),
29127            active_popup: None,
29128            active_button: None,
29129            active_user_item: None,
29130        });
29131        disp.event_queue
29132            .push_back(crate::trap::dispatch::QueuedEvent {
29133                what: 6,
29134                message: dialog_ptr,
29135                where_v: 0,
29136                where_h: 0,
29137                modifiers: 0,
29138            });
29139
29140        let result = disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus);
29141        assert!(result.unwrap().is_ok());
29142
29143        let screen_pixel = screen_base + 7 * 32 + 9;
29144        assert_eq!(
29145            bus.read_byte(screen_pixel),
29146            0x44,
29147            "ModalDialog update events must preserve userItem-rendered dialog pixels"
29148        );
29149        let tracking = disp.dialog_tracking.as_ref().unwrap();
29150        assert_eq!(tracking.rendered_pixels[snapshot_index], 0x44);
29151    }
29152
29153    #[test]
29154    fn finalize_dialog_draw_procs_snapshots_completed_user_item_pixels() {
29155        let (mut disp, _cpu, mut bus) = setup();
29156        let screen_base = bus.alloc((32 * 32) as u32);
29157        for i in 0..32u32 * 32 {
29158            bus.write_byte(screen_base + i, 0x00);
29159        }
29160        bus.write_long(0x0824, screen_base);
29161        disp.screen_mode = (screen_base, 32, 32, 32, 8);
29162
29163        let dialog_ptr = bus.alloc(64);
29164        let screen_pixel = screen_base + 7 * 32 + 9;
29165        bus.write_byte(screen_pixel, 0x44);
29166        disp.front_window = dialog_ptr;
29167        disp.window_bounds = (0, 0, 20, 20);
29168        disp.dialog_tracking = Some(crate::trap::dispatch::DialogTrackingState {
29169            dialog_ptr,
29170            bounds: (0, 0, 20, 20),
29171            title: String::new(),
29172            proc_id: 2,
29173            items: Vec::new(),
29174            default_item: 0,
29175            cancel_item: 0,
29176            edit_text: String::new(),
29177            edit_item: 0,
29178            saved_pixels: Vec::new(),
29179            stack_ptr: TEST_SP,
29180            item_hit_ptr: 0,
29181            rendered_pixels: Vec::new(),
29182            flash_remaining: 0,
29183            flash_delay: 0,
29184            flash_item: 0,
29185            edit_text_modified: false,
29186            draw_proc_queue: VecDeque::new(),
29187            draw_procs_done: false,
29188            rendered_pixels_final: false,
29189            filter_proc: 0,
29190            game_managed: false,
29191            last_filter_event: None,
29192            popup_draws: Vec::new(),
29193            active_popup: None,
29194            active_button: None,
29195            active_user_item: None,
29196        });
29197
29198        disp.finalize_dialog_draw_procs_if_idle(&mut bus);
29199
29200        let tracking = disp.dialog_tracking.as_ref().unwrap();
29201        assert!(tracking.draw_procs_done);
29202        assert!(tracking.rendered_pixels_final);
29203        let snapshot_width = 20 + (TrapDispatcher::DBOX_FRAME_MARGIN as usize * 2);
29204        let snapshot_index = (7 + TrapDispatcher::DBOX_FRAME_MARGIN as usize) * snapshot_width
29205            + (9 + TrapDispatcher::DBOX_FRAME_MARGIN as usize);
29206        assert_eq!(tracking.rendered_pixels[snapshot_index], 0x44);
29207    }
29208
29209    #[test]
29210    fn modal_dialog_mouse_down_return_consumes_queued_mouse_up() {
29211        let (mut disp, mut cpu, mut bus) = setup();
29212        let dialog_ptr = 0x200000u32;
29213        let item_hit_ptr = 0x300000u32;
29214
29215        disp.dialog_tracking = Some(crate::trap::dispatch::DialogTrackingState {
29216            dialog_ptr,
29217            bounds: (100, 200, 200, 360),
29218            title: String::new(),
29219            proc_id: 2,
29220            items: vec![DialogItem {
29221                item_type: 0,
29222                rect: (20, 30, 60, 110),
29223                text: String::new(),
29224                resource_id: 0,
29225                proc_ptr: 0,
29226                sel_start: 0,
29227                sel_end: 0,
29228            }],
29229            default_item: 1,
29230            cancel_item: 2,
29231            edit_text: String::new(),
29232            edit_item: 0,
29233            saved_pixels: Vec::new(),
29234            stack_ptr: TEST_SP,
29235            item_hit_ptr,
29236            rendered_pixels: Vec::new(),
29237            flash_remaining: 0,
29238            flash_delay: 0,
29239            flash_item: 0,
29240            edit_text_modified: false,
29241            draw_proc_queue: VecDeque::new(),
29242            draw_procs_done: true,
29243            rendered_pixels_final: true,
29244            filter_proc: 0,
29245            game_managed: true,
29246            last_filter_event: None,
29247            popup_draws: Vec::new(),
29248            active_popup: None,
29249            active_button: None,
29250            active_user_item: None,
29251        });
29252        disp.event_queue
29253            .push_back(crate::trap::dispatch::QueuedEvent {
29254                what: 1,
29255                message: 0,
29256                where_v: 130,
29257                where_h: 240,
29258                modifiers: 0,
29259            });
29260        disp.event_queue
29261            .push_back(crate::trap::dispatch::QueuedEvent {
29262                what: 2,
29263                message: 0,
29264                where_v: 130,
29265                where_h: 240,
29266                modifiers: 0x0080,
29267            });
29268
29269        let result = disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus);
29270        assert!(result.unwrap().is_ok());
29271        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
29272        assert_eq!(bus.read_word(item_hit_ptr), 1);
29273        assert!(disp.event_queue.iter().all(|event| event.what != 2));
29274    }
29275
29276    #[test]
29277    fn modal_dialog_plain_user_item_returns_on_mouse_down() {
29278        // A plain userItem's content and mouse tracking are application-owned
29279        // (IM:I I-405). ModalDialog returns the item on the initial press so
29280        // the application can run its own StillDown()/GetMouse() tracking loop
29281        // (e.g. EV Override's draggable Game Speed slider). The pending
29282        // mouse-up stays queued so that tracking loop still observes the
29283        // release.
29284        let (mut disp, mut cpu, mut bus) = setup();
29285        let dialog_ptr = 0x200000u32;
29286        let item_hit_ptr = 0x300000u32;
29287
29288        disp.dialog_tracking = Some(crate::trap::dispatch::DialogTrackingState {
29289            dialog_ptr,
29290            bounds: (100, 200, 200, 360),
29291            title: String::new(),
29292            proc_id: 2,
29293            items: vec![DialogItem {
29294                item_type: 0,
29295                rect: (20, 30, 60, 110),
29296                text: String::new(),
29297                resource_id: 0,
29298                proc_ptr: 0,
29299                sel_start: 0,
29300                sel_end: 0,
29301            }],
29302            default_item: 1,
29303            cancel_item: 2,
29304            edit_text: String::new(),
29305            edit_item: 0,
29306            saved_pixels: Vec::new(),
29307            stack_ptr: TEST_SP,
29308            item_hit_ptr,
29309            rendered_pixels: Vec::new(),
29310            flash_remaining: 0,
29311            flash_delay: 0,
29312            flash_item: 0,
29313            edit_text_modified: false,
29314            draw_proc_queue: VecDeque::new(),
29315            draw_procs_done: true,
29316            rendered_pixels_final: true,
29317            filter_proc: 0,
29318            game_managed: false,
29319            last_filter_event: None,
29320            popup_draws: Vec::new(),
29321            active_popup: None,
29322            active_button: None,
29323            active_user_item: None,
29324        });
29325        disp.mouse_button = true;
29326        disp.mouse_pos = (130, 240);
29327        disp.event_queue
29328            .push_back(crate::trap::dispatch::QueuedEvent {
29329                what: 1,
29330                message: 0,
29331                where_v: 130,
29332                where_h: 240,
29333                modifiers: 0,
29334            });
29335
29336        let result = disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus);
29337        assert!(result.unwrap().is_ok());
29338        // The hit is returned immediately on mouse-down: stack popped and the
29339        // item number written, with modal tracking released.
29340        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
29341        assert_eq!(bus.read_word(item_hit_ptr), 1);
29342        assert!(disp.dialog_tracking.is_none());
29343    }
29344
29345    #[test]
29346    fn modal_dialog_popup_candidate_user_item_returns_on_mouse_down_in_original_rect() {
29347        let (mut disp, mut cpu, mut bus) = setup();
29348        let dialog_ptr = 0x200000u32;
29349        let item_hit_ptr = 0x300000u32;
29350
29351        disp.dialog_tracking = Some(crate::trap::dispatch::DialogTrackingState {
29352            dialog_ptr,
29353            bounds: (100, 200, 200, 420),
29354            title: String::new(),
29355            proc_id: 2,
29356            items: vec![DialogItem {
29357                item_type: 0,
29358                rect: (20, 90, 40, 110),
29359                text: String::new(),
29360                resource_id: 0,
29361                proc_ptr: 0,
29362                sel_start: 0,
29363                sel_end: 0,
29364            }],
29365            default_item: 1,
29366            cancel_item: 2,
29367            edit_text: String::new(),
29368            edit_item: 0,
29369            saved_pixels: Vec::new(),
29370            stack_ptr: TEST_SP,
29371            item_hit_ptr,
29372            rendered_pixels: Vec::new(),
29373            flash_remaining: 0,
29374            flash_delay: 0,
29375            flash_item: 0,
29376            edit_text_modified: false,
29377            draw_proc_queue: VecDeque::new(),
29378            draw_procs_done: true,
29379            rendered_pixels_final: true,
29380            filter_proc: 0,
29381            game_managed: false,
29382            last_filter_event: None,
29383            popup_draws: Vec::new(),
29384            active_popup: None,
29385            active_button: None,
29386            active_user_item: None,
29387        });
29388        disp.dialog_popup_original_rects
29389            .insert((dialog_ptr, 1), (20, 30, 40, 150));
29390        disp.dialog_popup_candidate_items.insert((dialog_ptr, 1));
29391        disp.mouse_button = true;
29392        disp.mouse_pos = (130, 260);
29393        disp.event_queue
29394            .push_back(crate::trap::dispatch::QueuedEvent {
29395                what: 1,
29396                message: 0,
29397                where_v: 130,
29398                where_h: 260,
29399                modifiers: 0,
29400            });
29401
29402        let result = disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus);
29403        assert!(result.unwrap().is_ok());
29404        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
29405        assert_eq!(bus.read_word(item_hit_ptr), 1);
29406        assert_eq!(
29407            disp.dialog_tracking
29408                .as_ref()
29409                .and_then(|tracking| tracking.active_user_item.as_ref())
29410                .map(|active| active.item_no),
29411            None,
29412            "popup-candidate userItems must return on mouse-down so the app can run PopUpMenuSelect"
29413        );
29414    }
29415
29416    #[test]
29417    fn modal_dialog_button_waits_for_late_mouse_up_before_returning() {
29418        let (mut disp, mut cpu, mut bus) = setup();
29419        let dialog_ptr = 0x200000u32;
29420        let item_hit_ptr = 0x300000u32;
29421
29422        disp.dialog_tracking = Some(crate::trap::dispatch::DialogTrackingState {
29423            dialog_ptr,
29424            bounds: (100, 200, 200, 360),
29425            title: String::new(),
29426            proc_id: 2,
29427            items: vec![DialogItem {
29428                item_type: 4,
29429                rect: (20, 30, 60, 110),
29430                text: String::from("OK"),
29431                resource_id: 0,
29432                proc_ptr: 0,
29433                sel_start: 0,
29434                sel_end: 0,
29435            }],
29436            default_item: 1,
29437            cancel_item: 0,
29438            edit_text: String::new(),
29439            edit_item: 0,
29440            saved_pixels: Vec::new(),
29441            stack_ptr: TEST_SP,
29442            item_hit_ptr,
29443            rendered_pixels: Vec::new(),
29444            flash_remaining: 0,
29445            flash_delay: 0,
29446            flash_item: 0,
29447            edit_text_modified: false,
29448            draw_proc_queue: VecDeque::new(),
29449            draw_procs_done: true,
29450            rendered_pixels_final: true,
29451            filter_proc: 0,
29452            game_managed: true,
29453            last_filter_event: None,
29454            popup_draws: Vec::new(),
29455            active_popup: None,
29456            active_button: None,
29457            active_user_item: None,
29458        });
29459        disp.mouse_button = true;
29460        disp.mouse_pos = (130, 240);
29461        disp.event_queue
29462            .push_back(crate::trap::dispatch::QueuedEvent {
29463                what: 1,
29464                message: 0,
29465                where_v: 130,
29466                where_h: 240,
29467                modifiers: 0,
29468            });
29469
29470        let result = disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus);
29471        assert!(result.unwrap().is_ok());
29472        assert_eq!(cpu.read_reg(Register::A7), TEST_SP);
29473        assert_eq!(bus.read_word(item_hit_ptr), 0);
29474        assert!(disp
29475            .dialog_tracking
29476            .as_ref()
29477            .and_then(|tracking| tracking.active_button.as_ref())
29478            .is_some());
29479
29480        disp.mouse_button = false;
29481        disp.event_queue
29482            .push_back(crate::trap::dispatch::QueuedEvent {
29483                what: 2,
29484                message: 0,
29485                where_v: 130,
29486                where_h: 240,
29487                modifiers: 0x0080,
29488            });
29489        let result = disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus);
29490        assert!(result.unwrap().is_ok());
29491        assert!(disp.event_queue.iter().all(|event| event.what != 2));
29492        assert!(disp
29493            .dialog_tracking
29494            .as_ref()
29495            .and_then(|tracking| tracking.active_button.as_ref())
29496            .is_none());
29497        assert_eq!(cpu.read_reg(Register::A7), TEST_SP);
29498
29499        {
29500            let tracking = disp.dialog_tracking.as_mut().unwrap();
29501            tracking.flash_remaining = 1;
29502            tracking.flash_delay = 0;
29503        }
29504        let result = disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus);
29505        assert!(result.unwrap().is_ok());
29506        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
29507        assert_eq!(bus.read_word(item_hit_ptr), 1);
29508    }
29509
29510    #[test]
29511    fn modal_dialog_filter_handled_mouse_down_consumes_queued_mouse_up() {
29512        let (mut disp, mut cpu, mut bus) = setup();
29513        let dialog_ptr = 0x200000u32;
29514        let item_hit_ptr = 0x300000u32;
29515        let result_addr = 0x300100u32;
29516
29517        bus.write_word(result_addr, 0xFFFF);
29518        bus.write_word(item_hit_ptr, 12);
29519        disp.dialog_filter_result_addr = result_addr;
29520        disp.dialog_tracking = Some(crate::trap::dispatch::DialogTrackingState {
29521            dialog_ptr,
29522            bounds: (100, 200, 200, 360),
29523            title: String::new(),
29524            proc_id: 2,
29525            items: vec![DialogItem {
29526                item_type: 0,
29527                rect: (20, 30, 60, 110),
29528                text: String::new(),
29529                resource_id: 0,
29530                proc_ptr: 0,
29531                sel_start: 0,
29532                sel_end: 0,
29533            }],
29534            default_item: 1,
29535            cancel_item: 2,
29536            edit_text: String::new(),
29537            edit_item: 0,
29538            saved_pixels: Vec::new(),
29539            stack_ptr: TEST_SP,
29540            item_hit_ptr,
29541            rendered_pixels: Vec::new(),
29542            flash_remaining: 0,
29543            flash_delay: 0,
29544            flash_item: 0,
29545            edit_text_modified: false,
29546            draw_proc_queue: VecDeque::new(),
29547            draw_procs_done: true,
29548            rendered_pixels_final: true,
29549            filter_proc: 0x149F0,
29550            game_managed: true,
29551            last_filter_event: Some(crate::trap::dispatch::QueuedEvent {
29552                what: 1,
29553                message: 0,
29554                where_v: 130,
29555                where_h: 240,
29556                modifiers: 0,
29557            }),
29558            popup_draws: Vec::new(),
29559            active_popup: None,
29560            active_button: None,
29561            active_user_item: None,
29562        });
29563        disp.event_queue
29564            .push_back(crate::trap::dispatch::QueuedEvent {
29565                what: 2,
29566                message: 0,
29567                where_v: 130,
29568                where_h: 240,
29569                modifiers: 0x0080,
29570            });
29571
29572        let result = disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus);
29573        assert!(result.unwrap().is_ok());
29574        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
29575        assert!(disp.event_queue.iter().all(|event| event.what != 2));
29576    }
29577
29578    #[test]
29579    fn modal_dialog_return_key_activates_default_button_after_filter_false() {
29580        let (mut disp, mut cpu, mut bus) = setup();
29581        let dialog_ptr = 0x200000u32;
29582        let item_hit_ptr = 0x300000u32;
29583        let result_addr = 0x300100u32;
29584
29585        bus.write_word(result_addr, 0);
29586        bus.write_word(item_hit_ptr, 0);
29587        disp.dialog_filter_result_addr = result_addr;
29588        disp.dialog_tracking = Some(crate::trap::dispatch::DialogTrackingState {
29589            dialog_ptr,
29590            bounds: (100, 200, 200, 360),
29591            title: String::new(),
29592            proc_id: 2,
29593            items: vec![DialogItem {
29594                item_type: 4,
29595                rect: (20, 30, 60, 110),
29596                text: String::from("OK"),
29597                resource_id: 0,
29598                proc_ptr: 0,
29599                sel_start: 0,
29600                sel_end: 0,
29601            }],
29602            default_item: 1,
29603            cancel_item: 0,
29604            edit_text: String::new(),
29605            edit_item: 0,
29606            saved_pixels: Vec::new(),
29607            stack_ptr: TEST_SP,
29608            item_hit_ptr,
29609            rendered_pixels: Vec::new(),
29610            flash_remaining: 0,
29611            flash_delay: 0,
29612            flash_item: 0,
29613            edit_text_modified: false,
29614            draw_proc_queue: VecDeque::new(),
29615            draw_procs_done: true,
29616            rendered_pixels_final: true,
29617            filter_proc: 0x149F0,
29618            game_managed: true,
29619            last_filter_event: Some(crate::trap::dispatch::QueuedEvent {
29620                what: 3,
29621                message: 0x0000_240D,
29622                where_v: 130,
29623                where_h: 240,
29624                modifiers: 0,
29625            }),
29626            popup_draws: Vec::new(),
29627            active_popup: None,
29628            active_button: None,
29629            active_user_item: None,
29630        });
29631
29632        let result = disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus);
29633        assert!(result.unwrap().is_ok());
29634        assert_eq!(cpu.read_reg(Register::A7), TEST_SP);
29635        assert_eq!(bus.read_word(item_hit_ptr), 0);
29636        {
29637            let tracking = disp.dialog_tracking.as_ref().unwrap();
29638            assert_eq!(tracking.flash_item, 1);
29639            assert!(tracking.flash_remaining > 0);
29640        }
29641
29642        {
29643            let tracking = disp.dialog_tracking.as_mut().unwrap();
29644            tracking.flash_remaining = 1;
29645            tracking.flash_delay = 0;
29646        }
29647        let result = disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus);
29648        assert!(result.unwrap().is_ok());
29649        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
29650        assert_eq!(bus.read_word(item_hit_ptr), 1);
29651        assert!(disp.dialog_tracking.is_none());
29652    }
29653
29654    #[test]
29655    fn modal_dialog_filter_low_byte_nonzero_result_is_false() {
29656        let (mut disp, mut cpu, mut bus) = setup();
29657        let dialog_ptr = 0x200000u32;
29658        let item_hit_ptr = 0x300000u32;
29659        let result_addr = 0x300100u32;
29660
29661        bus.write_word(result_addr, 0x0001);
29662        bus.write_word(item_hit_ptr, 0);
29663        disp.dialog_filter_result_addr = result_addr;
29664        disp.dialog_tracking = Some(crate::trap::dispatch::DialogTrackingState {
29665            dialog_ptr,
29666            bounds: (100, 200, 200, 360),
29667            title: String::new(),
29668            proc_id: 2,
29669            items: vec![DialogItem {
29670                item_type: 4,
29671                rect: (20, 30, 60, 110),
29672                text: String::from("OK"),
29673                resource_id: 0,
29674                proc_ptr: 0,
29675                sel_start: 0,
29676                sel_end: 0,
29677            }],
29678            default_item: 1,
29679            cancel_item: 0,
29680            edit_text: String::new(),
29681            edit_item: 0,
29682            saved_pixels: Vec::new(),
29683            stack_ptr: TEST_SP,
29684            item_hit_ptr,
29685            rendered_pixels: Vec::new(),
29686            flash_remaining: 0,
29687            flash_delay: 0,
29688            flash_item: 0,
29689            edit_text_modified: false,
29690            draw_proc_queue: VecDeque::new(),
29691            draw_procs_done: true,
29692            rendered_pixels_final: true,
29693            filter_proc: 0x149F0,
29694            game_managed: false,
29695            last_filter_event: Some(crate::trap::dispatch::QueuedEvent {
29696                what: 1,
29697                message: 0,
29698                where_v: 130,
29699                where_h: 240,
29700                modifiers: 0,
29701            }),
29702            popup_draws: Vec::new(),
29703            active_popup: None,
29704            active_button: None,
29705            active_user_item: None,
29706        });
29707        disp.mouse_button = true;
29708        disp.mouse_pos = (130, 240);
29709
29710        let result = disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus);
29711        assert!(result.unwrap().is_ok());
29712        assert_eq!(cpu.read_reg(Register::A7), TEST_SP);
29713        assert_eq!(bus.read_word(item_hit_ptr), 0);
29714        assert!(disp
29715            .dialog_tracking
29716            .as_ref()
29717            .and_then(|tracking| tracking.active_button.as_ref())
29718            .is_some());
29719    }
29720
29721    #[test]
29722    fn modal_dialog_filter_true_zero_hit_returns_to_app() {
29723        let (mut disp, mut cpu, mut bus) = setup();
29724        let dialog_ptr = 0x200000u32;
29725        let item_hit_ptr = 0x300000u32;
29726        let result_addr = 0x300100u32;
29727
29728        bus.write_word(result_addr, 0xFFFF);
29729        bus.write_word(item_hit_ptr, 0);
29730        disp.dialog_filter_result_addr = result_addr;
29731        disp.dialog_tracking = Some(crate::trap::dispatch::DialogTrackingState {
29732            dialog_ptr,
29733            bounds: (100, 200, 200, 360),
29734            title: String::new(),
29735            proc_id: 2,
29736            items: vec![DialogItem {
29737                item_type: 4,
29738                rect: (20, 30, 60, 110),
29739                text: String::from("OK"),
29740                resource_id: 0,
29741                proc_ptr: 0,
29742                sel_start: 0,
29743                sel_end: 0,
29744            }],
29745            default_item: 1,
29746            cancel_item: 2,
29747            edit_text: String::new(),
29748            edit_item: 0,
29749            saved_pixels: Vec::new(),
29750            stack_ptr: TEST_SP,
29751            item_hit_ptr,
29752            rendered_pixels: Vec::new(),
29753            flash_remaining: 0,
29754            flash_delay: 0,
29755            flash_item: 0,
29756            edit_text_modified: false,
29757            draw_proc_queue: VecDeque::new(),
29758            draw_procs_done: true,
29759            rendered_pixels_final: true,
29760            filter_proc: 0x149F0,
29761            game_managed: false,
29762            last_filter_event: Some(crate::trap::dispatch::QueuedEvent {
29763                what: 3,
29764                message: 0x0000_240D,
29765                where_v: 130,
29766                where_h: 240,
29767                modifiers: 0,
29768            }),
29769            popup_draws: Vec::new(),
29770            active_popup: None,
29771            active_button: None,
29772            active_user_item: None,
29773        });
29774
29775        let result = disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus);
29776        assert!(result.unwrap().is_ok());
29777        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
29778        assert_eq!(bus.read_word(item_hit_ptr), 0);
29779        assert!(disp.dialog_tracking.is_none());
29780    }
29781
29782    // ---- TEInit ($A9CC) ----
29783
29784    #[test]
29785    fn teinit_first_call_allocates_empty_scrap_handle_and_zeros_length() {
29786        // IM:I I-376 + I-389: TEInit creates an empty TextEdit scrap
29787        // handle and sets TEScrpLength to 0.
29788        let (mut disp, mut cpu, mut bus) = setup();
29789        let sp_before = cpu.read_reg(Register::A7);
29790        bus.write_word(crate::memory::globals::addr::TE_SCRP_LENGTH, 0xFFFF);
29791        bus.write_long(crate::memory::globals::addr::TE_SCRP_HANDLE, 0);
29792
29793        let result = disp.dispatch_dialog(true, 0x1CC, &mut cpu, &mut bus);
29794        assert!(result.unwrap().is_ok());
29795        assert_eq!(cpu.read_reg(Register::A7), sp_before);
29796
29797        let scrap_handle = bus.read_long(crate::memory::globals::addr::TE_SCRP_HANDLE);
29798        assert_ne!(scrap_handle, 0, "TEInit must allocate TEScrpHandle");
29799        assert_eq!(
29800            bus.read_long(scrap_handle),
29801            0,
29802            "TEInit scrap handle should initially reference empty data"
29803        );
29804        assert_eq!(
29805            bus.read_word(crate::memory::globals::addr::TE_SCRP_LENGTH),
29806            0
29807        );
29808    }
29809
29810    #[test]
29811    fn teinit_reuses_existing_scrap_handle_and_resets_length() {
29812        // IM:I I-376 + I-389: TEInit restores empty-scrap state; HLE must
29813        // keep an existing handle stable to avoid churn/leaks on repeat calls.
29814        let (mut disp, mut cpu, mut bus) = setup();
29815        let existing_handle = TrapDispatcher::allocate_handle_with_data(&mut bus, 3);
29816        let existing_ptr = bus.read_long(existing_handle);
29817        bus.write_bytes(existing_ptr, b"xyz");
29818        bus.write_long(
29819            crate::memory::globals::addr::TE_SCRP_HANDLE,
29820            existing_handle,
29821        );
29822        bus.write_word(crate::memory::globals::addr::TE_SCRP_LENGTH, 3);
29823
29824        let result = disp.dispatch_dialog(true, 0x1CC, &mut cpu, &mut bus);
29825        assert!(result.unwrap().is_ok());
29826        assert_eq!(cpu.read_reg(Register::A7), TEST_SP);
29827        assert_eq!(
29828            bus.read_long(crate::memory::globals::addr::TE_SCRP_HANDLE),
29829            existing_handle
29830        );
29831        assert_eq!(
29832            bus.read_word(crate::memory::globals::addr::TE_SCRP_LENGTH),
29833            0
29834        );
29835        assert_eq!(bus.read_long(existing_handle), existing_ptr);
29836        assert_eq!(bus.read_bytes(existing_ptr, 3), b"xyz".to_vec());
29837    }
29838
29839    fn make_te_with_text(
29840        disp: &mut TrapDispatcher,
29841        bus: &mut crate::memory::MacMemoryBus,
29842        text: &[u8],
29843    ) -> u32 {
29844        let te_handle = TrapDispatcher::allocate_te_handle(bus);
29845        disp.current_port = 0x181000;
29846        disp.tx_font = 4;
29847        disp.tx_face = 0;
29848        disp.tx_mode = 0;
29849        disp.tx_size = 10;
29850        disp.initialize_te_record(bus, te_handle, (0, 0, 60, 160), (0, 0, 60, 160));
29851        disp.te_set_text_contents(bus, te_handle, text);
29852        te_handle
29853    }
29854
29855    // ---- TENew ----
29856
29857    #[test]
29858    fn tenew_pointer_arg_convention_initializes_destrect_viewrect_and_returns_non_nil_handle() {
29859        // IM:I I-373..I-374 + TextEdit.h ONEWORDINLINE(0xA9D2):
29860        // FUNCTION TENew(destRect, viewRect: Rect): TEHandle. Modern MPW
29861        // Universal Headers pass two `const Rect *` pointers (8 bytes of
29862        // args). After dispatch (**hTE).destRect and (**hTE).viewRect
29863        // must round-trip the caller's input rects exactly. Defeats stubs
29864        // that swap the args, zero the rects, or copy only a subset of
29865        // the four 2-byte fields.
29866        let (mut disp, mut cpu, mut bus) = setup();
29867
29868        // Build two DISTINCT Rect inputs in guest memory.
29869        let dest_rect_ptr: u32 = 0x190000;
29870        bus.write_word(dest_rect_ptr, (-1000i16) as u16); // top
29871        bus.write_word(dest_rect_ptr + 2, (-1000i16) as u16); // left
29872        bus.write_word(dest_rect_ptr + 4, (-700i16) as u16); // bottom
29873        bus.write_word(dest_rect_ptr + 6, (-800i16) as u16); // right
29874        let view_rect_ptr: u32 = 0x190010;
29875        bus.write_word(view_rect_ptr, (-900i16) as u16); // top
29876        bus.write_word(view_rect_ptr + 2, (-1100i16) as u16); // left
29877        bus.write_word(view_rect_ptr + 4, (-600i16) as u16); // bottom
29878        bus.write_word(view_rect_ptr + 6, (-700i16) as u16); // right
29879
29880        // Pascal FUNCTION stack frame (pointer-arg convention).
29881        // Pascal pushes args left-to-right, so the FIRST arg (destRect_ptr)
29882        // is DEEPEST on stack at trap entry. With 4-byte pointers:
29883        //   sp+0..3   viewRect_ptr  (last pushed, shallowest)
29884        //   sp+4..7   destRect_ptr  (first pushed, deepest)
29885        //   sp+8..11  TEHandle result slot (poisoned)
29886        bus.write_long(TEST_SP, view_rect_ptr);
29887        bus.write_long(TEST_SP + 4, dest_rect_ptr);
29888        bus.write_long(TEST_SP + 8, 0xDEAD_BEEF);
29889
29890        let result = disp.dispatch_dialog(true, 0x1D2, &mut cpu, &mut bus);
29891        assert!(result.unwrap().is_ok());
29892        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
29893
29894        let te_handle = bus.read_long(TEST_SP + 8);
29895        assert_ne!(te_handle, 0, "TENew returned NIL TEHandle");
29896        let te_ptr = bus.read_long(te_handle);
29897        assert_ne!(te_ptr, 0, "TEHandle master pointer is NIL");
29898
29899        // destRect round-trip (top, left, bottom, right at offset 0..6).
29900        assert_eq!(
29901            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET) as i16,
29902            -1000
29903        );
29904        assert_eq!(
29905            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 2) as i16,
29906            -1000
29907        );
29908        assert_eq!(
29909            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 4) as i16,
29910            -700
29911        );
29912        assert_eq!(
29913            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 6) as i16,
29914            -800
29915        );
29916
29917        // viewRect round-trip (top, left, bottom, right at offset 8..14).
29918        assert_eq!(
29919            bus.read_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET) as i16,
29920            -900
29921        );
29922        assert_eq!(
29923            bus.read_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 2) as i16,
29924            -1100
29925        );
29926        assert_eq!(
29927            bus.read_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 4) as i16,
29928            -600
29929        );
29930        assert_eq!(
29931            bus.read_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 6) as i16,
29932            -700
29933        );
29934    }
29935
29936    #[test]
29937    fn tenew_fresh_terec_has_zero_telength_and_empty_selection_per_im_i_373() {
29938        // IM:I I-373: a fresh TERec has no text and an empty selection
29939        // range. Witness teLength == 0, selStart == 0, selEnd == 0, and
29940        // hText is a non-NIL Handle.
29941        let (mut disp, mut cpu, mut bus) = setup();
29942
29943        let rect_ptr: u32 = 0x190020;
29944        bus.write_word(rect_ptr, 10); // top
29945        bus.write_word(rect_ptr + 2, 20); // left
29946        bus.write_word(rect_ptr + 4, 110); // bottom
29947        bus.write_word(rect_ptr + 6, 220); // right
29948
29949        bus.write_long(TEST_SP, rect_ptr);
29950        bus.write_long(TEST_SP + 4, rect_ptr);
29951
29952        let result = disp.dispatch_dialog(true, 0x1D2, &mut cpu, &mut bus);
29953        assert!(result.unwrap().is_ok());
29954
29955        let te_handle = bus.read_long(TEST_SP + 8);
29956        let te_ptr = bus.read_long(te_handle);
29957
29958        assert_eq!(
29959            bus.read_word(te_ptr + TrapDispatcher::TE_LENGTH_OFFSET),
29960            0,
29961            "fresh TERec must have teLength == 0"
29962        );
29963        assert_eq!(
29964            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
29965            0,
29966            "fresh TERec must have selStart == 0"
29967        );
29968        assert_eq!(
29969            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET),
29970            0,
29971            "fresh TERec must have selEnd == 0"
29972        );
29973        let h_text = bus.read_long(te_ptr + TrapDispatcher::TE_HTEXT_OFFSET);
29974        assert_ne!(h_text, 0, "fresh TERec must have a non-NIL hText handle");
29975    }
29976
29977    #[test]
29978    fn tenew_function_protocol_consumes_two_pointer_args_and_writes_4_byte_result() {
29979        // Pascal FUNCTION calling convention: 2 pointer args (8 bytes)
29980        // are popped, the 4-byte TEHandle result is written into the
29981        // caller's pre-allocated slot at the former SP+8. Sentinels
29982        // around the result slot must survive the trap call.
29983        let (mut disp, mut cpu, mut bus) = setup();
29984
29985        let rect_ptr: u32 = 0x190030;
29986        bus.write_word(rect_ptr, 0);
29987        bus.write_word(rect_ptr + 2, 0);
29988        bus.write_word(rect_ptr + 4, 100);
29989        bus.write_word(rect_ptr + 6, 200);
29990
29991        bus.write_long(TEST_SP, rect_ptr);
29992        bus.write_long(TEST_SP + 4, rect_ptr);
29993        bus.write_long(TEST_SP + 8, 0xDEAD_BEEF);
29994        bus.write_long(TEST_SP + 12, 0xCAFE_BABE); // sentinel past result
29995
29996        let result = disp.dispatch_dialog(true, 0x1D2, &mut cpu, &mut bus);
29997        assert!(result.unwrap().is_ok());
29998        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
29999
30000        let te_handle = bus.read_long(TEST_SP + 8);
30001        assert_ne!(te_handle, 0xDEAD_BEEF, "result slot must be overwritten");
30002        assert_ne!(te_handle, 0);
30003        assert_eq!(
30004            bus.read_long(TEST_SP + 12),
30005            0xCAFE_BABE,
30006            "sentinel past 4-byte result slot must survive"
30007        );
30008    }
30009
30010    #[test]
30011    fn tenew_pointer_arg_convention_accepts_wide_ordered_destrect() {
30012        // Some MPW callers pass a Rect embedded at the start of an
30013        // application record. TextEdit accepts the pointer convention
30014        // even when the destination Rect is wider than the visible
30015        // screen; the trap must still pop only the two pointer args.
30016        let (mut disp, mut cpu, mut bus) = setup();
30017
30018        let dest_record_ptr: u32 = 0x190040;
30019        bus.write_word(dest_record_ptr, 159);
30020        bus.write_word(dest_record_ptr + 2, 500);
30021        bus.write_word(dest_record_ptr + 4, 171);
30022        bus.write_word(dest_record_ptr + 6, 10500);
30023
30024        let view_rect_ptr: u32 = 0x190060;
30025        bus.write_word(view_rect_ptr, 159);
30026        bus.write_word(view_rect_ptr + 2, 500);
30027        bus.write_word(view_rect_ptr + 4, 171);
30028        bus.write_word(view_rect_ptr + 6, 10500);
30029
30030        bus.write_long(TEST_SP, view_rect_ptr);
30031        bus.write_long(TEST_SP + 4, dest_record_ptr);
30032        bus.write_long(TEST_SP + 8, 0xDEAD_BEEF);
30033        bus.write_long(TEST_SP + 12, 0xCAFE_BABE);
30034
30035        let result = disp.dispatch_dialog(true, 0x1D2, &mut cpu, &mut bus);
30036        assert!(result.unwrap().is_ok());
30037        assert_eq!(
30038            cpu.read_reg(Register::A7),
30039            TEST_SP + 8,
30040            "TENew pointer convention must pop exactly two pointers"
30041        );
30042
30043        let te_handle = bus.read_long(TEST_SP + 8);
30044        assert_ne!(te_handle, 0);
30045        assert_ne!(te_handle, 0xDEAD_BEEF);
30046        assert_eq!(
30047            bus.read_long(TEST_SP + 12),
30048            0xCAFE_BABE,
30049            "TENew must not write past the 4-byte function-result slot"
30050        );
30051
30052        let te_ptr = bus.read_long(te_handle);
30053        assert_ne!(te_ptr, 0);
30054        assert_eq!(
30055            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 6) as i16,
30056            10500,
30057            "wide destRect.right must round-trip from caller memory"
30058        );
30059        assert_eq!(
30060            bus.read_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 6) as i16,
30061            10500,
30062            "wide viewRect.right must round-trip from caller memory"
30063        );
30064    }
30065
30066    #[test]
30067    fn tenew_pointer_arg_convention_is_based_on_valid_pointers_not_rect_shape() {
30068        // The calling convention is determined by the stack containing
30069        // two Rect pointers. Unusual caller Rect contents must not make
30070        // the trap consume a legacy by-value frame and corrupt the
30071        // caller's saved registers.
30072        let (mut disp, mut cpu, mut bus) = setup();
30073
30074        let dest_rect_ptr: u32 = 0x190080;
30075        bus.write_word(dest_rect_ptr, 552);
30076        bus.write_word(dest_rect_ptr + 2, 525);
30077        bus.write_word(dest_rect_ptr + 4, 520);
30078        bus.write_word(dest_rect_ptr + 6, 10525);
30079
30080        let view_rect_ptr: u32 = 0x1900A0;
30081        bus.write_word(view_rect_ptr, 552);
30082        bus.write_word(view_rect_ptr + 2, 525);
30083        bus.write_word(view_rect_ptr + 4, 520);
30084        bus.write_word(view_rect_ptr + 6, 10525);
30085
30086        bus.write_long(TEST_SP, view_rect_ptr);
30087        bus.write_long(TEST_SP + 4, dest_rect_ptr);
30088        bus.write_long(TEST_SP + 8, 0xDEAD_BEEF);
30089        bus.write_long(TEST_SP + 12, 0xCAFE_BABE);
30090
30091        let result = disp.dispatch_dialog(true, 0x1D2, &mut cpu, &mut bus);
30092        assert!(result.unwrap().is_ok());
30093        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
30094
30095        let te_handle = bus.read_long(TEST_SP + 8);
30096        assert_ne!(te_handle, 0);
30097        assert_ne!(te_handle, 0xDEAD_BEEF);
30098        assert_eq!(bus.read_long(TEST_SP + 12), 0xCAFE_BABE);
30099
30100        let te_ptr = bus.read_long(te_handle);
30101        assert_eq!(
30102            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET) as i16,
30103            552
30104        );
30105        assert_eq!(
30106            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 4) as i16,
30107            520
30108        );
30109        assert_eq!(
30110            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 6) as i16,
30111            10525
30112        );
30113    }
30114
30115    // ---- TEDispose / TECalText / TESetSelect / TEDelete ----
30116
30117    #[test]
30118    fn tedispose_releases_te_record_text_handle_and_pops_arg() {
30119        // IM:I I-383..I-384.
30120        let (mut disp, mut cpu, mut bus) = setup();
30121        let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
30122        let te_ptr = bus.read_long(te_handle);
30123        let text_handle = bus.read_long(te_ptr + TrapDispatcher::TE_HTEXT_OFFSET);
30124        let text_ptr = bus.read_long(text_handle);
30125        assert_ne!(te_handle, 0);
30126        assert_ne!(te_ptr, 0);
30127        assert_ne!(text_handle, 0);
30128        assert_ne!(text_ptr, 0);
30129
30130        bus.write_long(TEST_SP, te_handle);
30131        let result = disp.dispatch_dialog(true, 0x1CD, &mut cpu, &mut bus);
30132        assert!(result.unwrap().is_ok());
30133        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
30134
30135        assert_eq!(bus.get_alloc_size(text_ptr), None);
30136        assert_eq!(bus.get_alloc_size(text_handle), None);
30137        assert_eq!(bus.get_alloc_size(te_ptr), None);
30138        assert_eq!(bus.get_alloc_size(te_handle), None);
30139    }
30140
30141    #[test]
30142    fn tecaltext_recomputes_line_metadata_and_pops_arg() {
30143        // IM:I I-390: lineStarts are recomputed from current text layout.
30144        let (mut disp, mut cpu, mut bus) = setup();
30145        let te_handle = make_te_with_text(&mut disp, &mut bus, b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
30146        let te_ptr = bus.read_long(te_handle);
30147        let text_len = TrapDispatcher::te_text_bytes(&bus, te_handle).len() as u16;
30148        assert_eq!(text_len, 32);
30149
30150        // Force stale metadata and a narrow destination width so wrapped
30151        // layout produces multiple lines.
30152        bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 6, 12);
30153        bus.write_word(te_ptr + TrapDispatcher::TE_LENGTH_OFFSET, 0);
30154        bus.write_word(te_ptr + TrapDispatcher::TE_N_LINES_OFFSET, 0x7777);
30155        bus.write_word(te_ptr + TrapDispatcher::TE_LINE_STARTS_OFFSET, 0x7777);
30156
30157        bus.write_long(TEST_SP, te_handle);
30158        let result = disp.dispatch_dialog(true, 0x1D0, &mut cpu, &mut bus);
30159        assert!(result.unwrap().is_ok());
30160        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
30161
30162        let te_ptr = bus.read_long(te_handle);
30163        let n_lines = bus.read_word(te_ptr + TrapDispatcher::TE_N_LINES_OFFSET);
30164        assert_ne!(n_lines, 0x7777);
30165        assert!(n_lines > 1);
30166        assert_eq!(
30167            bus.read_word(te_ptr + TrapDispatcher::TE_LENGTH_OFFSET),
30168            text_len
30169        );
30170        assert_eq!(
30171            bus.read_word(te_ptr + TrapDispatcher::TE_LINE_STARTS_OFFSET),
30172            0
30173        );
30174        assert_eq!(
30175            bus.read_word(te_ptr + TrapDispatcher::TE_LINE_STARTS_OFFSET + u32::from(n_lines) * 2),
30176            text_len
30177        );
30178    }
30179
30180    #[test]
30181    fn tegettext_returns_htext_handle_from_terec_and_pops_te_handle_arg() {
30182        // IM:I I-384: "TEGetText returns a handle to the text of the
30183        // specified edit record. The result is the same as the handle in
30184        // the hText field of the edit record."
30185        let (mut disp, mut cpu, mut bus) = setup();
30186        let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
30187        let te_ptr = bus.read_long(te_handle);
30188        let expected_h_text = bus.read_long(te_ptr + TrapDispatcher::TE_HTEXT_OFFSET);
30189        assert_ne!(expected_h_text, 0);
30190
30191        bus.write_long(TEST_SP, te_handle);
30192        bus.write_long(TEST_SP + 4, 0xDEAD_BEEF); // poison result slot
30193        let result = disp.dispatch_dialog(true, 0x1CB, &mut cpu, &mut bus);
30194        assert!(result.unwrap().is_ok());
30195        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
30196
30197        // The returned CharsHandle equals (**hTE).hText exactly.
30198        assert_eq!(bus.read_long(TEST_SP + 4), expected_h_text);
30199
30200        // The handle dereferences to the 5-byte payload "HELLO".
30201        let text_ptr = bus.read_long(expected_h_text);
30202        assert_ne!(text_ptr, 0);
30203        assert_eq!(bus.read_bytes(text_ptr, 5), b"HELLO".to_vec());
30204    }
30205
30206    #[test]
30207    fn tegettext_returns_nil_handle_when_te_handle_is_zero() {
30208        // Defensive: a NIL TEHandle has no TERec to read hText from, so
30209        // the result is NIL. Guards against any future change that would
30210        // crash or read past invalid memory when handed a NIL argument.
30211        let (mut disp, mut cpu, mut bus) = setup();
30212
30213        bus.write_long(TEST_SP, 0); // NIL TEHandle
30214        bus.write_long(TEST_SP + 4, 0xCAFE_BABE); // poison result slot
30215        let result = disp.dispatch_dialog(true, 0x1CB, &mut cpu, &mut bus);
30216        assert!(result.unwrap().is_ok());
30217        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
30218        assert_eq!(bus.read_long(TEST_SP + 4), 0);
30219    }
30220
30221    #[test]
30222    fn tesetselect_clamps_selend_to_text_length_and_pops_args() {
30223        // IM:I I-385: selEnd past end-of-text clamps to teLength.
30224        let (mut disp, mut cpu, mut bus) = setup();
30225        let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
30226        let te_ptr = bus.read_long(te_handle);
30227        assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_LENGTH_OFFSET), 5);
30228
30229        bus.write_long(TEST_SP, te_handle); // hTE
30230        bus.write_long(TEST_SP + 4, 999); // selEnd
30231        bus.write_long(TEST_SP + 8, 2); // selStart
30232        let result = disp.dispatch_dialog(true, 0x1D1, &mut cpu, &mut bus);
30233        assert!(result.unwrap().is_ok());
30234        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 12);
30235
30236        let te_ptr = bus.read_long(te_handle);
30237        assert_eq!(
30238            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
30239            2
30240        );
30241        assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET), 5);
30242    }
30243
30244    #[test]
30245    fn tedelete_removes_selection_without_touching_scrap_and_pops_arg() {
30246        // IM:I I-387: TEDelete deletes selection without writing TextEdit scrap.
30247        let (mut disp, mut cpu, mut bus) = setup();
30248        let existing_scrap = TrapDispatcher::allocate_handle_with_data(&mut bus, 2);
30249        let existing_ptr = bus.read_long(existing_scrap);
30250        bus.write_bytes(existing_ptr, b"QQ");
30251        bus.write_long(crate::memory::globals::addr::TE_SCRP_HANDLE, existing_scrap);
30252        bus.write_word(crate::memory::globals::addr::TE_SCRP_LENGTH, 2);
30253
30254        let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
30255        let te_ptr = bus.read_long(te_handle);
30256        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 1);
30257        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 4);
30258
30259        bus.write_long(TEST_SP, te_handle);
30260        let result = disp.dispatch_dialog(true, 0x1D7, &mut cpu, &mut bus);
30261        assert!(result.unwrap().is_ok());
30262        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
30263        assert_eq!(
30264            TrapDispatcher::te_text_bytes(&bus, te_handle),
30265            b"HO".to_vec()
30266        );
30267
30268        let te_ptr = bus.read_long(te_handle);
30269        assert_eq!(
30270            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
30271            1
30272        );
30273        assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET), 1);
30274        assert_eq!(
30275            bus.read_long(crate::memory::globals::addr::TE_SCRP_HANDLE),
30276            existing_scrap
30277        );
30278        assert_eq!(
30279            bus.read_word(crate::memory::globals::addr::TE_SCRP_LENGTH),
30280            2
30281        );
30282        assert_eq!(bus.read_bytes(existing_ptr, 2), b"QQ".to_vec());
30283    }
30284
30285    #[test]
30286    fn tedelete_insertion_point_selection_is_noop_and_pops_arg() {
30287        // IM:I I-387: insertion-point selection means "nothing happens".
30288        let (mut disp, mut cpu, mut bus) = setup();
30289        let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
30290        let te_ptr = bus.read_long(te_handle);
30291        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 3);
30292        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 3);
30293
30294        bus.write_long(TEST_SP, te_handle);
30295        let result = disp.dispatch_dialog(true, 0x1D7, &mut cpu, &mut bus);
30296        assert!(result.unwrap().is_ok());
30297        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
30298        assert_eq!(
30299            TrapDispatcher::te_text_bytes(&bus, te_handle),
30300            b"HELLO".to_vec()
30301        );
30302        assert_eq!(
30303            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
30304            3
30305        );
30306        assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET), 3);
30307    }
30308
30309    #[test]
30310    fn teupdate_pascal_form_redraws_text_and_pops_inline_rect_and_tehandle() {
30311        // IM:I I-387; Inside Macintosh: Text 1993, 2-88.
30312        let (mut disp, mut cpu, mut bus) = setup_with_port();
30313        let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
30314        let (screen_base, row_bytes, _screen_w, _screen_h, _pixel_size) = disp.screen_mode;
30315        for i in 0..(row_bytes * 80) {
30316            bus.write_byte(screen_base + i, 0);
30317        }
30318
30319        bus.write_long(TEST_SP, te_handle);
30320        bus.write_word(TEST_SP + 4, 0); // rUpdate.top
30321        bus.write_word(TEST_SP + 6, 0); // rUpdate.left
30322        bus.write_word(TEST_SP + 8, 40); // rUpdate.bottom
30323        bus.write_word(TEST_SP + 10, 120); // rUpdate.right
30324
30325        let result = disp.dispatch_dialog(true, 0x1D3, &mut cpu, &mut bus);
30326        assert!(result.unwrap().is_ok());
30327        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 12);
30328        assert_eq!(
30329            TrapDispatcher::te_text_bytes(&bus, te_handle),
30330            b"HELLO".to_vec()
30331        );
30332        let drew_any_pixel =
30333            (0..(row_bytes * 80)).any(|offset| bus.read_byte(screen_base + offset) != 0);
30334        assert!(
30335            drew_any_pixel,
30336            "TEUpdate should draw at least one non-white pixel for visible text"
30337        );
30338    }
30339
30340    #[test]
30341    fn teupdate_systemless_theme_routes_active_caret_through_provider() {
30342        let (mut classic, mut classic_cpu, mut classic_bus) = setup_with_port();
30343        let classic_te_handle = make_te_with_text(&mut classic, &mut classic_bus, b"");
30344        let classic_te_ptr = classic_bus.read_long(classic_te_handle);
30345        classic_bus.write_word(classic_te_ptr + TrapDispatcher::TE_ACTIVE_OFFSET, 1);
30346        classic_bus.write_word(classic_te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 0);
30347        classic_bus.write_word(classic_te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 0);
30348        let (screen_base, row_bytes, _screen_w, _screen_h, _pixel_size) = classic.screen_mode;
30349        for i in 0..(row_bytes * 80) {
30350            classic_bus.write_byte(screen_base + i, 0);
30351        }
30352        classic_bus.write_long(TEST_SP, classic_te_handle);
30353        classic_bus.write_word(TEST_SP + 4, 0);
30354        classic_bus.write_word(TEST_SP + 6, 0);
30355        classic_bus.write_word(TEST_SP + 8, 40);
30356        classic_bus.write_word(TEST_SP + 10, 120);
30357        let result = classic.dispatch_dialog(true, 0x1D3, &mut classic_cpu, &mut classic_bus);
30358        assert!(result.unwrap().is_ok());
30359
30360        let (mut themed, mut themed_cpu, mut themed_bus) = setup_with_port();
30361        themed.set_ui_theme_id(UiThemeId::SystemlessDefault);
30362        let themed_te_handle = make_te_with_text(&mut themed, &mut themed_bus, b"");
30363        let themed_te_ptr = themed_bus.read_long(themed_te_handle);
30364        themed_bus.write_word(themed_te_ptr + TrapDispatcher::TE_ACTIVE_OFFSET, 1);
30365        themed_bus.write_word(themed_te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 0);
30366        themed_bus.write_word(themed_te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 0);
30367        for i in 0..(row_bytes * 80) {
30368            themed_bus.write_byte(screen_base + i, 0);
30369        }
30370        themed_bus.write_long(TEST_SP, themed_te_handle);
30371        themed_bus.write_word(TEST_SP + 4, 0);
30372        themed_bus.write_word(TEST_SP + 6, 0);
30373        themed_bus.write_word(TEST_SP + 8, 40);
30374        themed_bus.write_word(TEST_SP + 10, 120);
30375        let result = themed.dispatch_dialog(true, 0x1D3, &mut themed_cpu, &mut themed_bus);
30376        assert!(result.unwrap().is_ok());
30377
30378        assert!(
30379            screen_pixel_is_set(&classic_bus, screen_base, row_bytes, 1, 0),
30380            "classic TEUpdate should draw a one-pixel caret for an active empty insertion point"
30381        );
30382        assert!(
30383            screen_pixel_is_set(&themed_bus, screen_base, row_bytes, 0, 0),
30384            "systemless-default TEUpdate should draw provider-owned caret chrome"
30385        );
30386    }
30387
30388    #[test]
30389    fn teactivate_and_tedeactivate_repaint_empty_insertion_caret() {
30390        // IM:I I-385 and Text 1993 p. 2-80: TEActivate displays the caret
30391        // when the active selection is an insertion point; TEDeactivate
30392        // removes it without changing the selection offsets.
30393        let (mut disp, mut cpu, mut bus) = setup_with_port();
30394        let te_handle = make_te_with_text(&mut disp, &mut bus, b"");
30395        let te_ptr = bus.read_long(te_handle);
30396        let (screen_base, row_bytes, _screen_w, _screen_h, _pixel_size) = disp.screen_mode;
30397        for i in 0..(row_bytes * 80) {
30398            bus.write_byte(screen_base + i, 0);
30399        }
30400        bus.write_word(te_ptr + TrapDispatcher::TE_ACTIVE_OFFSET, 0);
30401        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 0);
30402        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 0);
30403
30404        cpu.write_reg(Register::A7, TEST_SP);
30405        bus.write_long(TEST_SP, te_handle);
30406        let result = disp.dispatch_dialog(true, 0x1D8, &mut cpu, &mut bus);
30407        assert!(result.unwrap().is_ok());
30408        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
30409        assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_ACTIVE_OFFSET), 1);
30410        assert_eq!(
30411            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
30412            0
30413        );
30414        assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET), 0);
30415        assert!(
30416            screen_pixel_is_set(&bus, screen_base, row_bytes, 1, 0),
30417            "TEActivate should paint the visible insertion caret"
30418        );
30419
30420        cpu.write_reg(Register::A7, TEST_SP);
30421        bus.write_long(TEST_SP, te_handle);
30422        let result = disp.dispatch_dialog(true, 0x1D9, &mut cpu, &mut bus);
30423        assert!(result.unwrap().is_ok());
30424        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
30425        assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_ACTIVE_OFFSET), 0);
30426        assert_eq!(
30427            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
30428            0
30429        );
30430        assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET), 0);
30431        assert!(
30432            !screen_pixel_is_set(&bus, screen_base, row_bytes, 1, 0),
30433            "TEDeactivate should erase the insertion caret"
30434        );
30435    }
30436
30437    #[test]
30438    fn teupdate_classic_active_selection_inverts_selected_text_range() {
30439        fn render_classic_selection(selected: bool) -> u64 {
30440            let (mut disp, mut cpu, mut bus) = setup_with_port();
30441            let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
30442            let te_ptr = bus.read_long(te_handle);
30443            bus.write_word(te_ptr + TrapDispatcher::TE_ACTIVE_OFFSET, 1);
30444            bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 0);
30445            bus.write_word(
30446                te_ptr + TrapDispatcher::TE_SEL_END_OFFSET,
30447                if selected { 3 } else { 0 },
30448            );
30449            let line_height = bus.read_word(te_ptr + TrapDispatcher::TE_LINE_HEIGHT_OFFSET) as i16;
30450            let port = bus.read_long(te_ptr + TrapDispatcher::TE_IN_PORT_OFFSET);
30451            let base = bus.read_long(port + 2);
30452            let row_bytes = u32::from(bus.read_word(port + 6) & 0x3FFF);
30453            for i in 0..(row_bytes * 80) {
30454                bus.write_byte(base + i, 0);
30455            }
30456            bus.write_long(TEST_SP, te_handle);
30457            bus.write_word(TEST_SP + 4, 0);
30458            bus.write_word(TEST_SP + 6, 0);
30459            bus.write_word(TEST_SP + 8, 40);
30460            bus.write_word(TEST_SP + 10, 120);
30461            let result = disp.dispatch_dialog(true, 0x1D3, &mut cpu, &mut bus);
30462            assert!(result.unwrap().is_ok());
30463            sum_screen_bytes(&bus, base, row_bytes, 0, 0, line_height, 32)
30464        }
30465
30466        // IM:I I-375/I-385 and Text 1993 p. 2-80: active non-empty TextEdit
30467        // selections are highlighted. Classic highlighting is inverse video
30468        // over the selected character range, so the selected render must
30469        // differ from the same active insertion-point redraw.
30470        let selected = render_classic_selection(true);
30471        let unselected = render_classic_selection(false);
30472        assert!(
30473            selected != unselected,
30474            "classic TEUpdate should invert active non-empty selection pixels; selected={selected}, unselected={unselected}"
30475        );
30476    }
30477
30478    #[test]
30479    fn teupdate_systemless_theme_routes_multiline_selection_through_provider() {
30480        fn render_themed_multiline_selection(selected: bool) -> (u16, u16, i16, u64, u64) {
30481            let (mut disp, mut cpu, mut bus) = setup_with_port();
30482            disp.set_ui_theme_id(UiThemeId::SystemlessDefault);
30483            let te_handle = make_te_with_text(&mut disp, &mut bus, b"A\rB");
30484            let te_ptr = bus.read_long(te_handle);
30485            bus.write_word(te_ptr + TrapDispatcher::TE_ACTIVE_OFFSET, 1);
30486            bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 0);
30487            bus.write_word(
30488                te_ptr + TrapDispatcher::TE_SEL_END_OFFSET,
30489                if selected { 3 } else { 0 },
30490            );
30491            let line_height = bus.read_word(te_ptr + TrapDispatcher::TE_LINE_HEIGHT_OFFSET) as i16;
30492            let (base, row_bytes, _screen_w, _screen_h, _pixel_size) = disp.screen_mode;
30493            for i in 0..(row_bytes * 80) {
30494                bus.write_byte(base + i, 0);
30495            }
30496            bus.write_long(TEST_SP, te_handle);
30497            bus.write_word(TEST_SP + 4, 0);
30498            bus.write_word(TEST_SP + 6, 0);
30499            bus.write_word(TEST_SP + 8, 40);
30500            bus.write_word(TEST_SP + 10, 120);
30501            let result = disp.dispatch_dialog(true, 0x1D3, &mut cpu, &mut bus);
30502            assert!(result.unwrap().is_ok());
30503
30504            let first_line = sum_screen_bytes(&bus, base, row_bytes, 0, 0, line_height, 24);
30505            let second_line =
30506                sum_screen_bytes(&bus, base, row_bytes, line_height, 0, line_height * 2, 24);
30507            (
30508                bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
30509                bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET),
30510                line_height,
30511                first_line,
30512                second_line,
30513            )
30514        }
30515
30516        let selected = render_themed_multiline_selection(true);
30517        let unselected = render_themed_multiline_selection(false);
30518
30519        // Text 1993, appendix C: TERec.active marks whether the selection
30520        // range is highlighted or the caret is displayed; selStart/selEnd are
30521        // byte offsets into the contiguous selection range.
30522        assert_eq!((selected.0, selected.1), (0, 3));
30523        assert_eq!((unselected.0, unselected.1), (0, 0));
30524        assert_eq!(selected.2, unselected.2);
30525        assert!(
30526            selected.4 != unselected.4,
30527            "systemless-default TEUpdate should draw provider selection chrome on the second selected line"
30528        );
30529    }
30530
30531    #[test]
30532    fn teupdate_systemless_theme_renders_reversed_selection_without_rewriting_offsets() {
30533        fn render_themed_selection(sel_start: u16, sel_end: u16) -> (u16, u16, u16, i16, u32, u32) {
30534            let (mut disp, mut cpu, mut bus) = setup_with_port();
30535            disp.set_ui_theme_id(UiThemeId::SystemlessDefault);
30536            let te_handle = make_te_with_text(&mut disp, &mut bus, b"A\rB");
30537            let te_ptr = bus.read_long(te_handle);
30538            bus.write_word(te_ptr + TrapDispatcher::TE_ACTIVE_OFFSET, 1);
30539            bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, sel_start);
30540            bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, sel_end);
30541            let line_height = bus.read_word(te_ptr + TrapDispatcher::TE_LINE_HEIGHT_OFFSET) as i16;
30542            let (base, row_bytes, _screen_w, _screen_h, _pixel_size) = disp.screen_mode;
30543            for i in 0..(row_bytes * 80) {
30544                bus.write_byte(base + i, 0);
30545            }
30546
30547            bus.write_long(TEST_SP, te_handle);
30548            bus.write_word(TEST_SP + 4, 0);
30549            bus.write_word(TEST_SP + 6, 0);
30550            bus.write_word(TEST_SP + 8, 40);
30551            bus.write_word(TEST_SP + 10, 120);
30552            let result = disp.dispatch_dialog(true, 0x1D3, &mut cpu, &mut bus);
30553            assert!(result.unwrap().is_ok());
30554            assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 12);
30555
30556            let first_line_bottom_band =
30557                count_set_pixels(&bus, base, row_bytes, line_height - 2, 0, line_height, 24);
30558            let second_line_bottom_band = count_set_pixels(
30559                &bus,
30560                base,
30561                row_bytes,
30562                line_height * 2 - 2,
30563                0,
30564                line_height * 2,
30565                24,
30566            );
30567
30568            (
30569                bus.read_word(te_ptr + TrapDispatcher::TE_LENGTH_OFFSET),
30570                bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
30571                bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET),
30572                line_height,
30573                first_line_bottom_band,
30574                second_line_bottom_band,
30575            )
30576        }
30577
30578        let forward = render_themed_selection(0, 3);
30579        let reversed = render_themed_selection(3, 0);
30580
30581        // IM:I I-385 says TESetSelect owns selection-range changes, while
30582        // IM:I I-387 / Text 1993 p. 2-88 define TEUpdate as redraw. The theme
30583        // path can normalize reversed endpoints for drawing but must not
30584        // rewrite the guest TERec.
30585        assert_eq!(forward.0, 3);
30586        assert_eq!(reversed.0, 3);
30587        assert_eq!((forward.1, forward.2), (0, 3));
30588        assert_eq!((reversed.1, reversed.2), (3, 0));
30589        assert_eq!(forward.3, reversed.3);
30590        assert!(forward.4 > 0 && forward.5 > 0);
30591        assert_eq!(
30592            (reversed.4, reversed.5),
30593            (forward.4, forward.5),
30594            "systemless-default TEUpdate should render reversed selection ranges like their normalized range without rewriting selStart/selEnd"
30595        );
30596    }
30597
30598    #[test]
30599    fn teupdate_systemless_theme_routes_inactive_outline_selection_through_provider() {
30600        let (mut inactive_plain, mut inactive_plain_cpu, mut inactive_plain_bus) =
30601            setup_with_port();
30602        inactive_plain.set_ui_theme_id(UiThemeId::SystemlessDefault);
30603        let inactive_plain_te_handle =
30604            make_te_with_text(&mut inactive_plain, &mut inactive_plain_bus, b"HELLO");
30605        let inactive_plain_te_ptr = inactive_plain_bus.read_long(inactive_plain_te_handle);
30606        inactive_plain_bus.write_word(inactive_plain_te_ptr + TrapDispatcher::TE_ACTIVE_OFFSET, 0);
30607        inactive_plain_bus.write_word(
30608            inactive_plain_te_ptr + TrapDispatcher::TE_SEL_START_OFFSET,
30609            0,
30610        );
30611        inactive_plain_bus.write_word(inactive_plain_te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 3);
30612        let (plain_base, plain_row_bytes, _screen_w, _screen_h, _pixel_size) =
30613            inactive_plain.screen_mode;
30614        for i in 0..(plain_row_bytes * 80) {
30615            inactive_plain_bus.write_byte(plain_base + i, 0);
30616        }
30617        inactive_plain_bus.write_long(TEST_SP, inactive_plain_te_handle);
30618        inactive_plain_bus.write_word(TEST_SP + 4, 0);
30619        inactive_plain_bus.write_word(TEST_SP + 6, 0);
30620        inactive_plain_bus.write_word(TEST_SP + 8, 40);
30621        inactive_plain_bus.write_word(TEST_SP + 10, 120);
30622        let result = inactive_plain.dispatch_dialog(
30623            true,
30624            0x1D3,
30625            &mut inactive_plain_cpu,
30626            &mut inactive_plain_bus,
30627        );
30628        assert!(result.unwrap().is_ok());
30629
30630        let (mut inactive_outline, mut inactive_outline_cpu, mut inactive_outline_bus) =
30631            setup_with_port();
30632        inactive_outline.set_ui_theme_id(UiThemeId::SystemlessDefault);
30633        let inactive_outline_te_handle =
30634            make_te_with_text(&mut inactive_outline, &mut inactive_outline_bus, b"HELLO");
30635        let inactive_outline_te_ptr = inactive_outline_bus.read_long(inactive_outline_te_handle);
30636        inactive_outline_bus.write_word(
30637            inactive_outline_te_ptr + TrapDispatcher::TE_ACTIVE_OFFSET,
30638            0,
30639        );
30640        inactive_outline_bus.write_word(
30641            inactive_outline_te_ptr + TrapDispatcher::TE_SEL_START_OFFSET,
30642            0,
30643        );
30644        inactive_outline_bus.write_word(
30645            inactive_outline_te_ptr + TrapDispatcher::TE_SEL_END_OFFSET,
30646            3,
30647        );
30648        let inactive_line_height = inactive_outline_bus
30649            .read_word(inactive_outline_te_ptr + TrapDispatcher::TE_LINE_HEIGHT_OFFSET)
30650            as i16;
30651        let (outline_base, outline_row_bytes, _screen_w, _screen_h, _pixel_size) =
30652            inactive_outline.screen_mode;
30653        for i in 0..(outline_row_bytes * 80) {
30654            inactive_outline_bus.write_byte(outline_base + i, 0);
30655        }
30656
30657        inactive_outline_cpu.write_reg(Register::A7, TEST_SP);
30658        inactive_outline_bus.write_word(TEST_SP, 0x000E);
30659        inactive_outline_bus.write_long(TEST_SP + 2, inactive_outline_te_handle);
30660        inactive_outline_bus.write_word(TEST_SP + 6, TrapDispatcher::TE_BIT_SET as u16);
30661        inactive_outline_bus.write_word(TEST_SP + 8, TrapDispatcher::TE_FEATURE_OUTLINE_HILITE);
30662        inactive_outline_bus.write_word(TEST_SP + 10, 0xBEEF);
30663        let result = inactive_outline.dispatch_dialog(
30664            true,
30665            0x03D,
30666            &mut inactive_outline_cpu,
30667            &mut inactive_outline_bus,
30668        );
30669        assert!(result.unwrap().is_ok());
30670        assert_eq!(inactive_outline_bus.read_word(TEST_SP + 10), 0);
30671
30672        inactive_outline_cpu.write_reg(Register::A7, TEST_SP);
30673        inactive_outline_bus.write_long(TEST_SP, inactive_outline_te_handle);
30674        inactive_outline_bus.write_word(TEST_SP + 4, 0);
30675        inactive_outline_bus.write_word(TEST_SP + 6, 0);
30676        inactive_outline_bus.write_word(TEST_SP + 8, 40);
30677        inactive_outline_bus.write_word(TEST_SP + 10, 120);
30678        let result = inactive_outline.dispatch_dialog(
30679            true,
30680            0x1D3,
30681            &mut inactive_outline_cpu,
30682            &mut inactive_outline_bus,
30683        );
30684        assert!(result.unwrap().is_ok());
30685
30686        let (mut active, mut active_cpu, mut active_bus) = setup_with_port();
30687        active.set_ui_theme_id(UiThemeId::SystemlessDefault);
30688        let active_te_handle = make_te_with_text(&mut active, &mut active_bus, b"HELLO");
30689        let active_te_ptr = active_bus.read_long(active_te_handle);
30690        active_bus.write_word(active_te_ptr + TrapDispatcher::TE_ACTIVE_OFFSET, 1);
30691        active_bus.write_word(active_te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 0);
30692        active_bus.write_word(active_te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 3);
30693        let active_line_height =
30694            active_bus.read_word(active_te_ptr + TrapDispatcher::TE_LINE_HEIGHT_OFFSET) as i16;
30695        let (active_base, active_row_bytes, _screen_w, _screen_h, _pixel_size) = active.screen_mode;
30696        for i in 0..(active_row_bytes * 80) {
30697            active_bus.write_byte(active_base + i, 0);
30698        }
30699        active_bus.write_long(TEST_SP, active_te_handle);
30700        active_bus.write_word(TEST_SP + 4, 0);
30701        active_bus.write_word(TEST_SP + 6, 0);
30702        active_bus.write_word(TEST_SP + 8, 40);
30703        active_bus.write_word(TEST_SP + 10, 120);
30704        let result = active.dispatch_dialog(true, 0x1D3, &mut active_cpu, &mut active_bus);
30705        assert!(result.unwrap().is_ok());
30706
30707        // Text 1993 describes outline highlighting as framing an inactive
30708        // selection; TEActivate/TEDeactivate tie it to teFOutlineHilite.
30709        let inactive_plain_top_band = count_set_pixels(
30710            &inactive_plain_bus,
30711            plain_base,
30712            plain_row_bytes,
30713            0,
30714            0,
30715            1,
30716            24,
30717        );
30718        let inactive_outline_top_band = count_set_pixels(
30719            &inactive_outline_bus,
30720            outline_base,
30721            outline_row_bytes,
30722            0,
30723            0,
30724            1,
30725            24,
30726        );
30727        assert_eq!(
30728            inactive_plain_top_band, 0,
30729            "inactive TextEdit selection without teFOutlineHilite should not draw provider selection chrome"
30730        );
30731        assert!(
30732            inactive_outline_top_band > 0,
30733            "inactive TextEdit selection with teFOutlineHilite should draw provider outline chrome"
30734        );
30735        assert_eq!(
30736            inactive_outline_bus
30737                .read_word(inactive_outline_te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
30738            0
30739        );
30740        assert_eq!(
30741            inactive_outline_bus
30742                .read_word(inactive_outline_te_ptr + TrapDispatcher::TE_SEL_END_OFFSET),
30743            3
30744        );
30745        assert_eq!(inactive_line_height, active_line_height);
30746
30747        let inactive_bottom_band = count_set_pixels(
30748            &inactive_outline_bus,
30749            outline_base,
30750            outline_row_bytes,
30751            inactive_line_height - 2,
30752            0,
30753            inactive_line_height,
30754            24,
30755        );
30756        let active_bottom_band = count_set_pixels(
30757            &active_bus,
30758            active_base,
30759            active_row_bytes,
30760            active_line_height - 2,
30761            0,
30762            active_line_height,
30763            24,
30764        );
30765        assert!(
30766            active_bottom_band > inactive_bottom_band,
30767            "inactive outline selection should stay outline-only instead of drawing active bottom-stripe chrome"
30768        );
30769    }
30770
30771    #[test]
30772    fn teupdate_c_rect_pointer_form_is_accepted_and_pops_eight_bytes() {
30773        // Inside Macintosh: Text 1993, 2-88 update semantics plus
30774        // MPW C call-shape compatibility (Rect* + TEHandle).
30775        let (mut disp, mut cpu, mut bus) = setup_with_port();
30776        let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
30777        let rect_ptr = bus.alloc(8);
30778        bus.write_word(rect_ptr, 10);
30779        bus.write_word(rect_ptr + 2, 10);
30780        bus.write_word(rect_ptr + 4, 40);
30781        bus.write_word(rect_ptr + 6, 120);
30782
30783        bus.write_long(TEST_SP, te_handle);
30784        bus.write_long(TEST_SP + 4, rect_ptr);
30785
30786        let result = disp.dispatch_dialog(true, 0x1D3, &mut cpu, &mut bus);
30787        assert!(result.unwrap().is_ok());
30788        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
30789        assert_eq!(
30790            TrapDispatcher::te_text_bytes(&bus, te_handle),
30791            b"HELLO".to_vec()
30792        );
30793    }
30794
30795    #[test]
30796    fn teupdate_preserves_te_record_state_across_redraw() {
30797        // TEUpdate is a redraw operation and MUST NOT mutate hText,
30798        // teLength, selStart, or selEnd. Per IM:I I-387 + IM:Text 1993
30799        // p. 2-88: text bytes "HELLO" preserved, teLength==5,
30800        // selStart==1, selEnd==4 after both C-form and Pascal-form
30801        // TEUpdate calls.
30802        let (mut disp, mut cpu, mut bus) = setup_with_port();
30803        let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
30804        let te_ptr = bus.read_long(te_handle);
30805        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 1);
30806        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 4);
30807
30808        // C-form dispatch (8-byte pop).
30809        let rect_ptr = bus.alloc(8);
30810        bus.write_word(rect_ptr, 10);
30811        bus.write_word(rect_ptr + 2, 10);
30812        bus.write_word(rect_ptr + 4, 40);
30813        bus.write_word(rect_ptr + 6, 120);
30814        bus.write_long(TEST_SP, te_handle);
30815        bus.write_long(TEST_SP + 4, rect_ptr);
30816        let result = disp.dispatch_dialog(true, 0x1D3, &mut cpu, &mut bus);
30817        assert!(result.unwrap().is_ok());
30818        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
30819
30820        // Pascal-form dispatch on the same TE record (12-byte pop):
30821        // hTE at sp+0, inlined rect at sp+4..sp+11.
30822        cpu.write_reg(Register::A7, TEST_SP);
30823        bus.write_long(TEST_SP, te_handle);
30824        bus.write_word(TEST_SP + 4, 10);
30825        bus.write_word(TEST_SP + 6, 10);
30826        bus.write_word(TEST_SP + 8, 40);
30827        bus.write_word(TEST_SP + 10, 120);
30828        let result = disp.dispatch_dialog(true, 0x1D3, &mut cpu, &mut bus);
30829        assert!(result.unwrap().is_ok());
30830        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 12);
30831
30832        // State preservation contract.
30833        assert_eq!(
30834            TrapDispatcher::te_text_bytes(&bus, te_handle),
30835            b"HELLO".to_vec()
30836        );
30837        assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_LENGTH_OFFSET), 5);
30838        assert_eq!(
30839            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
30840            1
30841        );
30842        assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET), 4);
30843    }
30844
30845    #[test]
30846    fn teactivate_sets_active_flag_preserves_selection_and_pops_arg() {
30847        // IM:I I-385; Inside Macintosh: Text 1993, 2-80.
30848        let (mut disp, mut cpu, mut bus) = setup();
30849        let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
30850        let te_ptr = bus.read_long(te_handle);
30851        bus.write_word(te_ptr + TrapDispatcher::TE_ACTIVE_OFFSET, 0);
30852        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 1);
30853        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 4);
30854
30855        bus.write_long(TEST_SP, te_handle);
30856        let result = disp.dispatch_dialog(true, 0x1D8, &mut cpu, &mut bus);
30857        assert!(result.unwrap().is_ok());
30858        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
30859        assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_ACTIVE_OFFSET), 1);
30860        assert_eq!(
30861            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
30862            1
30863        );
30864        assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET), 4);
30865    }
30866
30867    #[test]
30868    fn tedeactivate_clears_active_flag_preserves_selection_and_pops_arg() {
30869        // IM:I I-385; Inside Macintosh: Text 1993, 2-80.
30870        let (mut disp, mut cpu, mut bus) = setup();
30871        let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
30872        let te_ptr = bus.read_long(te_handle);
30873        bus.write_word(te_ptr + TrapDispatcher::TE_ACTIVE_OFFSET, 1);
30874        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 2);
30875        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 5);
30876
30877        bus.write_long(TEST_SP, te_handle);
30878        let result = disp.dispatch_dialog(true, 0x1D9, &mut cpu, &mut bus);
30879        assert!(result.unwrap().is_ok());
30880        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
30881        assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_ACTIVE_OFFSET), 0);
30882        assert_eq!(
30883            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
30884            2
30885        );
30886        assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET), 5);
30887    }
30888
30889    #[test]
30890    fn teclick_consumes_point_extend_and_tehandle_arguments() {
30891        // Inside Macintosh Volume I (1985), p. I-376 and
30892        // Inside Macintosh: Text (1993), p. 2-85.
30893        let (mut disp, mut cpu, mut bus) = setup();
30894        let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
30895
30896        bus.write_long(TEST_SP, te_handle); // hTE
30897        bus.write_word(TEST_SP + 4, 0xFF00); // extend=TRUE in high byte
30898        bus.write_word(TEST_SP + 6, 18); // pt.v
30899        bus.write_word(TEST_SP + 8, 27); // pt.h
30900
30901        let result = disp.dispatch_dialog(true, 0x1D4, &mut cpu, &mut bus);
30902        assert!(result.unwrap().is_ok());
30903        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 10);
30904    }
30905
30906    #[test]
30907    fn teclick_empty_text_keeps_insertion_point_at_zero() {
30908        // Inside Macintosh Volume I (1985), p. I-376: TEClick places the
30909        // insertion point from mouse position; with empty text only offset 0
30910        // is valid.
30911        let (mut disp, mut cpu, mut bus) = setup();
30912        let te_handle = make_te_with_text(&mut disp, &mut bus, b"");
30913        let te_ptr = bus.read_long(te_handle);
30914        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 0);
30915        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 0);
30916
30917        bus.write_long(TEST_SP, te_handle); // hTE
30918        bus.write_word(TEST_SP + 4, 0x0000); // extend=FALSE
30919        bus.write_word(TEST_SP + 6, 12); // pt.v
30920        bus.write_word(TEST_SP + 8, 40); // pt.h
30921
30922        let result = disp.dispatch_dialog(true, 0x1D4, &mut cpu, &mut bus);
30923        assert!(result.unwrap().is_ok());
30924        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 10);
30925        assert_eq!(
30926            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
30927            0
30928        );
30929        assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET), 0);
30930    }
30931
30932    #[test]
30933    fn teclick_preserves_terec_active_flag_and_telength() {
30934        // Both BasiliskII System 7.5.3 ROM and Systemless HLE agree that
30935        // TEClick must not mutate the active flag (owned by
30936        // TEActivate/TEDeactivate per IM:I I-385) or teLength
30937        // (owned by TESetText/TEKey/TEDelete/TEInsert). Inside
30938        // Macintosh Volume I (1985), p. I-376; Inside Macintosh:
30939        // Text (1993), p. 2-85.
30940        let (mut disp, mut cpu, mut bus) = setup();
30941        let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
30942        let te_ptr = bus.read_long(te_handle);
30943        bus.write_word(te_ptr + TrapDispatcher::TE_ACTIVE_OFFSET, 0xFF);
30944        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 2);
30945        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 4);
30946        let pre_active = bus.read_word(te_ptr + TrapDispatcher::TE_ACTIVE_OFFSET);
30947        let pre_telength = bus.read_word(te_ptr + TrapDispatcher::TE_LENGTH_OFFSET);
30948
30949        bus.write_long(TEST_SP, te_handle);
30950        bus.write_word(TEST_SP + 4, 0x0000); // extend=FALSE
30951        bus.write_word(TEST_SP + 6, 8); // pt.v
30952        bus.write_word(TEST_SP + 8, 32); // pt.h
30953        let result = disp.dispatch_dialog(true, 0x1D4, &mut cpu, &mut bus);
30954        assert!(result.unwrap().is_ok());
30955        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 10);
30956        assert_eq!(
30957            bus.read_word(te_ptr + TrapDispatcher::TE_ACTIVE_OFFSET),
30958            pre_active
30959        );
30960        assert_eq!(
30961            bus.read_word(te_ptr + TrapDispatcher::TE_LENGTH_OFFSET),
30962            pre_telength
30963        );
30964    }
30965
30966    #[test]
30967    fn teclick_repeated_calls_balance_stack_no_drift() {
30968        // An 8-call composition of TEClick keeps the A7 advance balanced at
30969        // exactly 8 * 10 = 80 bytes; per-call pop errors that cancel
30970        // within a single call but drift over many invocations are
30971        // detected here. A7 is reset to TEST_SP between iterations so
30972        // each call exercises the SP discipline independently.
30973        let (mut disp, mut cpu, mut bus) = setup();
30974        let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
30975
30976        for k in 0..8i16 {
30977            cpu.write_reg(Register::A7, TEST_SP);
30978            bus.write_long(TEST_SP, te_handle);
30979            bus.write_word(TEST_SP + 4, 0x0000); // extend=FALSE
30980            bus.write_word(TEST_SP + 6, (4 + k) as u16); // pt.v varied
30981            bus.write_word(TEST_SP + 8, (16 + k * 3) as u16); // pt.h varied
30982            let result = disp.dispatch_dialog(true, 0x1D4, &mut cpu, &mut bus);
30983            assert!(result.unwrap().is_ok());
30984            assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 10);
30985        }
30986    }
30987
30988    fn textedit_selection_replacement_for_theme(
30989        theme_id: UiThemeId,
30990    ) -> (Vec<u8>, u16, u16, u16, u32) {
30991        let (mut disp, mut cpu, mut bus) = setup_with_port();
30992        disp.set_ui_theme_id(theme_id);
30993        let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
30994        let te_ptr = bus.read_long(te_handle);
30995        bus.write_word(te_ptr + TrapDispatcher::TE_ACTIVE_OFFSET, 1);
30996        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 1);
30997        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 4);
30998
30999        bus.write_long(TEST_SP, te_handle);
31000        bus.write_word(TEST_SP + 4, 0);
31001        bus.write_word(TEST_SP + 6, 0);
31002        bus.write_word(TEST_SP + 8, 40);
31003        bus.write_word(TEST_SP + 10, 120);
31004        let result = disp.dispatch_dialog(true, 0x1D3, &mut cpu, &mut bus);
31005        assert!(result.unwrap().is_ok());
31006        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 12);
31007
31008        cpu.write_reg(Register::A7, TEST_SP);
31009        bus.write_long(TEST_SP, te_handle);
31010        bus.write_word(TEST_SP + 4, u16::from(b'Y'));
31011        let result = disp.dispatch_dialog(true, 0x1DC, &mut cpu, &mut bus);
31012        assert!(result.unwrap().is_ok());
31013
31014        (
31015            TrapDispatcher::te_text_bytes(&bus, te_handle),
31016            bus.read_word(te_ptr + TrapDispatcher::TE_LENGTH_OFFSET),
31017            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
31018            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET),
31019            cpu.read_reg(Register::A7),
31020        )
31021    }
31022
31023    fn textedit_private_scrap_editing_results_for_theme(
31024        theme_id: UiThemeId,
31025    ) -> TextEditPrivateScrapEditingSnapshot {
31026        let (mut disp, mut cpu, mut bus) = setup();
31027        disp.set_ui_theme_id(theme_id);
31028
31029        let copy_te = make_te_with_text(&mut disp, &mut bus, b"HELLO");
31030        let copy_ptr = bus.read_long(copy_te);
31031        bus.write_word(copy_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 1);
31032        bus.write_word(copy_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 4);
31033        cpu.write_reg(Register::A7, TEST_SP);
31034        bus.write_long(TEST_SP, copy_te);
31035        disp.dispatch_dialog(true, 0x1D5, &mut cpu, &mut bus)
31036            .unwrap()
31037            .unwrap();
31038        let copy = textedit_scrap_edit_case(&bus, copy_te, cpu.read_reg(Register::A7));
31039
31040        let cut_te = make_te_with_text(&mut disp, &mut bus, b"HELLO");
31041        let cut_ptr = bus.read_long(cut_te);
31042        bus.write_word(cut_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 1);
31043        bus.write_word(cut_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 4);
31044        cpu.write_reg(Register::A7, TEST_SP);
31045        bus.write_long(TEST_SP, cut_te);
31046        disp.dispatch_dialog(true, 0x1D6, &mut cpu, &mut bus)
31047            .unwrap()
31048            .unwrap();
31049        let cut = textedit_scrap_edit_case(&bus, cut_te, cpu.read_reg(Register::A7));
31050
31051        let paste_te = make_te_with_text(&mut disp, &mut bus, b"HEXXO");
31052        let paste_ptr = bus.read_long(paste_te);
31053        bus.write_word(paste_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 2);
31054        bus.write_word(paste_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 4);
31055        let paste_scrap = TrapDispatcher::allocate_handle_with_data(&mut bus, 2);
31056        let paste_scrap_ptr = bus.read_long(paste_scrap);
31057        bus.write_bytes(paste_scrap_ptr, b"LL");
31058        bus.write_long(crate::memory::globals::addr::TE_SCRP_HANDLE, paste_scrap);
31059        bus.write_word(crate::memory::globals::addr::TE_SCRP_LENGTH, 2);
31060        cpu.write_reg(Register::A7, TEST_SP);
31061        bus.write_long(TEST_SP, paste_te);
31062        disp.dispatch_dialog(true, 0x1DB, &mut cpu, &mut bus)
31063            .unwrap()
31064            .unwrap();
31065        let paste = textedit_scrap_edit_case(&bus, paste_te, cpu.read_reg(Register::A7));
31066
31067        let delete_te = make_te_with_text(&mut disp, &mut bus, b"HELLO");
31068        let delete_ptr = bus.read_long(delete_te);
31069        bus.write_word(delete_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 1);
31070        bus.write_word(delete_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 4);
31071        let delete_scrap = TrapDispatcher::allocate_handle_with_data(&mut bus, 2);
31072        let delete_scrap_ptr = bus.read_long(delete_scrap);
31073        bus.write_bytes(delete_scrap_ptr, b"QQ");
31074        bus.write_long(crate::memory::globals::addr::TE_SCRP_HANDLE, delete_scrap);
31075        bus.write_word(crate::memory::globals::addr::TE_SCRP_LENGTH, 2);
31076        cpu.write_reg(Register::A7, TEST_SP);
31077        bus.write_long(TEST_SP, delete_te);
31078        disp.dispatch_dialog(true, 0x1D7, &mut cpu, &mut bus)
31079            .unwrap()
31080            .unwrap();
31081        let delete = textedit_scrap_edit_case(&bus, delete_te, cpu.read_reg(Register::A7));
31082
31083        TextEditPrivateScrapEditingSnapshot {
31084            copy,
31085            cut,
31086            paste,
31087            delete,
31088        }
31089    }
31090
31091    #[test]
31092    fn systemless_theme_does_not_change_textedit_selection_offsets() {
31093        // Text 1993 appendix C defines selStart/selEnd as byte offsets into
31094        // the selection range; Text 1993 p. 2-81 says TEKey replaces that
31095        // range and positions the insertion point just past the inserted byte.
31096        // Theme selection chrome must not change those guest-visible fields.
31097        let classic = textedit_selection_replacement_for_theme(UiThemeId::ClassicSystem7);
31098        let themed = textedit_selection_replacement_for_theme(UiThemeId::SystemlessDefault);
31099
31100        assert_eq!(classic.0, b"HYO".to_vec());
31101        assert_eq!(classic.1, 3);
31102        assert_eq!(classic.2, 2);
31103        assert_eq!(classic.3, 2);
31104        assert_eq!(classic.4, TEST_SP + 6);
31105        assert_eq!(
31106            themed, classic,
31107            "systemless-default must not change TextEdit replacement selection offsets"
31108        );
31109    }
31110
31111    #[test]
31112    fn systemless_theme_does_not_change_textedit_private_scrap_editing() {
31113        // IM:I I-373 says TextEdit's scrap is private to TextEdit, not the
31114        // Scrap Manager desk scrap. IM:I I-385..I-387 define TECopy, TECut,
31115        // TEPaste, and TEDelete selection/scrap effects; MTE 1992 p.
31116        // 6-132..6-134 routes dialog edit commands through those TextEdit
31117        // operations. Theme chrome must not change the private-scrap globals,
31118        // TERec text/selection fields, or stack ABI for these edit actions.
31119        let classic = textedit_private_scrap_editing_results_for_theme(UiThemeId::ClassicSystem7);
31120        let themed = textedit_private_scrap_editing_results_for_theme(UiThemeId::SystemlessDefault);
31121
31122        assert_eq!(classic.copy.text, b"HELLO".to_vec());
31123        assert_eq!(classic.copy.selection, (1, 4));
31124        assert_eq!(classic.copy.scrap_length, 3);
31125        assert_eq!(classic.copy.scrap_bytes, b"ELL".to_vec());
31126        assert_eq!(classic.copy.stack_after, TEST_SP + 4);
31127
31128        assert_eq!(classic.cut.text, b"HO".to_vec());
31129        assert_eq!(classic.cut.selection, (1, 1));
31130        assert_eq!(classic.cut.scrap_length, 3);
31131        assert_eq!(classic.cut.scrap_bytes, b"ELL".to_vec());
31132        assert_eq!(classic.cut.stack_after, TEST_SP + 4);
31133
31134        assert_eq!(classic.paste.text, b"HELLO".to_vec());
31135        assert_eq!(classic.paste.selection, (4, 4));
31136        assert_eq!(classic.paste.scrap_length, 2);
31137        assert_eq!(classic.paste.scrap_bytes, b"LL".to_vec());
31138        assert_eq!(classic.paste.stack_after, TEST_SP + 4);
31139
31140        assert_eq!(classic.delete.text, b"HO".to_vec());
31141        assert_eq!(classic.delete.selection, (1, 1));
31142        assert_eq!(classic.delete.scrap_length, 2);
31143        assert_eq!(classic.delete.scrap_bytes, b"QQ".to_vec());
31144        assert_eq!(classic.delete.stack_after, TEST_SP + 4);
31145        assert_eq!(
31146            themed, classic,
31147            "systemless-default must not change TextEdit private-scrap edit semantics"
31148        );
31149    }
31150
31151    #[test]
31152    fn teidle_consumes_tehandle_argument() {
31153        // Inside Macintosh Volume I (1985), p. I-374 and
31154        // Inside Macintosh: Text (1993), p. 2-84.
31155        let (mut disp, mut cpu, mut bus) = setup();
31156        let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
31157
31158        bus.write_long(TEST_SP, te_handle);
31159        let result = disp.dispatch_dialog(true, 0x1DA, &mut cpu, &mut bus);
31160        assert!(result.unwrap().is_ok());
31161        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
31162    }
31163
31164    #[test]
31165    fn teidle_toggles_visible_insertion_caret_after_blink_interval() {
31166        // IM:I I-374 and Text 1993 p. 2-84: TEIdle blinks an active
31167        // insertion caret, but only after the minimum blink interval
31168        // initially set to 32 ticks.
31169        let (mut disp, mut cpu, mut bus) = setup_with_port();
31170        let te_handle = make_te_with_text(&mut disp, &mut bus, b"");
31171        let te_ptr = bus.read_long(te_handle);
31172        let (screen_base, row_bytes, _screen_w, _screen_h, _pixel_size) = disp.screen_mode;
31173        for i in 0..(row_bytes * 80) {
31174            bus.write_byte(screen_base + i, 0);
31175        }
31176        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 0);
31177        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 0);
31178
31179        disp.tick_count = 100;
31180        bus.write_long(crate::memory::globals::addr::TICKS, 100);
31181        cpu.write_reg(Register::A7, TEST_SP);
31182        bus.write_long(TEST_SP, te_handle);
31183        let result = disp.dispatch_dialog(true, 0x1D8, &mut cpu, &mut bus);
31184        assert!(result.unwrap().is_ok());
31185        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
31186        assert!(
31187            screen_pixel_is_set(&bus, screen_base, row_bytes, 1, 0),
31188            "TEActivate should show the insertion caret before TEIdle"
31189        );
31190        assert_eq!(
31191            bus.read_long(te_ptr + TrapDispatcher::TE_CARET_TIME_OFFSET),
31192            100
31193        );
31194        assert_eq!(
31195            bus.read_word(te_ptr + TrapDispatcher::TE_CARET_STATE_OFFSET),
31196            0
31197        );
31198
31199        disp.tick_count = 131;
31200        bus.write_long(crate::memory::globals::addr::TICKS, 131);
31201        cpu.write_reg(Register::A7, TEST_SP);
31202        bus.write_long(TEST_SP, te_handle);
31203        let result = disp.dispatch_dialog(true, 0x1DA, &mut cpu, &mut bus);
31204        assert!(result.unwrap().is_ok());
31205        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
31206        assert!(
31207            screen_pixel_is_set(&bus, screen_base, row_bytes, 1, 0),
31208            "TEIdle before 32 ticks should leave the caret visible"
31209        );
31210        assert_eq!(
31211            bus.read_word(te_ptr + TrapDispatcher::TE_CARET_STATE_OFFSET),
31212            0
31213        );
31214
31215        disp.tick_count = 132;
31216        bus.write_long(crate::memory::globals::addr::TICKS, 132);
31217        cpu.write_reg(Register::A7, TEST_SP);
31218        bus.write_long(TEST_SP, te_handle);
31219        let result = disp.dispatch_dialog(true, 0x1DA, &mut cpu, &mut bus);
31220        assert!(result.unwrap().is_ok());
31221        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
31222        assert!(
31223            !screen_pixel_is_set(&bus, screen_base, row_bytes, 1, 0),
31224            "TEIdle at the 32-tick boundary should hide the caret"
31225        );
31226        assert_eq!(
31227            bus.read_long(te_ptr + TrapDispatcher::TE_CARET_TIME_OFFSET),
31228            132
31229        );
31230        assert_eq!(
31231            bus.read_word(te_ptr + TrapDispatcher::TE_CARET_STATE_OFFSET),
31232            1
31233        );
31234
31235        disp.tick_count = 164;
31236        bus.write_long(crate::memory::globals::addr::TICKS, 164);
31237        cpu.write_reg(Register::A7, TEST_SP);
31238        bus.write_long(TEST_SP, te_handle);
31239        let result = disp.dispatch_dialog(true, 0x1DA, &mut cpu, &mut bus);
31240        assert!(result.unwrap().is_ok());
31241        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
31242        assert!(
31243            screen_pixel_is_set(&bus, screen_base, row_bytes, 1, 0),
31244            "the next elapsed interval should show the caret again"
31245        );
31246        assert_eq!(
31247            bus.read_long(te_ptr + TrapDispatcher::TE_CARET_TIME_OFFSET),
31248            164
31249        );
31250        assert_eq!(
31251            bus.read_word(te_ptr + TrapDispatcher::TE_CARET_STATE_OFFSET),
31252            0
31253        );
31254    }
31255
31256    #[test]
31257    fn teidle_preserves_active_flag_and_selection_offsets() {
31258        // Inside Macintosh Volume I (1985), pp. I-374 and I-385: TEIdle
31259        // blinks caret for an active record, while TEActivate/TEDeactivate
31260        // control active state. The text length must also remain
31261        // unchanged across an idle call.
31262        let (mut disp, mut cpu, mut bus) = setup();
31263        let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
31264        let te_ptr = bus.read_long(te_handle);
31265        bus.write_word(te_ptr + TrapDispatcher::TE_ACTIVE_OFFSET, 0xFF);
31266        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 2);
31267        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 4);
31268        let pre_telength = bus.read_word(te_ptr + TrapDispatcher::TE_LENGTH_OFFSET);
31269
31270        bus.write_long(TEST_SP, te_handle);
31271        let result = disp.dispatch_dialog(true, 0x1DA, &mut cpu, &mut bus);
31272        assert!(result.unwrap().is_ok());
31273        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
31274        assert_eq!(
31275            bus.read_word(te_ptr + TrapDispatcher::TE_ACTIVE_OFFSET),
31276            0xFF
31277        );
31278        assert_eq!(
31279            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
31280            2
31281        );
31282        assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET), 4);
31283        assert_eq!(
31284            bus.read_word(te_ptr + TrapDispatcher::TE_LENGTH_OFFSET),
31285            pre_telength
31286        );
31287    }
31288
31289    #[test]
31290    fn teidle_repeated_calls_balance_stack_and_preserve_terec_state() {
31291        // Inside Macintosh Volume I (1985), p. I-374: TEIdle is intended
31292        // to be called on every null event from the application's event
31293        // loop, so per-call pop discipline must compose cleanly across
31294        // many invocations and the TERec must remain bit-for-bit
31295        // identical across the sequence: 8 successive TEIdle calls,
31296        // assert A7 advanced exactly 8 * 4 = 32 bytes AND active /
31297        // selStart / selEnd / teLength are all preserved.
31298        let (mut disp, mut cpu, mut bus) = setup();
31299        let te_handle = make_te_with_text(&mut disp, &mut bus, b"WORLD");
31300        let te_ptr = bus.read_long(te_handle);
31301        bus.write_word(te_ptr + TrapDispatcher::TE_ACTIVE_OFFSET, 0xFF);
31302        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 1);
31303        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 3);
31304        let pre_active = bus.read_word(te_ptr + TrapDispatcher::TE_ACTIVE_OFFSET);
31305        let pre_sel_start = bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET);
31306        let pre_sel_end = bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET);
31307        let pre_telength = bus.read_word(te_ptr + TrapDispatcher::TE_LENGTH_OFFSET);
31308
31309        for _ in 0..8 {
31310            cpu.write_reg(Register::A7, TEST_SP);
31311            bus.write_long(TEST_SP, te_handle);
31312            let result = disp.dispatch_dialog(true, 0x1DA, &mut cpu, &mut bus);
31313            assert!(result.unwrap().is_ok());
31314            assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
31315        }
31316
31317        assert_eq!(
31318            bus.read_word(te_ptr + TrapDispatcher::TE_ACTIVE_OFFSET),
31319            pre_active
31320        );
31321        assert_eq!(
31322            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
31323            pre_sel_start
31324        );
31325        assert_eq!(
31326            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET),
31327            pre_sel_end
31328        );
31329        assert_eq!(
31330            bus.read_word(te_ptr + TrapDispatcher::TE_LENGTH_OFFSET),
31331            pre_telength
31332        );
31333    }
31334
31335    #[test]
31336    fn tescroll_offsets_destrect_by_requested_delta_and_pops_args() {
31337        // IM:I I-388; Inside Macintosh: Text 1993, 2-91.
31338        let (mut disp, mut cpu, mut bus) = setup_with_port();
31339        let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
31340        let te_ptr = bus.read_long(te_handle);
31341        bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET, 10);
31342        bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 2, 20);
31343        bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 4, 70);
31344        bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 6, 180);
31345
31346        bus.write_long(TEST_SP, te_handle);
31347        bus.write_word(TEST_SP + 4, (-3i16) as u16); // dv
31348        bus.write_word(TEST_SP + 6, 5); // dh
31349        let result = disp.dispatch_dialog(true, 0x1DD, &mut cpu, &mut bus);
31350        assert!(result.unwrap().is_ok());
31351        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
31352        assert_eq!(
31353            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET),
31354            7
31355        );
31356        assert_eq!(
31357            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 2),
31358            25
31359        );
31360        assert_eq!(
31361            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 4),
31362            67
31363        );
31364        assert_eq!(
31365            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 6),
31366            185
31367        );
31368    }
31369
31370    #[test]
31371    fn teinsert_inserts_before_selection_shifts_range_and_preserves_scrap() {
31372        // IM:I I-387; Inside Macintosh: Text 1993, 2-94. TEInsert splices
31373        // the supplied bytes at the selStart offset (BEFORE the selection)
31374        // without replacing the selected range. The selection range is
31375        // preserved logically — selStart and selEnd shift forward by the
31376        // inserted length so the original selected characters still fall
31377        // inside the range.
31378        let (mut disp, mut cpu, mut bus) = setup();
31379        let existing_scrap = TrapDispatcher::allocate_handle_with_data(&mut bus, 2);
31380        let existing_scrap_ptr = bus.read_long(existing_scrap);
31381        bus.write_bytes(existing_scrap_ptr, b"QQ");
31382        bus.write_long(crate::memory::globals::addr::TE_SCRP_HANDLE, existing_scrap);
31383        bus.write_word(crate::memory::globals::addr::TE_SCRP_LENGTH, 2);
31384
31385        let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
31386        let te_ptr = bus.read_long(te_handle);
31387        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 1);
31388        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 4);
31389
31390        let insert_ptr = bus.alloc(3);
31391        bus.write_bytes(insert_ptr, b"XYZ");
31392        bus.write_long(TEST_SP, te_handle);
31393        bus.write_long(TEST_SP + 4, 3);
31394        bus.write_long(TEST_SP + 8, insert_ptr);
31395        let result = disp.dispatch_dialog(true, 0x1DE, &mut cpu, &mut bus);
31396        assert!(result.unwrap().is_ok());
31397        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 12);
31398
31399        assert_eq!(
31400            TrapDispatcher::te_text_bytes(&bus, te_handle),
31401            b"HXYZELLO".to_vec()
31402        );
31403        assert_eq!(
31404            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
31405            4
31406        );
31407        assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET), 7);
31408        assert_eq!(
31409            bus.read_long(crate::memory::globals::addr::TE_SCRP_HANDLE),
31410            existing_scrap
31411        );
31412        assert_eq!(
31413            bus.read_word(crate::memory::globals::addr::TE_SCRP_LENGTH),
31414            2
31415        );
31416        assert_eq!(bus.read_bytes(existing_scrap_ptr, 2), b"QQ".to_vec());
31417    }
31418
31419    #[test]
31420    fn teinsert_insertion_point_inserts_before_caret_and_shifts_caret() {
31421        // TEInsert inserts immediately before the current selection/insertion
31422        // point per IM:I I-387; Inside Macintosh: Text 1993, 2-94. The
31423        // selection range is preserved logically — the insertion point shifts
31424        // forward by the inserted length so it continues to mark the same
31425        // place relative to the surrounding original characters.
31426        let (mut disp, mut cpu, mut bus) = setup();
31427        let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
31428        let te_ptr = bus.read_long(te_handle);
31429        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 2);
31430        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 2);
31431
31432        let insert_ptr = bus.alloc(1);
31433        bus.write_byte(insert_ptr, b'X');
31434        bus.write_long(TEST_SP, te_handle);
31435        bus.write_long(TEST_SP + 4, 1);
31436        bus.write_long(TEST_SP + 8, insert_ptr);
31437        let result = disp.dispatch_dialog(true, 0x1DE, &mut cpu, &mut bus);
31438        assert!(result.unwrap().is_ok());
31439        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 12);
31440
31441        assert_eq!(
31442            TrapDispatcher::te_text_bytes(&bus, te_handle),
31443            b"HEXLLO".to_vec()
31444        );
31445        assert_eq!(
31446            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
31447            3
31448        );
31449        assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET), 3);
31450    }
31451
31452    #[test]
31453    fn tesetalignment_writes_just_field_and_pops_args() {
31454        // IM:I I-388 (TESetJust) and Inside Macintosh: Text 1993, 2-87.
31455        let (mut disp, mut cpu, mut bus) = setup();
31456        let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
31457        let te_ptr = bus.read_long(te_handle);
31458        bus.write_word(te_ptr + TrapDispatcher::TE_JUST_OFFSET, 0);
31459
31460        bus.write_long(TEST_SP, te_handle);
31461        bus.write_word(TEST_SP + 4, (-1i16) as u16);
31462        let result = disp.dispatch_dialog(true, 0x1DF, &mut cpu, &mut bus);
31463        assert!(result.unwrap().is_ok());
31464        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
31465        assert_eq!(
31466            bus.read_word(te_ptr + TrapDispatcher::TE_JUST_OFFSET) as i16,
31467            -1
31468        );
31469    }
31470
31471    // ---- TECopy / TECut / TEPaste ($A9D5 / $A9D6 / $A9DB) ----
31472
31473    #[test]
31474    fn tecopy_nonempty_selection_updates_textedit_scrap_globals() {
31475        // IM:I I-386 + I-389.
31476        let (mut disp, mut cpu, mut bus) = setup();
31477        let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
31478        let te_ptr = bus.read_long(te_handle);
31479        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 1);
31480        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 4);
31481        bus.write_long(TEST_SP, te_handle);
31482
31483        let result = disp.dispatch_dialog(true, 0x1D5, &mut cpu, &mut bus);
31484        assert!(result.unwrap().is_ok());
31485        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
31486
31487        let scrap_handle = bus.read_long(crate::memory::globals::addr::TE_SCRP_HANDLE);
31488        let scrap_ptr = bus.read_long(scrap_handle);
31489        assert_ne!(scrap_handle, 0);
31490        assert_ne!(scrap_ptr, 0);
31491        assert_eq!(
31492            bus.read_word(crate::memory::globals::addr::TE_SCRP_LENGTH),
31493            3
31494        );
31495        assert_eq!(bus.read_bytes(scrap_ptr, 3), b"ELL".to_vec());
31496    }
31497
31498    #[test]
31499    fn tecopy_empty_selection_preserves_existing_scrap_contents() {
31500        // IM:I I-386: no selected range => no copy.
31501        let (mut disp, mut cpu, mut bus) = setup();
31502        let existing_scrap = TrapDispatcher::allocate_handle_with_data(&mut bus, 2);
31503        let existing_ptr = bus.read_long(existing_scrap);
31504        bus.write_bytes(existing_ptr, b"ZZ");
31505        bus.write_long(crate::memory::globals::addr::TE_SCRP_HANDLE, existing_scrap);
31506        bus.write_word(crate::memory::globals::addr::TE_SCRP_LENGTH, 2);
31507
31508        let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
31509        let te_ptr = bus.read_long(te_handle);
31510        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 2);
31511        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 2);
31512        bus.write_long(TEST_SP, te_handle);
31513
31514        let result = disp.dispatch_dialog(true, 0x1D5, &mut cpu, &mut bus);
31515        assert!(result.unwrap().is_ok());
31516        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
31517        assert_eq!(
31518            bus.read_long(crate::memory::globals::addr::TE_SCRP_HANDLE),
31519            existing_scrap
31520        );
31521        assert_eq!(
31522            bus.read_word(crate::memory::globals::addr::TE_SCRP_LENGTH),
31523            2
31524        );
31525        assert_eq!(bus.read_bytes(existing_ptr, 2), b"ZZ".to_vec());
31526    }
31527
31528    #[test]
31529    fn tecut_nonempty_selection_copies_to_scrap_and_deletes_selected_text() {
31530        // IM:I I-391 + I-389.
31531        let (mut disp, mut cpu, mut bus) = setup();
31532        let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
31533        let te_ptr = bus.read_long(te_handle);
31534        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 1);
31535        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 4);
31536        bus.write_long(TEST_SP, te_handle);
31537
31538        let result = disp.dispatch_dialog(true, 0x1D6, &mut cpu, &mut bus);
31539        assert!(result.unwrap().is_ok());
31540        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
31541        assert_eq!(
31542            TrapDispatcher::te_text_bytes(&bus, te_handle),
31543            b"HO".to_vec()
31544        );
31545        assert_eq!(
31546            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
31547            1
31548        );
31549        assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET), 1);
31550
31551        let scrap_handle = bus.read_long(crate::memory::globals::addr::TE_SCRP_HANDLE);
31552        let scrap_ptr = bus.read_long(scrap_handle);
31553        assert_eq!(
31554            bus.read_word(crate::memory::globals::addr::TE_SCRP_LENGTH),
31555            3
31556        );
31557        assert_eq!(bus.read_bytes(scrap_ptr, 3), b"ELL".to_vec());
31558    }
31559
31560    #[test]
31561    fn tecut_empty_selection_preserves_text_and_scrap_contents() {
31562        // IM:I I-391: insertion-point selection performs no cut.
31563        let (mut disp, mut cpu, mut bus) = setup();
31564        let existing_scrap = TrapDispatcher::allocate_handle_with_data(&mut bus, 2);
31565        let existing_ptr = bus.read_long(existing_scrap);
31566        bus.write_bytes(existing_ptr, b"QQ");
31567        bus.write_long(crate::memory::globals::addr::TE_SCRP_HANDLE, existing_scrap);
31568        bus.write_word(crate::memory::globals::addr::TE_SCRP_LENGTH, 2);
31569
31570        let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
31571        let te_ptr = bus.read_long(te_handle);
31572        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 3);
31573        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 3);
31574        bus.write_long(TEST_SP, te_handle);
31575
31576        let result = disp.dispatch_dialog(true, 0x1D6, &mut cpu, &mut bus);
31577        assert!(result.unwrap().is_ok());
31578        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
31579        assert_eq!(
31580            TrapDispatcher::te_text_bytes(&bus, te_handle),
31581            b"HELLO".to_vec()
31582        );
31583        assert_eq!(
31584            bus.read_long(crate::memory::globals::addr::TE_SCRP_HANDLE),
31585            existing_scrap
31586        );
31587        assert_eq!(
31588            bus.read_word(crate::memory::globals::addr::TE_SCRP_LENGTH),
31589            2
31590        );
31591        assert_eq!(bus.read_bytes(existing_ptr, 2), b"QQ".to_vec());
31592    }
31593
31594    #[test]
31595    fn tepaste_nonempty_scrap_replaces_selection_and_advances_insertion_point() {
31596        // IM:I I-387 + I-389.
31597        let (mut disp, mut cpu, mut bus) = setup();
31598        let te_handle = make_te_with_text(&mut disp, &mut bus, b"HLO");
31599        let te_ptr = bus.read_long(te_handle);
31600        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 1);
31601        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 1);
31602
31603        let scrap_handle = TrapDispatcher::allocate_handle_with_data(&mut bus, 2);
31604        let scrap_ptr = bus.read_long(scrap_handle);
31605        bus.write_bytes(scrap_ptr, b"EL");
31606        bus.write_long(crate::memory::globals::addr::TE_SCRP_HANDLE, scrap_handle);
31607        bus.write_word(crate::memory::globals::addr::TE_SCRP_LENGTH, 2);
31608
31609        bus.write_long(TEST_SP, te_handle);
31610        let result = disp.dispatch_dialog(true, 0x1DB, &mut cpu, &mut bus);
31611        assert!(result.unwrap().is_ok());
31612        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
31613        assert_eq!(
31614            TrapDispatcher::te_text_bytes(&bus, te_handle),
31615            b"HELLO".to_vec()
31616        );
31617        assert_eq!(
31618            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
31619            3
31620        );
31621        assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET), 3);
31622    }
31623
31624    #[test]
31625    fn tepaste_empty_scrap_is_noop() {
31626        // IM:I I-387: empty scrap inserts nothing.
31627        let (mut disp, mut cpu, mut bus) = setup();
31628        let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
31629        let te_ptr = bus.read_long(te_handle);
31630        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 1);
31631        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 3);
31632
31633        let scrap_handle = TrapDispatcher::allocate_handle_with_data(&mut bus, 2);
31634        let scrap_ptr = bus.read_long(scrap_handle);
31635        bus.write_bytes(scrap_ptr, b"ZZ");
31636        bus.write_long(crate::memory::globals::addr::TE_SCRP_HANDLE, scrap_handle);
31637        bus.write_word(crate::memory::globals::addr::TE_SCRP_LENGTH, 0);
31638
31639        bus.write_long(TEST_SP, te_handle);
31640        let result = disp.dispatch_dialog(true, 0x1DB, &mut cpu, &mut bus);
31641        assert!(result.unwrap().is_ok());
31642        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
31643        assert_eq!(
31644            TrapDispatcher::te_text_bytes(&bus, te_handle),
31645            b"HELLO".to_vec()
31646        );
31647        assert_eq!(
31648            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
31649            1
31650        );
31651        assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET), 3);
31652    }
31653
31654    #[test]
31655    fn te_new_initializes_basic_record_fields() {
31656        let (mut disp, mut cpu, mut bus) = setup();
31657
31658        bus.write_word(TEST_SP, 0); // viewRect.top
31659        bus.write_word(TEST_SP + 2, 0); // viewRect.left
31660        bus.write_word(TEST_SP + 4, 100); // viewRect.bottom
31661        bus.write_word(TEST_SP + 6, 120); // viewRect.right
31662        bus.write_word(TEST_SP + 8, 10); // destRect.top
31663        bus.write_word(TEST_SP + 10, 20); // destRect.left
31664        bus.write_word(TEST_SP + 12, 90); // destRect.bottom
31665        bus.write_word(TEST_SP + 14, 80); // destRect.right
31666
31667        let result = disp.dispatch_dialog(true, 0x1D2, &mut cpu, &mut bus);
31668        assert!(result.unwrap().is_ok());
31669        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 16);
31670
31671        let te_handle = bus.read_long(TEST_SP + 16);
31672        let te_ptr = bus.read_long(te_handle);
31673        assert_eq!(
31674            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET),
31675            10
31676        );
31677        assert_eq!(
31678            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 2),
31679            20
31680        );
31681        assert_eq!(
31682            bus.read_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 4),
31683            100
31684        );
31685        assert_eq!(
31686            bus.read_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 6),
31687            120
31688        );
31689        assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_LENGTH_OFFSET), 0);
31690        let h_text = bus.read_long(te_ptr + TrapDispatcher::TE_HTEXT_OFFSET);
31691        assert_ne!(h_text, 0);
31692        assert_eq!(bus.read_long(h_text), 0);
31693    }
31694
31695    // Inside Macintosh: Text (1993), p. 2-78: TEStyleNew returns a
31696    // multistyled TEHandle, uses -1 sentinels in txSize/lineHeight/
31697    // fontAscent, and installs style metadata.
31698    #[test]
31699    fn testylenew_returns_styled_handle_and_initializes_sentinel_fields() {
31700        let (mut disp, mut cpu, mut bus) = setup();
31701
31702        bus.write_word(TEST_SP, 0); // viewRect.top
31703        bus.write_word(TEST_SP + 2, 0); // viewRect.left
31704        bus.write_word(TEST_SP + 4, 100); // viewRect.bottom
31705        bus.write_word(TEST_SP + 6, 120); // viewRect.right
31706        bus.write_word(TEST_SP + 8, 10); // destRect.top
31707        bus.write_word(TEST_SP + 10, 20); // destRect.left
31708        bus.write_word(TEST_SP + 12, 90); // destRect.bottom
31709        bus.write_word(TEST_SP + 14, 80); // destRect.right
31710
31711        let result = disp.dispatch_dialog(true, 0x03E, &mut cpu, &mut bus);
31712        assert!(result.unwrap().is_ok());
31713        assert_eq!(
31714            cpu.read_reg(Register::A7),
31715            TEST_SP + 16,
31716            "TEStyleNew should pop two Rect arguments (16 bytes)"
31717        );
31718
31719        let te_handle = bus.read_long(TEST_SP + 16);
31720        assert_ne!(te_handle, 0, "TEStyleNew should return a non-NIL TEHandle");
31721
31722        let te_ptr = bus.read_long(te_handle);
31723        assert_ne!(te_ptr, 0, "returned TEHandle should point to a TERec");
31724        assert_eq!(
31725            bus.read_word(te_ptr + TrapDispatcher::TE_TX_SIZE_OFFSET),
31726            0xFFFF
31727        );
31728        assert_eq!(
31729            bus.read_word(te_ptr + TrapDispatcher::TE_LINE_HEIGHT_OFFSET),
31730            0xFFFF
31731        );
31732        assert_eq!(
31733            bus.read_word(te_ptr + TrapDispatcher::TE_FONT_ASCENT_OFFSET),
31734            0xFFFF
31735        );
31736
31737        let style_handle = bus.read_long(te_ptr + TrapDispatcher::TE_TX_FONT_OFFSET);
31738        assert_ne!(style_handle, 0, "styled record should carry a style handle");
31739        let style_ptr = bus.read_long(style_handle);
31740        assert_ne!(
31741            style_ptr, 0,
31742            "style handle should dereference to a style record"
31743        );
31744        assert_ne!(
31745            bus.read_long(style_ptr + TrapDispatcher::TE_STYLE_NULL_STYLE_OFFSET),
31746            0,
31747            "style record should include a non-NIL null-style handle"
31748        );
31749    }
31750
31751    // ---- TEStyleNew (pointer-arg convention) ----
31752
31753    // Inside Macintosh: Text (1993), p. 2-78: TEStyleNew accepts a
31754    // pointer-arg convention `const Rect *destRect, const Rect *viewRect`
31755    // per MPW Universal Headers. Pascal LR push: destRect_ptr at SP+4
31756    // (deepest, pushed first), viewRect_ptr at SP+0 (shallowest, pushed
31757    // last). Net pop is 8 bytes.
31758    #[test]
31759    fn testylenew_pointer_arg_convention_initializes_destrect_viewrect_and_styled_sentinels() {
31760        let (mut disp, mut cpu, mut bus) = setup();
31761
31762        // Build distinct destRect and viewRect in guest memory.
31763        let dest_rect_ptr: u32 = 0x1A0000;
31764        let view_rect_ptr: u32 = 0x1A0010;
31765        bus.write_word(dest_rect_ptr, (-1200_i16) as u16);
31766        bus.write_word(dest_rect_ptr + 2, (-1200_i16) as u16);
31767        bus.write_word(dest_rect_ptr + 4, (-900_i16) as u16);
31768        bus.write_word(dest_rect_ptr + 6, (-1000_i16) as u16);
31769        bus.write_word(view_rect_ptr, (-1100_i16) as u16);
31770        bus.write_word(view_rect_ptr + 2, (-1300_i16) as u16);
31771        bus.write_word(view_rect_ptr + 4, (-800_i16) as u16);
31772        bus.write_word(view_rect_ptr + 6, (-900_i16) as u16);
31773
31774        // Pascal LR push: viewRect_ptr at SP+0 (last pushed),
31775        // destRect_ptr at SP+4 (first pushed).
31776        bus.write_long(TEST_SP, view_rect_ptr);
31777        bus.write_long(TEST_SP + 4, dest_rect_ptr);
31778
31779        let result = disp.dispatch_dialog(true, 0x03E, &mut cpu, &mut bus);
31780        assert!(result.unwrap().is_ok());
31781        assert_eq!(
31782            cpu.read_reg(Register::A7),
31783            TEST_SP + 8,
31784            "TEStyleNew pointer convention should pop 8 bytes (two pointer args)"
31785        );
31786
31787        let te_handle = bus.read_long(TEST_SP + 8);
31788        assert_ne!(te_handle, 0, "TEStyleNew should return a non-NIL TEHandle");
31789        let te_ptr = bus.read_long(te_handle);
31790        assert_ne!(te_ptr, 0, "TEHandle should dereference to a TERec");
31791
31792        // destRect at offset 0x00 round-trips from the destRect_ptr.
31793        assert_eq!(
31794            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET) as i16,
31795            -1200,
31796            "destRect.top must round-trip from caller-supplied destRect_ptr"
31797        );
31798        assert_eq!(
31799            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 2) as i16,
31800            -1200,
31801            "destRect.left must round-trip from caller-supplied destRect_ptr"
31802        );
31803        assert_eq!(
31804            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 4) as i16,
31805            -900,
31806            "destRect.bottom must round-trip from caller-supplied destRect_ptr"
31807        );
31808        assert_eq!(
31809            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 6) as i16,
31810            -1000,
31811            "destRect.right must round-trip from caller-supplied destRect_ptr"
31812        );
31813
31814        // viewRect at offset 0x08 round-trips from the viewRect_ptr.
31815        assert_eq!(
31816            bus.read_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET) as i16,
31817            -1100,
31818            "viewRect.top must round-trip from caller-supplied viewRect_ptr"
31819        );
31820        assert_eq!(
31821            bus.read_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 2) as i16,
31822            -1300,
31823            "viewRect.left must round-trip from caller-supplied viewRect_ptr"
31824        );
31825        assert_eq!(
31826            bus.read_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 4) as i16,
31827            -800,
31828            "viewRect.bottom must round-trip from caller-supplied viewRect_ptr"
31829        );
31830        assert_eq!(
31831            bus.read_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 6) as i16,
31832            -900,
31833            "viewRect.right must round-trip from caller-supplied viewRect_ptr"
31834        );
31835
31836        // Styled sentinels per IM:Text 1993 p. 2-78.
31837        assert_eq!(
31838            bus.read_word(te_ptr + TrapDispatcher::TE_TX_SIZE_OFFSET),
31839            0xFFFF,
31840            "TEStyleNew must set txSize to -1 (styled-record sentinel)"
31841        );
31842        assert_eq!(
31843            bus.read_word(te_ptr + TrapDispatcher::TE_LINE_HEIGHT_OFFSET),
31844            0xFFFF,
31845            "TEStyleNew must set lineHeight to -1 (styled-record sentinel)"
31846        );
31847        assert_eq!(
31848            bus.read_word(te_ptr + TrapDispatcher::TE_FONT_ASCENT_OFFSET),
31849            0xFFFF,
31850            "TEStyleNew must set fontAscent to -1 (styled-record sentinel)"
31851        );
31852
31853        // Style handle at txFont/txFace overlay (4 bytes at offset 0x4A).
31854        let style_handle = bus.read_long(te_ptr + TrapDispatcher::TE_TX_FONT_OFFSET);
31855        assert_ne!(
31856            style_handle, 0,
31857            "TEStyleNew must install a non-NIL TEStyleHandle in the txFont/txFace overlay"
31858        );
31859        let style_ptr = bus.read_long(style_handle);
31860        assert_ne!(
31861            style_ptr, 0,
31862            "TEStyleHandle should dereference to an allocated TEStyleRec"
31863        );
31864    }
31865
31866    // Inside Macintosh: Text (1993), p. 2-78 + MPW TextEdit.h
31867    // ONEWORDINLINE(0xA83E): TEStyleNew is `EXTERN_API(TEHandle)`.
31868    // Pascal FUNCTION protocol: caller pre-allocates 4-byte TEHandle
31869    // result slot, pushes 2 pointer args (8 bytes); trap pops 8 +
31870    // writes 4-byte result.
31871    #[test]
31872    fn testylenew_function_protocol_consumes_two_pointer_args_and_writes_4_byte_result() {
31873        let (mut disp, mut cpu, mut bus) = setup();
31874
31875        let dest_rect_ptr: u32 = 0x1A0020;
31876        let view_rect_ptr: u32 = 0x1A0030;
31877        // Both rects: non-empty distinct values so te_new_rect_args
31878        // selects the pointer convention (8 bytes).
31879        bus.write_word(dest_rect_ptr, (-500_i16) as u16);
31880        bus.write_word(dest_rect_ptr + 2, (-500_i16) as u16);
31881        bus.write_word(dest_rect_ptr + 4, (-300_i16) as u16);
31882        bus.write_word(dest_rect_ptr + 6, (-400_i16) as u16);
31883        bus.write_word(view_rect_ptr, (-450_i16) as u16);
31884        bus.write_word(view_rect_ptr + 2, (-550_i16) as u16);
31885        bus.write_word(view_rect_ptr + 4, (-250_i16) as u16);
31886        bus.write_word(view_rect_ptr + 6, (-350_i16) as u16);
31887
31888        bus.write_long(TEST_SP, view_rect_ptr);
31889        bus.write_long(TEST_SP + 4, dest_rect_ptr);
31890        // Poison the result slot at sp+8 and a sentinel past it.
31891        bus.write_long(TEST_SP + 8, 0xDEADBEEF);
31892        bus.write_long(TEST_SP + 12, 0xCAFEBABE);
31893
31894        let result = disp.dispatch_dialog(true, 0x03E, &mut cpu, &mut bus);
31895        assert!(result.unwrap().is_ok());
31896
31897        // A7 advanced exactly 8 bytes.
31898        assert_eq!(
31899            cpu.read_reg(Register::A7),
31900            TEST_SP + 8,
31901            "TEStyleNew pointer-arg convention must pop exactly 8 bytes"
31902        );
31903
31904        // The function result slot at the former sp+8 was overwritten
31905        // by a non-poison TEHandle.
31906        let te_handle = bus.read_long(TEST_SP + 8);
31907        assert_ne!(
31908            te_handle, 0xDEADBEEF,
31909            "TEStyleNew result slot must be overwritten by a real TEHandle, not the poison sentinel"
31910        );
31911        assert_ne!(te_handle, 0, "TEStyleNew result must be a non-NIL TEHandle");
31912
31913        // The sentinel at sp+12 (4 bytes past the 4-byte result slot)
31914        // must survive — TEStyleNew must not write past its result.
31915        assert_eq!(
31916            bus.read_long(TEST_SP + 12),
31917            0xCAFEBABE,
31918            "TEStyleNew must not write past the 4-byte function-result slot"
31919        );
31920    }
31921
31922    #[test]
31923    fn testyleinsert_clears_exhausted_nul_continuation() {
31924        let (mut disp, mut cpu, mut bus) = setup();
31925        let text_ptr = bus.alloc(1);
31926        bus.write_byte(text_ptr, 0);
31927        cpu.write_reg(Register::A3, text_ptr);
31928
31929        bus.write_word(TEST_SP, 0x0007);
31930        bus.write_long(TEST_SP + 2, 0);
31931        bus.write_long(TEST_SP + 6, 0);
31932        bus.write_long(TEST_SP + 10, 0);
31933        bus.write_long(TEST_SP + 14, text_ptr);
31934
31935        let result = disp.dispatch_dialog(true, 0x03D, &mut cpu, &mut bus);
31936        assert!(result.unwrap().is_ok());
31937        assert_eq!(cpu.read_reg(Register::A3), 0);
31938        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 18);
31939    }
31940
31941    #[test]
31942    fn testyleinsert_applies_style_scrap_runs_and_line_metrics() {
31943        let (mut disp, mut cpu, mut bus) = setup();
31944
31945        disp.tx_font = 0;
31946        disp.tx_face = 0;
31947        disp.tx_size = 12;
31948        let te_handle = TrapDispatcher::allocate_te_handle(&mut bus);
31949        disp.initialize_styled_te_record(&mut bus, te_handle, (0, 0, 80, 200), (0, 0, 80, 200));
31950
31951        let text = b"small\rLARGE";
31952        let text_ptr = bus.alloc(text.len() as u32);
31953        bus.write_bytes(text_ptr, text);
31954        let style_scrap = make_style_scrap(
31955            &mut bus,
31956            &[
31957                (0, 10, 8, 0, 1, 9, (0, 0, 0)),
31958                (6, 14, 11, 0, 0, 12, (0x1111, 0x2222, 0x3333)),
31959            ],
31960        );
31961
31962        bus.write_word(TEST_SP, 0x0007);
31963        bus.write_long(TEST_SP + 2, te_handle);
31964        bus.write_long(TEST_SP + 6, style_scrap);
31965        bus.write_long(TEST_SP + 10, text.len() as u32);
31966        bus.write_long(TEST_SP + 14, text_ptr);
31967
31968        let result = disp.dispatch_dialog(true, 0x03D, &mut cpu, &mut bus);
31969        assert!(result.unwrap().is_ok());
31970        assert_eq!(
31971            cpu.read_reg(Register::A7),
31972            TEST_SP + 18,
31973            "TEStyleInsert must consume selector + hTE + hST + length + text pointer"
31974        );
31975
31976        let te_ptr = bus.read_long(te_handle);
31977        assert_eq!(
31978            bus.read_word(te_ptr + TrapDispatcher::TE_LENGTH_OFFSET) as usize,
31979            text.len()
31980        );
31981        assert_eq!(
31982            bus.read_word(te_ptr + TrapDispatcher::TE_N_LINES_OFFSET),
31983            2,
31984            "explicit CR should produce two TextEdit lines"
31985        );
31986
31987        let style_handle = bus.read_long(te_ptr + TrapDispatcher::TE_TX_FONT_OFFSET);
31988        let style_ptr = bus.read_long(style_handle);
31989        assert_eq!(
31990            bus.read_word(style_ptr + TrapDispatcher::TE_STYLE_N_RUNS_OFFSET),
31991            2
31992        );
31993        assert_eq!(
31994            bus.read_word(style_ptr + TrapDispatcher::TE_STYLE_N_STYLES_OFFSET),
31995            2
31996        );
31997        assert_eq!(
31998            bus.read_word(style_ptr + TrapDispatcher::TE_STYLE_RUNS_OFFSET),
31999            0
32000        );
32001        assert_eq!(
32002            bus.read_word(style_ptr + TrapDispatcher::TE_STYLE_RUNS_OFFSET + 4),
32003            6
32004        );
32005
32006        let style_table = bus.read_long(style_ptr + TrapDispatcher::TE_STYLE_STYLE_TABLE_OFFSET);
32007        let style_table_ptr = bus.read_long(style_table);
32008        assert_eq!(
32009            bus.read_byte(style_table_ptr + TrapDispatcher::ST_ELEMENT_FACE_OFFSET),
32010            1,
32011            "first style run should preserve the bold face from the style scrap"
32012        );
32013        assert_eq!(
32014            bus.read_word(style_table_ptr + TrapDispatcher::ST_ELEMENT_SIZE_OFFSET),
32015            9,
32016            "first style run should preserve the smaller body size"
32017        );
32018        let second_style = style_table_ptr + TrapDispatcher::ST_ELEMENT_SIZE;
32019        assert_eq!(
32020            bus.read_word(second_style + TrapDispatcher::ST_ELEMENT_SIZE_OFFSET),
32021            12
32022        );
32023        assert_eq!(
32024            (
32025                bus.read_word(second_style + TrapDispatcher::ST_ELEMENT_COLOR_OFFSET),
32026                bus.read_word(second_style + TrapDispatcher::ST_ELEMENT_COLOR_OFFSET + 2),
32027                bus.read_word(second_style + TrapDispatcher::ST_ELEMENT_COLOR_OFFSET + 4),
32028            ),
32029            (0x1111, 0x2222, 0x3333)
32030        );
32031
32032        let lh_table = bus.read_long(style_ptr + TrapDispatcher::TE_STYLE_LH_TABLE_OFFSET);
32033        let lh_ptr = bus.read_long(lh_table);
32034        assert_eq!(
32035            bus.read_word(lh_ptr + TrapDispatcher::LH_ELEMENT_HEIGHT_OFFSET),
32036            10,
32037            "first TextEdit line should use first style's line height"
32038        );
32039        assert_eq!(
32040            bus.read_word(
32041                lh_ptr + TrapDispatcher::LH_ELEMENT_SIZE + TrapDispatcher::LH_ELEMENT_HEIGHT_OFFSET
32042            ),
32043            14,
32044            "second TextEdit line should use second style's line height"
32045        );
32046
32047        let runs = disp.te_style_runs(&bus, te_handle, text.len());
32048        assert_eq!(runs.len(), 2);
32049        assert_eq!(runs[0].start, 0);
32050        assert_eq!(runs[0].style_index, 0);
32051        assert_eq!(runs[0].style.size, 9);
32052        assert_eq!(runs[1].start, 6);
32053        assert_eq!(runs[1].style_index, 1);
32054        assert_eq!(runs[1].style.size, 12);
32055    }
32056
32057    #[test]
32058    fn testyleinsert_parses_stscrprec_style_table_after_count_word() {
32059        // IM:V V-274 and MPW TextEdit.h: StScrpRec is `short scrpNStyles`
32060        // immediately followed by `ScrpSTElement scrpStyleTab[]`.
32061        let (mut disp, mut cpu, mut bus) = setup();
32062
32063        disp.tx_font = 0;
32064        disp.tx_face = 0;
32065        disp.tx_size = 12;
32066        let te_handle = TrapDispatcher::allocate_te_handle(&mut bus);
32067        disp.initialize_styled_te_record(&mut bus, te_handle, (0, 0, 80, 200), (0, 0, 80, 200));
32068
32069        let text = b"TITLE body";
32070        let text_ptr = bus.alloc(text.len() as u32);
32071        bus.write_bytes(text_ptr, text);
32072
32073        let style_scrap = TrapDispatcher::allocate_handle_with_data(&mut bus, 2 + 2 * 0x14);
32074        let scrap_ptr = bus.read_long(style_scrap);
32075        bus.write_word(scrap_ptr, 2);
32076
32077        let first = scrap_ptr + 2;
32078        bus.write_long(first, 0);
32079        bus.write_word(first + 4, 10);
32080        bus.write_word(first + 6, 8);
32081        bus.write_word(first + 8, 0);
32082        bus.write_byte(first + 10, 1);
32083        bus.write_word(first + 12, 9);
32084        bus.write_word(first + 14, 0);
32085        bus.write_word(first + 16, 0);
32086        bus.write_word(first + 18, 0);
32087
32088        let second = first + 0x14;
32089        bus.write_long(second, 6);
32090        bus.write_word(second + 4, 14);
32091        bus.write_word(second + 6, 11);
32092        bus.write_word(second + 8, 3);
32093        bus.write_byte(second + 10, 0);
32094        bus.write_word(second + 12, 12);
32095        bus.write_word(second + 14, 0x1111);
32096        bus.write_word(second + 16, 0x2222);
32097        bus.write_word(second + 18, 0x3333);
32098
32099        bus.write_word(TEST_SP, 0x0007);
32100        bus.write_long(TEST_SP + 2, te_handle);
32101        bus.write_long(TEST_SP + 6, style_scrap);
32102        bus.write_long(TEST_SP + 10, text.len() as u32);
32103        bus.write_long(TEST_SP + 14, text_ptr);
32104
32105        let result = disp.dispatch_dialog(true, 0x03D, &mut cpu, &mut bus);
32106        assert!(result.unwrap().is_ok());
32107
32108        let runs = disp.te_style_runs(&bus, te_handle, text.len());
32109        assert_eq!(runs.len(), 2);
32110        assert_eq!(runs[0].start, 0);
32111        assert_eq!(runs[0].style.face, 1);
32112        assert_eq!(runs[0].style.size, 9);
32113        assert_eq!(runs[1].start, 6);
32114        assert_eq!(runs[1].style.font, 3);
32115        assert_eq!(runs[1].style.size, 12);
32116        assert_eq!(runs[1].style.color, (0x1111, 0x2222, 0x3333));
32117    }
32118
32119    // Inside Macintosh: Text (1993), p. 2-92: TEAutoView takes
32120    // `fAuto: Boolean; hTE: TEHandle`. With Pascal calling convention, hTE
32121    // (last parameter) is at SP+0 and fAuto at SP+4.
32122    #[test]
32123    fn teautoview_reads_hte_from_sp_plus_0_and_fauto_from_sp_plus_4() {
32124        let (mut disp, mut cpu, mut bus) = setup();
32125        let te_handle = TrapDispatcher::allocate_te_handle(&mut bus);
32126
32127        bus.write_long(TEST_SP, te_handle); // SP+0: hTE (last-pushed)
32128                                            // Pascal BOOLEAN in high byte (MPW C convention).
32129        bus.write_byte(TEST_SP + 4, 1); // SP+4: fAuto = TRUE
32130
32131        let result = disp.dispatch_dialog(true, 0x013, &mut cpu, &mut bus);
32132        assert!(result.unwrap().is_ok());
32133        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
32134        assert!(
32135            disp.te_feature_bit(te_handle, TrapDispatcher::TE_FEATURE_AUTO_SCROLL),
32136            "TEAutoView must enable auto-scroll on the TE handle at SP+0"
32137        );
32138    }
32139
32140    #[test]
32141    fn teautoview_clears_feature_when_fauto_is_false() {
32142        let (mut disp, mut cpu, mut bus) = setup();
32143        let te_handle = TrapDispatcher::allocate_te_handle(&mut bus);
32144
32145        // First turn it on.
32146        disp.set_te_feature_bit(te_handle, TrapDispatcher::TE_FEATURE_AUTO_SCROLL, true);
32147        assert!(disp.te_feature_bit(te_handle, TrapDispatcher::TE_FEATURE_AUTO_SCROLL));
32148
32149        bus.write_long(TEST_SP, te_handle);
32150        bus.write_word(TEST_SP + 4, 0); // fAuto = FALSE
32151
32152        let result = disp.dispatch_dialog(true, 0x013, &mut cpu, &mut bus);
32153        assert!(result.unwrap().is_ok());
32154        assert_eq!(
32155            cpu.read_reg(Register::A7),
32156            TEST_SP + 6,
32157            "TEAutoView should pop one Boolean and one TEHandle"
32158        );
32159        assert!(
32160            !disp.te_feature_bit(te_handle, TrapDispatcher::TE_FEATURE_AUTO_SCROLL),
32161            "TEAutoView(FALSE) must clear the auto-scroll bit on the supplied hTE"
32162        );
32163    }
32164
32165    // IM:Text 2-92 + IM:VI 15-22: TEAutoView and TEFeatureFlag
32166    // teFAutoScroll expose the same automatic-scroll state.
32167    #[test]
32168    fn teautoview_and_tefeatureflag_observe_shared_autoscroll_state() {
32169        let (mut disp, mut cpu, mut bus) = setup();
32170        let te_handle = TrapDispatcher::allocate_te_handle(&mut bus);
32171
32172        bus.write_long(TEST_SP, te_handle);
32173        bus.write_byte(TEST_SP + 4, 1); // fAuto = TRUE
32174        let result = disp.dispatch_dialog(true, 0x013, &mut cpu, &mut bus);
32175        assert!(result.unwrap().is_ok());
32176
32177        cpu.write_reg(Register::A7, TEST_SP);
32178        bus.write_word(TEST_SP, 0x000E); // selector
32179        bus.write_long(TEST_SP + 2, te_handle); // hTE
32180        bus.write_word(TEST_SP + 6, (-1i16) as u16); // teBitTest
32181        bus.write_word(TEST_SP + 8, TrapDispatcher::TE_FEATURE_AUTO_SCROLL);
32182        bus.write_word(TEST_SP + 10, 0xBEEF);
32183        let result = disp.dispatch_dialog(true, 0x03D, &mut cpu, &mut bus);
32184        assert!(result.unwrap().is_ok());
32185        assert_eq!(bus.read_word(TEST_SP + 10), 1);
32186
32187        cpu.write_reg(Register::A7, TEST_SP);
32188        bus.write_long(TEST_SP, te_handle);
32189        bus.write_word(TEST_SP + 4, 0); // fAuto = FALSE
32190        let result = disp.dispatch_dialog(true, 0x013, &mut cpu, &mut bus);
32191        assert!(result.unwrap().is_ok());
32192
32193        cpu.write_reg(Register::A7, TEST_SP);
32194        bus.write_word(TEST_SP, 0x000E); // selector
32195        bus.write_long(TEST_SP + 2, te_handle); // hTE
32196        bus.write_word(TEST_SP + 6, (-1i16) as u16); // teBitTest
32197        bus.write_word(TEST_SP + 8, TrapDispatcher::TE_FEATURE_AUTO_SCROLL);
32198        bus.write_word(TEST_SP + 10, 0xBEEF);
32199        let result = disp.dispatch_dialog(true, 0x03D, &mut cpu, &mut bus);
32200        assert!(result.unwrap().is_ok());
32201        assert_eq!(bus.read_word(TEST_SP + 10), 0);
32202    }
32203
32204    #[test]
32205    fn te_dispatch_feature_flag_tracks_auto_scroll_state() {
32206        let (mut disp, mut cpu, mut bus) = setup();
32207
32208        let te_handle = TrapDispatcher::allocate_te_handle(&mut bus);
32209        bus.write_word(TEST_SP, 0x000E);
32210        bus.write_long(TEST_SP + 2, te_handle);
32211        bus.write_word(TEST_SP + 6, 1); // teBitSet
32212        bus.write_word(TEST_SP + 8, 0); // teFAutoScroll
32213
32214        let result = disp.dispatch_dialog(true, 0x03D, &mut cpu, &mut bus);
32215        assert!(result.unwrap().is_ok());
32216        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 10);
32217        assert_eq!(bus.read_word(TEST_SP + 10), 0);
32218        assert!(disp.te_feature_bit(te_handle, TrapDispatcher::TE_FEATURE_AUTO_SCROLL));
32219    }
32220
32221    // Inside Macintosh Volume VI (1991), pp. 15-22 and 15-43: selector
32222    // $000E dispatches to TEFeatureFlag; action TEBitTest (-1) reports
32223    // current feature state without mutating it.
32224    #[test]
32225    fn te_dispatch_feature_flag_test_action_returns_current_state() {
32226        let (mut disp, mut cpu, mut bus) = setup();
32227
32228        let te_handle = TrapDispatcher::allocate_te_handle(&mut bus);
32229        disp.set_te_feature_bit(te_handle, TrapDispatcher::TE_FEATURE_AUTO_SCROLL, true);
32230
32231        bus.write_word(TEST_SP, 0x000E); // selector
32232        bus.write_long(TEST_SP + 2, te_handle); // hTE
32233        bus.write_word(TEST_SP + 6, (-1i16) as u16); // teBitTest
32234        bus.write_word(TEST_SP + 8, TrapDispatcher::TE_FEATURE_AUTO_SCROLL);
32235        bus.write_word(TEST_SP + 10, 0xBEEF);
32236
32237        let result = disp.dispatch_dialog(true, 0x03D, &mut cpu, &mut bus);
32238        assert!(result.unwrap().is_ok());
32239        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 10);
32240        assert_eq!(bus.read_word(TEST_SP + 10), 1);
32241        assert!(
32242            disp.te_feature_bit(te_handle, TrapDispatcher::TE_FEATURE_AUTO_SCROLL),
32243            "teBitTest should not mutate the feature bit"
32244        );
32245    }
32246
32247    // Inside Macintosh Volume VI (1991), p. 15-22: TEFeatureFlag with
32248    // action teBitClear=0 clears the selector bit and returns the prior
32249    // state. Symmetric counterpart of `te_dispatch_feature_flag_tracks_auto_scroll_state`
32250    // for the clear action.
32251    #[test]
32252    fn tefeatureflag_clear_action_returns_prior_one_state_and_clears_bit() {
32253        let (mut disp, mut cpu, mut bus) = setup();
32254
32255        let te_handle = TrapDispatcher::allocate_te_handle(&mut bus);
32256        // Pre-set autoscroll so teBitClear sees a prior-on state.
32257        disp.set_te_feature_bit(te_handle, TrapDispatcher::TE_FEATURE_AUTO_SCROLL, true);
32258        assert!(disp.te_feature_bit(te_handle, TrapDispatcher::TE_FEATURE_AUTO_SCROLL));
32259
32260        // THREEWORDINLINE(0x3F3C, 0x000E, 0xA83D) stack frame: selector
32261        // pre-pushed at sp+0; Pascal LR push order leaves hTE shallowest
32262        // (sp+2), action middle (sp+6), feature deepest (sp+8); short
32263        // function-result slot at sp+10.
32264        bus.write_word(TEST_SP, 0x000E); // selector $000E = TEFeatureFlag
32265        bus.write_long(TEST_SP + 2, te_handle); // hTE
32266        bus.write_word(TEST_SP + 6, 0); // action = teBitClear (0)
32267        bus.write_word(TEST_SP + 8, TrapDispatcher::TE_FEATURE_AUTO_SCROLL);
32268        bus.write_word(TEST_SP + 10, 0xBEEF); // poison result slot
32269
32270        let result = disp.dispatch_dialog(true, 0x03D, &mut cpu, &mut bus);
32271        assert!(result.unwrap().is_ok());
32272        assert_eq!(
32273            cpu.read_reg(Register::A7),
32274            TEST_SP + 10,
32275            "TEFeatureFlag must pop 10 bytes (selector + hTE + action + feature)"
32276        );
32277        assert_eq!(
32278            bus.read_word(TEST_SP + 10),
32279            1,
32280            "teBitClear must return PRIOR bit state (1 = was set), not the new state"
32281        );
32282        assert!(
32283            !disp.te_feature_bit(te_handle, TrapDispatcher::TE_FEATURE_AUTO_SCROLL),
32284            "teBitClear must clear the feature bit after returning its prior state"
32285        );
32286    }
32287
32288    // Inside Macintosh Volume VI (1991), p. 15-22: TEFeatureFlag is
32289    // FUNCTION TEFeatureFlag(feature, action: INTEGER; hTE: TEHandle): INTEGER.
32290    // Universal Headers TextEdit.h declares THREEWORDINLINE(0x3F3C, 0x000E, 0xA83D).
32291    // The 10-byte stack frame (2 selector + 4 hTE + 2 action + 2 feature)
32292    // is consumed atomically; the post-pop SP points at the 2-byte short
32293    // function-result slot.
32294    #[test]
32295    fn tedispatch_function_protocol_consumes_threewordinline_stack_frame_for_tefeatureflag() {
32296        let (mut disp, mut cpu, mut bus) = setup();
32297
32298        let te_handle = TrapDispatcher::allocate_te_handle(&mut bus);
32299
32300        bus.write_word(TEST_SP, 0x000E); // selector
32301        bus.write_long(TEST_SP + 2, te_handle);
32302        bus.write_word(TEST_SP + 6, (-1i16) as u16); // teBitTest
32303        bus.write_word(TEST_SP + 8, TrapDispatcher::TE_FEATURE_AUTO_SCROLL);
32304        // Poison both the result slot and a sentinel past it.
32305        bus.write_word(TEST_SP + 10, 0xDEAD);
32306        bus.write_word(TEST_SP + 12, 0xCAFE);
32307
32308        let result = disp.dispatch_dialog(true, 0x03D, &mut cpu, &mut bus);
32309        assert!(result.unwrap().is_ok());
32310
32311        // SP advanced exactly 10 bytes: selector(2) + hTE(4) + action(2) + feature(2).
32312        assert_eq!(
32313            cpu.read_reg(Register::A7),
32314            TEST_SP + 10,
32315            "TEFeatureFlag dispatch must advance A7 by exactly 10 bytes"
32316        );
32317
32318        // Result slot was overwritten by the prior bit state (0 here).
32319        assert_ne!(
32320            bus.read_word(TEST_SP + 10),
32321            0xDEAD,
32322            "TEFeatureFlag must overwrite the 2-byte result slot with the prior bit state"
32323        );
32324
32325        // Sentinel 2 bytes past the result slot must survive — TEFeatureFlag
32326        // must not write beyond its function-result word.
32327        assert_eq!(
32328            bus.read_word(TEST_SP + 12),
32329            0xCAFE,
32330            "TEFeatureFlag must not write past the 2-byte short function-result slot"
32331        );
32332    }
32333
32334    #[test]
32335    fn tesetstyle_reads_style_byte_without_treating_record_padding_as_face_bits() {
32336        // TextStyle.tsFace is a one-byte Style followed by an alignment byte
32337        // (Inside Macintosh: Text 1993, pp. 2-78..2-79). Marathon leaves the
32338        // padding byte nonzero while repeatedly applying terminal text styles.
32339        let (mut disp, mut cpu, mut bus) = setup();
32340        disp.tx_face = 1;
32341        disp.tx_size = 12;
32342        let te_handle = TrapDispatcher::allocate_te_handle(&mut bus);
32343        disp.initialize_styled_te_record(&mut bus, te_handle, (0, 0, 80, 200), (0, 0, 80, 200));
32344        disp.te_set_text_contents(&mut bus, te_handle, b"A");
32345        let te_ptr = bus.read_long(te_handle);
32346        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 1);
32347        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 1);
32348        let text_style = bus.alloc(12);
32349        bus.write_word(text_style, 22);
32350        bus.write_byte(text_style + 2, 0); // plain
32351        bus.write_byte(text_style + 3, 0x56); // unrelated alignment byte
32352        bus.write_word(text_style + 4, 12);
32353        bus.write_word(text_style + 6, 0);
32354        bus.write_word(text_style + 8, 0xFFFF);
32355        bus.write_word(text_style + 10, 0);
32356        bus.write_word(TEST_SP, 0x0001);
32357        bus.write_long(TEST_SP + 2, te_handle);
32358        bus.write_word(TEST_SP + 6, 0); // redraw = FALSE
32359        bus.write_long(TEST_SP + 8, text_style);
32360        bus.write_word(TEST_SP + 12, 0x000F); // doAll
32361
32362        let result = disp.dispatch_dialog(true, 0x03D, &mut cpu, &mut bus);
32363
32364        assert!(result.unwrap().is_ok());
32365        let style_handle = bus.read_long(te_ptr + TrapDispatcher::TE_TX_FONT_OFFSET);
32366        let style_record = bus.read_long(style_handle);
32367        let table_handle =
32368            bus.read_long(style_record + TrapDispatcher::TE_STYLE_STYLE_TABLE_OFFSET);
32369        let table = bus.read_long(table_handle);
32370        assert_eq!(
32371            bus.read_byte(table + TrapDispatcher::ST_ELEMENT_FACE_OFFSET),
32372            1,
32373            "an insertion-point style change must not restyle existing text"
32374        );
32375        let null_style_handle =
32376            bus.read_long(style_record + TrapDispatcher::TE_STYLE_NULL_STYLE_OFFSET);
32377        let null_style = bus.read_long(null_style_handle);
32378        let null_scrap_handle = bus.read_long(null_style + TrapDispatcher::NULL_STYLE_SCRAP_OFFSET);
32379        let null_scrap = bus.read_long(null_scrap_handle);
32380        assert_eq!(
32381            bus.read_byte(
32382                null_scrap
32383                    + TrapDispatcher::SCRAP_STYLE_TAB_OFFSET
32384                    + TrapDispatcher::SCRAP_STYLE_FACE_OFFSET
32385            ),
32386            0,
32387            "the insertion-point style must be stored in TextEdit's null scrap"
32388        );
32389
32390        let inserted = bus.alloc(1);
32391        bus.write_byte(inserted, b'B');
32392        cpu.write_reg(Register::A7, TEST_SP);
32393        bus.write_word(TEST_SP, 0x0007);
32394        bus.write_long(TEST_SP + 2, te_handle);
32395        bus.write_long(TEST_SP + 6, 0); // NIL means use the null style scrap
32396        bus.write_long(TEST_SP + 10, 1);
32397        bus.write_long(TEST_SP + 14, inserted);
32398        let result = disp.dispatch_dialog(true, 0x03D, &mut cpu, &mut bus);
32399        assert!(result.unwrap().is_ok());
32400        let runs = disp.te_style_runs(&bus, te_handle, 2);
32401        assert_eq!(runs.len(), 2);
32402        assert_eq!((runs[0].start, runs[0].style.face), (0, 1));
32403        assert_eq!((runs[1].start, runs[1].style.face), (1, 0));
32404    }
32405
32406    #[test]
32407    fn te_dispatch_continuous_style_returns_unstyled_record_style() {
32408        let (mut disp, mut cpu, mut bus) = setup();
32409
32410        let te_handle = TrapDispatcher::allocate_te_handle(&mut bus);
32411        disp.current_port = 0x181000;
32412        disp.tx_font = 4;
32413        disp.tx_face = 1;
32414        disp.tx_mode = 2;
32415        disp.tx_size = 12;
32416        disp.initialize_te_record(&mut bus, te_handle, (0, 0, 40, 80), (0, 0, 40, 80));
32417
32418        let te_ptr = bus.read_long(te_handle);
32419        bus.write_word(te_ptr + TrapDispatcher::TE_TX_FONT_OFFSET, 22);
32420        bus.write_byte(te_ptr + TrapDispatcher::TE_TX_FACE_OFFSET, 3);
32421        bus.write_word(te_ptr + TrapDispatcher::TE_TX_SIZE_OFFSET, 18);
32422
32423        let style_ptr = 0x300000u32;
32424        let mode_ptr = 0x300100u32;
32425        bus.write_word(mode_ptr, 0x000F); // doAll
32426        bus.write_word(TEST_SP, 0x000A);
32427        bus.write_long(TEST_SP + 2, te_handle);
32428        bus.write_long(TEST_SP + 6, style_ptr);
32429        bus.write_long(TEST_SP + 10, mode_ptr);
32430
32431        let result = disp.dispatch_dialog(true, 0x03D, &mut cpu, &mut bus);
32432        assert!(result.unwrap().is_ok());
32433        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 14);
32434        assert_eq!(bus.read_word(TEST_SP + 14), 0xFFFF);
32435        assert_eq!(bus.read_word(style_ptr), 22);
32436        assert_eq!(bus.read_byte(style_ptr + 2), 3);
32437        assert_eq!(bus.read_word(style_ptr + 4), 18);
32438        assert_eq!(bus.read_word(style_ptr + 6), 0);
32439        assert_eq!(bus.read_word(style_ptr + 8), 0);
32440        assert_eq!(bus.read_word(style_ptr + 10), 0);
32441    }
32442
32443    #[test]
32444    fn te_scroll_reads_handle_from_stack_top() {
32445        let (mut disp, mut cpu, mut bus) = setup();
32446
32447        let te_handle = TrapDispatcher::allocate_te_handle(&mut bus);
32448        disp.current_port = 0x181000;
32449        disp.tx_font = 4;
32450        disp.tx_face = 0;
32451        disp.tx_mode = 0;
32452        disp.tx_size = 10;
32453        disp.initialize_te_record(&mut bus, te_handle, (10, 20, 50, 80), (10, 20, 50, 80));
32454
32455        bus.write_long(TEST_SP, te_handle);
32456        bus.write_word(TEST_SP + 4, (-6i16) as u16);
32457        bus.write_word(TEST_SP + 6, 4);
32458
32459        let result = disp.dispatch_dialog(true, 0x1DD, &mut cpu, &mut bus);
32460        assert!(result.unwrap().is_ok());
32461        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
32462
32463        let te_ptr = bus.read_long(te_handle);
32464        assert_eq!(
32465            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET),
32466            4
32467        );
32468        assert_eq!(
32469            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 2),
32470            24
32471        );
32472        assert_eq!(
32473            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 4),
32474            44
32475        );
32476        assert_eq!(
32477            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 6),
32478            84
32479        );
32480    }
32481
32482    #[test]
32483    fn te_pin_scroll_reads_handle_from_stack_top() {
32484        let (mut disp, mut cpu, mut bus) = setup();
32485
32486        let te_handle = TrapDispatcher::allocate_te_handle(&mut bus);
32487        disp.current_port = 0x181000;
32488        disp.tx_font = 4;
32489        disp.tx_face = 0;
32490        disp.tx_mode = 0;
32491        disp.tx_size = 10;
32492        disp.initialize_te_record(&mut bus, te_handle, (10, 20, 50, 80), (10, 20, 50, 80));
32493        disp.te_set_text_contents(
32494            &mut bus,
32495            te_handle,
32496            b"one two three four five six seven eight nine ten",
32497        );
32498
32499        bus.write_long(TEST_SP, te_handle);
32500        bus.write_word(TEST_SP + 4, (-6i16) as u16);
32501        bus.write_word(TEST_SP + 6, 4);
32502
32503        let result = disp.dispatch_dialog(true, 0x012, &mut cpu, &mut bus);
32504        assert!(result.unwrap().is_ok());
32505        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
32506
32507        let te_ptr = bus.read_long(te_handle);
32508        assert_eq!(
32509            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET),
32510            4
32511        );
32512        assert_eq!(
32513            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 2),
32514            20
32515        );
32516        assert_eq!(
32517            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 4),
32518            44
32519        );
32520        assert_eq!(
32521            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 6),
32522            80
32523        );
32524    }
32525
32526    #[test]
32527    fn tepinscroll_in_range_negative_dv_offsets_destrect_top_and_bottom_exactly_by_dv() {
32528        // Inside Macintosh: Text 1993, p. 2-91: "The destination rectangle
32529        // is offset by the amount scrolled." In-range up-scroll with
32530        // multi-line text that overflows the view: with text
32531        // "A\rB\rC\rD\rE\rF\rG\rH"
32532        // and view height 10, any small negative dv is in-range so destRect.
32533        // top and destRect.bottom must shift by exactly dv with destRect.
32534        // left and destRect.right unchanged (dh=0).
32535        let (mut disp, mut cpu, mut bus) = setup();
32536
32537        let te_handle = make_te_with_text(&mut disp, &mut bus, b"A\rB\rC\rD\rE\rF\rG\rH");
32538        let te_ptr = bus.read_long(te_handle);
32539        bus.write_word(
32540            te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET,
32541            (-1000i16) as u16,
32542        );
32543        bus.write_word(
32544            te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 2,
32545            (-1000i16) as u16,
32546        );
32547        bus.write_word(
32548            te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 4,
32549            (-990i16) as u16,
32550        );
32551        bus.write_word(
32552            te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 6,
32553            (-800i16) as u16,
32554        );
32555        bus.write_word(
32556            te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET,
32557            (-1000i16) as u16,
32558        );
32559        bus.write_word(
32560            te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 2,
32561            (-1000i16) as u16,
32562        );
32563        bus.write_word(
32564            te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 4,
32565            (-990i16) as u16,
32566        );
32567        bus.write_word(
32568            te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 6,
32569            (-800i16) as u16,
32570        );
32571
32572        bus.write_long(TEST_SP, te_handle);
32573        bus.write_word(TEST_SP + 4, (-3i16) as u16);
32574        bus.write_word(TEST_SP + 6, 0);
32575
32576        let result = disp.dispatch_dialog(true, 0x012, &mut cpu, &mut bus);
32577        assert!(result.unwrap().is_ok());
32578        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
32579
32580        assert_eq!(
32581            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET) as i16,
32582            -1003,
32583            "destRect.top must shift by exactly dv=-3"
32584        );
32585        assert_eq!(
32586            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 2) as i16,
32587            -1000,
32588            "destRect.left must be unchanged (dh=0)"
32589        );
32590        assert_eq!(
32591            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 4) as i16,
32592            -993,
32593            "destRect.bottom must shift by exactly dv=-3"
32594        );
32595        assert_eq!(
32596            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 6) as i16,
32597            -800,
32598            "destRect.right must be unchanged (dh=0)"
32599        );
32600    }
32601
32602    #[test]
32603    fn tepinscroll_pascal_lr_stack_layout_reads_dh_dv_and_hte_from_correct_offsets() {
32604        // Per Pascal LR-push-first-arg-deepest convention (matching the
32605        // EXTERN_API expansion in MPW Universal Headers TextEdit.h):
32606        //   sp+0  hTE: TEHandle  (4) — last pushed, shallowest
32607        //   sp+4  dv:  INTEGER   (2) — middle
32608        //   sp+6  dh:  INTEGER   (2) — first pushed, deepest
32609        // Total pop = 8 bytes; no function-result slot.
32610        //
32611        // Stage hTE at sp+0, dv=-2 at sp+4, dh=+7 at sp+6 with a 0xCAFE
32612        // sentinel at sp+8 to detect over-reads. The HLE must consume
32613        // exactly 8 bytes and the sentinel must survive.
32614        let (mut disp, mut cpu, mut bus) = setup();
32615
32616        let te_handle = make_te_with_text(&mut disp, &mut bus, b"A\rB\rC\rD\rE\rF");
32617        let te_ptr = bus.read_long(te_handle);
32618        bus.write_word(
32619            te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET,
32620            (-500i16) as u16,
32621        );
32622        bus.write_word(
32623            te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 2,
32624            (-500i16) as u16,
32625        );
32626        bus.write_word(
32627            te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 4,
32628            (-490i16) as u16,
32629        );
32630        bus.write_word(
32631            te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 6,
32632            (-400i16) as u16,
32633        );
32634        bus.write_word(
32635            te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET,
32636            (-500i16) as u16,
32637        );
32638        // destRect.left = -510 (shifted left of view.left by 10, so positive
32639        // dh can be in-range to actually scroll right)
32640        bus.write_word(
32641            te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 2,
32642            (-510i16) as u16,
32643        );
32644        bus.write_word(
32645            te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 4,
32646            (-490i16) as u16,
32647        );
32648        bus.write_word(
32649            te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 6,
32650            (-310i16) as u16,
32651        );
32652
32653        bus.write_long(TEST_SP, te_handle);
32654        bus.write_word(TEST_SP + 4, (-2i16) as u16);
32655        bus.write_word(TEST_SP + 6, 7);
32656        bus.write_word(TEST_SP + 8, 0xCAFE);
32657
32658        let result = disp.dispatch_dialog(true, 0x012, &mut cpu, &mut bus);
32659        assert!(result.unwrap().is_ok());
32660        assert_eq!(
32661            cpu.read_reg(Register::A7),
32662            TEST_SP + 8,
32663            "Pascal PROCEDURE must pop exactly 8 bytes (dh 2 + dv 2 + hTE 4)"
32664        );
32665        assert_eq!(
32666            bus.read_word(TEST_SP + 8),
32667            0xCAFE,
32668            "sentinel past the arg frame must survive"
32669        );
32670        // Verify destRect was actually mutated to confirm the HLE read
32671        // its args from the correct stack offsets (a swapped-arg stub
32672        // would shift by wrong amounts or pick up the sentinel).
32673        assert_eq!(
32674            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET) as i16,
32675            -502,
32676            "destRect.top must shift by dv=-2"
32677        );
32678        assert_eq!(
32679            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 2) as i16,
32680            -503,
32681            "destRect.left must shift by dh=+7 (clamped to max_right=10)"
32682        );
32683    }
32684
32685    #[test]
32686    fn tepinscroll_clamps_overscroll_when_last_line_is_already_visible() {
32687        // Inside Macintosh: Text 1993, p. 2-91: TEPinScroll clamps movement
32688        // so scrolling stops once the last line is visible.
32689        let (mut disp, mut cpu, mut bus) = setup();
32690
32691        let te_handle = make_te_with_text(&mut disp, &mut bus, b"A");
32692        let te_ptr = bus.read_long(te_handle);
32693        bus.write_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET, 10);
32694        bus.write_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 2, 20);
32695        bus.write_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 4, 50);
32696        bus.write_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 6, 80);
32697        bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET, 10);
32698        bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 2, 20);
32699        bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 4, 50);
32700        bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 6, 80);
32701
32702        bus.write_long(TEST_SP, te_handle);
32703        bus.write_word(TEST_SP + 4, (-20i16) as u16);
32704        bus.write_word(TEST_SP + 6, 0);
32705        let result = disp.dispatch_dialog(true, 0x012, &mut cpu, &mut bus);
32706        assert!(result.unwrap().is_ok());
32707        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
32708
32709        assert_eq!(
32710            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET),
32711            10
32712        );
32713        assert_eq!(
32714            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 2),
32715            20
32716        );
32717        assert_eq!(
32718            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 4),
32719            50
32720        );
32721        assert_eq!(
32722            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 6),
32723            80
32724        );
32725    }
32726
32727    #[test]
32728    fn teselview_autoscroll_disabled_leaves_destrect_unchanged() {
32729        // Inside Macintosh: Text 1993, p. 2-92: TESelView only scrolls when
32730        // auto-scroll is enabled for the TE record.
32731        let (mut disp, mut cpu, mut bus) = setup();
32732        let te_handle = make_te_with_text(
32733            &mut disp,
32734            &mut bus,
32735            b"one two three four five six seven eight nine ten",
32736        );
32737        let te_ptr = bus.read_long(te_handle);
32738        bus.write_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET, 10);
32739        bus.write_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 2, 20);
32740        bus.write_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 4, 50);
32741        bus.write_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 6, 80);
32742        bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET, 0);
32743        bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 2, 20);
32744        bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 4, 40);
32745        bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 6, 80);
32746        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 0);
32747        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 0);
32748        disp.set_te_feature_bit(te_handle, TrapDispatcher::TE_FEATURE_AUTO_SCROLL, false);
32749
32750        bus.write_long(TEST_SP, te_handle);
32751        let result = disp.dispatch_dialog(true, 0x011, &mut cpu, &mut bus);
32752        assert!(result.unwrap().is_ok());
32753        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
32754        assert_eq!(
32755            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET),
32756            0
32757        );
32758        assert_eq!(
32759            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 2),
32760            20
32761        );
32762        assert_eq!(
32763            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 4),
32764            40
32765        );
32766        assert_eq!(
32767            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 6),
32768            80
32769        );
32770    }
32771
32772    #[test]
32773    fn teselview_autoscroll_enabled_scrolls_destrect_toward_selection() {
32774        // Inside Macintosh: Text 1993, p. 2-92: TESelView scrolls selection
32775        // into view when auto-scroll is enabled.
32776        let (mut disp, mut cpu, mut bus) = setup();
32777        let te_handle = make_te_with_text(
32778            &mut disp,
32779            &mut bus,
32780            b"one two three four five six seven eight nine ten",
32781        );
32782        let te_ptr = bus.read_long(te_handle);
32783        bus.write_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET, 10);
32784        bus.write_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 2, 20);
32785        bus.write_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 4, 50);
32786        bus.write_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 6, 80);
32787        bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET, 0);
32788        bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 2, 20);
32789        bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 4, 40);
32790        bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 6, 80);
32791        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 0);
32792        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 0);
32793        disp.set_te_feature_bit(te_handle, TrapDispatcher::TE_FEATURE_AUTO_SCROLL, true);
32794
32795        bus.write_long(TEST_SP, te_handle);
32796        let result = disp.dispatch_dialog(true, 0x011, &mut cpu, &mut bus);
32797        assert!(result.unwrap().is_ok());
32798        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
32799        assert_eq!(
32800            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET),
32801            10
32802        );
32803        assert_eq!(
32804            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 2),
32805            20
32806        );
32807        assert_eq!(
32808            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 4),
32809            50
32810        );
32811        assert_eq!(
32812            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 6),
32813            80
32814        );
32815    }
32816
32817    #[test]
32818    fn teselview_apple_canonical_below_view_shifts_destrect_top_up() {
32819        // Inside Macintosh: Text 1993, p. 2-92 (Apple canonical):
32820        //   "The top left part of the selection range is scrolled
32821        //    into view."
32822        // BasiliskII System 7.5.3 ROM does not scroll destRect in
32823        // this case (empirically verified); Systemless implements the
32824        // Apple-canonical semantic.
32825        //
32826        // Setup: destRect = (0, 0, 100, 200) tall enough for the
32827        // entire multi-line text, viewRect = (0, 0, 100, 30) showing
32828        // only the first ~1.5 lines, selection at the last character
32829        // (line 7 of "A\rB\r..\rH", well below viewRect.bottom).
32830        let (mut disp, mut cpu, mut bus) = setup();
32831        let te_handle = make_te_with_text(&mut disp, &mut bus, b"A\rB\rC\rD\rE\rF\rG\rH");
32832        let te_ptr = bus.read_long(te_handle);
32833        bus.write_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET, 0);
32834        bus.write_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 2, 0);
32835        bus.write_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 4, 30);
32836        bus.write_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 6, 100);
32837        bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET, 0);
32838        bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 2, 0);
32839        bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 4, 200);
32840        bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 6, 100);
32841        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 14);
32842        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 14);
32843        disp.set_te_feature_bit(te_handle, TrapDispatcher::TE_FEATURE_AUTO_SCROLL, true);
32844
32845        let pre_top = bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET) as i16;
32846
32847        bus.write_long(TEST_SP, te_handle);
32848        let result = disp.dispatch_dialog(true, 0x011, &mut cpu, &mut bus);
32849        assert!(result.unwrap().is_ok());
32850        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
32851
32852        let post_top = bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET) as i16;
32853        assert!(
32854            post_top < pre_top,
32855            "TESelView must shift destRect.top UP (negative direction) when selection is below viewRect per IM:Text 1993 p. 2-92; got pre_top={} post_top={}",
32856            pre_top,
32857            post_top
32858        );
32859    }
32860
32861    #[test]
32862    fn teselview_procedure_protocol_consumes_only_tehandle_arg() {
32863        // Pascal PROCEDURE: 4-byte hTE arg, no result. Sentinel guard
32864        // at TEST_SP+4 must survive the call (trap must not write past
32865        // its argument frame). A7 advances by exactly 4 bytes.
32866        let (mut disp, mut cpu, mut bus) = setup();
32867        let te_handle = make_te_with_text(&mut disp, &mut bus, b"hi");
32868        // Auto-scroll disabled so the trap is a guaranteed destRect
32869        // no-op and the only observable side-effect is the SP advance.
32870        disp.set_te_feature_bit(te_handle, TrapDispatcher::TE_FEATURE_AUTO_SCROLL, false);
32871
32872        bus.write_long(TEST_SP, te_handle);
32873        bus.write_word(TEST_SP + 4, 0xCAFE);
32874        bus.write_word(TEST_SP + 6, 0xBABE);
32875        let result = disp.dispatch_dialog(true, 0x011, &mut cpu, &mut bus);
32876        assert!(result.unwrap().is_ok());
32877        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
32878        assert_eq!(bus.read_word(TEST_SP + 4), 0xCAFE);
32879        assert_eq!(bus.read_word(TEST_SP + 6), 0xBABE);
32880    }
32881
32882    #[test]
32883    fn tegetoffset_point_above_destrect_returns_zero() {
32884        // Inside Macintosh Volume V (1986), p. V-172: TEGetOffset
32885        // returns the character offset of the start of the first line
32886        // when the point is above the first line. Pascal LR layout:
32887        //   sp+0..3 hTE (last pushed, shallowest)
32888        //   sp+4..5 pt.v
32889        //   sp+6..7 pt.h
32890        //   sp+8..9 INTEGER result slot
32891        let (mut disp, mut cpu, mut bus) = setup();
32892        let te_handle = make_te_with_text(&mut disp, &mut bus, b"ABC");
32893        let te_ptr = bus.read_long(te_handle);
32894        bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET, 10);
32895        bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 2, 20);
32896        bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 4, 40);
32897        bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 6, 140);
32898
32899        cpu.write_reg(Register::A7, TEST_SP);
32900        bus.write_long(TEST_SP, te_handle);
32901        bus.write_word(TEST_SP + 4, 5);
32902        bus.write_word(TEST_SP + 6, 30);
32903        bus.write_word(TEST_SP + 8, 0xBEEF);
32904        let result = disp.dispatch_dialog(true, 0x03C, &mut cpu, &mut bus);
32905        assert!(result.unwrap().is_ok());
32906        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
32907        assert_eq!(bus.read_word(TEST_SP + 8) as i16, 0);
32908    }
32909
32910    #[test]
32911    fn tegetoffset_point_below_last_line_returns_telength() {
32912        // Inside Macintosh Volume V (1986), p. V-172: TEGetOffset
32913        // returns the character offset of the end of the text when
32914        // the point is below the last line. Witnesses that the HLE
32915        // computes a value that varies with the point arg (vs the
32916        // always-zero result the pre-fix off-by-2 read produced).
32917        let (mut disp, mut cpu, mut bus) = setup();
32918        let te_handle = make_te_with_text(&mut disp, &mut bus, b"ABC");
32919        let te_ptr = bus.read_long(te_handle);
32920        bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET, 10);
32921        bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 2, 20);
32922        bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 4, 40);
32923        bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 6, 140);
32924
32925        cpu.write_reg(Register::A7, TEST_SP);
32926        bus.write_long(TEST_SP, te_handle);
32927        bus.write_word(TEST_SP + 4, 200);
32928        bus.write_word(TEST_SP + 6, 30);
32929        bus.write_word(TEST_SP + 8, 0xBEEF);
32930        let result = disp.dispatch_dialog(true, 0x03C, &mut cpu, &mut bus);
32931        assert!(result.unwrap().is_ok());
32932        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
32933        assert_eq!(bus.read_word(TEST_SP + 8) as i16, 3);
32934    }
32935
32936    #[test]
32937    fn tegetoffset_function_protocol_consumes_point_and_tehandle_args_writes_integer_result() {
32938        // Pascal FUNCTION protocol: 8 bytes of args (Point + TEHandle)
32939        // popped, 2-byte INTEGER result written at sp+8. Sentinel at
32940        // sp+10 must survive — the trap must not write past the
32941        // 2-byte result slot.
32942        let (mut disp, mut cpu, mut bus) = setup();
32943        let te_handle = make_te_with_text(&mut disp, &mut bus, b"ABC");
32944        let te_ptr = bus.read_long(te_handle);
32945        bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET, 10);
32946        bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 2, 20);
32947        bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 4, 40);
32948        bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 6, 140);
32949
32950        cpu.write_reg(Register::A7, TEST_SP);
32951        bus.write_long(TEST_SP, te_handle);
32952        bus.write_word(TEST_SP + 4, 5);
32953        bus.write_word(TEST_SP + 6, 30);
32954        bus.write_word(TEST_SP + 8, 0xDEAD);
32955        bus.write_word(TEST_SP + 10, 0xCAFE);
32956        let result = disp.dispatch_dialog(true, 0x03C, &mut cpu, &mut bus);
32957        assert!(result.unwrap().is_ok());
32958        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
32959        assert_ne!(bus.read_word(TEST_SP + 8), 0xDEAD);
32960        assert_eq!(bus.read_word(TEST_SP + 10), 0xCAFE);
32961    }
32962
32963    #[test]
32964    fn tefindword_returns_word_bounds_for_interior_positions() {
32965        // Inside Macintosh: Text (1993), pp. 2-60..2-61: TEFindWord
32966        // reports the word boundaries surrounding the current position.
32967        let (mut disp, mut cpu, mut bus) = setup();
32968        let te_handle = make_te_with_text(&mut disp, &mut bus, b"alpha beta");
32969        let te_ptr = bus.read_long(te_handle);
32970
32971        cpu.write_reg(Register::A7, TEST_SP);
32972        cpu.write_reg(Register::D0, 1); // inside "alpha"
32973        cpu.write_reg(Register::D2, 0x1111);
32974        cpu.write_reg(Register::A3, te_ptr);
32975        cpu.write_reg(Register::A4, te_handle);
32976
32977        let result = disp.dispatch_dialog(false, 0x0FE, &mut cpu, &mut bus);
32978        assert!(result.unwrap().is_ok());
32979        assert_eq!(cpu.read_reg(Register::A7), TEST_SP);
32980        assert_eq!(cpu.read_reg(Register::D0) as u16, 0);
32981        assert_eq!(cpu.read_reg(Register::D1) as u16, 5);
32982    }
32983
32984    #[test]
32985    fn tefindword_returns_second_word_bounds_without_touching_stack() {
32986        // Same TextEdit hook: a later position inside the second word
32987        // should return that word's bounds, and the register-based hook
32988        // must not consume any stack bytes.
32989        let (mut disp, mut cpu, mut bus) = setup();
32990        let te_handle = make_te_with_text(&mut disp, &mut bus, b"alpha beta");
32991        let te_ptr = bus.read_long(te_handle);
32992
32993        cpu.write_reg(Register::A7, TEST_SP);
32994        bus.write_word(TEST_SP, 0xCAFE);
32995        cpu.write_reg(Register::D0, 7); // inside "beta"
32996        cpu.write_reg(Register::D2, 0x2222);
32997        cpu.write_reg(Register::A3, te_ptr);
32998        cpu.write_reg(Register::A4, te_handle);
32999
33000        let result = disp.dispatch_dialog(false, 0x0FE, &mut cpu, &mut bus);
33001        assert!(result.unwrap().is_ok());
33002        assert_eq!(cpu.read_reg(Register::A7), TEST_SP);
33003        assert_eq!(bus.read_word(TEST_SP), 0xCAFE);
33004        assert_eq!(cpu.read_reg(Register::D0) as u16, 6);
33005        assert_eq!(cpu.read_reg(Register::D1) as u16, 10);
33006    }
33007
33008    #[test]
33009    fn tesettext_copies_bytes_updates_length_and_pops_arguments() {
33010        // Inside Macintosh Volume I (1985), p. I-378: TESetText replaces an
33011        // edit record's text contents.
33012        let (mut disp, mut cpu, mut bus) = setup();
33013        let te_handle = make_te_with_text(&mut disp, &mut bus, b"OLD");
33014        let te_ptr = bus.read_long(te_handle);
33015        let source_ptr = bus.alloc(4);
33016        bus.write_bytes(source_ptr, b"NEW!");
33017
33018        bus.write_long(TEST_SP, te_handle);
33019        bus.write_long(TEST_SP + 4, 4);
33020        bus.write_long(TEST_SP + 8, source_ptr);
33021        let result = disp.dispatch_dialog(true, 0x1CF, &mut cpu, &mut bus);
33022        assert!(result.unwrap().is_ok());
33023        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 12);
33024
33025        assert_eq!(
33026            TrapDispatcher::te_text_bytes(&bus, te_handle),
33027            b"NEW!".to_vec()
33028        );
33029        assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_LENGTH_OFFSET), 4);
33030        assert_eq!(
33031            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
33032            4
33033        );
33034        assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET), 4);
33035        let n_lines = u32::from(bus.read_word(te_ptr + TrapDispatcher::TE_N_LINES_OFFSET));
33036        assert!(n_lines > 0);
33037        assert_eq!(
33038            bus.read_word(te_ptr + TrapDispatcher::TE_LINE_STARTS_OFFSET + n_lines * 2),
33039            4
33040        );
33041    }
33042
33043    #[test]
33044    fn tesettext_nil_or_zero_length_input_clears_text() {
33045        // Inside Macintosh Volume I (1985), p. I-378: TESetText sets current
33046        // text contents; empty input yields empty text.
33047        let (mut disp, mut cpu, mut bus) = setup();
33048        let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
33049        let te_ptr = bus.read_long(te_handle);
33050
33051        bus.write_long(TEST_SP, te_handle);
33052        bus.write_long(TEST_SP + 4, 5);
33053        bus.write_long(TEST_SP + 8, 0);
33054        let result = disp.dispatch_dialog(true, 0x1CF, &mut cpu, &mut bus);
33055        assert!(result.unwrap().is_ok());
33056        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 12);
33057        assert_eq!(
33058            TrapDispatcher::te_text_bytes(&bus, te_handle),
33059            Vec::<u8>::new()
33060        );
33061        assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_LENGTH_OFFSET), 0);
33062        assert_eq!(
33063            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
33064            0
33065        );
33066        assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET), 0);
33067    }
33068
33069    #[test]
33070    fn tesettext_replaces_prior_contents_via_sequential_call() {
33071        // Inside Macintosh Volume I (1985), p. I-378: TESetText *sets*
33072        // (not appends to) the current text contents: TESetText("WORLD!", 6)
33073        // followed by TESetText("HI", 2) on the same TERec yields
33074        // teLength == 2 with the first two bytes equal to "HI".
33075        let (mut disp, mut cpu, mut bus) = setup();
33076        let te_handle = make_te_with_text(&mut disp, &mut bus, b"OLD");
33077        let te_ptr = bus.read_long(te_handle);
33078
33079        let first_ptr = bus.alloc(6);
33080        bus.write_bytes(first_ptr, b"WORLD!");
33081        bus.write_long(TEST_SP, te_handle);
33082        bus.write_long(TEST_SP + 4, 6);
33083        bus.write_long(TEST_SP + 8, first_ptr);
33084        let r1 = disp.dispatch_dialog(true, 0x1CF, &mut cpu, &mut bus);
33085        assert!(r1.unwrap().is_ok());
33086        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 12);
33087        assert_eq!(
33088            TrapDispatcher::te_text_bytes(&bus, te_handle),
33089            b"WORLD!".to_vec()
33090        );
33091        assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_LENGTH_OFFSET), 6);
33092
33093        cpu.write_reg(Register::A7, TEST_SP);
33094        let second_ptr = bus.alloc(2);
33095        bus.write_bytes(second_ptr, b"HI");
33096        bus.write_long(TEST_SP, te_handle);
33097        bus.write_long(TEST_SP + 4, 2);
33098        bus.write_long(TEST_SP + 8, second_ptr);
33099        let r2 = disp.dispatch_dialog(true, 0x1CF, &mut cpu, &mut bus);
33100        assert!(r2.unwrap().is_ok());
33101        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 12);
33102        assert_eq!(
33103            TrapDispatcher::te_text_bytes(&bus, te_handle),
33104            b"HI".to_vec()
33105        );
33106        assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_LENGTH_OFFSET), 2);
33107    }
33108
33109    #[test]
33110    fn tesettext_balances_stackspace_with_pascal_protocol() {
33111        // Inside Macintosh Volume I (1985), p. I-378: TESetText is a
33112        // PROCEDURE with three args (Ptr text, LONGINT length, TEHandle).
33113        // Pascal LR push order yields a 12-byte arg frame and no result
33114        // slot. A7 must advance by exactly 12 bytes regardless of arg
33115        // values.
33116        let (mut disp, mut cpu, mut bus) = setup();
33117        let te_handle = make_te_with_text(&mut disp, &mut bus, b"SEED");
33118        let source = bus.alloc(1);
33119        bus.write_bytes(source, b"X");
33120
33121        bus.write_long(TEST_SP, te_handle);
33122        bus.write_long(TEST_SP + 4, 1);
33123        bus.write_long(TEST_SP + 8, source);
33124        bus.write_word(TEST_SP + 12, 0xCAFE);
33125        let result = disp.dispatch_dialog(true, 0x1CF, &mut cpu, &mut bus);
33126        assert!(result.unwrap().is_ok());
33127        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 12);
33128        assert_eq!(bus.read_word(TEST_SP + 12), 0xCAFE);
33129        assert_eq!(
33130            TrapDispatcher::te_text_bytes(&bus, te_handle),
33131            b"X".to_vec()
33132        );
33133    }
33134
33135    #[test]
33136    fn tekey_inserts_character_at_caret_and_advances_selection() {
33137        // Inside Macintosh Volume I (1985), p. I-385 and Text 1993, p. 2-81:
33138        // TEKey inserts typed characters at the insertion point.
33139        let (mut disp, mut cpu, mut bus) = setup();
33140        let te_handle = make_te_with_text(&mut disp, &mut bus, b"HLO");
33141        let te_ptr = bus.read_long(te_handle);
33142        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 1);
33143        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 1);
33144
33145        bus.write_long(TEST_SP, te_handle);
33146        bus.write_word(TEST_SP + 4, u16::from(b'E'));
33147        let result = disp.dispatch_dialog(true, 0x1DC, &mut cpu, &mut bus);
33148        assert!(result.unwrap().is_ok());
33149        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
33150        assert_eq!(
33151            TrapDispatcher::te_text_bytes(&bus, te_handle),
33152            b"HELO".to_vec()
33153        );
33154        assert_eq!(
33155            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
33156            2
33157        );
33158        assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET), 2);
33159    }
33160
33161    #[test]
33162    fn tekey_backspace_deletes_selection_or_previous_character() {
33163        // Inside Macintosh Volume I (1985), p. I-385 and Text 1993, p. 2-81:
33164        // backspace deletes current selection, or the previous character.
33165        let (mut disp, mut cpu, mut bus) = setup();
33166        let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
33167        let te_ptr = bus.read_long(te_handle);
33168
33169        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 1);
33170        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 3);
33171        bus.write_long(TEST_SP, te_handle);
33172        bus.write_word(TEST_SP + 4, 0x0008);
33173        let result = disp.dispatch_dialog(true, 0x1DC, &mut cpu, &mut bus);
33174        assert!(result.unwrap().is_ok());
33175        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
33176        assert_eq!(
33177            TrapDispatcher::te_text_bytes(&bus, te_handle),
33178            b"HLO".to_vec()
33179        );
33180        assert_eq!(
33181            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
33182            1
33183        );
33184        assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET), 1);
33185
33186        disp.te_set_text_contents(&mut bus, te_handle, b"HELLO");
33187        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 2);
33188        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 2);
33189        cpu.write_reg(Register::A7, TEST_SP);
33190        bus.write_long(TEST_SP, te_handle);
33191        bus.write_word(TEST_SP + 4, 0x0008);
33192        let result = disp.dispatch_dialog(true, 0x1DC, &mut cpu, &mut bus);
33193        assert!(result.unwrap().is_ok());
33194        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
33195        assert_eq!(
33196            TrapDispatcher::te_text_bytes(&bus, te_handle),
33197            b"HLLO".to_vec()
33198        );
33199        assert_eq!(
33200            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
33201            1
33202        );
33203        assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET), 1);
33204    }
33205
33206    // ---- HideDialogItem / ShowDialogItem ($A827 / $A828) ----
33207    // Gate the move-offscreen-and-restore behaviour.
33208
33209    #[test]
33210    fn hide_dialog_item_moves_rect_offscreen_and_saves_original() {
33211        // MTE 1992, 6-123: HideDialogItem offsets left/right by +16384.
33212        let (mut disp, mut cpu, mut bus) = setup();
33213        let dialog_ptr = 0x210000u32;
33214        disp.dialog_items.insert(
33215            dialog_ptr,
33216            vec![DialogItem {
33217                item_type: 4,
33218                rect: (10, 20, 30, 120),
33219                text: "OK".into(),
33220                resource_id: 0,
33221                proc_ptr: 0,
33222                sel_start: 0,
33223                sel_end: 0,
33224            }],
33225        );
33226
33227        // Stack: SP+0=itemNo(2), SP+2=dialog_ptr(4). Pop 6.
33228        bus.write_word(TEST_SP, 1);
33229        bus.write_long(TEST_SP + 2, dialog_ptr);
33230
33231        let result = disp.dispatch_dialog(true, 0x027, &mut cpu, &mut bus);
33232        assert!(result.unwrap().is_ok());
33233        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
33234
33235        // Rect is now offscreen.
33236        let item_rect = disp.dialog_items.get(&dialog_ptr).unwrap()[0].rect;
33237        assert!(
33238            item_rect.0 == 10 && item_rect.2 == 30,
33239            "vertical coordinates are unchanged by HideDialogItem, got {item_rect:?}"
33240        );
33241        assert!(
33242            item_rect.1 == 20 + 16384 && item_rect.3 == 120 + 16384,
33243            "hidden item rect must be offscreen, got {item_rect:?}"
33244        );
33245        // Original saved.
33246        assert_eq!(
33247            disp.hidden_dialog_item_rects.get(&(dialog_ptr, 1)).copied(),
33248            Some((10, 20, 30, 120))
33249        );
33250    }
33251
33252    #[test]
33253    fn show_dialog_item_restores_saved_rect() {
33254        // MTE 1992, 6-124: ShowDialogItem restores pre-hide item rect.
33255        let (mut disp, mut cpu, mut bus) = setup();
33256        let dialog_ptr = 0x210000u32;
33257        disp.dialog_items.insert(
33258            dialog_ptr,
33259            vec![DialogItem {
33260                item_type: 4,
33261                rect: (10, 20 + 16384, 30, 120 + 16384),
33262                text: "OK".into(),
33263                resource_id: 0,
33264                proc_ptr: 0,
33265                sel_start: 0,
33266                sel_end: 0,
33267            }],
33268        );
33269        disp.hidden_dialog_item_rects
33270            .insert((dialog_ptr, 1), (10, 20, 30, 120));
33271
33272        bus.write_word(TEST_SP, 1);
33273        bus.write_long(TEST_SP + 2, dialog_ptr);
33274
33275        let result = disp.dispatch_dialog(true, 0x028, &mut cpu, &mut bus);
33276        assert!(result.unwrap().is_ok());
33277        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
33278
33279        assert_eq!(
33280            disp.dialog_items.get(&dialog_ptr).unwrap()[0].rect,
33281            (10, 20, 30, 120)
33282        );
33283        assert!(
33284            !disp.hidden_dialog_item_rects.contains_key(&(dialog_ptr, 1)),
33285            "saved rect must be removed after show"
33286        );
33287    }
33288
33289    #[test]
33290    fn hide_then_show_is_idempotent_roundtrip() {
33291        let (mut disp, mut cpu, mut bus) = setup();
33292        let dialog_ptr = 0x210000u32;
33293        let orig_rect = (50, 60, 80, 200);
33294        disp.dialog_items.insert(
33295            dialog_ptr,
33296            vec![DialogItem {
33297                item_type: 4,
33298                rect: orig_rect,
33299                text: "Cancel".into(),
33300                resource_id: 0,
33301                proc_ptr: 0,
33302                sel_start: 0,
33303                sel_end: 0,
33304            }],
33305        );
33306
33307        bus.write_word(TEST_SP, 1);
33308        bus.write_long(TEST_SP + 2, dialog_ptr);
33309        disp.dispatch_dialog(true, 0x027, &mut cpu, &mut bus)
33310            .unwrap()
33311            .unwrap();
33312
33313        // Reset SP + args for Show.
33314        cpu.write_reg(Register::A7, TEST_SP);
33315        bus.write_word(TEST_SP, 1);
33316        bus.write_long(TEST_SP + 2, dialog_ptr);
33317        disp.dispatch_dialog(true, 0x028, &mut cpu, &mut bus)
33318            .unwrap()
33319            .unwrap();
33320
33321        assert_eq!(
33322            disp.dialog_items.get(&dialog_ptr).unwrap()[0].rect,
33323            orig_rect,
33324            "round-trip hide→show must restore the exact original rect"
33325        );
33326    }
33327
33328    #[test]
33329    fn hide_dialog_item_already_hidden_left_coord_is_noop() {
33330        // MTE 1992, 6-123: left > 8192 means the item is already hidden.
33331        let (mut disp, mut cpu, mut bus) = setup();
33332        let dialog_ptr = 0x210000u32;
33333        let already_hidden = (10, 9000, 30, 9200);
33334        disp.dialog_items.insert(
33335            dialog_ptr,
33336            vec![DialogItem {
33337                item_type: 4,
33338                rect: already_hidden,
33339                text: "Hidden".into(),
33340                resource_id: 0,
33341                proc_ptr: 0,
33342                sel_start: 0,
33343                sel_end: 0,
33344            }],
33345        );
33346
33347        bus.write_word(TEST_SP, 1);
33348        bus.write_long(TEST_SP + 2, dialog_ptr);
33349
33350        let result = disp.dispatch_dialog(true, 0x027, &mut cpu, &mut bus);
33351        assert!(result.unwrap().is_ok());
33352        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
33353        assert_eq!(
33354            disp.dialog_items.get(&dialog_ptr).unwrap()[0].rect,
33355            already_hidden
33356        );
33357        assert!(
33358            !disp.hidden_dialog_item_rects.contains_key(&(dialog_ptr, 1)),
33359            "already hidden items must not record a new saved rect"
33360        );
33361    }
33362
33363    #[test]
33364    fn show_dialog_item_without_saved_rect_subtracts_offset_when_hidden() {
33365        // MTE 1992, 6-124: ShowDialogItem uses left > 8192 as hidden predicate.
33366        let (mut disp, mut cpu, mut bus) = setup();
33367        let dialog_ptr = 0x210000u32;
33368        disp.dialog_items.insert(
33369            dialog_ptr,
33370            vec![DialogItem {
33371                item_type: 4,
33372                rect: (10, 30 + 16384, 30, 130 + 16384),
33373                text: "ShowMe".into(),
33374                resource_id: 0,
33375                proc_ptr: 0,
33376                sel_start: 0,
33377                sel_end: 0,
33378            }],
33379        );
33380
33381        bus.write_word(TEST_SP, 1);
33382        bus.write_long(TEST_SP + 2, dialog_ptr);
33383
33384        let result = disp.dispatch_dialog(true, 0x028, &mut cpu, &mut bus);
33385        assert!(result.unwrap().is_ok());
33386        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
33387        assert_eq!(
33388            disp.dialog_items.get(&dialog_ptr).unwrap()[0].rect,
33389            (10, 30, 30, 130)
33390        );
33391    }
33392
33393    #[test]
33394    fn show_dialog_item_visible_left_coord_is_noop() {
33395        // MTE 1992, 6-124: left < 8192 means already visible; no-op.
33396        let (mut disp, mut cpu, mut bus) = setup();
33397        let dialog_ptr = 0x210000u32;
33398        let visible = (10, 20, 30, 120);
33399        disp.dialog_items.insert(
33400            dialog_ptr,
33401            vec![DialogItem {
33402                item_type: 4,
33403                rect: visible,
33404                text: "Visible".into(),
33405                resource_id: 0,
33406                proc_ptr: 0,
33407                sel_start: 0,
33408                sel_end: 0,
33409            }],
33410        );
33411
33412        bus.write_word(TEST_SP, 1);
33413        bus.write_long(TEST_SP + 2, dialog_ptr);
33414        let result = disp.dispatch_dialog(true, 0x028, &mut cpu, &mut bus);
33415        assert!(result.unwrap().is_ok());
33416        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
33417        assert_eq!(disp.dialog_items.get(&dialog_ptr).unwrap()[0].rect, visible);
33418    }
33419
33420    // ---- FindDItem ($A984) ----
33421
33422    #[test]
33423    fn findditem_returns_zero_based_index_for_hit_and_pops_stack() {
33424        // MTE 1992, 6-125: FindDialogItem/FindDItem returns 0 for the first
33425        // item, 1 for the second, etc.
33426        let (mut disp, mut cpu, mut bus) = setup();
33427        let dialog_ptr = 0x220000u32;
33428        disp.dialog_items.insert(
33429            dialog_ptr,
33430            vec![
33431                DialogItem {
33432                    item_type: 4,
33433                    rect: (10, 20, 30, 40),
33434                    text: "A".into(),
33435                    resource_id: 0,
33436                    proc_ptr: 0,
33437                    sel_start: 0,
33438                    sel_end: 0,
33439                },
33440                DialogItem {
33441                    item_type: 4,
33442                    rect: (40, 50, 70, 90),
33443                    text: "B".into(),
33444                    resource_id: 0,
33445                    proc_ptr: 0,
33446                    sel_start: 0,
33447                    sel_end: 0,
33448                },
33449            ],
33450        );
33451
33452        bus.write_word(TEST_SP, 45);
33453        bus.write_word(TEST_SP + 2, 55);
33454        bus.write_long(TEST_SP + 4, dialog_ptr);
33455        let result = disp.dispatch_dialog(true, 0x184, &mut cpu, &mut bus);
33456        assert!(result.unwrap().is_ok());
33457        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
33458        assert_eq!(bus.read_word(TEST_SP + 8) as i16, 1);
33459    }
33460
33461    #[test]
33462    fn findditem_point_outside_all_items_returns_minus_one() {
33463        // IM:IV 1986, p. IV-60: FindDItem returns -1 when the point does not
33464        // lie within any item rectangle.
33465        let (mut disp, mut cpu, mut bus) = setup();
33466        let dialog_ptr = 0x220100u32;
33467        disp.dialog_items.insert(
33468            dialog_ptr,
33469            vec![DialogItem {
33470                item_type: 4,
33471                rect: (10, 20, 30, 40),
33472                text: "A".into(),
33473                resource_id: 0,
33474                proc_ptr: 0,
33475                sel_start: 0,
33476                sel_end: 0,
33477            }],
33478        );
33479
33480        bus.write_word(TEST_SP, 99);
33481        bus.write_word(TEST_SP + 2, 99);
33482        bus.write_long(TEST_SP + 4, dialog_ptr);
33483        let result = disp.dispatch_dialog(true, 0x184, &mut cpu, &mut bus);
33484        assert!(result.unwrap().is_ok());
33485        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
33486        assert_eq!(bus.read_word(TEST_SP + 8) as i16, -1);
33487    }
33488
33489    #[test]
33490    fn findditem_returns_first_overlapping_item_and_includes_disabled_items() {
33491        // IM:IV 1986, p. IV-60: overlap resolution is first item in list.
33492        // IM:IV 1986, p. IV-60 note: disabled items are still returned.
33493        let (mut disp, mut cpu, mut bus) = setup();
33494        let dialog_ptr = 0x220200u32;
33495        disp.dialog_items.insert(
33496            dialog_ptr,
33497            vec![
33498                DialogItem {
33499                    item_type: 0x80 | 4, // disabled
33500                    rect: (10, 20, 40, 60),
33501                    text: "DisabledTop".into(),
33502                    resource_id: 0,
33503                    proc_ptr: 0,
33504                    sel_start: 0,
33505                    sel_end: 0,
33506                },
33507                DialogItem {
33508                    item_type: 4,
33509                    rect: (20, 30, 50, 70),
33510                    text: "EnabledBottom".into(),
33511                    resource_id: 0,
33512                    proc_ptr: 0,
33513                    sel_start: 0,
33514                    sel_end: 0,
33515                },
33516            ],
33517        );
33518
33519        // Point lies within both rectangles.
33520        bus.write_word(TEST_SP, 25);
33521        bus.write_word(TEST_SP + 2, 35);
33522        bus.write_long(TEST_SP + 4, dialog_ptr);
33523        let result = disp.dispatch_dialog(true, 0x184, &mut cpu, &mut bus);
33524        assert!(result.unwrap().is_ok());
33525        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
33526        assert_eq!(bus.read_word(TEST_SP + 8) as i16, 0);
33527    }
33528
33529    // ========== Cursor Manager ==========
33530
33531    // ---- InitCursor ($A850) ----
33532
33533    #[test]
33534    fn init_cursor_resets_cursor_level_to_zero_and_sets_arrow_visible() {
33535        // IM:I I-167: InitCursor sets arrow cursor, sets cursor level to 0,
33536        // and makes cursor visible.
33537        let (mut disp, mut cpu, mut bus) = setup();
33538
33539        // Start from a hidden nested level to prove InitCursor reset.
33540        disp.cursor_data = None;
33541        disp.cursor_level = -3;
33542        disp.cursor_visible = false;
33543
33544        let result = disp.dispatch_dialog(true, 0x050, &mut cpu, &mut bus);
33545        assert!(result.unwrap().is_ok());
33546        assert!(disp.cursor_data.is_some());
33547        assert_eq!(disp.cursor_level, 0);
33548        assert!(disp.cursor_visible);
33549    }
33550
33551    // ---- SetCursor ($A851) ----
33552
33553    #[test]
33554    fn set_cursor_reads_cursor_data_and_pops_4_bytes() {
33555        let (mut disp, mut cpu, mut bus) = setup();
33556
33557        let crsr_ptr = 0x300000u32;
33558
33559        // Write cursor bitmap data (32 bytes)
33560        for i in 0..32u32 {
33561            bus.write_byte(crsr_ptr + i, 0xAA);
33562        }
33563        // Write cursor mask data (32 bytes)
33564        for i in 0..32u32 {
33565            bus.write_byte(crsr_ptr + 32 + i, 0xFF);
33566        }
33567        // Write hotspot
33568        bus.write_word(crsr_ptr + 64, 5); // hot_v
33569        bus.write_word(crsr_ptr + 66, 3); // hot_h
33570
33571        // SP+0: crsr_ptr (4 bytes)
33572        bus.write_long(TEST_SP, crsr_ptr);
33573
33574        let result = disp.dispatch_dialog(true, 0x051, &mut cpu, &mut bus);
33575        assert!(result.unwrap().is_ok());
33576        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
33577
33578        let (data, mask, hot_v, hot_h) = disp.cursor_data().unwrap();
33579        assert!(data.iter().all(|&b| b == 0xAA));
33580        assert!(mask.iter().all(|&b| b == 0xFF));
33581        assert_eq!(hot_v, 5);
33582        assert_eq!(hot_h, 3);
33583    }
33584
33585    #[test]
33586    fn set_cursor_does_not_force_visibility_when_cursor_is_hidden() {
33587        // IM:I I-167: if the cursor is hidden, SetCursor changes the
33588        // current cursor image but it remains hidden until uncovered.
33589        let (mut disp, mut cpu, mut bus) = setup();
33590        let crsr_ptr = 0x300100u32;
33591        for i in 0..32u32 {
33592            bus.write_byte(crsr_ptr + i, 0x11);
33593            bus.write_byte(crsr_ptr + 32 + i, 0x22);
33594        }
33595        bus.write_word(crsr_ptr + 64, 9);
33596        bus.write_word(crsr_ptr + 66, 4);
33597        bus.write_long(TEST_SP, crsr_ptr);
33598
33599        disp.cursor_level = -1;
33600        disp.cursor_visible = false;
33601        let result = disp.dispatch_dialog(true, 0x051, &mut cpu, &mut bus);
33602        assert!(result.unwrap().is_ok());
33603        assert_eq!(disp.cursor_level, -1);
33604        assert!(!disp.cursor_visible);
33605        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
33606    }
33607
33608    // ---- HideCursor ($A852) ----
33609
33610    #[test]
33611    fn hide_cursor_decrements_level_and_hides_cursor() {
33612        // IM:I I-168: HideCursor decrements cursor level (from 0 to -1)
33613        // and removes the cursor from the screen.
33614        let (mut disp, mut cpu, mut bus) = setup();
33615        disp.cursor_level = 0;
33616        disp.cursor_visible = true;
33617
33618        let result = disp.dispatch_dialog(true, 0x052, &mut cpu, &mut bus);
33619        assert!(result.unwrap().is_ok());
33620        assert_eq!(disp.cursor_level, -1);
33621        assert!(!disp.cursor_visible);
33622    }
33623
33624    // ---- ShowCursor ($A853) ----
33625
33626    #[test]
33627    fn show_cursor_balances_hidecursor_and_reveals_at_level_zero() {
33628        // IM:I I-168: ShowCursor increments toward 0 and only shows the
33629        // cursor when level becomes 0.
33630        let (mut disp, mut cpu, mut bus) = setup();
33631        disp.cursor_level = -2;
33632        disp.cursor_visible = false;
33633
33634        let result = disp.dispatch_dialog(true, 0x053, &mut cpu, &mut bus);
33635        assert!(result.unwrap().is_ok());
33636        assert_eq!(disp.cursor_level, -1);
33637        assert!(!disp.cursor_visible);
33638
33639        let result = disp.dispatch_dialog(true, 0x053, &mut cpu, &mut bus);
33640        assert!(result.unwrap().is_ok());
33641        assert_eq!(disp.cursor_level, 0);
33642        assert!(disp.cursor_visible);
33643    }
33644
33645    #[test]
33646    fn show_cursor_at_level_zero_is_noop() {
33647        // IM:I I-168: extra ShowCursor calls have no effect and do not
33648        // increment cursor level above 0.
33649        let (mut disp, mut cpu, mut bus) = setup();
33650        disp.cursor_level = 0;
33651        disp.cursor_visible = true;
33652
33653        let result = disp.dispatch_dialog(true, 0x053, &mut cpu, &mut bus);
33654        assert!(result.unwrap().is_ok());
33655        assert_eq!(disp.cursor_level, 0);
33656        assert!(disp.cursor_visible);
33657    }
33658
33659    // ---- ObscureCursor ($A856) ----
33660
33661    #[test]
33662    fn obscure_cursor_noop_preserves_cursor_level_visibility_and_stack() {
33663        // IM:I I-168: ObscureCursor has no effect on cursor level and takes
33664        // no arguments. Systemless's HLE compromise keeps it as a no-op because
33665        // synthesized mouse-move events would immediately un-obscure anyway.
33666        let (mut disp, mut cpu, mut bus) = setup();
33667        disp.cursor_level = -1;
33668        disp.cursor_visible = false;
33669        let sp_before = cpu.read_reg(Register::A7);
33670
33671        let result = disp.dispatch_dialog(true, 0x056, &mut cpu, &mut bus);
33672        assert!(result.unwrap().is_ok());
33673        assert_eq!(cpu.read_reg(Register::A7), sp_before);
33674        assert_eq!(disp.cursor_level, -1);
33675        assert!(!disp.cursor_visible);
33676    }
33677
33678    #[test]
33679    fn obscure_cursor_five_call_composition_preserves_stack_pointer_and_level() {
33680        // 5 successive ObscureCursor dispatches inside one
33681        // StackSpace sandwich leave SP unchanged. Per IM:I I-168 the
33682        // PROCEDURE has no arguments and no result slot, so each call
33683        // pops 0 bytes; the cumulative SP delta after N calls is zero.
33684        // Also pins the cursor-level no-effect contract per IM:I I-168:
33685        // ObscureCursor "has no effect on the cursor level and must
33686        // not be balanced by a call to ShowCursor."
33687        let (mut disp, mut cpu, mut bus) = setup();
33688        disp.cursor_level = -2;
33689        disp.cursor_visible = false;
33690        let sp_before = cpu.read_reg(Register::A7);
33691
33692        for i in 0..5 {
33693            let result = disp.dispatch_dialog(true, 0x056, &mut cpu, &mut bus);
33694            assert!(result.unwrap().is_ok(), "iteration {i} dispatch failed");
33695            assert_eq!(
33696                cpu.read_reg(Register::A7),
33697                sp_before,
33698                "iteration {i}: ObscureCursor must leave SP unchanged (0-byte pop)",
33699            );
33700            assert_eq!(
33701                disp.cursor_level, -2,
33702                "iteration {i}: ObscureCursor must NOT change cursor level per IM:I I-168",
33703            );
33704        }
33705    }
33706
33707    // ---- GetCursor ($A9B9) — IM:I I-474 contract ----
33708
33709    #[test]
33710    fn get_cursor_returns_handle_for_system_cursor() {
33711        // crossCursor (ID 2) is one of the four standard system
33712        // cursors per IM:I I-475..I-477. Systemless synthesises it via
33713        // [`TrapDispatcher::synthesize_system_cursor`] because the
33714        // System file's resource fork isn't loaded; the result must
33715        // be a non-NIL handle whose master ptr's bitmap matches the
33716        // synthesised crosshair.
33717        let (mut disp, mut cpu, mut bus) = setup();
33718        bus.write_word(TEST_SP, 2); // crossCursor
33719
33720        let _ = disp.dispatch_dialog(true, 0x1B9, &mut cpu, &mut bus);
33721        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 2);
33722
33723        let handle = bus.read_long(TEST_SP + 2);
33724        assert_ne!(
33725            handle, 0,
33726            "GetCursor(crossCursor) must return non-NIL handle for a built-in system cursor"
33727        );
33728        let crsr = bus.read_long(handle);
33729        let hot_v = bus.read_word(crsr + 64) as i16;
33730        let hot_h = bus.read_word(crsr + 66) as i16;
33731        assert_eq!(
33732            (hot_v, hot_h),
33733            (7, 7),
33734            "crossCursor's hotspot is documented at (7,7) in IM:I I-475"
33735        );
33736    }
33737
33738    #[test]
33739    fn get_cursor_returns_nil_for_unknown_id_per_im_i_474() {
33740        // IM:I I-474: "If the resource can't be read, GetCursor
33741        // returns NIL." For an ID that's neither a CURS resource we
33742        // have loaded nor one of the four standard built-ins, the
33743        // miss path is NIL — not a fresh empty 68-byte block. Apps
33744        // that defensively check `if handle = NIL then use_arrow
33745        // else SetCursor(handle^^)` would otherwise SetCursor a
33746        // blank cursor onto the screen.
33747        let (mut disp, mut cpu, mut bus) = setup();
33748        bus.write_word(TEST_SP, 999); // not a system cursor, no CURS 999 installed
33749
33750        let _ = disp.dispatch_dialog(true, 0x1B9, &mut cpu, &mut bus);
33751        assert_eq!(
33752            cpu.read_reg(Register::A7),
33753            TEST_SP + 2,
33754            "GetCursor must pop 2 bytes (cursorID INTEGER)"
33755        );
33756        assert_eq!(
33757            bus.read_long(TEST_SP + 2),
33758            0,
33759            "GetCursor must return NIL when CURS resource is missing per IM:I I-474"
33760        );
33761    }
33762
33763    #[test]
33764    fn get_cursor_returns_stable_handle_across_repeated_calls_for_system_cursor() {
33765        // Apps cache GetCursor results at boot and pass them to
33766        // SetCursor every frame; without handle stability the
33767        // dispatcher would leak a 68-byte block per call. Pin the
33768        // cache hit so a future "tighten alloc" change doesn't
33769        // accidentally drop the cache.
33770        let (mut disp, mut cpu, mut bus) = setup();
33771        bus.write_word(TEST_SP, 4); // watchCursor
33772
33773        let _ = disp.dispatch_dialog(true, 0x1B9, &mut cpu, &mut bus);
33774        let h1 = bus.read_long(TEST_SP + 2);
33775        cpu.write_reg(Register::A7, TEST_SP);
33776        bus.write_word(TEST_SP, 4);
33777
33778        let _ = disp.dispatch_dialog(true, 0x1B9, &mut cpu, &mut bus);
33779        let h2 = bus.read_long(TEST_SP + 2);
33780        assert_eq!(
33781            h1, h2,
33782            "GetCursor must return the same handle for repeated calls on a system cursor ID"
33783        );
33784    }
33785
33786    #[test]
33787    fn get_cursor_returns_handle_to_loaded_curs_resource() {
33788        // With a CURS resource installed, GetCursor must return a
33789        // handle whose master ptr equals the loaded resource bytes
33790        // — same value-asserting gate as get_icon_returns_handle_to_loaded_icon_resource.
33791        let (mut disp, mut cpu, mut bus) = setup();
33792        // CURS records are 68 bytes (32 data + 32 mask + 4 hotSpot).
33793        let curs_data: Vec<u8> = (0..68).map(|i| (i as u8).wrapping_mul(11)).collect();
33794        let res_ptr = disp.install_test_resource(&mut bus, *b"CURS", 200, &curs_data);
33795        bus.write_word(TEST_SP, 200);
33796
33797        let _ = disp.dispatch_dialog(true, 0x1B9, &mut cpu, &mut bus);
33798        let handle = bus.read_long(TEST_SP + 2);
33799        assert_ne!(
33800            handle, 0,
33801            "GetCursor must return non-NIL handle when CURS resource is loaded"
33802        );
33803        assert_eq!(
33804            bus.read_long(handle),
33805            res_ptr,
33806            "GetCursor's handle must dereference to the loaded CURS resource bytes"
33807        );
33808    }
33809
33810    // ---- GetPattern ($A9B8) — IM:I I-473 contract ----
33811
33812    #[test]
33813    fn get_pattern_returns_nil_for_missing_resource_per_im_i_473() {
33814        // IM:I I-473: "If the resource can't be read, GetPattern
33815        // returns NIL." The previous Stub returned a handle to a
33816        // fresh all-0xFF (white) 8-byte block, strictly worse than
33817        // NIL because callers branching on `handle = NIL` took the
33818        // FillRect-with-white path instead of the recovery path.
33819        let (mut disp, mut cpu, mut bus) = setup();
33820        bus.write_word(TEST_SP, 1); // patID = 1, no PAT 1 installed
33821
33822        let _ = disp.dispatch_dialog(true, 0x1B8, &mut cpu, &mut bus);
33823        assert_eq!(
33824            cpu.read_reg(Register::A7),
33825            TEST_SP + 2,
33826            "GetPattern must pop 2 bytes (patID INTEGER)"
33827        );
33828        assert_eq!(
33829            bus.read_long(TEST_SP + 2),
33830            0,
33831            "GetPattern must return NIL when PAT resource is missing per IM:I I-473"
33832        );
33833    }
33834
33835    #[test]
33836    fn get_pattern_returns_handle_to_loaded_pat_resource() {
33837        // With a PAT resource installed, GetPattern must return a
33838        // handle whose master ptr equals the loaded resource bytes.
33839        // Pin master-ptr-equality so a future regression that
33840        // allocates fresh memory instead of reusing the loaded ptr
33841        // fails here. Same gate shape as
33842        // get_icon_returns_handle_to_loaded_icon_resource.
33843        let (mut disp, mut cpu, mut bus) = setup();
33844        let pat_data: [u8; 8] = [0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA];
33845        let res_ptr = disp.install_test_resource(&mut bus, *b"PAT ", 16, &pat_data);
33846        bus.write_word(TEST_SP, 16);
33847
33848        let _ = disp.dispatch_dialog(true, 0x1B8, &mut cpu, &mut bus);
33849        let handle = bus.read_long(TEST_SP + 2);
33850        assert_ne!(
33851            handle, 0,
33852            "GetPattern must return non-NIL handle when PAT resource is loaded"
33853        );
33854        assert_eq!(
33855            bus.read_long(handle),
33856            res_ptr,
33857            "GetPattern's handle must dereference to the loaded PAT resource bytes \
33858             (not a fresh all-0xFF allocation)"
33859        );
33860        // Verify the bytes through the handle deref are the
33861        // installed sentinel pattern (50/AA stripes), not 0xFF.
33862        let master = bus.read_long(handle);
33863        for (i, want) in pat_data.iter().enumerate() {
33864            assert_eq!(
33865                bus.read_byte(master + i as u32),
33866                *want,
33867                "GetPattern PAT byte +{i} must match installed resource byte"
33868            );
33869        }
33870    }
33871
33872    #[test]
33873    fn get_pattern_returns_stable_handle_across_repeated_calls() {
33874        // Apps that paint with a pattern in tight loops cache the
33875        // GetPattern result; pin handle stability so a future
33876        // regression that drops `get_or_create_resource_handle` for
33877        // a fresh `bus.alloc(4)` per call surfaces here.
33878        let (mut disp, mut cpu, mut bus) = setup();
33879        let pat_data: [u8; 8] = [0x0F, 0xF0, 0x0F, 0xF0, 0x0F, 0xF0, 0x0F, 0xF0];
33880        let _ = disp.install_test_resource(&mut bus, *b"PAT ", 100, &pat_data);
33881
33882        bus.write_word(TEST_SP, 100);
33883        let _ = disp.dispatch_dialog(true, 0x1B8, &mut cpu, &mut bus);
33884        let h1 = bus.read_long(TEST_SP + 2);
33885        cpu.write_reg(Register::A7, TEST_SP);
33886        bus.write_word(TEST_SP, 100);
33887
33888        let _ = disp.dispatch_dialog(true, 0x1B8, &mut cpu, &mut bus);
33889        let h2 = bus.read_long(TEST_SP + 2);
33890        assert_eq!(
33891            h1, h2,
33892            "GetPattern must return the same handle for repeated calls on a loaded PAT resource"
33893        );
33894    }
33895
33896    // ---- GetIcon ($A9BB) — IM:I I-473 contract ----
33897
33898    #[test]
33899    fn get_icon_returns_nil_when_resource_missing() {
33900        // IM:I I-473: "If the resource can't be read, GetIcon
33901        // returns NIL." Critical for apps that defensively check
33902        // `if handle = NIL` before dereferencing — the prior Stub
33903        // always returned a non-NIL handle to uninitialised
33904        // memory which broke that branch.
33905        let (mut disp, mut cpu, mut bus) = setup();
33906        bus.write_word(TEST_SP, 1); // iconID = 1, no ICON 1 installed
33907        let _ = disp.dispatch_dialog(true, 0x1BB, &mut cpu, &mut bus);
33908        assert_eq!(
33909            cpu.read_reg(Register::A7),
33910            TEST_SP + 2,
33911            "GetIcon must pop 2 bytes (iconID INTEGER)"
33912        );
33913        assert_eq!(
33914            bus.read_long(TEST_SP + 2),
33915            0,
33916            "GetIcon must return NIL when ICON resource is missing per IM:I I-473"
33917        );
33918    }
33919
33920    #[test]
33921    fn get_icon_returns_handle_to_loaded_icon_resource() {
33922        // With an ICON resource installed, GetIcon must return a
33923        // non-NIL handle whose master ptr points at the loaded
33924        // resource bytes. Pin the master-ptr-equality so a future
33925        // regression that allocates fresh memory instead of
33926        // reusing the loaded ptr fails here.
33927        let (mut disp, mut cpu, mut bus) = setup();
33928        // Real ICON resources are 128 bytes (32x32 1bpp); fill
33929        // with a recognisable sentinel pattern so handle deref
33930        // assertions can verify "this is the right resource."
33931        let icon_data: Vec<u8> = (0..128).map(|i| (i as u8).wrapping_mul(7)).collect();
33932        let res_ptr = disp.install_test_resource(&mut bus, *b"ICON", 128, &icon_data);
33933        bus.write_word(TEST_SP, 128);
33934
33935        let _ = disp.dispatch_dialog(true, 0x1BB, &mut cpu, &mut bus);
33936        let handle = bus.read_long(TEST_SP + 2);
33937        assert_ne!(
33938            handle, 0,
33939            "GetIcon must return a non-NIL handle when ICON resource is loaded"
33940        );
33941        assert_eq!(
33942            bus.read_long(handle),
33943            res_ptr,
33944            "GetIcon's handle must dereference to the loaded ICON resource bytes \
33945             (not a fresh uninitialised allocation)"
33946        );
33947        // Verify the bytes through the handle deref are the
33948        // installed sentinel pattern.
33949        let master = bus.read_long(handle);
33950        for (i, want) in icon_data.iter().enumerate() {
33951            assert_eq!(
33952                bus.read_byte(master + i as u32),
33953                *want,
33954                "GetIcon ICON byte +{i} must match installed resource byte"
33955            );
33956        }
33957    }
33958
33959    #[test]
33960    fn get_icon_returns_stable_handle_across_repeated_calls() {
33961        // Per IM:Resource Manager, GetResource returns the SAME
33962        // handle on repeat calls (unless ReleaseResource has run).
33963        // GetIcon delegates to GetResource per IM:I I-473, so it
33964        // must inherit this stability — apps that cache the handle
33965        // depend on it not changing between alert cycles.
33966        let (mut disp, mut cpu, mut bus) = setup();
33967        let icon_data: Vec<u8> = vec![0xCC; 128];
33968        disp.install_test_resource(&mut bus, *b"ICON", 200, &icon_data);
33969
33970        bus.write_word(TEST_SP, 200);
33971        let _ = disp.dispatch_dialog(true, 0x1BB, &mut cpu, &mut bus);
33972        let h1 = bus.read_long(TEST_SP + 2);
33973
33974        // Reset SP and call again with the same iconID.
33975        cpu.write_reg(Register::A7, TEST_SP);
33976        bus.write_word(TEST_SP, 200);
33977        let _ = disp.dispatch_dialog(true, 0x1BB, &mut cpu, &mut bus);
33978        let h2 = bus.read_long(TEST_SP + 2);
33979
33980        assert_ne!(h1, 0, "first GetIcon must return non-NIL");
33981        assert_eq!(
33982            h1, h2,
33983            "GetIcon must return the SAME handle on repeated calls per IM:Resource Mgr stability"
33984        );
33985    }
33986
33987    #[test]
33988    fn get_icon_pops_two_bytes_per_pascal_signature() {
33989        // FUNCTION GetIcon(iconID: INTEGER): Handle;
33990        // Pascal stack frame: result Handle (4) pre-pushed by
33991        // caller, iconID (2) on top. Trap pops 2 bytes (iconID),
33992        // result lands at new SP+0 (= old SP+2).
33993        let (mut disp, mut cpu, mut bus) = setup();
33994        bus.write_word(TEST_SP, 999); // any iconID
33995        let pre_a7 = cpu.read_reg(Register::A7);
33996        let _ = disp.dispatch_dialog(true, 0x1BB, &mut cpu, &mut bus);
33997        assert_eq!(
33998            cpu.read_reg(Register::A7),
33999            pre_a7 + 2,
34000            "GetIcon must advance A7 by 2 bytes (iconID INTEGER)"
34001        );
34002    }
34003
34004    // ---- GetPicture ($A9BC) ----
34005
34006    #[test]
34007    fn get_picture_returns_nil_without_resource() {
34008        let (mut disp, mut cpu, mut bus) = setup();
34009
34010        // SP+0: picture id (2 bytes)
34011        bus.write_word(TEST_SP, 1);
34012
34013        let result = disp.dispatch_dialog(true, 0x1BC, &mut cpu, &mut bus);
34014        assert!(result.unwrap().is_ok());
34015        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 2);
34016
34017        let handle = bus.read_long(TEST_SP + 2);
34018        assert_eq!(handle, 0); // NIL when no PICT resource loaded
34019    }
34020
34021    #[test]
34022    fn get_picture_reloads_released_resource_from_open_resource_file() {
34023        fn make_resource_fork_bytes(resources: &[([u8; 4], i16, &[u8])]) -> Vec<u8> {
34024            let mut type_groups: Vec<([u8; 4], Vec<(i16, &[u8], u32)>)> = Vec::new();
34025            for (res_type, res_id, data) in resources {
34026                let group_idx = type_groups
34027                    .iter()
34028                    .position(|(existing_type, _)| existing_type == res_type)
34029                    .unwrap_or_else(|| {
34030                        type_groups.push((*res_type, Vec::new()));
34031                        type_groups.len() - 1
34032                    });
34033                type_groups[group_idx].1.push((*res_id, *data, 0));
34034            }
34035            type_groups.sort_by_key(|(res_type, _)| *res_type);
34036            for (_, entries) in &mut type_groups {
34037                entries.sort_by_key(|(res_id, _, _)| *res_id);
34038            }
34039
34040            let data_offset = 16u32;
34041            let mut data_section = Vec::new();
34042            for (_, entries) in &mut type_groups {
34043                for (_, data, data_pos) in entries {
34044                    *data_pos = data_section.len() as u32;
34045                    data_section.extend_from_slice(&(data.len() as u32).to_be_bytes());
34046                    data_section.extend_from_slice(data);
34047                }
34048            }
34049
34050            let map_offset = data_offset + data_section.len() as u32;
34051            let type_list_offset = 30u16;
34052            let type_count = type_groups.len();
34053            let resource_count: usize = type_groups.iter().map(|(_, entries)| entries.len()).sum();
34054            let ref_lists_offset = 2 + type_count * 8;
34055            let name_list_offset =
34056                type_list_offset as usize + ref_lists_offset + resource_count * 12;
34057            let map_length = name_list_offset as u32;
34058
34059            let mut bytes = vec![0u8; (map_offset + map_length) as usize];
34060            let mut header = [0u8; 16];
34061            header[0..4].copy_from_slice(&data_offset.to_be_bytes());
34062            header[4..8].copy_from_slice(&map_offset.to_be_bytes());
34063            header[8..12].copy_from_slice(&(data_section.len() as u32).to_be_bytes());
34064            header[12..16].copy_from_slice(&map_length.to_be_bytes());
34065            bytes[0..16].copy_from_slice(&header);
34066            bytes[data_offset as usize..data_offset as usize + data_section.len()]
34067                .copy_from_slice(&data_section);
34068
34069            let map_start = map_offset as usize;
34070            bytes[map_start..map_start + 16].copy_from_slice(&header);
34071            bytes[map_start + 24..map_start + 26].copy_from_slice(&type_list_offset.to_be_bytes());
34072            bytes[map_start + 26..map_start + 28]
34073                .copy_from_slice(&(name_list_offset as u16).to_be_bytes());
34074            bytes[map_start + 28..map_start + 30]
34075                .copy_from_slice(&((type_count as u16) - 1).to_be_bytes());
34076
34077            let type_list_start = map_start + type_list_offset as usize;
34078            bytes[type_list_start..type_list_start + 2]
34079                .copy_from_slice(&((type_count as u16) - 1).to_be_bytes());
34080            let mut next_ref_list_offset = ref_lists_offset;
34081            for (i, (res_type, entries)) in type_groups.iter().enumerate() {
34082                let type_entry = type_list_start + 2 + i * 8;
34083                bytes[type_entry..type_entry + 4].copy_from_slice(res_type);
34084                bytes[type_entry + 4..type_entry + 6]
34085                    .copy_from_slice(&((entries.len() as u16) - 1).to_be_bytes());
34086                bytes[type_entry + 6..type_entry + 8]
34087                    .copy_from_slice(&(next_ref_list_offset as u16).to_be_bytes());
34088
34089                let ref_list_start = type_list_start + next_ref_list_offset;
34090                for (j, (res_id, _, data_pos)) in entries.iter().enumerate() {
34091                    let ref_entry = ref_list_start + j * 12;
34092                    bytes[ref_entry..ref_entry + 2]
34093                        .copy_from_slice(&(*res_id as u16).to_be_bytes());
34094                    bytes[ref_entry + 2..ref_entry + 4].copy_from_slice(&0xFFFFu16.to_be_bytes());
34095                    bytes[ref_entry + 4] = 0;
34096                    let data_offset_bytes = data_pos.to_be_bytes();
34097                    bytes[ref_entry + 5..ref_entry + 8].copy_from_slice(&data_offset_bytes[1..4]);
34098                }
34099
34100                next_ref_list_offset += entries.len() * 12;
34101            }
34102
34103            bytes
34104        }
34105
34106        let (mut disp, mut cpu, mut bus) = setup();
34107        let pict_data = b"\x00\x11Wordtris reloadable picture data";
34108        let fork = make_resource_fork_bytes(&[(*b"PICT", 1127, &pict_data[..])]);
34109        disp.vfs_rsrc.insert("Pictures".to_string(), fork);
34110        disp.open_resource_file_from_vfs_key(&mut bus, "Pictures", false);
34111
34112        bus.write_word(TEST_SP, 1127);
34113        let first = disp.dispatch_dialog(true, 0x1BC, &mut cpu, &mut bus);
34114        assert!(first.unwrap().is_ok());
34115        let first_handle = bus.read_long(TEST_SP + 2);
34116        assert_ne!(first_handle, 0, "first GetPicture must find PICT 1127");
34117        let first_ptr = bus.read_long(first_handle);
34118        assert_eq!(bus.read_bytes(first_ptr, pict_data.len()), pict_data);
34119
34120        cpu.write_reg(Register::A7, TEST_SP);
34121        bus.write_long(TEST_SP, first_handle);
34122        let release = disp.dispatch_resource(true, 0x1A3, &mut cpu, &mut bus);
34123        assert!(release.unwrap().is_ok());
34124        assert_eq!(
34125            bus.read_long(first_handle),
34126            0,
34127            "ReleaseResource should clear the old master pointer"
34128        );
34129
34130        cpu.write_reg(Register::A7, TEST_SP);
34131        bus.write_word(TEST_SP, 1127);
34132        let second = disp.dispatch_dialog(true, 0x1BC, &mut cpu, &mut bus);
34133        assert!(second.unwrap().is_ok());
34134        let second_handle = bus.read_long(TEST_SP + 2);
34135        assert_ne!(
34136            second_handle, 0,
34137            "GetPicture must reload a released PICT from the open resource file"
34138        );
34139        assert_ne!(
34140            second_handle, first_handle,
34141            "a released resource should be returned through a fresh handle"
34142        );
34143        let second_ptr = bus.read_long(second_handle);
34144        assert_eq!(bus.read_bytes(second_ptr, pict_data.len()), pict_data);
34145    }
34146
34147    // ---- GetString ($A9BA) ----
34148
34149    #[test]
34150    fn get_string_returns_loaded_str_resource_handle() {
34151        let (mut disp, mut cpu, mut bus) = setup();
34152        let str_ptr = bus.alloc(6);
34153        bus.write_byte(str_ptr, 5);
34154        bus.write_bytes(str_ptr + 1, b"Hello");
34155        disp.resources = Some(crate::trap::dispatch::LoadedResources {
34156            files: std::collections::HashMap::from([(
34157                0,
34158                crate::trap::dispatch::ResourceFileMap {
34159                    loaded: std::collections::HashMap::from([((*b"STR ", 1), str_ptr)]),
34160                    named: std::collections::HashMap::new(),
34161                    names_by_id: std::collections::HashMap::new(),
34162                    attrs: std::collections::HashMap::new(),
34163                    map_attrs: 0,
34164                },
34165            )]),
34166            names: std::collections::HashMap::new(),
34167            search_order: vec![0],
34168            current_file: 0,
34169        });
34170
34171        // SP+0: string id (2 bytes)
34172        bus.write_word(TEST_SP, 1);
34173
34174        let result = disp.dispatch_dialog(true, 0x1BA, &mut cpu, &mut bus);
34175        assert!(result.unwrap().is_ok());
34176        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 2);
34177
34178        let handle = bus.read_long(TEST_SP + 2);
34179        assert_ne!(handle, 0);
34180        assert_eq!(bus.read_long(handle), str_ptr);
34181    }
34182
34183    #[test]
34184    fn get_string_returns_nil_when_resource_is_missing() {
34185        let (mut disp, mut cpu, mut bus) = setup();
34186        bus.write_word(TEST_SP, 999);
34187
34188        let result = disp.dispatch_dialog(true, 0x1BA, &mut cpu, &mut bus);
34189        assert!(result.unwrap().is_ok());
34190        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 2);
34191        assert_eq!(bus.read_long(TEST_SP + 2), 0);
34192    }
34193
34194    // ---- Unhandled trap returns None ----
34195
34196    #[test]
34197    fn unhandled_trap_returns_none() {
34198        let (mut disp, mut cpu, mut bus) = setup();
34199
34200        let result = disp.dispatch_dialog(true, 0xFFFF, &mut cpu, &mut bus);
34201        assert!(result.is_none());
34202    }
34203}