//! Dialog Manager, Cursor Manager, and misc stub trap handlers.
use super::dispatch::{
DialogItem, DialogPopupDraw, DialogPopupTrackingState, DialogTrackingState,
DialogUserItemTrackingState, PendingDialogPopupMenu, PersistentDialogSnapshot, QueuedEvent,
RetainedModalDialogClickState,
};
use super::types::{decode_mac_roman_for_render, Rect, ShapeOp};
use crate::cpu::{CpuOps, Register};
use crate::display::CursorImage;
use crate::memory::{MacMemoryBus, MemoryBus};
use crate::quickdraw::fonts::get_font_face_scaled;
use crate::quickdraw::text::get_font_metrics;
use crate::ui_theme::{ControlKind, UiThemeId};
use crate::Result;
use std::collections::VecDeque;
use std::sync::OnceLock;
static TRACE_DIALOG_PROCS: OnceLock<bool> = OnceLock::new();
static TRACE_DIALOG_FILTER: OnceLock<bool> = OnceLock::new();
static TRACE_TEXTEDIT: OnceLock<bool> = OnceLock::new();
static TRACE_DIALOG_ITEMS: OnceLock<bool> = OnceLock::new();
static TRACE_DIALOG_TEXT_INLINE: OnceLock<bool> = OnceLock::new();
const MANAGER_DIALOG_RECORD_ALIGNMENT: u32 = 256;
struct DialogCIconLayout {
width: i16,
height: i16,
pm_row_bytes: u32,
mask_row_bytes: u32,
bmap_row_bytes: u32,
pixel_size: u16,
mask_data_ptr: u32,
bmap_data_ptr: u32,
pixel_data_ptr: u32,
}
#[derive(Clone, Copy)]
struct FullscreenOffscreenDialogRestoreCandidate {
base: u32,
row_bytes: u32,
top: i16,
left: i16,
bottom: i16,
right: i16,
ctab_handle: u32,
score: u32,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct TeResolvedStyle {
font: i16,
face: i16,
size: i16,
color: (u16, u16, u16),
line_height: i16,
ascent: i16,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct TeStyleRun {
start: usize,
style_index: usize,
style: TeResolvedStyle,
}
fn trace_dialog_procs_enabled() -> bool {
*TRACE_DIALOG_PROCS.get_or_init(|| std::env::var_os("SYSTEMLESS_TRACE_DIALOG_PROCS").is_some())
}
fn trace_dialog_filter_enabled() -> bool {
*TRACE_DIALOG_FILTER
.get_or_init(|| std::env::var_os("SYSTEMLESS_TRACE_DIALOG_FILTER").is_some())
}
fn trace_textedit_enabled() -> bool {
*TRACE_TEXTEDIT.get_or_init(|| std::env::var_os("SYSTEMLESS_TRACE_TEXTEDIT").is_some())
}
fn trace_dialog_items_enabled() -> bool {
*TRACE_DIALOG_ITEMS.get_or_init(|| std::env::var_os("SYSTEMLESS_TRACE_DIALOG_ITEMS").is_some())
}
fn trace_dialog_text_inline_enabled() -> bool {
*TRACE_DIALOG_TEXT_INLINE
.get_or_init(|| std::env::var_os("SYSTEMLESS_TRACE_DIALOG_TEXT").is_some())
}
fn input_trace_event_name(what: u16) -> &'static str {
match what {
0 => "nullEvent",
1 => "mouseDown",
2 => "mouseUp",
3 => "keyDown",
4 => "keyUp",
5 => "autoKey",
6 => "updateEvt",
7 => "diskEvt",
8 => "activateEvt",
_ => "unknownEvent",
}
}
fn input_trace_nonzero(value: u32) -> String {
if value == 0 {
"$00000000".to_string()
} else {
"$NONZERO".to_string()
}
}
fn input_trace_event_message(what: u16, message: u32) -> String {
match what {
6 | 8 => input_trace_nonzero(message),
_ => format!("${message:08X}"),
}
}
fn input_trace_text_bytes(value: &str) -> String {
if value.is_empty() {
return "empty".to_string();
}
let mut out = String::from("hex:");
for byte in value.as_bytes() {
out.push_str(&format!("{byte:02X}"));
}
out
}
impl super::TrapDispatcher {
fn record_dialog_input_trace(
&mut self,
trap: &str,
event_ptr: u32,
what: u16,
message: u32,
where_v: i16,
where_h: i16,
modifiers: u16,
active_dialog: Option<u32>,
result: bool,
detail: &str,
) {
if !self.input_trace_enabled {
return;
}
let active_dialog = active_dialog
.map(input_trace_nonzero)
.unwrap_or_else(|| "none".to_string());
let mut line = format!(
"{trap} action=guest_event:{} event_ptr={} delivered=EventRecord{{what={}({}), message={}, where=({where_v},{where_h}), modifiers=${modifiers:04X}}} {} active_dialog={} result={}",
input_trace_event_name(what),
input_trace_nonzero(event_ptr),
input_trace_event_name(what),
what,
input_trace_event_message(what, message),
self.input_trace_state_fields(),
active_dialog,
if result { "true" } else { "false" },
);
if !detail.is_empty() {
line.push(' ');
line.push_str(detail);
}
self.record_input_trace_line(line);
}
fn record_modal_dialog_input_trace(
&mut self,
action: &str,
dialog_ptr: u32,
bounds: (i16, i16, i16, i16),
item_hit: i16,
item_type: Option<u8>,
highlighted: Option<bool>,
result: &str,
outcome: &str,
) {
if !self.input_trace_enabled {
return;
}
let item_type = item_type
.map(|value| format!("${value:02X}"))
.unwrap_or_else(|| "none".to_string());
let highlighted = highlighted
.map(|value| if value { "true" } else { "false" }.to_string())
.unwrap_or_else(|| "none".to_string());
self.record_input_trace_line(format!(
"A991 action={} live_mouse=({},{}) {} dialog={} bounds=({},{},{},{}) item_hit={} item_type={} highlighted={} result={} outcome={}",
action,
self.mouse_pos.0,
self.mouse_pos.1,
self.input_trace_state_fields(),
input_trace_nonzero(dialog_ptr),
bounds.0,
bounds.1,
bounds.2,
bounds.3,
item_hit,
item_type,
highlighted,
result,
outcome,
));
}
fn record_modal_dialog_text_input_trace(
&mut self,
action: &str,
dialog_ptr: u32,
bounds: (i16, i16, i16, i16),
edit_item: i16,
item_type: Option<u8>,
key_code: u8,
char_code: u8,
text_before: &str,
text_after: &str,
result: &str,
outcome: &str,
) {
if !self.input_trace_enabled {
return;
}
let item_type = item_type
.map(|value| format!("${value:02X}"))
.unwrap_or_else(|| "none".to_string());
self.record_input_trace_line(format!(
"A991 action={} live_mouse=({},{}) {} dialog={} bounds=({},{},{},{}) edit_item={} item_type={} key_code=${:02X} char_code=${:02X} text_before={} text_after={} result={} outcome={}",
action,
self.mouse_pos.0,
self.mouse_pos.1,
self.input_trace_state_fields(),
input_trace_nonzero(dialog_ptr),
bounds.0,
bounds.1,
bounds.2,
bounds.3,
edit_item,
item_type,
key_code,
char_code,
input_trace_text_bytes(text_before),
input_trace_text_bytes(text_after),
result,
outcome,
));
}
fn record_modal_dialog_filter_input_trace(
&mut self,
action: &str,
dialog_ptr: u32,
bounds: (i16, i16, i16, i16),
filter_proc: u32,
event: Option<&QueuedEvent>,
item_hit: i16,
item_type: Option<u8>,
handled_mouse_down: bool,
dialog_retained: bool,
result: &str,
outcome: &str,
) {
if !self.input_trace_enabled {
return;
}
let item_type = item_type
.map(|value| format!("${value:02X}"))
.unwrap_or_else(|| "none".to_string());
let (event_name, message, where_text, modifiers) = if let Some(event) = event {
(
format!("{}({})", input_trace_event_name(event.what), event.what),
input_trace_event_message(event.what, event.message),
format!("({},{})", event.where_v, event.where_h),
format!("${:04X}", event.modifiers),
)
} else {
(
"none".to_string(),
"none".to_string(),
"none".to_string(),
"none".to_string(),
)
};
self.record_input_trace_line(format!(
"A991 action={} live_mouse=({},{}) {} dialog={} bounds=({},{},{},{}) filter_proc={} event={} message={} where={} modifiers={} item_hit={} item_type={} handled_mouse_down={} dialog_retained={} result={} outcome={}",
action,
self.mouse_pos.0,
self.mouse_pos.1,
self.input_trace_state_fields(),
input_trace_nonzero(dialog_ptr),
bounds.0,
bounds.1,
bounds.2,
bounds.3,
input_trace_nonzero(filter_proc),
event_name,
message,
where_text,
modifiers,
item_hit,
item_type,
handled_mouse_down,
dialog_retained,
result,
outcome,
));
}
fn dialog_template_res_err(&self, dialog_id: i16) -> i16 {
if self.find_resource_any(*b"DLOG", dialog_id).is_some() {
0
} else {
0
}
}
fn loaded_resource_handle_in_file(
&self,
res_type: [u8; 4],
res_id: i16,
ptr: u32,
refnum: u16,
) -> Option<u32> {
self.loaded_handles
.iter()
.find_map(|(&handle, &(loaded_ptr, loaded_type, loaded_id))| {
if loaded_ptr == ptr
&& loaded_type == res_type
&& loaded_id == res_id
&& self.resource_handle_files.get(&handle).copied() == Some(refnum)
{
Some(handle)
} else {
None
}
})
}
fn set_handle_purgeable(&mut self, handle: u32, purgeable: bool) {
if handle == 0 {
return;
}
if purgeable {
*self.handle_state_bits.entry(handle).or_insert(0) |= 0x40;
} else if let Some(bits) = self.handle_state_bits.get_mut(&handle) {
*bits &= !0x40;
if *bits == 0 {
self.handle_state_bits.remove(&handle);
}
}
}
fn prepare_dialog_resource_handle(
&mut self,
bus: &mut MacMemoryBus,
res_type: [u8; 4],
res_id: i16,
purgeable: bool,
load_if_missing: bool,
) -> Option<(u32, u32)> {
let (refnum, ptr) = self.find_resource_any(res_type, res_id)?;
let handle = if load_if_missing {
let handle =
self.get_or_create_resource_handle_in_file(bus, res_type, res_id, ptr, refnum);
if ptr != 0 && bus.read_long(handle) == 0 {
bus.write_long(handle, ptr);
self.ptr_to_handle.insert(ptr, handle);
}
handle
} else {
self.loaded_resource_handle_in_file(res_type, res_id, ptr, refnum)?
};
self.set_handle_purgeable(handle, purgeable);
Some((handle, ptr))
}
fn dialog_item_resource_type(item_type: u8) -> Option<[u8; 4]> {
match item_type & 0x7F {
7 => Some(*b"CNTL"),
32 => Some(*b"ICON"),
64 => Some(*b"PICT"),
_ => None,
}
}
fn cascade_dialog_resource_purgeability(
&mut self,
bus: &mut MacMemoryBus,
template_type: [u8; 4],
template_id: i16,
purgeable: bool,
load_if_missing: bool,
) {
let Some((_, template_ptr)) = self.prepare_dialog_resource_handle(
bus,
template_type,
template_id,
purgeable,
load_if_missing,
) else {
return;
};
let items_id = match template_type {
// IM:I I-437: DLOG item-list ID is at offset 18.
[b'D', b'L', b'O', b'G'] if bus.get_alloc_size(template_ptr).unwrap_or(0) >= 20 => {
bus.read_word(template_ptr + 18) as i16
}
// IM:I I-425..I-426: ALRT item-list ID is at offset 8.
[b'A', b'L', b'R', b'T'] if bus.get_alloc_size(template_ptr).unwrap_or(0) >= 10 => {
bus.read_word(template_ptr + 8) as i16
}
_ => return,
};
let Some((_, ditl_ptr)) = self.prepare_dialog_resource_handle(
bus,
*b"DITL",
items_id,
purgeable,
load_if_missing,
) else {
return;
};
let ditl_len = bus.get_alloc_size(ditl_ptr).unwrap_or(0);
let items = Self::parse_ditl(bus, ditl_ptr, ditl_len);
for item in items {
if let Some(res_type) = Self::dialog_item_resource_type(item.item_type) {
self.prepare_dialog_resource_handle(
bus,
res_type,
item.resource_id,
purgeable,
load_if_missing,
);
}
}
}
const TE_DEST_RECT_OFFSET: u32 = 0x00;
const TE_VIEW_RECT_OFFSET: u32 = 0x08;
const TE_SEL_RECT_OFFSET: u32 = 0x10;
const TE_LINE_HEIGHT_OFFSET: u32 = 0x18;
const TE_FONT_ASCENT_OFFSET: u32 = 0x1A;
const TE_SEL_POINT_OFFSET: u32 = 0x1C;
const TE_SEL_START_OFFSET: u32 = 0x20;
const TE_SEL_END_OFFSET: u32 = 0x22;
const TE_ACTIVE_OFFSET: u32 = 0x24;
const TE_CARET_TIME_OFFSET: u32 = 0x34;
const TE_CARET_STATE_OFFSET: u32 = 0x38;
#[allow(dead_code)]
const TE_CR_ONLY_OFFSET: u32 = 0x48;
const TE_JUST_OFFSET: u32 = 0x3A;
const TE_LENGTH_OFFSET: u32 = 0x3C;
const TE_HTEXT_OFFSET: u32 = 0x3E;
const TE_TX_FONT_OFFSET: u32 = 0x4A;
const TE_TX_FACE_OFFSET: u32 = 0x4C;
const TE_TX_MODE_OFFSET: u32 = 0x4E;
const TE_TX_SIZE_OFFSET: u32 = 0x50;
const TE_IN_PORT_OFFSET: u32 = 0x52;
const TE_N_LINES_OFFSET: u32 = 0x5E;
const TE_LINE_STARTS_OFFSET: u32 = 0x60;
const TE_REC_MIN_SIZE: u32 = 128;
const TE_CARET_BLINK_TICKS: u32 = 32;
// TextEdit draws inside the destination rectangle (IM:I I-373 to I-374);
// BasiliskII/System 7.5.3 `dialog_visual_textedit_smoke` pins the ROM's
// flush-left glyph origin one pixel in from destRect.left.
const TE_LINE_LEFT_INSET: i16 = 1;
const DBOX_FRAME_MARGIN: i16 = 8;
const EDIT_TEXT_FRAME_OUTSET: i16 = 3;
const STANDARD_CONTROL_MARK_SIZE: i16 = 12;
const STANDARD_CONTROL_MARK_LEFT_INSET: i16 = 2;
const STANDARD_CONTROL_TITLE_GAP: i16 = 4;
const INACTIVE_STANDARD_CONTROL_TITLE_RGB: [u16; 3] = [0xA1A1, 0xA1A1, 0xA1A1];
const CLASSIC_RADIO_OUTLINE_12: [&'static str; 12] = [
"....####....",
"..##....##..",
".#........#.",
".#........#.",
"#..........#",
"#..........#",
"#..........#",
"#..........#",
".#........#.",
".#........#.",
"..##....##..",
"....####....",
];
const CLASSIC_RADIO_DOT_12: [&'static str; 12] = [
"............",
"............",
"............",
"....####....",
"...######...",
"...######...",
"...######...",
"...######...",
"....####....",
"............",
"............",
"............",
];
const TE_STYLE_N_RUNS_OFFSET: u32 = 0x00;
const TE_STYLE_N_STYLES_OFFSET: u32 = 0x02;
const TE_STYLE_STYLE_TABLE_OFFSET: u32 = 0x04;
const TE_STYLE_LH_TABLE_OFFSET: u32 = 0x08;
const TE_STYLE_NULL_STYLE_OFFSET: u32 = 0x10;
const TE_STYLE_RUNS_OFFSET: u32 = 0x14;
const ST_ELEMENT_REFCOUNT_OFFSET: u32 = 0x00;
const ST_ELEMENT_HEIGHT_OFFSET: u32 = 0x02;
const ST_ELEMENT_ASCENT_OFFSET: u32 = 0x04;
const ST_ELEMENT_FONT_OFFSET: u32 = 0x06;
const ST_ELEMENT_FACE_OFFSET: u32 = 0x08;
const ST_ELEMENT_SIZE_OFFSET: u32 = 0x0A;
const ST_ELEMENT_COLOR_OFFSET: u32 = 0x0C;
const ST_ELEMENT_SIZE: u32 = 0x12;
const LH_ELEMENT_HEIGHT_OFFSET: u32 = 0x00;
const LH_ELEMENT_ASCENT_OFFSET: u32 = 0x02;
const LH_ELEMENT_SIZE: u32 = 0x04;
const NULL_STYLE_SCRAP_OFFSET: u32 = 0x04;
const NULL_STYLE_REC_SIZE: u32 = 0x08;
// Inside Macintosh Volume V, V-274: StScrpRec is a count word
// followed immediately by ScrpSTElement entries (`scrpStyleTab EQU 2`).
const SCRAP_N_STYLES_OFFSET: u32 = 0x00;
const SCRAP_STYLE_TAB_OFFSET: u32 = 0x02;
const SCRAP_STYLE_START_CHAR_OFFSET: u32 = 0x00;
const SCRAP_STYLE_HEIGHT_OFFSET: u32 = 0x04;
const SCRAP_STYLE_ASCENT_OFFSET: u32 = 0x06;
const SCRAP_STYLE_FONT_OFFSET: u32 = 0x08;
const SCRAP_STYLE_FACE_OFFSET: u32 = 0x0A;
const SCRAP_STYLE_SIZE_OFFSET: u32 = 0x0C;
const SCRAP_STYLE_COLOR_OFFSET: u32 = 0x0E;
const SCRAP_STYLE_ELEMENT_SIZE: u32 = 0x14;
const STYLE_SCRAP_REC_SIZE: u32 = Self::SCRAP_STYLE_TAB_OFFSET + Self::SCRAP_STYLE_ELEMENT_SIZE;
const TE_FEATURE_AUTO_SCROLL: u16 = 0;
const TE_FEATURE_TEXT_BUFFERING: u16 = 1;
const TE_FEATURE_OUTLINE_HILITE: u16 = 2;
const TE_FEATURE_INLINE_INPUT: u16 = 3;
const TE_FEATURE_USE_TEXT_SERVICES: u16 = 4;
const TE_BIT_CLEAR: i16 = 0;
const TE_BIT_SET: i16 = 1;
const TE_BIT_TEST: i16 = -1;
fn dialog_window_proc_id(&self, bus: &MacMemoryBus, dialog_ptr: u32) -> i16 {
self.window_proc_ids
.get(&dialog_ptr)
.copied()
// Older tests seed pre-side-table dialogs by writing the WDEF
// procID at +108. Real DialogRecords use +108 as windowKind.
.unwrap_or_else(|| bus.read_word(dialog_ptr + 108) as i16)
}
fn dialog_window_title(bus: &MacMemoryBus, dialog_ptr: u32) -> String {
if dialog_ptr == 0 {
return String::new();
}
// WindowRecord.titleHandle is a StringHandle at +134; GetWTitle uses
// the same documented field (IM:I I-284).
let title_handle = bus.read_long(dialog_ptr + 134);
if title_handle == 0 {
return String::new();
}
let title_ptr = bus.read_long(title_handle);
if title_ptr == 0 {
return String::new();
}
decode_mac_roman_for_render(&bus.read_pstring(title_ptr))
}
fn ensure_text_handle_size(bus: &mut MacMemoryBus, item_handle: u32, size: usize) -> u32 {
if item_handle == 0 {
return 0;
}
let current_ptr = bus.read_long(item_handle);
if size == 0 {
if current_ptr != 0 {
bus.free(current_ptr);
bus.write_long(item_handle, 0);
}
return 0;
}
let required = size as u32;
let current_size = if current_ptr != 0 {
bus.get_alloc_size(current_ptr).unwrap_or(0)
} else {
0
};
if current_ptr != 0 && current_size == required {
return current_ptr;
}
let new_ptr = bus.alloc(required);
if new_ptr == 0 {
return current_ptr;
}
if current_ptr != 0 {
bus.free(current_ptr);
}
bus.write_long(item_handle, new_ptr);
new_ptr
}
fn text_item_string_from_handle(bus: &MacMemoryBus, item_handle: u32) -> String {
Self::text_item_string_from_handle_if_present(bus, item_handle).unwrap_or_default()
}
fn text_item_string_from_handle_if_present(
bus: &MacMemoryBus,
item_handle: u32,
) -> Option<String> {
Self::text_item_bytes_from_handle_if_present(bus, item_handle)
.map(|bytes| decode_mac_roman_for_render(&bytes))
}
fn text_item_bytes_from_handle_if_present(
bus: &MacMemoryBus,
item_handle: u32,
) -> Option<Vec<u8>> {
if item_handle == 0 {
return None;
}
let data_ptr = bus.read_long(item_handle);
if data_ptr == 0 {
return Some(Vec::new());
}
let len = bus.get_alloc_size(data_ptr).unwrap_or(0) as usize;
Some(bus.read_bytes(data_ptr, len))
}
fn dialog_item_handle_addr(bus: &MacMemoryBus, dialog_ptr: u32, item_no: i16) -> Option<u32> {
if item_no <= 0 {
return None;
}
let items_handle = bus.read_long(dialog_ptr + 156);
if items_handle == 0 {
return None;
}
let ditl_ptr = bus.read_long(items_handle);
if ditl_ptr == 0 {
return None;
}
let max_index = bus.read_word(ditl_ptr) as i16;
if item_no > max_index + 1 {
return None;
}
let ditl_len = bus.get_alloc_size(ditl_ptr).unwrap_or(u32::MAX);
let mut offset = 2u32;
for current_item in 1..=max_index + 1 {
let item_handle_addr = ditl_ptr + offset;
offset += 4; // itmhand
offset += 8; // itmr
let item_type = bus.read_byte(ditl_ptr + offset);
let data_len_byte = bus.read_byte(ditl_ptr + offset + 1);
offset += 2; // itmtype + itmlen
let base_type = item_type & 0x7F;
let remaining = ditl_len.saturating_sub(offset);
let payload_len = Self::ditl_item_payload_len(base_type, data_len_byte, remaining)?;
let padded = (payload_len + 1) & !1;
if current_item == item_no {
return Some(item_handle_addr);
}
offset += padded;
}
None
}
fn dialog_item_handle(bus: &MacMemoryBus, dialog_ptr: u32, item_no: i16) -> u32 {
Self::dialog_item_handle_addr(bus, dialog_ptr, item_no)
.map(|addr| bus.read_long(addr))
.unwrap_or(0)
}
fn set_dialog_item_handle(bus: &mut MacMemoryBus, dialog_ptr: u32, item_no: i16, handle: u32) {
if let Some(addr) = Self::dialog_item_handle_addr(bus, dialog_ptr, item_no) {
bus.write_long(addr, handle);
}
}
pub(crate) fn front_dialog_for_user_item_proc(&self, proc_ptr: u32) -> Option<u32> {
if proc_ptr == 0 {
return None;
}
let front = self.front_window;
if front == 0 {
return None;
}
let Some(items) = self.dialog_items.get(&front) else {
return None;
};
if items
.iter()
.any(|item| (item.item_type & 0x7F) == 0 && item.proc_ptr == proc_ptr)
{
Some(front)
} else {
None
}
}
pub(crate) fn front_dialog_has_user_item_proc(&self, proc_ptr: u32) -> bool {
self.front_dialog_for_user_item_proc(proc_ptr).is_some()
}
pub(crate) fn redraw_dialog_window_contents(
&mut self,
bus: &mut MacMemoryBus,
dialog_ptr: u32,
) {
if let Some(mut items) = self.dialog_items.get(&dialog_ptr).cloned() {
Self::refresh_ditl_proc_ptrs(bus, dialog_ptr, &mut items);
let bounds = Self::dialog_screen_bounds(bus, dialog_ptr);
let proc_id = self.dialog_window_proc_id(bus, dialog_ptr);
let (edit_text, edit_item, default_item) =
Self::dialog_edit_state(bus, dialog_ptr, &items);
self.draw_dialog(
bus,
bounds,
proc_id,
"",
&items,
default_item,
&edit_text,
edit_item,
false,
dialog_ptr,
);
if Self::dialog_is_game_managed(bounds, &items) {
self.dialog_visible_snapshots.remove(&dialog_ptr);
} else {
let pixels = self.save_dialog_pixels(bus, bounds);
self.dialog_visible_snapshots
.insert(dialog_ptr, PersistentDialogSnapshot { bounds, pixels });
}
self.dialog_items.insert(dialog_ptr, items);
}
}
pub(crate) fn queue_modeless_dialog_draw_procs(
&mut self,
bus: &mut MacMemoryBus,
dialog_ptr: u32,
) {
self.queue_modeless_dialog_draw_procs_intersecting(bus, dialog_ptr, None);
}
fn update_dialog_window_contents<C: CpuOps>(
&mut self,
bus: &mut MacMemoryBus,
cpu: &mut C,
dialog_ptr: u32,
update_rect: Option<(i16, i16, i16, i16)>,
) -> bool {
let Some(mut items) = self.dialog_items.get(&dialog_ptr).cloned() else {
return false;
};
Self::refresh_ditl_proc_ptrs(bus, dialog_ptr, &mut items);
let bounds = Self::dialog_screen_bounds(bus, dialog_ptr);
let update_screen_rect =
update_rect.and_then(|rect| Self::dialog_update_screen_rect(bounds, rect));
let proc_id = self.dialog_window_proc_id(bus, dialog_ptr);
let (edit_text, edit_item, default_item) = Self::dialog_edit_state(bus, dialog_ptr, &items);
// MTE 1992 p. 6-141: DialogSelect handles update events by calling
// DrawDialog. MTE 1992 p. 6-143: UpdateDialog uses SetPort before
// redrawing the update region. Keep both dialog update paths on the
// dialog port before any standard-item drawing or userItem callbacks.
self.set_current_port_state(bus, cpu, dialog_ptr, None);
let Some(update_screen_rect) = update_screen_rect else {
return false;
};
let before_pixels = self.save_dialog_pixels(bus, bounds);
self.dialog_initial_draw_deferred.remove(&dialog_ptr);
self.draw_dialog(
bus,
bounds,
proc_id,
"",
&items,
default_item,
&edit_text,
edit_item,
false,
dialog_ptr,
);
// MTE 1992 p. 6-143: UpdateDialog redraws only the supplied update
// region. Most Dialog Manager HLE drawing helpers write directly to the
// framebuffer, so clip the full redraw by restoring pre-update pixels
// outside the update region after standard items have been refreshed.
self.restore_dialog_pixels_outside_rect(bus, bounds, &before_pixels, update_screen_rect);
self.dialog_items.insert(dialog_ptr, items);
// Application-defined userItems are guest-owned. IM:I I-405 says
// their item handle is a draw procedure; queue only the callbacks
// whose display rectangles intersect the active update region.
self.queue_modeless_dialog_draw_procs_intersecting(bus, dialog_ptr, update_rect);
true
}
fn dialog_update_screen_rect(
bounds: (i16, i16, i16, i16),
rect: (i16, i16, i16, i16),
) -> Option<(i16, i16, i16, i16)> {
let screen_rect = if Self::rects_intersect(rect, bounds) {
rect
} else {
(
bounds.0.saturating_add(rect.0),
bounds.1.saturating_add(rect.1),
bounds.0.saturating_add(rect.2),
bounds.1.saturating_add(rect.3),
)
};
Self::rect_intersection(
screen_rect,
(
bounds.0 - Self::DBOX_FRAME_MARGIN,
bounds.1 - Self::DBOX_FRAME_MARGIN,
bounds.2 + Self::DBOX_FRAME_MARGIN,
bounds.3 + Self::DBOX_FRAME_MARGIN,
),
)
}
fn queue_modeless_dialog_draw_procs_intersecting(
&mut self,
bus: &mut MacMemoryBus,
dialog_ptr: u32,
update_rect: Option<(i16, i16, i16, i16)>,
) {
let Some(mut items) = self.dialog_items.get(&dialog_ptr).cloned() else {
return;
};
Self::refresh_ditl_proc_ptrs(bus, dialog_ptr, &mut items);
let bounds = Self::dialog_screen_bounds(bus, dialog_ptr);
let effective_update_rect = update_rect.map(|rect| {
if Self::rects_intersect(rect, bounds) {
rect
} else {
(
bounds.0.saturating_add(rect.0),
bounds.1.saturating_add(rect.1),
bounds.0.saturating_add(rect.2),
bounds.1.saturating_add(rect.3),
)
}
});
for (i, item) in items.iter().enumerate() {
let item_rect = Self::dialog_item_screen_rect(bounds, item.rect);
if (item.item_type & 0x7F) == 0
&& item.proc_ptr != 0
&& Self::rects_intersect(item_rect, bounds)
&& effective_update_rect
.map(|rect| Self::rects_intersect(item_rect, rect))
.unwrap_or(true)
{
self.modeless_dialog_draw_proc_queue.push_back((
dialog_ptr,
item.proc_ptr,
(i + 1) as i16,
));
}
}
self.dialog_items.insert(dialog_ptr, items);
}
fn allocate_te_handle(bus: &mut MacMemoryBus) -> u32 {
// TENew returns a handle to a zeroed TERec owned by the caller.
// Text 1993, 2-85 to 2-86
let te_ptr = bus.alloc(Self::TE_REC_MIN_SIZE);
if te_ptr == 0 {
return 0;
}
let handle = bus.alloc(4);
if handle == 0 {
return 0;
}
bus.write_long(handle, te_ptr);
handle
}
fn allocate_handle_with_data(bus: &mut MacMemoryBus, size: u32) -> u32 {
let handle = bus.alloc(4);
if handle == 0 {
return 0;
}
let data_ptr = if size == 0 { 0 } else { bus.alloc(size) };
bus.write_long(handle, data_ptr);
handle
}
fn ensure_handle_capacity(bus: &mut MacMemoryBus, handle: u32, min_size: u32) -> u32 {
if handle == 0 {
return 0;
}
let current_ptr = bus.read_long(handle);
let current_size = if current_ptr != 0 {
bus.get_alloc_size(current_ptr).unwrap_or(0)
} else {
0
};
if current_size >= min_size {
return current_ptr;
}
let new_ptr = bus.alloc(min_size);
if new_ptr == 0 {
return current_ptr;
}
if current_ptr != 0 && current_size != 0 {
let existing = bus.read_bytes(current_ptr, current_size as usize);
bus.write_bytes(new_ptr, &existing);
bus.free(current_ptr);
}
bus.write_long(handle, new_ptr);
new_ptr
}
fn te_record_size_for_line_count(line_count: usize) -> u32 {
let line_entries = line_count.saturating_add(4) as u32;
Self::TE_REC_MIN_SIZE.max(Self::TE_LINE_STARTS_OFFSET + line_entries * 2)
}
fn ensure_te_record_line_capacity(
bus: &mut MacMemoryBus,
te_handle: u32,
line_count: usize,
) -> u32 {
Self::ensure_handle_capacity(
bus,
te_handle,
Self::te_record_size_for_line_count(line_count),
)
}
fn te_read_rect(bus: &MacMemoryBus, addr: u32) -> (i16, i16, i16, i16) {
(
bus.read_word(addr) as i16,
bus.read_word(addr + 2) as i16,
bus.read_word(addr + 4) as i16,
bus.read_word(addr + 6) as i16,
)
}
fn te_write_rect_words(bus: &mut MacMemoryBus, addr: u32, rect: (i16, i16, i16, i16)) {
bus.write_word(addr, rect.0 as u16);
bus.write_word(addr + 2, rect.1 as u16);
bus.write_word(addr + 4, rect.2 as u16);
bus.write_word(addr + 6, rect.3 as u16);
}
fn te_rect_is_empty(rect: (i16, i16, i16, i16)) -> bool {
rect.0 == 0 && rect.1 == 0 && rect.2 == 0 && rect.3 == 0
}
fn te_rect_ptr_candidate(bus: &MacMemoryBus, ptr: u32) -> Option<(i16, i16, i16, i16)> {
if ptr == 0 || (ptr & 1) != 0 || ptr < 0x1000 {
return None;
}
let end = ptr.checked_add(8)?;
if end > bus.ram_size() {
return None;
}
Some(Self::te_read_rect(bus, ptr))
}
/// Decode TENew/TEStyleNew arguments off the Pascal stack, sniffing
/// either the modern Universal Headers pointer convention (8 bytes,
/// two `const Rect *` ptrs) or the legacy Inside Macintosh by-value
/// convention (16 bytes, two Rects pushed by value).
///
/// Pascal calling convention pushes left-to-right, so the first
/// arg (destRect / destRect_ptr) lands DEEPER on the stack than the
/// second arg (viewRect / viewRect_ptr). Concretely:
/// * Pointer convention (8 bytes, MPW Universal Headers):
/// sp+0..3 viewRect_ptr (last pushed, shallowest)
/// sp+4..7 destRect_ptr (first pushed, deepest)
/// * By-value convention (16 bytes, classic IM:I I-373):
/// sp+0..7 viewRect bytes
/// sp+8..15 destRect bytes
///
/// Returns (destRect, viewRect, stack_pop).
#[allow(clippy::type_complexity)] // (destRect, viewRect, stack_pop) — local return shape, no aliasing benefit
fn te_new_rect_args(
bus: &MacMemoryBus,
sp: u32,
) -> ((i16, i16, i16, i16), (i16, i16, i16, i16), u32) {
// Pascal first arg (destRect / destRect_ptr) is DEEPEST → sp+4.
// Pascal second arg (viewRect / viewRect_ptr) is SHALLOWEST → sp+0.
let view_ptr_word = bus.read_long(sp);
let dest_ptr_word = bus.read_long(sp + 4);
let view_via_ptr = Self::te_rect_ptr_candidate(bus, view_ptr_word);
let dest_via_ptr = Self::te_rect_ptr_candidate(bus, dest_ptr_word);
if let (Some(dest), Some(view)) = (dest_via_ptr, view_via_ptr) {
// Modern MPW Universal Headers pointer convention (8 bytes).
if Self::te_rect_is_empty(view) && !Self::te_rect_is_empty(dest) {
(dest, dest, 8)
} else if Self::te_rect_is_empty(dest) && !Self::te_rect_is_empty(view) {
(view, view, 8)
} else if Self::te_rect_is_empty(dest) && Self::te_rect_is_empty(view) {
(
Self::te_read_rect(bus, sp + 8),
Self::te_read_rect(bus, sp),
16,
)
} else {
(dest, view, 8)
}
} else {
// Legacy by-value convention (16 bytes).
(
Self::te_read_rect(bus, sp + 8),
Self::te_read_rect(bus, sp),
16,
)
}
}
fn te_record_ptr(bus: &MacMemoryBus, te_handle: u32) -> u32 {
if te_handle == 0 {
0
} else {
bus.read_long(te_handle)
}
}
fn te_is_styled_record(bus: &MacMemoryBus, te_ptr: u32) -> bool {
te_ptr != 0 && bus.read_word(te_ptr + Self::TE_TX_SIZE_OFFSET) == 0xFFFF
}
fn te_text_handle(bus: &MacMemoryBus, te_handle: u32) -> u32 {
let te_ptr = Self::te_record_ptr(bus, te_handle);
if te_ptr == 0 {
0
} else {
bus.read_long(te_ptr + Self::TE_HTEXT_OFFSET)
}
}
fn te_style_handle(bus: &MacMemoryBus, te_handle: u32) -> u32 {
let te_ptr = Self::te_record_ptr(bus, te_handle);
if Self::te_is_styled_record(bus, te_ptr) {
bus.read_long(te_ptr + Self::TE_TX_FONT_OFFSET)
} else {
0
}
}
fn te_write_style_handle(bus: &mut MacMemoryBus, te_handle: u32, style_handle: u32) {
let te_ptr = Self::te_record_ptr(bus, te_handle);
if Self::te_is_styled_record(bus, te_ptr) {
bus.write_long(te_ptr + Self::TE_TX_FONT_OFFSET, style_handle);
}
}
fn te_line_starts(bus: &MacMemoryBus, te_handle: u32) -> Vec<usize> {
let te_ptr = Self::te_record_ptr(bus, te_handle);
if te_ptr == 0 {
return vec![0];
}
let n_lines = bus.read_word(te_ptr + Self::TE_N_LINES_OFFSET) as usize;
let mut starts = Vec::with_capacity(n_lines.saturating_add(1));
for index in 0..=n_lines {
starts.push(
bus.read_word(te_ptr + Self::TE_LINE_STARTS_OFFSET + (index as u32 * 2)) as usize,
);
}
if starts.is_empty() {
starts.push(0);
}
starts
}
fn te_char_to_line_index(bus: &MacMemoryBus, te_handle: u32, offset: usize) -> usize {
let starts = Self::te_line_starts(bus, te_handle);
let n_lines = starts.len().saturating_sub(1);
if n_lines <= 1 {
return 0;
}
let mut low = 0usize;
let mut high = n_lines;
let mut current = (high + low) / 2;
while low < high && starts[current] != offset {
if starts[current] < offset {
low = current + 1;
} else {
high = current.saturating_sub(1);
}
current = (high + low) / 2;
}
if starts[current] > offset || current == n_lines {
current.saturating_sub(1)
} else {
current
}
}
fn font_lookup_size(size: i16) -> i16 {
// QuickDraw grafPorts initialize txSize to 0, which the Font Manager
// interprets as the system font size. Preserve that sentinel for font
// lookup; coercing it to 1 selects the wrong strike/fallback.
// Inside Macintosh Volume I, I-163 and I-178.
if size == 0 {
0
} else {
size.max(1)
}
}
fn te_char_to_point(&self, bus: &MacMemoryBus, te_handle: u32, offset: usize) -> (i16, i16) {
let te_ptr = Self::te_record_ptr(bus, te_handle);
if te_ptr == 0 {
return (0, 0);
}
let dest_rect = Self::te_read_rect(bus, te_ptr + Self::TE_DEST_RECT_OFFSET);
let just = bus.read_word(te_ptr + Self::TE_JUST_OFFSET) as i16;
let text_bytes = Self::te_text_bytes(bus, te_handle);
let text_len = text_bytes.len();
let clamped = offset.min(text_len);
let starts = Self::te_line_starts(bus, te_handle);
let line_index = Self::te_char_to_line_index(bus, te_handle, clamped);
let line_start = starts.get(line_index).copied().unwrap_or(0);
let line_end = starts.get(line_index + 1).copied().unwrap_or(text_len);
let on_break = clamped != 0
&& clamped == text_len
&& text_bytes.get(clamped - 1).copied() == Some(b'\r');
let styled_runs = if Self::te_is_styled_record(bus, te_ptr) {
self.te_style_runs(bus, te_handle, text_len)
} else {
Vec::new()
};
let uses_styled_runs = !styled_runs.is_empty() && Self::te_is_styled_record(bus, te_ptr);
let (font, _, size, _, _, _) = self.te_primary_style(bus, te_handle);
let rect_width = dest_rect.3.saturating_sub(dest_rect.1);
// Per Inside Macintosh: Text 1993, lines 7320-7323:
// teJustLeft = 0 (flush left — system default for LTR)
// teJustCenter = 1 (centered)
// teJustRight = -1 (flush right)
// teForceLeft = -2 (force flush left)
let left_offset = match just {
1 | -1 => {
let line_width = if on_break {
rect_width.saturating_sub(1)
} else {
let line_width = if uses_styled_runs {
self.te_measure_text_width_styled(
&styled_runs,
&text_bytes,
line_start,
line_end,
)
} else {
self.te_measure_text_width(
font,
Self::font_lookup_size(size),
&text_bytes,
line_start,
line_end,
)
};
rect_width.saturating_sub(line_width).saturating_sub(1)
};
if just == 1 {
line_width / 2
} else {
line_width
}
}
_ => Self::TE_LINE_LEFT_INSET,
};
let x = if on_break {
dest_rect.1.saturating_add(left_offset)
} else {
dest_rect
.1
.saturating_add(left_offset)
.saturating_add(if uses_styled_runs {
self.te_measure_text_width_styled(
&styled_runs,
&text_bytes,
line_start,
clamped,
)
} else {
self.te_measure_text_width(
font,
Self::font_lookup_size(size),
&text_bytes,
line_start,
clamped,
)
})
};
let mut top = dest_rect.0;
for current_line in 0..line_index {
top = top.saturating_add(Self::te_height_for_line(bus, te_handle, current_line));
}
if on_break {
top = top.saturating_add(Self::te_height_for_line(bus, te_handle, line_index));
}
(top, x)
}
/// Inverse of `te_char_to_point`: locate the character offset whose
/// glyph cell contains `point` (vertical, horizontal). Used by
/// TEGetOffset ($A83C) per Inside Macintosh Volume V, V-172.
fn te_point_to_char(&self, bus: &MacMemoryBus, te_handle: u32, point: (i16, i16)) -> i16 {
let te_ptr = Self::te_record_ptr(bus, te_handle);
if te_ptr == 0 {
return 0;
}
let text_bytes = Self::te_text_bytes(bus, te_handle);
let text_len = text_bytes.len();
let starts = Self::te_line_starts(bus, te_handle);
let n_lines = starts.len().saturating_sub(1);
if text_len == 0 || n_lines == 0 {
return 0;
}
let dest_rect = Self::te_read_rect(bus, te_ptr + Self::TE_DEST_RECT_OFFSET);
let (point_v, point_h) = point;
if point_v < dest_rect.0 {
return 0;
}
let mut top = dest_rect.0;
let mut line_index = n_lines.saturating_sub(1);
let mut found_line = false;
for current_line in 0..n_lines {
let height = Self::te_height_for_line(bus, te_handle, current_line);
let bottom = top.saturating_add(height);
if point_v < bottom {
line_index = current_line;
found_line = true;
break;
}
top = bottom;
}
if !found_line {
return text_len.min(i16::MAX as usize) as i16;
}
let line_start = starts[line_index];
let line_end = starts.get(line_index + 1).copied().unwrap_or(text_len);
let just = bus.read_word(te_ptr + Self::TE_JUST_OFFSET) as i16;
let styled_runs = if Self::te_is_styled_record(bus, te_ptr) {
self.te_style_runs(bus, te_handle, text_len)
} else {
Vec::new()
};
let uses_styled_runs = !styled_runs.is_empty() && Self::te_is_styled_record(bus, te_ptr);
let (font, _, size, _, _, _) = self.te_primary_style(bus, te_handle);
let rect_width = dest_rect.3.saturating_sub(dest_rect.1);
let left_offset = match just {
1 | -1 => {
let measured_width = if uses_styled_runs {
self.te_measure_text_width_styled(
&styled_runs,
&text_bytes,
line_start,
line_end,
)
} else {
self.te_measure_text_width(
font,
Self::font_lookup_size(size),
&text_bytes,
line_start,
line_end,
)
};
let line_width = rect_width.saturating_sub(measured_width).saturating_sub(1);
if just == 1 {
line_width / 2
} else {
line_width
}
}
_ => Self::TE_LINE_LEFT_INSET,
};
let mut x_cursor = dest_rect.1.saturating_add(left_offset);
for (offset, byte) in text_bytes[line_start..line_end].iter().enumerate() {
let index = line_start + offset;
if matches!(*byte, b'\r' | b'\n') {
return index.min(i16::MAX as usize) as i16;
}
let advance = if uses_styled_runs {
let style = Self::te_style_at_offset(&styled_runs, index);
self.te_char_width(style.font, style.size, *byte)
} else {
self.te_char_width(font, Self::font_lookup_size(size), *byte)
};
// Hit-test against the glyph midpoint so a click on the right
// half of a character lands on the next offset, matching the
// semantics described in Inside Macintosh Volume V, V-172.
let mid = x_cursor.saturating_add(advance / 2);
if point_h < mid {
return index.min(i16::MAX as usize) as i16;
}
x_cursor = x_cursor.saturating_add(advance);
}
line_end.min(i16::MAX as usize) as i16
}
fn te_scroll_contents(
&mut self,
cpu: &mut impl CpuOps,
bus: &mut MacMemoryBus,
te_handle: u32,
dh: i16,
dv: i16,
) {
if dh == 0 && dv == 0 {
return;
}
let te_ptr = Self::te_record_ptr(bus, te_handle);
if te_ptr == 0 {
return;
}
let mut dest_rect = Self::te_read_rect(bus, te_ptr + Self::TE_DEST_RECT_OFFSET);
dest_rect.0 = dest_rect.0.saturating_add(dv);
dest_rect.1 = dest_rect.1.saturating_add(dh);
dest_rect.2 = dest_rect.2.saturating_add(dv);
dest_rect.3 = dest_rect.3.saturating_add(dh);
Self::te_write_rect_words(bus, te_ptr + Self::TE_DEST_RECT_OFFSET, dest_rect);
self.draw_te_contents(cpu, bus, te_handle);
// Refresh rendered_pixels so redraw_chrome restores the scrolled state rather
// than the pre-scroll snapshot captured at dialog creation.
// Text 1993, 2-89 (TEScroll/TEPinScroll modify destRect and redraw).
if let Some(ref tracking) = self.dialog_tracking {
if !tracking.game_managed && tracking.rendered_pixels_final {
let bounds = tracking.bounds;
let new_pixels = self.save_dialog_pixels(bus, bounds);
self.dialog_tracking.as_mut().unwrap().rendered_pixels = new_pixels;
}
}
}
fn te_getdelta(sel_start: i16, sel_stop: i16, view_start: i16, view_stop: i16) -> i16 {
if sel_start < view_start {
view_start.saturating_sub(sel_start)
} else if sel_stop > view_stop {
if sel_stop.saturating_sub(sel_start) > view_stop.saturating_sub(view_start) {
view_start.saturating_sub(sel_start)
} else {
view_stop.saturating_sub(sel_stop)
}
} else {
0
}
}
fn te_height_for_line(bus: &MacMemoryBus, te_handle: u32, line_index: usize) -> i16 {
let te_ptr = Self::te_record_ptr(bus, te_handle);
if te_ptr == 0 {
return 0;
}
if Self::te_is_styled_record(bus, te_ptr) {
let style_handle = Self::te_style_handle(bus, te_handle);
if style_handle != 0 {
let style_ptr = bus.read_long(style_handle);
if style_ptr != 0 {
let lh_handle = bus.read_long(style_ptr + Self::TE_STYLE_LH_TABLE_OFFSET);
let lh_ptr = if lh_handle != 0 {
bus.read_long(lh_handle)
} else {
0
};
if lh_ptr != 0 {
return bus.read_word(
lh_ptr
+ (line_index as u32 * Self::LH_ELEMENT_SIZE)
+ Self::LH_ELEMENT_HEIGHT_OFFSET,
) as i16;
}
}
}
}
bus.read_word(te_ptr + Self::TE_LINE_HEIGHT_OFFSET) as i16
}
fn te_ascent_for_line(bus: &MacMemoryBus, te_handle: u32, line_index: usize) -> i16 {
let te_ptr = Self::te_record_ptr(bus, te_handle);
if te_ptr == 0 {
return 0;
}
if Self::te_is_styled_record(bus, te_ptr) {
let style_handle = Self::te_style_handle(bus, te_handle);
if style_handle != 0 {
let style_ptr = bus.read_long(style_handle);
if style_ptr != 0 {
let lh_handle = bus.read_long(style_ptr + Self::TE_STYLE_LH_TABLE_OFFSET);
let lh_ptr = if lh_handle != 0 {
bus.read_long(lh_handle)
} else {
0
};
if lh_ptr != 0 {
return bus.read_word(
lh_ptr
+ (line_index as u32 * Self::LH_ELEMENT_SIZE)
+ Self::LH_ELEMENT_ASCENT_OFFSET,
) as i16;
}
}
}
}
bus.read_word(te_ptr + Self::TE_FONT_ASCENT_OFFSET) as i16
}
fn te_primary_style(
&self,
bus: &MacMemoryBus,
te_handle: u32,
) -> (i16, i16, i16, (u16, u16, u16), i16, i16) {
let te_ptr = Self::te_record_ptr(bus, te_handle);
if te_ptr == 0 {
let size = Self::font_lookup_size(self.tx_size);
let metrics = get_font_metrics(self.tx_font, size);
return (
self.tx_font,
self.tx_face,
size,
self.fg_color,
metrics.ascent + metrics.descent + metrics.leading,
metrics.ascent,
);
}
if Self::te_is_styled_record(bus, te_ptr) {
let style_handle = bus.read_long(te_ptr + Self::TE_TX_FONT_OFFSET);
let style_ptr = if style_handle != 0 {
bus.read_long(style_handle)
} else {
0
};
if style_ptr != 0 {
let style_table_handle =
bus.read_long(style_ptr + Self::TE_STYLE_STYLE_TABLE_OFFSET);
let style_table_ptr = if style_table_handle != 0 {
bus.read_long(style_table_handle)
} else {
0
};
if style_table_ptr != 0 {
return (
bus.read_word(style_table_ptr + Self::ST_ELEMENT_FONT_OFFSET) as i16,
bus.read_word(style_table_ptr + Self::ST_ELEMENT_FACE_OFFSET) as i16,
bus.read_word(style_table_ptr + Self::ST_ELEMENT_SIZE_OFFSET) as i16,
(
bus.read_word(style_table_ptr + Self::ST_ELEMENT_COLOR_OFFSET),
bus.read_word(style_table_ptr + Self::ST_ELEMENT_COLOR_OFFSET + 2),
bus.read_word(style_table_ptr + Self::ST_ELEMENT_COLOR_OFFSET + 4),
),
bus.read_word(style_table_ptr + Self::ST_ELEMENT_HEIGHT_OFFSET) as i16,
bus.read_word(style_table_ptr + Self::ST_ELEMENT_ASCENT_OFFSET) as i16,
);
}
}
}
let tx_font = bus.read_word(te_ptr + Self::TE_TX_FONT_OFFSET) as i16;
let tx_face = bus.read_word(te_ptr + Self::TE_TX_FACE_OFFSET) as i16;
let tx_size =
Self::font_lookup_size(bus.read_word(te_ptr + Self::TE_TX_SIZE_OFFSET) as i16);
let metrics = get_font_metrics(tx_font, tx_size);
(
tx_font,
tx_face,
tx_size,
self.fg_color,
metrics.ascent + metrics.descent + metrics.leading,
metrics.ascent,
)
}
fn te_resolved_style_from_parts(
font: i16,
face: i16,
size: i16,
color: (u16, u16, u16),
line_height: i16,
ascent: i16,
) -> TeResolvedStyle {
let resolved_size = Self::font_lookup_size(size);
let metrics = get_font_metrics(font, resolved_size);
let fallback_height = metrics.ascent + metrics.descent + metrics.leading;
TeResolvedStyle {
font,
face,
size: resolved_size,
color,
line_height: if line_height > 0 {
line_height
} else {
fallback_height
},
ascent: if ascent > 0 { ascent } else { metrics.ascent },
}
}
fn te_primary_resolved_style(&self, bus: &MacMemoryBus, te_handle: u32) -> TeResolvedStyle {
let (font, face, size, color, line_height, ascent) = self.te_primary_style(bus, te_handle);
Self::te_resolved_style_from_parts(font, face, size, color, line_height, ascent)
}
fn te_style_from_table_element(bus: &MacMemoryBus, style_ptr: u32) -> TeResolvedStyle {
Self::te_resolved_style_from_parts(
bus.read_word(style_ptr + Self::ST_ELEMENT_FONT_OFFSET) as i16,
bus.read_word(style_ptr + Self::ST_ELEMENT_FACE_OFFSET) as i16,
bus.read_word(style_ptr + Self::ST_ELEMENT_SIZE_OFFSET) as i16,
(
bus.read_word(style_ptr + Self::ST_ELEMENT_COLOR_OFFSET),
bus.read_word(style_ptr + Self::ST_ELEMENT_COLOR_OFFSET + 2),
bus.read_word(style_ptr + Self::ST_ELEMENT_COLOR_OFFSET + 4),
),
bus.read_word(style_ptr + Self::ST_ELEMENT_HEIGHT_OFFSET) as i16,
bus.read_word(style_ptr + Self::ST_ELEMENT_ASCENT_OFFSET) as i16,
)
}
fn te_style_from_scrap_element(bus: &MacMemoryBus, scrap_style_ptr: u32) -> TeResolvedStyle {
Self::te_resolved_style_from_parts(
bus.read_word(scrap_style_ptr + Self::SCRAP_STYLE_FONT_OFFSET) as i16,
bus.read_word(scrap_style_ptr + Self::SCRAP_STYLE_FACE_OFFSET) as i16,
bus.read_word(scrap_style_ptr + Self::SCRAP_STYLE_SIZE_OFFSET) as i16,
(
bus.read_word(scrap_style_ptr + Self::SCRAP_STYLE_COLOR_OFFSET),
bus.read_word(scrap_style_ptr + Self::SCRAP_STYLE_COLOR_OFFSET + 2),
bus.read_word(scrap_style_ptr + Self::SCRAP_STYLE_COLOR_OFFSET + 4),
),
bus.read_word(scrap_style_ptr + Self::SCRAP_STYLE_HEIGHT_OFFSET) as i16,
bus.read_word(scrap_style_ptr + Self::SCRAP_STYLE_ASCENT_OFFSET) as i16,
)
}
fn te_write_style_table_element(
bus: &mut MacMemoryBus,
style_ptr: u32,
style: TeResolvedStyle,
) {
bus.write_word(style_ptr + Self::ST_ELEMENT_REFCOUNT_OFFSET, 1);
bus.write_word(
style_ptr + Self::ST_ELEMENT_HEIGHT_OFFSET,
style.line_height as u16,
);
bus.write_word(
style_ptr + Self::ST_ELEMENT_ASCENT_OFFSET,
style.ascent as u16,
);
bus.write_word(style_ptr + Self::ST_ELEMENT_FONT_OFFSET, style.font as u16);
bus.write_word(style_ptr + Self::ST_ELEMENT_FACE_OFFSET, style.face as u16);
bus.write_word(style_ptr + Self::ST_ELEMENT_SIZE_OFFSET, style.size as u16);
bus.write_word(style_ptr + Self::ST_ELEMENT_COLOR_OFFSET, style.color.0);
bus.write_word(style_ptr + Self::ST_ELEMENT_COLOR_OFFSET + 2, style.color.1);
bus.write_word(style_ptr + Self::ST_ELEMENT_COLOR_OFFSET + 4, style.color.2);
}
fn te_style_runs(
&self,
bus: &MacMemoryBus,
te_handle: u32,
text_len: usize,
) -> Vec<TeStyleRun> {
let fallback = self.te_primary_resolved_style(bus, te_handle);
let te_ptr = Self::te_record_ptr(bus, te_handle);
if !Self::te_is_styled_record(bus, te_ptr) {
return vec![TeStyleRun {
start: 0,
style_index: 0,
style: fallback,
}];
}
let style_handle = Self::te_style_handle(bus, te_handle);
let style_ptr = if style_handle != 0 {
bus.read_long(style_handle)
} else {
0
};
if style_ptr == 0 {
return vec![TeStyleRun {
start: 0,
style_index: 0,
style: fallback,
}];
}
let style_table_handle = bus.read_long(style_ptr + Self::TE_STYLE_STYLE_TABLE_OFFSET);
let style_table_ptr = if style_table_handle != 0 {
bus.read_long(style_table_handle)
} else {
0
};
if style_table_ptr == 0 {
return vec![TeStyleRun {
start: 0,
style_index: 0,
style: fallback,
}];
}
let n_runs = bus.read_word(style_ptr + Self::TE_STYLE_N_RUNS_OFFSET) as usize;
let n_styles = bus.read_word(style_ptr + Self::TE_STYLE_N_STYLES_OFFSET) as usize;
let style_record_size = bus.get_alloc_size(style_ptr).unwrap_or(0);
let style_table_size = bus.get_alloc_size(style_table_ptr).unwrap_or(0);
let max_runs = style_record_size
.saturating_sub(Self::TE_STYLE_RUNS_OFFSET)
.checked_div(4)
.unwrap_or(0) as usize;
let max_styles = style_table_size
.checked_div(Self::ST_ELEMENT_SIZE)
.unwrap_or(0) as usize;
let run_count = n_runs.min(max_runs);
let style_count = n_styles.min(max_styles);
if run_count == 0 || style_count == 0 {
return vec![TeStyleRun {
start: 0,
style_index: 0,
style: fallback,
}];
}
let mut runs = Vec::with_capacity(run_count);
for run_index in 0..run_count {
let run_ptr = style_ptr + Self::TE_STYLE_RUNS_OFFSET + (run_index as u32 * 4);
let style_index_word = bus.read_word(run_ptr + 2);
if style_index_word == 0xFFFF {
break;
}
let style_index = style_index_word as usize;
if style_index >= style_count {
continue;
}
let style_element_ptr = style_table_ptr + (style_index as u32 * Self::ST_ELEMENT_SIZE);
runs.push(TeStyleRun {
start: (bus.read_word(run_ptr) as usize).min(text_len),
style_index,
style: Self::te_style_from_table_element(bus, style_element_ptr),
});
}
if runs.is_empty() {
return vec![TeStyleRun {
start: 0,
style_index: 0,
style: fallback,
}];
}
runs.sort_by_key(|run| run.start);
let mut folded: Vec<TeStyleRun> = Vec::with_capacity(runs.len() + 1);
for run in runs {
if let Some(last) = folded.last_mut() {
if last.start == run.start {
*last = run;
continue;
}
if last.style == run.style {
continue;
}
}
folded.push(run);
}
if folded.first().is_none_or(|run| run.start != 0) {
folded.insert(
0,
TeStyleRun {
start: 0,
style_index: 0,
style: fallback,
},
);
}
folded
}
fn te_style_at_offset(runs: &[TeStyleRun], offset: usize) -> TeResolvedStyle {
let mut style = runs
.first()
.map(|run| run.style)
.unwrap_or_else(|| Self::te_resolved_style_from_parts(0, 0, 0, (0, 0, 0), 0, 0));
for run in runs {
if run.start > offset {
break;
}
style = run.style;
}
style
}
fn te_measure_text_width_styled(
&self,
runs: &[TeStyleRun],
text_bytes: &[u8],
start: usize,
end: usize,
) -> i16 {
let start = start.min(text_bytes.len());
let end = end.min(text_bytes.len());
let mut width = 0i16;
for (offset, &byte) in text_bytes[start..end].iter().enumerate() {
let style = Self::te_style_at_offset(runs, start + offset);
width = width.saturating_add(self.te_char_width(style.font, style.size, byte));
}
width
}
fn te_next_break_styled(
&self,
runs: &[TeStyleRun],
text_bytes: &[u8],
offset: usize,
max_width: i16,
) -> usize {
let len = text_bytes.len();
if offset >= len {
return len;
}
let mut width = 0i16;
let mut index = offset;
while width <= max_width && index < len && !matches!(text_bytes[index], b'\r' | b'\n') {
let style = Self::te_style_at_offset(runs, index);
width =
width.saturating_add(self.te_char_width(style.font, style.size, text_bytes[index]));
index += 1;
}
let mut next = if width > max_width {
let max_index = index.saturating_sub(1);
if text_bytes[max_index] == b' ' {
let mut scan = index;
while scan < len && text_bytes[scan] == b' ' {
scan += 1;
}
scan
} else {
let mut scan = max_index;
while scan > offset && !Self::te_word_break_byte(text_bytes[scan - 1]) {
scan -= 1;
}
if scan == offset {
max_index
} else {
scan
}
}
} else if index == len {
len
} else {
index + 1
};
if next == offset && offset < len {
next += 1;
}
next
}
fn te_wrap_lines_styled(
&self,
runs: &[TeStyleRun],
text_bytes: &[u8],
box_width: i16,
) -> Vec<(usize, usize)> {
let mut lines = Vec::new();
let mut line_start = 0usize;
while line_start < text_bytes.len() {
let next = self.te_next_break_styled(runs, text_bytes, line_start, box_width);
lines.push((line_start, next.min(text_bytes.len())));
line_start = next;
}
lines
}
fn te_line_metrics_for_range(runs: &[TeStyleRun], start: usize, end: usize) -> (i16, i16) {
if runs.is_empty() {
return (0, 0);
}
let mut line_height = 0i16;
let mut ascent = 0i16;
if start >= end {
let style = Self::te_style_at_offset(runs, start);
return (style.line_height, style.ascent);
}
for offset in start..end {
let style = Self::te_style_at_offset(runs, offset);
line_height = line_height.max(style.line_height);
ascent = ascent.max(style.ascent);
}
(line_height.max(1), ascent.max(0))
}
fn te_update_styled_run_sentinel(bus: &mut MacMemoryBus, te_handle: u32, text_len: usize) {
let style_handle = Self::te_style_handle(bus, te_handle);
if style_handle == 0 {
return;
}
let style_ptr = bus.read_long(style_handle);
if style_ptr == 0 {
return;
}
let n_runs = bus.read_word(style_ptr + Self::TE_STYLE_N_RUNS_OFFSET) as usize;
let needed = Self::TE_STYLE_RUNS_OFFSET + ((n_runs as u32 + 1) * 4);
let style_ptr = Self::ensure_handle_capacity(bus, style_handle, needed);
if style_ptr == 0 {
return;
}
let sentinel = style_ptr + Self::TE_STYLE_RUNS_OFFSET + (n_runs as u32 * 4);
bus.write_word(
sentinel,
text_len.saturating_add(1).min(u16::MAX as usize) as u16,
);
bus.write_word(sentinel + 2, 0xFFFF);
}
fn te_apply_style_scrap_to_range(
&mut self,
bus: &mut MacMemoryBus,
te_handle: u32,
style_scrap: u32,
range_start: usize,
range_len: usize,
) -> bool {
let te_ptr = Self::te_record_ptr(bus, te_handle);
if !Self::te_is_styled_record(bus, te_ptr) || style_scrap == 0 || range_len == 0 {
return false;
}
let text_len = Self::te_text_length(bus, te_handle);
let range_start = range_start.min(text_len);
let range_end = range_start.saturating_add(range_len).min(text_len);
if range_start >= range_end {
return false;
}
let scrap_ptr = bus.read_long(style_scrap);
if scrap_ptr == 0 {
return false;
}
let scrap_size = bus.get_alloc_size(scrap_ptr).unwrap_or(0);
if scrap_size < Self::SCRAP_STYLE_TAB_OFFSET {
return false;
}
let style_count = (bus.read_word(scrap_ptr + Self::SCRAP_N_STYLES_OFFSET) as usize).min(
scrap_size
.saturating_sub(Self::SCRAP_STYLE_TAB_OFFSET)
.checked_div(Self::SCRAP_STYLE_ELEMENT_SIZE)
.unwrap_or(0) as usize,
);
if style_count == 0 {
return false;
}
let existing_runs = self.te_style_runs(bus, te_handle, text_len);
let mut candidates: Vec<(usize, u8, TeResolvedStyle)> =
Vec::with_capacity(existing_runs.len() + style_count + 2);
for run in &existing_runs {
if run.start < range_start || run.start > range_end {
candidates.push((run.start.min(text_len), 0, run.style));
}
}
for style_index in 0..style_count {
let scrap_style_ptr = scrap_ptr
+ Self::SCRAP_STYLE_TAB_OFFSET
+ (style_index as u32 * Self::SCRAP_STYLE_ELEMENT_SIZE);
let local_start =
bus.read_long(scrap_style_ptr + Self::SCRAP_STYLE_START_CHAR_OFFSET) as usize;
let run_start = range_start.saturating_add(local_start.min(range_end - range_start));
candidates.push((
run_start,
1,
Self::te_style_from_scrap_element(bus, scrap_style_ptr),
));
}
if range_end < text_len {
candidates.push((
range_end,
2,
Self::te_style_at_offset(&existing_runs, range_end),
));
}
if !candidates.iter().any(|(start, _, _)| *start == 0) {
candidates.push((0, 0, Self::te_style_at_offset(&existing_runs, 0)));
}
candidates.sort_by_key(|(start, order, _)| (*start, *order));
let mut merged: Vec<(usize, TeResolvedStyle)> = Vec::with_capacity(candidates.len());
for (start, _, style) in candidates {
let start = start.min(text_len);
if let Some((last_start, last_style)) = merged.last_mut() {
if *last_start == start {
*last_style = style;
continue;
}
if *last_style == style {
continue;
}
}
merged.push((start, style));
}
if merged.is_empty() {
merged.push((0, self.te_primary_resolved_style(bus, te_handle)));
}
if merged[0].0 != 0 {
merged.insert(0, (0, Self::te_style_at_offset(&existing_runs, 0)));
}
let run_count = merged.len().min(u16::MAX as usize);
let style_handle = Self::te_style_handle(bus, te_handle);
if style_handle == 0 {
return false;
}
let style_ptr = Self::ensure_handle_capacity(
bus,
style_handle,
Self::TE_STYLE_RUNS_OFFSET + ((run_count as u32 + 1) * 4),
);
if style_ptr == 0 {
return false;
}
let mut style_table_handle = bus.read_long(style_ptr + Self::TE_STYLE_STYLE_TABLE_OFFSET);
if style_table_handle == 0 {
style_table_handle = Self::allocate_handle_with_data(bus, 0);
bus.write_long(
style_ptr + Self::TE_STYLE_STYLE_TABLE_OFFSET,
style_table_handle,
);
}
let style_table_ptr = Self::ensure_handle_capacity(
bus,
style_table_handle,
(run_count as u32) * Self::ST_ELEMENT_SIZE,
);
if style_table_ptr == 0 {
return false;
}
bus.write_word(style_ptr + Self::TE_STYLE_N_RUNS_OFFSET, run_count as u16);
bus.write_word(style_ptr + Self::TE_STYLE_N_STYLES_OFFSET, run_count as u16);
for (index, (start, style)) in merged.iter().take(run_count).enumerate() {
let style_element_ptr = style_table_ptr + (index as u32 * Self::ST_ELEMENT_SIZE);
Self::te_write_style_table_element(bus, style_element_ptr, *style);
let run_ptr = style_ptr + Self::TE_STYLE_RUNS_OFFSET + (index as u32 * 4);
bus.write_word(run_ptr, (*start).min(u16::MAX as usize) as u16);
bus.write_word(run_ptr + 2, index.min(u16::MAX as usize) as u16);
}
Self::te_update_styled_run_sentinel(bus, te_handle, text_len);
true
}
#[allow(dead_code)]
fn te_line_metrics(&self, bus: &MacMemoryBus, te_handle: u32) -> (i16, i16) {
let te_ptr = Self::te_record_ptr(bus, te_handle);
if te_ptr == 0 {
let metrics = get_font_metrics(self.tx_font, Self::font_lookup_size(self.tx_size));
return (
metrics.ascent + metrics.descent + metrics.leading,
metrics.ascent,
);
}
let line_height = bus.read_word(te_ptr + Self::TE_LINE_HEIGHT_OFFSET) as i16;
let font_ascent = bus.read_word(te_ptr + Self::TE_FONT_ASCENT_OFFSET) as i16;
if line_height > 0 && font_ascent >= 0 {
return (line_height, font_ascent);
}
let (_, _, _, _, resolved_line_height, resolved_ascent) =
self.te_primary_style(bus, te_handle);
(resolved_line_height, resolved_ascent)
}
fn initialize_te_record(
&mut self,
bus: &mut MacMemoryBus,
te_handle: u32,
dest_rect: (i16, i16, i16, i16),
view_rect: (i16, i16, i16, i16),
) {
let te_ptr = Self::te_record_ptr(bus, te_handle);
if te_ptr == 0 {
return;
}
let metrics = get_font_metrics(self.tx_font, Self::font_lookup_size(self.tx_size));
let line_height = metrics.ascent + metrics.descent + metrics.leading;
let h_text = bus.alloc(4);
if h_text != 0 {
bus.write_long(h_text, 0);
}
Self::te_write_rect_words(bus, te_ptr + Self::TE_DEST_RECT_OFFSET, dest_rect);
Self::te_write_rect_words(bus, te_ptr + Self::TE_VIEW_RECT_OFFSET, view_rect);
Self::te_write_rect_words(bus, te_ptr + Self::TE_SEL_RECT_OFFSET, dest_rect);
bus.write_word(te_ptr + Self::TE_LINE_HEIGHT_OFFSET, line_height as u16);
bus.write_word(te_ptr + Self::TE_FONT_ASCENT_OFFSET, metrics.ascent as u16);
bus.write_word(te_ptr + Self::TE_SEL_POINT_OFFSET, dest_rect.0 as u16);
bus.write_word(te_ptr + Self::TE_SEL_POINT_OFFSET + 2, dest_rect.1 as u16);
bus.write_word(te_ptr + Self::TE_SEL_START_OFFSET, 0);
bus.write_word(te_ptr + Self::TE_SEL_END_OFFSET, 0);
bus.write_long(te_ptr + Self::TE_CARET_TIME_OFFSET, self.tick_count);
bus.write_word(te_ptr + Self::TE_CARET_STATE_OFFSET, 0);
bus.write_word(te_ptr + Self::TE_LENGTH_OFFSET, 0);
bus.write_long(te_ptr + Self::TE_HTEXT_OFFSET, h_text);
bus.write_word(te_ptr + Self::TE_TX_FONT_OFFSET, self.tx_font as u16);
bus.write_word(te_ptr + Self::TE_TX_FACE_OFFSET, self.tx_face as u16);
bus.write_word(te_ptr + Self::TE_TX_MODE_OFFSET, self.tx_mode as u16);
bus.write_word(te_ptr + Self::TE_TX_SIZE_OFFSET, self.tx_size as u16);
bus.write_long(te_ptr + Self::TE_IN_PORT_OFFSET, self.current_port);
self.textedit_states.remove(&te_handle);
}
fn initialize_styled_te_record(
&mut self,
bus: &mut MacMemoryBus,
te_handle: u32,
dest_rect: (i16, i16, i16, i16),
view_rect: (i16, i16, i16, i16),
) {
let te_ptr = Self::te_record_ptr(bus, te_handle);
if te_ptr == 0 {
return;
}
let style_size = Self::font_lookup_size(self.tx_size);
let metrics = get_font_metrics(self.tx_font, style_size);
let line_height = metrics.ascent + metrics.descent + metrics.leading;
let h_text = Self::allocate_handle_with_data(bus, 0);
let style_table = Self::allocate_handle_with_data(bus, Self::ST_ELEMENT_SIZE);
let lh_table = Self::allocate_handle_with_data(bus, Self::LH_ELEMENT_SIZE);
let null_scrap = Self::allocate_handle_with_data(bus, Self::STYLE_SCRAP_REC_SIZE);
let null_style = Self::allocate_handle_with_data(bus, Self::NULL_STYLE_REC_SIZE);
let style_handle = Self::allocate_handle_with_data(bus, 0x1C);
let style_table_ptr = if style_table != 0 {
bus.read_long(style_table)
} else {
0
};
if style_table_ptr != 0 {
bus.write_word(style_table_ptr + Self::ST_ELEMENT_REFCOUNT_OFFSET, 1);
bus.write_word(
style_table_ptr + Self::ST_ELEMENT_HEIGHT_OFFSET,
line_height as u16,
);
bus.write_word(
style_table_ptr + Self::ST_ELEMENT_ASCENT_OFFSET,
metrics.ascent as u16,
);
bus.write_word(
style_table_ptr + Self::ST_ELEMENT_FONT_OFFSET,
self.tx_font as u16,
);
bus.write_word(
style_table_ptr + Self::ST_ELEMENT_FACE_OFFSET,
self.tx_face as u16,
);
bus.write_word(
style_table_ptr + Self::ST_ELEMENT_SIZE_OFFSET,
style_size as u16,
);
bus.write_word(
style_table_ptr + Self::ST_ELEMENT_COLOR_OFFSET,
self.fg_color.0,
);
bus.write_word(
style_table_ptr + Self::ST_ELEMENT_COLOR_OFFSET + 2,
self.fg_color.1,
);
bus.write_word(
style_table_ptr + Self::ST_ELEMENT_COLOR_OFFSET + 4,
self.fg_color.2,
);
}
let lh_table_ptr = if lh_table != 0 {
bus.read_long(lh_table)
} else {
0
};
if lh_table_ptr != 0 {
bus.write_word(
lh_table_ptr + Self::LH_ELEMENT_HEIGHT_OFFSET,
line_height as u16,
);
bus.write_word(
lh_table_ptr + Self::LH_ELEMENT_ASCENT_OFFSET,
metrics.ascent as u16,
);
}
let null_scrap_ptr = if null_scrap != 0 {
bus.read_long(null_scrap)
} else {
0
};
if null_scrap_ptr != 0 {
bus.write_word(null_scrap_ptr + Self::SCRAP_N_STYLES_OFFSET, 0);
bus.write_long(
null_scrap_ptr + Self::SCRAP_STYLE_TAB_OFFSET + Self::SCRAP_STYLE_START_CHAR_OFFSET,
0,
);
bus.write_word(
null_scrap_ptr + Self::SCRAP_STYLE_TAB_OFFSET + Self::SCRAP_STYLE_HEIGHT_OFFSET,
line_height as u16,
);
bus.write_word(
null_scrap_ptr + Self::SCRAP_STYLE_TAB_OFFSET + Self::SCRAP_STYLE_ASCENT_OFFSET,
metrics.ascent as u16,
);
bus.write_word(
null_scrap_ptr + Self::SCRAP_STYLE_TAB_OFFSET + Self::SCRAP_STYLE_FONT_OFFSET,
self.tx_font as u16,
);
bus.write_word(
null_scrap_ptr + Self::SCRAP_STYLE_TAB_OFFSET + Self::SCRAP_STYLE_FACE_OFFSET,
self.tx_face as u16,
);
bus.write_word(
null_scrap_ptr + Self::SCRAP_STYLE_TAB_OFFSET + Self::SCRAP_STYLE_SIZE_OFFSET,
style_size as u16,
);
bus.write_word(
null_scrap_ptr + Self::SCRAP_STYLE_TAB_OFFSET + Self::SCRAP_STYLE_COLOR_OFFSET,
self.fg_color.0,
);
bus.write_word(
null_scrap_ptr + Self::SCRAP_STYLE_TAB_OFFSET + Self::SCRAP_STYLE_COLOR_OFFSET + 2,
self.fg_color.1,
);
bus.write_word(
null_scrap_ptr + Self::SCRAP_STYLE_TAB_OFFSET + Self::SCRAP_STYLE_COLOR_OFFSET + 4,
self.fg_color.2,
);
}
let null_style_ptr = if null_style != 0 {
bus.read_long(null_style)
} else {
0
};
if null_style_ptr != 0 {
bus.write_long(null_style_ptr + Self::NULL_STYLE_SCRAP_OFFSET, null_scrap);
}
let style_ptr = if style_handle != 0 {
bus.read_long(style_handle)
} else {
0
};
if style_ptr != 0 {
bus.write_word(style_ptr + Self::TE_STYLE_N_RUNS_OFFSET, 1);
bus.write_word(style_ptr + Self::TE_STYLE_N_STYLES_OFFSET, 1);
bus.write_long(style_ptr + Self::TE_STYLE_STYLE_TABLE_OFFSET, style_table);
bus.write_long(style_ptr + Self::TE_STYLE_LH_TABLE_OFFSET, lh_table);
bus.write_long(style_ptr + Self::TE_STYLE_NULL_STYLE_OFFSET, null_style);
bus.write_word(style_ptr + Self::TE_STYLE_RUNS_OFFSET, 0);
bus.write_word(style_ptr + Self::TE_STYLE_RUNS_OFFSET + 2, 0);
bus.write_word(style_ptr + Self::TE_STYLE_RUNS_OFFSET + 4, 1);
bus.write_word(style_ptr + Self::TE_STYLE_RUNS_OFFSET + 6, 0xFFFF);
}
Self::te_write_rect_words(bus, te_ptr + Self::TE_DEST_RECT_OFFSET, dest_rect);
Self::te_write_rect_words(bus, te_ptr + Self::TE_VIEW_RECT_OFFSET, view_rect);
Self::te_write_rect_words(bus, te_ptr + Self::TE_SEL_RECT_OFFSET, dest_rect);
bus.write_word(te_ptr + Self::TE_LINE_HEIGHT_OFFSET, 0xFFFF);
bus.write_word(te_ptr + Self::TE_FONT_ASCENT_OFFSET, 0xFFFF);
bus.write_word(te_ptr + Self::TE_SEL_POINT_OFFSET, dest_rect.0 as u16);
bus.write_word(te_ptr + Self::TE_SEL_POINT_OFFSET + 2, dest_rect.1 as u16);
bus.write_word(te_ptr + Self::TE_SEL_START_OFFSET, 0);
bus.write_word(te_ptr + Self::TE_SEL_END_OFFSET, 0);
bus.write_long(te_ptr + Self::TE_CARET_TIME_OFFSET, self.tick_count);
bus.write_word(te_ptr + Self::TE_CARET_STATE_OFFSET, 0);
bus.write_word(te_ptr + Self::TE_JUST_OFFSET, 0);
bus.write_word(te_ptr + Self::TE_LENGTH_OFFSET, 0);
bus.write_long(te_ptr + Self::TE_HTEXT_OFFSET, h_text);
bus.write_long(te_ptr + Self::TE_TX_FONT_OFFSET, style_handle);
bus.write_word(te_ptr + Self::TE_TX_MODE_OFFSET, self.tx_mode as u16);
bus.write_word(te_ptr + Self::TE_TX_SIZE_OFFSET, 0xFFFF);
bus.write_long(te_ptr + Self::TE_IN_PORT_OFFSET, self.current_port);
bus.write_word(te_ptr + Self::TE_N_LINES_OFFSET, 0);
bus.write_word(te_ptr + Self::TE_LINE_STARTS_OFFSET, 0);
self.textedit_states.remove(&te_handle);
}
fn te_text_length(bus: &MacMemoryBus, te_handle: u32) -> usize {
let te_ptr = Self::te_record_ptr(bus, te_handle);
if te_ptr == 0 {
return 0;
}
let stored = bus.read_word(te_ptr + Self::TE_LENGTH_OFFSET) as usize;
if stored != 0 {
return stored;
}
let h_text = bus.read_long(te_ptr + Self::TE_HTEXT_OFFSET);
if h_text == 0 {
return 0;
}
let text_ptr = bus.read_long(h_text);
if text_ptr == 0 {
0
} else {
bus.get_alloc_size(text_ptr).unwrap_or(0) as usize
}
}
fn te_text_bytes(bus: &MacMemoryBus, te_handle: u32) -> Vec<u8> {
let len = Self::te_text_length(bus, te_handle);
if len == 0 {
return Vec::new();
}
let h_text = Self::te_text_handle(bus, te_handle);
if h_text == 0 {
return Vec::new();
}
let text_ptr = bus.read_long(h_text);
if text_ptr == 0 {
return Vec::new();
}
bus.read_bytes(text_ptr, len)
}
pub(crate) fn te_find_word_bounds(
&self,
bus: &MacMemoryBus,
te_handle: u32,
current_pos: usize,
) -> (u16, u16) {
let text_bytes = Self::te_text_bytes(bus, te_handle);
let len = text_bytes.len();
if len == 0 {
return (0, 0);
}
let pos = current_pos.min(len);
if pos < len && Self::te_word_break_byte(text_bytes[pos]) {
return (pos as u16, pos as u16);
}
let mut start = pos;
while start > 0 && !Self::te_word_break_byte(text_bytes[start - 1]) {
start -= 1;
}
let mut end = pos;
while end < len && !Self::te_word_break_byte(text_bytes[end]) {
end += 1;
}
(start as u16, end as u16)
}
#[allow(dead_code)]
fn te_reset_styled_metadata(
&mut self,
bus: &mut MacMemoryBus,
te_handle: u32,
text_len: usize,
) {
let style_handle = Self::te_style_handle(bus, te_handle);
if style_handle == 0 {
return;
}
let (font, face, size, color, line_height, ascent) = self.te_primary_style(bus, te_handle);
let style_ptr = bus.read_long(style_handle);
if style_ptr == 0 {
return;
}
let style_table_handle = bus.read_long(style_ptr + Self::TE_STYLE_STYLE_TABLE_OFFSET);
let style_table_ptr = if style_table_handle != 0 {
bus.read_long(style_table_handle)
} else {
0
};
if style_table_ptr != 0 {
bus.write_word(style_table_ptr + Self::ST_ELEMENT_REFCOUNT_OFFSET, 1);
bus.write_word(
style_table_ptr + Self::ST_ELEMENT_HEIGHT_OFFSET,
line_height as u16,
);
bus.write_word(
style_table_ptr + Self::ST_ELEMENT_ASCENT_OFFSET,
ascent as u16,
);
bus.write_word(style_table_ptr + Self::ST_ELEMENT_FONT_OFFSET, font as u16);
bus.write_word(style_table_ptr + Self::ST_ELEMENT_FACE_OFFSET, face as u16);
bus.write_word(style_table_ptr + Self::ST_ELEMENT_SIZE_OFFSET, size as u16);
bus.write_word(style_table_ptr + Self::ST_ELEMENT_COLOR_OFFSET, color.0);
bus.write_word(style_table_ptr + Self::ST_ELEMENT_COLOR_OFFSET + 2, color.1);
bus.write_word(style_table_ptr + Self::ST_ELEMENT_COLOR_OFFSET + 4, color.2);
}
let lh_handle = bus.read_long(style_ptr + Self::TE_STYLE_LH_TABLE_OFFSET);
let lh_ptr = if lh_handle != 0 {
bus.read_long(lh_handle)
} else {
0
};
if lh_ptr != 0 {
bus.write_word(lh_ptr + Self::LH_ELEMENT_HEIGHT_OFFSET, line_height as u16);
bus.write_word(lh_ptr + Self::LH_ELEMENT_ASCENT_OFFSET, ascent as u16);
}
bus.write_word(style_ptr + Self::TE_STYLE_N_RUNS_OFFSET, 1);
bus.write_word(style_ptr + Self::TE_STYLE_N_STYLES_OFFSET, 1);
bus.write_word(style_ptr + Self::TE_STYLE_RUNS_OFFSET, 0);
bus.write_word(style_ptr + Self::TE_STYLE_RUNS_OFFSET + 2, 0);
bus.write_word(
style_ptr + Self::TE_STYLE_RUNS_OFFSET + 4,
text_len.saturating_add(1).min(u16::MAX as usize) as u16,
);
bus.write_word(style_ptr + Self::TE_STYLE_RUNS_OFFSET + 6, 0xFFFF);
}
fn te_set_text_contents(&mut self, bus: &mut MacMemoryBus, te_handle: u32, text: &[u8]) {
let te_ptr = Self::te_record_ptr(bus, te_handle);
if te_ptr == 0 {
return;
}
let mut h_text = bus.read_long(te_ptr + Self::TE_HTEXT_OFFSET);
if h_text == 0 {
h_text = Self::allocate_handle_with_data(bus, 0);
bus.write_long(te_ptr + Self::TE_HTEXT_OFFSET, h_text);
}
let text_ptr = Self::ensure_text_handle_size(bus, h_text, text.len());
if !text.is_empty() && text_ptr != 0 {
bus.write_bytes(text_ptr, text);
}
let clamped_len = text.len().min(u16::MAX as usize) as u16;
bus.write_word(te_ptr + Self::TE_LENGTH_OFFSET, clamped_len);
bus.write_word(te_ptr + Self::TE_SEL_START_OFFSET, clamped_len);
bus.write_word(te_ptr + Self::TE_SEL_END_OFFSET, clamped_len);
self.te_recalculate_layout(bus, te_handle);
}
fn textedit_idle(&mut self, cpu: &mut impl CpuOps, bus: &mut MacMemoryBus, te_handle: u32) {
if trace_textedit_enabled() {
eprintln!("[TE] TEIdle hTE=${te_handle:08X}");
}
let te_ptr = Self::te_record_ptr(bus, te_handle);
if te_ptr == 0 {
return;
}
if bus.read_word(te_ptr + Self::TE_ACTIVE_OFFSET) == 0 {
return;
}
let sel_start = bus.read_word(te_ptr + Self::TE_SEL_START_OFFSET);
let sel_end = bus.read_word(te_ptr + Self::TE_SEL_END_OFFSET);
if sel_start != sel_end {
return;
}
let last_toggle = bus.read_long(te_ptr + Self::TE_CARET_TIME_OFFSET);
if self.tick_count.wrapping_sub(last_toggle) < Self::TE_CARET_BLINK_TICKS {
return;
}
let caret_state = bus.read_word(te_ptr + Self::TE_CARET_STATE_OFFSET);
bus.write_word(
te_ptr + Self::TE_CARET_STATE_OFFSET,
if caret_state == 0 { 1 } else { 0 },
);
bus.write_long(te_ptr + Self::TE_CARET_TIME_OFFSET, self.tick_count);
self.draw_te_contents(cpu, bus, te_handle);
}
fn te_insert_text(&mut self, bus: &mut MacMemoryBus, te_handle: u32, text: &[u8]) {
let te_ptr = Self::te_record_ptr(bus, te_handle);
if te_ptr == 0 {
return;
}
let existing = Self::te_text_bytes(bus, te_handle);
let mut sel_start = bus.read_word(te_ptr + Self::TE_SEL_START_OFFSET) as usize;
let mut sel_end = bus.read_word(te_ptr + Self::TE_SEL_END_OFFSET) as usize;
let text_len = existing.len();
sel_start = sel_start.min(text_len);
sel_end = sel_end.min(text_len);
if sel_end < sel_start {
std::mem::swap(&mut sel_start, &mut sel_end);
}
let mut merged =
Vec::with_capacity(sel_start + text.len() + text_len.saturating_sub(sel_end));
merged.extend_from_slice(&existing[..sel_start]);
merged.extend_from_slice(text);
merged.extend_from_slice(&existing[sel_end..]);
self.te_set_text_contents(bus, te_handle, &merged);
let insertion_end = (sel_start + text.len()).min(u16::MAX as usize) as u16;
bus.write_word(te_ptr + Self::TE_SEL_START_OFFSET, insertion_end);
bus.write_word(te_ptr + Self::TE_SEL_END_OFFSET, insertion_end);
}
fn te_char_width(&self, font: i16, size: i16, byte: u8) -> i16 {
let size = Self::font_lookup_size(size);
let (_face, scale) = get_font_face_scaled(font, size);
let ch = byte as char;
if let Some((glyph, _)) = crate::quickdraw::text::get_glyph(font, size, ch) {
self.glyph_advance(glyph) * scale
} else {
self.missing_glyph_advance() * scale
}
}
fn te_measure_text_width(
&self,
font: i16,
size: i16,
text_bytes: &[u8],
start: usize,
end: usize,
) -> i16 {
let mut width = 0i16;
for &b in &text_bytes[start..end] {
width += self.te_char_width(font, size, b);
}
width
}
fn te_word_break_byte(byte: u8) -> bool {
byte <= 0x20
}
fn te_next_break(
&self,
font: i16,
size: i16,
text_bytes: &[u8],
offset: usize,
max_width: i16,
) -> usize {
let len = text_bytes.len();
if offset >= len {
return len;
}
let mut width = 0i16;
let mut index = offset;
while width <= max_width && index < len && !matches!(text_bytes[index], b'\r' | b'\n') {
width = width.saturating_add(self.te_char_width(font, size, text_bytes[index]));
index += 1;
}
let mut next = if width > max_width {
let max_index = index.saturating_sub(1);
if text_bytes[max_index] == b' ' {
let mut scan = index;
while scan < len && text_bytes[scan] == b' ' {
scan += 1;
}
scan
} else {
let mut scan = max_index;
while scan > offset && !Self::te_word_break_byte(text_bytes[scan - 1]) {
scan -= 1;
}
if scan == offset {
max_index
} else {
scan
}
}
} else if index == len {
len
} else {
index + 1
};
if next == offset && offset < len {
next += 1;
}
next
}
fn te_wrap_lines(
&self,
font: i16,
size: i16,
text_bytes: &[u8],
box_width: i16,
) -> Vec<(usize, usize)> {
let mut lines: Vec<(usize, usize)> = Vec::new();
let mut line_start = 0usize;
while line_start < text_bytes.len() {
let next = self.te_next_break(font, size, text_bytes, line_start, box_width);
lines.push((line_start, next.min(text_bytes.len())));
line_start = next;
}
lines
}
fn te_recalculate_layout(&mut self, bus: &mut MacMemoryBus, te_handle: u32) {
let te_ptr = Self::te_record_ptr(bus, te_handle);
if te_ptr == 0 {
return;
}
let text_bytes = Self::te_text_bytes(bus, te_handle);
let text_len = text_bytes.len();
let dest_rect = Self::te_read_rect(bus, te_ptr + Self::TE_DEST_RECT_OFFSET);
let (font, _, size, _, line_height, font_ascent) = self.te_primary_style(bus, te_handle);
let styled = Self::te_is_styled_record(bus, te_ptr);
let style_runs = if styled {
self.te_style_runs(bus, te_handle, text_len)
} else {
Vec::new()
};
let uses_styled_runs = styled && !style_runs.is_empty();
let lines = if text_bytes.is_empty() {
Vec::new()
} else if uses_styled_runs {
self.te_wrap_lines_styled(&style_runs, &text_bytes, (dest_rect.3 - dest_rect.1).max(0))
} else {
self.te_wrap_lines(font, size, &text_bytes, (dest_rect.3 - dest_rect.1).max(0))
};
let line_count = lines.len();
let te_ptr = Self::ensure_te_record_line_capacity(bus, te_handle, line_count);
if te_ptr == 0 {
return;
}
bus.write_word(
te_ptr + Self::TE_N_LINES_OFFSET,
line_count.min(u16::MAX as usize) as u16,
);
if styled {
bus.write_word(te_ptr + Self::TE_LINE_HEIGHT_OFFSET, 0xFFFF);
bus.write_word(te_ptr + Self::TE_FONT_ASCENT_OFFSET, 0xFFFF);
} else {
bus.write_word(te_ptr + Self::TE_LINE_HEIGHT_OFFSET, line_height as u16);
bus.write_word(te_ptr + Self::TE_FONT_ASCENT_OFFSET, font_ascent as u16);
}
for index in 0..=line_count.saturating_add(1) {
bus.write_word(te_ptr + Self::TE_LINE_STARTS_OFFSET + (index as u32 * 2), 0);
}
for (index, (start, _)) in lines.iter().enumerate() {
bus.write_word(
te_ptr + Self::TE_LINE_STARTS_OFFSET + (index as u32 * 2),
(*start).min(u16::MAX as usize) as u16,
);
}
bus.write_word(
te_ptr + Self::TE_LINE_STARTS_OFFSET + (line_count as u32 * 2),
text_len.min(u16::MAX as usize) as u16,
);
bus.write_word(
te_ptr + Self::TE_LINE_STARTS_OFFSET + ((line_count as u32 + 1) * 2),
0,
);
if styled {
let style_handle = Self::te_style_handle(bus, te_handle);
if style_handle != 0 {
let style_ptr = bus.read_long(style_handle);
if style_ptr != 0 {
let lh_handle = bus.read_long(style_ptr + Self::TE_STYLE_LH_TABLE_OFFSET);
if lh_handle != 0 {
let lh_ptr = Self::ensure_handle_capacity(
bus,
lh_handle,
((line_count + 1) as u32) * Self::LH_ELEMENT_SIZE,
);
if lh_ptr != 0 {
for index in 0..=line_count {
let (line_height, font_ascent) = if uses_styled_runs
&& index < lines.len()
{
let (start, end) = lines[index];
Self::te_line_metrics_for_range(&style_runs, start, end)
} else if uses_styled_runs {
Self::te_line_metrics_for_range(&style_runs, text_len, text_len)
} else {
(line_height, font_ascent)
};
let element = lh_ptr + (index as u32 * Self::LH_ELEMENT_SIZE);
bus.write_word(
element + Self::LH_ELEMENT_HEIGHT_OFFSET,
line_height as u16,
);
bus.write_word(
element + Self::LH_ELEMENT_ASCENT_OFFSET,
font_ascent as u16,
);
}
}
}
}
}
}
if styled {
Self::te_update_styled_run_sentinel(bus, te_handle, text_len);
}
}
fn te_line_origin_x(just: i16, box_left: i16, box_right: i16, line_width: i16) -> i16 {
// Per Inside Macintosh: Text 1993, p. 7320-7323 (and MPW
// Universal Headers `TextEdit.h`):
// teJustLeft = 0 (flush left — system default for LTR)
// teJustCenter = 1 (centered)
// teJustRight = -1 (flush right)
// teForceLeft = -2 (force flush left, overrides localised right-to-left)
match just {
1 => box_left + ((box_right - box_left) - line_width) / 2,
-1 => box_right - line_width,
_ => box_left.saturating_add(Self::TE_LINE_LEFT_INSET), // 0 / -2 / any other → flush left inside destRect
}
}
fn port_has_drawable_bitmap(bus: &MacMemoryBus, port: u32) -> bool {
if port == 0 {
return false;
}
let port_version = bus.read_word(port + 6);
let is_color = (port_version & 0xC000) != 0;
let (base, row_bytes, top, left, bottom, right) = if is_color {
let pix_map_handle = bus.read_long(port + 2);
if pix_map_handle == 0 {
return false;
}
let pix_map_ptr = bus.read_long(pix_map_handle);
if pix_map_ptr == 0 {
return false;
}
(
bus.read_long(pix_map_ptr),
bus.read_word(pix_map_ptr + 4) & 0x3FFF,
bus.read_word(pix_map_ptr + 6) as i16,
bus.read_word(pix_map_ptr + 8) as i16,
bus.read_word(pix_map_ptr + 10) as i16,
bus.read_word(pix_map_ptr + 12) as i16,
)
} else {
(
bus.read_long(port + 2),
bus.read_word(port + 6) & 0x3FFF,
bus.read_word(port + 8) as i16,
bus.read_word(port + 10) as i16,
bus.read_word(port + 12) as i16,
bus.read_word(port + 14) as i16,
)
};
base != 0 && row_bytes != 0 && top < bottom && left < right
}
fn draw_te_contents(&mut self, cpu: &mut impl CpuOps, bus: &mut MacMemoryBus, te_handle: u32) {
let te_ptr = Self::te_record_ptr(bus, te_handle);
if te_ptr == 0 {
return;
}
let text_bytes = Self::te_text_bytes(bus, te_handle);
let dest_rect = Self::te_read_rect(bus, te_ptr + Self::TE_DEST_RECT_OFFSET);
let view_rect = Self::te_read_rect(bus, te_ptr + Self::TE_VIEW_RECT_OFFSET);
let te_port = bus.read_long(te_ptr + Self::TE_IN_PORT_OFFSET);
if !Self::port_has_drawable_bitmap(bus, te_port) {
return;
}
let just = bus.read_word(te_ptr + Self::TE_JUST_OFFSET) as i16;
let (font, face, size, color, _, _) = self.te_primary_style(bus, te_handle);
let styled_runs = if Self::te_is_styled_record(bus, te_ptr) {
self.te_style_runs(bus, te_handle, text_bytes.len())
} else {
Vec::new()
};
let uses_styled_runs = !styled_runs.is_empty() && Self::te_is_styled_record(bus, te_ptr);
let old_font = self.tx_font;
let old_face = self.tx_face;
let old_size = self.tx_size;
let old_fg = self.fg_color;
let old_loc = self.pn_loc;
let previous_port = self.current_port;
let previous_gdevice = self.current_gdevice;
let switched_port = te_port != 0 && te_port != previous_port;
// Always sync A5 globals to te_port before drawing, even if port didn't change.
// draw_generic_shape reads from A5 globals, and untrack_window can set self.current_port
// without updating A5 globals, causing a divergence that sends drawing to the wrong port.
if te_port != 0 {
self.set_current_port_state(bus, cpu, te_port, None);
}
if trace_textedit_enabled() {
let (vis_top, vis_left, vis_bottom, vis_right) = {
let vis_handle = bus.read_long(self.current_port + 24);
let vis_ptr = if vis_handle != 0 {
bus.read_long(vis_handle)
} else {
0
};
if vis_ptr != 0 {
(
bus.read_word(vis_ptr + 2) as i16,
bus.read_word(vis_ptr + 4) as i16,
bus.read_word(vis_ptr + 6) as i16,
bus.read_word(vis_ptr + 8) as i16,
)
} else {
(0, 0, 0, 0)
}
};
let (clip_top, clip_left, clip_bottom, clip_right) = {
let clip_handle = bus.read_long(self.current_port + 28);
let clip_ptr = if clip_handle != 0 {
bus.read_long(clip_handle)
} else {
0
};
if clip_ptr != 0 {
(
bus.read_word(clip_ptr + 2) as i16,
bus.read_word(clip_ptr + 4) as i16,
bus.read_word(clip_ptr + 6) as i16,
bus.read_word(clip_ptr + 8) as i16,
)
} else {
(0, 0, 0, 0)
}
};
eprintln!(
"[TE] draw_te_contents hTE=${:08X} te_port=${:08X} prev_port=${:08X} current_port=${:08X} switched={} gworld={} dest=({},{},{},{}) view=({},{},{},{}) vis=({},{},{},{}) clip=({},{},{},{})",
te_handle,
te_port,
previous_port,
self.current_port,
switched_port,
self.gworld_devices.contains_key(&te_port),
dest_rect.0,
dest_rect.1,
dest_rect.2,
dest_rect.3,
view_rect.0,
view_rect.1,
view_rect.2,
view_rect.3,
vis_top,
vis_left,
vis_bottom,
vis_right,
clip_top,
clip_left,
clip_bottom,
clip_right,
);
}
let checksum_view_rect =
|bus: &MacMemoryBus, port: u32, rect: (i16, i16, i16, i16)| -> u64 {
if port == 0 {
return 0;
}
let port_version = bus.read_word(port + 6);
let is_color = (port_version & 0xC000) != 0;
let (pix_base, row_bytes, bounds_top, bounds_left, bounds_bottom, bounds_right) =
if is_color {
let pix_map_handle = bus.read_long(port + 2);
if pix_map_handle == 0 {
return 0;
}
let pix_map_ptr = bus.read_long(pix_map_handle);
if pix_map_ptr == 0 {
return 0;
}
(
bus.read_long(pix_map_ptr),
(bus.read_word(pix_map_ptr + 4) & 0x3FFF) as u32,
bus.read_word(pix_map_ptr + 6) as i16,
bus.read_word(pix_map_ptr + 8) as i16,
bus.read_word(pix_map_ptr + 10) as i16,
bus.read_word(pix_map_ptr + 12) as i16,
)
} else {
(
bus.read_long(port + 2),
(bus.read_word(port + 6) & 0x3FFF) as u32,
bus.read_word(port + 8) as i16,
bus.read_word(port + 10) as i16,
bus.read_word(port + 12) as i16,
bus.read_word(port + 14) as i16,
)
};
let top = rect.0.max(bounds_top);
let left = rect.1.max(bounds_left);
let bottom = rect.2.min(bounds_bottom);
let right = rect.3.min(bounds_right);
if top >= bottom || left >= right || pix_base == 0 {
return 0;
}
let mut sum = 0u64;
for y in top..bottom {
let dy = (y - bounds_top) as u32;
for x in left..right {
let dx = (x - bounds_left) as u32;
sum =
sum.wrapping_add(bus.read_byte(pix_base + dy * row_bytes + dx) as u64);
}
}
sum
};
let checksum_before = if trace_textedit_enabled() {
checksum_view_rect(bus, self.current_port, view_rect)
} else {
0
};
self.tx_font = font;
self.tx_face = face;
self.tx_size = size;
self.fg_color = color;
let clip_top = view_rect.0;
let clip_left = view_rect.1;
let clip_bottom = view_rect.2;
let clip_right = view_rect.3;
let box_width = (dest_rect.3 - dest_rect.1).max(0);
self.draw_rect(
cpu,
bus,
&Rect {
top: clip_top,
left: clip_left,
bottom: clip_bottom,
right: clip_right,
},
ShapeOp::Erase,
);
let lines = if text_bytes.is_empty() {
vec![(0usize, 0usize)]
} else if uses_styled_runs {
self.te_wrap_lines_styled(&styled_runs, &text_bytes, box_width)
} else {
self.te_wrap_lines(font, size, &text_bytes, box_width)
};
let mut top = dest_rect.0;
let mut visible_lines = Vec::new();
let mut visual_lines = Vec::new();
for (index, (start, end)) in lines.iter().enumerate() {
let line_height = Self::te_height_for_line(bus, te_handle, index);
let line_ascent = Self::te_ascent_for_line(bus, te_handle, index);
let line_bottom = top.saturating_add(line_height);
if line_bottom <= clip_top {
top = line_bottom;
continue;
}
if top >= clip_bottom {
break;
}
visible_lines.push(index);
let mut trimmed_end = *end;
while trimmed_end > *start
&& matches!(text_bytes[trimmed_end - 1], b' ' | b'\r' | b'\n')
{
trimmed_end -= 1;
}
let line_width = if uses_styled_runs {
self.te_measure_text_width_styled(&styled_runs, &text_bytes, *start, trimmed_end)
} else {
self.te_measure_text_width(font, size, &text_bytes, *start, trimmed_end)
};
let x = Self::te_line_origin_x(just, dest_rect.1, dest_rect.3, line_width);
visual_lines.push((*start, *end, top, line_bottom, x));
self.pn_loc = (top.saturating_add(line_ascent), x);
for (offset, &byte) in text_bytes[*start..trimmed_end].iter().enumerate() {
if uses_styled_runs {
let style = Self::te_style_at_offset(&styled_runs, *start + offset);
self.tx_font = style.font;
self.tx_face = style.face;
self.tx_size = style.size;
self.fg_color = style.color;
}
self.draw_char(cpu, bus, byte as char);
}
top = line_bottom;
}
let te_active = bus.read_word(te_ptr + Self::TE_ACTIVE_OFFSET) != 0;
let caret_visible = bus.read_word(te_ptr + Self::TE_CARET_STATE_OFFSET) == 0;
let outline_hilite = self.te_feature_bit(te_handle, Self::TE_FEATURE_OUTLINE_HILITE);
let sel_start = bus.read_word(te_ptr + Self::TE_SEL_START_OFFSET) as usize;
let sel_end = bus.read_word(te_ptr + Self::TE_SEL_END_OFFSET) as usize;
let (selection_start, selection_end) = if sel_start <= sel_end {
(sel_start, sel_end)
} else {
(sel_end, sel_start)
};
if selection_start == selection_end {
if te_active && caret_visible {
if let Some(&(line_start, line_end, line_top, line_bottom, line_x)) = visual_lines
.iter()
.find(|&&(start, end, _, _, _)| {
selection_start >= start && selection_start <= end
})
.or_else(|| visual_lines.last())
{
let caret_offset = selection_start.min(line_end).max(line_start);
let measured_width = if uses_styled_runs {
self.te_measure_text_width_styled(
&styled_runs,
&text_bytes,
line_start,
caret_offset,
)
} else {
self.te_measure_text_width(
font,
size,
&text_bytes,
line_start,
caret_offset,
)
};
let mut caret_x = line_x + measured_width;
if caret_offset > line_start {
caret_x = caret_x.saturating_sub(1);
}
let caret_width = self.ui_theme().text_theme().caret_width.max(1);
if !self.draw_theme_caret(
bus,
line_top,
caret_x,
line_bottom,
caret_x.saturating_add(caret_width),
) {
self.draw_rect(
cpu,
bus,
&Rect {
top: line_top,
left: caret_x,
bottom: line_bottom,
right: caret_x.saturating_add(1),
},
ShapeOp::Paint,
);
}
}
}
} else if te_active || outline_hilite {
// IM:I I-375/I-385 and Text 1993 p. 2-80: an active edit
// record displays its selection range by highlighting the
// selected characters. Classic black-and-white TextEdit uses
// inverse video; non-classic themes own their selection chrome.
for &(line_start, line_end, line_top, line_bottom, line_x) in &visual_lines {
let start = selection_start.max(line_start);
let end = selection_end.min(line_end);
if start >= end {
continue;
}
let mut selection_left = line_x
+ if uses_styled_runs {
self.te_measure_text_width_styled(
&styled_runs,
&text_bytes,
line_start,
start,
)
} else {
self.te_measure_text_width(font, size, &text_bytes, line_start, start)
};
let selection_right = line_x
+ if uses_styled_runs {
self.te_measure_text_width_styled(
&styled_runs,
&text_bytes,
line_start,
end,
)
} else {
self.te_measure_text_width(font, size, &text_bytes, line_start, end)
};
if start == line_start && matches!(just, 0 | -2) {
// BasiliskII/System 7.5.3 `a9d3_teupdate_selection_visual`
// shows a selection beginning at a left-justified visual
// line includes TextEdit's one-pixel left inset; glyphs
// still draw at destRect.left + TE_LINE_LEFT_INSET.
selection_left = selection_left.saturating_sub(Self::TE_LINE_LEFT_INSET);
}
if self.ui_theme_id() == UiThemeId::ClassicSystem7 {
if te_active {
self.draw_rect(
cpu,
bus,
&Rect {
top: line_top,
left: selection_left,
bottom: line_bottom,
right: selection_right,
},
ShapeOp::Invert,
);
}
} else {
// Text 1993, "Outline Highlighting" and TEFeatureFlag
// teFOutlineHilite: inactive selection ranges are framed,
// while active ranges keep normal highlighted selection chrome.
self.draw_theme_text_selection(
bus,
line_top,
selection_left,
line_bottom,
selection_right,
te_active,
);
}
}
}
self.tx_font = old_font;
self.tx_face = old_face;
self.tx_size = old_size;
self.fg_color = old_fg;
self.pn_loc = old_loc;
if trace_textedit_enabled() {
eprintln!(
"[TE] draw_te_contents visible_lines hTE=${:08X} {:?}",
te_handle, visible_lines
);
eprintln!(
"[TE] draw_te_contents checksum hTE=${:08X} before={} after={}",
te_handle,
checksum_before,
checksum_view_rect(bus, self.current_port, view_rect)
);
}
if switched_port {
self.set_current_port_state(bus, cpu, previous_port, Some(previous_gdevice));
}
}
fn te_feature_bit(&self, te_handle: u32, feature: u16) -> bool {
let mask = 1u16.checked_shl(feature as u32).unwrap_or(0);
self.textedit_states
.get(&te_handle)
.map(|state| (state.feature_bits & mask) != 0)
.unwrap_or(false)
}
/// Whether auto-scroll is enabled on the TextEdit record at
/// `te_handle` (set by `TEAutoView` per IM:V V-173). Public
/// projection over `textedit_states[te].feature_bits` so
/// integration tests can observe TEAutoView's effect without
/// access to the private feature-bit constants.
pub fn te_auto_scroll_enabled(&self, te_handle: u32) -> bool {
self.te_feature_bit(te_handle, Self::TE_FEATURE_AUTO_SCROLL)
}
fn set_te_feature_bit(&mut self, te_handle: u32, feature: u16, enabled: bool) {
let mask = 1u16.checked_shl(feature as u32).unwrap_or(0);
let state = self.textedit_states.entry(te_handle).or_default();
if enabled {
state.feature_bits |= mask;
} else {
state.feature_bits &= !mask;
}
}
fn initialize_dialog_item_handles(
&mut self,
bus: &mut MacMemoryBus,
dialog_ptr: u32,
items: &[DialogItem],
) {
self.initialize_dialog_item_handles_from(bus, dialog_ptr, items, 0);
}
fn initialize_dialog_item_handles_from(
&mut self,
bus: &mut MacMemoryBus,
dialog_ptr: u32,
items: &[DialogItem],
start_index: usize,
) {
// Item lists in memory hold live item handles or userItem ProcPtrs.
// GetNewDialog reaches here with a copy of the DITL resource; NewDialog
// initializes the caller-supplied item-list handle.
// Inside Macintosh Volume I, I-405, I-412; MTE 1992 p. 6-114.
for (index, item) in items.iter().enumerate().skip(start_index) {
let item_no = index as i16 + 1;
let Some(item_handle_addr) = Self::dialog_item_handle_addr(bus, dialog_ptr, item_no)
else {
continue;
};
let base_type = item.item_type & 0x7F;
let item_handle = match base_type {
8 | 16 => {
let text_bytes = item.text.as_bytes();
let handle = bus.alloc(4);
let text_ptr = if text_bytes.is_empty() {
0
} else {
let ptr = bus.alloc(text_bytes.len() as u32);
bus.write_bytes(ptr, text_bytes);
ptr
};
bus.write_long(handle, text_ptr);
self.dialog_item_handles.insert(handle, (dialog_ptr, index));
handle
}
32 => self
.find_resource_any(*b"ICON", item.resource_id)
.map(|(_, ptr)| {
self.get_or_create_resource_handle(bus, *b"ICON", item.resource_id, ptr)
})
.unwrap_or(0),
4..=6 => {
let existing = bus.read_long(item_handle_addr);
if existing != 0 {
existing
} else {
self.create_standard_dialog_control_handle(bus, dialog_ptr, item_no, item)
}
}
7 => self
.find_resource_any(*b"CNTL", item.resource_id)
.map(|(_, cntl_ptr)| {
let value = bus.read_word(cntl_ptr + 8) as i16;
let vis_word = bus.read_word(cntl_ptr + 10);
let max = bus.read_word(cntl_ptr + 12) as i16;
let min = bus.read_word(cntl_ptr + 14) as i16;
let proc_id = bus.read_word(cntl_ptr + 16) as i16;
let ref_con = bus.read_long(cntl_ptr + 18);
let title_len = bus.read_byte(cntl_ptr + 22) as usize;
let title = bus.read_bytes(cntl_ptr + 23, title_len.min(255));
let ctrl_ptr = bus.alloc(296);
let handle = bus.alloc(4);
bus.write_long(handle, ctrl_ptr);
self.initialize_control_record(
bus,
ctrl_ptr,
dialog_ptr,
item.rect,
&title,
vis_word != 0,
value,
min,
max,
proc_id,
ref_con,
);
self.ensure_control_aux_record(bus, handle);
let old_head = bus.read_long(dialog_ptr + 140);
bus.write_long(ctrl_ptr, old_head);
bus.write_long(dialog_ptr + 140, handle);
self.dialog_control_handles
.insert(handle, (dialog_ptr, item_no));
self.dialog_control_values
.insert((dialog_ptr, item_no), value);
handle
})
.unwrap_or(0),
64 => self
.find_resource_any(*b"PICT", item.resource_id)
.map(|(_, ptr)| {
self.get_or_create_resource_handle(bus, *b"PICT", item.resource_id, ptr)
})
.unwrap_or(0),
0 => item.proc_ptr,
_ => 0,
};
bus.write_long(item_handle_addr, item_handle);
}
}
fn create_standard_dialog_control_handle(
&mut self,
bus: &mut MacMemoryBus,
dialog_ptr: u32,
item_no: i16,
item: &DialogItem,
) -> u32 {
let proc_id = match item.item_type & 0x7F {
4 => 0, // btnCtrl -> pushButProc
5 => 1, // chkCtrl -> checkBoxProc
6 => 2, // radCtrl -> radioButProc
_ => return 0,
};
let value = self
.dialog_control_values
.get(&(dialog_ptr, item_no))
.copied()
.unwrap_or(0);
let ctrl_ptr = bus.alloc(296);
let handle = bus.alloc(4);
if ctrl_ptr == 0 || handle == 0 {
return 0;
}
bus.write_long(handle, ctrl_ptr);
self.initialize_control_record(
bus,
ctrl_ptr,
dialog_ptr,
item.rect,
item.text.as_bytes(),
true,
value,
0,
1,
proc_id,
0,
);
self.ensure_control_aux_record(bus, handle);
let old_head = bus.read_long(dialog_ptr + 140);
bus.write_long(ctrl_ptr, old_head);
bus.write_long(dialog_ptr + 140, handle);
self.dialog_control_handles
.insert(handle, (dialog_ptr, item_no));
self.dialog_control_values
.insert((dialog_ptr, item_no), value);
handle
}
fn ditl_item_record_len_at(
bus: &MacMemoryBus,
ptr: u32,
data_len: u32,
record_offset: u32,
) -> Option<u32> {
if record_offset + 14 > data_len {
return None;
}
let item_type = bus.read_byte(ptr + record_offset + 12);
let data_len_byte = bus.read_byte(ptr + record_offset + 13);
let base_type = item_type & 0x7F;
let payload_offset = record_offset + 14;
let remaining = data_len.saturating_sub(payload_offset);
let payload_len = Self::ditl_item_payload_len(base_type, data_len_byte, remaining)?;
let padded = (payload_len + 1) & !1;
if padded > remaining {
return None;
}
Some(14 + padded)
}
fn ditl_used_len(bus: &MacMemoryBus, ptr: u32, data_len: u32) -> u32 {
if ptr == 0 || data_len < 2 {
return data_len;
}
let max_index = bus.read_word(ptr) as i16;
let count = if max_index < 0 {
0
} else {
max_index as usize + 1
};
let mut offset = 2u32;
for _ in 0..count {
let Some(record_len) = Self::ditl_item_record_len_at(bus, ptr, data_len, offset) else {
break;
};
offset = offset.saturating_add(record_len);
}
offset.min(data_len)
}
fn dialog_local_size(bus: &MacMemoryBus, dialog_ptr: u32) -> (i16, i16) {
let height = (bus.read_word(dialog_ptr + 20) as i16)
.saturating_sub(bus.read_word(dialog_ptr + 16) as i16);
let width = (bus.read_word(dialog_ptr + 22) as i16)
.saturating_sub(bus.read_word(dialog_ptr + 18) as i16);
(height, width)
}
fn append_ditl_offset(&self, bus: &MacMemoryBus, dialog_ptr: u32, method: i16) -> (i16, i16) {
match method {
// overlayDITL: appended rectangles are already dialog-local.
// Macintosh Toolbox Essentials 1992, pp. 6-108, 6-153.
0 => (0, 0),
// appendDITLRight / appendDITLBottom position relative to the
// dialog's upper-right or lower-left coordinate. The HLE updates
// item rectangles here; full window resizing is handled by callers
// that subsequently show/redraw the dialog.
1 => {
let (_, width) = Self::dialog_local_size(bus, dialog_ptr);
(0, width)
}
2 => {
let (height, _) = Self::dialog_local_size(bus, dialog_ptr);
(height, 0)
}
item_method if item_method < 0 => {
let item_no = item_method.checked_neg().unwrap_or(i16::MAX) as usize;
self.dialog_items
.get(&dialog_ptr)
.and_then(|items| item_no.checked_sub(1).and_then(|index| items.get(index)))
.map(|item| (item.rect.0, item.rect.1))
.unwrap_or((0, 0))
}
_ => (0, 0),
}
}
fn offset_dialog_item_rect(item: &mut DialogItem, v_delta: i16, h_delta: i16) {
item.rect = (
item.rect.0.saturating_add(v_delta),
item.rect.1.saturating_add(h_delta),
item.rect.2.saturating_add(v_delta),
item.rect.3.saturating_add(h_delta),
);
}
fn erase_retained_dialog_items_after_ditl_shorten(
&mut self,
bus: &mut MacMemoryBus,
dialog_ptr: u32,
removed_items: &[DialogItem],
) {
if removed_items.is_empty() || dialog_ptr == 0 {
return;
}
let bounds = Self::dialog_screen_bounds(bus, dialog_ptr);
for item in removed_items {
let (top, left, bottom, right) = Self::dialog_item_screen_rect(bounds, item.rect);
self.fill_rect_clipped_to_dialog(
bus,
bounds,
(top - 2, left - 2, bottom + 2, right + 2),
false,
);
}
if self.dialog_visible_snapshots.contains_key(&dialog_ptr) {
let pixels = self.save_dialog_pixels(bus, bounds);
self.dialog_visible_snapshots
.insert(dialog_ptr, PersistentDialogSnapshot { bounds, pixels });
}
let tracking_bounds = self
.dialog_tracking
.as_ref()
.filter(|tracking| tracking.dialog_ptr == dialog_ptr)
.map(|tracking| tracking.bounds);
if let Some(bounds) = tracking_bounds {
let rendered = self.save_dialog_pixels(bus, bounds);
if let Some(tracking) = self.dialog_tracking.as_mut() {
if tracking.dialog_ptr == dialog_ptr {
tracking.rendered_pixels = rendered;
tracking.rendered_pixels_final = true;
}
}
}
}
pub(crate) fn append_ditl_to_dialog(
&mut self,
bus: &mut MacMemoryBus,
dialog_ptr: u32,
ditl_handle: u32,
method: i16,
) -> u16 {
if dialog_ptr == 0 || ditl_handle == 0 {
return self
.dialog_items
.get(&dialog_ptr)
.map(|items| items.len() as u16)
.unwrap_or(0);
}
let source_ptr = bus.read_long(ditl_handle);
let Some(source_alloc_len) = bus.get_alloc_size(source_ptr) else {
return self
.dialog_items
.get(&dialog_ptr)
.map(|items| items.len() as u16)
.unwrap_or(0);
};
let source_len = Self::ditl_used_len(bus, source_ptr, source_alloc_len);
if source_len <= 2 {
return self
.dialog_items
.get(&dialog_ptr)
.map(|items| items.len() as u16)
.unwrap_or(0);
}
let mut appended_items = Self::parse_ditl(bus, source_ptr, source_len);
if appended_items.is_empty() {
return self
.dialog_items
.get(&dialog_ptr)
.map(|items| items.len() as u16)
.unwrap_or(0);
}
let mut items_handle = bus.read_long(dialog_ptr + 156);
if items_handle == 0 {
items_handle = bus.alloc(4);
let empty_ditl_ptr = bus.alloc(2);
bus.write_word(empty_ditl_ptr, 0xFFFF);
bus.write_long(items_handle, empty_ditl_ptr);
bus.write_long(dialog_ptr + 156, items_handle);
}
let old_ptr = bus.read_long(items_handle);
let old_alloc_len = bus.get_alloc_size(old_ptr).unwrap_or(0);
let old_len = if old_ptr != 0 && old_alloc_len >= 2 {
Self::ditl_used_len(bus, old_ptr, old_alloc_len).max(2)
} else {
2
};
let raw_existing_items = if old_ptr != 0 && old_alloc_len >= 2 {
Self::parse_ditl(bus, old_ptr, old_len)
} else {
Vec::new()
};
let mut all_items = self
.dialog_items
.get(&dialog_ptr)
.cloned()
.unwrap_or_else(|| raw_existing_items.clone());
if all_items.len() != raw_existing_items.len() && !raw_existing_items.is_empty() {
all_items = raw_existing_items;
}
let start_index = all_items.len();
let source_body_len = source_len.saturating_sub(2);
let new_len = old_len.saturating_add(source_body_len);
let new_ptr = bus.alloc(new_len);
if new_ptr == 0 {
return all_items.len() as u16;
}
if old_ptr != 0 && old_alloc_len >= 2 {
let old_bytes = bus.read_bytes(old_ptr, old_len as usize);
bus.write_bytes(new_ptr, &old_bytes);
} else {
bus.write_word(new_ptr, 0xFFFF);
}
let source_body = bus.read_bytes(source_ptr + 2, source_body_len as usize);
bus.write_bytes(new_ptr + old_len, &source_body);
let (v_delta, h_delta) = self.append_ditl_offset(bus, dialog_ptr, method);
let mut source_offset = 2u32;
let mut dest_offset = old_len;
for item in &mut appended_items {
if v_delta != 0 || h_delta != 0 {
Self::offset_dialog_item_rect(item, v_delta, h_delta);
bus.write_word(new_ptr + dest_offset + 4, item.rect.0 as u16);
bus.write_word(new_ptr + dest_offset + 6, item.rect.1 as u16);
bus.write_word(new_ptr + dest_offset + 8, item.rect.2 as u16);
bus.write_word(new_ptr + dest_offset + 10, item.rect.3 as u16);
}
let Some(record_len) =
Self::ditl_item_record_len_at(bus, source_ptr, source_len, source_offset)
else {
break;
};
source_offset = source_offset.saturating_add(record_len);
dest_offset = dest_offset.saturating_add(record_len);
}
all_items.extend(appended_items);
let new_count = all_items.len();
let max_index = if new_count == 0 {
0xFFFF
} else {
(new_count as u16).saturating_sub(1)
};
bus.write_word(new_ptr, max_index);
bus.write_long(items_handle, new_ptr);
self.dialog_items.insert(dialog_ptr, all_items.clone());
self.initialize_dialog_item_handles_from(bus, dialog_ptr, &all_items, start_index);
new_count as u16
}
pub(crate) fn shorten_ditl_in_dialog(
&mut self,
bus: &mut MacMemoryBus,
dialog_ptr: u32,
number_items: u16,
) -> u16 {
if dialog_ptr == 0 {
return 0;
}
let items_handle = bus.read_long(dialog_ptr + 156);
let ditl_ptr = if items_handle != 0 {
bus.read_long(items_handle)
} else {
0
};
let ditl_len = bus.get_alloc_size(ditl_ptr).unwrap_or(0);
let raw_items = if ditl_ptr != 0 && ditl_len >= 2 {
Self::parse_ditl(bus, ditl_ptr, Self::ditl_used_len(bus, ditl_ptr, ditl_len))
} else {
Vec::new()
};
let mut items = self
.dialog_items
.get(&dialog_ptr)
.cloned()
.unwrap_or(raw_items);
let remove_count = (number_items as usize).min(items.len());
let keep_count = items.len().saturating_sub(remove_count);
for item_no in (keep_count + 1)..=items.len() {
let item_no = item_no as i16;
let handle = Self::dialog_item_handle(bus, dialog_ptr, item_no);
self.dialog_control_values.remove(&(dialog_ptr, item_no));
self.dialog_control_handles.remove(&handle);
self.dialog_item_handles.remove(&handle);
}
let removed_items = items[keep_count..].to_vec();
items.truncate(keep_count);
if ditl_ptr != 0 && ditl_len >= 2 {
let max_index = if keep_count == 0 {
0xFFFF
} else {
(keep_count as u16).saturating_sub(1)
};
bus.write_word(ditl_ptr, max_index);
}
self.dialog_items.insert(dialog_ptr, items);
self.erase_retained_dialog_items_after_ditl_shorten(bus, dialog_ptr, &removed_items);
keep_count as u16
}
// ========== DLOG / DITL Resource Parsing ==========
/// Parse a DLOG resource from guest memory.
/// Returns (bounds, procID, visible, itemsID, title, position).
/// Inside Macintosh Volume I, I-437
/// Macintosh Toolbox Essentials 1992, p. 6-148
fn parse_dlog(
bus: &MacMemoryBus,
ptr: u32,
data_len: u32,
) -> ((i16, i16, i16, i16), i16, bool, i16, String, u16) {
let top = bus.read_word(ptr) as i16;
let left = bus.read_word(ptr + 2) as i16;
let bottom = bus.read_word(ptr + 4) as i16;
let right = bus.read_word(ptr + 6) as i16;
let proc_id = bus.read_word(ptr + 8) as i16;
let visible = bus.read_byte(ptr + 10) != 0;
// +11: filler
// +12: goAwayFlag (1 byte)
// +13: filler
let _ref_con = bus.read_long(ptr + 14);
let items_id = bus.read_word(ptr + 18) as i16;
// +20: title as Pascal string
let title_len = bus.read_byte(ptr + 20) as usize;
let mut title_bytes = vec![0u8; title_len];
for (i, byte) in title_bytes.iter_mut().enumerate() {
*byte = bus.read_byte(ptr + 21 + i as u32);
}
let title = decode_mac_roman_for_render(&title_bytes);
// Read positioning constant after the title Pascal string.
// Macintosh Toolbox Essentials 1992, pp. 4-125 to 4-126
// The position word follows the title, padded to an even boundary.
let title_end = 21 + title_len as u32;
let padded_end = (title_end + 1) & !1;
let position = if padded_end + 2 <= data_len {
bus.read_word(ptr + padded_end)
} else {
0
};
(
(top, left, bottom, right),
proc_id,
visible,
items_id,
title,
position,
)
}
/// Parse an ALRT resource from guest memory.
/// Returns (bounds, itemsID, stages, position).
/// Inside Macintosh Volume I, I-425 to I-426
/// Macintosh Toolbox Essentials 1992, p. 6-150
fn parse_alrt(
bus: &MacMemoryBus,
ptr: u32,
data_len: u32,
) -> ((i16, i16, i16, i16), i16, u16, u16) {
let bounds = if data_len >= 8 {
(
bus.read_word(ptr) as i16,
bus.read_word(ptr + 2) as i16,
bus.read_word(ptr + 4) as i16,
bus.read_word(ptr + 6) as i16,
)
} else {
(0, 0, 0, 0)
};
let items_id = if data_len >= 10 {
bus.read_word(ptr + 8) as i16
} else {
0
};
let stages = if data_len >= 12 {
bus.read_word(ptr + 10)
} else {
0
};
// System 7 compiled ALRT resources append the same positioning
// constants as DLOG resources after the 12-byte classic template.
// Macintosh Toolbox Essentials 1992, p. 6-150
let position = if data_len >= 14 {
bus.read_word(ptr + 12)
} else {
0
};
(bounds, items_id, stages, position)
}
fn positioned_dialog_bounds(
&self,
mut bounds: (i16, i16, i16, i16),
position: u16,
) -> (i16, i16, i16, i16) {
match position {
// alertPositionMainScreen / ParentWindow / ParentWindowScreen:
// MTE 1992 p. 4-126 defines alert position as about one-fifth
// of the unused screen/window space above the new window.
0x300A | 0x700A | 0xB00A => {
let (_, _, screen_w, screen_h, _) = self.get_screen_params();
let dialog_w = bounds.3 - bounds.1;
let dialog_h = bounds.2 - bounds.0;
let new_left = (screen_w - dialog_w) / 2;
let new_top = (screen_h - dialog_h) / 5;
bounds = (new_top, new_left, new_top + dialog_h, new_left + dialog_w);
}
// centerMainScreen / centerParentWindow: true vertical center
// Macintosh Toolbox Essentials 1992, p. 4-126
0x280A | 0x680A | 0xA80A | 0x380A => {
let (_, _, screen_w, screen_h, _) = self.get_screen_params();
let dialog_w = bounds.3 - bounds.1;
let dialog_h = bounds.2 - bounds.0;
let new_left = (screen_w - dialog_w) / 2;
let new_top = (screen_h - dialog_h) / 2;
bounds = (new_top, new_left, new_top + dialog_h, new_left + dialog_w);
}
_ => {} // noAutoCenter (0x0000) or unknown: use raw bounds
}
bounds
}
fn alert_stage_default_item(stages: u16, stage_word: u16) -> (u32, u32, Option<i16>) {
let stage_idx = (stage_word as u32).min(3);
// Each stage occupies 4 bits, stage 1 in the low nibble.
let nibble = ((stages as u32) >> (stage_idx * 4)) & 0xF;
let box_drawn = (nibble & 0x04) != 0;
let default_item = if (nibble & 0x08) == 0 { 1 } else { 2 };
(stage_idx, nibble, box_drawn.then_some(default_item))
}
fn enabled_button_count(items: &[DialogItem]) -> usize {
items
.iter()
.filter(|item| (item.item_type & 0x7F) == 4 && (item.item_type & 0x80) == 0)
.count()
}
fn begin_interactive_alert<C: CpuOps>(
&mut self,
cpu: &mut C,
bus: &mut MacMemoryBus,
sp: u32,
alert_id: i16,
_filter_proc: u32,
bounds: (i16, i16, i16, i16),
items_id: i16,
position: u16,
default_item: i16,
) -> bool {
let Some((_, ditl_data)) = self.find_resource_any(*b"DITL", items_id) else {
return false;
};
let ditl_len = bus.get_alloc_size(ditl_data).unwrap_or(0);
let items = Self::parse_ditl(bus, ditl_data, ditl_len);
if Self::enabled_button_count(&items) <= 1 {
return false;
}
let items_handle = {
let handle = bus.alloc(4);
bus.write_long(handle, ditl_data);
Self::duplicate_handle_data(bus, handle)
};
let bounds = self.positioned_dialog_bounds(bounds, position);
let dialog_ptr = self.finish_dialog_creation(
bus,
cpu,
0,
bounds,
"",
true,
1,
false,
alert_id as u32,
items_handle,
items.clone(),
);
if dialog_ptr == 0 {
return false;
}
bus.write_word(dialog_ptr + 168, default_item as u16);
let saved_pixels = self
.dialog_saved_pixels
.get(&dialog_ptr)
.cloned()
.unwrap_or_else(|| self.save_dialog_pixels(bus, bounds));
let rendered_pixels = self.save_dialog_pixels(bus, bounds);
bus.write_word(sp + 6, 0);
self.dialog_modal_entered.insert(dialog_ptr);
self.dialog_tracking = Some(DialogTrackingState {
dialog_ptr,
bounds,
title: String::new(),
proc_id: 1,
items,
default_item,
cancel_item: 0,
edit_text: String::new(),
edit_item: 0,
saved_pixels,
// Alert is a Pascal FUNCTION with a 6-byte argument frame
// (filterProc + alertID) and a 2-byte result slot at SP+6. The
// shared dialog completion code writes A7 as stack_ptr + 8, so
// store SP-2 for alerts.
stack_ptr: sp.wrapping_sub(2),
item_hit_ptr: sp + 6,
rendered_pixels,
flash_remaining: 0,
flash_delay: 0,
flash_item: 0,
edit_text_modified: false,
draw_proc_queue: VecDeque::new(),
draw_procs_done: true,
rendered_pixels_final: true,
filter_proc: 0,
game_managed: false,
last_filter_event: None,
popup_draws: Vec::new(),
active_popup: None,
active_button: None,
active_user_item: None,
});
true
}
fn finish_interactive_alert<C: CpuOps>(
&mut self,
cpu: &mut C,
bus: &mut MacMemoryBus,
hit: i16,
) {
let Some(saved) = self.dialog_tracking.take() else {
return;
};
if saved.item_hit_ptr != 0 {
bus.write_word(saved.item_hit_ptr, hit as u16);
}
let stack_after = saved.stack_ptr + 8;
let dialog_ptr = saved.dialog_ptr;
self.dialog_saved_pixels
.insert(dialog_ptr, saved.saved_pixels.clone());
self.close_dialog_window(bus, cpu, dialog_ptr, true);
cpu.write_reg(Register::A7, stack_after);
}
fn handle_interactive_alert_refire<C: CpuOps>(&mut self, cpu: &mut C, bus: &mut MacMemoryBus) {
if self.dialog_tracking.is_none() {
return;
}
if self
.dialog_tracking
.as_ref()
.is_some_and(|tracking| tracking.active_button.is_some())
{
self.handle_dialog_button_tracking(bus);
return;
}
if self
.dialog_tracking
.as_ref()
.is_some_and(|tracking| tracking.flash_remaining > 0)
{
let (remaining, button_draw, finished_hit) = {
let t = self.dialog_tracking.as_mut().unwrap();
if t.flash_delay > 0 {
t.flash_delay -= 1;
return;
}
t.flash_remaining -= 1;
t.flash_delay = 3;
let remaining = t.flash_remaining;
let flash_item = t.flash_item;
let button_draw =
if remaining > 0 && flash_item > 0 && (flash_item as usize) <= t.items.len() {
let item = &t.items[(flash_item - 1) as usize];
Some((
Self::dialog_item_screen_rect(t.bounds, item.rect),
item.text.clone(),
flash_item == t.default_item,
remaining % 2 == 0,
))
} else {
None
};
(remaining, button_draw, flash_item)
};
if let Some((screen_rect, title, is_default, highlighted)) = button_draw {
self.draw_dialog_button_highlight_state(
bus,
screen_rect,
&title,
is_default,
highlighted,
);
}
if remaining == 0 {
self.finish_interactive_alert(cpu, bus, finished_hit);
}
return;
}
let event = if !self.event_queue.is_empty() {
let mut event = None;
while let Some(e) = self.event_queue.pop_front() {
match e.what {
1 | 2 | 3 | 6 => {
event = Some(e);
break;
}
_ => {}
}
}
event
} else {
None
};
let Some(event) = event else {
return;
};
match event.what {
1 => {
let (dialog_ptr, bounds, items, default_item) = {
let tracking = self.dialog_tracking.as_ref().unwrap();
(
tracking.dialog_ptr,
tracking.bounds,
tracking.items.clone(),
tracking.default_item,
)
};
let mut hit = self.dialog_item_hit_test(
bus,
&items,
bounds,
event.where_v,
event.where_h,
&self.dialog_popup_original_rects,
dialog_ptr,
);
if hit <= 0 {
hit =
Self::dialog_button_hit_test(&items, bounds, event.where_v, event.where_h);
}
if hit <= 0 {
return;
}
let item = &items[(hit - 1) as usize];
let base_type = item.item_type & 0x7F;
let is_disabled = (item.item_type & 0x80) != 0;
if base_type != 4 || is_disabled {
return;
}
let rect = Self::dialog_item_screen_rect(bounds, item.rect);
let is_default = hit == default_item;
self.draw_dialog_button_highlight_state(bus, rect, &item.text, is_default, true);
if self.mouse_button {
let tracking = self.dialog_tracking.as_mut().unwrap();
tracking.active_button = Some(super::dispatch::DialogButtonTrackingState {
item_no: hit,
rect: item.rect,
title: item.text.clone(),
is_default,
highlighted: true,
});
} else {
self.start_dialog_button_flash(
bus, bounds, hit, item.rect, &item.text, is_default, true,
);
}
}
3 => {
let (default_item, cancel_item) = self
.dialog_tracking
.as_ref()
.map(|tracking| (tracking.default_item, tracking.cancel_item))
.unwrap_or((0, 0));
let key_code = (event.message >> 8) as u16;
let char_code = (event.message & 0xFF) as u8;
let hit = if char_code == b'\r' || char_code == 0x03 {
default_item
} else if char_code == 0x1B || key_code == 0x35 {
cancel_item
} else {
0
};
if hit > 0 {
self.finish_interactive_alert(cpu, bus, hit);
}
}
6 => {
let Some(tracking) = self.dialog_tracking.as_ref() else {
return;
};
let dialog_ptr = tracking.dialog_ptr;
let bounds = tracking.bounds;
let items = tracking.items.clone();
let default_item = tracking.default_item;
self.begin_update_window(bus, dialog_ptr);
self.set_current_port_state(bus, cpu, dialog_ptr, None);
self.redraw_standard_dialog_items(
bus,
bounds,
&items,
default_item,
"",
0,
dialog_ptr,
);
let rendered = self.save_dialog_pixels(bus, bounds);
if let Some(tracking) = self.dialog_tracking.as_mut() {
tracking.rendered_pixels = rendered;
tracking.rendered_pixels_final = true;
}
self.end_update_window(bus, dialog_ptr);
}
_ => {}
}
}
/// Parse a DITL resource from guest memory into a list of DialogItems.
/// Inside Macintosh Volume I, I-439
fn parse_ditl(bus: &MacMemoryBus, ptr: u32, data_len: u32) -> Vec<DialogItem> {
if data_len < 2 {
return Vec::new();
}
let max_index = bus.read_word(ptr) as i16; // number of items minus 1
let count = if max_index < 0 {
0
} else {
max_index as usize + 1
};
let mut items = Vec::with_capacity(count);
let mut offset = 2u32; // skip dlgMaxIndex
for _ in 0..count {
if offset + 14 > data_len {
break;
}
// Read 4-byte handle/procPtr field.
// For userItem types, the game may write a procedure pointer here
// via SetDItem or direct memory manipulation.
// Inside Macintosh Volume I, I-427
let item_handle = bus.read_long(ptr + offset);
offset += 4;
// Read display rectangle
let top = bus.read_word(ptr + offset) as i16;
let left = bus.read_word(ptr + offset + 2) as i16;
let bottom = bus.read_word(ptr + offset + 4) as i16;
let right = bus.read_word(ptr + offset + 6) as i16;
offset += 8;
// Read type byte and data length
let item_type = bus.read_byte(ptr + offset);
let data_len_byte = bus.read_byte(ptr + offset + 1);
offset += 2;
let base_type = item_type & 0x7F; // strip itemDisable bit
let mut text = String::new();
let mut resource_id: i16 = 0;
let remaining = data_len - offset;
let Some(payload_len) =
Self::ditl_item_payload_len(base_type, data_len_byte, remaining)
else {
break;
};
if matches!(base_type, 7 | 32 | 64) {
resource_id = bus.read_word(ptr + offset) as i16;
}
let padded = (payload_len + 1) & !1;
if padded > remaining {
break;
}
if payload_len > 0 {
match base_type {
// button (4), checkbox (5), radio (6), statText (8),
// editText (16): title/text data. IM:I I-427.
4 | 5 | 6 | 8 | 16 => {
let bytes = bus.read_bytes(ptr + offset, payload_len as usize);
text = decode_mac_roman_for_render(&bytes);
}
_ => {}
}
}
// Advance past data, padded to even boundary
offset += padded;
// For userItems, itmhand is a ProcPtr, not a relocatable handle.
// Dialog Manager preserves it in the duplicated DITL and calls it
// during update/redraw. Some apps replace it later via SetDItem or
// by writing the duplicated DITL directly.
// Inside Macintosh Volume I, I-405, I-426 to I-427.
let proc_ptr = if base_type == 0 { item_handle } else { 0 };
items.push(DialogItem {
item_type,
rect: (top, left, bottom, right),
text,
resource_id,
proc_ptr,
sel_start: 0,
sel_end: 0,
});
}
items
}
fn ditl_item_payload_len(base_type: u8, data_len_byte: u8, remaining: u32) -> Option<u32> {
let payload_len = match base_type {
// userItem has only the byte after itmtype (reserved/empty),
// not a following payload.
0 => 0,
// Help items use the byte after itmtype as a sized payload.
// Macintosh Toolbox Essentials 1992, p. 6-154
1 => u32::from(data_len_byte),
// icon, picture, resCtrl: 2-byte resource ID. IM:I I-427
// describes the byte after itmtype as length=2; MTE 1992
// p. 6-153 documents the same compiled records as a
// reserved byte plus the two-byte resource ID. Accept both.
7 | 32 | 64 => {
if remaining < 2 {
return None;
}
if data_len_byte >= 2 {
u32::from(data_len_byte)
} else {
2
}
}
_ => u32::from(data_len_byte),
};
Some(payload_len)
}
/// Re-read userItem proc pointers from the DITL data in guest memory.
/// The game may write proc pointers directly to the DITL handle data
/// after GetNewDialog returns, bypassing SetDItem.
/// Inside Macintosh Volume I, I-427
fn refresh_ditl_proc_ptrs(bus: &MacMemoryBus, dialog_ptr: u32, items: &mut [DialogItem]) {
// Read the items handle from the DialogRecord (offset 156)
let items_handle = bus.read_long(dialog_ptr + 156);
if items_handle == 0 {
return;
}
let ditl_ptr = bus.read_long(items_handle);
if ditl_ptr == 0 {
return;
}
// Walk the DITL data structure, reading the 4-byte itmhand field
// for each item. Format: 2-byte count-1, then per item:
// 4 bytes: itmhand (handle or proc pointer)
// 8 bytes: itmr (Rect)
// 1 byte: itmtype
// 1 byte: itmlen
// itmlen bytes: data (padded to even)
let mut offset = 2u32; // skip count word
for item in items.iter_mut() {
let handle = bus.read_long(ditl_ptr + offset);
offset += 4; // itmhand
offset += 8; // itmr (Rect)
let _item_type = bus.read_byte(ditl_ptr + offset);
let data_len = bus.read_byte(ditl_ptr + offset + 1) as u32;
offset += 2; // itmtype + itmlen
let padded = (data_len + 1) & !1;
offset += padded;
let base_type = item.item_type & 0x7F;
if base_type == 0 {
item.proc_ptr = handle;
}
}
}
fn duplicate_handle_data(bus: &mut MacMemoryBus, handle: u32) -> u32 {
if handle == 0 {
return 0;
}
let data_ptr = bus.read_long(handle);
let new_handle = bus.alloc(4);
if data_ptr == 0 {
bus.write_long(new_handle, 0);
return new_handle;
}
let size = bus.get_alloc_size(data_ptr).unwrap_or(0);
let new_data_ptr = bus.alloc(size);
for offset in 0..size {
bus.write_byte(new_data_ptr + offset, bus.read_byte(data_ptr + offset));
}
bus.write_long(new_handle, new_data_ptr);
new_handle
}
fn dispose_dialog_handle_storage(&mut self, bus: &mut MacMemoryBus, handle: u32) {
if handle == 0 {
return;
}
let resource_backing = self.loaded_handles.get(&handle).copied();
self.detached_handles.remove(&handle);
self.forget_resource_handle_index_for_handle(handle);
self.loaded_handles.remove(&handle);
self.resource_handle_files.remove(&handle);
self.detached_handle_files.remove(&handle);
self.handle_state_bits.remove(&handle);
let data_ptr = bus.read_long(handle);
if resource_backing.is_none() {
bus.free(data_ptr);
}
bus.free(handle);
}
fn dispose_dialog_te_storage(&mut self, bus: &mut MacMemoryBus, te_handle: u32) {
if te_handle == 0 {
return;
}
let te_ptr = bus.read_long(te_handle);
if te_ptr != 0 {
let h_text = bus.read_long(te_ptr + Self::TE_HTEXT_OFFSET);
self.dispose_dialog_handle_storage(bus, h_text);
if Self::te_is_styled_record(bus, te_ptr) {
let style_handle = bus.read_long(te_ptr + Self::TE_TX_FONT_OFFSET);
self.dispose_dialog_handle_storage(bus, style_handle);
}
bus.free(te_ptr);
}
bus.free(te_handle);
self.textedit_states.remove(&te_handle);
}
fn dispose_dialog_control_storage(&mut self, bus: &mut MacMemoryBus, ctrl_handle: u32) {
if ctrl_handle == 0 {
return;
}
let ctrl_ptr = bus.read_long(ctrl_handle);
if ctrl_ptr != 0 {
let owner = bus.read_long(ctrl_ptr + 4);
if owner != 0 {
let mut prev_handle = 0u32;
let mut cur_handle = bus.read_long(owner + 140);
while cur_handle != 0 {
let cur_ptr = bus.read_long(cur_handle);
if cur_ptr == 0 {
break;
}
if cur_handle == ctrl_handle {
let next = bus.read_long(cur_ptr);
if prev_handle == 0 {
bus.write_long(owner + 140, next);
} else {
let prev_ptr = bus.read_long(prev_handle);
if prev_ptr != 0 {
bus.write_long(prev_ptr, next);
}
}
break;
}
prev_handle = cur_handle;
cur_handle = bus.read_long(cur_ptr);
}
}
self.release_control_aux_record(bus, ctrl_handle);
self.control_proc_ids.remove(&ctrl_ptr);
bus.free(ctrl_ptr);
}
bus.free(ctrl_handle);
}
fn dialog_item_storage_handles_from_ditl(
bus: &MacMemoryBus,
dialog_ptr: u32,
) -> Vec<(u8, u32)> {
let items_handle = bus.read_long(dialog_ptr + 156);
if items_handle == 0 {
return Vec::new();
}
let ditl_ptr = bus.read_long(items_handle);
if ditl_ptr == 0 {
return Vec::new();
}
let data_len = bus.get_alloc_size(ditl_ptr).unwrap_or(0);
if data_len < 2 {
return Vec::new();
}
let max_index = bus.read_word(ditl_ptr) as i16;
if max_index < 0 {
return Vec::new();
}
let mut handles = Vec::new();
let mut offset = 2u32;
for _ in 0..=max_index {
if offset + 14 > data_len {
break;
}
let item_handle = bus.read_long(ditl_ptr + offset);
offset += 12;
let item_type = bus.read_byte(ditl_ptr + offset);
let data_len_byte = bus.read_byte(ditl_ptr + offset + 1);
offset += 2;
let base_type = item_type & 0x7F;
handles.push((base_type, item_handle));
let payload_len = match base_type {
0 => 0,
7 | 32 | 64 if data_len_byte < 2 => 2,
_ => u32::from(data_len_byte),
};
let padded = (payload_len + 1) & !1;
if padded > data_len.saturating_sub(offset) {
break;
}
offset += padded;
}
handles
}
fn clear_dialog_scoped_item_state(&mut self, dialog_ptr: u32) {
self.dialog_items.remove(&dialog_ptr);
self.dialog_item_handles
.retain(|_, (dlg, _)| *dlg != dialog_ptr);
self.dialog_control_handles
.retain(|_, (dlg, _)| *dlg != dialog_ptr);
self.dialog_control_values
.retain(|(dlg, _), _| *dlg != dialog_ptr);
self.dialog_edit_text_modified_items
.retain(|(dlg, _)| *dlg != dialog_ptr);
self.hidden_dialog_item_rects
.retain(|(dlg, _), _| *dlg != dialog_ptr);
self.dialog_item_popup_menus
.retain(|(dlg, _), _| *dlg != dialog_ptr);
self.dialog_popup_original_rects
.retain(|(dlg, _), _| *dlg != dialog_ptr);
self.dialog_popup_candidate_items
.retain(|(dlg, _)| *dlg != dialog_ptr);
if self
.pending_dialog_popup_menu
.is_some_and(|pending| pending.dialog_ptr == dialog_ptr)
{
self.pending_dialog_popup_menu = None;
}
self.dialog_cancel_items.remove(&dialog_ptr);
}
fn dispose_dialog_owned_items(&mut self, bus: &mut MacMemoryBus, dialog_ptr: u32) {
let can_read_dialog_record =
self.dialog_items.contains_key(&dialog_ptr) || self.window_list.contains(&dialog_ptr);
let mut text_handles = Vec::new();
let mut control_handles = Vec::new();
if can_read_dialog_record {
for (base_type, handle) in Self::dialog_item_storage_handles_from_ditl(bus, dialog_ptr)
{
match base_type {
8 | 16 => text_handles.push(handle),
4..=7 => control_handles.push(handle),
_ => {}
}
}
}
text_handles.extend(
self.dialog_item_handles
.iter()
.filter_map(|(&handle, &(dlg, _))| {
if dlg == dialog_ptr {
Some(handle)
} else {
None
}
}),
);
control_handles.extend(self.dialog_control_handles.iter().filter_map(
|(&handle, &(dlg, _))| {
if dlg == dialog_ptr {
Some(handle)
} else {
None
}
},
));
text_handles.sort_unstable();
text_handles.dedup();
control_handles.sort_unstable();
control_handles.dedup();
for handle in text_handles {
self.dispose_dialog_handle_storage(bus, handle);
}
for handle in control_handles {
self.dispose_dialog_control_storage(bus, handle);
}
if can_read_dialog_record {
let text_h = bus.read_long(dialog_ptr + 160);
self.dispose_dialog_te_storage(bus, text_h);
bus.write_long(dialog_ptr + 160, 0);
}
self.clear_dialog_scoped_item_state(dialog_ptr);
}
fn dispose_dialog_record_and_item_list(&mut self, bus: &mut MacMemoryBus, dialog_ptr: u32) {
if !self.dialog_items.contains_key(&dialog_ptr) && !self.window_list.contains(&dialog_ptr) {
return;
}
let items_handle = bus.read_long(dialog_ptr + 156);
self.dispose_dialog_handle_storage(bus, items_handle);
bus.free(dialog_ptr);
}
pub(crate) fn dialog_screen_bounds(
bus: &MacMemoryBus,
dialog_ptr: u32,
) -> (i16, i16, i16, i16) {
let port_height =
bus.read_word(dialog_ptr + 20) as i16 - bus.read_word(dialog_ptr + 16) as i16;
let port_width =
bus.read_word(dialog_ptr + 22) as i16 - bus.read_word(dialog_ptr + 18) as i16;
let port_version = bus.read_word(dialog_ptr + 6);
let is_cgraf = (port_version & 0xC000) == 0xC000;
let (top, left) = if is_cgraf {
let pixmap_handle = bus.read_long(dialog_ptr + 2);
let pixmap_ptr = if pixmap_handle != 0 {
bus.read_long(pixmap_handle)
} else {
0
};
if pixmap_ptr != 0 {
(
-(bus.read_word(pixmap_ptr + 6) as i16),
-(bus.read_word(pixmap_ptr + 8) as i16),
)
} else {
(0, 0)
}
} else {
(
-(bus.read_word(dialog_ptr + 8) as i16),
-(bus.read_word(dialog_ptr + 10) as i16),
)
};
(top, left, top + port_height, left + port_width)
}
fn dialog_edit_state(
bus: &MacMemoryBus,
dialog_ptr: u32,
items: &[DialogItem],
) -> (String, i16, i16) {
let default_item = match bus.read_word(dialog_ptr + 168) as i16 {
value if value > 0 => value,
_ => 1,
};
let edit_field = bus.read_word(dialog_ptr + 164) as i16;
let stored_edit_item = if edit_field >= 0 { edit_field + 1 } else { 0 };
let stored_is_valid = stored_edit_item > 0
&& items
.get((stored_edit_item - 1) as usize)
.is_some_and(|item| (item.item_type & 0x7F) == 16 && (item.item_type & 0x80) == 0);
let edit_item = if stored_is_valid {
stored_edit_item
} else {
items
.iter()
.position(|item| (item.item_type & 0x7F) == 16 && (item.item_type & 0x80) == 0)
.map(|idx| (idx + 1) as i16)
.unwrap_or(0)
};
if edit_item <= 0 {
return (String::new(), 0, default_item);
}
let edit_text = Self::text_item_string_from_handle_if_present(
bus,
Self::dialog_item_handle(bus, dialog_ptr, edit_item),
);
if let Some(edit_text) = edit_text {
return (edit_text, edit_item, default_item);
}
let fallback = items
.get((edit_item - 1) as usize)
.map(|item| item.text.clone())
.unwrap_or_default();
(fallback, edit_item, default_item)
}
fn sync_tracking_active_edit_item(tracking: &mut DialogTrackingState) {
let edit_item = tracking.edit_item;
if edit_item <= 0 {
return;
}
if let Some(item) = tracking.items.get_mut((edit_item - 1) as usize) {
if (item.item_type & 0x7F) == 16 {
item.text = tracking.edit_text.clone();
}
}
}
fn set_tracking_active_edit_selection(
tracking: &mut DialogTrackingState,
sel_start: usize,
sel_end: usize,
) {
let edit_item = tracking.edit_item;
if edit_item <= 0 {
return;
}
if let Some(item) = tracking.items.get_mut((edit_item - 1) as usize) {
if (item.item_type & 0x7F) == 16 {
let text_len = tracking.edit_text.len();
item.sel_start = sel_start.min(text_len).min(i16::MAX as usize) as i16;
item.sel_end = sel_end.min(text_len).min(i16::MAX as usize) as i16;
}
}
}
fn textedit_key_result(
existing: &[u8],
sel_start: usize,
sel_end: usize,
key: u8,
) -> (Vec<u8>, usize) {
let text_len = existing.len();
let s = sel_start.min(text_len);
let e = sel_end.min(text_len);
let (s, e) = if s > e { (e, s) } else { (s, e) };
if key == 0x08 {
if s != e {
let mut merged = Vec::with_capacity(text_len - (e - s));
merged.extend_from_slice(&existing[..s]);
merged.extend_from_slice(&existing[e..]);
return (merged, s);
}
if s > 0 {
let mut merged = Vec::with_capacity(text_len - 1);
merged.extend_from_slice(&existing[..s - 1]);
merged.extend_from_slice(&existing[s..]);
return (merged, s - 1);
}
return (existing.to_vec(), s);
}
let mut merged = Vec::with_capacity(s + 1 + text_len.saturating_sub(e));
merged.extend_from_slice(&existing[..s]);
merged.push(key);
merged.extend_from_slice(&existing[e..]);
(merged, s + 1)
}
fn apply_dialog_select_key_to_edit_item(
&mut self,
bus: &mut MacMemoryBus,
dialog_ptr: u32,
items: &mut [DialogItem],
edit_item: i16,
key: u8,
) -> bool {
if edit_item <= 0 {
return false;
}
let Some(item) = items.get_mut((edit_item - 1) as usize) else {
return false;
};
let base_type = item.item_type & 0x7F;
let is_disabled = (item.item_type & 0x80) != 0;
if base_type != 16 || is_disabled {
return false;
}
let item_handle = Self::dialog_item_handle(bus, dialog_ptr, edit_item);
let existing = Self::text_item_bytes_from_handle_if_present(bus, item_handle)
.unwrap_or_else(|| item.text.as_bytes().to_vec());
let text_handle = bus.read_long(dialog_ptr + 160);
let te_ptr = Self::te_record_ptr(bus, text_handle);
let (sel_start, sel_end) = if te_ptr != 0 {
(
bus.read_word(te_ptr + Self::TE_SEL_START_OFFSET) as usize,
bus.read_word(te_ptr + Self::TE_SEL_END_OFFSET) as usize,
)
} else {
(item.sel_start.max(0) as usize, item.sel_end.max(0) as usize)
};
let (updated, insertion_point) =
Self::textedit_key_result(&existing, sel_start, sel_end, key);
let clamped_insertion = insertion_point.min(u16::MAX as usize) as u16;
item.text = decode_mac_roman_for_render(&updated);
item.sel_start = clamped_insertion as i16;
item.sel_end = clamped_insertion as i16;
if item_handle != 0 {
let len = updated.len().min(255);
let data_ptr = Self::ensure_text_handle_size(bus, item_handle, len);
if data_ptr != 0 && len > 0 {
bus.write_bytes(data_ptr, &updated[..len]);
}
}
if te_ptr != 0 {
self.te_set_text_contents(bus, text_handle, &updated);
bus.write_word(te_ptr + Self::TE_SEL_START_OFFSET, clamped_insertion);
bus.write_word(te_ptr + Self::TE_SEL_END_OFFSET, clamped_insertion);
}
true
}
fn activate_dialog_edit_item<C: CpuOps>(
&mut self,
bus: &mut MacMemoryBus,
cpu: &mut C,
dialog_ptr: u32,
items: &[DialogItem],
edit_item: i16,
) -> bool {
if edit_item <= 0 {
return false;
}
let Some(item) = items.get((edit_item - 1) as usize) else {
return false;
};
if (item.item_type & 0x7F) != 16 || (item.item_type & 0x80) != 0 {
return false;
}
bus.write_word(dialog_ptr + 164, (edit_item - 1) as u16);
let text_handle = bus.read_long(dialog_ptr + 160);
let te_ptr = Self::te_record_ptr(bus, text_handle);
if te_ptr == 0 {
return true;
}
let item_handle = Self::dialog_item_handle(bus, dialog_ptr, edit_item);
let text = Self::text_item_bytes_from_handle_if_present(bus, item_handle)
.unwrap_or_else(|| item.text.as_bytes().to_vec());
self.te_set_text_contents(bus, text_handle, &text);
let text_len = text.len().min(u16::MAX as usize);
let sel_start = item.sel_start.max(0) as usize;
let sel_end = item.sel_end.max(0) as usize;
bus.write_word(
te_ptr + Self::TE_SEL_START_OFFSET,
sel_start.min(text_len) as u16,
);
bus.write_word(
te_ptr + Self::TE_SEL_END_OFFSET,
sel_end.min(text_len) as u16,
);
// MTE 1992 p. 6-139 and IM:I I-417: DialogSelect mouse-down in an
// enabled editText item displays the insertion point or selection.
// The Dialog Manager shares one TERecord across editText items, so
// activating an item must make that record active before later null
// events call TEIdle for caret blinking.
bus.write_word(te_ptr + Self::TE_ACTIVE_OFFSET, 1);
bus.write_long(te_ptr + Self::TE_CARET_TIME_OFFSET, self.tick_count);
bus.write_word(te_ptr + Self::TE_CARET_STATE_OFFSET, 0);
self.draw_te_contents(cpu, bus, text_handle);
true
}
fn dialog_item_selection_range(item: &DialogItem, text_len: usize) -> Option<(usize, usize)> {
let start = (item.sel_start.max(0) as usize).min(text_len);
let end = (item.sel_end.max(0) as usize).min(text_len);
let (start, end) = if start <= end {
(start, end)
} else {
(end, start)
};
(start < end).then_some((start, end))
}
fn redraw_dialog_text_item(&mut self, bus: &mut MacMemoryBus, dialog_ptr: u32, item_no: i16) {
if !self.window_visible(bus, dialog_ptr) {
return;
}
let Some(items) = self.dialog_items.get(&dialog_ptr).cloned() else {
return;
};
let Some(item) = items.get((item_no - 1) as usize) else {
return;
};
let base_type = item.item_type & 0x7F;
if base_type != 8 && base_type != 16 {
return;
}
let bounds = Self::dialog_screen_bounds(bus, dialog_ptr);
let (top, left, bottom, right) = bounds;
let (it, il, ib, ir) = item.rect;
let abs_top = top + it;
let abs_left = left + il;
let abs_bottom = top + ib;
let abs_right = left + ir;
if abs_top >= bottom || abs_bottom <= top || abs_left >= right || abs_right <= left {
return;
}
let (edit_text, edit_item, default_item) = Self::dialog_edit_state(bus, dialog_ptr, &items);
self.redraw_standard_dialog_items(
bus,
bounds,
&items,
default_item,
&edit_text,
edit_item,
dialog_ptr,
);
if self.dialog_visible_snapshots.contains_key(&dialog_ptr) {
let pixels = self.save_dialog_pixels(bus, bounds);
self.dialog_visible_snapshots
.insert(dialog_ptr, PersistentDialogSnapshot { bounds, pixels });
}
self.capture_gui_frame(
bus,
&format!("set_dialog_item_text_{:08X}_{}", dialog_ptr, item_no),
);
}
fn flush_dialog_edit_item_texts(
&mut self,
bus: &mut MacMemoryBus,
dialog_ptr: u32,
items: &[DialogItem],
active_edit_item: i16,
active_edit_text: &str,
) {
for (idx, item) in items.iter().enumerate() {
if (item.item_type & 0x7F) != 16 {
continue;
}
let item_no = (idx + 1) as i16;
let text = if item_no == active_edit_item {
active_edit_text
} else {
&item.text
};
let bytes = text.as_bytes();
let len = bytes.len().min(255);
let item_handle = Self::dialog_item_handle(bus, dialog_ptr, item_no);
if item_handle != 0 {
let data_ptr = Self::ensure_text_handle_size(bus, item_handle, len);
if data_ptr != 0 && len > 0 {
bus.write_bytes(data_ptr, &bytes[..len]);
}
}
if let Some(cached_items) = self.dialog_items.get_mut(&dialog_ptr) {
if idx < cached_items.len() {
cached_items[idx].text = text.to_string();
cached_items[idx].sel_start = item.sel_start;
cached_items[idx].sel_end = item.sel_end;
}
}
}
}
fn finish_dialog_creation<C: CpuOps>(
&mut self,
bus: &mut MacMemoryBus,
cpu: &mut C,
storage_ptr: u32,
bounds: (i16, i16, i16, i16),
title: &str,
visible: bool,
proc_id: i16,
go_away_flag: bool,
ref_con: u32,
items_handle: u32,
items: Vec<DialogItem>,
) -> u32 {
let dlg_ptr = if storage_ptr != 0 {
storage_ptr
} else {
// GetNewDialog/NewDialog allocate storage when dStorage is NIL
// (IM:I I-424, I-412). System 7's Memory Manager zone layout
// gives manager-owned records stable low-byte placement that
// some 68K apps accidentally depend on; keep that shape local
// to Dialog Manager records instead of changing all heap blocks.
bus.alloc_aligned(170, MANAGER_DIALOG_RECORD_ALIGNMENT)
};
self.window_stack.push((
self.front_window,
self.window_bounds,
self.window_proc_id,
self.window_title.clone(),
));
let screen_base: u32 = bus.read_long(0x0824);
self.init_cgraf_window(
bus,
cpu,
dlg_ptr,
screen_base,
bounds.0,
bounds.1,
bounds.2,
bounds.3,
title,
proc_id,
visible,
false,
go_away_flag,
ref_con,
);
self.set_current_port_state(bus, cpu, dlg_ptr, None);
// DialogRecord starts with a WindowRecord; +108 is windowKind,
// not the WDEF procID. Dialog boxes and alerts must use
// dialogKind=2. The WDEF procID is retained in window_proc_ids.
// Inside Macintosh Volume I, I-407..I-408, I-273; Volume VI, 11-42.
bus.write_word(dlg_ptr + 108, 2);
bus.write_long(dlg_ptr + 156, items_handle);
// SetDAFont / SetDialogFont affect subsequently created dialog and
// alert grafPorts; assembly callers may set DlgFont directly.
// Inside Macintosh Volume I, I-412; MTE 1992 p. 6-104.
let dialog_font = bus.read_word(crate::memory::globals::addr::DLG_FONT) as i16;
bus.write_word(dlg_ptr + 68, dialog_font as u16);
self.tx_font = dialog_font;
if let Some(state) = self.port_draw_states.get_mut(&dlg_ptr) {
state.tx_font = dialog_font;
}
let text_h = Self::allocate_te_handle(bus);
bus.write_long(dlg_ptr + 160, text_h);
bus.write_word(dlg_ptr + 164, 0xFFFF); // editField = -1
bus.write_word(dlg_ptr + 166, 0); // editOpen
bus.write_word(dlg_ptr + 168, 1); // aDefItem
self.initialize_dialog_item_handles(bus, dlg_ptr, &items);
self.dialog_items.insert(dlg_ptr, items.clone());
if visible {
// NewDialog/GetNewDialog create and display visible dialogs
// immediately. userItem contents remain app-owned and are skipped
// until DrawDialog/ModalDialog/update callbacks can run, but the
// standard shell, controls, text, and resource items must still be
// visible. Inside Macintosh Volume I, I-405, I-412, I-421.
let (edit_text, edit_item, default_item) =
Self::dialog_edit_state(bus, dlg_ptr, &items);
self.draw_dialog(
bus,
bounds,
proc_id,
title,
&items,
default_item,
&edit_text,
edit_item,
false,
dlg_ptr,
);
if !Self::dialog_is_game_managed(bounds, &items) {
let pixels = self.save_dialog_pixels(bus, bounds);
self.dialog_visible_snapshots
.insert(dlg_ptr, PersistentDialogSnapshot { bounds, pixels });
}
for item in &items {
if Self::dialog_item_intersects_bounds(bounds, item) {
self.validate_window_rect(bus, dlg_ptr, item.rect);
}
}
self.queue_window_update_event(dlg_ptr);
self.capture_gui_frame(bus, &format!("get_new_dialog_initial_{:08X}", dlg_ptr));
} else {
self.dialog_initial_draw_deferred.remove(&dlg_ptr);
}
dlg_ptr
}
// ========== Dialog Drawing Helpers ==========
/// Save framebuffer pixels under a dialog region (including shadow).
/// Supports both 1bpp and 8bpp modes.
pub(crate) fn save_dialog_pixels(
&self,
bus: &MacMemoryBus,
rect: (i16, i16, i16, i16),
) -> Vec<u8> {
let (screen_base, row_bytes, _, screen_h, pixel_size) = self.get_screen_params();
let (save_top, save_left, save_bottom, save_right) = Self::dialog_saved_pixel_rect(rect);
// Guard against negative y (top < 5) and y >= screen_h. `y as u32`
// sign-extends a negative i16 to a huge value that overflows when
// multiplied by row_bytes. Off-screen rows contribute zeros.
let row_width = (save_right - save_left) as usize;
let row_count = (save_bottom - save_top) as usize;
let mut saved = Vec::new();
let screen_h_i16 = screen_h;
for y in save_top..save_bottom {
let y_on_screen = y >= 0 && y < screen_h_i16;
if pixel_size == 8 {
let on_screen_row =
y_on_screen && save_left >= 0 && (save_right as u32) <= row_bytes;
if on_screen_row {
// Pre-allocate capacity on first iteration.
if saved.capacity() == 0 {
saved.reserve(row_count * row_width);
}
let start = saved.len();
saved.resize(start + row_width, 0);
let row_addr = screen_base + (y as u32) * row_bytes + (save_left as u32);
bus.read_bytes_into(row_addr, &mut saved[start..start + row_width]);
} else if y_on_screen {
for x in save_left..save_right {
if x < 0 || (x as u32) >= row_bytes {
saved.push(0);
} else {
let addr = screen_base + (y as u32) * row_bytes + (x as u32);
saved.push(bus.read_byte(addr));
}
}
} else {
// Entire row off-screen — pad with zeros.
saved.resize(saved.len() + row_width, 0);
}
} else if y_on_screen {
let byte_left = (save_left.max(0) as u32) / 8;
let byte_right = (save_right as u32).div_ceil(8);
let bx_end = byte_right.min(row_bytes);
if byte_left < bx_end {
let len = (bx_end - byte_left) as usize;
let start = saved.len();
saved.resize(start + len, 0);
let row_addr = screen_base + (y as u32) * row_bytes + byte_left;
bus.read_bytes_into(row_addr, &mut saved[start..start + len]);
}
}
// 1bpp off-screen row: silently produces nothing — the
// byte_left < bx_end check + row_count-based saved.len tracking
// tolerates short rows.
}
saved
}
pub(crate) fn ensure_dialog_background_saved(
&mut self,
bus: &MacMemoryBus,
dialog_ptr: u32,
bounds: (i16, i16, i16, i16),
) {
if dialog_ptr == 0
|| !self.dialog_items.contains_key(&dialog_ptr)
|| self.dialog_saved_pixels.contains_key(&dialog_ptr)
{
return;
}
let background = self.save_dialog_pixels(bus, bounds);
self.dialog_saved_pixels.insert(dialog_ptr, background);
}
pub(crate) fn ensure_dialog_background_saved_for_screen_port(
&mut self,
bus: &MacMemoryBus,
port: u32,
) {
if port == 0 || !self.dialog_items.contains_key(&port) {
return;
}
let bounds = Self::dialog_screen_bounds(bus, port);
self.ensure_dialog_background_saved(bus, port, bounds);
}
fn dialog_saved_pixel_rect(rect: (i16, i16, i16, i16)) -> (i16, i16, i16, i16) {
let (top, left, bottom, right) = rect;
let margin = Self::DBOX_FRAME_MARGIN;
(top - margin, left - margin, bottom + margin, right + margin)
}
pub(crate) fn refresh_dialog_saved_pixels_after_screen_draw(
&mut self,
bus: &MacMemoryBus,
drawing_port: u32,
screen_rect: (i16, i16, i16, i16),
) {
if screen_rect.0 >= screen_rect.2 || screen_rect.1 >= screen_rect.3 {
return;
}
let mut dialogs: Vec<(u32, (i16, i16, i16, i16))> = self
.dialog_visible_snapshots
.iter()
.map(|(&dialog_ptr, snapshot)| (dialog_ptr, snapshot.bounds))
.collect();
if let Some(tracking) = self.dialog_tracking.as_ref() {
if !dialogs
.iter()
.any(|(dialog_ptr, _)| *dialog_ptr == tracking.dialog_ptr)
{
dialogs.push((tracking.dialog_ptr, tracking.bounds));
}
}
for (dialog_ptr, bounds) in dialogs {
let drawing_active_modal_dialog =
self.dialog_tracking.as_ref().is_some_and(|tracking| {
tracking.dialog_ptr == dialog_ptr && dialog_ptr == drawing_port
});
let drawing_premodal_dialog =
dialog_ptr == drawing_port && !self.dialog_modal_entered.contains(&dialog_ptr);
if drawing_active_modal_dialog
|| drawing_premodal_dialog
|| self.active_modeless_dialog_draw_proc == Some(dialog_ptr)
{
continue;
}
self.refresh_single_dialog_saved_pixels_after_screen_draw(
bus,
dialog_ptr,
bounds,
screen_rect,
);
}
}
fn refresh_single_dialog_saved_pixels_after_screen_draw(
&mut self,
bus: &MacMemoryBus,
dialog_ptr: u32,
bounds: (i16, i16, i16, i16),
screen_rect: (i16, i16, i16, i16),
) {
let save_rect = Self::dialog_saved_pixel_rect(bounds);
let Some(intersection) = Self::rect_intersection(save_rect, screen_rect) else {
return;
};
let (screen_base, row_bytes, screen_w, screen_h, pixel_size) = self.get_screen_params();
let Some(saved) = self.dialog_saved_pixels.get_mut(&dialog_ptr) else {
return;
};
let (save_top, save_left, save_bottom, save_right) = save_rect;
let row_width = save_right.saturating_sub(save_left) as usize;
let row_count = save_bottom.saturating_sub(save_top) as usize;
if row_width == 0 || row_count == 0 {
return;
}
match pixel_size {
8 => {
let expected = row_width.saturating_mul(row_count);
if saved.len() < expected {
return;
}
let top = intersection.0.max(0);
let left = intersection.1.max(0);
let bottom = intersection.2.min(screen_h);
let right = intersection.3.min(screen_w).min(row_bytes as i16);
if top >= bottom || left >= right {
return;
}
for y in top..bottom {
let saved_offset =
(y - save_top) as usize * row_width + (left - save_left) as usize;
let len = (right - left) as usize;
let row_addr = screen_base + (y as u32) * row_bytes + (left as u32);
let row = bus.read_bytes(row_addr, len);
saved[saved_offset..saved_offset + len].copy_from_slice(&row);
}
}
1 => {
if save_right <= 0 {
return;
}
let byte_left = (save_left.max(0) as u32) / 8;
let byte_right = (save_right as u32).div_ceil(8);
let byte_end = byte_right.min(row_bytes);
if byte_left >= byte_end {
return;
}
let row_len = (byte_end - byte_left) as usize;
let first_saved_row = save_top.max(0);
let top = intersection.0.max(0);
let left = intersection.1.max(0);
let bottom = intersection.2.min(screen_h);
let right = intersection
.3
.min(screen_w)
.min((row_bytes.saturating_mul(8)) as i16);
if top >= bottom || left >= right {
return;
}
for y in top..bottom {
let row_offset = (y - first_saved_row) as usize * row_len;
if row_offset + row_len > saved.len() {
return;
}
for x in left..right {
let byte_x = (x as u32) / 8;
if byte_x < byte_left || byte_x >= byte_end {
continue;
}
let bit = 7 - ((x as u32) & 7);
let mask = 1u8 << bit;
let screen_byte =
bus.read_byte(screen_base + (y as u32) * row_bytes + byte_x);
let saved_idx = row_offset + (byte_x - byte_left) as usize;
if (screen_byte & mask) != 0 {
saved[saved_idx] |= mask;
} else {
saved[saved_idx] &= !mask;
}
}
}
}
_ => {}
}
}
/// Restore previously saved framebuffer pixels under a dialog.
pub(crate) fn restore_dialog_pixels(
&self,
bus: &mut MacMemoryBus,
rect: (i16, i16, i16, i16),
saved: &[u8],
) {
let (screen_base, row_bytes, _, screen_h, pixel_size) = self.get_screen_params();
let (save_top, save_left, save_bottom, save_right) = Self::dialog_saved_pixel_rect(rect);
// Mirror save_dialog_pixels y-bounds guard — saved bytes for off-screen
// rows are skipped so write position stays aligned with the packed input buffer.
let row_width = (save_right - save_left) as usize;
let mut idx = 0;
let screen_h_i16 = screen_h;
for y in save_top..save_bottom {
let y_on_screen = y >= 0 && y < screen_h_i16;
if pixel_size == 8 {
let on_screen_row =
y_on_screen && save_left >= 0 && (save_right as u32) <= row_bytes;
if on_screen_row && idx + row_width <= saved.len() {
let row_addr = screen_base + (y as u32) * row_bytes + (save_left as u32);
bus.write_bytes(row_addr, &saved[idx..idx + row_width]);
idx += row_width;
} else if y_on_screen {
for x in save_left..save_right {
if idx < saved.len() {
if x >= 0 && (x as u32) < row_bytes {
let addr = screen_base + (y as u32) * row_bytes + (x as u32);
bus.write_byte(addr, saved[idx]);
}
idx += 1;
}
}
} else {
// Off-screen row: skip the packed bytes that
// save_dialog_pixels padded in.
idx += row_width.min(saved.len() - idx);
}
} else if y_on_screen {
let byte_left = (save_left.max(0) as u32) / 8;
let byte_right = (save_right as u32).div_ceil(8);
let bx_end = byte_right.min(row_bytes);
if byte_left < bx_end {
let len = (bx_end - byte_left) as usize;
if idx + len <= saved.len() {
let row_addr = screen_base + (y as u32) * row_bytes + byte_left;
bus.write_bytes(row_addr, &saved[idx..idx + len]);
idx += len;
} else {
for bx in byte_left..bx_end {
if idx < saved.len() {
bus.write_byte(
screen_base + (y as u32) * row_bytes + bx,
saved[idx],
);
idx += 1;
}
}
}
}
}
// 1bpp off-screen: save produced no bytes for this row,
// so there's nothing to advance idx over here.
}
}
fn restore_dialog_pixels_outside_rect(
&self,
bus: &mut MacMemoryBus,
rect: (i16, i16, i16, i16),
saved: &[u8],
keep_rect: (i16, i16, i16, i16),
) {
let (screen_base, row_bytes, _, screen_h, pixel_size) = self.get_screen_params();
let (top, left, bottom, right) = rect;
let margin = Self::DBOX_FRAME_MARGIN;
let save_top = top - margin;
let save_left = left - margin;
let save_bottom = bottom + margin;
let save_right = right + margin;
let row_width = (save_right - save_left) as usize;
let mut idx = 0;
let screen_h_i16 = screen_h;
for y in save_top..save_bottom {
let y_on_screen = y >= 0 && y < screen_h_i16;
if pixel_size == 8 {
if y_on_screen {
for x in save_left..save_right {
if idx < saved.len() {
if (y < keep_rect.0
|| y >= keep_rect.2
|| x < keep_rect.1
|| x >= keep_rect.3)
&& x >= 0
&& (x as u32) < row_bytes
{
let addr = screen_base + (y as u32) * row_bytes + (x as u32);
bus.write_byte(addr, saved[idx]);
}
idx += 1;
}
}
} else {
idx += row_width.min(saved.len().saturating_sub(idx));
}
} else if y_on_screen {
let byte_left = (save_left.max(0) as u32) / 8;
let byte_right = (save_right as u32).div_ceil(8);
let bx_end = byte_right.min(row_bytes);
if byte_left < bx_end {
for bx in byte_left..bx_end {
if idx >= saved.len() {
break;
}
let mut restore_mask = 0u8;
for bit in 0..8u32 {
let x = (bx * 8 + bit) as i16;
if x >= save_left
&& x < save_right
&& (y < keep_rect.0
|| y >= keep_rect.2
|| x < keep_rect.1
|| x >= keep_rect.3)
{
restore_mask |= 0x80 >> bit;
}
}
if restore_mask != 0 {
let addr = screen_base + (y as u32) * row_bytes + bx;
let current = bus.read_byte(addr);
bus.write_byte(
addr,
(current & !restore_mask) | (saved[idx] & restore_mask),
);
}
idx += 1;
}
}
}
}
}
/// Save framebuffer pixels for an exact rectangle (no margin).
/// Guards off-screen y (y < 0 or y >= screen_h) from sign-extend overflow.
/// Off-screen rows pad 8bpp output with zeros; 1bpp output is short by that row.
fn save_rect_pixels(&self, bus: &MacMemoryBus, rect: (i16, i16, i16, i16)) -> Vec<u8> {
let (screen_base, row_bytes, _, screen_h, pixel_size) = self.get_screen_params();
let (top, left, bottom, right) = rect;
let row_width = (right - left).max(0) as usize;
let mut saved = Vec::with_capacity((bottom - top).max(0) as usize * row_width);
let screen_h_i16 = screen_h;
for y in top..bottom {
let y_on_screen = y >= 0 && y < screen_h_i16;
if pixel_size == 8 {
let on_screen_row = y_on_screen && left >= 0 && (right as u32) <= row_bytes;
if on_screen_row {
let row_addr = screen_base + (y as u32) * row_bytes + (left as u32);
saved.extend_from_slice(&bus.read_bytes(row_addr, row_width));
} else if y_on_screen {
for x in left..right {
if x < 0 || (x as u32) >= row_bytes {
saved.push(0);
} else {
saved.push(
bus.read_byte(screen_base + (y as u32) * row_bytes + (x as u32)),
);
}
}
} else {
saved.resize(saved.len() + row_width, 0);
}
} else if y_on_screen {
let byte_left = (left.max(0) as u32) / 8;
let byte_right = (right.max(0) as u32).div_ceil(8);
let bx_end = byte_right.min(row_bytes);
if byte_left < bx_end {
let len = (bx_end - byte_left) as usize;
let row_addr = screen_base + (y as u32) * row_bytes + byte_left;
saved.extend_from_slice(&bus.read_bytes(row_addr, len));
}
}
}
saved
}
fn saved_rect_has_non_background_content(&self, saved: &[u8]) -> bool {
saved.iter().any(|&byte| byte != 0)
}
fn user_item_preserve_rects(
&self,
dialog_ptr: u32,
bounds: (i16, i16, i16, i16),
items: &[DialogItem],
skip_popup_user_items: bool,
skip_disabled_placeholders: bool,
) -> Vec<((i16, i16, i16, i16), bool)> {
items
.iter()
.enumerate()
.filter(|(i, it)| {
(it.item_type & 0x7F) == 0
&& (!skip_disabled_placeholders
|| (it.item_type & 0x80) == 0
|| it.proc_ptr != 0)
&& (!skip_popup_user_items
|| !self
.dialog_item_popup_menus
.contains_key(&(dialog_ptr, (i + 1) as i16)))
})
.map(|(_, it)| {
let (it_t, it_l, it_b, it_r) = it.rect;
let rect = (
bounds.0 + it_t,
bounds.1 + it_l,
bounds.0 + it_b,
bounds.1 + it_r,
);
(rect, it.item_type == 0)
})
.collect()
}
fn restore_user_item_preserved_pixels(
&self,
bus: &mut MacMemoryBus,
rects: &[((i16, i16, i16, i16), bool)],
backups: &[Vec<u8>],
) {
for ((rect, always_restore), pixels) in rects.iter().zip(backups.iter()) {
if *always_restore || self.saved_rect_has_non_background_content(pixels) {
self.restore_rect_pixels(bus, *rect, pixels);
}
}
}
fn draw_dialog_preserving_user_items(
&mut self,
bus: &mut MacMemoryBus,
bounds: (i16, i16, i16, i16),
proc_id: i16,
title: &str,
items: &[DialogItem],
default_item: i16,
edit_text: &str,
edit_item: i16,
skip_pictures: bool,
dialog_ptr: u32,
preserve_user_items: bool,
skip_popup_user_items: bool,
skip_disabled_placeholders: bool,
) {
let user_item_rects = if preserve_user_items {
self.user_item_preserve_rects(
dialog_ptr,
bounds,
items,
skip_popup_user_items,
skip_disabled_placeholders,
)
} else {
Vec::new()
};
let user_item_backups: Vec<Vec<u8>> = user_item_rects
.iter()
.map(|&(r, _)| self.save_rect_pixels(bus, r))
.collect();
self.draw_dialog(
bus,
bounds,
proc_id,
title,
items,
default_item,
edit_text,
edit_item,
skip_pictures,
dialog_ptr,
);
self.restore_user_item_preserved_pixels(bus, &user_item_rects, &user_item_backups);
self.capture_gui_frame(bus, &format!("draw_dialog_preserved_{:08X}", dialog_ptr));
}
fn fill_rect_clipped_to_dialog(
&self,
bus: &mut MacMemoryBus,
bounds: (i16, i16, i16, i16),
rect: (i16, i16, i16, i16),
value: bool,
) {
let top = rect.0.max(bounds.0);
let left = rect.1.max(bounds.1);
let bottom = rect.2.min(bounds.2);
let right = rect.3.min(bounds.3);
if top >= bottom || left >= right {
return;
}
let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
self.get_screen_params();
Self::fb_fill_rect(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
top,
left,
bottom,
right,
value,
);
}
fn dialog_item_enclosing_local_rect(
item_type: u8,
rect: (i16, i16, i16, i16),
) -> (i16, i16, i16, i16) {
match item_type & 0x7F {
// IM:IV IV-59 notes that Dialog Manager drawing can extend
// outside an editText item's display rectangle by 3 pixels.
16 => (rect.0 - 3, rect.1 - 3, rect.2 + 3, rect.3 + 3),
_ => rect,
}
}
fn erase_dialog_item_enclosing_rect(
&self,
bus: &mut MacMemoryBus,
dialog_ptr: u32,
local_rect: (i16, i16, i16, i16),
) {
let bounds = Self::dialog_screen_bounds(bus, dialog_ptr);
let screen_rect = (
bounds.0 + local_rect.0,
bounds.1 + local_rect.1,
bounds.0 + local_rect.2,
bounds.1 + local_rect.3,
);
self.fill_rect_clipped_to_dialog(bus, bounds, screen_rect, false);
}
/// Restore framebuffer pixels to an exact rectangle (no margin).
///
/// Mirrors save_rect_pixels off-screen guards so the idx position stays
/// aligned with the packed input buffer.
fn restore_rect_pixels(
&self,
bus: &mut MacMemoryBus,
rect: (i16, i16, i16, i16),
saved: &[u8],
) {
let (screen_base, row_bytes, _, screen_h, pixel_size) = self.get_screen_params();
let (top, left, bottom, right) = rect;
let row_width = (right - left).max(0) as usize;
let mut idx = 0;
let screen_h_i16 = screen_h;
for y in top..bottom {
let y_on_screen = y >= 0 && y < screen_h_i16;
if pixel_size == 8 {
let on_screen_row = y_on_screen && left >= 0 && (right as u32) <= row_bytes;
if on_screen_row && idx + row_width <= saved.len() {
let row_addr = screen_base + (y as u32) * row_bytes + (left as u32);
bus.write_bytes(row_addr, &saved[idx..idx + row_width]);
idx += row_width;
} else if y_on_screen {
for x in left..right {
if idx < saved.len() {
if x >= 0 && (x as u32) < row_bytes {
bus.write_byte(
screen_base + (y as u32) * row_bytes + (x as u32),
saved[idx],
);
}
idx += 1;
}
}
} else {
idx = (idx + row_width).min(saved.len());
}
} else if y_on_screen {
let byte_left = (left.max(0) as u32) / 8;
let byte_right = (right.max(0) as u32).div_ceil(8);
let bx_end = byte_right.min(row_bytes);
if byte_left < bx_end {
let len = (bx_end - byte_left) as usize;
if idx + len <= saved.len() {
let row_addr = screen_base + (y as u32) * row_bytes + byte_left;
bus.write_bytes(row_addr, &saved[idx..idx + len]);
idx += len;
} else {
for bx in byte_left..bx_end {
if idx < saved.len() {
bus.write_byte(
screen_base + (y as u32) * row_bytes + bx,
saved[idx],
);
idx += 1;
}
}
}
}
}
}
}
/// Draw a complete dialog box with frame and all items.
/// When `skip_pictures` is true, icon and picture items are skipped
/// (used during redraw_chrome to avoid re-parsing PICTs every frame).
pub(crate) fn draw_dialog(
&mut self,
bus: &mut MacMemoryBus,
bounds: (i16, i16, i16, i16),
proc_id: i16,
title: &str,
items: &[DialogItem],
default_item: i16,
edit_text: &str,
edit_item: i16,
skip_pictures: bool,
dialog_ptr: u32,
) {
self.ensure_dialog_background_saved(bus, dialog_ptr, bounds);
self.dialog_initial_draw_deferred.remove(&dialog_ptr);
let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
self.get_screen_params();
let (top, left, bottom, right) = bounds;
// Fill dialog background with white — but skip for game-managed
// dialogs whose in-bounds items are userItems, since the game draws
// its own background and the white fill would overwrite that content.
let game_managed = Self::dialog_is_game_managed(bounds, items);
if game_managed {
eprintln!(
"[DIALOG] Skipping white fill for game-managed dialog at ({},{},{},{}), {} items",
top,
left,
bottom,
right,
items.len()
);
}
let themed_frame = match proc_id {
1 => (
top - Self::DBOX_FRAME_MARGIN,
left - Self::DBOX_FRAME_MARGIN,
bottom + Self::DBOX_FRAME_MARGIN,
right + Self::DBOX_FRAME_MARGIN,
),
2 => (top - 1, left - 1, bottom + 1, right + 1),
3 => (top - 1, left - 1, bottom + 3, right + 3),
_ => (top, left, bottom + 2, right + 2),
};
if !self.draw_theme_dialog_frame(
bus,
(top, left, bottom, right),
themed_frame,
proc_id,
true,
!game_managed,
) {
if !game_managed {
Self::fb_fill_rect(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
top,
left,
bottom,
right,
false,
);
}
// Draw border
// Inside Macintosh Volume I, I-299:
// procID 0 = documentProc (title bar + border)
// procID 1 = dBoxProc (double border, no title)
// procID 2 = plainDBox (single border, no title)
// procID 3 = altDBoxProc (shadow border, no title)
// procID 4 = noGrowDocProc (title bar + border, no grow)
match proc_id {
1 => {
self.draw_classic_dbox_frame(bus, top, left, bottom, right);
}
2 => {
// plainDBox: single-pixel WDEF border outside the content
// bounds, matching the Window Manager procID 2 path.
// Inside Macintosh Volume I, I-275.
self.draw_rect_border(bus, top - 1, left - 1, bottom + 1, right + 1);
}
3 => {
// altDBoxProc: single border + shadow
self.draw_rect_border(bus, top - 1, left - 1, bottom + 1, right + 1);
self.draw_shadow(bus, top - 1, left - 1, bottom + 1, right + 1);
}
0 | 4 => {
// documentProc/noGrowDocProc use the Window Manager's
// document WDEF title-bar chrome. DrawDialog is allowed
// on modeless dialogs too (IM:I I-417; MTE 1992 p. 6-142),
// so route through the same WDEF frame path used by
// visible window creation instead of the modal-box fallback.
let saved_bounds = self.window_bounds;
let saved_proc_id = self.window_proc_id;
let saved_title = self.window_title.clone();
let saved_go_away = self.go_away_flag;
self.window_bounds = bounds;
self.window_proc_id = proc_id;
self.window_title = if title.is_empty() {
Self::dialog_window_title(bus, dialog_ptr)
} else {
title.to_string()
};
self.go_away_flag = dialog_ptr != 0 && bus.read_byte(dialog_ptr + 112) != 0;
self.draw_window_frame(bus);
self.window_bounds = saved_bounds;
self.window_proc_id = saved_proc_id;
self.window_title = saved_title;
self.go_away_flag = saved_go_away;
}
_ => {
// Default: single border + shadow
self.draw_rect_border(bus, top, left, bottom, right);
self.draw_shadow(bus, top, left, bottom, right);
}
}
}
// Optional per-iteration trace: gate SYSTEMLESS_TRACE_DIALOG_ITEMS=1
// logs each item's type + relative rect + computed absolute rect.
// Use to localize artifact-producing items in modal dialog rendering.
let trace_items = trace_dialog_items_enabled();
if trace_items {
eprintln!(
"[DLG] draw_dialog bounds=({},{},{},{}) proc_id={} edit_item={} default_item={} items={}",
top,
left,
bottom,
right,
proc_id,
edit_item,
default_item,
items.len()
);
}
// IM:I I-407 and MTE 1992 p. 6-56: aDefItem/first-item default
// controls Return/Enter semantics for modal dialogs, but ordinary
// dialogs draw any bold default-button outline with an application
// userItem. MTE 1992 glossary confirms Dialog Manager auto-draws
// the bold outline for alerts, while applications draw it for
// dialogs. DrawDialog therefore draws standard buttons without
// synthesizing default-button chrome.
let auto_default_outline = false;
// Draw each item
for (i, item) in items.iter().enumerate() {
let item_num = (i + 1) as i16; // 1-based
let (it, il, ib, ir) = item.rect;
// Offset by dialog origin
let abs_top = top + it;
let abs_left = left + il;
let abs_bottom = top + ib;
let abs_right = left + ir;
let base_type = item.item_type & 0x7F;
if trace_items {
eprintln!(
"[DLG] item #{} type=0x{:02X} (base={}) rel=({},{},{},{}) abs=({},{},{},{}) rsrc={} text={:?}",
item_num,
item.item_type,
base_type,
it,
il,
ib,
ir,
abs_top,
abs_left,
abs_bottom,
abs_right,
item.resource_id,
item.text,
);
}
// Generic "fully outside dialog" clip. Inside Macintosh
// Volume I, I-309: dialog items are drawn through the dialog
// window's port whose visRgn/clipRgn restrict drawing to the
// dialog bounds. Real Mac OS QuickDraw clips item draws
// automatically; Systemless's per-type handlers write to the
// framebuffer directly with no clip. Items whose rect partially
// extends beyond bounds are NOT clipped here — the per-type
// handlers can refine if needed.
if abs_top >= bottom || abs_bottom <= top || abs_left >= right || abs_right <= left {
if trace_items {
eprintln!("[DLG] -> skipped (fully outside dialog rect)");
}
continue;
}
// Buttons, checkboxes, radios, and resource-backed controls are
// Control Manager controls. Draw them in a second pass below so
// their z-order matches DrawControls: reverse creation order,
// with the earliest-created control frontmost (IM:I I-322; MTE
// 1992 pp. 5-87..5-88).
if matches!(base_type, 4..=7) {
continue;
}
let enabled = (item.item_type & 0x80) == 0;
match base_type {
// Button (ctrlItem + btnCtrl = 4)
4 => {
// IM:I I-405: itemDisable stops Dialog Manager event
// reporting, while the standard System 7 control
// appearance is unchanged. Route the semantic state to
// non-classic theme chrome without changing metrics or
// existing hit handling.
self.draw_button_with_enabled(
bus,
abs_top,
abs_left,
abs_bottom,
abs_right,
&item.text,
auto_default_outline && item_num == default_item,
enabled,
);
}
// Checkbox (ctrlItem + chkCtrl = 5)
5 => {
let checked = self
.dialog_control_values
.get(&(dialog_ptr, item_num))
.copied()
.unwrap_or(0)
!= 0;
self.draw_checkbox_with_enabled(
bus, abs_top, abs_left, abs_bottom, abs_right, &item.text, checked, enabled,
);
}
// Radio button (ctrlItem + radCtrl = 6)
6 => {
let selected = self
.dialog_control_values
.get(&(dialog_ptr, item_num))
.copied()
.unwrap_or(0)
!= 0;
self.draw_radio_with_enabled(
bus, abs_top, abs_left, abs_bottom, abs_right, &item.text, selected,
enabled,
);
}
// Static text (8)
8 => {
self.draw_static_text(
bus, abs_top, abs_left, abs_bottom, abs_right, &item.text,
);
}
// Edit text (16)
16 => {
let display_text = if item_num == edit_item {
edit_text
} else {
&item.text
};
// MTE 1992 glossary "disabled item": disabled dialog
// items do not report user events. Route that semantic
// state to themed field chrome while leaving TextEdit
// metrics and existing hit handling unchanged.
let selection_range = if item_num == edit_item {
Self::dialog_item_selection_range(item, display_text.len())
} else {
None
};
self.draw_edit_text_with_cursor(
bus,
abs_top,
abs_left,
abs_bottom,
abs_right,
display_text,
selection_range,
false,
enabled,
);
}
// Icon (32)
32 => {
if !skip_pictures && item.resource_id != 0 {
let drew_cicn = self
.find_resource_any(*b"cicn", item.resource_id)
.is_some_and(|(_, icon_ptr)| {
self.draw_cicn_icon(
bus, abs_top, abs_left, abs_bottom, abs_right, icon_ptr,
)
});
if !drew_cicn {
if let Some((_, icon_ptr)) =
self.find_resource_any(*b"ICON", item.resource_id)
{
// ICON resource: 32x32 1-bit bitmap = 128 bytes
// Inside Macintosh Volume I, I-205
self.draw_icon(
bus, abs_top, abs_left, abs_bottom, abs_right, icon_ptr,
);
}
}
}
}
// Picture (64)
64 => {
// Items reaching here overlap the dialog rect (the
// fully-outside clip happens above the match).
if !skip_pictures && item.resource_id != 0 {
if let Some((_, pic_ptr)) =
self.find_resource_any(*b"PICT", item.resource_id)
{
// Draw PICT into the item's display rectangle
let device_ct_seed =
Self::ctab_seed(bus, self.current_gdevice_ctab_handle(bus))
.unwrap_or(0);
super::pict::draw_picture(
bus,
pic_ptr,
abs_top,
abs_left,
abs_bottom,
abs_right,
self.screen_mode,
&self.device_clut,
device_ct_seed,
None,
);
}
}
}
// resCtrl — DITL item backed by a live CNTL resource.
// Macintosh Toolbox Essentials 1992, 3-31.
7 => {
if let Some(ctrl_handle) =
self.dialog_control_handle_for_item(dialog_ptr, item_num)
{
let ctrl_ptr = bus.read_long(ctrl_handle);
let proc_id_ctrl =
self.control_proc_ids.get(&ctrl_ptr).copied().unwrap_or(0);
let value = self
.dialog_control_values
.get(&(dialog_ptr, item_num))
.copied()
.unwrap_or_else(|| bus.read_word(ctrl_ptr + 18) as i16);
let min = bus.read_word(ctrl_ptr + 20) as i16;
let max = bus.read_word(ctrl_ptr + 22) as i16;
let hilite = bus.read_byte(ctrl_ptr + 17);
let title =
decode_mac_roman_for_render(&Self::control_title_bytes(bus, ctrl_ptr));
match proc_id_ctrl {
0 => self.draw_button_with_enabled(
bus,
abs_top,
abs_left,
abs_bottom,
abs_right,
&title,
auto_default_outline && item_num == default_item,
enabled,
),
1 => self.draw_checkbox_with_enabled(
bus,
abs_top,
abs_left,
abs_bottom,
abs_right,
&title,
value != 0,
enabled,
),
2 => self.draw_radio_with_enabled(
bus,
abs_top,
abs_left,
abs_bottom,
abs_right,
&title,
value != 0,
enabled,
),
16 => self.draw_scroll_bar(
bus, abs_top, abs_left, abs_bottom, abs_right, value, min, max,
hilite,
),
proc_id if Self::is_popup_menu_proc_id(proc_id) => {
let menu_id = self.popup_control_menu_id(bus, ctrl_ptr, min);
let selected = value.max(1) as usize;
let item_title = self.popup_menu_item_title(bus, menu_id, selected);
let (draw_top, draw_left, draw_bottom, draw_right) = self
.popup_control_box_rect(
bus, abs_top, abs_left, abs_bottom, abs_right, menu_id,
max, proc_id,
);
self.draw_popup_control_label(
bus,
abs_top,
abs_left,
abs_bottom,
draw_left,
&title,
enabled && hilite != 255,
);
self.draw_popup_control_with_state(
bus,
draw_top,
draw_left,
draw_bottom,
draw_right,
&item_title.unwrap_or_default(),
enabled && hilite != 255,
hilite == 1,
);
}
_ => {
self.draw_button_with_enabled(
bus,
abs_top,
abs_left,
abs_bottom,
abs_right,
&title,
auto_default_outline && item_num == default_item,
enabled,
);
}
}
} else {
self.draw_button_with_enabled(
bus,
abs_top,
abs_left,
abs_bottom,
abs_right,
"",
auto_default_outline && item_num == default_item,
enabled,
);
}
}
// userItem (0) and unknown: skip
_ => {}
}
}
// Draw standard controls in reverse DITL/control creation order.
// NewControl adds each control to the beginning of the window's
// control list, and DrawControls draws that list in reverse so the
// earliest-created controls appear frontmost on overlap (IM:I
// I-319/I-322). Dialog Manager standard DITL controls use the same
// Control Manager semantics.
for (i, item) in items.iter().enumerate().rev() {
let item_num = (i + 1) as i16;
let (it, il, ib, ir) = item.rect;
let abs_top = top + it;
let abs_left = left + il;
let abs_bottom = top + ib;
let abs_right = left + ir;
let base_type = item.item_type & 0x7F;
if !matches!(base_type, 4..=7) {
continue;
}
if abs_top >= bottom || abs_bottom <= top || abs_left >= right || abs_right <= left {
continue;
}
let enabled = (item.item_type & 0x80) == 0;
if trace_items {
if let Some(ctrl_handle) = self.dialog_control_handle_for_item(dialog_ptr, item_num)
{
let ctrl_ptr = bus.read_long(ctrl_handle);
let proc_id_ctrl = self.control_proc_ids.get(&ctrl_ptr).copied().unwrap_or(0);
eprintln!(
"[DLG] item={} control handle=${:08X} ptr=${:08X} visible={} hilite={} value={} min={} max={} proc_id={}",
item_num,
ctrl_handle,
ctrl_ptr,
ctrl_ptr != 0 && bus.read_byte(ctrl_ptr + 16) != 0,
if ctrl_ptr == 0 {
-1
} else {
bus.read_byte(ctrl_ptr + 17) as i16
},
if ctrl_ptr == 0 {
0
} else {
bus.read_word(ctrl_ptr + 18) as i16
},
if ctrl_ptr == 0 {
0
} else {
bus.read_word(ctrl_ptr + 20) as i16
},
if ctrl_ptr == 0 {
0
} else {
bus.read_word(ctrl_ptr + 22) as i16
},
proc_id_ctrl,
);
} else {
eprintln!("[DLG] item={} control handle=<none>", item_num);
}
}
match base_type {
4 => {
self.draw_button_with_enabled(
bus,
abs_top,
abs_left,
abs_bottom,
abs_right,
&item.text,
auto_default_outline && item_num == default_item,
enabled,
);
}
5 => {
let inactive = self.dialog_control_inactive(bus, dialog_ptr, item_num);
let checked = self
.dialog_control_values
.get(&(dialog_ptr, item_num))
.copied()
.unwrap_or(0)
!= 0;
self.draw_checkbox_with_enabled_and_inactive(
bus, abs_top, abs_left, abs_bottom, abs_right, &item.text, checked,
enabled, inactive,
);
}
6 => {
let inactive = self.dialog_control_inactive(bus, dialog_ptr, item_num);
let selected = self
.dialog_control_values
.get(&(dialog_ptr, item_num))
.copied()
.unwrap_or(0)
!= 0;
self.draw_radio_with_enabled_and_inactive(
bus, abs_top, abs_left, abs_bottom, abs_right, &item.text, selected,
enabled, inactive,
);
}
7 => {
if let Some(ctrl_handle) =
self.dialog_control_handle_for_item(dialog_ptr, item_num)
{
let ctrl_ptr = bus.read_long(ctrl_handle);
let proc_id_ctrl =
self.control_proc_ids.get(&ctrl_ptr).copied().unwrap_or(0);
let value = self
.dialog_control_values
.get(&(dialog_ptr, item_num))
.copied()
.unwrap_or_else(|| bus.read_word(ctrl_ptr + 18) as i16);
let min = bus.read_word(ctrl_ptr + 20) as i16;
let max = bus.read_word(ctrl_ptr + 22) as i16;
let hilite = bus.read_byte(ctrl_ptr + 17);
let title =
decode_mac_roman_for_render(&Self::control_title_bytes(bus, ctrl_ptr));
match proc_id_ctrl {
0 => self.draw_button_with_enabled(
bus,
abs_top,
abs_left,
abs_bottom,
abs_right,
&title,
auto_default_outline && item_num == default_item,
enabled,
),
1 => self.draw_checkbox_with_enabled_and_inactive(
bus,
abs_top,
abs_left,
abs_bottom,
abs_right,
&title,
value != 0,
enabled,
hilite == 255,
),
2 => self.draw_radio_with_enabled_and_inactive(
bus,
abs_top,
abs_left,
abs_bottom,
abs_right,
&title,
value != 0,
enabled,
hilite == 255,
),
16 => self.draw_scroll_bar(
bus, abs_top, abs_left, abs_bottom, abs_right, value, min, max,
hilite,
),
proc_id if Self::is_popup_menu_proc_id(proc_id) => {
let menu_id = self.popup_control_menu_id(bus, ctrl_ptr, min);
let selected = value.max(1) as usize;
let item_title = self.popup_menu_item_title(bus, menu_id, selected);
let (draw_top, draw_left, draw_bottom, draw_right) = self
.popup_control_box_rect(
bus, abs_top, abs_left, abs_bottom, abs_right, menu_id,
max, proc_id,
);
self.draw_popup_control_label(
bus,
abs_top,
abs_left,
abs_bottom,
draw_left,
&title,
enabled && hilite != 255,
);
self.draw_popup_control_with_state(
bus,
draw_top,
draw_left,
draw_bottom,
draw_right,
&item_title.unwrap_or_default(),
enabled && hilite != 255,
hilite == 1,
);
}
_ => {
self.draw_button_with_enabled(
bus,
abs_top,
abs_left,
abs_bottom,
abs_right,
&title,
auto_default_outline && item_num == default_item,
enabled,
);
}
}
} else {
self.draw_button_with_enabled(
bus,
abs_top,
abs_left,
abs_bottom,
abs_right,
"",
auto_default_outline && item_num == default_item,
enabled,
);
}
}
_ => {}
}
}
self.capture_gui_frame(bus, &format!("draw_dialog_{:08X}", dialog_ptr));
}
fn redraw_standard_dialog_items(
&self,
bus: &mut MacMemoryBus,
bounds: (i16, i16, i16, i16),
items: &[DialogItem],
default_item: i16,
edit_text: &str,
edit_item: i16,
dialog_ptr: u32,
) {
let (top, left, bottom, right) = bounds;
// Ordinary dialog default outlines are application-owned userItem
// drawing, not synthesized by DrawDialog. See the DrawDialog path
// above for the IM:I/MTE references.
let auto_default_outline = false;
for (i, item) in items.iter().enumerate() {
let item_num = (i + 1) as i16;
let (it, il, ib, ir) = item.rect;
let abs_top = top + it;
let abs_left = left + il;
let abs_bottom = top + ib;
let abs_right = left + ir;
if abs_top >= bottom || abs_bottom <= top || abs_left >= right || abs_right <= left {
continue;
}
let enabled = (item.item_type & 0x80) == 0;
match item.item_type & 0x7F {
4 => self.draw_button_with_enabled(
bus,
abs_top,
abs_left,
abs_bottom,
abs_right,
&item.text,
auto_default_outline && item_num == default_item,
enabled,
),
5 => {
let inactive = self.dialog_control_inactive(bus, dialog_ptr, item_num);
let checked = self
.dialog_control_values
.get(&(dialog_ptr, item_num))
.copied()
.unwrap_or(0)
!= 0;
self.fill_rect_clipped_to_dialog(
bus,
bounds,
(abs_top, abs_left, abs_bottom, abs_right),
false,
);
self.draw_checkbox_with_enabled_and_inactive(
bus, abs_top, abs_left, abs_bottom, abs_right, &item.text, checked,
enabled, inactive,
);
}
6 => {
let inactive = self.dialog_control_inactive(bus, dialog_ptr, item_num);
self.fill_rect_clipped_to_dialog(
bus,
bounds,
(abs_top, abs_left, abs_bottom, abs_right),
false,
);
self.draw_radio_with_enabled_and_inactive(
bus, abs_top, abs_left, abs_bottom, abs_right, &item.text, false, enabled,
inactive,
);
}
7 => {
if let Some(ctrl_handle) =
self.dialog_control_handle_for_item(dialog_ptr, item_num)
{
let ctrl_ptr = bus.read_long(ctrl_handle);
let proc_id_ctrl =
self.control_proc_ids.get(&ctrl_ptr).copied().unwrap_or(0);
let value = self
.dialog_control_values
.get(&(dialog_ptr, item_num))
.copied()
.unwrap_or_else(|| bus.read_word(ctrl_ptr + 18) as i16);
let min = bus.read_word(ctrl_ptr + 20) as i16;
let max = bus.read_word(ctrl_ptr + 22) as i16;
let hilite = bus.read_byte(ctrl_ptr + 17);
let title =
decode_mac_roman_for_render(&Self::control_title_bytes(bus, ctrl_ptr));
match proc_id_ctrl {
0 => self.draw_button_with_enabled(
bus,
abs_top,
abs_left,
abs_bottom,
abs_right,
&title,
auto_default_outline && item_num == default_item,
enabled,
),
1 => {
self.fill_rect_clipped_to_dialog(
bus,
bounds,
(abs_top, abs_left, abs_bottom, abs_right),
false,
);
self.draw_checkbox_with_enabled_and_inactive(
bus,
abs_top,
abs_left,
abs_bottom,
abs_right,
&title,
value != 0,
enabled,
hilite == 255,
)
}
2 => {
self.fill_rect_clipped_to_dialog(
bus,
bounds,
(abs_top, abs_left, abs_bottom, abs_right),
false,
);
self.draw_radio_with_enabled_and_inactive(
bus,
abs_top,
abs_left,
abs_bottom,
abs_right,
&title,
value != 0,
enabled,
hilite == 255,
)
}
16 => self.draw_scroll_bar(
bus, abs_top, abs_left, abs_bottom, abs_right, value, min, max,
hilite,
),
proc_id if Self::is_popup_menu_proc_id(proc_id) => {
let menu_id = self.popup_control_menu_id(bus, ctrl_ptr, min);
let selected = value.max(1) as usize;
let item_title = self.popup_menu_item_title(bus, menu_id, selected);
let (draw_top, draw_left, draw_bottom, draw_right) = self
.popup_control_box_rect(
bus, abs_top, abs_left, abs_bottom, abs_right, menu_id,
max, proc_id,
);
self.draw_popup_control_label(
bus,
abs_top,
abs_left,
abs_bottom,
draw_left,
&title,
enabled && hilite != 255,
);
self.draw_popup_control_with_state(
bus,
draw_top,
draw_left,
draw_bottom,
draw_right,
&item_title.unwrap_or_default(),
enabled && hilite != 255,
hilite == 1,
);
}
_ => self.draw_button_with_enabled(
bus,
abs_top,
abs_left,
abs_bottom,
abs_right,
&title,
auto_default_outline && item_num == default_item,
enabled,
),
}
} else {
self.draw_button_with_enabled(
bus,
abs_top,
abs_left,
abs_bottom,
abs_right,
"",
auto_default_outline && item_num == default_item,
enabled,
);
}
}
8 => {
self.fill_rect_clipped_to_dialog(
bus,
bounds,
(abs_top, abs_left, abs_bottom, abs_right),
false,
);
self.draw_static_text(bus, abs_top, abs_left, abs_bottom, abs_right, &item.text)
}
16 => {
let display_text = if item_num == edit_item {
edit_text
} else {
&item.text
};
let selection_range = if item_num == edit_item {
Self::dialog_item_selection_range(item, display_text.len())
} else {
None
};
self.draw_edit_text_with_cursor(
bus,
abs_top,
abs_left,
abs_bottom,
abs_right,
display_text,
selection_range,
false,
enabled,
);
}
_ => {}
}
}
}
fn refresh_dialog_tracking_snapshot(&mut self, bus: &mut MacMemoryBus) {
let Some(tracking) = self.dialog_tracking.as_ref() else {
return;
};
if tracking.game_managed || !tracking.rendered_pixels_final {
return;
}
let bounds = tracking.bounds;
let items = tracking.items.clone();
let default_item = tracking.default_item;
let edit_text = tracking.edit_text.clone();
let edit_item = tracking.edit_item;
let dialog_ptr = tracking.dialog_ptr;
let popup_draws = tracking.popup_draws.clone();
let rendered_pixels = tracking.rendered_pixels.clone();
if !rendered_pixels.is_empty() {
self.restore_dialog_pixels(bus, bounds, &rendered_pixels);
}
self.redraw_standard_dialog_items(
bus,
bounds,
&items,
default_item,
&edit_text,
edit_item,
dialog_ptr,
);
self.redraw_dialog_popup_controls(bus, &popup_draws);
let rendered = self.save_dialog_pixels(bus, bounds);
if let Some(tracking) = self.dialog_tracking.as_mut() {
tracking.rendered_pixels = rendered;
}
}
fn persist_visible_dialog_snapshot(
&mut self,
bus: &MacMemoryBus,
tracking: &DialogTrackingState,
) {
let pixels = if tracking.rendered_pixels.is_empty() {
self.save_dialog_pixels(bus, tracking.bounds)
} else {
tracking.rendered_pixels.clone()
};
self.dialog_visible_snapshots.insert(
tracking.dialog_ptr,
PersistentDialogSnapshot {
bounds: tracking.bounds,
pixels,
},
);
}
pub(crate) fn refresh_visible_dialog_snapshot_for_port(
&mut self,
bus: &MacMemoryBus,
port: u32,
) {
if port == 0 {
return;
}
let Some(bounds) = self
.dialog_visible_snapshots
.get(&port)
.map(|snapshot| snapshot.bounds)
.or_else(|| {
if self.dialog_items.contains_key(&port) && self.window_visible(bus, port) {
Some(Self::dialog_screen_bounds(bus, port))
} else {
None
}
})
else {
return;
};
let pixels = self.save_dialog_pixels(bus, bounds);
self.dialog_visible_snapshots
.insert(port, PersistentDialogSnapshot { bounds, pixels });
}
pub(crate) fn refresh_visible_dialog_snapshot_after_bulk_port_draw(
&mut self,
bus: &MacMemoryBus,
port: u32,
) {
if port == 0 {
return;
}
let game_managed_visible = self
.dialog_items
.get(&port)
.filter(|_| self.window_visible(bus, port))
.is_some_and(|items| {
let bounds = Self::dialog_screen_bounds(bus, port);
Self::dialog_is_game_managed(bounds, items)
});
if game_managed_visible {
self.refresh_visible_dialog_snapshot_for_port(bus, port);
return;
}
if !self.dialog_visible_snapshots.contains_key(&port) {
return;
}
let modal_tracking_matches = self
.dialog_tracking
.as_ref()
.is_some_and(|tracking| tracking.dialog_ptr == port);
let app_draw_proc_matches = self.active_modeless_dialog_draw_proc == Some(port);
if self.dialog_modal_entered.contains(&port)
|| modal_tracking_matches
|| app_draw_proc_matches
{
self.refresh_visible_dialog_snapshot_for_port(bus, port);
}
}
pub(crate) fn redraw_retained_modal_dialog_click(&self, bus: &mut MacMemoryBus) {
let Some(click) = self.retained_modal_dialog_click.as_ref() else {
return;
};
if !click.highlighted || click.item_no <= 0 || self.front_window != click.dialog_ptr {
return;
}
let bounds = self
.dialog_visible_snapshots
.get(&click.dialog_ptr)
.map(|snapshot| snapshot.bounds)
.unwrap_or_else(|| Self::dialog_screen_bounds(bus, click.dialog_ptr));
let rect = Self::dialog_item_screen_rect(bounds, click.rect);
self.draw_dialog_button_highlight_state(bus, rect, &click.title, click.is_default, true);
}
pub(crate) fn finalize_dialog_draw_procs_if_idle(&mut self, bus: &mut MacMemoryBus) {
let Some(tracking) = self.dialog_tracking.as_ref() else {
if let Some(dialog_ptr) = self.active_modeless_dialog_draw_proc.take() {
self.refresh_visible_dialog_snapshot_for_port(bus, dialog_ptr);
self.capture_gui_frame(
bus,
&format!("modeless_dialog_draw_proc_{:08X}", dialog_ptr),
);
}
return;
};
if tracking.draw_procs_done || !tracking.draw_proc_queue.is_empty() {
return;
}
let bounds = tracking.bounds;
let items = tracking.items.clone();
let default_item = tracking.default_item;
let edit_text = tracking.edit_text.clone();
let edit_item = tracking.edit_item;
let dialog_ptr = tracking.dialog_ptr;
let popup_draws = tracking.popup_draws.clone();
let game_managed = tracking.game_managed;
if !game_managed {
if self.front_window == dialog_ptr {
self.blit_window_to_screen(bus);
}
self.redraw_standard_dialog_items(
bus,
bounds,
&items,
default_item,
&edit_text,
edit_item,
dialog_ptr,
);
}
self.redraw_dialog_popup_controls(bus, &popup_draws);
let rendered = self.save_dialog_pixels(bus, bounds);
if let Some(tracking) = self.dialog_tracking.as_mut() {
tracking.rendered_pixels = rendered;
tracking.rendered_pixels_final = true;
tracking.draw_procs_done = true;
}
}
/// Draw a 1-pixel black rectangle border.
pub(crate) fn draw_rect_border(
&self,
bus: &mut MacMemoryBus,
top: i16,
left: i16,
bottom: i16,
right: i16,
) {
let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
self.get_screen_params();
// Top edge
Self::fb_hline(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
top,
left,
right - 1,
true,
);
// Bottom edge
Self::fb_hline(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
bottom - 1,
left,
right - 1,
true,
);
// Left edge
for y in top..bottom {
Self::fb_set_pixel(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
left,
y,
true,
);
}
// Right edge
for y in top..bottom {
Self::fb_set_pixel(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
right - 1,
y,
true,
);
}
}
fn fill_dialog_rect(
&self,
bus: &mut MacMemoryBus,
top: i16,
left: i16,
bottom: i16,
right: i16,
black: bool,
) {
let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
self.get_screen_params();
Self::fb_fill_rect(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
top,
left,
bottom,
right,
black,
);
}
fn draw_classic_dbox_frame(
&self,
bus: &mut MacMemoryBus,
top: i16,
left: i16,
bottom: i16,
right: i16,
) {
let margin = Self::DBOX_FRAME_MARGIN;
// dBoxProc is a Window Manager WDEF variant, not a content-edge
// dialog item border. IM:I I-273 describes a modal dialog box as a
// rectangular window with its border inside the structure edge; the
// boundsRect remains the grafPort content region (IM:I I-282).
self.fill_dialog_rect(
bus,
top - margin,
left - margin,
bottom + margin,
right + margin,
false,
);
// System 7.5.3's standard WDEF draws an asymmetric black structure
// edge: one pixel on top/left and a two-pixel shadow edge on
// bottom/right, with a two-pixel inner band inset from the outer edge.
self.fill_dialog_rect(bus, top - 8, left - 8, top - 7, right + 8, true);
self.fill_dialog_rect(bus, top - 8, left - 8, bottom + 8, left - 7, true);
self.fill_dialog_rect(bus, top - 8, right + 6, bottom + 8, right + 8, true);
self.fill_dialog_rect(bus, bottom + 6, left - 8, bottom + 8, right + 8, true);
self.fill_dialog_rect(bus, top - 5, left - 5, top - 4, right + 5, true);
self.fill_dialog_rect(bus, top - 4, left - 5, top - 3, right + 4, true);
self.fill_dialog_rect(bus, top - 5, left - 5, bottom + 5, left - 4, true);
self.fill_dialog_rect(bus, top - 5, left - 4, bottom + 4, left - 3, true);
self.fill_dialog_rect(bus, top - 5, right + 3, bottom + 4, right + 4, true);
self.fill_dialog_rect(bus, bottom + 3, left - 5, bottom + 4, right + 4, true);
}
/// Draw a 2-pixel shadow on bottom and right edges.
pub(crate) fn draw_shadow(
&self,
bus: &mut MacMemoryBus,
top: i16,
left: i16,
bottom: i16,
right: i16,
) {
let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
self.get_screen_params();
// Right shadow (2px wide)
for dx in 0..2i16 {
for y in (top + 2)..=(bottom + dx) {
Self::fb_set_pixel(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
right + dx,
y,
true,
);
}
}
// Bottom shadow (2px tall)
for dy in 0..2i16 {
Self::fb_hline(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
bottom + dy,
left + 2,
right + 2,
true,
);
}
}
/// Draw a popup menu control button (procID 1008 / popupMenuProc).
/// Shows a bordered rectangle with a 1-px drop shadow, a downward-pointing
/// triangle on the right, and the currently-selected item title.
/// Macintosh Toolbox Essentials 1992, 3-31
pub(crate) fn draw_popup_control(
&self,
bus: &mut MacMemoryBus,
top: i16,
left: i16,
bottom: i16,
right: i16,
title: &str,
) {
self.draw_popup_control_with_state(bus, top, left, bottom, right, title, true, false);
}
pub(crate) fn popup_control_box_rect(
&self,
bus: &MacMemoryBus,
top: i16,
left: i16,
bottom: i16,
right: i16,
menu_id: i16,
title_width: i16,
proc_id: i16,
) -> (i16, i16, i16, i16) {
let (_, _, screen_width, _, _) = self.get_screen_params();
let box_top = top + 1;
let box_left = left + title_width.max(0);
let box_bottom = bottom - 2;
let fixed_width = ((proc_id - 1008) & 0x0001) != 0;
let box_right = if fixed_width {
right - 1
} else {
let text_width = self.popup_menu_max_item_width(bus, menu_id);
let auto_width = (text_width + 40).max(80);
(box_left + auto_width).min(right - 1)
};
(
box_top,
box_left.min(screen_width),
box_bottom,
box_right.min(screen_width),
)
}
fn popup_menu_max_item_width(&self, bus: &MacMemoryBus, menu_id: i16) -> i16 {
let font_id = 0i16;
let font_size = 12i16;
let mut max_width = 0;
for item_no in 1..=255usize {
let Some(title) = self.popup_menu_item_title(bus, menu_id, item_no) else {
break;
};
max_width = max_width.max(Self::fb_measure_string(&title, font_id, font_size));
}
max_width
}
pub(crate) fn draw_popup_control_label(
&self,
bus: &mut MacMemoryBus,
top: i16,
left: i16,
bottom: i16,
popup_left: i16,
title: &str,
enabled: bool,
) {
if title.is_empty() || popup_left <= left + 4 {
return;
}
let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
self.get_screen_params();
let font_id = 0i16;
let font_size = 12i16;
let metrics = get_font_metrics(font_id, font_size);
let text_width = Self::fb_measure_string(title, font_id, font_size);
let text_right = popup_left - 6;
let text_x = (text_right - text_width).max(left);
let text_y = top + ((bottom - top) + metrics.ascent - metrics.descent) / 2;
Self::fb_draw_string(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
text_x,
text_y,
title,
font_id,
font_size,
);
if !enabled {
self.dim_rect(bus, top, left, bottom, popup_left);
}
}
pub(crate) fn draw_popup_control_with_state(
&self,
bus: &mut MacMemoryBus,
top: i16,
left: i16,
bottom: i16,
right: i16,
title: &str,
enabled: bool,
pressed: bool,
) {
let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
self.get_screen_params();
let font_id = 0i16;
let font_size = 12i16;
if !self.draw_theme_control_chrome(
bus,
ControlKind::PopupButton,
top,
left,
bottom,
right,
enabled,
pressed,
false,
false,
) {
// Standard popup menu button appearance.
// Macintosh Toolbox Essentials 1992, 5-26 to 5-27.
Self::fb_fill_rect(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
top,
left,
bottom,
right + 1,
false,
);
if enabled {
Self::fb_hline(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
top,
left,
right,
true,
);
Self::fb_hline(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
bottom - 2,
left,
right,
true,
);
for y in top..(bottom - 1) {
Self::fb_set_pixel(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
left,
y,
true,
);
Self::fb_set_pixel(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
right - 1,
y,
true,
);
}
for y in (top + 3)..(bottom - 1) {
Self::fb_set_pixel(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
right,
y,
true,
);
}
Self::fb_hline(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
bottom - 1,
left + 3,
right + 1,
true,
);
} else {
for y in (top + 3)..(bottom - 2) {
if (y - top) % 2 == 1 {
Self::fb_set_pixel(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
right,
y,
true,
);
}
}
for x in (left + 1)..=right {
if (x - left) % 2 == 1 {
Self::fb_set_pixel(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
x,
bottom - 2,
true,
);
}
}
}
// Downward-pointing triangle on right side (popup indicator)
// Macintosh Toolbox Essentials 1992, 5-26
let tri_x = right - 12;
if enabled {
for row in 0..6i16 {
Self::fb_hline(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
top + 6 + row,
tri_x - 5 + row,
tri_x + 6 - row,
true,
);
}
} else {
for row in [0i16, 2, 4] {
let start = tri_x - 4 + row;
let end = tri_x + 5 - row;
for x in start..end {
if (x - start) % 2 == 0 {
Self::fb_set_pixel(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
x,
top + 7 + row,
true,
);
}
}
}
}
}
// Selected item text inside the box
// Macintosh Toolbox Essentials 1992, 5-26
if enabled && !title.is_empty() {
let metrics = get_font_metrics(font_id, font_size);
let text_x = left + 15;
let text_y =
top + (bottom - top - (metrics.ascent + metrics.descent)) / 2 + metrics.ascent - 1;
Self::fb_draw_string_styled(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
text_x,
text_y,
title,
font_id,
font_size,
0,
);
}
}
fn redraw_dialog_popup_controls(
&self,
bus: &mut MacMemoryBus,
popup_draws: &[DialogPopupDraw],
) {
for draw in popup_draws {
let (top, left, bottom, right) = draw.rect;
if draw.enabled && !draw.pressed {
self.draw_popup_control(bus, top, left, bottom, right, &draw.title);
continue;
}
self.draw_popup_control_with_state(
bus,
top,
left,
bottom,
right,
&draw.title,
draw.enabled,
draw.pressed,
);
}
}
/// Draw a framed round-rect directly to the framebuffer.
/// Used for dialog default button outlines where no CpuOps/port is available.
/// Macintosh Toolbox Essentials 1992, Listing 6-17
fn fb_frame_round_rect(
&self,
bus: &mut MacMemoryBus,
top: i16,
left: i16,
bottom: i16,
right: i16,
oval_width: i16,
oval_height: i16,
pen_size: i16,
) {
let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
self.get_screen_params();
let r = Rect {
top,
left,
bottom,
right,
};
let outer_spans = self.compute_rrect_spans(&r, oval_width, oval_height);
let r_inset = Rect {
top: top + pen_size,
left: left + pen_size,
bottom: bottom - pen_size,
right: right - pen_size,
};
let inner_spans = self.compute_rrect_spans(
&r_inset,
(oval_width - 2 * pen_size).max(0),
(oval_height - 2 * pen_size).max(0),
);
for y in top..bottom {
let outer_idx = (y - top) as usize;
if outer_idx >= outer_spans.len() {
continue;
}
let (ol, or) = outer_spans[outer_idx];
let inner_idx = (y - r_inset.top) as usize;
let (il, ir) =
if y >= r_inset.top && y < r_inset.bottom && inner_idx < inner_spans.len() {
inner_spans[inner_idx]
} else {
(or, ol) // no inner = draw full outer span
};
// Left border segment
for x in ol..il.min(or) {
Self::fb_set_pixel(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
x,
y,
true,
);
}
// Right border segment
for x in ir.max(ol)..or {
Self::fb_set_pixel(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
x,
y,
true,
);
}
}
}
/// Draw a button with optional default (thick) border.
/// Inside Macintosh Volume I, I-405
pub(crate) fn draw_button(
&self,
bus: &mut MacMemoryBus,
top: i16,
left: i16,
bottom: i16,
right: i16,
title: &str,
is_default: bool,
) {
self.draw_button_state(bus, top, left, bottom, right, title, is_default, true);
}
fn draw_button_with_enabled(
&self,
bus: &mut MacMemoryBus,
top: i16,
left: i16,
bottom: i16,
right: i16,
title: &str,
is_default: bool,
enabled: bool,
) {
if enabled {
self.draw_button(bus, top, left, bottom, right, title, is_default);
return;
}
self.draw_button_state(bus, top, left, bottom, right, title, is_default, enabled);
}
fn draw_button_state(
&self,
bus: &mut MacMemoryBus,
top: i16,
left: i16,
bottom: i16,
right: i16,
title: &str,
is_default: bool,
enabled: bool,
) {
if !self.draw_theme_push_button_chrome(
bus, top, left, bottom, right, enabled, false, is_default,
) {
self.fill_classic_button_shape(bus, top, left, bottom, right);
self.draw_classic_button_outline(bus, top, left, bottom, right);
// Default button: rounded bold outline (3px thick)
// Macintosh Toolbox Essentials 1992, Listing 6-17
// references/executor/src/error/system_error.cpp
if is_default {
let hilite_top = top - 4;
let hilite_left = left - 4;
let hilite_bottom = bottom + 4;
let hilite_right = right + 4;
let hilite_height = hilite_bottom - hilite_top;
let oval = (hilite_height / 2 - 4).max(4);
self.fb_frame_round_rect(
bus,
hilite_top,
hilite_left,
hilite_bottom,
hilite_right,
oval,
oval,
3,
);
}
}
self.draw_button_label(bus, top, left, bottom, right, title);
}
fn fill_classic_button_shape(
&self,
bus: &mut MacMemoryBus,
top: i16,
left: i16,
bottom: i16,
right: i16,
) {
if bottom - top < 8 || right - left < 8 {
let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
self.get_screen_params();
Self::fb_fill_rect(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
top,
left,
bottom,
right,
false,
);
return;
}
// Classic push buttons are rounded controls inside the DITL display
// rectangle. Fill the same rounded shape the CDEF frames so pixels in
// the rectangular corner cutouts remain whatever the dialog port had
// underneath.
if bottom - top >= 24 {
self.fill_dialog_rect(bus, top, left + 4, top + 1, right - 4, false);
self.fill_dialog_rect(bus, top + 1, left + 2, top + 2, right - 2, false);
self.fill_dialog_rect(bus, top + 2, left + 1, top + 4, right - 1, false);
self.fill_dialog_rect(bus, top + 4, left, bottom - 4, right, false);
self.fill_dialog_rect(bus, bottom - 4, left + 1, bottom - 2, right - 1, false);
self.fill_dialog_rect(bus, bottom - 2, left + 2, bottom - 1, right - 2, false);
self.fill_dialog_rect(bus, bottom - 1, left + 4, bottom, right - 4, false);
return;
}
self.fill_dialog_rect(bus, top, left + 3, top + 1, right - 3, false);
self.fill_dialog_rect(bus, top + 1, left + 1, top + 3, right - 1, false);
self.fill_dialog_rect(bus, top + 3, left, bottom - 3, right, false);
self.fill_dialog_rect(bus, bottom - 3, left + 1, bottom - 1, right - 1, false);
self.fill_dialog_rect(bus, bottom - 1, left + 3, bottom, right - 3, false);
}
fn draw_button_label(
&self,
bus: &mut MacMemoryBus,
top: i16,
left: i16,
bottom: i16,
right: i16,
title: &str,
) {
let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
self.get_screen_params();
let font_id = 0i16; // Chicago
let font_size = 12i16;
let metrics = get_font_metrics(font_id, font_size);
let text_w = Self::fb_measure_string(title, font_id, font_size);
let text_x = left + (right - left - text_w) / 2;
let text_y = top + (bottom - top - (metrics.ascent + metrics.descent)) / 2 + metrics.ascent;
Self::fb_draw_string(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
text_x,
text_y,
title,
font_id,
font_size,
);
}
fn draw_classic_button_outline(
&self,
bus: &mut MacMemoryBus,
top: i16,
left: i16,
bottom: i16,
right: i16,
) {
if bottom - top < 8 || right - left < 8 {
self.draw_rect_border(bus, top, left, bottom, right);
return;
}
// Dialog Manager buttons are standard controls whose display
// rectangle becomes the control's enclosing rectangle (IM:I I-405).
// The classic System 7 control definition draws the push button as a
// one-pixel rounded rectangle inside that enclosing rectangle.
if bottom - top >= 24 {
self.fill_dialog_rect(bus, top, left + 4, top + 1, right - 4, true);
self.fill_dialog_rect(bus, top + 1, left + 2, top + 2, left + 4, true);
self.fill_dialog_rect(bus, top + 1, right - 4, top + 2, right - 2, true);
self.fill_dialog_rect(bus, top + 2, left + 1, top + 3, left + 2, true);
self.fill_dialog_rect(bus, top + 2, right - 2, top + 3, right - 1, true);
self.fill_dialog_rect(bus, top + 3, left + 1, top + 4, left + 2, true);
self.fill_dialog_rect(bus, top + 3, right - 2, top + 4, right - 1, true);
self.fill_dialog_rect(bus, top + 4, left, bottom - 4, left + 1, true);
self.fill_dialog_rect(bus, top + 4, right - 1, bottom - 4, right, true);
self.fill_dialog_rect(bus, bottom - 4, left + 1, bottom - 3, left + 2, true);
self.fill_dialog_rect(bus, bottom - 4, right - 2, bottom - 3, right - 1, true);
self.fill_dialog_rect(bus, bottom - 3, left + 1, bottom - 2, left + 2, true);
self.fill_dialog_rect(bus, bottom - 3, right - 2, bottom - 2, right - 1, true);
self.fill_dialog_rect(bus, bottom - 2, left + 2, bottom - 1, left + 4, true);
self.fill_dialog_rect(bus, bottom - 2, right - 4, bottom - 1, right - 2, true);
self.fill_dialog_rect(bus, bottom - 1, left + 4, bottom, right - 4, true);
return;
}
self.fill_dialog_rect(bus, top, left + 3, top + 1, right - 3, true);
self.fill_dialog_rect(bus, top + 1, left + 1, top + 2, left + 3, true);
self.fill_dialog_rect(bus, top + 1, right - 3, top + 2, right - 1, true);
self.fill_dialog_rect(bus, top + 2, left + 1, top + 3, left + 2, true);
self.fill_dialog_rect(bus, top + 2, right - 2, top + 3, right - 1, true);
self.fill_dialog_rect(bus, top + 3, left, bottom - 3, left + 1, true);
self.fill_dialog_rect(bus, top + 3, right - 1, bottom - 3, right, true);
self.fill_dialog_rect(bus, bottom - 3, left + 1, bottom - 2, left + 2, true);
self.fill_dialog_rect(bus, bottom - 3, right - 2, bottom - 2, right - 1, true);
self.fill_dialog_rect(bus, bottom - 2, left + 1, bottom - 1, left + 3, true);
self.fill_dialog_rect(bus, bottom - 2, right - 3, bottom - 1, right - 1, true);
self.fill_dialog_rect(bus, bottom - 1, left + 3, bottom, right - 3, true);
}
fn draw_dialog_button_highlight_state(
&self,
bus: &mut MacMemoryBus,
rect: (i16, i16, i16, i16),
title: &str,
is_default: bool,
highlighted: bool,
) {
let (top, left, bottom, right) = rect;
if self.draw_theme_push_button_chrome(
bus,
top,
left,
bottom,
right,
true,
highlighted,
is_default,
) {
self.draw_button_label(bus, top, left, bottom, right, title);
return;
}
self.invert_button_rect(bus, top, left, bottom, right);
}
/// Draw a checkbox item.
pub(crate) fn draw_checkbox(
&self,
bus: &mut MacMemoryBus,
top: i16,
left: i16,
bottom: i16,
_right: i16,
title: &str,
checked: bool,
) {
self.draw_checkbox_state(bus, top, left, bottom, _right, title, checked, true, false);
}
fn draw_checkbox_with_enabled(
&self,
bus: &mut MacMemoryBus,
top: i16,
left: i16,
bottom: i16,
_right: i16,
title: &str,
checked: bool,
enabled: bool,
) {
self.draw_checkbox_with_enabled_and_inactive(
bus, top, left, bottom, _right, title, checked, enabled, false,
);
}
fn draw_checkbox_with_enabled_and_inactive(
&self,
bus: &mut MacMemoryBus,
top: i16,
left: i16,
bottom: i16,
_right: i16,
title: &str,
checked: bool,
enabled: bool,
inactive: bool,
) {
if enabled && !inactive {
self.draw_checkbox(bus, top, left, bottom, _right, title, checked);
return;
}
self.draw_checkbox_state(
bus, top, left, bottom, _right, title, checked, enabled, inactive,
);
}
fn draw_checkbox_state(
&self,
bus: &mut MacMemoryBus,
top: i16,
left: i16,
bottom: i16,
_right: i16,
title: &str,
checked: bool,
enabled: bool,
inactive: bool,
) {
let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
self.get_screen_params();
let box_size = Self::STANDARD_CONTROL_MARK_SIZE;
// Standard dialog checkbox/radio controls are drawn by the System
// CDEF as a 12-pixel mark plus title inside the item rectangle
// (MTE 1992 pp. 5-4..5-5; HIG 1992 pp. 209..212).
let height = bottom - top;
let box_top = top + (height - box_size) / 2;
let box_left = left + Self::STANDARD_CONTROL_MARK_LEFT_INSET;
if !self.draw_theme_control_chrome(
bus,
ControlKind::Checkbox,
box_top,
box_left,
box_top + box_size,
box_left + box_size,
enabled && !inactive,
false,
checked,
false,
) {
// Draw checkbox box
Self::fb_fill_rect(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
box_top,
box_left,
box_top + box_size,
box_left + box_size,
false,
);
self.draw_rect_border(
bus,
box_top,
box_left,
box_top + box_size,
box_left + box_size,
);
if checked {
// Draw X inside
for i in 1..box_size - 1 {
Self::fb_set_pixel(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
box_left + i,
box_top + i,
true,
);
Self::fb_set_pixel(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
box_left + box_size - 1 - i,
box_top + i,
true,
);
}
}
}
// Draw label text to the right of the box. Checkbox/radio items are
// controls, and SetDialogFont/SetDAFont does not affect control
// titles (IM:I I-412; MTE 1992 p. 6-105).
let font_id = 0i16;
let font_size = 12i16;
let metrics = get_font_metrics(font_id, font_size);
let text_x = box_left + box_size + Self::STANDARD_CONTROL_TITLE_GAP;
let text_y = top + (height + metrics.ascent - metrics.descent) / 2;
let label_left = box_left + box_size + 1;
let dim_disabled_title = !enabled && self.ui_theme_id() != UiThemeId::ClassicSystem7;
self.draw_standard_control_title_text(
bus,
top,
label_left,
bottom,
_right,
text_x,
text_y,
title,
font_id,
font_size,
inactive,
dim_disabled_title,
);
}
/// Draw a radio button item.
pub(crate) fn draw_radio(
&self,
bus: &mut MacMemoryBus,
top: i16,
left: i16,
bottom: i16,
_right: i16,
title: &str,
selected: bool,
) {
self.draw_radio_state(bus, top, left, bottom, _right, title, selected, true, false);
}
fn draw_radio_with_enabled(
&self,
bus: &mut MacMemoryBus,
top: i16,
left: i16,
bottom: i16,
_right: i16,
title: &str,
selected: bool,
enabled: bool,
) {
self.draw_radio_with_enabled_and_inactive(
bus, top, left, bottom, _right, title, selected, enabled, false,
);
}
fn draw_radio_with_enabled_and_inactive(
&self,
bus: &mut MacMemoryBus,
top: i16,
left: i16,
bottom: i16,
_right: i16,
title: &str,
selected: bool,
enabled: bool,
inactive: bool,
) {
if enabled && !inactive {
self.draw_radio(bus, top, left, bottom, _right, title, selected);
return;
}
self.draw_radio_state(
bus, top, left, bottom, _right, title, selected, enabled, inactive,
);
}
fn draw_radio_state(
&self,
bus: &mut MacMemoryBus,
top: i16,
left: i16,
bottom: i16,
_right: i16,
title: &str,
selected: bool,
enabled: bool,
inactive: bool,
) {
let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
self.get_screen_params();
let mark_size = Self::STANDARD_CONTROL_MARK_SIZE;
let height = bottom - top;
let mark_top = top + (height - mark_size) / 2;
let mark_left = left + Self::STANDARD_CONTROL_MARK_LEFT_INSET;
if !self.draw_theme_control_chrome(
bus,
ControlKind::RadioButton,
mark_top,
mark_left,
mark_top + mark_size,
mark_left + mark_size,
enabled && !inactive,
false,
selected,
false,
) {
Self::fb_fill_rect(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
mark_top,
mark_left,
mark_top + mark_size,
mark_left + mark_size,
false,
);
// Radio buttons are small circles, with a dot when selected
// (IM:I I-312; MTE 1992 pp. 5-5..5-6; HIG 1992 pp. 209..210).
self.draw_control_mask_12(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
mark_left,
mark_top,
&Self::CLASSIC_RADIO_OUTLINE_12,
);
if selected {
self.draw_control_mask_12(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
mark_left,
mark_top,
&Self::CLASSIC_RADIO_DOT_12,
);
}
}
// Label
// SetDialogFont/SetDAFont affects statText and editText items but
// not control titles (IM:I I-412; MTE 1992 p. 6-105).
let font_id = 0i16;
let font_size = 12i16;
let metrics = get_font_metrics(font_id, font_size);
let text_x = mark_left + mark_size + Self::STANDARD_CONTROL_TITLE_GAP;
let text_y = top + (height + metrics.ascent - metrics.descent) / 2;
let label_left = mark_left + mark_size + 1;
let dim_disabled_title = !enabled && self.ui_theme_id() != UiThemeId::ClassicSystem7;
self.draw_standard_control_title_text(
bus,
top,
label_left,
bottom,
_right,
text_x,
text_y,
title,
font_id,
font_size,
inactive,
dim_disabled_title,
);
}
fn draw_standard_control_title_text(
&self,
bus: &mut MacMemoryBus,
label_top: i16,
label_left: i16,
label_bottom: i16,
label_right: i16,
text_x: i16,
text_y: i16,
title: &str,
font_id: i16,
font_size: i16,
inactive: bool,
dim_disabled_title: bool,
) {
let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
self.get_screen_params();
if inactive && pixel_size == 8 {
// Standard DITL controls are backed by ControlRecords. When a
// game calls HiliteControl(255), their titles follow inactive
// Control Manager rendering; itemDisable alone stays visually
// active in classic dialogs.
let [r, g, b] = Self::INACTIVE_STANDARD_CONTROL_TITLE_RGB;
let gray_index = super::pict::closest_clut_index(r, g, b, &self.device_clut);
Self::fb_draw_string_styled_index(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
text_x,
text_y,
title,
font_id,
font_size,
0,
gray_index,
);
return;
}
Self::fb_draw_string(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
text_x,
text_y,
title,
font_id,
font_size,
);
if inactive || dim_disabled_title {
self.dim_rect(bus, label_top, label_left, label_bottom, label_right);
}
}
fn draw_control_mask_12(
&self,
bus: &mut MacMemoryBus,
screen_base: u32,
row_bytes: u32,
pixel_size: u16,
screen_width: i16,
screen_height: i16,
left: i16,
top: i16,
mask: &[&str; 12],
) {
for (dy, row) in mask.iter().enumerate() {
for (dx, pixel) in row.as_bytes().iter().enumerate() {
if *pixel == b'#' {
Self::fb_set_pixel(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
left + dx as i16,
top + dy as i16,
true,
);
}
}
}
}
/// Replace `^0`..`^3` in dialog/alert text with the strings most
/// recently passed to `ParamText`. Returns a borrowed reference to
/// the original `text` when no placeholders are present (the common
/// case — most DITL items don't use ParamText), avoiding an
/// allocation per draw_static_text call.
/// Inside Macintosh Volume I, I-422.
pub(crate) fn apply_param_text<'a>(&self, text: &'a str) -> std::borrow::Cow<'a, str> {
if !text.contains('^') {
return std::borrow::Cow::Borrowed(text);
}
let mut out = String::with_capacity(text.len());
let mut chars = text.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '^' {
if let Some(&next) = chars.peek() {
if let Some(idx) = next.to_digit(10) {
if (idx as usize) < self.param_text.len() {
chars.next();
out.push_str(&decode_mac_roman_for_render(
&self.param_text[idx as usize],
));
continue;
}
}
}
}
out.push(ch);
}
std::borrow::Cow::Owned(out)
}
fn draw_static_text(
&self,
bus: &mut MacMemoryBus,
top: i16,
left: i16,
bottom: i16,
right: i16,
text: &str,
) {
let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
self.get_screen_params();
// SetDialogFont/SetDAFont affects statText and editText items but
// not control titles (IM:I I-412; MTE 1992 p. 6-105).
let font_id = self.tx_font;
let font_size = Self::font_lookup_size(self.tx_size);
let metrics = get_font_metrics(font_id, font_size);
let line_height = metrics.ascent + metrics.descent + metrics.leading;
let max_width = (right - left).saturating_sub(Self::TE_LINE_LEFT_INSET);
let mut y = top + metrics.ascent;
if y >= bottom && bottom > top {
y = bottom - 1;
}
let substituted = self.apply_param_text(text);
let text_bytes = substituted.as_bytes();
let lines = self.te_wrap_lines(font_id, self.tx_size, text_bytes, max_width);
for (start, end) in lines {
let mut trimmed_end = end;
while trimmed_end > start && matches!(text_bytes[trimmed_end - 1], b' ' | b'\r' | b'\n')
{
trimmed_end -= 1;
}
if trimmed_end > start && y <= bottom {
// IM:I I-405 to I-406: statText draws like editText text
// inside the display rectangle, including wrap and clipping,
// but without the editText frame.
let line: String = text_bytes[start..trimmed_end]
.iter()
.map(|&byte| byte as char)
.collect();
Self::fb_draw_string(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
left + Self::TE_LINE_LEFT_INSET,
y,
&line,
font_id,
font_size,
);
}
y += line_height;
if y > bottom {
break;
}
}
}
/// Draw an editable text field with border and text.
pub(crate) fn draw_edit_text(
&self,
bus: &mut MacMemoryBus,
top: i16,
left: i16,
bottom: i16,
right: i16,
text: &str,
selected: bool,
) {
let selection_range = selected.then_some((0, text.len()));
self.draw_edit_text_with_cursor(
bus,
top,
left,
bottom,
right,
text,
selection_range,
!selected,
true,
);
}
fn draw_edit_text_with_cursor(
&self,
bus: &mut MacMemoryBus,
top: i16,
left: i16,
bottom: i16,
right: i16,
text: &str,
selection_range: Option<(usize, usize)>,
show_cursor: bool,
enabled: bool,
) {
let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
self.get_screen_params();
// IM:I I-414 and HIG 1992 p. 184: the active editText item is a
// text-entry field with visible focus feedback. The theme provider owns
// that field chrome only; TextEdit metrics, text drawing, selection,
// and caret positioning remain classic guest-visible behavior.
let frame_top = top - Self::EDIT_TEXT_FRAME_OUTSET;
let frame_left = left - Self::EDIT_TEXT_FRAME_OUTSET;
let frame_bottom = bottom + Self::EDIT_TEXT_FRAME_OUTSET;
let frame_right = right + Self::EDIT_TEXT_FRAME_OUTSET;
if !self.draw_theme_text_field(
bus,
frame_top,
frame_left,
frame_bottom,
frame_right,
enabled,
selection_range.is_some() || show_cursor,
) {
// White fill
Self::fb_fill_rect(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
frame_top,
frame_left,
frame_bottom,
frame_right,
false,
);
// IM:I I-405: editText's display rectangle becomes the TextEdit
// view/dest rectangle, and Dialog Manager frames it three pixels
// outside that display rectangle.
self.draw_rect_border(bus, frame_top, frame_left, frame_bottom, frame_right);
}
let font_id = self.tx_font;
let font_size = Self::font_lookup_size(self.tx_size);
let metrics = get_font_metrics(font_id, font_size);
let text_y = top + metrics.ascent;
Self::fb_draw_string(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
left + 1,
text_y,
text,
font_id,
font_size,
);
if let Some((selection_start, selection_end)) = selection_range {
// IM:I I-422 and MTE 1992 p. 6-131: SelIText /
// SelectDialogItemText displays the selected range by inverting
// the selected characters. Match TextEdit's left-justified
// selection rectangle: glyphs draw at destRect.left+1, but a
// selection beginning at character 0 includes the one-pixel inset.
if !text.is_empty() {
let text_bytes = text.as_bytes();
let start = selection_start.min(text_bytes.len());
let end = selection_end.min(text_bytes.len());
if start < end {
let line_height = metrics.ascent + metrics.descent + metrics.leading;
let selection_top = top;
let selection_bottom = top.saturating_add(line_height).min(bottom);
let selection_left = if start == 0 {
left
} else {
left + Self::TE_LINE_LEFT_INSET
+ self.te_measure_text_width(
font_id,
self.tx_size,
text_bytes,
0,
start,
)
};
let selection_right = if end >= text_bytes.len() {
right
} else {
left + Self::TE_LINE_LEFT_INSET
+ self.te_measure_text_width(font_id, self.tx_size, text_bytes, 0, end)
};
if selection_left < selection_right && selection_top < selection_bottom {
if self.ui_theme_id() == UiThemeId::ClassicSystem7 {
self.invert_rect_exact(
bus,
selection_top,
selection_left,
selection_bottom,
selection_right,
);
} else {
self.draw_theme_text_selection(
bus,
selection_top,
selection_left,
selection_bottom,
selection_right,
true,
);
}
}
}
}
} else if show_cursor {
// Draw cursor bar at end of text
let text_width = Self::fb_measure_string(text, font_id, font_size);
let cursor_x = left + 3 + text_width;
if cursor_x < right - 1
&& !self.draw_theme_caret(bus, top + 2, cursor_x, bottom - 1, cursor_x + 1)
{
for y in (top + 2)..=(bottom - 2) {
Self::fb_set_pixel(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
cursor_x,
y,
true,
);
}
}
}
}
fn dialog_cicn_layout(bus: &MacMemoryBus, icon_ptr: u32) -> Option<DialogCIconLayout> {
if bus.get_alloc_size(icon_ptr).is_some_and(|size| size < 82) {
return None;
}
// Imaging With QuickDraw 1994 p. 4-106: a compiled 'cicn'
// resource starts with a 50-byte PixMap, 14-byte mask BitMap,
// 14-byte 1-bit fallback BitMap, 4-byte iconData handle, then
// mask bits, fallback bitmap bits, ColorTable, and PixMap data.
let pm_row_bytes = (bus.read_word(icon_ptr + 4) & 0x3FFF) as u32;
let pm_top = bus.read_word(icon_ptr + 6) as i16;
let pm_left = bus.read_word(icon_ptr + 8) as i16;
let pm_bottom = bus.read_word(icon_ptr + 10) as i16;
let pm_right = bus.read_word(icon_ptr + 12) as i16;
let pixel_size = bus.read_word(icon_ptr + 32);
let mask_row_bytes = (bus.read_word(icon_ptr + 54) & 0x3FFF) as u32;
let bmap_row_bytes = (bus.read_word(icon_ptr + 68) & 0x3FFF) as u32;
let width = pm_right - pm_left;
let height = pm_bottom - pm_top;
if width <= 0 || height <= 0 || pm_row_bytes == 0 || mask_row_bytes == 0 {
return None;
}
let height_u32 = height as u32;
let mask_data_size = mask_row_bytes.checked_mul(height_u32)?;
let bmap_data_size = bmap_row_bytes.checked_mul(height_u32)?;
let mask_data_ptr = icon_ptr + 82;
let bmap_data_ptr = mask_data_ptr.checked_add(mask_data_size)?;
let ctab_ptr = bmap_data_ptr.checked_add(bmap_data_size)?;
if let Some(resource_size) = bus.get_alloc_size(icon_ptr) {
let resource_end = icon_ptr.checked_add(resource_size)?;
if ctab_ptr.checked_add(8)? > resource_end {
return None;
}
}
let ct_size = bus.read_word(ctab_ptr + 6) as u32;
let ctab_total_bytes = 8u32.checked_add((ct_size + 1).checked_mul(8)?)?;
let pixel_data_ptr = ctab_ptr.checked_add(ctab_total_bytes)?;
if let Some(resource_size) = bus.get_alloc_size(icon_ptr) {
let resource_end = icon_ptr.checked_add(resource_size)?;
let pixel_data_size = pm_row_bytes.checked_mul(height_u32)?;
if pixel_data_ptr.checked_add(pixel_data_size)? > resource_end {
return None;
}
}
Some(DialogCIconLayout {
width,
height,
pm_row_bytes,
mask_row_bytes,
bmap_row_bytes,
pixel_size,
mask_data_ptr,
bmap_data_ptr,
pixel_data_ptr,
})
}
fn dialog_cicn_pixel_index(
bus: &MacMemoryBus,
data_ptr: u32,
row_bytes: u32,
pixel_size: u16,
x: u32,
y: u32,
) -> Option<u8> {
let row_ptr = data_ptr.checked_add(y.checked_mul(row_bytes)?)?;
match pixel_size {
1 => {
let byte = bus.read_byte(row_ptr.checked_add(x / 8)?);
Some(((byte >> (7 - (x % 8))) & 1) as u8)
}
2 => {
let byte = bus.read_byte(row_ptr.checked_add(x / 4)?);
Some((byte >> (6 - 2 * (x % 4))) & 0x03)
}
4 => {
let byte = bus.read_byte(row_ptr.checked_add(x / 2)?);
Some(if x % 2 == 0 {
(byte >> 4) & 0x0F
} else {
byte & 0x0F
})
}
8 => Some(bus.read_byte(row_ptr.checked_add(x)?)),
size if size > 8 && size % 8 == 0 => {
let bytes_per_pixel = u32::from(size / 8);
Some(bus.read_byte(row_ptr.checked_add(x.checked_mul(bytes_per_pixel)?)?))
}
_ => None,
}
}
fn draw_cicn_icon(
&self,
bus: &mut MacMemoryBus,
top: i16,
left: i16,
bottom: i16,
right: i16,
icon_ptr: u32,
) -> bool {
let Some(layout) = Self::dialog_cicn_layout(bus, icon_ptr) else {
return false;
};
let (screen_base, row_bytes, screen_width, screen_height, screen_pixel_size) =
self.get_screen_params();
let dst_w = right - left;
let dst_h = bottom - top;
if dst_w <= 0 || dst_h <= 0 {
return false;
}
for dy in 0..dst_h {
let sy = (dy as i32 * i32::from(layout.height) / i32::from(dst_h)) as u32;
let dst_y = top + dy;
for dx in 0..dst_w {
let sx = (dx as i32 * i32::from(layout.width) / i32::from(dst_w)) as u32;
let dst_x = left + dx;
let mask_byte =
bus.read_byte(layout.mask_data_ptr + sy * layout.mask_row_bytes + sx / 8);
if (mask_byte & (0x80 >> (sx % 8))) == 0 {
continue;
}
if screen_pixel_size == 8 && layout.pixel_size >= 2 {
let Some(pixel_index) = Self::dialog_cicn_pixel_index(
bus,
layout.pixel_data_ptr,
layout.pm_row_bytes,
layout.pixel_size,
sx,
sy,
) else {
continue;
};
if dst_x >= 0 && dst_y >= 0 && dst_x < screen_width && dst_y < screen_height {
bus.write_byte(
screen_base + (dst_y as u32) * row_bytes + dst_x as u32,
pixel_index,
);
}
continue;
}
let source_data_ptr = if layout.bmap_row_bytes != 0 {
layout.bmap_data_ptr
} else {
layout.pixel_data_ptr
};
let source_row_bytes = if layout.bmap_row_bytes != 0 {
layout.bmap_row_bytes
} else {
layout.pm_row_bytes
};
let source_pixel_size = if layout.bmap_row_bytes != 0 {
1
} else {
layout.pixel_size
};
let Some(pixel_index) = Self::dialog_cicn_pixel_index(
bus,
source_data_ptr,
source_row_bytes,
source_pixel_size,
sx,
sy,
) else {
continue;
};
Self::fb_set_pixel(
bus,
screen_base,
row_bytes,
screen_pixel_size,
screen_width,
screen_height,
dst_x,
dst_y,
pixel_index != 0,
);
}
}
true
}
/// Draw a 32x32 1-bit ICON resource, scaled to fit the display rect.
/// Inside Macintosh Volume I, I-205
fn draw_icon(
&self,
bus: &mut MacMemoryBus,
top: i16,
left: i16,
bottom: i16,
right: i16,
icon_ptr: u32,
) {
let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
self.get_screen_params();
let dst_w = right - left;
let dst_h = bottom - top;
// ICON is 32x32 pixels, 1 bit per pixel, 4 bytes per row = 128 bytes
for row in 0..32i16 {
let row_data = bus.read_long(icon_ptr + (row as u32) * 4);
for col in 0..32i16 {
let bit = (row_data >> (31 - col)) & 1;
if bit != 0 {
// Scale to destination rect
let dx = left + col * dst_w / 32;
let dy = top + row * dst_h / 32;
Self::fb_set_pixel(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
dx,
dy,
true,
);
}
}
}
}
/// Invert the pixels within a button rect (for flash animation).
pub(crate) fn invert_button_rect(
&self,
bus: &mut MacMemoryBus,
top: i16,
left: i16,
bottom: i16,
right: i16,
) {
let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
self.get_screen_params();
let y_start = (top as i32 + 1).clamp(0, screen_height as i32);
let y_end = (bottom as i32 - 1).clamp(0, screen_height as i32);
let x_start = (left as i32 + 1).clamp(0, screen_width as i32);
let x_end = (right as i32 - 1).clamp(0, screen_width as i32);
if y_start >= y_end || x_start >= x_end {
return;
}
for y in y_start..y_end {
for x in x_start..x_end {
if pixel_size == 8 {
let addr = screen_base + (y as u32) * row_bytes + (x as u32);
let b = bus.read_byte(addr);
bus.write_byte(addr, 255 - b);
} else {
let byte_offset = (y as u32) * row_bytes + (x as u32 / 8);
let bit = 7 - (x as u32 % 8);
let addr = screen_base + byte_offset;
let b = bus.read_byte(addr);
bus.write_byte(addr, b ^ (1 << bit));
}
}
}
}
fn invert_rect_exact(
&self,
bus: &mut MacMemoryBus,
top: i16,
left: i16,
bottom: i16,
right: i16,
) {
let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
self.get_screen_params();
let y_start = (top as i32).clamp(0, screen_height as i32);
let y_end = (bottom as i32).clamp(0, screen_height as i32);
let x_start = (left as i32).clamp(0, screen_width as i32);
let x_end = (right as i32).clamp(0, screen_width as i32);
if y_start >= y_end || x_start >= x_end {
return;
}
for y in y_start..y_end {
for x in x_start..x_end {
if pixel_size == 8 {
let addr = screen_base + (y as u32) * row_bytes + (x as u32);
let b = bus.read_byte(addr);
bus.write_byte(addr, 255 - b);
} else {
let byte_offset = (y as u32) * row_bytes + (x as u32 / 8);
let bit = 7 - (x as u32 % 8);
let addr = screen_base + byte_offset;
let b = bus.read_byte(addr);
bus.write_byte(addr, b ^ (1 << bit));
}
}
}
}
/// Hit-test a point against dialog items. Returns 1-based item number or 0.
fn dialog_item_hit_test(
&self,
bus: &MacMemoryBus,
items: &[DialogItem],
bounds: (i16, i16, i16, i16),
screen_v: i16,
screen_h: i16,
popup_original_rects: &std::collections::HashMap<(u32, i16), (i16, i16, i16, i16)>,
dialog_ptr: u32,
) -> i16 {
let (top, left, _, _) = bounds;
let local_v = screen_v - top;
let local_h = screen_h - left;
for (i, item) in items.iter().enumerate() {
let item_no = (i + 1) as i16;
// For popup userItems, use the original DITL rect (before SetDItem
// narrowed it) so clicks on the full popup area register.
let mut rect = popup_original_rects
.get(&(dialog_ptr, item_no))
.copied()
.unwrap_or(item.rect);
let base_type = item.item_type & 0x7F;
if (4..=7).contains(&base_type) {
if let Some(ctrl_handle) = self.dialog_control_handle_for_item(dialog_ptr, item_no)
{
let ctrl_ptr = bus.read_long(ctrl_handle);
if ctrl_ptr != 0 && bus.read_byte(ctrl_ptr + 16) != 0 {
rect = (
bus.read_word(ctrl_ptr + 8) as i16,
bus.read_word(ctrl_ptr + 10) as i16,
bus.read_word(ctrl_ptr + 12) as i16,
bus.read_word(ctrl_ptr + 14) as i16,
);
}
}
}
let (it, il, ib, ir) = rect;
if local_v >= it && local_v < ib && local_h >= il && local_h < ir {
return item_no;
}
}
0
}
fn dialog_button_hit_test(
items: &[DialogItem],
bounds: (i16, i16, i16, i16),
screen_v: i16,
screen_h: i16,
) -> i16 {
for (i, item) in items.iter().enumerate() {
let base_type = item.item_type & 0x7F;
let is_disabled = (item.item_type & 0x80) != 0;
if base_type != 4 || is_disabled {
continue;
}
let (top, left, bottom, right) = Self::dialog_item_screen_rect(bounds, item.rect);
if screen_v >= top && screen_v < bottom && screen_h >= left && screen_h < right {
return (i + 1) as i16;
}
}
0
}
fn is_plain_modal_user_item(
&self,
tracking: &DialogTrackingState,
item_no: i16,
item: &DialogItem,
) -> bool {
let base_type = item.item_type & 0x7F;
let is_disabled = (item.item_type & 0x80) != 0;
base_type == 0
&& !is_disabled
&& item.proc_ptr == 0
&& !tracking.game_managed
&& !self
.dialog_item_popup_menus
.contains_key(&(tracking.dialog_ptr, item_no))
&& !self
.dialog_popup_candidate_items
.contains(&(tracking.dialog_ptr, item_no))
}
fn dialog_plain_user_item_hit_test(
&self,
tracking: &DialogTrackingState,
screen_v: i16,
screen_h: i16,
) -> i16 {
for (i, item) in tracking.items.iter().enumerate() {
let item_no = (i + 1) as i16;
if !self.is_plain_modal_user_item(tracking, item_no, item) {
continue;
}
let (top, left, bottom, right) =
Self::dialog_item_screen_rect(tracking.bounds, item.rect);
if screen_v >= top && screen_v < bottom && screen_h >= left && screen_h < right {
return item_no;
}
}
0
}
pub(crate) fn pending_dialog_plain_user_item_mouse_down(&self) -> bool {
let Some(tracking) = self.dialog_tracking.as_ref() else {
return false;
};
if tracking.active_user_item.is_some() {
return true;
}
let Some(event) = self
.event_queue
.iter()
.find(|event| matches!(event.what, 1 | 2 | 3 | 4 | 6))
else {
return false;
};
event.what == 1
&& self.dialog_plain_user_item_hit_test(tracking, event.where_v, event.where_h) > 0
}
pub(crate) fn mouse_down_over_dialog_button(&self) -> bool {
if !self.mouse_button {
return false;
}
let Some(tracking) = self.dialog_tracking.as_ref() else {
return false;
};
Self::dialog_button_hit_test(
&tracking.items,
tracking.bounds,
self.mouse_pos.0,
self.mouse_pos.1,
) > 0
}
pub(crate) fn mouse_down_over_dialog_plain_user_item(&self) -> bool {
if !self.mouse_button {
return false;
}
let Some(tracking) = self.dialog_tracking.as_ref() else {
return false;
};
tracking.active_user_item.is_some()
|| self.dialog_plain_user_item_hit_test(tracking, self.mouse_pos.0, self.mouse_pos.1)
> 0
}
fn read_guest_event_record(bus: &MacMemoryBus, event_ptr: u32) -> (u16, u32, i16, i16, u16) {
if event_ptr == 0 {
return (0, 0, 0, 0, 0);
}
(
bus.read_word(event_ptr),
bus.read_long(event_ptr + 2),
bus.read_word(event_ptr + 10) as i16,
bus.read_word(event_ptr + 12) as i16,
bus.read_word(event_ptr + 14),
)
}
fn front_dialog_ptr(&self) -> Option<u32> {
let dialog_ptr = self.front_window;
if dialog_ptr != 0 && self.dialog_items.contains_key(&dialog_ptr) {
Some(dialog_ptr)
} else {
None
}
}
fn front_retained_modal_dialog(
&self,
bus: &MacMemoryBus,
) -> Option<(u32, (i16, i16, i16, i16))> {
if self.dialog_tracking.is_some() {
return None;
}
let dialog_ptr = self.front_dialog_ptr()?;
// This retained-click path is only for dialogs whose event handling
// has already entered our ModalDialog HLE and returned with the
// dialog still visible. Dialogs created by GetNewDialog/NewDialog
// but handled by the application through WaitNextEvent, DialogSelect,
// or custom Control/TextEdit/Window Manager code must keep receiving
// their queued events so the app can decide how to respond.
// Macintosh Toolbox Essentials 1992, pp. 6-136, 6-138..6-141.
if !self.dialog_modal_entered.contains(&dialog_ptr) {
return None;
}
if !self.window_visible(bus, dialog_ptr) {
return None;
}
let proc_id = self.dialog_window_proc_id(bus, dialog_ptr);
// Dialog-box WDEFs 1/2/3 are modal-box variants; 5 is the
// movable modal dialog. noGrowDocProc (4) is modeless and must
// continue to pass events to the application.
// Inside Macintosh Volume I, I-273; Macintosh Toolbox Essentials
// 1992, p. 6-137.
if !matches!(proc_id, 1 | 2 | 3 | 5) {
return None;
}
Some((dialog_ptr, Self::dialog_screen_bounds(bus, dialog_ptr)))
}
fn front_app_owned_modal_dialog(
&self,
bus: &MacMemoryBus,
) -> Option<(u32, (i16, i16, i16, i16))> {
if self.dialog_tracking.is_some() || self.retained_modal_dialog_click.is_some() {
return None;
}
let dialog_ptr = self.front_dialog_ptr()?;
if self.dialog_modal_entered.contains(&dialog_ptr) || !self.window_visible(bus, dialog_ptr)
{
return None;
}
let proc_id = self.dialog_window_proc_id(bus, dialog_ptr);
if !matches!(proc_id, 1 | 2 | 3 | 5) {
return None;
}
Some((dialog_ptr, Self::dialog_screen_bounds(bus, dialog_ptr)))
}
pub(crate) fn begin_app_owned_modal_dialog_button_tracking(
&mut self,
bus: &mut MacMemoryBus,
event: &QueuedEvent,
) {
if event.what != 1 {
return;
}
let Some((dialog_ptr, bounds)) = self.front_app_owned_modal_dialog(bus) else {
return;
};
if !Self::dialog_contains_screen_point(bounds, event.where_v, event.where_h) {
return;
}
let Some(mut items) = self.dialog_items.get(&dialog_ptr).cloned() else {
return;
};
Self::refresh_ditl_proc_ptrs(bus, dialog_ptr, &mut items);
let mut hit = self.dialog_item_hit_test(
bus,
&items,
bounds,
event.where_v,
event.where_h,
&self.dialog_popup_original_rects,
dialog_ptr,
);
if hit <= 0 {
hit = Self::dialog_button_hit_test(&items, bounds, event.where_v, event.where_h);
}
self.dialog_items.insert(dialog_ptr, items.clone());
if hit <= 0 {
return;
}
let item = &items[(hit - 1) as usize];
let base_type = item.item_type & 0x7F;
let is_disabled = (item.item_type & 0x80) != 0;
if base_type != 4 || is_disabled {
return;
}
// Some games create a modal DLOG and run their own WaitNextEvent
// loop. They still expect the Dialog Manager's standard push-button
// press/release affordance for modal WDEFs, but the mouseDown must
// remain deliverable to app code. Track only enabled DITL buttons and
// finish the press on mouseUp; DialogSelect cancels this state if the
// app explicitly asks the Dialog Manager to handle the event.
// Macintosh Toolbox Essentials 1992, pp. 6-136, 6-138..6-141.
let rect = Self::dialog_item_screen_rect(bounds, item.rect);
self.invert_button_rect(bus, rect.0, rect.1, rect.2, rect.3);
self.retained_modal_dialog_click = Some(RetainedModalDialogClickState {
dialog_ptr,
item_no: hit,
rect: item.rect,
title: item.text.clone(),
is_default: false,
highlighted: true,
delivered_to_app: true,
});
}
fn cancel_app_owned_modal_dialog_button_tracking(
&mut self,
bus: &mut MacMemoryBus,
dialog_ptr: u32,
) {
let should_cancel = self
.retained_modal_dialog_click
.as_ref()
.is_some_and(|click| click.dialog_ptr == dialog_ptr && click.delivered_to_app);
if !should_cancel {
return;
}
let Some(click) = self.retained_modal_dialog_click.take() else {
return;
};
if click.item_no > 0 && click.highlighted {
let bounds = Self::dialog_screen_bounds(bus, click.dialog_ptr);
let rect = Self::dialog_item_screen_rect(bounds, click.rect);
self.invert_button_rect(bus, rect.0, rect.1, rect.2, rect.3);
}
}
fn dialog_from_window_event(&self, what: u16, message: u32) -> Option<u32> {
match what {
6 | 8 if self.dialog_items.contains_key(&message) => Some(message),
_ => self.front_dialog_ptr(),
}
}
fn dialog_contains_screen_point(bounds: (i16, i16, i16, i16), v: i16, h: i16) -> bool {
v >= bounds.0 && v < bounds.2 && h >= bounds.1 && h < bounds.3
}
fn point_in_screen_rect(v: i16, h: i16, rect: (i16, i16, i16, i16)) -> bool {
v >= rect.0 && v < rect.2 && h >= rect.1 && h < rect.3
}
fn close_dialog_window<C: CpuOps>(
&mut self,
bus: &mut MacMemoryBus,
cpu: &mut C,
dialog_ptr: u32,
dispose_storage: bool,
) {
let was_front = self.front_window == dialog_ptr;
let retained_visible_bounds = self
.dialog_visible_snapshots
.get(&dialog_ptr)
.map(|snapshot| snapshot.bounds);
let dialog_record_bounds = if dialog_ptr != 0 && self.dialog_items.contains_key(&dialog_ptr)
{
Self::dialog_screen_bounds(bus, dialog_ptr)
} else {
self.window_bounds
};
let exposed_rect = if was_front {
self.window_bounds
} else {
retained_visible_bounds.unwrap_or(dialog_record_bounds)
};
let initial_draw_deferred = self.dialog_initial_draw_deferred.remove(&dialog_ptr);
let retained_stale_saved_under = self
.dialog_saved_pixels
.get(&dialog_ptr)
.is_some_and(|saved| self.saved_pixels_are_mostly_logical_white(saved));
let mut restored_stale_saved_under = false;
if was_front && !initial_draw_deferred {
if let Some(saved) = self.dialog_saved_pixels.remove(&dialog_ptr) {
restored_stale_saved_under = self.saved_pixels_are_mostly_logical_white(&saved);
self.restore_dialog_pixels(bus, self.window_bounds, &saved);
}
} else {
self.dialog_saved_pixels.remove(&dialog_ptr);
}
self.dialog_visible_snapshots.remove(&dialog_ptr);
self.dialog_modal_entered.remove(&dialog_ptr);
if !was_front && retained_stale_saved_under && self.screen_is_hidden_menu_game_surface(bus)
{
let _ = self.restore_dialog_exposure_from_fullscreen_offscreen_port(bus, exposed_rect);
}
if self.pending_modal_button_dispose_dialog == Some(dialog_ptr) {
self.pending_modal_button_dispose_dialog = None;
}
if self
.retained_modal_dialog_click
.as_ref()
.is_some_and(|click| click.dialog_ptr == dialog_ptr)
{
self.retained_modal_dialog_click = None;
}
self.dispose_dialog_owned_items(bus, dialog_ptr);
if dispose_storage {
self.dispose_dialog_record_and_item_list(bus, dialog_ptr);
}
self.untrack_window(bus, dialog_ptr);
if was_front {
if let Some((prev_window, prev_bounds, prev_proc_id, prev_title)) =
self.window_stack.pop()
{
self.set_current_port_state(bus, cpu, prev_window, None);
self.front_window = prev_window;
self.window_bounds = prev_bounds;
self.window_proc_id = prev_proc_id;
self.window_title = prev_title;
if prev_window != 0 {
bus.write_byte(prev_window + 111, 0xFF);
if self.window_visible(bus, prev_window) {
self.blit_window_to_screen(bus);
self.blit_large_manual_cport_to_screen(bus);
}
if !self.event_queue.iter().any(|event| {
event.what == 8
&& event.message == prev_window
&& (event.modifiers & 1) != 0
}) {
self.event_queue
.push_back(crate::trap::dispatch::QueuedEvent {
what: 8,
message: prev_window,
where_v: 0,
where_h: 0,
modifiers: 1,
});
}
self.draw_single_window_chrome_inline(bus, prev_window, true);
let exposed_local = (
exposed_rect.0.saturating_sub(prev_bounds.0),
exposed_rect.1.saturating_sub(prev_bounds.1),
exposed_rect.2.saturating_sub(prev_bounds.0),
exposed_rect.3.saturating_sub(prev_bounds.1),
);
self.invalidate_window_rect(bus, prev_window, exposed_local);
self.queue_window_update_event(prev_window);
}
let should_repair_stale_exposure = restored_stale_saved_under
&& if prev_window != 0 {
self.promoted_window_is_fullscreen_game_surface(bus, prev_bounds)
} else {
self.screen_is_hidden_menu_game_surface(bus)
};
if should_repair_stale_exposure {
let _ = self
.restore_dialog_exposure_from_fullscreen_offscreen_port(bus, exposed_rect);
}
}
}
}
fn dialog_saved_white_like_count(&self, saved: &[u8]) -> usize {
let (_, _, _, _, pixel_size) = self.get_screen_params();
match pixel_size {
8 => saved
.iter()
.filter(|&&idx| {
let [r, g, b] = self.device_clut[idx as usize];
r >= 0xEEEE && g >= 0xEEEE && b >= 0xEEEE
})
.count(),
_ => saved.iter().filter(|&&byte| byte == 0).count(),
}
}
fn saved_pixels_are_mostly_logical_white(&self, saved: &[u8]) -> bool {
let (_, _, _, _, pixel_size) = self.get_screen_params();
pixel_size == 8
&& !saved.is_empty()
&& self
.dialog_saved_white_like_count(saved)
.saturating_mul(100)
>= saved.len().saturating_mul(80)
}
fn promoted_window_is_fullscreen_game_surface(
&self,
bus: &MacMemoryBus,
bounds: (i16, i16, i16, i16),
) -> bool {
let (_, _, screen_w, screen_h, _) = self.get_screen_params();
let menu_bar_height = bus.read_word(crate::memory::globals::addr::MBAR_HEIGHT) as i16;
bounds.0 <= 0
&& bounds.1 <= 0
&& bounds.2 >= screen_h
&& bounds.3 >= screen_w
&& (self.menu_bar_hidden || self.fullscreen_locked || menu_bar_height == 0)
}
fn screen_is_hidden_menu_game_surface(&self, bus: &MacMemoryBus) -> bool {
let menu_bar_height = bus.read_word(crate::memory::globals::addr::MBAR_HEIGHT) as i16;
self.menu_bar_hidden || self.fullscreen_locked || menu_bar_height == 0
}
fn restore_dialog_exposure_from_fullscreen_offscreen_port(
&self,
bus: &mut MacMemoryBus,
bounds: (i16, i16, i16, i16),
) -> bool {
let (screen_base, screen_row_bytes, screen_w, screen_h, pixel_size) =
self.get_screen_params();
if pixel_size != 8 || screen_w == 0 || screen_h == 0 {
return false;
}
let (top, left, bottom, right) = Self::dialog_saved_pixel_rect(bounds);
let top = top.max(0).min(screen_h);
let left = left.max(0).min(screen_w);
let bottom = bottom.max(0).min(screen_h);
let right = right.max(0).min(screen_w);
if top >= bottom || left >= right {
return false;
}
let mut ports = Vec::new();
for port in self
.cport_ports
.iter()
.copied()
.chain(self.gworld_devices.keys().copied())
{
if !ports.contains(&port) {
ports.push(port);
}
}
let mut best: Option<FullscreenOffscreenDialogRestoreCandidate> = None;
for port in ports {
let Some(mut candidate) =
self.fullscreen_offscreen_dialog_restore_candidate(bus, port, screen_base)
else {
continue;
};
if candidate.top > 0
|| candidate.left > 0
|| candidate.bottom < screen_h
|| candidate.right < screen_w
{
continue;
}
let Some(score) = self.score_offscreen_dialog_restore_candidate(
bus,
candidate,
(top, left, bottom, right),
) else {
continue;
};
candidate.score = score;
if best.map(|current| score > current.score).unwrap_or(true) {
best = Some(candidate);
}
}
let Some(candidate) = best else {
return false;
};
let screen_ctab_handle = Self::gdevice_ctab_handle(bus, self.main_gdevice_handle);
let src_ctab_seed = Self::ctab_seed(bus, candidate.ctab_handle);
let dst_ctab_seed = Self::ctab_seed(bus, screen_ctab_handle);
let src_clut = self.read_port_clut(bus, candidate.ctab_handle);
let dst_clut = self.read_port_clut(bus, screen_ctab_handle);
let hardware_palette_active = self.device_clut != self.color_manager_clut;
let skip_canonical_to_screen = Self::uses_canonical_system_8bpp_clut(&src_clut);
let translation = if candidate.ctab_handle != screen_ctab_handle
&& matches!(src_ctab_seed, Some(src_seed) if src_seed != 0)
&& src_ctab_seed != dst_ctab_seed
&& !skip_canonical_to_screen
&& !hardware_palette_active
{
Some(self.build_palette_translation(bus, &src_clut, &dst_clut, screen_ctab_handle))
} else {
None
};
let width = (right - left) as u32;
for y in top..bottom {
let src_y = (y - candidate.top) as u32;
let src_x = (left - candidate.left) as u32;
let dst_addr = screen_base + (y as u32) * screen_row_bytes + left as u32;
let src_addr = candidate.base + src_y * candidate.row_bytes + src_x;
if let Some(translation) = translation.as_ref() {
for col in 0..width {
let src_idx = bus.read_byte(src_addr + col);
bus.write_byte(dst_addr + col, translation[src_idx as usize]);
}
} else {
let row = bus.read_bytes(src_addr, width as usize);
bus.write_bytes(dst_addr, &row);
}
}
true
}
fn fullscreen_offscreen_dialog_restore_candidate(
&self,
bus: &MacMemoryBus,
port: u32,
screen_base: u32,
) -> Option<FullscreenOffscreenDialogRestoreCandidate> {
if port == 0 || (bus.read_word(port + 6) & 0xC000) != 0xC000 {
return None;
}
let pm_handle = bus.read_long(port + 2);
if pm_handle == 0 {
return None;
}
let pm_ptr = bus.read_long(pm_handle);
if pm_ptr == 0 {
return None;
}
let base = bus.read_long(pm_ptr) & 0x3FFF_FFFF;
let row_bytes = (bus.read_word(pm_ptr + 4) & 0x3FFF) as u32;
let top = bus.read_word(pm_ptr + 6) as i16;
let left = bus.read_word(pm_ptr + 8) as i16;
let bottom = bus.read_word(pm_ptr + 10) as i16;
let right = bus.read_word(pm_ptr + 12) as i16;
let pixel_size = bus.read_word(pm_ptr + 32);
if base == 0
|| base == screen_base
|| pixel_size != 8
|| bottom <= top
|| right <= left
|| row_bytes < (right - left) as u32
{
return None;
}
Some(FullscreenOffscreenDialogRestoreCandidate {
base,
row_bytes,
top,
left,
bottom,
right,
ctab_handle: bus.read_long(pm_ptr + 42),
score: 0,
})
}
fn score_offscreen_dialog_restore_candidate(
&self,
bus: &MacMemoryBus,
candidate: FullscreenOffscreenDialogRestoreCandidate,
rect: (i16, i16, i16, i16),
) -> Option<u32> {
let (top, left, bottom, right) = rect;
let width = right - left;
let height = bottom - top;
if width <= 0 || height <= 0 {
return None;
}
let step_x = (width as u32 / 32).max(1);
let step_y = (height as u32 / 24).max(1);
let mut total = 0u32;
let mut visible = 0u32;
let mut white_like = 0u32;
let mut black_like = 0u32;
let mut y = top as u32 + step_y / 2;
while y < bottom as u32 {
let mut x = left as u32 + step_x / 2;
while x < right as u32 {
let src_y = y as i16 - candidate.top;
let src_x = x as i16 - candidate.left;
if src_y >= 0 && src_x >= 0 {
let idx = bus.read_byte(
candidate.base + src_y as u32 * candidate.row_bytes + src_x as u32,
) as usize;
let [r, g, b] = self.device_clut[idx];
total += 1;
if r > 0x1111 || g > 0x1111 || b > 0x1111 {
visible += 1;
}
if r >= 0xEEEE && g >= 0xEEEE && b >= 0xEEEE {
white_like += 1;
}
if r <= 0x1111 && g <= 0x1111 && b <= 0x1111 {
black_like += 1;
}
}
x += step_x;
}
y += step_y;
}
if total == 0
|| visible < 8
|| white_like.saturating_mul(100) >= total.saturating_mul(70)
|| black_like.saturating_mul(100) >= total.saturating_mul(98)
{
return None;
}
Some(visible.saturating_mul(2).saturating_sub(white_like))
}
fn resolve_dispos_dialog_ptr_after_modal_button_hit(
&mut self,
requested_dialog_ptr: u32,
) -> u32 {
let Some(dialog_ptr) = self.pending_modal_button_dispose_dialog.take() else {
return requested_dialog_ptr;
};
if requested_dialog_ptr != 0
&& requested_dialog_ptr != dialog_ptr
&& !self.dialog_items.contains_key(&requested_dialog_ptr)
&& !self.window_list.contains(&requested_dialog_ptr)
&& self.front_window == dialog_ptr
&& self.dialog_modal_entered.contains(&dialog_ptr)
&& self.dialog_items.contains_key(&dialog_ptr)
{
eprintln!(
"[TRAP] DisposDialog(${:08X}) recovered front ModalDialog button target ${:08X}",
requested_dialog_ptr, dialog_ptr
);
dialog_ptr
} else {
requested_dialog_ptr
}
}
pub(crate) fn consume_retained_modal_dialog_event<C: CpuOps>(
&mut self,
cpu: &mut C,
bus: &mut MacMemoryBus,
event: &QueuedEvent,
) -> bool {
match event.what {
1 => {
let Some((dialog_ptr, bounds)) = self.front_retained_modal_dialog(bus) else {
return false;
};
if !Self::dialog_contains_screen_point(bounds, event.where_v, event.where_h) {
// Modal dialogs own the mouse while visible. A real Dialog
// Manager click outside the box beeps and does not pass
// through to windows behind it.
// Inside Macintosh Volume I, I-418; Macintosh Toolbox
// Essentials 1992, p. 6-136.
self.retained_modal_dialog_click = Some(RetainedModalDialogClickState {
dialog_ptr,
item_no: 0,
rect: (0, 0, 0, 0),
title: String::new(),
is_default: false,
highlighted: false,
delivered_to_app: false,
});
return true;
}
let Some(mut items) = self.dialog_items.get(&dialog_ptr).cloned() else {
return false;
};
Self::refresh_ditl_proc_ptrs(bus, dialog_ptr, &mut items);
let mut hit = self.dialog_item_hit_test(
bus,
&items,
bounds,
event.where_v,
event.where_h,
&self.dialog_popup_original_rects,
dialog_ptr,
);
if hit <= 0 {
hit =
Self::dialog_button_hit_test(&items, bounds, event.where_v, event.where_h);
}
self.dialog_items.insert(dialog_ptr, items.clone());
if hit <= 0 {
self.retained_modal_dialog_click = Some(RetainedModalDialogClickState {
dialog_ptr,
item_no: 0,
rect: (0, 0, 0, 0),
title: String::new(),
is_default: false,
highlighted: false,
delivered_to_app: false,
});
return true;
}
let item = &items[(hit - 1) as usize];
let base_type = item.item_type & 0x7F;
let is_disabled = (item.item_type & 0x80) != 0;
if base_type == 4 && !is_disabled {
let rect = Self::dialog_item_screen_rect(bounds, item.rect);
let is_default = hit == bus.read_word(dialog_ptr + 168) as i16;
self.draw_dialog_button_highlight_state(
bus, rect, &item.text, is_default, true,
);
self.retained_modal_dialog_click = Some(RetainedModalDialogClickState {
dialog_ptr,
item_no: hit,
rect: item.rect,
title: item.text.clone(),
is_default,
highlighted: true,
delivered_to_app: false,
});
} else {
self.retained_modal_dialog_click = Some(RetainedModalDialogClickState {
dialog_ptr,
item_no: 0,
rect: (0, 0, 0, 0),
title: String::new(),
is_default: false,
highlighted: false,
delivered_to_app: false,
});
}
true
}
2 => {
let Some(click) = self.retained_modal_dialog_click.take() else {
return false;
};
if click.item_no > 0 {
let bounds = Self::dialog_screen_bounds(bus, click.dialog_ptr);
let rect = Self::dialog_item_screen_rect(bounds, click.rect);
if click.highlighted {
self.draw_dialog_button_highlight_state(
bus,
rect,
&click.title,
click.is_default,
false,
);
}
if self.front_window == click.dialog_ptr
&& Self::point_in_screen_rect(event.where_v, event.where_h, rect)
{
self.close_dialog_window(bus, cpu, click.dialog_ptr, true);
self.capture_gui_frame(
bus,
&format!("retained_modal_dialog_button_{}", click.item_no),
);
}
}
!click.delivered_to_app
}
_ => false,
}
}
fn consume_dialog_mouse_up(&mut self) {
if let Some(idx) = self.event_queue.iter().position(|e| e.what == 2) {
self.event_queue.remove(idx);
}
}
fn dialog_item_screen_rect(
bounds: (i16, i16, i16, i16),
rect: (i16, i16, i16, i16),
) -> (i16, i16, i16, i16) {
let (it, il, ib, ir) = rect;
(bounds.0 + it, bounds.1 + il, bounds.0 + ib, bounds.1 + ir)
}
fn rects_intersect(a: (i16, i16, i16, i16), b: (i16, i16, i16, i16)) -> bool {
a.0 < b.2 && a.2 > b.0 && a.1 < b.3 && a.3 > b.1
}
fn dialog_item_intersects_bounds(bounds: (i16, i16, i16, i16), item: &DialogItem) -> bool {
Self::rects_intersect(Self::dialog_item_screen_rect(bounds, item.rect), bounds)
}
fn dialog_is_game_managed(bounds: (i16, i16, i16, i16), items: &[DialogItem]) -> bool {
let mut has_visible_item = false;
for item in items {
if !Self::dialog_item_intersects_bounds(bounds, item) {
continue;
}
has_visible_item = true;
if (item.item_type & 0x7F) != 0 {
return false;
}
}
has_visible_item
}
fn start_dialog_button_flash(
&mut self,
bus: &mut MacMemoryBus,
bounds: (i16, i16, i16, i16),
item_no: i16,
rect: (i16, i16, i16, i16),
title: &str,
is_default: bool,
highlighted: bool,
) {
if !highlighted {
let screen_rect = Self::dialog_item_screen_rect(bounds, rect);
self.draw_dialog_button_highlight_state(bus, screen_rect, title, is_default, true);
}
if let Some(t) = self.dialog_tracking.as_mut() {
t.flash_remaining = 6;
t.flash_delay = 3;
t.flash_item = item_no;
}
}
fn dialog_control_handle_for_item(&self, dialog_ptr: u32, item_no: i16) -> Option<u32> {
self.dialog_control_handles
.iter()
.find_map(|(&handle, &(dlg, item))| {
if dlg == dialog_ptr && item == item_no {
Some(handle)
} else {
None
}
})
}
fn dialog_control_inactive(&self, bus: &MacMemoryBus, dialog_ptr: u32, item_no: i16) -> bool {
self.dialog_control_handle_for_item(dialog_ptr, item_no)
.map(|handle| bus.read_long(handle))
.is_some_and(|ctrl_ptr| ctrl_ptr != 0 && bus.read_byte(ctrl_ptr + 17) == 255)
}
fn begin_dialog_popup_tracking(
&mut self,
bus: &mut MacMemoryBus,
dialog_ptr: u32,
item_no: i16,
) -> bool {
let Some(ctrl_handle) = self.dialog_control_handle_for_item(dialog_ptr, item_no) else {
return false;
};
let ctrl_ptr = bus.read_long(ctrl_handle);
if ctrl_ptr == 0 {
return false;
}
let proc_id = self.control_proc_ids.get(&ctrl_ptr).copied().unwrap_or(0);
if !Self::is_popup_menu_proc_id(proc_id) {
return false;
}
let menu_id =
self.popup_control_menu_id(bus, ctrl_ptr, bus.read_word(ctrl_ptr + 20) as i16);
let Some(menu_idx) = self.menus.iter().rposition(|menu| menu.id == menu_id) else {
return false;
};
let dropdown_rect = self.popup_control_dropdown_rect(bus, ctrl_ptr, menu_idx);
let saved_pixels = self.save_dropdown_pixels(bus, dropdown_rect);
self.draw_menu_dropdown(bus, menu_idx, dropdown_rect);
if let Some(tracking) = self.dialog_tracking.as_mut() {
tracking.active_popup = Some(DialogPopupTrackingState {
item_no,
ctrl_handle,
ctrl_ptr,
active_menu: menu_idx,
highlighted_item: 0,
saved_pixels,
dropdown_rect,
});
true
} else {
false
}
}
fn dialog_popup_item_at_point(&self, bus: &MacMemoryBus, mouse_x: i16, mouse_y: i16) -> i16 {
let Some(popup) = self
.dialog_tracking
.as_ref()
.and_then(|tracking| tracking.active_popup.as_ref())
else {
return 0;
};
let (top, left, bottom, right) = popup.dropdown_rect;
if mouse_x < left || mouse_x >= right || mouse_y < top || mouse_y >= bottom {
return 0;
}
let Some(menu) = self.menus.get(popup.active_menu) else {
return 0;
};
let mut item_top = top + 1;
for (item_idx, item) in menu.items.iter().enumerate() {
let item_bottom = item_top + self.menu_item_height(bus, item);
if mouse_y >= item_top && mouse_y < item_bottom {
if item.text == "-" || !item.enabled {
return 0;
}
return item_idx as i16 + 1;
}
item_top = item_bottom;
}
0
}
fn handle_dialog_popup_tracking<C: CpuOps>(&mut self, cpu: &mut C, bus: &mut MacMemoryBus) {
if self.mouse_button {
let (mv, mh) = self.mouse_pos;
let new_item = self.dialog_popup_item_at_point(bus, mh, mv);
let old_item = self
.dialog_tracking
.as_ref()
.and_then(|tracking| tracking.active_popup.as_ref())
.map(|popup| popup.highlighted_item)
.unwrap_or(0);
if new_item != old_item {
let popup_state = self
.dialog_tracking
.as_ref()
.and_then(|tracking| tracking.active_popup.as_ref())
.map(|popup| (popup.active_menu, popup.dropdown_rect));
if let Some((active_menu, dropdown_rect)) = popup_state {
if old_item > 0 {
self.invert_dropdown_item_rect(bus, active_menu, dropdown_rect, old_item);
}
if let Some(popup) = self
.dialog_tracking
.as_mut()
.and_then(|tracking| tracking.active_popup.as_mut())
{
popup.highlighted_item = new_item;
}
if new_item > 0 {
self.invert_dropdown_item_rect(bus, active_menu, dropdown_rect, new_item);
}
}
}
return;
}
let Some(popup) = self
.dialog_tracking
.as_mut()
.and_then(|tracking| tracking.active_popup.take())
else {
return;
};
self.restore_dropdown_pixels(bus, popup.dropdown_rect, &popup.saved_pixels);
self.consume_dialog_mouse_up();
let selected_item = popup.highlighted_item;
if selected_item <= 0 {
return;
}
self.write_control_value(bus, popup.ctrl_handle, selected_item);
self.draw_control(cpu, bus, popup.ctrl_ptr);
if self.dialog_tracking.is_some() {
let (dialog_ptr, edit_item, edit_text, items) = {
let tracking = self.dialog_tracking.as_mut().unwrap();
Self::sync_tracking_active_edit_item(tracking);
(
tracking.dialog_ptr,
tracking.edit_item,
tracking.edit_text.clone(),
tracking.items.clone(),
)
};
self.flush_dialog_edit_item_texts(bus, dialog_ptr, &items, edit_item, &edit_text);
}
if let Some((bounds, game_managed)) = self
.dialog_tracking
.as_ref()
.map(|tracking| (tracking.bounds, tracking.game_managed))
{
if !game_managed {
let rendered = self.save_dialog_pixels(bus, bounds);
if let Some(tracking) = self.dialog_tracking.as_mut() {
tracking.rendered_pixels = rendered;
tracking.rendered_pixels_final = true;
}
}
}
let Some(saved) = self.dialog_tracking.take() else {
return;
};
self.persist_visible_dialog_snapshot(bus, &saved);
self.dialog_saved_pixels
.insert(saved.dialog_ptr, saved.saved_pixels);
if saved.item_hit_ptr != 0 {
bus.write_word(saved.item_hit_ptr, popup.item_no as u16);
}
cpu.write_reg(Register::A7, saved.stack_ptr + 8);
}
fn handle_dialog_button_tracking(&mut self, bus: &mut MacMemoryBus) {
let Some((dialog_ptr, bounds, item_no, rect, title, is_default, highlighted, item_type)) =
self.dialog_tracking.as_ref().and_then(|tracking| {
tracking.active_button.as_ref().map(|button| {
(
tracking.dialog_ptr,
tracking.bounds,
button.item_no,
button.rect,
button.title.clone(),
button.is_default,
button.highlighted,
tracking
.items
.get(button.item_no.saturating_sub(1) as usize)
.map(|item| item.item_type)
.unwrap_or(4),
)
})
})
else {
return;
};
let (top, left, bottom, right) = Self::dialog_item_screen_rect(bounds, rect);
let (mouse_v, mouse_h) = self.mouse_pos;
let inside = mouse_v >= top && mouse_v < bottom && mouse_h >= left && mouse_h < right;
if self.mouse_button {
if inside != highlighted {
self.draw_dialog_button_highlight_state(
bus,
(top, left, bottom, right),
&title,
is_default,
inside,
);
if let Some(button) = self
.dialog_tracking
.as_mut()
.and_then(|tracking| tracking.active_button.as_mut())
{
button.highlighted = inside;
}
self.record_modal_dialog_input_trace(
"tracking_update",
dialog_ptr,
bounds,
item_no,
Some(item_type),
Some(inside),
"pending",
if inside {
"button_highlighted"
} else {
"button_unhighlighted"
},
);
}
return;
}
self.consume_dialog_mouse_up();
if let Some(t) = self.dialog_tracking.as_mut() {
t.active_button = None;
}
if inside {
self.record_modal_dialog_input_trace(
"release",
dialog_ptr,
bounds,
item_no,
Some(item_type),
Some(true),
"pending",
"start_flash",
);
self.start_dialog_button_flash(
bus,
bounds,
item_no,
rect,
&title,
is_default,
highlighted,
);
} else if highlighted {
self.draw_dialog_button_highlight_state(
bus,
(top, left, bottom, right),
&title,
is_default,
false,
);
self.record_modal_dialog_input_trace(
"release",
dialog_ptr,
bounds,
item_no,
Some(item_type),
Some(false),
"pending",
"button_no_selection",
);
} else {
self.record_modal_dialog_input_trace(
"release",
dialog_ptr,
bounds,
item_no,
Some(item_type),
Some(false),
"pending",
"button_no_selection",
);
}
}
fn handle_dialog_user_item_tracking<C: CpuOps>(&mut self, cpu: &mut C, bus: &mut MacMemoryBus) {
let Some((bounds, item_no, rect)) = self.dialog_tracking.as_ref().and_then(|tracking| {
tracking
.active_user_item
.as_ref()
.map(|user_item| (tracking.bounds, user_item.item_no, user_item.rect))
}) else {
return;
};
if self.mouse_button {
return;
}
self.consume_dialog_mouse_up();
if let Some(t) = self.dialog_tracking.as_mut() {
t.active_user_item = None;
}
let (top, left, bottom, right) = Self::dialog_item_screen_rect(bounds, rect);
let (mouse_v, mouse_h) = self.mouse_pos;
let inside = mouse_v >= top && mouse_v < bottom && mouse_h >= left && mouse_h < right;
if !inside {
return;
}
let (dlg_ptr, edit_item, edit_text, items) = {
let tracking = self.dialog_tracking.as_mut().unwrap();
Self::sync_tracking_active_edit_item(tracking);
(
tracking.dialog_ptr,
tracking.edit_item,
tracking.edit_text.clone(),
tracking.items.clone(),
)
};
self.flush_dialog_edit_item_texts(bus, dlg_ptr, &items, edit_item, &edit_text);
let saved = self.dialog_tracking.take().unwrap();
self.persist_visible_dialog_snapshot(bus, &saved);
self.dialog_saved_pixels
.insert(saved.dialog_ptr, saved.saved_pixels);
if saved.item_hit_ptr != 0 {
bus.write_word(saved.item_hit_ptr, item_no as u16);
}
cpu.write_reg(Register::A7, saved.stack_ptr + 8);
}
pub(crate) fn dispatch_dialog<C: CpuOps>(
&mut self,
is_tool: bool,
trap_num: u16,
cpu: &mut C,
bus: &mut MacMemoryBus,
) -> Option<Result<()>> {
Some(match (is_tool, trap_num) {
// ========== Dialog Manager ==========
// InitDialogs ($A97B)
// PROCEDURE InitDialogs(resumeProc: ProcPtr);
// Inside Macintosh Volume I, I-411.
//
// Performs the 3 documented Dialog Manager init steps:
// 1. Stores resumeProc into the ResumeProc low-mem
// global at $0A8C, for later access by the System
// Error Handler when a fatal system error occurs
// (NIL is the documented default — no resume).
// 2. Installs the standard sound procedure (we store
// NIL into DABeeper at $0A9C since Systemless's HLE
// doesn't model menu-bar-blink sound; ErrorSound
// ($A98C) can override later).
// 3. Passes empty strings to ParamText — implemented
// as zeroing the 4-handle DAStrings array at
// $0AA0 (16 bytes = 4 × Handle, all NIL meaning
// "no substitution" so dialog/alert text rendered
// with `^0`..`^3` escapes prints the raw escape).
//
// Also resets AlertStage at $0A9A to 0 so the first
// Alert*-family call starts at stage 1 (per IM:I I-417
// — InitDialogs is the canonical place to ensure
// alert state is fresh per app launch). ANumber at
// $0A98 is left untouched since IM:I I-423 only
// documents it as "the resource ID of the last alert
// that occurred" with no init contract.
//
// Pop = 4 bytes (resumeProc ProcPtr).
// 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
(true, 0x17B) => {
let sp = cpu.read_reg(Register::A7);
let resume_proc = bus.read_long(sp);
use crate::memory::globals::addr;
bus.write_long(addr::RESUME_PROC, resume_proc);
bus.write_long(addr::DA_BEEPER, 0);
bus.write_word(addr::ALERT_STAGE, 0);
// Zero the 4-handle DAStrings array (16 bytes).
for i in 0..4u32 {
bus.write_long(addr::DA_STRINGS + i * 4, 0);
}
cpu.write_reg(Register::A7, sp + 4);
Ok(())
}
// GetNewDialog ($A97C)
// Creates a dialog from DLOG and DITL resources.
// FUNCTION GetNewDialog (dialogID: INTEGER; dStorage: Ptr;
// behind: WindowPtr) : DialogPtr;
// Inside Macintosh Volume I, I-424
//
// Stack: SP+0 behind(4), SP+4 dStorage(4), SP+8 dialogID(2),
// SP+10 result(4).
// Honors `behind` at SP+0 via finish_dialog_creation —
// see NewDialog/NewCDialog and window.rs apply_behind_parameter.
// 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
(true, 0x17C) => {
let sp = cpu.read_reg(Register::A7);
let behind = bus.read_long(sp);
let storage_ptr = bus.read_long(sp + 4);
let dialog_id = bus.read_word(sp + 8) as i16;
eprintln!("[TRAP] GetNewDialog({})", dialog_id);
// Look up DLOG resource
let dlog_ptr = self
.find_resource_any(*b"DLOG", dialog_id)
.map(|(_, ptr)| ptr);
if let Some(dlog_data) = dlog_ptr {
let dlog_len = bus.get_alloc_size(dlog_data).unwrap_or(0);
let (raw_bounds, proc_id, visible, items_id, title, position) =
Self::parse_dlog(bus, dlog_data, dlog_len);
let mut bounds = raw_bounds;
// Look up DITL resource
let ditl_info = self
.find_resource_any(*b"DITL", items_id)
.map(|(_, ptr)| ptr);
let Some(ditl_data) = ditl_info else {
eprintln!(
"[TRAP] GetNewDialog({}): DITL {} not found",
dialog_id, items_id
);
// MTE 1992 p. 6-114: GetNewDialog returns NIL if
// either the DLOG or item-list resource cannot be
// read. Resource Manager ResError reports
// resNotFound (-192) for missing resources.
bus.write_word(0x0A60, Self::RES_NOT_FOUND as u16);
bus.write_long(sp + 10, 0);
cpu.write_reg(Register::A7, sp + 10);
return Some(Ok(()));
};
bus.write_word(0x0A60, 0);
let ditl_len = bus.get_alloc_size(ditl_data).unwrap_or(0);
let items = Self::parse_ditl(bus, ditl_data, ditl_len);
// Apply System 7 DLOG positioning constants.
// Macintosh Toolbox Essentials 1992, pp. 4-125 to 4-126.
bounds = self.positioned_dialog_bounds(bounds, position);
eprintln!(
"[TRAP] GetNewDialog({}) bounds=({},{},{},{}) raw_bounds=({},{},{},{}) position=${:04X} len={} procID={} items={} title=\"{}\"",
dialog_id, bounds.0, bounds.1, bounds.2, bounds.3,
raw_bounds.0, raw_bounds.1, raw_bounds.2, raw_bounds.3,
position,
dlog_len,
proc_id, items.len(), title
);
for (i, item) in items.iter().enumerate() {
let base_type = item.item_type & 0x7F;
let type_name = match base_type {
4 => "button",
5 => "checkbox",
6 => "radio",
7 => "resCtrl",
8 => "statText",
16 => "editText",
32 => "icon",
64 => "picture",
0 => "userItem",
_ => "unknown",
};
if base_type == 32 || base_type == 64 {
eprintln!(
"[TRAP] item {}: type={}({}) rect=({},{},{},{}) resID={}",
i + 1,
type_name,
item.item_type,
item.rect.0,
item.rect.1,
item.rect.2,
item.rect.3,
item.resource_id
);
} else {
eprintln!(
"[TRAP] item {}: type={}({}) rect=({},{},{},{}) text=\"{}\"",
i + 1,
type_name,
item.item_type,
item.rect.0,
item.rect.1,
item.rect.2,
item.rect.3,
item.text
);
}
}
let items_handle = if let Some((_, ditl_handle_ptr)) =
self.find_resource_any(*b"DITL", items_id)
{
let handle = bus.alloc(4);
bus.write_long(handle, ditl_handle_ptr);
Self::duplicate_handle_data(bus, handle)
} else {
0
};
// Honor the DLOG resource's visible flag per IM:I I-424.
let dlg_ptr = self.finish_dialog_creation(
bus,
cpu,
storage_ptr,
bounds,
&title,
visible,
proc_id,
false,
0,
items_handle,
items,
);
// Install any 'pltt' resource whose id matches the
// dialog id onto the freshly-created window. This
// mirrors what GetNewWindow / NewCWindow do in
// window.rs and is what the real Window Manager
// does: a Window with an associated palette gets
// its palette activated when the window is created
// and made active. Without this hook, a dialog
// whose PICT items rely on an auto-installed
// palette (e.g. EV's landing-scene dialog id 1000)
// renders through whatever canonical CLUT happens
// to be live, producing palette-mismatched colour
// noise. Inside Macintosh Volume VI, 20-12 to
// 20-13 (palette association and activation).
if dlg_ptr != 0 {
let palette = self.copy_palette_resource(bus, dialog_id);
if palette != 0 {
self.set_window_palette_association(dlg_ptr, palette, -0x2000);
self.activate_palette_for_window(bus, dlg_ptr);
}
self.apply_behind_parameter(bus, dlg_ptr, behind);
}
bus.write_long(sp + 10, dlg_ptr);
} else {
eprintln!("[TRAP] GetNewDialog({}): DLOG not found -> NIL", dialog_id);
// MTE 1992 p. 6-114: GetNewDialog returns NIL if the
// DLOG resource cannot be read. Resource Manager ResError
// reports resNotFound (-192) for missing resources.
bus.write_word(0x0A60, Self::RES_NOT_FOUND as u16);
bus.write_long(sp + 10, 0);
}
cpu.write_reg(Register::A7, sp + 10);
Ok(())
}
// Alert ($A985) / StopAlert ($A986) / NoteAlert ($A987)
// / CautionAlert ($A988)
//
// FUNCTION Alert(alertID: INTEGER; filterProc: ProcPtr): INTEGER;
// (and identical signatures for Stop/Note/Caution Alert)
//
// The four alert variants differ only in the ICON they
// display (none / Stop hand / Note speaker / Caution
// triangle); their dispatch into the ALRT template +
// ALRT stages logic is identical. OK-only alerts collapse
// to "return the bold/default item for the current stage,
// increment AlertStage." Multi-button alerts enter the
// normal dialog tracking loop so a script or user can
// choose a non-default button before the trap returns.
//
// ALRT template per IM:I I-422:
// +0 boundsRect: Rect (8 bytes)
// +8 itemsID: INTEGER (DITL resource ID)
// +10 stages: INTEGER (16-bit, 4 nibbles, 1
// per stage 1..4 from low nibble
// to high). Each nibble:
// bit 3: boldItmNum flag
// (0 = item 1 bold/
// default, 1 = item 2
// bold/default)
// bit 2: boxDrawn flag
// (alert is shown if
// set; suppressed if
// clear)
// bits 0-1: soundNum (0..3 —
// 0=silent, 1=note,
// 2=caution, 3=stop)
//
// The current alert stage lives in the AlertStage
// low-mem global at $0A9A (1 byte, holds 0..3 mapping
// to stages 1..4). After each Alert*-family call the
// byte is incremented and capped at 3, so the fourth
// and subsequent calls all use the stage-4 nibble.
// ResetAlrtStage ($A98B) zeros the byte; GetAlrtStage
// ($A9B7) returns it.
//
// HLE compromise: filterProc is NOT invoked (no guest-
// fn dispatch infrastructure for the ProcPtr argument
// — same compromise as Pack1 LSearch and other guest-
// ProcPtr-taking traps).
// The missing-ALRT path is a defensive probe guard:
// return -1 but leave AlertStage and ANumber exactly as
// the caller left them.
//
// Inside Macintosh Volume I, I-417..I-422 (Alert
// family + ALRT template); Macintosh Toolbox Essentials
// 1992, 6-105..6-119 (System 7 alert dispatch).
// 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
// StopAlert ($A986): Same as Alert with Stop icon; identical dispatch path
// NoteAlert ($A987): Same as Alert with Note icon; identical dispatch path
// CautionAlert ($A988): Same as Alert with Caution icon; identical dispatch path
(true, 0x185) | (true, 0x186) | (true, 0x187) | (true, 0x188) => {
const ALERT_MISSING_RESOURCE_RESULT: i16 = -1;
let sp = cpu.read_reg(Register::A7);
if self.dialog_tracking.is_some() {
self.handle_interactive_alert_refire(cpu, bus);
return Some(Ok(()));
}
let filter_proc = bus.read_long(sp);
let alert_id = bus.read_word(sp + 4) as i16;
let trap_name = match trap_num {
0x185 => "Alert",
0x186 => "StopAlert",
0x187 => "NoteAlert",
0x188 => "CautionAlert",
_ => "Alert?",
};
let alert_stage_before = bus.read_word(crate::memory::globals::addr::ALERT_STAGE);
let anumber_before = bus.read_word(crate::memory::globals::addr::ANUMBER);
let mut alert_trace_stage: Option<(u16, u16, u32, u32)> = None;
// Look up the ALRT resource and, if present, also pull
// the referenced DITL's static-text items so the trap
// log surfaces the alert message before we silently
// dismiss it. Mid-90s titles (Bumbler, Steel Fighters,
// etc.) emit StopAlert+ExitToShell on incompatibility;
// without the message text in the log there's no way
// to know *which* check failed without RE'ing the
// binary. Inside Macintosh Volume I, I-422 (ALRT
// template) and I-426 (DITL).
let alrt_ptr = self
.find_resource_any(*b"ALRT", alert_id)
.map(|(_, ptr)| ptr);
let result: i16 = if let Some(alrt_data) = alrt_ptr {
let alrt_len = bus.get_alloc_size(alrt_data).unwrap_or(0);
let (bounds, items_id, stages, position) =
Self::parse_alrt(bus, alrt_data, alrt_len);
// AlertStage / ACount lives at $0A9A as a
// 16-bit WORD (NOT a byte) per IM:I I-423 +
// MTb 1992 22620 `#define GetAlertStage()
// (* (short*) 0x0A9A)`. On big-endian 68k a
// word value 0..3 stores byte 0 at $0A9A and
// value at $0A9B, so reading as a byte at
// $0A9A would always return 0 — must use
// read_word/write_word.
let stage_word = bus.read_word(crate::memory::globals::addr::ALERT_STAGE);
let (stage_idx, nibble, default_item) =
Self::alert_stage_default_item(stages, stage_word);
alert_trace_stage = Some((stages, stage_word, stage_idx, nibble));
// bit 3 (MSB) of nibble = boldItm per StageList
// PACKED RECORD layout (IM:I I-422): boldItm,
// boxDrwn, sound[2]. Assembly mask okDismissal=8
// and alBit=4 confirm bit masks. MTE 1992 p. 6-106
// says Alert returns -1 when boxDrwn is clear.
// Increment AlertStage, capped at 3, so the
// next call uses the next stage's nibble.
let next_stage = ((stage_word as u32) + 1).min(3) as u16;
bus.write_word(crate::memory::globals::addr::ALERT_STAGE, next_stage);
// ANumber records the resource ID of the last
// alert that occurred (IM:I I-423). Apps that
// probe ANumber after a sequence of Alert
// calls expect this to reflect the most-recent
// ID — used by some defensive resume logic.
bus.write_word(crate::memory::globals::addr::ANUMBER, alert_id as u16);
if let Some(default_item) = default_item {
// Until Alert filterProc dispatch is implemented, keep
// non-NIL filterProc calls on the old default-item
// path. Entering local button tracking would ignore
// caller-supplied filter decisions.
if filter_proc == 0
&& self.begin_interactive_alert(
cpu,
bus,
sp,
alert_id,
filter_proc,
bounds,
items_id,
position,
default_item,
)
{
if super::dispatch::trace_dialog_traps_enabled() {
eprintln!(
"[TRAP] {} id={} -> interactive dialog (PC=${:08X}) stages=${:04X} alertStage={} stageIdx={} nibble=${:X} defaultItem={}",
trap_name,
alert_id,
cpu.read_reg(Register::PC),
stages,
stage_word,
stage_idx,
nibble,
default_item
);
}
return Some(Ok(()));
}
default_item
} else {
ALERT_MISSING_RESOURCE_RESULT
}
} else {
bus.write_word(
crate::memory::globals::addr::ALERT_STAGE,
alert_stage_before,
);
bus.write_word(crate::memory::globals::addr::ANUMBER, anumber_before);
ALERT_MISSING_RESOURCE_RESULT
};
if super::dispatch::trace_dialog_traps_enabled() {
let mut detail = format!(
"[TRAP] {} id={} -> item {} (PC=${:08X})",
trap_name,
alert_id,
result,
cpu.read_reg(Register::PC)
);
if let Some((stages, stage_word, stage_idx, nibble)) = alert_trace_stage {
detail.push_str(&format!(
" stages=${stages:04X} alertStage={stage_word} stageIdx={stage_idx} nibble=${nibble:X}"
));
}
if let Some(alrt_data) = alrt_ptr {
// ALRT template: bounds(8) + itemsID(2) + stages(2).
// Inside Macintosh Volume I, I-422. Some titles
// (Bumbler Bee-Luxe in particular) ship ALRT data
// whose first 4 bytes don't decode as a Rect — most
// likely a non-standard prologue prepended by the
// build's PowerPC fragment or resource compiler.
// Detect via implausible bounds (top > bottom or
// wildly negative coordinates) and suppress the
// usual itemsID lookup so the trace reflects that
// the resource isn't in the documented format.
let alrt_len = bus.get_alloc_size(alrt_data).unwrap_or(0);
let ((top, left, bottom, right), items_id, _, _) =
Self::parse_alrt(bus, alrt_data, alrt_len);
let bounds_plausible = (-32..1024).contains(&top)
&& (-32..2048).contains(&left)
&& bottom > top
&& right > left
&& (bottom - top) < 1024
&& (right - left) < 2048;
if !bounds_plausible {
detail.push_str(&format!(
" bounds=({},{},{},{}) — implausible, skipping DITL lookup",
top, left, bottom, right
));
} else {
let ditl_match = self.find_resource_any(*b"DITL", items_id);
detail.push_str(&format!(
" bounds=({},{},{},{}) itemsID={} ditl={}",
top,
left,
bottom,
right,
items_id,
if ditl_match.is_some() {
"found"
} else {
"missing"
}
));
if let Some((_, ditl_data)) = ditl_match {
let ditl_len = bus.get_alloc_size(ditl_data).unwrap_or(0);
let items = Self::parse_ditl(bus, ditl_data, ditl_len);
for (i, item) in items.iter().enumerate() {
if !item.text.is_empty() {
detail.push_str(&format!(
"\n[TRAP] item {} (type=${:02X}): {:?}",
i + 1,
item.item_type,
item.text
));
}
}
}
}
}
eprintln!("{}", detail);
}
bus.write_word(sp + 6, result as u16);
cpu.write_reg(Register::A7, sp + 6);
Ok(())
}
// IsDialogEvent ($A97F)
// Tests whether an event should be handled as part of an active
// modeless or movable modal dialog.
// FUNCTION IsDialogEvent(theEvent: EventRecord): Boolean;
// Inside Macintosh Volume I, I-416; Macintosh Toolbox Essentials 1992, 6-138
(true, 0x17F) => {
let sp = cpu.read_reg(Register::A7);
let event_ptr = bus.read_long(sp);
let (what, message, where_v, where_h, _modifiers) =
Self::read_guest_event_record(bus, event_ptr);
let target_dialog = self.dialog_from_window_event(what, message);
let result = if let Some(dialog_ptr) = target_dialog {
match what {
6 | 8 => message == dialog_ptr,
1 => Self::dialog_contains_screen_point(
Self::dialog_screen_bounds(bus, dialog_ptr),
where_v,
where_h,
),
_ => true,
}
} else {
false
};
self.record_dialog_input_trace(
"A97F",
event_ptr,
what,
message,
where_v,
where_h,
_modifiers,
target_dialog,
result,
"outcome=is_dialog_event",
);
bus.write_word(sp + 4, if result { 0xFFFF } else { 0 });
cpu.write_reg(Register::A7, sp + 4);
Ok(())
}
// DialogSelect ($A980)
// Handles events for modeless and movable modal dialogs.
// FUNCTION DialogSelect(theEvent: EventRecord; VAR theDialog: DialogPtr; VAR itemHit: INTEGER): BOOLEAN;
// Inside Macintosh Volume I, I-417; Macintosh Toolbox Essentials 1992, 6-139
(true, 0x180) => {
let sp = cpu.read_reg(Register::A7);
let item_hit_ptr = bus.read_long(sp);
let dialog_out_ptr = bus.read_long(sp + 4);
let event_ptr = bus.read_long(sp + 8);
let (what, message, where_v, where_h, _modifiers) =
Self::read_guest_event_record(bus, event_ptr);
let mut result = false;
let mut trace_detail = String::from("outcome=no_dialog_target");
let target_dialog = self.dialog_from_window_event(what, message);
if let Some(dialog_ptr) = target_dialog {
let bounds = Self::dialog_screen_bounds(bus, dialog_ptr);
trace_detail = format!(
"bounds=({},{},{},{}) outcome=no_item",
bounds.0, bounds.1, bounds.2, bounds.3
);
if let Some(mut items) = self.dialog_items.get(&dialog_ptr).cloned() {
Self::refresh_ditl_proc_ptrs(bus, dialog_ptr, &mut items);
match what {
6 => {
// MTE 1992 p. 6-141: DialogSelect wraps the
// update redraw in BeginUpdate/EndUpdate,
// calls DrawDialog, and returns FALSE.
let update_rect =
Self::region_handle_rect(bus, bus.read_long(dialog_ptr + 122));
self.begin_update_window(bus, dialog_ptr);
self.update_dialog_window_contents(
bus,
cpu,
dialog_ptr,
update_rect,
);
self.end_update_window(bus, dialog_ptr);
trace_detail = format!(
"bounds=({},{},{},{}) outcome=update_redraw",
bounds.0, bounds.1, bounds.2, bounds.3
);
}
1 if Self::dialog_contains_screen_point(bounds, where_v, where_h) => {
let hit = self.dialog_item_hit_test(
bus,
&items,
bounds,
where_v,
where_h,
&self.dialog_popup_original_rects,
dialog_ptr,
);
if hit > 0 {
let item = &items[(hit - 1) as usize];
let is_disabled = (item.item_type & 0x80) != 0;
trace_detail = format!(
"bounds=({},{},{},{}) item_hit={} item_type=${:02X} disabled={} outcome={}",
bounds.0,
bounds.1,
bounds.2,
bounds.3,
hit,
item.item_type,
if is_disabled { "true" } else { "false" },
if is_disabled {
"disabled_item"
} else {
"enabled_item"
},
);
if trace_dialog_items_enabled() {
eprintln!(
"[DIALOG-SELECT] mouseDown dialog=${:08X} where=({},{}) bounds=({},{},{},{}) hit={} type=${:02X} disabled={}",
dialog_ptr,
where_v,
where_h,
bounds.0,
bounds.1,
bounds.2,
bounds.3,
hit,
item.item_type,
is_disabled,
);
}
if !is_disabled {
self.cancel_app_owned_modal_dialog_button_tracking(
bus, dialog_ptr,
);
if (item.item_type & 0x7F) == 16 {
// MTE 1992 p. 6-139 / IM:I I-417:
// mouseDown in an enabled editText item makes
// that item the active edit field before
// reporting the item hit. TEClick's pixel-to-
// caret mapping remains the documented HLE
// compromise in the TEClick trap.
self.activate_dialog_edit_item(
bus, cpu, dialog_ptr, &items, hit,
);
}
if dialog_out_ptr != 0 {
bus.write_long(dialog_out_ptr, dialog_ptr);
}
if item_hit_ptr != 0 {
bus.write_word(item_hit_ptr, hit as u16);
}
result = true;
}
} else {
trace_detail = format!(
"bounds=({},{},{},{}) item_hit=0 outcome=no_item",
bounds.0, bounds.1, bounds.2, bounds.3
);
if trace_dialog_items_enabled() {
eprintln!(
"[DIALOG-SELECT] mouseDown dialog=${:08X} where=({},{}) bounds=({},{},{},{}) hit=0",
dialog_ptr,
where_v,
where_h,
bounds.0,
bounds.1,
bounds.2,
bounds.3,
);
}
}
}
0 => {
let (_edit_text, edit_item, _default_item) =
Self::dialog_edit_state(bus, dialog_ptr, &items);
trace_detail = format!(
"bounds=({},{},{},{}) edit_item={} outcome=teidle",
bounds.0, bounds.1, bounds.2, bounds.3, edit_item
);
if edit_item > 0 {
// MTE 1992 p. 6-139 / IM:I I-417:
// DialogSelect calls TEIdle for null events when an
// editText item is present, letting TextEdit advance
// the insertion-caret blink without changing text or
// selection fields.
let text_handle = bus.read_long(dialog_ptr + 160);
self.textedit_idle(cpu, bus, text_handle);
}
}
1 => {
trace_detail = format!(
"bounds=({},{},{},{}) item_hit=0 outcome=outside_dialog",
bounds.0, bounds.1, bounds.2, bounds.3
);
if trace_dialog_items_enabled() {
eprintln!(
"[DIALOG-SELECT] mouseDown dialog=${:08X} where=({},{}) outside bounds=({},{},{},{})",
dialog_ptr,
where_v,
where_h,
bounds.0,
bounds.1,
bounds.2,
bounds.3,
);
}
}
3 | 5 => {
let (_edit_text, edit_item, _default_item) =
Self::dialog_edit_state(bus, dialog_ptr, &items);
trace_detail = format!(
"bounds=({},{},{},{}) edit_item={} outcome=no_enabled_edittext",
bounds.0, bounds.1, bounds.2, bounds.3, edit_item
);
if edit_item > 0 {
// IM:I I-417: keyDown/autoKey dialog handling applies to
// editable text items. If no enabled editText item is
// active, DialogSelect returns FALSE.
if let Some(item) = items.get((edit_item - 1) as usize) {
let item_type = item.item_type;
let base_type = item_type & 0x7F;
let is_disabled = (item_type & 0x80) != 0;
trace_detail = format!(
"bounds=({},{},{},{}) edit_item={} item_type=${:02X} disabled={} outcome={}",
bounds.0,
bounds.1,
bounds.2,
bounds.3,
edit_item,
item_type,
if is_disabled { "true" } else { "false" },
if base_type == 16 && !is_disabled {
"enabled_edittext"
} else {
"no_enabled_edittext"
},
);
if base_type == 16 && !is_disabled {
let char_code = (message & 0xFF) as u8;
// MTE 1992 p. 6-139: DialogSelect uses TextEdit
// to handle key-down and auto-key events in
// editable text items before reporting itemHit.
self.apply_dialog_select_key_to_edit_item(
bus, dialog_ptr, &mut items, edit_item, char_code,
);
if dialog_out_ptr != 0 {
bus.write_long(dialog_out_ptr, dialog_ptr);
}
if item_hit_ptr != 0 {
bus.write_word(item_hit_ptr, edit_item as u16);
}
result = true;
}
}
}
}
_ => {}
}
self.dialog_items.insert(dialog_ptr, items);
} else {
trace_detail = format!(
"bounds=({},{},{},{}) outcome=no_dialog_items",
bounds.0, bounds.1, bounds.2, bounds.3
);
if trace_dialog_items_enabled() {
eprintln!(
"[DIALOG-SELECT] event what={} dialog=${:08X} has no dialog_items",
what, dialog_ptr
);
}
}
} else if trace_dialog_items_enabled() {
eprintln!(
"[DIALOG-SELECT] event what={} message=${:08X} where=({},{}) has no dialog target front=${:08X}",
what, message, where_v, where_h, self.front_window
);
}
self.record_dialog_input_trace(
"A980",
event_ptr,
what,
message,
where_v,
where_h,
_modifiers,
target_dialog,
result,
&trace_detail,
);
bus.write_word(sp + 12, if result { 0xFFFF } else { 0 });
cpu.write_reg(Register::A7, sp + 12);
Ok(())
}
// DrawDialog ($A981)
// Draws the entire contents of the specified dialog box.
// PROCEDURE DrawDialog (theDialog: DialogPtr);
// Inside Macintosh Volume I, I-417; Macintosh Toolbox Essentials 1992, 6-142
// DrawDialog ($A981): Renders all dialog items with full DITL support
(true, 0x181) => {
let sp = cpu.read_reg(Register::A7);
let dialog_ptr = bus.read_long(sp);
if let Some(mut items) = self.dialog_items.get(&dialog_ptr).cloned() {
Self::refresh_ditl_proc_ptrs(bus, dialog_ptr, &mut items);
let bounds = Self::dialog_screen_bounds(bus, dialog_ptr);
let proc_id = self.dialog_window_proc_id(bus, dialog_ptr);
let (edit_text, edit_item, default_item) =
Self::dialog_edit_state(bus, dialog_ptr, &items);
self.dialog_initial_draw_deferred.remove(&dialog_ptr);
self.draw_dialog_preserving_user_items(
bus,
bounds,
proc_id,
"",
&items,
default_item,
&edit_text,
edit_item,
false,
dialog_ptr,
true,
false,
false,
);
self.dialog_items.insert(dialog_ptr, items);
// MTE 1992 p. 6-142: DrawDialog calls
// application-defined item draw procs for userItem
// records. Queue them after the HLE redraw so the
// runner trampoline executes guest drawing before the
// caller resumes.
self.queue_modeless_dialog_draw_procs_intersecting(bus, dialog_ptr, None);
}
cpu.write_reg(Register::A7, sp + 4);
Ok(())
}
// DisposDialog ($A983)
// Removes a dialog and frees its storage.
// PROCEDURE DisposDialog (theDialog: DialogPtr);
// Inside Macintosh Volume I, I-425
//
// DisposDialog internally calls CloseWindow, which on real
// Mac OS invalidates the window's frame and the Window
// Manager's PaintBehind / CalcVisBehind restores the
// content beneath via update events to the back windows.
//
// In HLE we emulate PaintBehind by blitting the saved-under
// pixels captured immediately before the dialog first draws to
// the visible screen. Without this restore, any app that skips
// ModalDialog and runs its own event loop leaves a dialog-shaped
// hole over the window behind.
//
// Only restore when `was_front` holds: a non-front dialog
// would require the saved bounds to still be valid, but
// by the time we'd get here the window stack has already
// moved on. In practice modal dialogs are always the front
// window, so this covers every real case.
//
// Inside Macintosh Volume I, I-283 (CloseWindow), I-425 (DisposDialog)
//
// Regression coverage:
// src/trap/dialog.rs::tests::disposdialog_restores_saved_background_pixels
// src/trap/dialog.rs::tests::disposdialog_non_front_does_not_restore
// DisposDialog ($A983): Frees dialog, cleans up dialog_items, restores saved background pixels (IM:I I-425 PaintBehind)
(true, 0x183) => {
let sp = cpu.read_reg(Register::A7);
let requested_dialog_ptr = bus.read_long(sp);
let dialog_ptr =
self.resolve_dispos_dialog_ptr_after_modal_button_hit(requested_dialog_ptr);
eprintln!("[TRAP] DisposDialog(${:08X})", requested_dialog_ptr);
self.close_dialog_window(bus, cpu, dialog_ptr, true);
self.capture_gui_frame(bus, &format!("dispos_dialog_{:08X}", dialog_ptr));
cpu.write_reg(Register::A7, sp + 4);
Ok(())
}
// ParamText ($A98B)
// Saves the four strings for ^0..^3 substitution into any
// subsequently drawn dialog or alert static-text item.
// PROCEDURE ParamText(param0, param1, param2, param3: Str255);
// Inside Macintosh Volume I, I-422
// ParamText ($A98B): Saves all 4 Pascal strings for ^0..^3 dialog/alert text substitution
(true, 0x18B) => {
let sp = cpu.read_reg(Register::A7);
let trap_pc = cpu.read_reg(Register::PC).wrapping_sub(2);
// Stack layout (top-down): SP+0:param3, +4:param2, +8:param1, +12:param0.
// Per Inside Macintosh Volume I, I-422, passing NIL for any
// parameter leaves that slot's previous value unchanged —
// it's a "set this slot, leave others alone" idiom. Clearing
// on NIL would erase ^N output that an earlier ParamText
// had legitimately staged.
let offsets: [u32; 4] = [12, 8, 4, 0];
for (i, &off) in offsets.iter().enumerate() {
let ptr = bus.read_long(sp + off);
if ptr == 0 {
continue;
}
let s = bus.read_pstring(ptr);
self.param_text[i] = s.clone();
// Write to DAStrings low-memory global ($0AA0 + i*4).
// The ROM stores each param string as a StringHandle at
// *(StringHandle*)($0AA0 + i*4).
// Inside Macintosh Volume I, I-422 (DAStrings global).
use crate::memory::globals::addr;
let data_len = 1u32 + s.len() as u32;
let data_ptr = bus.alloc(data_len);
if data_ptr != 0 {
bus.write_byte(data_ptr, s.len() as u8);
for (j, &b) in s.iter().enumerate() {
bus.write_byte(data_ptr + 1 + j as u32, b);
}
let handle = bus.alloc(4);
if handle != 0 {
bus.write_long(handle, data_ptr);
bus.write_long(addr::DA_STRINGS + i as u32 * 4, handle);
}
}
}
eprintln!(
"[TRAP] ParamText pc=${:08X} ^0=\"{}\" ^1=\"{}\" ^2=\"{}\" ^3=\"{}\"",
trap_pc,
String::from_utf8_lossy(&self.param_text[0]),
String::from_utf8_lossy(&self.param_text[1]),
String::from_utf8_lossy(&self.param_text[2]),
String::from_utf8_lossy(&self.param_text[3]),
);
cpu.write_reg(Register::A7, sp + 16);
Ok(())
}
// GetDItem ($A98D)
// Returns information about a dialog item.
// PROCEDURE GetDItem (theDialog: DialogPtr; itemNo: INTEGER;
// VAR itemType: INTEGER; VAR item: Handle; VAR box: Rect);
// Inside Macintosh Volume I, I-421
// GetDItem ($A98D): Returns real item type, handle (with text for statText/editText, proc_ptr for userItem), and rect from dialog_items
(true, 0x18D) => {
let sp = cpu.read_reg(Register::A7);
let box_ptr = bus.read_long(sp);
let item_handle_ptr = bus.read_long(sp + 4);
let type_ptr = bus.read_long(sp + 8);
let item_no = bus.read_word(sp + 12) as i16;
let dialog_ptr = bus.read_long(sp + 14);
// Look up real item data
let found = self.dialog_items.get(&dialog_ptr).and_then(|items| {
if item_no > 0 && (item_no as usize) <= items.len() {
Some(&items[(item_no - 1) as usize])
} else {
None
}
});
if let Some(item) = found {
if trace_dialog_items_enabled() && (item.item_type & 0x7F) == 0 {
eprintln!(
"[DIALOG-ITEM] GetDItem pc=${:08X} dialog=${:08X} item={} type={} proc=${:08X} out_type=${:08X} out_item=${:08X} out_box=${:08X} rect=({},{},{},{})",
cpu.read_reg(Register::PC),
dialog_ptr,
item_no,
item.item_type,
item.proc_ptr,
type_ptr,
item_handle_ptr,
box_ptr,
item.rect.0,
item.rect.1,
item.rect.2,
item.rect.3,
);
}
if type_ptr != 0 {
bus.write_word(type_ptr, item.item_type as u16);
}
if item_handle_ptr != 0 {
let current_handle = Self::dialog_item_handle(bus, dialog_ptr, item_no);
let base_type = item.item_type & 0x7F;
if current_handle != 0 || !(4..=6).contains(&base_type) {
bus.write_long(item_handle_ptr, current_handle);
} else {
// Create a full ControlRecord so draw_control can render it.
// ControlRecord layout:
// +0: nextControl (4)
// +4: contrlOwner (4) = window ptr
// +8: contrlRect (8) = top, left, bottom, right
// +16: contrlVis (1) = 255 (visible)
// +17: contrlHilite (1) = 0
// +18: contrlValue (2)
// +20: contrlMin (2)
// +22: contrlMax (2) = 1
// +24: contrlDefProc (4) = procID encoding
// +40: contrlTitle (pascal string)
let title_len = item.text.len().min(255);
let ctrl_rec = bus.alloc(42 + title_len as u32);
bus.write_long(ctrl_rec, 0); // nextControl
bus.write_long(ctrl_rec + 4, dialog_ptr); // contrlOwner
// contrlRect: dialog-local coordinates (draw_control gets
// screen offset from the owner window's PixMap bounds)
bus.write_word(ctrl_rec + 8, item.rect.0 as u16);
bus.write_word(ctrl_rec + 10, item.rect.1 as u16);
bus.write_word(ctrl_rec + 12, item.rect.2 as u16);
bus.write_word(ctrl_rec + 14, item.rect.3 as u16);
bus.write_byte(ctrl_rec + 16, 255); // contrlVis = visible
bus.write_byte(ctrl_rec + 17, 0); // contrlHilite
let value = self
.dialog_control_values
.get(&(dialog_ptr, item_no))
.copied()
.unwrap_or(0);
bus.write_word(ctrl_rec + 18, value as u16);
bus.write_word(ctrl_rec + 20, 0); // contrlMin
bus.write_word(ctrl_rec + 22, 1); // contrlMax
// Write title as pascal string at offset 40
bus.write_byte(ctrl_rec + 40, title_len as u8);
for (i, &ch) in item.text.as_bytes().iter().take(title_len).enumerate()
{
bus.write_byte(ctrl_rec + 41 + i as u32, ch);
}
// Map DITL item type to Control Manager procID
let proc_id: i16 = match base_type {
4 => 0, // btnCtrl → pushButProc
5 => 1, // chkCtrl → checkBoxProc
6 => 2, // radCtrl → radioButProc
_ => 0,
};
self.control_proc_ids.insert(ctrl_rec, proc_id);
let handle = bus.alloc(4);
bus.write_long(handle, ctrl_rec);
bus.write_long(item_handle_ptr, handle);
self.dialog_control_handles
.insert(handle, (dialog_ptr, item_no));
// Also update the DITL item handle storage
Self::set_dialog_item_handle(bus, dialog_ptr, item_no, handle);
}
}
if box_ptr != 0 {
bus.write_word(box_ptr, item.rect.0 as u16);
bus.write_word(box_ptr + 2, item.rect.1 as u16);
bus.write_word(box_ptr + 4, item.rect.2 as u16);
bus.write_word(box_ptr + 6, item.rect.3 as u16);
}
// Some apps use InsertMenu -> GetDItem -> SetDItem to
// build a custom popup control backed by a userItem. Do
// not promote on GetDItem alone: plain userItem grids also
// query their item rectangles and must not inherit a stale
// popup-menu association.
if let Some(menu_id) = self.last_inserted_menu_id.take() {
let enabled_user_item =
(item.item_type & 0x7F) == 0 && (item.item_type & 0x80) == 0;
if enabled_user_item {
if item.proc_ptr != 0 {
self.dialog_item_popup_menus
.insert((dialog_ptr, item_no), menu_id);
self.dialog_popup_original_rects
.insert((dialog_ptr, item_no), item.rect);
self.pending_dialog_popup_menu = None;
} else {
self.pending_dialog_popup_menu = Some(PendingDialogPopupMenu {
dialog_ptr,
item_no,
menu_id,
rect: item.rect,
});
}
} else {
self.pending_dialog_popup_menu = None;
}
}
} else {
if type_ptr != 0 {
bus.write_word(type_ptr, 0);
}
if item_handle_ptr != 0 {
bus.write_long(item_handle_ptr, 0);
}
if box_ptr != 0 {
bus.write_long(box_ptr, 0);
bus.write_long(box_ptr + 4, 0);
}
}
cpu.write_reg(Register::A7, sp + 18);
Ok(())
}
// SetDItem ($A98E)
// Sets information about a dialog item.
// PROCEDURE SetDItem (theDialog: DialogPtr; itemNo: INTEGER;
// itemType: INTEGER; item: Handle; box: Rect);
// Inside Macintosh Volume I, I-421
//
// Stack layout (box passed as pointer-to-Rect, not inline):
// SP+0..3: &box (pointer to Rect — MPW compiler passes large
// value params by reference)
// SP+4..7: item (Handle / ProcPtr)
// SP+8..9: itemType
// SP+10..11: itemNo
// SP+12..15: theDialog
// SetDItem ($A98E): Stores item type, rect, and proc_ptr (for userItem); updates both dialog_items and active tracking state
(true, 0x18E) => {
let sp = cpu.read_reg(Register::A7);
let box_ptr = bus.read_long(sp);
let item_handle = bus.read_long(sp + 4);
let item_type = bus.read_word(sp + 8) as u8;
let item_no = bus.read_word(sp + 10) as i16;
let dialog_ptr = bus.read_long(sp + 12);
let (box_top, box_left, box_bottom, box_right) = if box_ptr != 0 {
(
bus.read_word(box_ptr) as i16,
bus.read_word(box_ptr + 2) as i16,
bus.read_word(box_ptr + 4) as i16,
bus.read_word(box_ptr + 6) as i16,
)
} else {
(0, 0, 0, 0)
};
let base_type = item_type & 0x7F;
let previous_handle = Self::dialog_item_handle(bus, dialog_ptr, item_no);
let previous_item = self
.dialog_items
.get(&dialog_ptr)
.and_then(|items| {
(item_no > 0)
.then(|| items.get((item_no - 1) as usize))
.flatten()
})
.cloned();
if trace_dialog_items_enabled() {
eprintln!(
"[DIALOG-ITEM] SetDItem pc=${:08X} sp=${:08X} rawbytes={:02X?} dialog=${:08X} item={} type={} proc=${:08X} rect=({},{},{},{})",
cpu.read_reg(Register::PC),
sp,
(0..24u32).map(|i| bus.read_byte(sp + i)).collect::<Vec<u8>>(),
dialog_ptr,
item_no,
item_type,
item_handle,
box_top,
box_left,
box_bottom,
box_right,
);
}
if previous_handle != 0 {
self.dialog_item_handles.remove(&previous_handle);
self.dialog_control_handles.remove(&previous_handle);
}
if let Some(item_handle_addr) =
Self::dialog_item_handle_addr(bus, dialog_ptr, item_no)
{
bus.write_long(item_handle_addr, item_handle);
bus.write_word(item_handle_addr + 4, box_top as u16);
bus.write_word(item_handle_addr + 6, box_left as u16);
bus.write_word(item_handle_addr + 8, box_bottom as u16);
bus.write_word(item_handle_addr + 10, box_right as u16);
bus.write_byte(item_handle_addr + 12, item_type);
}
if let Some(items) = self.dialog_items.get_mut(&dialog_ptr) {
if item_no > 0 && (item_no as usize) <= items.len() {
let item = &mut items[(item_no - 1) as usize];
item.item_type = item_type;
item.rect = (box_top, box_left, box_bottom, box_right);
if base_type == 0 {
// userItem: the "item" parameter is a ProcPtr
// Inside Macintosh Volume I, I-405
item.proc_ptr = item_handle;
} else if base_type == 8 || base_type == 16 {
item.text = Self::text_item_string_from_handle(bus, item_handle);
}
}
}
if let Some(pending) = self.pending_dialog_popup_menu {
if pending.dialog_ptr == dialog_ptr && pending.item_no == item_no {
if base_type == 0 && (item_type & 0x80) == 0 && item_handle != 0 {
self.dialog_item_popup_menus
.insert((dialog_ptr, item_no), pending.menu_id);
self.dialog_popup_original_rects
.insert((dialog_ptr, item_no), pending.rect);
}
self.pending_dialog_popup_menu = None;
}
}
if let Some(previous_item) = previous_item {
let key = (dialog_ptr, item_no);
let previous_base_type = previous_item.item_type & 0x7F;
let previous_enabled_user_item =
previous_base_type == 0 && (previous_item.item_type & 0x80) == 0;
let current_enabled_user_item = base_type == 0 && (item_type & 0x80) == 0;
let old_width = previous_item.rect.3 - previous_item.rect.1;
let old_height = previous_item.rect.2 - previous_item.rect.0;
let new_width = box_right - box_left;
let new_height = box_bottom - box_top;
let narrowed_to_popup_indicator = previous_enabled_user_item
&& current_enabled_user_item
&& item_handle == 0
&& old_width >= 40
&& new_width > 0
&& new_width <= 24
&& old_height > 0
&& new_height > 0
&& (old_height - new_height).abs() <= 4;
if narrowed_to_popup_indicator {
self.dialog_popup_original_rects
.entry(key)
.or_insert(previous_item.rect);
self.dialog_popup_candidate_items.insert(key);
} else if !current_enabled_user_item {
self.dialog_popup_candidate_items.remove(&key);
if !self.dialog_item_popup_menus.contains_key(&key) {
self.dialog_popup_original_rects.remove(&key);
}
}
}
if base_type == 8 || base_type == 16 {
if item_handle != 0 {
self.dialog_item_handles
.insert(item_handle, (dialog_ptr, (item_no - 1) as usize));
}
} else if (base_type == 4 || base_type == 5 || base_type == 6 || base_type == 7)
&& item_handle != 0
{
self.dialog_control_handles
.insert(item_handle, (dialog_ptr, item_no));
let ctrl_ptr = bus.read_long(item_handle);
if ctrl_ptr != 0 {
bus.write_word(ctrl_ptr + 8, box_top as u16);
bus.write_word(ctrl_ptr + 10, box_left as u16);
bus.write_word(ctrl_ptr + 12, box_bottom as u16);
bus.write_word(ctrl_ptr + 14, box_right as u16);
self.dialog_control_values
.insert((dialog_ptr, item_no), bus.read_word(ctrl_ptr + 18) as i16);
}
}
// Also update tracking state if dialog is currently active
if let Some(ref mut tracking) = self.dialog_tracking {
if tracking.dialog_ptr == dialog_ptr
&& item_no > 0
&& (item_no as usize) <= tracking.items.len()
{
let item = &mut tracking.items[(item_no - 1) as usize];
item.item_type = item_type;
item.rect = (box_top, box_left, box_bottom, box_right);
if base_type == 0 {
item.proc_ptr = item_handle;
} else if base_type == 8 || base_type == 16 {
item.text = Self::text_item_string_from_handle(bus, item_handle);
if tracking.edit_item == item_no {
tracking.edit_text = item.text.clone();
}
}
}
}
cpu.write_reg(Register::A7, sp + 16);
Ok(())
}
// ModalDialog ($A991)
// Handles events in a modal dialog until an enabled item is hit.
// PROCEDURE ModalDialog (filterProc: ProcPtr; VAR itemHit: INTEGER);
// Inside Macintosh Volume I, I-415
// 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
(true, 0x191) => {
// Check if draw procs need to finish before entering event loop.
if let Some(ref tracking) = self.dialog_tracking {
if !tracking.draw_procs_done {
return Some(Ok(()));
}
}
// Re-snapshot rendered_pixels after draw procs or filter proc completes.
// rendered_pixels_final is cleared when either is injected; the snapshot
// here captures whatever they drew before redraw_chrome can restore it.
if let Some(ref tracking) = self.dialog_tracking {
if !tracking.rendered_pixels_final {
let bounds = tracking.bounds;
let items = tracking.items.clone();
let default_item = tracking.default_item;
let edit_text = tracking.edit_text.clone();
let edit_item = tracking.edit_item;
let dialog_ptr = tracking.dialog_ptr;
let popup_draws = tracking.popup_draws.clone();
if !tracking.game_managed {
if self.front_window == dialog_ptr {
self.blit_window_to_screen(bus);
}
self.redraw_standard_dialog_items(
bus,
bounds,
&items,
default_item,
&edit_text,
edit_item,
dialog_ptr,
);
}
self.redraw_dialog_popup_controls(bus, &popup_draws);
let rendered = self.save_dialog_pixels(bus, bounds);
let t = self.dialog_tracking.as_mut().unwrap();
t.rendered_pixels = rendered;
t.rendered_pixels_final = true;
}
}
if let Some(ref tracking) = self.dialog_tracking {
if tracking.active_button.is_some() {
self.handle_dialog_button_tracking(bus);
return Some(Ok(()));
}
if tracking.active_popup.is_some() {
self.handle_dialog_popup_tracking(cpu, bus);
return Some(Ok(()));
}
if tracking.active_user_item.is_some() {
self.handle_dialog_user_item_tracking(cpu, bus);
return Some(Ok(()));
}
// Fast path — when nothing can produce an item hit or visible
// update on this step (no filter proc, no flash animation, no
// pending event, no queued events), return Ok without running
// any of the re-fire body. Any of these flags being non-default
// routes through the full handler below.
if tracking.filter_proc == 0
&& tracking.flash_remaining == 0
&& tracking.active_button.is_none()
&& tracking.active_popup.is_none()
&& tracking.active_user_item.is_none()
&& self.event_queue.is_empty()
{
return Some(Ok(()));
}
// Re-fire: dialog tracking is active
let dialog_ptr = tracking.dialog_ptr;
let bounds = tracking.bounds;
let item_hit_ptr = tracking.item_hit_ptr;
let stack_ptr = tracking.stack_ptr;
let filter_proc = tracking.filter_proc;
let flash_remaining = tracking.flash_remaining;
// Items_clone is built lazily in the mouseDown branch below to
// avoid cloning Vec<DialogItem> on every ModalDialog refire.
let mut pending_event = None;
// For any dialog with a filter proc, check whether the filter
// handled the most recent event. Per Inside Macintosh Volume I, I-415:
// TRUE means the filter handled the event and set itemHit;
// FALSE means ModalDialog should process the event itself.
let mut waiting_for_filter_proc_event = false;
if filter_proc != 0 {
let result_addr = self.dialog_filter_result_addr;
let tracking_dialog_ptr = tracking.dialog_ptr;
let filter_result_word = if result_addr != 0 {
bus.read_word(result_addr)
} else {
0
};
let filter_returned_true = if result_addr != 0 {
// Stack-based Pascal BOOLEAN results are encoded
// in bit 0 of the high-order byte, not as any
// nonzero word. Inside Macintosh Volume I,
// "Using Assembly Language", stack-based routines.
(filter_result_word & 0x0100) != 0
} else {
false
};
let mut hit = 0i16;
if filter_returned_true && item_hit_ptr != 0 {
hit = bus.read_word(item_hit_ptr) as i16;
}
let filter_event = self
.dialog_tracking
.as_mut()
.and_then(|t| t.last_filter_event.take());
if trace_dialog_filter_enabled() {
eprintln!(
"[DIALOG-FILTER] result dialog=${:08X} result_word=${:04X} returned_true={} item_hit={} item_hit_ptr=${:08X}",
tracking_dialog_ptr,
filter_result_word,
filter_returned_true,
hit,
item_hit_ptr
);
}
if hit > 0 || filter_returned_true {
let handled_mouse_down =
filter_event.as_ref().is_some_and(|e| e.what == 1);
let (dialog_ptr, edit_item, edit_text, items) = {
let tracking = self.dialog_tracking.as_mut().unwrap();
Self::sync_tracking_active_edit_item(tracking);
(
tracking.dialog_ptr,
tracking.edit_item,
tracking.edit_text.clone(),
tracking.items.clone(),
)
};
let keep_dialog_visible = items
.get((hit - 1) as usize)
.map(|item| (item.item_type & 0x7F) != 4)
.unwrap_or(true);
let item_type =
items.get((hit - 1) as usize).map(|item| item.item_type);
self.flush_dialog_edit_item_texts(
bus, dialog_ptr, &items, edit_item, &edit_text,
);
// Filter handled the event — end tracking and return.
let saved = self.dialog_tracking.take().unwrap();
let saved_dialog_ptr = saved.dialog_ptr;
let saved_bounds = saved.bounds;
if keep_dialog_visible {
self.persist_visible_dialog_snapshot(bus, &saved);
}
self.dialog_saved_pixels
.insert(saved_dialog_ptr, saved.saved_pixels);
if hit > 0
&& item_type.is_some_and(|ty| (ty & 0x7F) == 4 && (ty & 0x80) == 0)
{
self.pending_modal_button_dispose_dialog = Some(saved_dialog_ptr);
}
if handled_mouse_down {
self.consume_dialog_mouse_up();
}
if trace_dialog_filter_enabled() {
let actual_hit = bus.read_word(item_hit_ptr) as i16;
eprintln!(
"[DIALOG-FILTER] ModalDialog returning: filter hit={} actual_item_hit_ptr=${:08X} actual_hit={} handled_mouseDown={} stack_ptr=${:08X} queue_len={}",
hit, item_hit_ptr, actual_hit, handled_mouse_down, stack_ptr, self.event_queue.len()
);
}
let outcome = if hit > 0 {
if keep_dialog_visible {
"filter_item_hit_retained"
} else {
"filter_item_hit_dismissed"
}
} else {
"filter_true_zero_hit"
};
self.record_modal_dialog_filter_input_trace(
"filter_result",
saved_dialog_ptr,
saved_bounds,
filter_proc,
filter_event.as_ref(),
hit,
item_type,
handled_mouse_down,
keep_dialog_visible,
"returned",
outcome,
);
cpu.write_reg(Register::A7, stack_ptr + 8);
return Some(Ok(()));
}
pending_event = filter_event;
if let Some(ref event) = pending_event {
self.record_modal_dialog_filter_input_trace(
"filter_result",
tracking_dialog_ptr,
bounds,
filter_proc,
Some(event),
0,
None,
false,
true,
"passed",
"filter_declined",
);
}
waiting_for_filter_proc_event = pending_event.is_none();
}
if flash_remaining > 0 {
// Button flash animation
let flash_item = self.dialog_tracking.as_ref().unwrap().flash_item;
let (remaining, button_draw) = {
let t = self.dialog_tracking.as_mut().unwrap();
if t.flash_delay > 0 {
t.flash_delay -= 1;
return Some(Ok(()));
}
t.flash_remaining -= 1;
t.flash_delay = 3;
let remaining = t.flash_remaining;
let button_draw = if remaining > 0
&& flash_item > 0
&& (flash_item as usize) <= t.items.len()
{
let item = &t.items[(flash_item - 1) as usize];
Some((
Self::dialog_item_screen_rect(bounds, item.rect),
item.text.clone(),
flash_item == t.default_item,
remaining % 2 == 0,
))
} else {
None
};
(remaining, button_draw)
};
if let Some((screen_rect, title, is_default, highlighted)) = button_draw {
// Toggle button highlight.
self.draw_dialog_button_highlight_state(
bus,
screen_rect,
&title,
is_default,
highlighted,
);
}
if remaining == 0 {
// Flash complete — write all editText item handles
// back before returning the hit item to the app.
let (dialog_ptr, edit_item, edit_text, items) = {
let tracking = self.dialog_tracking.as_mut().unwrap();
Self::sync_tracking_active_edit_item(tracking);
(
tracking.dialog_ptr,
tracking.edit_item,
tracking.edit_text.clone(),
tracking.items.clone(),
)
};
self.flush_dialog_edit_item_texts(
bus, dialog_ptr, &items, edit_item, &edit_text,
);
let saved = self.dialog_tracking.take().unwrap();
let saved_dialog_ptr = saved.dialog_ptr;
let saved_bounds = saved.bounds;
let item_type = saved
.items
.get(flash_item.saturating_sub(1) as usize)
.map(|item| item.item_type);
self.persist_visible_dialog_snapshot(bus, &saved);
self.dialog_saved_pixels
.insert(saved_dialog_ptr, saved.saved_pixels);
self.pending_modal_button_dispose_dialog = Some(saved_dialog_ptr);
// ModalDialog button clicks should consume the
// matching mouseUp before returning to the app.
// If we leave it queued, the release can leak into
// the underlying game view after the dialog closes.
self.consume_dialog_mouse_up();
if item_hit_ptr != 0 {
bus.write_word(item_hit_ptr, flash_item as u16);
}
self.record_modal_dialog_input_trace(
"finish",
saved_dialog_ptr,
saved_bounds,
flash_item,
item_type,
None,
"returned",
"button_item_hit",
);
cpu.write_reg(Register::A7, stack_ptr + 8);
}
return Some(Ok(()));
}
// With a non-NIL filterProc, ModalDialog passes each event
// to the filter before Dialog Manager default handling.
// Leave queued events alone here; runner.rs will inject the
// filter callback, and this handler will process the returned
// event only if that callback returns FALSE. MTE 1992 6-136.
if waiting_for_filter_proc_event {
return Some(Ok(()));
}
let event = if let Some(e) = pending_event {
Some(e)
} else if !self.event_queue.is_empty() {
// Drain events looking for actionable ones
let mut event = None;
while let Some(e) = self.event_queue.pop_front() {
match e.what {
1 | 2 | 3 | 6 => {
event = Some(e);
break;
}
_ => {} // discard other events
}
}
event
} else {
None
};
if let Some(mut e) = event {
if e.what == 0 && self.mouse_button {
e.what = 1;
e.where_v = self.mouse_pos.0;
e.where_h = self.mouse_pos.1;
}
match e.what {
// updateEvt — re-snapshot rendered_pixels.
// Redraw HLE popup controls first, since the game
// may have drawn narrow indicator boxes that would
// taint the snapshot.
6 => {
// MTE 1992 pp. 6-135 and 6-141: ModalDialog
// handles dialog update events through the same
// DialogSelect path that brackets DrawDialog
// with BeginUpdate/EndUpdate and makes the
// dialog the current graphics port.
self.begin_update_window(bus, dialog_ptr);
self.set_current_port_state(bus, cpu, dialog_ptr, None);
let previous_rendered = self
.dialog_tracking
.as_ref()
.filter(|t| t.rendered_pixels_final)
.map(|t| t.rendered_pixels.clone())
.unwrap_or_default();
if !previous_rendered.is_empty() {
self.restore_dialog_pixels(bus, bounds, &previous_rendered);
}
if let Some(ref t) = self.dialog_tracking {
if !t.game_managed {
self.redraw_standard_dialog_items(
bus,
bounds,
&t.items,
t.default_item,
&t.edit_text,
t.edit_item,
t.dialog_ptr,
);
}
let popup_draws = t.popup_draws.clone();
self.redraw_dialog_popup_controls(bus, &popup_draws);
}
let rendered = self.save_dialog_pixels(bus, bounds);
let t = self.dialog_tracking.as_mut().unwrap();
t.rendered_pixels = rendered;
t.rendered_pixels_final = true;
self.end_update_window(bus, dialog_ptr);
}
// mouseDown
1 => {
// Clone items lazily — only when actually processing
// a mouseDown. Hot path (no events) skips this entirely.
let items_clone: Vec<DialogItem> = self
.dialog_tracking
.as_ref()
.map(|t| t.items.clone())
.unwrap_or_default();
let mut hit = self.dialog_item_hit_test(
bus,
&items_clone,
bounds,
e.where_v,
e.where_h,
&self.dialog_popup_original_rects,
dialog_ptr,
);
if hit <= 0 {
hit = Self::dialog_button_hit_test(
&items_clone,
bounds,
e.where_v,
e.where_h,
);
}
if hit > 0 {
let item = &items_clone[(hit - 1) as usize];
let base_type = item.item_type & 0x7F;
let is_disabled = (item.item_type & 0x80) != 0;
if !is_disabled {
match base_type {
// Button click: start flash
4 => {
let (abs_top, abs_left, abs_bottom, abs_right) =
Self::dialog_item_screen_rect(
bounds, item.rect,
);
let is_default = hit
== self
.dialog_tracking
.as_ref()
.map(|tracking| tracking.default_item)
.unwrap_or(0);
self.draw_dialog_button_highlight_state(
bus,
(abs_top, abs_left, abs_bottom, abs_right),
&item.text,
is_default,
true,
);
if self.mouse_button {
let t = self.dialog_tracking.as_mut().unwrap();
t.active_button = Some(
super::dispatch::DialogButtonTrackingState {
item_no: hit,
rect: item.rect,
title: item.text.clone(),
is_default,
highlighted: true,
},
);
self.record_modal_dialog_input_trace(
"mouse_down",
dialog_ptr,
bounds,
hit,
Some(item.item_type),
Some(true),
"pending",
"button_tracking_started",
);
} else {
self.start_dialog_button_flash(
bus, bounds, hit, item.rect, &item.text,
is_default, true,
);
self.record_modal_dialog_input_trace(
"mouse_down",
dialog_ptr,
bounds,
hit,
Some(item.item_type),
Some(true),
"pending",
"start_flash",
);
}
}
// Checkbox click: return item number immediately
// The dialog stays on screen — the app toggles
// the checkbox value and calls ModalDialog again.
// Inside Macintosh Volume I, I-415
5 => {
let (dlg_ptr, edit_item, edit_text, items) = {
let tracking =
self.dialog_tracking.as_mut().unwrap();
Self::sync_tracking_active_edit_item(tracking);
(
tracking.dialog_ptr,
tracking.edit_item,
tracking.edit_text.clone(),
tracking.items.clone(),
)
};
self.flush_dialog_edit_item_texts(
bus, dlg_ptr, &items, edit_item, &edit_text,
);
// Preserve saved background pixels for re-entry
let saved = self.dialog_tracking.take().unwrap();
let saved_dialog_ptr = saved.dialog_ptr;
let saved_bounds = saved.bounds;
self.persist_visible_dialog_snapshot(bus, &saved);
self.dialog_saved_pixels
.insert(saved_dialog_ptr, saved.saved_pixels);
self.consume_dialog_mouse_up();
// Don't restore pixels — dialog stays visible
if item_hit_ptr != 0 {
bus.write_word(item_hit_ptr, hit as u16);
}
self.record_modal_dialog_input_trace(
"mouse_down",
saved_dialog_ptr,
saved_bounds,
hit,
Some(item.item_type),
None,
"returned",
"checkbox_item_hit_retained",
);
cpu.write_reg(Register::A7, stack_ptr + 8);
}
// EditText click: set as active
16 => {
let (dlg_ptr, edit_item, edit_text, items) = {
let tracking =
self.dialog_tracking.as_mut().unwrap();
Self::sync_tracking_active_edit_item(tracking);
tracking.edit_item = hit;
tracking.edit_text = tracking
.items
.get((hit - 1) as usize)
.map(|item| item.text.clone())
.unwrap_or_default();
tracking.edit_text_modified = false;
(
tracking.dialog_ptr,
tracking.edit_item,
tracking.edit_text.clone(),
tracking.items.clone(),
)
};
// IM:I I-415: mouseDown in an enabled editText
// item is TextEdit-handled and ModalDialog
// returns that item. TEClick's pixel-to-caret
// mapping remains the documented HLE compromise,
// but the active editField/TERecord mirror is
// still guest-visible Dialog Manager state.
self.activate_dialog_edit_item(
bus, cpu, dlg_ptr, &items, edit_item,
);
self.flush_dialog_edit_item_texts(
bus, dlg_ptr, &items, edit_item, &edit_text,
);
let saved = self.dialog_tracking.take().unwrap();
let saved_dialog_ptr = saved.dialog_ptr;
let saved_bounds = saved.bounds;
self.persist_visible_dialog_snapshot(bus, &saved);
self.dialog_saved_pixels
.insert(saved_dialog_ptr, saved.saved_pixels);
self.consume_dialog_mouse_up();
if item_hit_ptr != 0 {
bus.write_word(item_hit_ptr, hit as u16);
}
self.record_modal_dialog_input_trace(
"mouse_down",
saved_dialog_ptr,
saved_bounds,
hit,
Some(item.item_type),
None,
"returned",
"edittext_item_hit_retained",
);
cpu.write_reg(Register::A7, stack_ptr + 8);
}
// resCtrl popup controls are tracked by
// ModalDialog itself, matching the standard
// Dialog Manager control-item path.
7 if self.begin_dialog_popup_tracking(
bus, dialog_ptr, hit,
) => {}
// UserItem with popup menu: return item number.
// Dialog stays on screen — the app calls
// PopUpMenuSelect to show the popup dropdown.
// Macintosh Toolbox Essentials 1992, 5-26.
// Original Marathon-style compact popup candidates
// use the same app-owned tracking path after
// InsertMenu/GetDItem/SetDItem narrows the DITL rect.
0 if self
.dialog_item_popup_menus
.contains_key(&(dialog_ptr, hit))
|| self
.dialog_popup_candidate_items
.contains(&(dialog_ptr, hit)) =>
{
let (dlg_ptr, edit_item, edit_text, items) = {
let tracking =
self.dialog_tracking.as_mut().unwrap();
Self::sync_tracking_active_edit_item(tracking);
(
tracking.dialog_ptr,
tracking.edit_item,
tracking.edit_text.clone(),
tracking.items.clone(),
)
};
self.flush_dialog_edit_item_texts(
bus, dlg_ptr, &items, edit_item, &edit_text,
);
let saved = self.dialog_tracking.take().unwrap();
self.persist_visible_dialog_snapshot(bus, &saved);
self.dialog_saved_pixels
.insert(saved.dialog_ptr, saved.saved_pixels);
self.consume_dialog_mouse_up();
if item_hit_ptr != 0 {
bus.write_word(item_hit_ptr, hit as u16);
}
cpu.write_reg(Register::A7, stack_ptr + 8);
}
// Plain userItems in standard dialogs are custom
// hit targets. Track the physical click until
// release so the application receives the item
// hit after the completed click, matching the
// standard control paths above.
0 if self.dialog_tracking.as_ref().is_some_and(
|tracking| {
self.is_plain_modal_user_item(
tracking, hit, item,
)
},
) && self.mouse_button =>
{
let active_rect = self
.dialog_tracking
.as_ref()
.and_then(|tracking| {
self.dialog_popup_original_rects
.get(&(tracking.dialog_ptr, hit))
.copied()
})
.unwrap_or(item.rect);
if let Some(t) = self.dialog_tracking.as_mut() {
t.active_user_item =
Some(DialogUserItemTrackingState {
item_no: hit,
rect: active_rect,
});
}
}
// Any other enabled item: return item number immediately.
// Inside Macintosh Volume I, I-428
_ => {
let (dlg_ptr, edit_item, edit_text, items) = {
let tracking =
self.dialog_tracking.as_mut().unwrap();
Self::sync_tracking_active_edit_item(tracking);
(
tracking.dialog_ptr,
tracking.edit_item,
tracking.edit_text.clone(),
tracking.items.clone(),
)
};
self.flush_dialog_edit_item_texts(
bus, dlg_ptr, &items, edit_item, &edit_text,
);
let saved = self.dialog_tracking.take().unwrap();
self.persist_visible_dialog_snapshot(bus, &saved);
self.dialog_saved_pixels
.insert(saved.dialog_ptr, saved.saved_pixels);
self.consume_dialog_mouse_up();
if item_hit_ptr != 0 {
bus.write_word(item_hit_ptr, hit as u16);
}
cpu.write_reg(Register::A7, stack_ptr + 8);
}
}
}
}
}
// keyDown
3 => {
let char_code = (e.message & 0xFF) as u8;
let key_code = ((e.message >> 8) & 0xFF) as u8;
let command_period =
char_code == b'.' && (e.modifiers & 0x0100) != 0;
let command_select_all = char_code.eq_ignore_ascii_case(&b'a')
&& (e.modifiers & 0x0100) != 0;
match char_code {
// Return or Enter: trigger default button
0x0D | 0x03 => {
let target =
self.dialog_tracking.as_ref().and_then(|tracking| {
let def = tracking.default_item;
if def <= 0 {
return None;
}
tracking
.items
.get(def.saturating_sub(1) as usize)
.map(|item| {
(def, item.rect, item.text.clone(), true)
})
});
if let Some((def, rect, title, is_default)) = target {
let screen_rect =
Self::dialog_item_screen_rect(bounds, rect);
self.draw_dialog_button_highlight_state(
bus,
screen_rect,
&title,
is_default,
true,
);
let t = self.dialog_tracking.as_mut().unwrap();
t.flash_remaining = 6;
t.flash_delay = 3;
t.flash_item = def;
}
}
// Escape or Command-period: trigger cancel button.
// MTE 1992 p. 6-138 maps Esc and Command-period
// to Cancel before dialog-select style handling.
0x1B | b'.' if char_code == 0x1B || command_period => {
let target =
self.dialog_tracking.as_ref().and_then(|tracking| {
let cancel = tracking.cancel_item;
if cancel <= 0 {
return None;
}
tracking
.items
.get(cancel.saturating_sub(1) as usize)
.map(|item| {
(
cancel,
item.rect,
item.text.clone(),
cancel == tracking.default_item,
)
})
});
if let Some((cancel, rect, title, is_default)) = target {
let screen_rect =
Self::dialog_item_screen_rect(bounds, rect);
self.draw_dialog_button_highlight_state(
bus,
screen_rect,
&title,
is_default,
true,
);
let t = self.dialog_tracking.as_mut().unwrap();
t.flash_remaining = 6;
t.flash_delay = 3;
t.flash_item = cancel;
}
}
// Tab: move to the next editText item, wrapping.
0x09 => {
let mut switched = false;
if let Some(tracking) = self.dialog_tracking.as_mut() {
if tracking.edit_item > 0 {
Self::sync_tracking_active_edit_item(tracking);
}
if !tracking.items.is_empty() {
let start = tracking.edit_item.max(0) as usize;
let next =
(0..tracking.items.len()).find_map(|offset| {
let idx =
(start + offset) % tracking.items.len();
if (tracking.items[idx].item_type & 0x7F)
== 16
{
Some(idx)
} else {
None
}
});
if let Some(idx) = next {
tracking.edit_item = (idx + 1) as i16;
tracking.edit_text =
tracking.items[idx].text.clone();
tracking.edit_text_modified = false;
bus.write_word(dialog_ptr + 164, idx as u16);
switched = true;
}
}
}
if switched {
self.refresh_dialog_tracking_snapshot(bus);
}
}
// Backspace/Delete or printable ASCII.
_ if command_select_all => {
let mut select_trace = None;
let mut modified_key_to_clear = None;
if let Some(tracking) = self.dialog_tracking.as_mut() {
let text_before = tracking.edit_text.clone();
if tracking.edit_item > 0 {
let text_len = tracking.edit_text.len();
tracking.edit_text_modified = false;
Self::set_tracking_active_edit_selection(
tracking, 0, text_len,
);
Self::sync_tracking_active_edit_item(tracking);
let edit_item = tracking.edit_item;
let item_type = tracking
.items
.get((edit_item - 1) as usize)
.map(|item| item.item_type);
let enabled_edit_text = item_type
.map(|ty| (ty & 0x7F) == 16 && (ty & 0x80) == 0)
.unwrap_or(false);
let text_after = tracking.edit_text.clone();
select_trace = Some((
edit_item,
item_type,
text_before,
text_after,
enabled_edit_text,
));
modified_key_to_clear =
Some((tracking.dialog_ptr, edit_item));
}
}
if let Some(key) = modified_key_to_clear {
self.dialog_edit_text_modified_items.remove(&key);
}
if let Some((
edit_item,
item_type,
text_before,
text_after,
enabled_edit_text,
)) = select_trace
{
self.refresh_dialog_tracking_snapshot(bus);
let outcome = if enabled_edit_text {
"enabled_edittext_select_all"
} else {
"edittext_select_all"
};
if enabled_edit_text {
let (dlg_ptr, active_edit_item, edit_text, items) = {
let tracking =
self.dialog_tracking.as_mut().unwrap();
Self::sync_tracking_active_edit_item(tracking);
(
tracking.dialog_ptr,
tracking.edit_item,
tracking.edit_text.clone(),
tracking.items.clone(),
)
};
self.flush_dialog_edit_item_texts(
bus,
dlg_ptr,
&items,
active_edit_item,
&edit_text,
);
let saved = self.dialog_tracking.take().unwrap();
let saved_dialog_ptr = saved.dialog_ptr;
self.persist_visible_dialog_snapshot(bus, &saved);
self.dialog_saved_pixels
.insert(saved_dialog_ptr, saved.saved_pixels);
if item_hit_ptr != 0 {
bus.write_word(
item_hit_ptr,
active_edit_item as u16,
);
}
cpu.write_reg(Register::A7, stack_ptr + 8);
self.record_modal_dialog_text_input_trace(
"key_down",
dialog_ptr,
bounds,
edit_item,
item_type,
key_code,
char_code,
&text_before,
&text_after,
"returned",
outcome,
);
} else {
self.record_modal_dialog_text_input_trace(
"key_down",
dialog_ptr,
bounds,
edit_item,
item_type,
key_code,
char_code,
&text_before,
&text_after,
"pending",
outcome,
);
}
}
}
0x08 | 0x20..=0x7E => {
let mut text_trace = None;
let mut modified_key_to_set = None;
if let Some(tracking) = self.dialog_tracking.as_mut() {
let text_before = tracking.edit_text.clone();
if tracking.edit_item > 0 {
if char_code == 0x08 {
if !tracking.edit_text_modified {
// First backspace clears selection
tracking.edit_text.clear();
tracking.edit_text_modified = true;
} else if !tracking.edit_text.is_empty() {
tracking.edit_text.pop();
}
} else {
if !tracking.edit_text_modified {
// First keypress replaces selection
tracking.edit_text.clear();
tracking.edit_text_modified = true;
}
tracking.edit_text.push(char_code as char);
}
let cursor = tracking.edit_text.len();
Self::set_tracking_active_edit_selection(
tracking, cursor, cursor,
);
Self::sync_tracking_active_edit_item(tracking);
modified_key_to_set =
Some((tracking.dialog_ptr, tracking.edit_item));
let edit_item = tracking.edit_item;
let item_type = tracking
.items
.get((edit_item - 1) as usize)
.map(|item| item.item_type);
let enabled_edit_text = item_type
.map(|ty| (ty & 0x7F) == 16 && (ty & 0x80) == 0)
.unwrap_or(false);
let text_after = tracking.edit_text.clone();
text_trace = Some((
edit_item,
item_type,
text_before,
text_after,
enabled_edit_text,
));
}
}
if let Some(key) = modified_key_to_set {
self.dialog_edit_text_modified_items.insert(key);
}
if let Some((
edit_item,
item_type,
text_before,
text_after,
enabled_edit_text,
)) = text_trace
{
self.refresh_dialog_tracking_snapshot(bus);
let outcome = if enabled_edit_text {
"enabled_edittext_item_hit"
} else {
"edittext_updated"
};
if enabled_edit_text {
let (dlg_ptr, active_edit_item, edit_text, items) = {
let tracking =
self.dialog_tracking.as_mut().unwrap();
Self::sync_tracking_active_edit_item(tracking);
(
tracking.dialog_ptr,
tracking.edit_item,
tracking.edit_text.clone(),
tracking.items.clone(),
)
};
self.flush_dialog_edit_item_texts(
bus,
dlg_ptr,
&items,
active_edit_item,
&edit_text,
);
let saved = self.dialog_tracking.take().unwrap();
let saved_dialog_ptr = saved.dialog_ptr;
self.persist_visible_dialog_snapshot(bus, &saved);
self.dialog_saved_pixels
.insert(saved_dialog_ptr, saved.saved_pixels);
if item_hit_ptr != 0 {
bus.write_word(
item_hit_ptr,
active_edit_item as u16,
);
}
cpu.write_reg(Register::A7, stack_ptr + 8);
self.record_modal_dialog_text_input_trace(
"key_down",
dialog_ptr,
bounds,
edit_item,
item_type,
key_code,
char_code,
&text_before,
&text_after,
"returned",
outcome,
);
} else {
self.record_modal_dialog_text_input_trace(
"key_down",
dialog_ptr,
bounds,
edit_item,
item_type,
key_code,
char_code,
&text_before,
&text_after,
"pending",
outcome,
);
}
}
}
_ => {}
}
}
_ => {}
}
}
} else {
// First call: initialize dialog tracking
let sp = cpu.read_reg(Register::A7);
let item_hit_ptr = bus.read_long(sp);
let filter_proc = bus.read_long(sp + 4);
if item_hit_ptr != 0 {
bus.write_word(item_hit_ptr, 0);
}
// Find the modal dialog's items. Most callers keep the
// dialog as the front window, but games can temporarily
// select another port/window between ModalDialog returns
// and re-entry (for example while tracking a popup menu).
// A real modal dialog remains the Dialog Manager target
// until DisposDialog, so prefer the retained visible
// modal snapshot when the current front window is not a
// known dialog.
let mut dialog_ptr = self.front_window;
if !self.dialog_items.contains_key(&dialog_ptr) {
if let Some((&retained_dialog_ptr, snapshot)) =
self.dialog_visible_snapshots.iter().next()
{
dialog_ptr = retained_dialog_ptr;
self.front_window = retained_dialog_ptr;
self.window_bounds = snapshot.bounds;
self.window_proc_id = self
.window_proc_ids
.get(&retained_dialog_ptr)
.copied()
.unwrap_or(self.window_proc_id);
self.window_title.clear();
}
}
if let Some(mut items) = self.dialog_items.get(&dialog_ptr).cloned() {
if trace_dialog_filter_enabled() {
eprintln!(
"[DIALOG-FILTER] init dialog=${:08X} items={} filter_proc=${:08X} item_hit_ptr=${:08X}",
dialog_ptr,
items.len(),
filter_proc,
item_hit_ptr
);
}
// Re-read userItem proc pointers from guest memory.
// The game may have written them directly to the DITL
// handle data after GetNewDialog returned.
Self::refresh_ditl_proc_ptrs(bus, dialog_ptr, &mut items);
let bounds = self.window_bounds;
let proc_id = self.window_proc_id;
let title = self.window_title.clone();
let (edit_text, edit_item, default_item) =
Self::dialog_edit_state(bus, dialog_ptr, &items);
let cancel_item = self
.dialog_cancel_items
.get(&dialog_ptr)
.copied()
.unwrap_or(2);
let edit_text_modified = edit_item > 0
&& self
.dialog_edit_text_modified_items
.contains(&(dialog_ptr, edit_item));
// If in-bounds items are all userItems, the game
// manages drawing itself; offscreen placeholders do
// not make this a standard dialog.
let game_managed = Self::dialog_is_game_managed(bounds, &items);
if trace_dialog_procs_enabled() {
for (i, item) in items.iter().enumerate() {
eprintln!(
"[DIALOG-PROC] dialog=${:08X} item={} type={} proc=${:08X} rect=({},{},{},{}) text={:?}",
dialog_ptr, i + 1, item.item_type, item.proc_ptr,
item.rect.0, item.rect.1, item.rect.2, item.rect.3,
item.text,
);
if (item.item_type & 0x7F) == 0 {
eprintln!(
"[DIALOG-PROC] dialog=${:08X} item={} type={} proc=${:08X}",
dialog_ptr,
i + 1,
item.item_type,
item.proc_ptr,
);
}
}
}
// Save pixels under dialog area (background to restore on dismiss).
// If we have preserved pixels from a previous non-dismissing return
// (e.g., popup click), reuse those instead of capturing the
// currently visible dialog as "background."
let is_reentry = self.dialog_modal_entered.contains(&dialog_ptr);
let preserved_visible_snapshot =
self.dialog_visible_snapshots.remove(&dialog_ptr);
let reused_retained_visible_snapshot =
is_reentry && preserved_visible_snapshot.is_some();
let preserved_saved_pixels =
self.dialog_saved_pixels.get(&dialog_ptr).cloned();
let restored_visible_snapshot = preserved_visible_snapshot.is_some();
let saved_pixels = preserved_saved_pixels
.unwrap_or_else(|| self.save_dialog_pixels(bus, bounds));
if let Some(snapshot) = preserved_visible_snapshot {
self.restore_dialog_pixels(bus, snapshot.bounds, &snapshot.pixels);
}
if !game_managed && !is_reentry {
// First entry: draw the dialog chrome and controls.
// Before draw_dialog fills the dialog area white, save the
// pixel content of every userItem rect when those pixels
// come from a visible-dialog snapshot. Games (e.g.
// Marathon) often draw custom controls (popup buttons,
// sliders) into userItem rects via QuickDraw before
// calling ModalDialog. Saved-under background pixels are
// only for dismissal restore and must not be treated as
// application-owned userItem drawing.
// Inside Macintosh Volume I, I-405
self.dialog_initial_draw_deferred.remove(&dialog_ptr);
self.draw_dialog_preserving_user_items(
bus,
bounds,
proc_id,
&title,
&items,
default_item,
&edit_text,
edit_item,
false,
dialog_ptr,
restored_visible_snapshot,
true,
true,
);
}
// HLE-draw popup controls for type-0 userItems that were
// associated with MENU resources via the InsertMenu → GetDItem
// pattern used by games (e.g. Marathon) to set up popup
// controls in dialogs. We find the checked item (mark=0x12)
// in each menu and draw a standard popup button for it.
// Inside Macintosh Volume I, I-405 (userItem draw responsibilities)
let implicit_popup_candidate_count = items
.iter()
.enumerate()
.filter(|(i, item)| {
let item_no = (*i + 1) as i16;
(item.item_type & 0x7F) == 0
&& !self
.dialog_item_popup_menus
.contains_key(&(dialog_ptr, item_no))
&& self
.dialog_popup_candidate_items
.contains(&(dialog_ptr, item_no))
})
.count();
let mut implicit_popup_menu_ids: Vec<i16> = self
.menus
.iter()
.filter(|menu| {
menu.in_menu_bar
&& !menu.visible_in_menu_bar
&& menu.items.iter().any(|item| item.mark == 0x12)
})
.map(|menu| menu.id)
.collect();
if implicit_popup_menu_ids.len() < implicit_popup_candidate_count {
implicit_popup_menu_ids = self
.menus
.iter()
.filter(|menu| menu.items.iter().any(|item| item.mark == 0x12))
.map(|menu| menu.id)
.collect();
}
let mut implicit_popup_menu_index = 0usize;
let popup_draws: Vec<DialogPopupDraw> = items
.iter()
.enumerate()
.filter_map(|(i, item)| {
let item_no = (i + 1) as i16;
if (item.item_type & 0x7F) != 0 {
return None;
}
let key = (dialog_ptr, item_no);
let (menu_id, original_rect) = if let Some(&menu_id) =
self.dialog_item_popup_menus.get(&key)
{
let rect = self
.dialog_popup_original_rects
.get(&key)
.copied()
.unwrap_or(item.rect);
(menu_id, rect)
} else if self.dialog_popup_candidate_items.contains(&key) {
let menu_id =
*implicit_popup_menu_ids.get(implicit_popup_menu_index)?;
implicit_popup_menu_index += 1;
let rect = self
.dialog_popup_original_rects
.get(&key)
.copied()
.unwrap_or(item.rect);
(menu_id, rect)
} else {
return None;
};
let checked_text = self
.menus
.iter()
.find(|m| m.id == menu_id)
.and_then(|m| {
m.items
.iter()
.find(|mi| mi.mark == 0x12)
.map(|mi| mi.text.clone())
})
.unwrap_or_default();
// Use the original DITL rect (before SetDItem narrowed it)
let (it_t, it_l, it_b, it_r) = original_rect;
Some(DialogPopupDraw {
rect: (
bounds.0 + it_t,
bounds.1 + it_l,
bounds.0 + it_b,
bounds.1 + it_r,
),
title: checked_text,
enabled: (item.item_type & 0x80) == 0,
pressed: false,
})
})
.collect();
self.redraw_dialog_popup_controls(bus, &popup_draws);
// Snapshot the fully rendered dialog (including PICTs) so
// redraw_chrome can restore it without re-parsing pictures.
// If there are userItem draw procs, this will be re-snapshotted
// after they execute.
let rendered_pixels = self.save_dialog_pixels(bus, bounds);
// Collect userItem draw procs to call. ShowWindow can
// create a visible snapshot before the first
// ModalDialog entry, and that first entry still needs
// the initial userItem update pass. Only skip the
// queue when the same retained modal dialog is
// re-entered after a non-dismissing return: that
// restored visible snapshot already contains the
// completed userItem output, and a real ModalDialog
// re-entry does not manufacture a fresh update pass
// just because the app called it again.
// Inside Macintosh Volume I, I-405 and I-415.
let mut draw_proc_queue = VecDeque::new();
if !reused_retained_visible_snapshot {
for (i, item) in items.iter().enumerate() {
let base_type = item.item_type & 0x7F;
if base_type == 0
&& item.proc_ptr != 0
&& Self::dialog_item_intersects_bounds(bounds, item)
{
draw_proc_queue.push_back((item.proc_ptr, (i + 1) as i16));
}
}
}
let has_draw_procs = !draw_proc_queue.is_empty();
self.dialog_modal_entered.insert(dialog_ptr);
self.dialog_tracking = Some(super::dispatch::DialogTrackingState {
dialog_ptr,
bounds,
title,
proc_id,
items,
default_item,
cancel_item,
edit_text,
edit_item,
saved_pixels,
stack_ptr: sp,
item_hit_ptr,
rendered_pixels,
flash_remaining: 0,
flash_delay: 0,
flash_item: 0,
edit_text_modified,
draw_proc_queue,
draw_procs_done: !has_draw_procs,
rendered_pixels_final: !has_draw_procs,
filter_proc,
game_managed,
last_filter_event: None,
popup_draws,
active_popup: None,
active_button: None,
active_user_item: None,
});
self.record_modal_dialog_input_trace(
"start",
dialog_ptr,
bounds,
0,
None,
None,
"pending",
"open_modal_tracking",
);
// Don't pop stack or advance PC — re-fire pattern
} else {
// No items found — fall back to returning item 1
eprintln!("[TRAP] ModalDialog: no items found, returning 1");
if item_hit_ptr != 0 {
bus.write_word(item_hit_ptr, 1);
}
cpu.write_reg(Register::A7, sp + 8);
}
}
Ok(())
}
// ========== TextEdit Manager ==========
// TEInit ($A9CC)
// Initializes TextEdit's internal globals.
// PROCEDURE TEInit;
// Inside Macintosh Volume I, I-376 ("TEInit
// initializes TextEdit by allocating a handle for
// the TextEdit scrap. The scrap is initially empty.
// Call this procedure once and only once at the
// beginning of your program."). Also note from IM:I
// I-376: "You should call TEInit even if your
// application doesn't use TextEdit, so that desk
// accessories and dialog and alert boxes will work
// correctly."
//
// Per IM:I I-389 the scrap globals are TEScrpHandle
// ($0AB4, 4-byte Handle to the empty/cut/copied
// text block) and TEScrpLength ($0AB0, 2-byte
// INTEGER byte count). TEInit must:
// 1. Allocate a zero-length relocatable block
// and store its handle at TEScrpHandle.
// 2. Set TEScrpLength to 0 (empty scrap).
//
// Idempotent: per IM the routine is documented as
// "call once and only once" but defensive impls
// check for an existing handle and skip the
// re-allocation to avoid leaking the prior one. We
// do the same — apps that violate the IM contract
// and call TEInit twice get a stable handle (no
// double-free).
// 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.
(true, 0x1CC) => {
use crate::memory::globals::addr;
// Idempotency: skip re-allocation if a prior
// TEInit (or first-touch by TECopy / TECut /
// TEPaste) already populated the handle.
let existing = bus.read_long(addr::TE_SCRP_HANDLE);
if existing == 0 {
// Allocate a handle whose master ptr is
// NIL (== empty scrap). Subsequent
// TECopy / TECut grow the underlying
// block via ensure_text_handle_size which
// tolerates the NIL master ptr by lazy-
// allocating on first non-empty write.
// This matches the existing
// TECopy / TECut first-touch pattern that
// calls allocate_handle_with_data(bus, 0).
let handle = Self::allocate_handle_with_data(bus, 0);
bus.write_long(addr::TE_SCRP_HANDLE, handle);
}
bus.write_word(addr::TE_SCRP_LENGTH, 0);
Ok(())
}
// TEPinScroll ($A812)
// Scrolls the text within the view rectangle by the
// requested (dh, dv); stops scrolling when the last line
// of text is scrolled into view.
// PROCEDURE TEPinScroll(dh: INTEGER; dv: INTEGER; hTE: TEHandle);
// Inside Macintosh: Text 1993, p. 2-91.
//
// IM:Text 1993 p. 2-91 verbatim:
// "The TEPinScroll procedure scrolls the text within the
// view rectangle of the specified edit record by the
// designated number of pixels. Scrolling stops when the
// last line of text is scrolled into view. ... The
// destination rectangle is offset by the amount
// scrolled. ... When the edit record is longer than the
// text it contains, TEPinScroll displays up to the last
// line of text inclusive, and not beyond it."
//
// Sign convention (IM:Text 1993 p. 2-91):
// dh > 0: text moves right → destRect.left/right += dh
// dh < 0: text moves left → destRect.left/right += dh
// dv > 0: text moves down → destRect.top/bottom += dv
// dv < 0: text moves up → destRect.top/bottom += dv
//
// Pascal stack frame (args push left-to-right, first
// source arg deepest):
// sp+0 hTE: TEHandle (4) — last arg, shallowest
// sp+4 dv: INTEGER (2) — middle arg
// sp+6 dh: INTEGER (2) — first arg, deepest
// Total pop = 8 bytes; no function-result slot.
//
// MPW Universal Headers TextEdit.h:
// EXTERN_API(void) TEPinScroll(short dh, short dv,
// TEHandle hTE)
// ONEWORDINLINE(0xA812);
//
// Pin semantics: the dv arm clamps so dest_rect.top stays
// within [view_top - (text_bottom - view_bottom), view_top]
// — i.e. far enough that the last line of text remains
// visible at the bottom of the view. The dh arm applies a
// symmetric horizontal clamp. For in-range scrolls the
// call behaves exactly like TEScroll ($A9DD) per the IM
// "offset by the amount scrolled" guarantee.
//
// Regression coverage:
// dialog::tests::te_pin_scroll_reads_handle_from_stack_top
// dialog::tests::tepinscroll_in_range_negative_dv_offsets_destrect_top_and_bottom_exactly_by_dv
// dialog::tests::tepinscroll_pascal_lr_stack_layout_reads_dh_dv_and_hte_from_correct_offsets
// dialog::tests::tepinscroll_clamps_overscroll_when_last_line_is_already_visible
// TEPinScroll ($A812): Offsets `destRect` by the requested delta and pops 8 bytes
(true, 0x012) => {
let sp = cpu.read_reg(Register::A7);
let te_handle = bus.read_long(sp);
let mut dv = bus.read_word(sp + 4) as i16;
let mut dh = bus.read_word(sp + 6) as i16;
cpu.write_reg(Register::A7, sp + 8);
let te_ptr = Self::te_record_ptr(bus, te_handle);
if te_ptr != 0 {
let view_rect = Self::te_read_rect(bus, te_ptr + Self::TE_VIEW_RECT_OFFSET);
let dest_rect = Self::te_read_rect(bus, te_ptr + Self::TE_DEST_RECT_OFFSET);
if dv > 0 {
let max_down = view_rect.0.saturating_sub(dest_rect.0);
if dv > max_down {
dv = max_down;
}
} else if dv < 0 {
// Scrolling up (dv < 0) is bounded by the
// distance between the text's bottom and
// the view's bottom — pinning behaviour
// stops once the last line is visible per
// Text 1993, 2-91. max_up = view_bottom -
// text_bottom: if text already fits (value
// ≥ 0) there's nothing to scroll up to, so
// dv clamps to 0. Otherwise max_up < 0
// gives the amount of up-scroll still available; clamp dv
// upward to max_up so it can't exceed that.
let text_len = Self::te_text_length(bus, te_handle);
let (end_top, _) = self.te_char_to_point(bus, te_handle, text_len);
let end_line = Self::te_char_to_line_index(bus, te_handle, text_len);
let text_bottom = end_top
.saturating_add(Self::te_height_for_line(bus, te_handle, end_line));
let max_up = view_rect.2.saturating_sub(text_bottom);
dv = if max_up >= 0 {
0
} else if dv < max_up {
max_up
} else {
dv
};
}
if dh > 0 {
let max_right = view_rect.1.saturating_sub(dest_rect.1);
dh = if max_right > 0 { dh.min(max_right) } else { 0 };
} else if dh < 0 {
let max_left = view_rect.1.saturating_sub(dest_rect.1);
dh = if max_left < 0 { dh.max(max_left) } else { 0 };
}
if trace_textedit_enabled() {
let adjusted = (
dest_rect.0.saturating_add(dv),
dest_rect.1.saturating_add(dh),
dest_rect.2.saturating_add(dv),
dest_rect.3.saturating_add(dh),
);
eprintln!(
"[TE] TEPinScroll hTE=${:08X} dh={} dv={} dest=({},{},{},{})",
te_handle, dh, dv, adjusted.0, adjusted.1, adjusted.2, adjusted.3
);
}
self.te_scroll_contents(cpu, bus, te_handle, dh, dv);
}
Ok(())
}
// TEAutoView ($A813)
// Enables or disables automatic scrolling for an edit record.
// PROCEDURE TEAutoView(fAuto: Boolean; hTE: TEHandle);
// Text 1993, 2-92
//
// hTE is the LAST parameter (Pascal left-to-right push), so it
// sits at SP+0 above the BOOLEAN at SP+4.
// TEAutoView ($A813): Tracks the auto-scroll feature bit per TEHandle
(true, 0x013) => {
let sp = cpu.read_reg(Register::A7);
let te_handle = bus.read_long(sp);
// Pascal BOOLEAN in high byte (MPW C convention).
let enabled = bus.read_byte(sp + 4) != 0;
self.set_te_feature_bit(te_handle, Self::TE_FEATURE_AUTO_SCROLL, enabled);
if trace_textedit_enabled() {
eprintln!("[TE] TEAutoView hTE=${:08X} enabled={}", te_handle, enabled);
}
cpu.write_reg(Register::A7, sp + 6);
Ok(())
}
// TESelView ($A811)
// PROCEDURE TESelView(hTE: TEHandle);
// Inside Macintosh: Text (1993), p. 2-92.
//
// Per IM:Text 1993 p. 2-92 verbatim: "Once automatic scrolling
// has been enabled by a call to the TEAutoView procedure or
// through the TEFeatureFlag function, the TESelView procedure
// ensures that the selection range is visible and scrolls it
// into the view rectangle if necessary. ... The top left part
// of the selection range is scrolled into view. ... If
// automatic scrolling is disabled, TESelView has no effect."
//
// MPW Universal Headers TextEdit.h:
// EXTERN_API(void) TESelView(TEHandle hTE)
// ONEWORDINLINE(0xA811);
//
// Pascal stack frame:
// sp+0 hTE: TEHandle (4)
// Total pop = 4 bytes. No function result.
//
// Algorithm (matches Apple's documented contract):
// 1. If TE_FEATURE_AUTO_SCROLL is OFF on this hTE → no-op.
// 2. Read viewRect, destRect, and the current selection
// range from the TERec.
// 3. Resolve sel_start and sel_end character offsets to
// pixel coordinates (top_left of selection range and
// bottom_right via line-height lookup).
// 4. Compute dh, dv via te_getdelta — the per-axis shift
// that brings the selection rectangle inside viewRect
// (zero if the selection is already inside).
// 5. Call te_scroll_contents which adds (dh, dv) to
// destRect.{top,left,bottom,right} and redraws.
//
// BasiliskII-vs-Apple divergence note:
// BasiliskII System 7.5.3 ROM does NOT scroll destRect when
// auto-scroll is enabled and the selection lies below viewRect
// — pre and post destRect coincide at (0,0,200,30). Apple's
// IM:Text 1993 p. 2-92 says this case must scroll. Systemless
// implements the Apple-canonical semantic; the divergent rule
// is pinned by the assertion-bearing tests in this module.
(true, 0x011) => {
let sp = cpu.read_reg(Register::A7);
let te_handle = bus.read_long(sp);
cpu.write_reg(Register::A7, sp + 4);
if self.te_feature_bit(te_handle, Self::TE_FEATURE_AUTO_SCROLL) {
let te_ptr = Self::te_record_ptr(bus, te_handle);
if te_ptr != 0 {
let view_rect = Self::te_read_rect(bus, te_ptr + Self::TE_VIEW_RECT_OFFSET);
let dest_rect = Self::te_read_rect(bus, te_ptr + Self::TE_DEST_RECT_OFFSET);
let sel_start = bus.read_word(te_ptr + Self::TE_SEL_START_OFFSET) as usize;
let sel_end = bus.read_word(te_ptr + Self::TE_SEL_END_OFFSET) as usize;
let (start_top, start_left) =
self.te_char_to_point(bus, te_handle, sel_start);
let (stop_top, stop_left) = self.te_char_to_point(bus, te_handle, sel_end);
let stop_line = Self::te_char_to_line_index(bus, te_handle, sel_end);
let stop_bottom = stop_top
.saturating_add(Self::te_height_for_line(bus, te_handle, stop_line));
let dv =
Self::te_getdelta(start_top, stop_bottom, view_rect.0, view_rect.2);
let dh = Self::te_getdelta(start_left, stop_left, view_rect.1, view_rect.3);
if trace_textedit_enabled() {
let adjusted = (
dest_rect.0.saturating_add(dv),
dest_rect.1.saturating_add(dh),
dest_rect.2.saturating_add(dv),
dest_rect.3.saturating_add(dh),
);
eprintln!(
"[TE] TESelView hTE=${:08X} view=({},{},{},{}) dest=({},{},{},{}) adjusted=({},{},{},{})",
te_handle,
view_rect.0,
view_rect.1,
view_rect.2,
view_rect.3,
dest_rect.0,
dest_rect.1,
dest_rect.2,
dest_rect.3,
adjusted.0,
adjusted.1,
adjusted.2,
adjusted.3
);
}
self.te_scroll_contents(cpu, bus, te_handle, dh, dv);
}
}
Ok(())
}
// ========== Cursor Manager ==========
// InitCursor ($A850) - Toolbox version
// Resets to standard arrow cursor
// InitCursor ($A850): Sets arrow cursor, resets cursor level to 0,
// makes visible (IM:I I-167).
(true, 0x050) => {
self.cursor_data = Some(Self::default_arrow_cursor_image());
self.cursor_level = 0;
self.cursor_visible = true;
Ok(())
}
// SetCursor ($A851)
// PROCEDURE SetCursor(crsr: Cursor);
// Cursor record: data[32] + mask[32] + hotSpot.v(2) + hotSpot.h(2) = 68 bytes
// SetCursor ($A851): Reads 68-byte cursor record (16×16 data + mask + hotspot).
// Per IM:I I-167, if the cursor is hidden it stays hidden and only
// changes appearance when uncovered by matching ShowCursor calls.
(true, 0x051) => {
let sp = cpu.read_reg(Register::A7);
let crsr_ptr = bus.read_long(sp);
cpu.write_reg(Register::A7, sp + 4);
// Read cursor bitmap (16x16 = 32 bytes)
let mut data = [0u8; 32];
for (i, byte) in data.iter_mut().enumerate() {
*byte = bus.read_byte(crsr_ptr + i as u32);
}
// Read cursor mask (16x16 = 32 bytes)
let mut mask = [0u8; 32];
for (i, byte) in mask.iter_mut().enumerate() {
*byte = bus.read_byte(crsr_ptr + 32 + i as u32);
}
// Read hotspot
let hot_v = bus.read_word(crsr_ptr + 64) as i16;
let hot_h = bus.read_word(crsr_ptr + 66) as i16;
self.cursor_data = Some(CursorImage::mono(data, mask, hot_v, hot_h));
self.cursor_visible = self.cursor_level == 0;
Ok(())
}
// HideCursor ($A852)
// HideCursor ($A852): Decrements cursor level and hides while level < 0
// per IM:I I-168.
(true, 0x052) => {
self.cursor_level = self.cursor_level.saturating_sub(1);
self.cursor_visible = self.cursor_level == 0;
Ok(())
}
// ShowCursor ($A853)
// ShowCursor ($A853): Increments cursor level toward 0; extra calls
// at level 0 are no-op (IM:I I-168).
(true, 0x053) => {
if self.cursor_level < 0 {
self.cursor_level += 1;
}
self.cursor_visible = self.cursor_level == 0;
Ok(())
}
// ObscureCursor ($A856)
// PROCEDURE ObscureCursor;
// Inside Macintosh Volume I, I-168
// Imaging With QuickDraw 1994, p. 8-29
//
// MPW Universal Headers (Quickdraw.h):
//
// EXTERN_API(void) ObscureCursor(void) ONEWORDINLINE(0xA856);
//
// Pascal PROCEDURE with no arguments and no result slot:
// caller pushes 0 bytes; trap pops 0 bytes; SP unchanged.
//
// Per IM:I I-168: "ObscureCursor hides the cursor until
// the next time the mouse is moved. It's normally
// called when the user begins to type. Unlike
// HideCursor, it has no effect on the cursor level and
// must not be balanced by a call to ShowCursor."
//
// HLE compromise: Systemless synthesizes mouse-move events
// every frame from the scripted event source (or
// every interactive frame from systemless). Honouring
// the "hide until next mouse move" semantic would keep
// the cursor PERMANENTLY hidden because each
// synthesized mouse-move arrives before any "is the
// mouse stationary?" check can materialise the cursor
// (every frame produces both the obscure-trigger and
// the un-obscure-trigger simultaneously). Treating it
// as a no-op preserves cursor visibility — HideCursor
// ($A852) / ShowCursor ($A853) still operate the
// level-counter hide/show stack for explicit pairs in
// apps that need them. Per IM:I I-168 explicit
// "must not be balanced by a call to ShowCursor"
// means apps universally call ObscureCursor without a
// matching ShowCursor — so the no-op contract leaves
// them in the same observable state (cursor visible,
// level unchanged) regardless of dispatch.
//
// The Apple-canonical "hides until mouse move" and
// "must not be balanced by ShowCursor" rules are pinned
// in-Rust via `obscure_cursor_noop_preserves_cursor_level_visibility_and_stack`.
// BII and Systemless HLE diverge on the LowMem CrsrVis
// side-effect (BII System 7.5.3 ROM writes CrsrVis;
// Systemless HLE keeps cursor state internal).
//
// 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.
(true, 0x056) => Ok(()),
// GetCursor ($A9B9)
// FUNCTION GetCursor(cursorID: INTEGER): CursHandle;
// Inside Macintosh Volume I, I-474
//
// "GetCursor returns a handle to the cursor having the
// given resource ID, reading it from the resource file if
// necessary. It calls the Resource Manager function
// GetResource('CURS', cursorID). If the resource can't be
// read, GetCursor returns NIL." — IM:I I-474.
//
// HLE compromise: Systemless doesn't load the System file's
// resource fork, so the four standard system cursor IDs
// documented at IM:I I-475..I-477 are synthesized here
// via [`Self::synthesize_system_cursor`] (cached for
// handle stability — apps cache the GetCursor result at
// boot and pass it to SetCursor every frame). Any other
// ID falls through to the IM-correct NIL miss path.
//
// The previous Stub allocated a fresh 68-byte zero-filled
// block on every miss and returned a handle to it —
// strictly worse than NIL since callers that defensively
// check `if handle = NIL` got a non-NIL pointer to an
// empty/white cursor and SetCursor'd a blank cursor onto
// the screen. Same fallback issue as the GetIcon ($A9BB)
// 128-byte uninitialised-heap stub closed by the
// family-level resource fallback audit.
//
// Pop = 2 (cursorID INTEGER), result CursHandle at new SP+0.
// 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.
(true, 0x1B9) => {
let sp = cpu.read_reg(Register::A7);
let cursor_id = bus.read_word(sp) as i16;
let handle = if let Some((refnum, ptr)) =
self.find_or_load_resource_any(bus, *b"CURS", cursor_id)
{
self.get_or_create_resource_handle_in_file(
bus, *b"CURS", cursor_id, ptr, refnum,
)
} else if let Some(ptr) = self.synthesize_system_cursor(bus, cursor_id) {
// Built-in cursor synthesised + cached. Use the
// resource-handle helper so subsequent GetCursor
// calls for the same ID return the same handle.
self.get_or_create_resource_handle(bus, *b"CURS", cursor_id, ptr)
} else {
0
};
bus.write_long(sp + 2, handle);
cpu.write_reg(Register::A7, sp + 2);
Ok(())
}
// GetPattern ($A9B8)
// FUNCTION GetPattern(patID: INTEGER): PatHandle;
// Inside Macintosh Volume I, I-473
//
// "GetPattern returns a handle to the pattern having the
// given resource ID, reading it from the resource file if
// necessary. It calls the Resource Manager function
// GetResource('PAT ', patID). If the resource can't be
// read, GetPattern returns NIL." — IM:I I-473.
//
// The previous Stub allocated a fresh 8-byte all-0xFF
// (white) pattern on every miss and returned a handle to
// it — strictly worse than NIL since callers that
// defensively check `if handle = NIL then use_default
// else FillRect(rect, handle^^)` got a non-NIL handle and
// proceeded to FillRect with white instead of taking the
// recovery branch. Same fallback issue as the GetIcon
// ($A9BB) and GetCursor ($A9B9) fallbacks closed in this
// family's audit pass.
//
// Pop = 2 (patID INTEGER), result PatHandle at new SP+0.
// 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.
(true, 0x1B8) => {
let sp = cpu.read_reg(Register::A7);
let pat_id = bus.read_word(sp) as i16;
let handle = if let Some((refnum, ptr)) =
self.find_or_load_resource_any(bus, *b"PAT ", pat_id)
{
self.get_or_create_resource_handle_in_file(bus, *b"PAT ", pat_id, ptr, refnum)
} else {
0
};
bus.write_long(sp + 2, handle);
cpu.write_reg(Register::A7, sp + 2);
Ok(())
}
// GetIcon ($A9BB)
// Returns a handle to the icon stored in the 'ICON' resource
// with the given ID. Equivalent to GetResource('ICON', iconID).
// The resource is a 128-byte black-and-white bitmap (32x32
// pixels at 1 bit each).
// FUNCTION GetIcon(iconID: INTEGER): Handle;
// Inside Macintosh Volume I, I-473
//
// Mirrors GetPicture ($A9BC) exactly: look up the
// resource via the dispatcher's resource search chain;
// on hit, materialise (or reuse) a stable handle that
// points at the loaded resource bytes; on miss, return
// NIL per IM:I I-473 ("If the resource can't be read,
// GetIcon returns NIL").
//
// The previous Stub allocated a fresh 128-byte block of
// UNINITIALISED memory and returned a handle to it on
// every call — strictly worse than NIL since callers
// pass that handle to PlotIcon ($A94B) which CopyBits
// the random bytes onto the framebuffer. Apps with a
// missing 'ICON' that defensively check `if handle =
// NIL` would crash on the dereference path; apps that
// trust the result blindly would render a junk icon.
// The proper Partial impl returns NIL on miss so both
// branches behave correctly.
//
// Pop = 2 (iconID INTEGER), result Handle at new SP+0.
// 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).
(true, 0x1BB) => {
let sp = cpu.read_reg(Register::A7);
let icon_id = bus.read_word(sp) as i16;
let handle = if let Some((refnum, ptr)) =
self.find_or_load_resource_any(bus, *b"ICON", icon_id)
{
let h = self
.get_or_create_resource_handle_in_file(bus, *b"ICON", icon_id, ptr, refnum);
eprintln!(
"[TRAP] GetIcon({}) -> handle=${:08X} ptr=${:08X}",
icon_id, h, ptr
);
h
} else {
eprintln!("[TRAP] GetIcon({}) -> NIL (not found)", icon_id);
0
};
bus.write_long(sp + 2, handle);
cpu.write_reg(Register::A7, sp + 2);
Ok(())
}
// GetPicture ($A9BC)
// Returns a handle to the picture stored in the 'PICT' resource
// with the given ID. Equivalent to GetResource('PICT', picID).
// FUNCTION GetPicture(picID: INTEGER): PicHandle;
// Inside Macintosh Volume I, I-475
// GetPicture ($A9BC): Loads PICT resource via GetResource, returns handle
(true, 0x1BC) => {
let sp = cpu.read_reg(Register::A7);
let pic_id = bus.read_word(sp) as i16;
let handle = if let Some((refnum, ptr)) =
self.find_or_load_resource_any(bus, *b"PICT", pic_id)
{
let h = self
.get_or_create_resource_handle_in_file(bus, *b"PICT", pic_id, ptr, refnum);
eprintln!(
"[TRAP] GetPicture({}) -> handle=${:08X} ptr=${:08X}",
pic_id, h, ptr
);
h
} else {
eprintln!("[TRAP] GetPicture({}) -> NIL (not found)", pic_id);
0
};
bus.write_long(sp + 2, handle);
cpu.write_reg(Register::A7, sp + 2);
Ok(())
}
// GetString ($A9BA)
// Returns a handle to the 'STR ' resource with the given ID.
// FUNCTION GetString (stringID: INTEGER): StringHandle;
// Text 1993, 5-49; Inside Macintosh Volume I, I-468
// GetString ($A9BA): Returns the loaded `'STR '` resource handle or NIL when missing
(true, 0x1BA) => {
let sp = cpu.read_reg(Register::A7);
let string_id = bus.read_word(sp) as i16;
let handle = if let Some((refnum, ptr)) =
self.find_or_load_resource_any(bus, *b"STR ", string_id)
{
self.get_or_create_resource_handle_in_file(
bus, *b"STR ", string_id, ptr, refnum,
)
} else if let Some(ptr) = self.synthesize_system_str(bus, string_id) {
self.get_or_create_resource_handle(bus, *b"STR ", string_id, ptr)
} else {
0
};
bus.write_long(sp + 2, handle);
cpu.write_reg(Register::A7, sp + 2);
Ok(())
}
// ========== TextEdit Manager Stubs ==========
// TENew ($A9D2)
// FUNCTION TENew(destRect, viewRect: Rect): TEHandle;
// Inside Macintosh Volume I (1985), p. I-373..I-374.
// Text 1993, 2-85..2-86.
//
// IM:I I-374: TENew "creates and initializes the necessary
// data structures, allocates an edit record, returns a
// handle to it, and sets that handle's selection range,
// view rectangle, destination rectangle, and other fields."
//
// Fresh TERec state per IM:I I-373:
// destRect, viewRect = caller-supplied
// selStart = selEnd = 0
// teLength = 0
// hText = handle to empty char buffer (non-NIL)
// txFont, txFace, txMode, txSize copied from current grafPort
// inPort = current grafPort
//
// Calling-convention duality. Classic Inside Macintosh
// declares TENew with Pascal by-value Rect parameters
// (16 bytes on the stack). MPW Universal Headers
// (TextEdit.h) modernise it to pointer parameters:
// EXTERN_API(TEHandle) TENew(const Rect *destRect,
// const Rect *viewRect)
// ONEWORDINLINE(0xA9D2);
// BasiliskII System 7.5.3 ROM accepts the pointer-arg
// convention. Systemless's HLE
// sniffs which convention the caller used by inspecting
// whether the first two long words on the stack are valid
// guest pointers and pops either 8 bytes (pointer convention)
// or 16 bytes (by-value convention) accordingly.
//
// Regression coverage (this file):
// tenew_pointer_arg_convention_initializes_destrect_viewrect_and_returns_non_nil_handle
// tenew_fresh_terec_has_zero_telength_and_empty_selection_per_im_i_373
// tenew_function_protocol_consumes_two_pointer_args_and_writes_4_byte_result
//
// 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
(true, 0x1D2) => {
let sp = cpu.read_reg(Register::A7);
let handle = Self::allocate_te_handle(bus);
let (dest_rect, view_rect, stack_pop) = Self::te_new_rect_args(bus, sp);
self.initialize_te_record(bus, handle, dest_rect, view_rect);
bus.write_long(sp + stack_pop, handle);
cpu.write_reg(Register::A7, sp + stack_pop);
Ok(())
}
// TEStyleNew ($A83E)
// Creates a multistyled edit record in the current port.
// FUNCTION TEStyleNew(destRect: Rect; viewRect: Rect): TEHandle;
// Inside Macintosh: Text (1993), p. 2-78.
//
// IM:Text 1993 p. 2-78 verbatim:
// "The TEStyleNew function creates a multistyled edit
// record and allocates a handle to it... TEStyleNew
// sets the txSize, lineHeight, and fontAscent fields
// of the edit record to -1, allocates a style record,
// and stores a handle to the style record in the
// txFont and txFace fields. The TEStyleNew function
// creates and initializes a null scrap that is used
// by TextEdit routines throughout the life of the
// edit record."
//
// MPW Universal Headers (TextEdit.h):
// EXTERN_API(TEHandle)
// TEStyleNew(const Rect *destRect,
// const Rect *viewRect) ONEWORDINLINE(0xA83E);
//
// Calling convention: identical to TENew. Pascal pushes
// left-to-right, so destRect (first arg) lands deepest
// and viewRect (second arg) lands shallowest:
// sp+0..3 viewRect_ptr (last pushed)
// sp+4..7 destRect_ptr (first pushed)
// Both pointer (8-byte) and by-value (16-byte) forms are
// accepted via te_new_rect_args sniffing.
//
// Styled-record signature, per IM:Text 1993 p. 2-78
// (initialize_styled_te_record at dialog.rs:843..):
// txSize = -1 sentinel at offset 0x50
// lineHeight = -1 sentinel at offset 0x18
// fontAscent = -1 sentinel at offset 0x1A
// txFont/txFace (4-byte overlay at offset 0x4A) holds
// the TEStyleHandle.
//
// Regression coverage (this file):
// testylenew_returns_styled_handle_and_initializes_sentinel_fields
// testylenew_pointer_arg_convention_initializes_destrect_viewrect_and_styled_sentinels
// testylenew_function_protocol_consumes_two_pointer_args_and_writes_4_byte_result
//
// 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
(true, 0x03E) => {
let sp = cpu.read_reg(Register::A7);
let handle = Self::allocate_te_handle(bus);
let (dest_rect, view_rect, stack_pop) = Self::te_new_rect_args(bus, sp);
self.initialize_styled_te_record(bus, handle, dest_rect, view_rect);
bus.write_long(sp + stack_pop, handle);
cpu.write_reg(Register::A7, sp + stack_pop);
Ok(())
}
// TEGetOffset ($A83C)
// FUNCTION TEGetOffset(pt: Point; hTE: TEHandle): INTEGER;
// Inside Macintosh Volume V (1986), p. V-172.
//
// IM:V V-172 verbatim: "TEGetOffset returns the character
// position closest to the point pt. The point pt is in
// local coordinates relative to the destination rectangle.
// If pt is above the first line, TEGetOffset returns the
// character offset of the start of the first line. If pt
// is below the last line, TEGetOffset returns the
// character offset of the end of the text."
//
// MPW Universal Headers (TextEdit.h):
// EXTERN_API(short)
// TEGetOffset(Point pt, TEHandle hTE) ONEWORDINLINE(0xA83C);
//
// Calling convention. `EXTERN_API` expands to `extern
// pascal` on the 68k target, so the Pascal LR push order
// applies: pt (the first arg) is pushed FIRST and lands
// DEEPEST on the stack; hTE (the last arg) is pushed LAST
// and lands SHALLOWEST. Point is a 4-byte record with
// pt.v at the lower address and pt.h at the higher
// address. Pascal FUNCTION pre-allocates the 2-byte
// INTEGER result slot just above the args. Stack layout
// at trap entry:
// sp+0..3 hTE (4 bytes, last pushed)
// sp+4..5 pt.v (2 bytes, first half of Point)
// sp+6..7 pt.h (2 bytes, second half of Point)
// sp+8..9 function result slot (2 bytes)
//
// Pre-fix (commit ca6a0ebf — A9D2 te_new_rect_args
// Pascal-LR fix only covered TENew + TEStyleNew sharing
// the te_new_rect_args helper): this arm read te_handle
// from sp+2 and pt.v/pt.h from sp+6/sp+8, off-by-2 versus
// the canonical Pascal LR layout. That off-by-2 read placed
// garbage in te_handle so the te_point_to_char helper bailed
// via the NIL TERec branch and returned 0 instead of the
// expected teLength=5. Fixed by reading args at the canonical
// sp+0, sp+4, sp+6 offsets.
//
// Regression coverage (this file):
// tegetoffset_point_above_destrect_returns_zero
// tegetoffset_point_below_last_line_returns_telength
// tegetoffset_function_protocol_consumes_point_and_tehandle_args_writes_integer_result
//
// 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.
(true, 0x03C) => {
let sp = cpu.read_reg(Register::A7);
let te_handle = bus.read_long(sp);
let point_v = bus.read_word(sp + 4) as i16;
let point_h = bus.read_word(sp + 6) as i16;
let offset = self.te_point_to_char(bus, te_handle, (point_v, point_h));
bus.write_word(sp + 8, offset as u16);
cpu.write_reg(Register::A7, sp + 8);
Ok(())
}
// TEFindWord ($A0FE)
// Register-based TextEdit hook:
// currentPos in D0.W, caller in D2.W, pTE in A3.L, hTE in A4.L.
// wordStart returns in D0.W and wordEnd in D1.W.
// Inside Macintosh: Text (1993), pp. 2-60..2-61.
(false, 0x0FE) => {
let current_pos = (cpu.read_reg(Register::D0) as u16) as usize;
let _caller = cpu.read_reg(Register::D2);
let _p_te = cpu.read_reg(Register::A3);
let h_te = cpu.read_reg(Register::A4);
let (word_start, word_end) = self.te_find_word_bounds(bus, h_te, current_pos);
cpu.write_reg(Register::D0, u32::from(word_start));
cpu.write_reg(Register::D1, u32::from(word_end));
Ok(())
}
// TEDispatch ($A83D)
// Selector-based dispatcher for styled TextEdit routines.
// FUNCTION/PROCEDURE TEDispatch(...); selector is the first stack word.
// Inside Macintosh Volume VI, 15-22 (TEFeatureFlag),
// 15-25..15-43 (selector mapping),
// 15-34 (TEContinuousStyle).
// Inside Macintosh: Text (1993), p. 2-102 (TEContinuousStyle),
// p. 2-92 / 2-97 (autoscroll default).
//
// TEDispatch ($A83D) selectors: $0000 TEStylPaste,
// $0001 TESetStyle, $0002 TEReplaceStyle, $0003 TEGetStyle,
// $0004 GetStylHandle, $0005 SetStylHandle, $0006 GetStylScrap,
// $0007 TEStylInsert, $0008 TEGetPoint, $0009 TEGetHeight,
// $000A TEContinuousStyle, $000B TEUseStyleScrap,
// $000C TECustomHook, $000D TENumStyles, $000E TEFeatureFlag.
//
// MPW Universal Headers TextEdit.h declares each entry point via
// THREEWORDINLINE(0x3F3C, <selector>, 0xA83D). The 0x3F3C is
// `MOVE.W #imm,-(A7)` which pre-pushes the selector word at the
// call site, immediately before the A-line trap. Pascal LR
// calling convention pushes args left-to-right (first arg
// deepest), so for any N-arg TEDispatch selector the stack
// layout at trap entry is:
// sp+0 selector word (2 bytes, last pushed)
// sp+2 arg[N-1] (last Pascal arg, shallowest)
// ...
// sp+(2 + S_0+..+S_{N-2}) arg[0] (first Pascal arg, deepest)
// sp+(2 + Σ S_i) function result slot (for FUNCTION selectors)
//
// Selector $000A TEContinuousStyle is FUNCTION (Boolean result):
// EXTERN_API(Boolean) TEContinuousStyle(short *mode,
// TextStyle *aStyle,
// TEHandle hTE)
// THREEWORDINLINE(0x3F3C, 0x000A, 0xA83D);
// Stack: sp+0 selector, sp+2 hTE (4), sp+6 aStyle* (4),
// sp+10 mode* (4), sp+14 Boolean result slot.
// Per IM:Text 1993 p. 2-102: returns TRUE for unstyled edit
// records and reports the global style attributes for the
// mode bits requested by *mode (font=1, face=2, size=4,
// color=8).
//
// Selector $000E TEFeatureFlag is FUNCTION (short result):
// EXTERN_API(short) TEFeatureFlag(short feature, short action,
// TEHandle hTE)
// THREEWORDINLINE(0x3F3C, 0x000E, 0xA83D);
// Stack: sp+0 selector, sp+2 hTE (4), sp+6 action (2),
// sp+8 feature (2), sp+10 short result slot.
// Per IM:VI 15-22: turns features on/off or tests them;
// returns the PRIOR setting of the bit (which, for
// teBitTest=-1, coincides with the current setting since
// the bit is not mutated). Action codes are teBitClear=0,
// teBitSet=1, teBitTest=-1.
//
// Contract test coverage (this module):
// te_dispatch_feature_flag_test_action_returns_current_state
// te_dispatch_feature_flag_tracks_auto_scroll_state
// (teBitSet on default-off feature returns prior 0 and mutates the bit)
// tefeatureflag_clear_action_returns_prior_one_state_and_clears_bit
// (teBitClear on previously-set feature returns prior 1 and clears the bit)
// te_dispatch_continuous_style_returns_unstyled_record_style
// tedispatch_function_protocol_consumes_threewordinline_stack_frame_for_tefeatureflag
// teautoview_and_tefeatureflag_observe_shared_autoscroll_state
(true, 0x03D) => {
let sp = cpu.read_reg(Register::A7);
let selector = bus.read_word(sp);
if trace_textedit_enabled() {
eprintln!("[TE] TEDispatch selector=${:04X} sp=${:08X}", selector, sp);
}
match selector {
0x0000 => {
// TEStylePaste ($A83D, selector $0000)
// PROCEDURE TEStylePaste(hTE: TEHandle);
cpu.write_reg(Register::A7, sp + 6);
}
0x0001 => {
// TESetStyle ($A83D, selector $0001)
// Sets the current selection's style in a styled edit record.
// PROCEDURE TESetStyle(mode: INTEGER; newStyle: TextStyle; redraw: BOOLEAN; hTE: TEHandle);
// Inside Macintosh Volume VI, 15-32
let te_handle = bus.read_long(sp + 2);
// Pascal BOOLEAN in high byte (MPW C convention).
let redraw = bus.read_byte(sp + 6) != 0;
let style_ptr = bus.read_long(sp + 8);
let mode = bus.read_word(sp + 12);
if trace_textedit_enabled() {
eprintln!(
"[TE] TESetStyle hTE=${:08X} mode=${:04X} redraw={} style_ptr=${:08X}",
te_handle, mode, redraw, style_ptr
);
if style_ptr != 0 {
eprintln!(
"[TE] TESetStyle values font={} face=${:04X} size={} color=(${:04X},${:04X},${:04X})",
bus.read_word(style_ptr) as i16,
bus.read_word(style_ptr + 2),
bus.read_word(style_ptr + 4) as i16,
bus.read_word(style_ptr + 6),
bus.read_word(style_ptr + 8),
bus.read_word(style_ptr + 10),
);
}
}
if style_ptr != 0 {
let te_ptr = Self::te_record_ptr(bus, te_handle);
if te_ptr != 0 {
let style_handle = Self::te_style_handle(bus, te_handle);
if style_handle != 0 {
let style_ptr_record = bus.read_long(style_handle);
if style_ptr_record != 0 {
let table_handle = bus.read_long(
style_ptr_record + Self::TE_STYLE_STYLE_TABLE_OFFSET,
);
let table_ptr = if table_handle != 0 {
bus.read_long(table_handle)
} else {
0
};
if table_ptr != 0 {
if (mode & 0x0001) != 0 {
bus.write_word(
table_ptr + Self::ST_ELEMENT_FONT_OFFSET,
bus.read_word(style_ptr),
);
}
if (mode & 0x0002) != 0 {
bus.write_word(
table_ptr + Self::ST_ELEMENT_FACE_OFFSET,
bus.read_word(style_ptr + 2),
);
}
if (mode & 0x0004) != 0 {
bus.write_word(
table_ptr + Self::ST_ELEMENT_SIZE_OFFSET,
bus.read_word(style_ptr + 4),
);
}
if (mode & 0x0008) != 0 {
bus.write_word(
table_ptr + Self::ST_ELEMENT_COLOR_OFFSET,
bus.read_word(style_ptr + 6),
);
bus.write_word(
table_ptr + Self::ST_ELEMENT_COLOR_OFFSET + 2,
bus.read_word(style_ptr + 8),
);
bus.write_word(
table_ptr + Self::ST_ELEMENT_COLOR_OFFSET + 4,
bus.read_word(style_ptr + 10),
);
}
let resolved_font = bus
.read_word(table_ptr + Self::ST_ELEMENT_FONT_OFFSET)
as i16;
let resolved_size = bus
.read_word(table_ptr + Self::ST_ELEMENT_SIZE_OFFSET)
as i16;
let metrics = get_font_metrics(
resolved_font,
Self::font_lookup_size(resolved_size),
);
let line_height =
metrics.ascent + metrics.descent + metrics.leading;
bus.write_word(
table_ptr + Self::ST_ELEMENT_HEIGHT_OFFSET,
line_height as u16,
);
bus.write_word(
table_ptr + Self::ST_ELEMENT_ASCENT_OFFSET,
metrics.ascent as u16,
);
let lh_handle = bus.read_long(
style_ptr_record + Self::TE_STYLE_LH_TABLE_OFFSET,
);
let lh_ptr = if lh_handle != 0 {
bus.read_long(lh_handle)
} else {
0
};
if lh_ptr != 0 {
bus.write_word(
lh_ptr + Self::LH_ELEMENT_HEIGHT_OFFSET,
line_height as u16,
);
bus.write_word(
lh_ptr + Self::LH_ELEMENT_ASCENT_OFFSET,
metrics.ascent as u16,
);
}
}
}
} else {
if (mode & 0x0001) != 0 {
bus.write_word(
te_ptr + Self::TE_TX_FONT_OFFSET,
bus.read_word(style_ptr),
);
}
if (mode & 0x0002) != 0 {
bus.write_word(
te_ptr + Self::TE_TX_FACE_OFFSET,
bus.read_word(style_ptr + 2),
);
}
if (mode & 0x0004) != 0 {
bus.write_word(
te_ptr + Self::TE_TX_SIZE_OFFSET,
bus.read_word(style_ptr + 4),
);
}
let resolved_font =
bus.read_word(te_ptr + Self::TE_TX_FONT_OFFSET) as i16;
let resolved_size =
bus.read_word(te_ptr + Self::TE_TX_SIZE_OFFSET) as i16;
let metrics = get_font_metrics(
resolved_font,
Self::font_lookup_size(resolved_size),
);
bus.write_word(
te_ptr + Self::TE_LINE_HEIGHT_OFFSET,
(metrics.ascent + metrics.descent + metrics.leading) as u16,
);
bus.write_word(
te_ptr + Self::TE_FONT_ASCENT_OFFSET,
metrics.ascent as u16,
);
}
}
}
let _ = redraw;
cpu.write_reg(Register::A7, sp + 14);
}
0x0002 => {
// TEReplaceStyle ($A83D, selector $0002)
// PROCEDURE TEReplaceStyle(mode: INTEGER;
// oldStyle, newStyle: TextStyle;
// redraw: BOOLEAN; hTE: TEHandle);
// Inside Macintosh Volume V, V-271..V-272.
// MPW C glue passes both TextStyle records by
// pointer (not by value), giving an 18-byte arg
// frame: selector(2) + hTE(4) + redraw(2) +
// newStyle ptr(4) + oldStyle ptr(4) + mode(2).
let te_handle = bus.read_long(sp + 2);
let _redraw = bus.read_byte(sp + 6) != 0;
let new_style_ptr = bus.read_long(sp + 8);
let old_style_ptr = bus.read_long(sp + 12);
let mode = bus.read_word(sp + 16);
if old_style_ptr != 0 && new_style_ptr != 0 {
let style_handle = Self::te_style_handle(bus, te_handle);
if style_handle != 0 {
let style_ptr_record = bus.read_long(style_handle);
if style_ptr_record != 0 {
let table_handle = bus.read_long(
style_ptr_record + Self::TE_STYLE_STYLE_TABLE_OFFSET,
);
let table_ptr = if table_handle != 0 {
bus.read_long(table_handle)
} else {
0
};
if table_ptr != 0 {
// Per IM:V V-270, replace only when the
// existing style's selected attributes
// match oldStyle exactly. With Systemless's
// single-element style table this collapses
// to one comparison.
let mut matches_old = true;
if (mode & 0x0001) != 0
&& bus
.read_word(table_ptr + Self::ST_ELEMENT_FONT_OFFSET)
!= bus.read_word(old_style_ptr)
{
matches_old = false;
}
if (mode & 0x0002) != 0
&& bus
.read_word(table_ptr + Self::ST_ELEMENT_FACE_OFFSET)
!= bus.read_word(old_style_ptr + 2)
{
matches_old = false;
}
if (mode & 0x0004) != 0
&& bus
.read_word(table_ptr + Self::ST_ELEMENT_SIZE_OFFSET)
!= bus.read_word(old_style_ptr + 4)
{
matches_old = false;
}
if (mode & 0x0008) != 0
&& (bus.read_word(
table_ptr + Self::ST_ELEMENT_COLOR_OFFSET,
) != bus.read_word(old_style_ptr + 6)
|| bus.read_word(
table_ptr + Self::ST_ELEMENT_COLOR_OFFSET + 2,
) != bus.read_word(old_style_ptr + 8)
|| bus.read_word(
table_ptr + Self::ST_ELEMENT_COLOR_OFFSET + 4,
) != bus.read_word(old_style_ptr + 10))
{
matches_old = false;
}
if matches_old {
if (mode & 0x0001) != 0 {
bus.write_word(
table_ptr + Self::ST_ELEMENT_FONT_OFFSET,
bus.read_word(new_style_ptr),
);
}
if (mode & 0x0002) != 0 {
bus.write_word(
table_ptr + Self::ST_ELEMENT_FACE_OFFSET,
bus.read_word(new_style_ptr + 2),
);
}
if (mode & 0x0004) != 0 {
bus.write_word(
table_ptr + Self::ST_ELEMENT_SIZE_OFFSET,
bus.read_word(new_style_ptr + 4),
);
}
if (mode & 0x0008) != 0 {
bus.write_word(
table_ptr + Self::ST_ELEMENT_COLOR_OFFSET,
bus.read_word(new_style_ptr + 6),
);
bus.write_word(
table_ptr + Self::ST_ELEMENT_COLOR_OFFSET + 2,
bus.read_word(new_style_ptr + 8),
);
bus.write_word(
table_ptr + Self::ST_ELEMENT_COLOR_OFFSET + 4,
bus.read_word(new_style_ptr + 10),
);
}
}
}
}
}
}
let _ = mode;
cpu.write_reg(Register::A7, sp + 18);
}
0x0003 => {
// TEGetStyle ($A83D, selector $0003)
// PROCEDURE TEGetStyle(sel: INTEGER; VAR attrs: TextStyle;
// VAR lineHeight: INTEGER; VAR fontAscent: INTEGER; hTE: TEHandle);
let te_handle = bus.read_long(sp + 2);
let font_ascent_ptr = bus.read_long(sp + 6);
let line_height_ptr = bus.read_long(sp + 10);
let attrs_ptr = bus.read_long(sp + 14);
let _sel = bus.read_word(sp + 18) as i16;
let (font, face, size, color, line_height, font_ascent) =
self.te_primary_style(bus, te_handle);
if attrs_ptr != 0 {
bus.write_word(attrs_ptr, font as u16);
bus.write_word(attrs_ptr + 2, face as u16);
bus.write_word(attrs_ptr + 4, size as u16);
bus.write_word(attrs_ptr + 6, color.0);
bus.write_word(attrs_ptr + 8, color.1);
bus.write_word(attrs_ptr + 10, color.2);
}
if line_height_ptr != 0 {
bus.write_word(line_height_ptr, line_height as u16);
}
if font_ascent_ptr != 0 {
bus.write_word(font_ascent_ptr, font_ascent as u16);
}
cpu.write_reg(Register::A7, sp + 20);
}
0x0004 => {
// TEGetStyleHandle ($A83D, selector $0004)
// FUNCTION TEGetStyleHandle(hTE: TEHandle): TEStyleHandle;
let te_handle = bus.read_long(sp + 2);
bus.write_long(sp + 6, Self::te_style_handle(bus, te_handle));
cpu.write_reg(Register::A7, sp + 6);
}
0x0005 => {
// TESetStyleHandle ($A83D, selector $0005)
// PROCEDURE TESetStyleHandle(theHandle: TEStyleHandle; hTE: TEHandle);
let te_handle = bus.read_long(sp + 2);
let style_handle = bus.read_long(sp + 6);
Self::te_write_style_handle(bus, te_handle, style_handle);
cpu.write_reg(Register::A7, sp + 10);
}
0x0006 => {
// TEGetStyleScrapHandle ($A83D, selector $0006)
// FUNCTION TEGetStyleScrapHandle(hTE: TEHandle): STScrpHandle;
let te_handle = bus.read_long(sp + 2);
let style_handle = Self::te_style_handle(bus, te_handle);
let mut result = 0;
if style_handle != 0 {
let style_ptr = bus.read_long(style_handle);
if style_ptr != 0 {
let null_style_handle =
bus.read_long(style_ptr + Self::TE_STYLE_NULL_STYLE_OFFSET);
if null_style_handle != 0 {
let null_style_ptr = bus.read_long(null_style_handle);
if null_style_ptr != 0 {
let scrap_handle = bus.read_long(
null_style_ptr + Self::NULL_STYLE_SCRAP_OFFSET,
);
result = Self::duplicate_handle_data(bus, scrap_handle);
}
}
}
}
bus.write_long(sp + 6, result);
cpu.write_reg(Register::A7, sp + 6);
}
0x0007 => {
// TEStyleInsert ($A83D, selector $0007)
// PROCEDURE TEStyleInsert(text: Ptr; length: LONGINT; hST: StScrpHandle; hTE: TEHandle);
let te_handle = bus.read_long(sp + 2);
let style_scrap = bus.read_long(sp + 6);
let length = bus.read_long(sp + 10) as usize;
let text_ptr = bus.read_long(sp + 14);
if text_ptr != 0 && length != 0 {
let text = bus.read_bytes(text_ptr, length);
let insert_start = {
let te_ptr = Self::te_record_ptr(bus, te_handle);
let text_len = Self::te_text_length(bus, te_handle);
if te_ptr != 0 {
let mut sel_start =
bus.read_word(te_ptr + Self::TE_SEL_START_OFFSET) as usize;
let mut sel_end =
bus.read_word(te_ptr + Self::TE_SEL_END_OFFSET) as usize;
sel_start = sel_start.min(text_len);
sel_end = sel_end.min(text_len);
if sel_end < sel_start {
std::mem::swap(&mut sel_start, &mut sel_end);
}
sel_start
} else {
0
}
};
let preview_len = text.len().min(64);
if trace_textedit_enabled() {
eprintln!(
"[TE] TEStyleInsert hTE=${:08X} hST=${:08X} len={} text_ptr=${:08X} preview={:?}",
te_handle,
style_scrap,
length,
text_ptr,
String::from_utf8_lossy(&text[..preview_len])
);
}
self.te_insert_text(bus, te_handle, &text);
if self.te_apply_style_scrap_to_range(
bus,
te_handle,
style_scrap,
insert_start,
text.len(),
) {
self.te_recalculate_layout(bus, te_handle);
}
self.draw_te_contents(cpu, bus, te_handle);
}
cpu.write_reg(Register::A7, sp + 18);
}
0x0008 => {
// TEGetPoint ($A83D, selector $0008)
// Returns the point for a character offset within the edit record.
// FUNCTION TEGetPoint(offset: INTEGER; hTE: TEHandle): Point;
// Inside Macintosh Volume VI, 15-31
let te_handle = bus.read_long(sp + 2);
let offset = bus.read_word(sp + 6) as i16;
let te_ptr = Self::te_record_ptr(bus, te_handle);
let result_addr = sp + 8;
if te_ptr != 0 {
let text_len = Self::te_text_length(bus, te_handle);
let clamped = i32::from(offset).clamp(0, text_len as i32) as usize;
let line_index = Self::te_char_to_line_index(bus, te_handle, clamped);
let line_start = Self::te_line_starts(bus, te_handle)
.get(line_index)
.copied()
.unwrap_or(0);
let (top, x) = self.te_char_to_point(bus, te_handle, clamped);
let y = top.saturating_add(Self::te_ascent_for_line(
bus, te_handle, line_index,
));
if trace_textedit_enabled() {
let starts = Self::te_line_starts(bus, te_handle);
let pc = cpu.read_reg(Register::PC);
eprintln!(
"[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}]",
te_handle,
clamped,
line_index,
line_start,
clamped,
y,
x,
pc,
bus.read_word(pc),
bus.read_word(pc + 2),
bus.read_word(pc + 4),
bus.read_word(pc + 6),
bus.read_word(pc + 8),
bus.read_word(pc + 10),
bus.read_word(pc + 12),
bus.read_word(pc + 14),
bus.read_word(pc + 16),
bus.read_word(pc + 18),
bus.read_word(pc + 20),
bus.read_word(pc + 22),
bus.read_word(pc + 24),
bus.read_word(pc + 26),
bus.read_word(pc + 28),
bus.read_word(pc + 30),
bus.read_word(pc + 32),
bus.read_word(pc + 34),
bus.read_word(pc + 36),
bus.read_word(pc + 38)
);
eprintln!(
"[TE] TEGetPoint layout nLines={} teLength={} lineStarts={:?}",
starts.len().saturating_sub(1),
text_len,
starts
);
eprintln!(
"[TE] TEGetPoint helper@35614=[{:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X}]",
bus.read_word(0x0003_5614),
bus.read_word(0x0003_5616),
bus.read_word(0x0003_5618),
bus.read_word(0x0003_561A),
bus.read_word(0x0003_561C),
bus.read_word(0x0003_561E),
bus.read_word(0x0003_5620),
bus.read_word(0x0003_5622),
bus.read_word(0x0003_5624),
bus.read_word(0x0003_5626),
bus.read_word(0x0003_5628),
bus.read_word(0x0003_562A)
);
let table_base = cpu.read_reg(Register::A5).wrapping_sub(0x37B4);
let rect_table = bus.read_long(table_base);
let rect_ptr = rect_table.wrapping_add(21 * 8);
let a6 = cpu.read_reg(Register::A6);
let ret = bus.read_long(a6 + 4);
eprintln!(
"[TE] TEGetPoint rect21 table=${:08X} rect_ptr=${:08X} rect=({},{},{},{}) a4=${:08X} d5={} d6={} d7={} a6=${:08X} ret=${:08X}",
rect_table,
rect_ptr,
bus.read_word(rect_ptr) as i16,
bus.read_word(rect_ptr + 2) as i16,
bus.read_word(rect_ptr + 4) as i16,
bus.read_word(rect_ptr + 6) as i16,
cpu.read_reg(Register::A4),
cpu.read_reg(Register::D5) as i32,
cpu.read_reg(Register::D6) as i32,
cpu.read_reg(Register::D7) as i32,
a6,
ret
);
eprintln!(
"[TE] TEGetPoint caller@{:08X}=[{:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X}]",
ret,
bus.read_word(ret),
bus.read_word(ret + 2),
bus.read_word(ret + 4),
bus.read_word(ret + 6),
bus.read_word(ret + 8),
bus.read_word(ret + 10),
bus.read_word(ret + 12),
bus.read_word(ret + 14),
bus.read_word(ret + 16),
bus.read_word(ret + 18),
bus.read_word(ret + 20),
bus.read_word(ret + 22)
);
let caller_start = ret.wrapping_sub(0x20);
eprintln!(
"[TE] TEGetPoint caller_pre@{:08X}=[{:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X}]",
caller_start,
bus.read_word(caller_start),
bus.read_word(caller_start + 2),
bus.read_word(caller_start + 4),
bus.read_word(caller_start + 6),
bus.read_word(caller_start + 8),
bus.read_word(caller_start + 10),
bus.read_word(caller_start + 12),
bus.read_word(caller_start + 14),
bus.read_word(caller_start + 16),
bus.read_word(caller_start + 18),
bus.read_word(caller_start + 20),
bus.read_word(caller_start + 22),
bus.read_word(caller_start + 24),
bus.read_word(caller_start + 26),
bus.read_word(caller_start + 28),
bus.read_word(caller_start + 30)
);
eprintln!(
"[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}]",
bus.read_word(0x0003_5F7C),
bus.read_word(0x0003_5F7E),
bus.read_word(0x0003_5F80),
bus.read_word(0x0003_5F82),
bus.read_word(0x0003_5F84),
bus.read_word(0x0003_5F86),
bus.read_word(0x0003_5F88),
bus.read_word(0x0003_5F8A),
bus.read_word(0x0003_5F8C),
bus.read_word(0x0003_5F8E),
bus.read_word(0x0003_5F90),
bus.read_word(0x0003_5F92),
bus.read_word(0x0003_5F94),
bus.read_word(0x0003_5F96),
bus.read_word(0x0003_5F98),
bus.read_word(0x0003_5F9A),
bus.read_word(0x0003_5F9C),
bus.read_word(0x0003_5F9E),
bus.read_word(0x0003_5FA0),
bus.read_word(0x0003_5FA2),
bus.read_word(0x0003_5FA4),
bus.read_word(0x0003_5FA6),
bus.read_word(0x0003_5FA8),
bus.read_word(0x0003_5FAA),
bus.read_word(0x0003_5FAC),
bus.read_word(0x0003_5FAE),
bus.read_word(0x0003_5FB0),
bus.read_word(0x0003_5FB2),
bus.read_word(0x0003_5FB4),
bus.read_word(0x0003_5FB6),
bus.read_word(0x0003_5FB8),
bus.read_word(0x0003_5FBA)
);
eprintln!(
"[TE] TEGetPoint branch_c6@00035FC6=[{:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X}] branch_e2@00035FE2=[{:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X}]",
bus.read_word(0x0003_5FC6),
bus.read_word(0x0003_5FC8),
bus.read_word(0x0003_5FCA),
bus.read_word(0x0003_5FCC),
bus.read_word(0x0003_5FCE),
bus.read_word(0x0003_5FD0),
bus.read_word(0x0003_5FD2),
bus.read_word(0x0003_5FD4),
bus.read_word(0x0003_5FE2),
bus.read_word(0x0003_5FE4),
bus.read_word(0x0003_5FE6),
bus.read_word(0x0003_5FE8),
bus.read_word(0x0003_5FEA),
bus.read_word(0x0003_5FEC),
bus.read_word(0x0003_5FEE),
bus.read_word(0x0003_5FF0)
);
eprintln!(
"[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}]",
bus.read_word(0x0003_6B9A),
bus.read_word(0x0003_6B9C),
bus.read_word(0x0003_6B9E),
bus.read_word(0x0003_6BA0),
bus.read_word(0x0003_6BA2),
bus.read_word(0x0003_6BA4),
bus.read_word(0x0003_6BA6),
bus.read_word(0x0003_6BA8),
bus.read_word(0x0003_6BAA),
bus.read_word(0x0003_6BAC),
bus.read_word(0x0003_6BAE),
bus.read_word(0x0003_6BB0),
bus.read_word(0x0003_6BB2),
bus.read_word(0x0003_6BB4),
bus.read_word(0x0003_6BB6),
bus.read_word(0x0003_6BB8),
bus.read_word(0x0003_6BBA),
bus.read_word(0x0003_6BBC),
bus.read_word(0x0003_6BBE),
bus.read_word(0x0003_6BC0),
bus.read_word(0x0003_6BC2),
bus.read_word(0x0003_6BC4),
bus.read_word(0x0003_6BC6),
bus.read_word(0x0003_6BC8)
);
}
bus.write_word(result_addr, y as u16);
bus.write_word(result_addr + 2, x as u16);
} else {
bus.write_word(result_addr, 0);
bus.write_word(result_addr + 2, 0);
}
cpu.write_reg(Register::A7, result_addr);
}
0x0009 => {
// TEGetHeight ($A83D, selector $0009)
// Returns the total height of the requested line range.
// FUNCTION TEGetHeight(endLine: LONGINT; startLine: LONGINT; hTE: TEHandle): LONGINT;
// Text 1993, 2-90
let te_handle = bus.read_long(sp + 2);
let mut start_line = bus.read_long(sp + 6) as i32;
let mut end_line = bus.read_long(sp + 10) as i32;
let te_ptr = Self::te_record_ptr(bus, te_handle);
let n_lines = if te_ptr != 0 {
bus.read_word(te_ptr + Self::TE_N_LINES_OFFSET) as i32
} else {
0
};
if start_line > 0 {
start_line -= 1;
} else {
start_line = 0;
}
end_line = end_line.min(n_lines);
if end_line < 0 {
end_line = 0;
} else if end_line > 0 {
end_line -= 1;
}
if start_line > end_line {
std::mem::swap(&mut start_line, &mut end_line);
}
let text_bytes = Self::te_text_bytes(bus, te_handle);
let line_starts = Self::te_line_starts(bus, te_handle);
if !text_bytes.is_empty() {
while end_line >= start_line {
let current = end_line as usize;
let Some(&line_start) = line_starts.get(current) else {
break;
};
let line_end = line_starts
.get(current + 1)
.copied()
.unwrap_or(text_bytes.len());
let blank_trailing_line = current + 1 == line_starts.len() - 1
&& line_start < line_end
&& text_bytes[line_start..line_end]
.iter()
.all(|&b| matches!(b, b'\r' | b'\n'));
if blank_trailing_line {
end_line -= 1;
} else {
break;
}
}
}
let mut total_height = 0i32;
if end_line >= start_line {
for current_line in
start_line.max(0) as usize..=end_line.max(0) as usize
{
total_height += i32::from(Self::te_height_for_line(
bus,
te_handle,
current_line,
));
}
}
if trace_textedit_enabled() {
eprintln!(
"[TE] TEGetHeight hTE=${:08X} start_line={} end_line={} result={}",
te_handle, start_line, end_line, total_height
);
}
bus.write_long(sp + 14, total_height as u32);
cpu.write_reg(Register::A7, sp + 14);
}
0x000A => {
// TEContinuousStyle ($A83D, selector $000A)
// Returns the common style across the current selection.
// FUNCTION TEContinuousStyle(VAR mode: INTEGER; VAR aStyle: TextStyle; hTE: TEHandle): BOOLEAN;
// Inside Macintosh Volume VI, 15-34 to 15-35
let te_handle = bus.read_long(sp + 2);
let style_ptr = bus.read_long(sp + 6);
let mode_ptr = bus.read_long(sp + 10);
let result_addr = sp + 14;
let requested_mode = if mode_ptr != 0 {
bus.read_word(mode_ptr)
} else {
0
};
if style_ptr != 0 {
let (tx_font, tx_face, tx_size, color, _, _) =
self.te_primary_style(bus, te_handle);
if (requested_mode & 0x0001) != 0 {
bus.write_word(style_ptr, tx_font as u16);
}
if (requested_mode & 0x0002) != 0 {
bus.write_word(style_ptr + 2, tx_face as u16);
}
if (requested_mode & 0x0004) != 0 {
bus.write_word(style_ptr + 4, tx_size as u16);
}
if (requested_mode & 0x0008) != 0 {
bus.write_word(style_ptr + 6, color.0);
bus.write_word(style_ptr + 8, color.1);
bus.write_word(style_ptr + 10, color.2);
}
}
bus.write_word(result_addr, 0xFFFF);
cpu.write_reg(Register::A7, result_addr);
}
0x000B => {
// TEUseStyleScrap ($A83D, selector $000B)
// Sets style data for the specified text range from a style scrap handle.
// PROCEDURE TEUseStyleScrap(rangeStart: LONGINT; rangeEnd: LONGINT;
// newStyles: StScrpHandle; redraw: BOOLEAN; hTE: TEHandle);
// Inside Macintosh Volume VI, 15-35 to 15-36
let te_handle = bus.read_long(sp + 2);
// Pascal BOOLEAN in high byte (MPW C convention).
let redraw = bus.read_byte(sp + 6) != 0;
let new_styles = bus.read_long(sp + 8);
let range_end = bus.read_long(sp + 12) as usize;
let range_start = bus.read_long(sp + 16) as usize;
let (range_start, range_end) = if range_end < range_start {
(range_end, range_start)
} else {
(range_start, range_end)
};
if self.te_apply_style_scrap_to_range(
bus,
te_handle,
new_styles,
range_start,
range_end.saturating_sub(range_start),
) {
self.te_recalculate_layout(bus, te_handle);
if redraw {
self.draw_te_contents(cpu, bus, te_handle);
}
}
cpu.write_reg(Register::A7, sp + 20);
}
0x000C => {
// TECustomHook ($A83D, selector $000C)
// Reads or replaces one of TextEdit's internal hook procedures.
// PROCEDURE TECustomHook(which: TEIntHook; VAR addr: ProcPtr; hTE: TEHandle);
// Inside Macintosh Volume VI, 15-25 to 15-26.
// Per IM:VI 15-26, addr is a VAR ProcPtr — on
// return it must hold the previous hook. Systemless
// does not invoke registered hooks (no guest-fn
// dispatch infrastructure), so we report "no
// previous hook" by writing 0 into *addr.
let addr_ptr = bus.read_long(sp + 6);
if addr_ptr != 0 {
bus.write_long(addr_ptr, 0);
}
cpu.write_reg(Register::A7, sp + 12);
}
0x000D => {
// TENumStyles ($A83D, selector $000D)
// Returns the number of style changes contained in the
// given range, counting one for the start of the range.
// FUNCTION TENumStyles(rangeStart: LONGINT; rangeEnd: LONGINT;
// hTE: TEHandle): LONGINT;
// Inside Macintosh Volume VI, 15-36.
// Per IM:VI 15-36, an unstyled record always
// returns 1. With Systemless's single-run styled
// record the in-range transitions count is also
// zero, so the +1 for the start-of-range gives 1.
let te_handle = bus.read_long(sp + 2);
let style_handle = Self::te_style_handle(bus, te_handle);
let mut count: u32 = 1;
if style_handle != 0 {
let style_ptr_record = bus.read_long(style_handle);
if style_ptr_record != 0 {
let n_runs = bus
.read_word(style_ptr_record + Self::TE_STYLE_N_RUNS_OFFSET)
as u32;
count = n_runs.max(1);
}
}
bus.write_long(sp + 14, count);
cpu.write_reg(Register::A7, sp + 14);
}
0x000E => {
// TEFeatureFlag ($A83D, selector $000E)
// Tests or changes a TextEdit feature bit and returns the previous state.
// FUNCTION TEFeatureFlag(feature: INTEGER; action: INTEGER; hTE: TEHandle): INTEGER;
// Inside Macintosh Volume VI, 15-22 to 15-24
let te_handle = bus.read_long(sp + 2);
let action = bus.read_word(sp + 6) as i16;
let feature = bus.read_word(sp + 8);
let was_set = self.te_feature_bit(te_handle, feature);
if matches!(
feature,
Self::TE_FEATURE_AUTO_SCROLL
| Self::TE_FEATURE_TEXT_BUFFERING
| Self::TE_FEATURE_OUTLINE_HILITE
| Self::TE_FEATURE_INLINE_INPUT
| Self::TE_FEATURE_USE_TEXT_SERVICES
) {
match action {
Self::TE_BIT_CLEAR => {
self.set_te_feature_bit(te_handle, feature, false)
}
Self::TE_BIT_SET => {
self.set_te_feature_bit(te_handle, feature, true)
}
Self::TE_BIT_TEST => {}
_ => {}
}
}
bus.write_word(sp + 10, if was_set { 1 } else { 0 });
cpu.write_reg(Register::A7, sp + 10);
}
_ => return None,
}
Ok(())
}
// TEDispose ($A9CD)
// Disposes of the edit record and releases memory used by
// the text and record structures.
// PROCEDURE TEDispose(hTE: TEHandle);
// Inside Macintosh Volume I, I-383 to I-384; Text 1993, 2-79
//
// Regression coverage:
// dialog::tests::tedispose_releases_te_record_text_handle_and_pops_arg
// TEDispose ($A9CD): Frees TERec, hText, and style handles per IM:I I-383..I-384
(true, 0x1CD) => {
let sp = cpu.read_reg(Register::A7);
let te_handle = bus.read_long(sp);
if te_handle != 0 {
let te_ptr = bus.read_long(te_handle);
if te_ptr != 0 {
// Free the text handle and its data
let h_text = bus.read_long(te_ptr + Self::TE_HTEXT_OFFSET);
if h_text != 0 {
let text_ptr = bus.read_long(h_text);
if text_ptr != 0 {
bus.free(text_ptr);
}
bus.free(h_text);
}
// For styled records, free the style handle
if Self::te_is_styled_record(bus, te_ptr) {
let style_handle = bus.read_long(te_ptr + Self::TE_TX_FONT_OFFSET);
if style_handle != 0 {
let style_ptr = bus.read_long(style_handle);
if style_ptr != 0 {
bus.free(style_ptr);
}
bus.free(style_handle);
}
}
bus.free(te_ptr);
}
bus.free(te_handle);
self.textedit_states.remove(&te_handle);
}
cpu.write_reg(Register::A7, sp + 4);
Ok(())
}
// TESetText ($A9CF)
// PROCEDURE TESetText(text: Ptr; length: LONGINT; hTE: TEHandle);
// Inside Macintosh Volume I (1985), p. I-378.
//
// IM:I I-378 verbatim: "TESetText sets the current text
// contents of the edit record specified by hTE. The text
// parameter points to the text, and the length parameter
// contains the number of bytes."
//
// MPW Universal Headers `TextEdit.h` declares:
// EXTERN_API(void) TESetText(const void *text, long length,
// TEHandle hTE) ONEWORDINLINE(0xA9CF);
//
// Pascal calling convention pushes args left-to-right (first
// arg deepest). At trap entry the stack layout is therefore:
// sp+0 TEHandle hTE (last pushed, shallowest)
// sp+4 LONGINT length (middle, 4 bytes)
// sp+8 Ptr text (first pushed, deepest)
// The trap pops 12 bytes (no result slot — PROCEDURE).
//
// Empty-input semantics: per IM:I I-378 passing zero length
// yields an empty edit record. Systemless additionally defends
// against NIL text pointer (clears regardless of length);
// the real Mac ROM (BasiliskII System 7.5.3) does NOT safely
// handle NIL source pointers.
//
// Contract tests in this file:
// tesettext_copies_bytes_updates_length_and_pops_arguments
// tesettext_nil_or_zero_length_input_clears_text
// tesettext_replaces_prior_contents_via_sequential_call
// tesettext_balances_stackspace_with_pascal_protocol
(true, 0x1CF) => {
let sp = cpu.read_reg(Register::A7);
let te_handle = bus.read_long(sp);
let length = bus.read_long(sp + 4) as usize;
let text_ptr = bus.read_long(sp + 8);
if text_ptr != 0 && length != 0 {
let text = bus.read_bytes(text_ptr, length);
self.te_set_text_contents(bus, te_handle, &text);
} else {
self.te_set_text_contents(bus, te_handle, &[]);
}
cpu.write_reg(Register::A7, sp + 12);
Ok(())
}
// TEGetText ($A9CB)
// FUNCTION TEGetText(hTE: TEHandle): CharsHandle;
// Inside Macintosh Volume I (1985), p. I-384.
//
// IM:I I-384 verbatim: "TEGetText returns a handle to the text
// of the specified edit record. The result is the same as the
// handle in the hText field of the edit record, but has the
// CharsHandle data type, which is defined as:
//
// TYPE CharsHandle = ^CharsPtr;
// CharsPtr = ^Chars;
// Chars = PACKED ARRAY[0..32000] OF CHAR;
//
// You can get the length of the text from the teLength field
// of the edit record."
//
// Pascal FUNCTION calling convention: the caller pre-allocates
// a 4-byte CharsHandle result slot at SP+4, pushes a 4-byte
// TEHandle argument at SP+0. The trap reads the TEHandle,
// looks up TERec.hText (offset TE_HTEXT_OFFSET = +18 per
// IM:I I-379), writes that handle to the result slot, and
// pops the 4-byte argument. The C wrapper then pops the
// 4-byte result slot — net externally-observed SP delta is
// zero.
//
// MPW Universal Headers `TextEdit.h` declares:
// EXTERN_API(CharsHandle) TEGetText(TEHandle hTE)
// ONEWORDINLINE(0xA9CB);
(true, 0x1CB) => {
let sp = cpu.read_reg(Register::A7);
let te_handle = bus.read_long(sp);
bus.write_long(sp + 4, Self::te_text_handle(bus, te_handle));
cpu.write_reg(Register::A7, sp + 4);
Ok(())
}
// TETextBox ($A9CE)
//
// PROCEDURE TETextBox(text: Ptr; length: LONGINT;
// box: Rect; align: INTEGER);
//
// Inside Macintosh: Text (1993), p. 2-88; alignment
// constants on p. 2-87.
//
// MPW Universal Headers TextEdit.h:
// EXTERN_API(void) TETextBox(const void *text,
// long length,
// const Rect * box,
// short just)
// ONEWORDINLINE(0xA9CE);
//
// IM:Text 1993 p. 2-88: "TETextBox erases the specified
// rectangle and then draws the text into it... TETextBox
// creates a transient edit record, draws the text wrapped
// to fit the rectangle, and disposes of the record."
// IM:Text 1993 p. 2-87 alignment constants:
// teJustLeft = 0 (flush left — system default)
// teJustCenter = 1 (centered)
// teJustRight = -1 (flush right)
// teForceLeft = -2 (force flush left for right-to-left)
//
// Calling convention (MPW C canonical, the only one MPW
// emits because the Universal Headers declaration uses
// `const Rect *`):
// sp+ 0 short just (last pushed, shallowest)
// sp+ 2 Rect * box (4-byte pointer)
// sp+ 6 long length (4 bytes)
// sp+10 const void * text (4 bytes, deepest)
// pop = 14 bytes; no result slot (PROCEDURE).
//
// (The Pascal canonical signature inlines the 8-byte Rect
// by value for an 18-byte frame, but MPW C never produces
// that form. Systemless's HLE only supports the MPW C
// canonical layout; the in-Rust contract tests exercise
// this layout exclusively.)
//
// Documented behaviors (BII System 7.5.3 ROM and Systemless
// HLE produce the same boolean predicates):
// 1. Erases the destination box before drawing (probe a
// pre-blackened pixel far from any glyph is WHITE after
// the call) — IM:Text 1993 p. 2-88.
// 2. Word-wraps when text exceeds rect width (40x60 box
// with "A B C D E F G" has black pixels at row 14+;
// 800x16 box with same text has none) — IM:Text 1993
// p. 2-88 line layout behavior.
// 3. Alignment parameter controls horizontal origin
// (teJustLeft / teJustCenter / teJustRight produce
// strictly increasing leftmost-black-pixel columns)
// — IM:Text 1993 p. 2-87.
// 4. Pascal PROCEDURE pops 14 bytes, no result slot
// (StackSpace bookends equal).
(true, 0x1CE) => {
let sp = cpu.read_reg(Register::A7);
let align = bus.read_word(sp) as i16;
let box_ptr = bus.read_long(sp + 2);
let box_top = bus.read_word(box_ptr) as i16;
let box_left = bus.read_word(box_ptr + 2) as i16;
let box_bottom = bus.read_word(box_ptr + 4) as i16;
let box_right = bus.read_word(box_ptr + 6) as i16;
let length = bus.read_long(sp + 6) as usize;
let text_ptr = bus.read_long(sp + 10);
cpu.write_reg(Register::A7, sp + 14);
if trace_dialog_text_inline_enabled() {
let mut stack_bytes = [0u8; 24];
for (i, byte) in stack_bytes.iter_mut().enumerate() {
*byte = bus.read_byte(sp + i as u32);
}
let preview_len = length.min(64);
let preview = if text_ptr != 0 {
bus.read_bytes(text_ptr, preview_len)
} else {
Vec::new()
};
eprintln!(
"[DIALOG-TEXT] TETextBox params current_port=${:08X} sp=${:08X} stack={:02X?} text_ptr=${:08X} len={} box=({},{}..{},{} ) align={} preview={:02X?}",
self.current_port,
sp,
stack_bytes,
text_ptr,
length,
box_top,
box_left,
box_bottom,
box_right,
align,
preview,
);
}
if box_right > box_left && box_bottom > box_top {
// TETextBox creates a transient edit record and clears the
// destination box before drawing the wrapped text. The erase
// happens even when length is zero.
// Text 1993, 2-88; Executor textedit/teDisplay.cpp C_TETextBox
self.draw_rect(
cpu,
bus,
&Rect {
top: box_top,
left: box_left,
bottom: box_bottom,
right: box_right,
},
ShapeOp::Erase,
);
if text_ptr != 0 && length > 0 {
// Read the text bytes from guest memory
let text_bytes = bus.read_bytes(text_ptr, length);
if trace_dialog_text_inline_enabled() {
let preview_len = text_bytes.len().min(160);
let first_char =
text_bytes.first().copied().map(char::from).unwrap_or('\0');
let first_glyph = crate::quickdraw::text::get_glyph(
self.tx_font,
self.tx_size,
first_char,
)
.is_some();
eprintln!(
"[DIALOG-TEXT] TETextBox current_port=${:08X} box=({},{}..{},{} ) align={} len={} txFont={} txFace=${:04X} txMode={} txSize={} firstChar={:?} firstGlyph={} text=\"{}\"",
self.current_port,
box_top,
box_left,
box_bottom,
box_right,
align,
length,
self.tx_font,
self.tx_face,
self.tx_mode,
self.tx_size,
first_char,
first_glyph,
String::from_utf8_lossy(&text_bytes[..preview_len]),
);
}
// Capture font params to avoid borrowing self in closures
let font_id = self.tx_font;
let font_size = self.tx_size;
let advance_extra = self.advance_extra();
let missing_advance = self.missing_glyph_advance();
let metrics = crate::quickdraw::text::get_font_metrics(font_id, font_size);
let line_height = metrics.ascent + metrics.descent + metrics.leading.max(2);
let box_width = box_right - box_left;
// Measure a run of bytes (no &self borrow needed)
let measure = |start: usize, end: usize| -> i16 {
let mut w = 0i16;
for &b in &text_bytes[start..end] {
let ch = b as char;
if let Some((g, _)) =
crate::quickdraw::text::get_glyph(font_id, font_size, ch)
{
w += g.advance as i16 + advance_extra;
} else {
w += missing_advance;
}
}
w
};
// Word-wrap: split into lines that fit within box_width.
// Break on spaces; if a single word exceeds box_width, break mid-word.
let mut lines: Vec<(usize, usize)> = Vec::new();
let mut line_start = 0usize;
let mut last_break = 0usize;
let mut line_width = 0i16;
for (i, &b) in text_bytes.iter().enumerate() {
if b == b'\r' || b == b'\n' {
lines.push((line_start, i));
line_start = i + 1;
last_break = line_start;
line_width = 0;
continue;
}
let ch = b as char;
let char_w = if let Some((g, _)) =
crate::quickdraw::text::get_glyph(font_id, font_size, ch)
{
g.advance as i16 + advance_extra
} else {
missing_advance
};
line_width += char_w;
if b == b' ' {
last_break = i + 1;
}
if line_width > box_width && i > line_start {
if last_break > line_start {
lines.push((line_start, last_break - 1));
line_start = last_break;
} else {
lines.push((line_start, i));
line_start = i;
}
last_break = line_start;
line_width = measure(line_start, i + 1);
}
}
if line_start < text_bytes.len() {
lines.push((line_start, text_bytes.len()));
}
// Draw each line
let mut y = box_top + metrics.ascent;
for (start, end) in &lines {
if y + metrics.descent > box_bottom {
break;
}
let mut trimmed_end = *end;
while trimmed_end > *start && text_bytes[trimmed_end - 1] == b' ' {
trimmed_end -= 1;
}
let lw = measure(*start, trimmed_end);
// Per Inside Macintosh: Text 1993, lines 7320-7323:
// teJustLeft = 0 (flush left — system default)
// teJustCenter = 1 (centered)
// teJustRight = -1 (flush right)
// teForceLeft = -2 (force flush left)
let x = Self::te_line_origin_x(align, box_left, box_right, lw);
self.pn_loc = (y, x);
for &byte in &text_bytes[*start..trimmed_end] {
self.draw_char(cpu, bus, byte as char);
}
y += line_height;
}
}
}
self.refresh_visible_dialog_snapshot_for_port(bus, self.current_port);
Ok(())
}
// TECalText ($A9D0)
// PROCEDURE TECalText(hTE: TEHandle);
// TECalText ($A9D0): Syncs TE_LENGTH and recalculates TE line layout per IM:I I-390
(true, 0x1D0) => {
let sp = cpu.read_reg(Register::A7);
let te_handle = bus.read_long(sp);
let te_ptr = Self::te_record_ptr(bus, te_handle);
if te_ptr != 0 {
let text_len =
Self::te_text_length(bus, te_handle).min(u16::MAX as usize) as u16;
bus.write_word(te_ptr + Self::TE_LENGTH_OFFSET, text_len);
self.te_recalculate_layout(bus, te_handle);
}
cpu.write_reg(Register::A7, sp + 4);
Ok(())
}
// TESetSelect ($A9D1)
// PROCEDURE TESetSelect(selStart: LONGINT; selEnd: LONGINT; hTE: TEHandle);
// Inside Macintosh Volume I, I-385
// TESetSelect ($A9D1): Clamps selEnd to teLength per IM:I I-385; selStart/selEnd range 0..32767 stored as u16
(true, 0x1D1) => {
let sp = cpu.read_reg(Register::A7);
let te_handle = bus.read_long(sp);
let te_ptr = Self::te_record_ptr(bus, te_handle);
if te_ptr != 0 {
let te_length = u32::from(bus.read_word(te_ptr + Self::TE_LENGTH_OFFSET));
// IM:I I-385: "SelEnd and selStart can range from 0 to 32767.
// If selEnd is anywhere beyond the last character of the text,
// the position just past the last character is used."
let sel_end = bus
.read_long(sp + 4)
.min(u32::from(u16::MAX))
.min(te_length);
let sel_start = bus.read_long(sp + 8).min(u32::from(u16::MAX));
bus.write_word(te_ptr + Self::TE_SEL_START_OFFSET, sel_start as u16);
bus.write_word(te_ptr + Self::TE_SEL_END_OFFSET, sel_end as u16);
}
cpu.write_reg(Register::A7, sp + 12);
Ok(())
}
// TEUpdate ($A9D3)
// PROCEDURE TEUpdate(rUpdate: Rect; hTE: TEHandle);
// Inside Macintosh Volume I (1985), p. I-387.
// Inside Macintosh: Text (1993), p. 2-88.
// MPW Universal Headers TextEdit.h:
// EXTERN_API(void) TEUpdate(const Rect *rUpdate, TEHandle hTE)
// ONEWORDINLINE(0xA9D3);
//
// IM:I I-387 quote: "TEUpdate draws the text specified by hTE
// within the rectangle rUpdate of the destination rectangle
// for the edit record specified by hTE. You normally call
// this procedure in response to an update event for the
// window."
// IM:Text 1993 p. 2-88 quote: "TEUpdate redraws the text
// specified by the edit record within the update rectangle."
//
// TEUpdate has two stack layouts that real Mac ROMs (System
// 6.0+ onward) auto-tolerate:
//
// * Pascal canonical (per IM:I I-387 PROCEDURE signature):
// sp+0 hTE (4 bytes — last pushed, Pascal LR
// order: last arg = shallowest)
// sp+4 rUpdate (8 bytes — inlined Rect, deepest)
// Net pop = 12 bytes; no result slot (PROCEDURE).
//
// * MPW C canonical (Universal Headers TextEdit.h
// declaration with `const Rect *`):
// sp+0 hTE (4 bytes)
// sp+4 rUpdatePtr (4 bytes — pointer to Rect)
// Net pop = 8 bytes; no result slot.
//
// The two forms agree on the contract: TEUpdate is a redraw
// operation that does NOT mutate the underlying edit-record
// state (hText, teLength, selStart, selEnd) — it only paints
// pixels into the TE's inPort within the rUpdate region.
//
// Both the C-form and Pascal-form stack disciplines are
// exercised, and TE record state is preserved across the two
// calls (BasiliskII System 7.5.3 ROM agrees). The heuristic
// below correctly identifies both call shapes.
//
// Contract coverage:
// dialog::tests::teupdate_pascal_form_redraws_text_and_pops_inline_rect_and_tehandle
// dialog::tests::teupdate_c_rect_pointer_form_is_accepted_and_pops_eight_bytes
// dialog::tests::teupdate_preserves_te_record_state_across_redraw
(true, 0x1D3) => {
let sp = cpu.read_reg(Register::A7);
// Both Pascal and C forms put hTE at SP+0 (last argument
// pushed in Pascal left-to-right convention). The forms
// differ at SP+4:
// * C form (8-byte total push): SP+4..7 is `rUpdatePtr`,
// a 4-byte pointer to a Rect on the heap or A5 globals.
// * Pascal form (12-byte total push): SP+4..11 is the
// 8-byte Rect inlined directly on the stack.
//
// The earlier heuristic (`te_record_ptr(first_long) != 0`)
// ALWAYS picked Pascal because first_long is the same handle
// in both forms. POD MARS Master uses the C form; the wrong
// pop-size leaked 4 bytes per TEUpdate call, which compounded
// into 8 bytes per update event (two TEUpdates) and within
// ~50 update cycles drifted A6/A7 enough to corrupt the
// EventRecord pointer the next WaitNextEvent passed in,
// starving the click hit-test. Per d4a8444b / 172f262a.
//
// The reliable test is: does the longword at SP+4 deref to
// a plausible Rect (top<=bottom, left<=right, both in
// -32000..32000)? If yes, C form. If not, fall back to
// Pascal form. Fallback covers the rare case where SP+4..11
// genuinely contains an inlined rect.
//
// Two extra guards beyond the bounds check:
// * `ptr_candidate` must be word-aligned. A Pascal-form
// small-coord rect like `(top=1, left=1, …)` produces
// `(top<<16)|left = 0x0001_0001`, which is odd and
// therefore NOT a valid 68k pointer (heap and globals
// are always even-aligned).
// * The dereffed Rect must have at least one non-zero
// word. A Pascal form with small even coords like
// `(top=10, left=10, bottom=40, right=200)` produces
// ptr_candidate `0x000A_000A`, which derefs into
// typically-zero low-memory or uninitialized heap —
// an all-zero "rect" passes the bounds test but isn't
// a meaningful update region a real C-form caller
// would pass.
let hte_at_sp = bus.read_long(sp);
let ptr_candidate = bus.read_long(sp + 4);
let looks_like_c_form = ptr_candidate != 0
&& (ptr_candidate & 1) == 0
&& (0x0000_0010..0x0400_0000).contains(&ptr_candidate)
&& {
let top = bus.read_word(ptr_candidate) as i16;
let left = bus.read_word(ptr_candidate + 2) as i16;
let bottom = bus.read_word(ptr_candidate + 4) as i16;
let right = bus.read_word(ptr_candidate + 6) as i16;
top <= bottom
&& left <= right
&& (-32000..=32000).contains(&top)
&& (-32000..=32000).contains(&left)
&& (-32000..=32000).contains(&bottom)
&& (-32000..=32000).contains(&right)
&& !(top == 0 && left == 0 && bottom == 0 && right == 0)
};
let (te_handle, stack_pop) = if looks_like_c_form {
(hte_at_sp, 8)
} else {
(hte_at_sp, 12)
};
let te_ptr = Self::te_record_ptr(bus, te_handle);
if te_ptr != 0 {
if trace_textedit_enabled() {
let dest_rect = Self::te_read_rect(bus, te_ptr + Self::TE_DEST_RECT_OFFSET);
let view_rect = Self::te_read_rect(bus, te_ptr + Self::TE_VIEW_RECT_OFFSET);
eprintln!(
"[TE] TEUpdate hTE=${:08X} dest=({},{},{},{}) view=({},{},{},{})",
te_handle,
dest_rect.0,
dest_rect.1,
dest_rect.2,
dest_rect.3,
view_rect.0,
view_rect.1,
view_rect.2,
view_rect.3
);
}
self.draw_te_contents(cpu, bus, te_handle);
}
cpu.write_reg(Register::A7, sp + stack_pop);
Ok(())
}
// TEClick ($A9D4)
// PROCEDURE TEClick(pt: Point; extend: BOOLEAN; hTE: TEHandle);
// Inside Macintosh Volume I, I-376; Inside Macintosh: Text 1993, 2-85
//
// MPW Universal Headers TextEdit.h:
// EXTERN_API(void) TEClick(Point pt, Boolean fExtend, TEHandle hTE)
// ONEWORDINLINE(0xA9D4);
//
// IM:I I-376 verbatim: "TEClick controls the placement and
// highlighting of the selection range as determined by mouse
// events. ... If the mouse button is down outside of the
// selection range or if the Shift key was not down when the
// mouse button was pressed (depending on the value of
// extend), the selection range is set to an insertion point
// at the offset corresponding to the position of the mouse."
//
// Pascal stack frame (args push left-to-right, first
// source arg deepest):
// sp+0 hTE: TEHandle (4) — last arg, shallowest
// sp+4 extend: BOOLEAN (2) — middle arg (value
// in high byte per
// MPW C convention)
// sp+6 pt.v: INTEGER (2) — first arg, second word
// sp+8 pt.h: INTEGER (2) — first arg, first word
// Total pop = 10 bytes; no function-result slot.
//
// HLE compromise: Systemless doesn't model the interactive
// drag-to-select mouse loop that real-Mac TEClick runs.
// Per IM:I I-376 a real TEClick would: (a) hit-test the
// click point against the TE's destRect, (b) compute
// the byte offset in the text corresponding to the
// click position, (c) extend or replace the selection
// range based on the `extend` flag, (d) loop polling
// the mouse for drag-to-select until WaitMouseUp. Steps
// (a)..(c) require a working caret-from-pixel mapping
// that depends on the current font + size + measured
// line widths; step (d) requires interactive mouse
// synthesis from the host event source. Neither is
// wired up in the current scripted event
// model. So the TE record's selStart/selEnd are NOT
// updated to reflect the click — apps that depend on
// click-to-position-cursor will not see the cursor
// move; apps that programmatically set selStart/selEnd
// via TESetSelect ($A9D1, already Complete) work as
// expected.
//
// BasiliskII-vs-Systemless divergence: BII System 7.5.3 ROM
// DOES update selStart/selEnd on a TEClick call (collapsing
// to the byte offset corresponding to the click position
// per IM:I I-376) because it has the real pixel-to-character
// mapping. Systemless leaves them unchanged; the two engines
// still agree on the active flag, teLength preservation, and
// Pascal PROCEDURE stack discipline.
//
// Both engines DO agree that TEClick must not mutate the
// active flag (owned by TEActivate/TEDeactivate per
// IM:I I-385) or teLength (owned by
// TESetText/TEKey/TEDelete/TEInsert).
//
// NIL hTE is a defensive no-op (matches the real-Mac
// safety contract — TEClick on NIL is undefined but
// shouldn't crash the host).
//
// Regression coverage:
// dialog::tests::teclick_consumes_point_extend_and_tehandle_arguments
// dialog::tests::teclick_empty_text_keeps_insertion_point_at_zero
// dialog::tests::teclick_preserves_terec_active_flag_and_telength
// dialog::tests::teclick_repeated_calls_balance_stack_no_drift
(true, 0x1D4) => {
let sp = cpu.read_reg(Register::A7);
if trace_textedit_enabled() {
let te_handle = bus.read_long(sp);
// Pascal BOOLEAN in high byte (MPW C convention).
let extend = bus.read_byte(sp + 4) != 0;
let pt_v = bus.read_word(sp + 6) as i16;
let pt_h = bus.read_word(sp + 8) as i16;
eprintln!(
"[TE] TEClick hTE=${:08X} extend={} pt=({}, {})",
te_handle, extend, pt_v, pt_h
);
}
cpu.write_reg(Register::A7, sp + 10);
Ok(())
}
// TECopy ($A9D5)
// PROCEDURE TECopy(hTE: TEHandle);
// Copies the currently-selected bytes into the TextEdit
// scrap (TEScrpLength low-mem global at $0AB0 (word) /
// TEScrpHandle at $0AB4 (long)). Empty selection leaves
// the scrap untouched per IM:I I-386.
// Inside Macintosh Volume I, I-386
// 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.
(true, 0x1D5) => {
let sp = cpu.read_reg(Register::A7);
let te_handle = bus.read_long(sp);
let te_ptr = Self::te_record_ptr(bus, te_handle);
if te_ptr != 0 {
let sel_start = bus.read_word(te_ptr + Self::TE_SEL_START_OFFSET) as usize;
let sel_end = bus.read_word(te_ptr + Self::TE_SEL_END_OFFSET) as usize;
let (s, e) = if sel_start <= sel_end {
(sel_start, sel_end)
} else {
(sel_end, sel_start)
};
if s != e {
use crate::memory::globals::addr;
let existing = Self::te_text_bytes(bus, te_handle);
let s = s.min(existing.len());
let e = e.min(existing.len());
let selected = &existing[s..e];
let mut scrap_handle = bus.read_long(addr::TE_SCRP_HANDLE);
if scrap_handle == 0 {
scrap_handle = Self::allocate_handle_with_data(bus, 0);
bus.write_long(addr::TE_SCRP_HANDLE, scrap_handle);
}
let text_ptr =
Self::ensure_text_handle_size(bus, scrap_handle, selected.len());
if text_ptr != 0 && !selected.is_empty() {
bus.write_bytes(text_ptr, selected);
}
bus.write_word(
addr::TE_SCRP_LENGTH,
selected.len().min(u16::MAX as usize) as u16,
);
}
}
cpu.write_reg(Register::A7, sp + 4);
Ok(())
}
// TECut ($A9D6)
// PROCEDURE TECut(hTE: TEHandle);
// Removes the currently-selected text from the edit record
// and places a copy in the TextEdit scrap (TEScrpLength at
// $0AB0 / TEScrpHandle at $0AB4). Any previous scrap
// contents are replaced. Insertion-point selection is a
// no-op per IM:I I-391.
// 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.
(true, 0x1D6) => {
let sp = cpu.read_reg(Register::A7);
let te_handle = bus.read_long(sp);
let te_ptr = Self::te_record_ptr(bus, te_handle);
if te_ptr != 0 {
let sel_start = bus.read_word(te_ptr + Self::TE_SEL_START_OFFSET) as usize;
let sel_end = bus.read_word(te_ptr + Self::TE_SEL_END_OFFSET) as usize;
let (s, e) = if sel_start <= sel_end {
(sel_start, sel_end)
} else {
(sel_end, sel_start)
};
if s != e {
let existing = Self::te_text_bytes(bus, te_handle);
let s = s.min(existing.len());
let e = e.min(existing.len());
// Copy selection into the scrap (replace, not
// append — IM:I I-391).
{
use crate::memory::globals::addr;
let selected = &existing[s..e];
let mut scrap_handle = bus.read_long(addr::TE_SCRP_HANDLE);
if scrap_handle == 0 {
scrap_handle = Self::allocate_handle_with_data(bus, 0);
bus.write_long(addr::TE_SCRP_HANDLE, scrap_handle);
}
let text_ptr =
Self::ensure_text_handle_size(bus, scrap_handle, selected.len());
if text_ptr != 0 && !selected.is_empty() {
bus.write_bytes(text_ptr, selected);
}
bus.write_word(
addr::TE_SCRP_LENGTH,
selected.len().min(u16::MAX as usize) as u16,
);
}
// Delete selection from the TE text.
let mut merged = Vec::with_capacity(existing.len() - (e - s));
merged.extend_from_slice(&existing[..s]);
merged.extend_from_slice(&existing[e..]);
self.te_set_text_contents(bus, te_handle, &merged);
let new_pos = s.min(u16::MAX as usize) as u16;
bus.write_word(te_ptr + Self::TE_SEL_START_OFFSET, new_pos);
bus.write_word(te_ptr + Self::TE_SEL_END_OFFSET, new_pos);
}
}
cpu.write_reg(Register::A7, sp + 4);
Ok(())
}
// TEDelete ($A9D7)
// Removes the currently selected text from the edit record and
// redraws. If selStart == selEnd (insertion point), does nothing.
// Does NOT transfer text to the scrap (unlike TECut).
// PROCEDURE TEDelete(hTE: TEHandle);
// Inside Macintosh Volume I, I-387; Text 1993, 2-93
//
// Regression coverage:
// dialog::tests::tedelete_removes_selection_without_touching_scrap_and_pops_arg
// dialog::tests::tedelete_insertion_point_selection_is_noop_and_pops_arg
// TEDelete ($A9D7): Removes selected text, collapses selection; no-op at insertion point. IM:I I-387
(true, 0x1D7) => {
let sp = cpu.read_reg(Register::A7);
let te_handle = bus.read_long(sp);
if trace_textedit_enabled() {
eprintln!("[TE] TEDelete hTE=${:08X}", te_handle);
}
let te_ptr = Self::te_record_ptr(bus, te_handle);
if te_ptr != 0 {
let sel_start = bus.read_word(te_ptr + Self::TE_SEL_START_OFFSET) as usize;
let sel_end = bus.read_word(te_ptr + Self::TE_SEL_END_OFFSET) as usize;
if sel_start != sel_end {
let existing = Self::te_text_bytes(bus, te_handle);
let s = sel_start.min(existing.len());
let e = sel_end.min(existing.len());
let (s, e) = if s > e { (e, s) } else { (s, e) };
let mut merged = Vec::with_capacity(existing.len() - (e - s));
merged.extend_from_slice(&existing[..s]);
merged.extend_from_slice(&existing[e..]);
self.te_set_text_contents(bus, te_handle, &merged);
let new_pos = s.min(u16::MAX as usize) as u16;
bus.write_word(te_ptr + Self::TE_SEL_START_OFFSET, new_pos);
bus.write_word(te_ptr + Self::TE_SEL_END_OFFSET, new_pos);
}
}
cpu.write_reg(Register::A7, sp + 4);
Ok(())
}
// TEActivate ($A9D8)
// Activates the edit record, highlighting the selection range or
// displaying a blinking caret at the insertion point.
// PROCEDURE TEActivate(hTE: TEHandle);
// Inside Macintosh Volume I, I-385; Text 1993, 2-80
//
// Regression coverage:
// teactivate_and_tedeactivate_repaint_empty_insertion_caret
// TEActivate ($A9D8): Sets TERec.active flag per IM:I I-385
(true, 0x1D8) => {
let sp = cpu.read_reg(Register::A7);
let te_handle = bus.read_long(sp);
if trace_textedit_enabled() {
eprintln!("[TE] TEActivate hTE=${:08X}", te_handle);
}
let te_ptr = Self::te_record_ptr(bus, te_handle);
if te_ptr != 0 {
bus.write_word(te_ptr + Self::TE_ACTIVE_OFFSET, 1);
bus.write_long(te_ptr + Self::TE_CARET_TIME_OFFSET, self.tick_count);
bus.write_word(te_ptr + Self::TE_CARET_STATE_OFFSET, 0);
self.draw_te_contents(cpu, bus, te_handle);
}
cpu.write_reg(Register::A7, sp + 4);
Ok(())
}
// TEDeactivate ($A9D9)
// Deactivates the edit record, unhighlighting the selection range
// or removing the caret. Does not affect selStart/selEnd.
// PROCEDURE TEDeactivate(hTE: TEHandle);
// Inside Macintosh Volume I, I-385; Text 1993, 2-80
//
// Regression coverage:
// teactivate_and_tedeactivate_repaint_empty_insertion_caret
// TEDeactivate ($A9D9): Clears TERec.active flag per IM:I I-385
(true, 0x1D9) => {
let sp = cpu.read_reg(Register::A7);
let te_handle = bus.read_long(sp);
if trace_textedit_enabled() {
eprintln!("[TE] TEDeactivate hTE=${:08X}", te_handle);
}
let te_ptr = Self::te_record_ptr(bus, te_handle);
if te_ptr != 0 {
bus.write_word(te_ptr + Self::TE_ACTIVE_OFFSET, 0);
self.draw_te_contents(cpu, bus, te_handle);
}
cpu.write_reg(Register::A7, sp + 4);
Ok(())
}
// TEIdle ($A9DA)
// PROCEDURE TEIdle(hTE: TEHandle);
// Inside Macintosh Volume I (1985), p. I-374; Inside
// Macintosh: Text (1993), p. 2-84.
// MPW Universal Headers TextEdit.h:
// EXTERN_API(void) TEIdle(TEHandle hTE) ONEWORDINLINE(0xA9DA);
//
// Pascal stack frame:
// sp+0 hTE: TEHandle (4)
// Total pop = 4 bytes; no function-result slot. Net
// externally-observed SP delta is zero (StackSpace
// bookends are equal across one call).
//
// IM:I I-374 quote: "Call TEIdle from your event loop...
// TEIdle causes the caret of the edit record to blink at
// the rate specified in caretTime."
//
// IM:Text 1993 p. 2-84 quote: "TEIdle blinks the caret if
// the destination rectangle contains the caret position;
// if the specified edit record is inactive (such as when
// the window is inactive), TEIdle has no effect."
//
// HLE model: TextEdit's TERec layout includes the private
// caretTime and caretState fields before just/teLength. Systemless
// treats caretTime as the last toggle tick and caretState == 0 as
// visible. On an active insertion point, TEIdle waits the
// documented initial 32-tick interval, toggles caretState, and
// redraws the edit record. Non-insertion selections do not blink.
//
// Crucial no-mutation contract: TEIdle MUST NOT mutate TERec.active,
// TERec.selStart, TERec.selEnd, TERec.teLength, or any
// other selection / text fields. Selection updates are
// the exclusive responsibility of TESetSelect /
// TEClick / TEKey / TEDelete; activation toggles are
// handled by TEActivate / TEDeactivate.
//
// NIL hTE is a defensive no-op.
//
// Regression coverage:
// dialog::tests::teidle_consumes_tehandle_argument
// dialog::tests::teidle_toggles_visible_insertion_caret_after_blink_interval
// dialog::tests::teidle_preserves_active_flag_and_selection_offsets
// dialog::tests::teidle_repeated_calls_balance_stack_and_preserve_terec_state
(true, 0x1DA) => {
let sp = cpu.read_reg(Register::A7);
let te_handle = bus.read_long(sp);
self.textedit_idle(cpu, bus, te_handle);
cpu.write_reg(Register::A7, sp + 4);
Ok(())
}
// TEPaste ($A9DB)
// PROCEDURE TEPaste(hTE: TEHandle);
// Replaces the current selection with the contents of the
// TextEdit scrap (TEScrpLength at $0AB0 word, TEScrpHandle
// at $0AB4 long). Empty scrap leaves the TERec unchanged
// per IM:I I-387.
// Inside Macintosh Volume I, I-387
// 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.
(true, 0x1DB) => {
let sp = cpu.read_reg(Register::A7);
let te_handle = bus.read_long(sp);
if trace_textedit_enabled() {
eprintln!("[TE] TEPaste hTE=${:08X}", te_handle);
}
use crate::memory::globals::addr;
let scrap_len = bus.read_word(addr::TE_SCRP_LENGTH) as usize;
let scrap_handle = bus.read_long(addr::TE_SCRP_HANDLE);
if scrap_handle != 0 && scrap_len > 0 {
let scrap_ptr = bus.read_long(scrap_handle);
if scrap_ptr != 0 {
let scrap = bus.read_bytes(scrap_ptr, scrap_len);
self.te_insert_text(bus, te_handle, &scrap);
}
}
cpu.write_reg(Register::A7, sp + 4);
Ok(())
}
// TEKey ($A9DC)
// Replaces the selection range with the input character and positions
// the insertion point just past it. Backspace ($08) deletes the
// selection or the character before the insertion point.
// PROCEDURE TEKey(key: CHAR; hTE: TEHandle);
// Inside Macintosh Volume I, I-385; Text 1993, 2-81
//
// TEKey ($A9DC): Inserts character at insertion point, replaces selection; backspace deletes. IM:I I-385
(true, 0x1DC) => {
let sp = cpu.read_reg(Register::A7);
let te_handle = bus.read_long(sp);
let key = bus.read_word(sp + 4) as u8;
if trace_textedit_enabled() {
eprintln!(
"[TE] TEKey hTE=${:08X} key=${:02X} {:?}",
te_handle,
key,
char::from(key).escape_default().to_string()
);
}
let te_ptr = Self::te_record_ptr(bus, te_handle);
if te_ptr != 0 {
let sel_start = bus.read_word(te_ptr + Self::TE_SEL_START_OFFSET) as usize;
let sel_end = bus.read_word(te_ptr + Self::TE_SEL_END_OFFSET) as usize;
let existing = Self::te_text_bytes(bus, te_handle);
let text_len = existing.len();
let s = sel_start.min(text_len);
let e = sel_end.min(text_len);
let (s, e) = if s > e { (e, s) } else { (s, e) };
if key == 0x08 {
// Backspace
if s != e {
// Delete selected text
let mut merged = Vec::with_capacity(text_len - (e - s));
merged.extend_from_slice(&existing[..s]);
merged.extend_from_slice(&existing[e..]);
self.te_set_text_contents(bus, te_handle, &merged);
let new_pos = s.min(u16::MAX as usize) as u16;
bus.write_word(te_ptr + Self::TE_SEL_START_OFFSET, new_pos);
bus.write_word(te_ptr + Self::TE_SEL_END_OFFSET, new_pos);
} else if s > 0 {
// Delete character before insertion point
let mut merged = Vec::with_capacity(text_len - 1);
merged.extend_from_slice(&existing[..s - 1]);
merged.extend_from_slice(&existing[s..]);
self.te_set_text_contents(bus, te_handle, &merged);
let new_pos = (s - 1).min(u16::MAX as usize) as u16;
bus.write_word(te_ptr + Self::TE_SEL_START_OFFSET, new_pos);
bus.write_word(te_ptr + Self::TE_SEL_END_OFFSET, new_pos);
}
// At start with no selection: do nothing
} else {
// Insert printable character (replacing selection if any)
self.te_insert_text(bus, te_handle, &[key]);
}
}
cpu.write_reg(Register::A7, sp + 6);
Ok(())
}
// TEScroll ($A9DD)
// PROCEDURE TEScroll(dh: INTEGER; dv: INTEGER; hTE: TEHandle);
// TEScroll ($A9DD): Scrolls destRect by (dh, dv) and redraws per IM:I I-389 / Text 1993 2-89
(true, 0x1DD) => {
let sp = cpu.read_reg(Register::A7);
let te_handle = bus.read_long(sp);
let dv = bus.read_word(sp + 4) as i16;
let dh = bus.read_word(sp + 6) as i16;
if trace_textedit_enabled() {
eprintln!("[TE] TEScroll hTE=${:08X} dh={} dv={}", te_handle, dh, dv);
}
self.te_scroll_contents(cpu, bus, te_handle, dh, dv);
cpu.write_reg(Register::A7, sp + 8);
Ok(())
}
// TEInsert ($A9DE)
// PROCEDURE TEInsert(text: Ptr; length: LONGINT; hTE: TEHandle);
// Inside Macintosh Volume I, I-387; Inside Macintosh: Text 1993,
// 2-94. TEInsert splices the supplied bytes into hText at the
// selStart offset (inserting JUST BEFORE the selection range, not
// replacing it). The selection range is preserved logically —
// selStart and selEnd both shift forward by `length` so the
// selection continues to span the same original characters.
// TEInsert does not touch the scrap.
(true, 0x1DE) => {
let sp = cpu.read_reg(Register::A7);
let te_handle = bus.read_long(sp);
let length = bus.read_long(sp + 4) as usize;
let text_ptr = bus.read_long(sp + 8);
if text_ptr != 0 && length != 0 {
let te_ptr = Self::te_record_ptr(bus, te_handle);
if te_ptr != 0 {
let existing = Self::te_text_bytes(bus, te_handle);
let sel_start = (bus.read_word(te_ptr + Self::TE_SEL_START_OFFSET)
as usize)
.min(existing.len());
let sel_end = (bus.read_word(te_ptr + Self::TE_SEL_END_OFFSET) as usize)
.min(existing.len());
let text = bus.read_bytes(text_ptr, length);
let mut merged = Vec::with_capacity(existing.len() + text.len());
merged.extend_from_slice(&existing[..sel_start]);
merged.extend_from_slice(&text);
merged.extend_from_slice(&existing[sel_start..]);
self.te_set_text_contents(bus, te_handle, &merged);
let shifted_start = (sel_start + text.len()).min(u16::MAX as usize) as u16;
let shifted_end = (sel_end + text.len()).min(u16::MAX as usize) as u16;
bus.write_word(te_ptr + Self::TE_SEL_START_OFFSET, shifted_start);
bus.write_word(te_ptr + Self::TE_SEL_END_OFFSET, shifted_end);
}
}
cpu.write_reg(Register::A7, sp + 12);
Ok(())
}
// TESetAlignment ($A9DF)
// Sets the alignment (justification) of the text in the edit record.
// PROCEDURE TESetAlignment(just: INTEGER; hTE: TEHandle);
// Inside Macintosh Volume I, I-387; Text 1993, 2-87
//
// TESetAlignment ($A9DF): Writes just field to TERec per IM:I I-387
(true, 0x1DF) => {
let sp = cpu.read_reg(Register::A7);
let te_handle = bus.read_long(sp);
let just = bus.read_word(sp + 4) as i16;
let te_ptr = Self::te_record_ptr(bus, te_handle);
if te_ptr != 0 {
bus.write_word(te_ptr + Self::TE_JUST_OFFSET, just as u16);
}
cpu.write_reg(Register::A7, sp + 6);
Ok(())
}
// SelectDialogItemText ($A97E)
// PROCEDURE SelectDialogItemText(theDialog: DialogPtr;
// itemNo: INTEGER; strtSel: INTEGER; endSel: INTEGER);
// Selects and highlights text in an editable text dialog item.
// Inside Macintosh Volume I, I-414; Macintosh Toolbox Essentials
// 1992, 6-131..6-132
//
// Pascal stack frame:
// SP+0 endSel: INTEGER (2) — last arg, shallowest
// SP+2 strtSel: INTEGER (2)
// SP+4 itemNo: INTEGER (2)
// SP+6 theDialog: DialogPtr (4) — first arg, deepest
// Total pop = 10 bytes.
//
// Behaviour per IM:I I-414:
// - "Selects the text from character strtSel through
// character endSel-1 in the editable text item with
// item number itemNo of the specified dialog box."
// - Special case: "If you set strtSel = 0 and
// endSel = -1, the entire text is selected" — we
// normalize this to (0, text.len()) at dispatch.
// - If itemNo is not an editText item (type 16), the
// procedure is a no-op (real Mac just ignores it).
// - The named item becomes the dialog's active edit
// field (DialogRecord.editField at offset 164).
//
// Systemless stores the normalized range in DialogItem and mirrors
// it into DialogRecord.textH's TERecord when present, then redraws
// the visible editText item so the selection highlight is
// immediate like SelIText / SelectDialogItemText.
(true, 0x17E) => {
let sp = cpu.read_reg(Register::A7);
let end_sel = bus.read_word(sp) as i16;
let start_sel = bus.read_word(sp + 2) as i16;
let item_no = bus.read_word(sp + 4) as i16;
let dialog_ptr = bus.read_long(sp + 6);
let mut redraw_item = None;
if dialog_ptr != 0 && item_no > 0 {
if let Some(items) = self.dialog_items.get_mut(&dialog_ptr) {
if let Some(item) = items.get_mut((item_no - 1) as usize) {
if item.item_type & 0x7F == 16 {
// IM:I I-414 special case: (0, -1)
// means "select all" — normalize
// to (0, text.len()).
let text_len = item.text.len() as i16;
let (s, e) = if start_sel == 0 && end_sel == -1 {
(0, text_len)
} else {
// Clamp to text bounds.
let s = start_sel.max(0).min(text_len);
let e = end_sel.max(0).min(text_len);
// Swap if reversed (defensive
// — real Mac also normalizes).
if s <= e {
(s, e)
} else {
(e, s)
}
};
item.sel_start = s;
item.sel_end = e;
bus.write_word(dialog_ptr + 164, (item_no - 1) as u16);
// Mirror selStart/selEnd into the TERecord so
// callers that read (**textH).selStart via the
// canonical DialogRecord layout can verify the
// selection (IM:I I-382, I-414).
// textH is a TEHandle at dialog_ptr+160
// (IM:I I-411 DialogRecord layout).
let text_h_handle = bus.read_long(dialog_ptr + 160);
if text_h_handle != 0 {
let te_ptr = bus.read_long(text_h_handle);
if te_ptr != 0 {
bus.write_word(
te_ptr + Self::TE_SEL_START_OFFSET,
s as u16,
);
bus.write_word(te_ptr + Self::TE_SEL_END_OFFSET, e as u16);
}
}
redraw_item = Some((dialog_ptr, item_no));
}
}
}
}
if let Some((dialog_ptr, item_no)) = redraw_item {
self.redraw_dialog_text_item(bus, dialog_ptr, item_no);
}
cpu.write_reg(Register::A7, sp + 10);
Ok(())
}
// NewCDialog ($AA4B)
// Color-aware variant of NewDialog. Identical parameters and
// semantics; internally uses a CGrafPort instead of GrafPort
// so the dialog supports color drawing. For our HLE this is
// the same code path as NewDialog.
// FUNCTION NewCDialog(dStorage: Ptr; boundsRect: Rect;
// title: Str255; visible: BOOLEAN; procID: INTEGER;
// behind: WindowPtr; goAwayFlag: BOOLEAN;
// refCon: LongInt; items: Handle): CDialogPtr;
// Inside Macintosh Volume V, V-243
//
// NewDialog ($A97D)
// Creates a dialog from a caller-supplied item list handle.
// FUNCTION NewDialog(dStorage: Ptr; boundsRect: Rect; title: Str255;
// visible: BOOLEAN; procID: INTEGER; behind: WindowPtr;
// goAwayFlag: BOOLEAN; refCon: LONGINT; items: Handle): DialogPtr;
// Inside Macintosh Volume I, I-412
// NewDialog ($A97D): Allocates DialogRecord, processes DITL items, pops 32 bytes
// NewCDialog ($AA4B): Color-aware NewDialog; same code path, CGrafPort variant per IM:V V-243
(true, 0x17D) | (true, 0x24B) => {
let sp = cpu.read_reg(Register::A7);
// 68K Pascal stack:
// SP+0: items(4)
// SP+4: refCon(4)
// SP+8: goAwayFlag(2)
// SP+10: behind(4)
// SP+14: procID(2)
// SP+16: visible(2)
// SP+18: title(4)
// SP+22: boundsRect(4)
// SP+26: dStorage(4)
// SP+30: result(4)
let items_handle = bus.read_long(sp);
let ref_con = bus.read_long(sp + 4);
// Pascal BOOLEAN as the HIGH byte of its 2-byte stack slot
// (MPW C convention).
let go_away_flag = bus.read_byte(sp + 8) != 0;
let behind = bus.read_long(sp + 10);
let proc_id = bus.read_word(sp + 14) as i16;
let visible = bus.read_byte(sp + 16) != 0;
let title_ptr = bus.read_long(sp + 18);
let bounds_rect_ptr = bus.read_long(sp + 22);
let storage_ptr = bus.read_long(sp + 26);
let bounds = if bounds_rect_ptr != 0 {
(
bus.read_word(bounds_rect_ptr) as i16,
bus.read_word(bounds_rect_ptr + 2) as i16,
bus.read_word(bounds_rect_ptr + 4) as i16,
bus.read_word(bounds_rect_ptr + 6) as i16,
)
} else {
(0, 0, 0, 0)
};
let title = if title_ptr != 0 {
decode_mac_roman_for_render(&bus.read_pstring(title_ptr))
} else {
String::new()
};
let items_ptr = if items_handle != 0 {
bus.read_long(items_handle)
} else {
0
};
let items_len = if items_ptr != 0 {
bus.get_alloc_size(items_ptr).unwrap_or(0)
} else {
0
};
let items = if items_ptr != 0 && items_len != 0 {
Self::parse_ditl(bus, items_ptr, items_len)
} else {
Vec::new()
};
let dlg_ptr = self.finish_dialog_creation(
bus,
cpu,
storage_ptr,
bounds,
&title,
visible,
proc_id,
go_away_flag,
ref_con,
items_handle,
items,
);
// Honor Pascal `behind` param at SP+10 per IM:I I-412.
self.apply_behind_parameter(bus, dlg_ptr, behind);
bus.write_long(sp + 30, dlg_ptr);
cpu.write_reg(Register::A7, sp + 30);
Ok(())
}
// CloseDialog ($A982)
// PROCEDURE CloseDialog(theDialog: DialogPtr);
// Inside Macintosh Volume I, I-413
// CloseDialog ($A982): Removes the dialog from the window list
// and restores previous window state (bounds, procID, title)
// when the closed dialog was frontmost. IM:I I-413 explicitly
// states "deletes the dialog window from the window list" —
// routed through untrack_window so FrontWindow sees the
// updated list, matching DisposDialog's pattern.
(true, 0x182) => {
let sp = cpu.read_reg(Register::A7);
let dialog_ptr = bus.read_long(sp);
self.close_dialog_window(bus, cpu, dialog_ptr, false);
cpu.write_reg(Register::A7, sp + 4);
Ok(())
}
// UpdtDialog ($A978)
// Redraws the dialog items in the specified update region.
// PROCEDURE UpdtDialog(theDialog: DialogPtr; updateRgn: RgnHandle);
// Inside Macintosh Volume I, I-415; Macintosh Toolbox Essentials 1992, 6-142..6-143
(true, 0x178) => {
let sp = cpu.read_reg(Register::A7);
let update_rgn = bus.read_long(sp);
let dialog_ptr = bus.read_long(sp + 4);
cpu.write_reg(Register::A7, sp + 8);
if update_rgn == 0 {
return Some(Ok(()));
}
let update_rect = Self::region_handle_rect(bus, update_rgn);
self.update_dialog_window_contents(bus, cpu, dialog_ptr, update_rect);
Ok(())
}
// CouldDialog ($A979) / FreeDialog ($A97A)
// PROCEDURE CouldDialog(dialogID: INTEGER);
// PROCEDURE FreeDialog (dialogID: INTEGER);
// Inside Macintosh Volume I, I-415
//
// Per IM:I I-415, CouldDialog makes the DLOG, its DITL, and
// resource-backed DITL items unpurgeable, loading them first if
// needed; FreeDialog makes the same already-loaded resources
// purgeable again. Systemless models that visible handle-state
// axis for DLOG/DITL plus CNTL/ICON/PICT DITL resources. WDEF
// cascade is intentionally omitted: HLE dialog window procs are
// not loaded or executed as guest WDEF resources.
// CouldDialog ($A979): Loads and HNoPurge-equivalent marks DLOG/DITL/resource-backed items; writes ResErr noErr on both hit and miss in BasiliskII.
// FreeDialog ($A97A): HPurge-equivalent marks the already-loaded DLOG/DITL/resource-backed items; writes ResErr noErr on both hit and miss in BasiliskII.
(true, 0x179) | (true, 0x17A) => {
let sp = cpu.read_reg(Register::A7);
let dialog_id = bus.read_word(sp) as i16;
let load_if_missing = trap_num == 0x179;
let purgeable = trap_num == 0x17A;
self.cascade_dialog_resource_purgeability(
bus,
*b"DLOG",
dialog_id,
purgeable,
load_if_missing,
);
let res_err = self.dialog_template_res_err(dialog_id);
bus.write_word(0x0A60, res_err as u16);
cpu.write_reg(Register::A7, sp + 2);
Ok(())
}
// HideDialogItem ($A827)
// Erases the item's enclosing rectangle, then moves the
// item's display rect offscreen so it isn't drawn or hit-tested.
// The original rect is saved so ShowDialogItem can restore it.
// PROCEDURE HideDialogItem(theDialog: DialogPtr; itemNo: INTEGER);
// Inside Macintosh Volume IV, IV-59
// Stack: SP+0=itemNo(2), SP+2=theDialog(4). Pop 6.
// HideDialogItem ($A827): If itemRect.left < 8192, calls EraseRect on
// the item's enclosing rectangle, adds that rectangle to the update
// region, and offsets itemRect.left/right by +16384 so the item
// becomes offscreen; already-hidden items are unchanged
// (Inside Macintosh Volume IV, IV-59; MTE 1992, 6-123).
// Pops 6 bytes per IM:IV IV-59.
(true, 0x027) => {
let sp = cpu.read_reg(Register::A7);
let item_no = bus.read_word(sp) as i16;
let dialog_ptr = bus.read_long(sp + 2);
cpu.write_reg(Register::A7, sp + 6);
let mut updated_control_rect = None;
let mut redraw_local_rect = None;
if dialog_ptr != 0 && item_no > 0 {
let key = (dialog_ptr, item_no);
if let Some(items) = self.dialog_items.get_mut(&dialog_ptr) {
let idx = (item_no as usize).wrapping_sub(1);
if idx < items.len() {
let rect = items[idx].rect;
// MTE 1992, 6-123: already-hidden items (left > 8192) are a no-op.
if rect.1 < 8192 {
let enclosing = Self::dialog_item_enclosing_local_rect(
items[idx].item_type,
rect,
);
self.hidden_dialog_item_rects.entry(key).or_insert(rect);
items[idx].rect.1 = rect.1.wrapping_add(16384);
items[idx].rect.3 = rect.3.wrapping_add(16384);
updated_control_rect = Some(items[idx].rect);
redraw_local_rect = Some(enclosing);
}
}
}
if let Some(rect) = redraw_local_rect {
self.erase_dialog_item_enclosing_rect(bus, dialog_ptr, rect);
self.invalidate_window_rect(bus, dialog_ptr, rect);
}
if let Some(rect) = updated_control_rect {
if let Some(ctrl_handle) =
self.dialog_control_handle_for_item(dialog_ptr, item_no)
{
let ctrl_ptr = bus.read_long(ctrl_handle);
if ctrl_ptr != 0 {
bus.write_word(ctrl_ptr + 8, rect.0 as u16);
bus.write_word(ctrl_ptr + 10, rect.1 as u16);
bus.write_word(ctrl_ptr + 12, rect.2 as u16);
bus.write_word(ctrl_ptr + 14, rect.3 as u16);
}
}
if let Some(tracking) = self.dialog_tracking.as_mut() {
if tracking.dialog_ptr == dialog_ptr {
let idx = (item_no as usize).wrapping_sub(1);
if idx < tracking.items.len() {
tracking.items[idx].rect = rect;
}
}
}
}
}
Ok(())
}
// ShowDialogItem ($A828)
// Restores the rect saved by HideDialogItem and invalidates
// the new rect so the dialog redraws the item.
// PROCEDURE ShowDialogItem(theDialog: DialogPtr; itemNo: INTEGER);
// Inside Macintosh Volume IV, IV-59
// ShowDialogItem ($A828): If itemRect.left > 8192, restores visibility by
// subtracting 16384 from itemRect.left/right; already-visible items are unchanged
// (MTE 1992, 6-124). Pops 6 bytes per IM:IV IV-59.
(true, 0x028) => {
let sp = cpu.read_reg(Register::A7);
let item_no = bus.read_word(sp) as i16;
let dialog_ptr = bus.read_long(sp + 2);
cpu.write_reg(Register::A7, sp + 6);
let mut updated_control_rect = None;
let mut redraw_local_rect = None;
if dialog_ptr != 0 && item_no > 0 {
let key = (dialog_ptr, item_no);
if let Some(items) = self.dialog_items.get_mut(&dialog_ptr) {
let idx = (item_no as usize).wrapping_sub(1);
if idx < items.len() {
let rect = items[idx].rect;
// MTE 1992, 6-124: already-visible items (left < 8192) are a no-op.
if rect.1 > 8192 {
let restored_rect = if let Some(orig_rect) =
self.hidden_dialog_item_rects.remove(&key)
{
orig_rect
} else {
(
rect.0,
rect.1.wrapping_sub(16384),
rect.2,
rect.3.wrapping_sub(16384),
)
};
let enclosing = Self::dialog_item_enclosing_local_rect(
items[idx].item_type,
restored_rect,
);
items[idx].rect = restored_rect;
updated_control_rect = Some(restored_rect);
redraw_local_rect = Some(enclosing);
}
}
}
if let Some(rect) = redraw_local_rect {
self.invalidate_window_rect(bus, dialog_ptr, rect);
}
if let Some(rect) = updated_control_rect {
if let Some(ctrl_handle) =
self.dialog_control_handle_for_item(dialog_ptr, item_no)
{
let ctrl_ptr = bus.read_long(ctrl_handle);
if ctrl_ptr != 0 {
bus.write_word(ctrl_ptr + 8, rect.0 as u16);
bus.write_word(ctrl_ptr + 10, rect.1 as u16);
bus.write_word(ctrl_ptr + 12, rect.2 as u16);
bus.write_word(ctrl_ptr + 14, rect.3 as u16);
}
}
if let Some(tracking) = self.dialog_tracking.as_mut() {
if tracking.dialog_ptr == dialog_ptr {
let idx = (item_no as usize).wrapping_sub(1);
if idx < tracking.items.len() {
tracking.items[idx].rect = rect;
}
}
}
}
}
Ok(())
}
// FindDItem ($A984)
// Returns the 0-indexed item number of the item containing thePt
// (in coordinates local to the dialog box), or –1 if no item
// contains the point. Disabled items are returned per IM:IV-60;
// hidden items naturally fail the rect-contains check because
// HideDialogItem moves their rect to (16384, 16384, 16385, 16385).
// Overlapping items resolve to the first matching item in DITL
// order (Macintosh Toolbox Essentials 1992, 6-125).
// FUNCTION FindDItem(theDialog: DialogPtr; thePt: Point): INTEGER;
// Inside Macintosh Volume IV, IV-60; Macintosh Toolbox Essentials 1992, 6-125
//
// Stack layout (Pascal — args pushed left-to-right, result slot
// pre-pushed by caller):
// SP+0..3: thePt (Point — v at +0..1, h at +2..3)
// SP+4..7: theDialog (DialogPtr)
// SP+8..9: result slot (INTEGER)
//
// FindDItem ($A984): Walks dialog_items in order; first item whose
// local rect contains thePt wins; returns 0-indexed item number or
// -1 per Macintosh Toolbox Essentials 1992, p. 6-125.
(true, 0x184) => {
let sp = cpu.read_reg(Register::A7);
let pt_v = bus.read_word(sp) as i16;
let pt_h = bus.read_word(sp + 2) as i16;
let dialog_ptr = bus.read_long(sp + 4);
let result: i16 = if dialog_ptr == 0 {
-1
} else if let Some(items) = self.dialog_items.get(&dialog_ptr) {
let mut hit: i16 = -1;
for (idx, item) in items.iter().enumerate() {
let (top, left, bottom, right) = item.rect;
if pt_v >= top && pt_v < bottom && pt_h >= left && pt_h < right {
hit = idx as i16;
break;
}
}
hit
} else {
-1
};
bus.write_word(sp + 8, result as u16);
cpu.write_reg(Register::A7, sp + 8);
Ok(())
}
// CouldAlert ($A989)
// PROCEDURE CouldAlert(alertID: INTEGER);
// Inside Macintosh Volume I, I-420
//
// FreeAlert ($A98A)
// PROCEDURE FreeAlert(alertID: INTEGER);
// Inside Macintosh Volume I, I-420
//
// Companion of CouldDialog/FreeDialog ($A979/$A97A) for ALRT
// templates. Per IM:I I-420, CouldAlert loads and makes the
// ALRT, DITL, and resource-backed DITL items unpurgeable;
// FreeAlert marks the already-loaded equivalents purgeable.
// BasiliskII treats missing alert IDs as harmless no-ops and
// leaves ResErr at noErr.
(true, 0x189) | (true, 0x18A) => {
let sp = cpu.read_reg(Register::A7);
let alert_id = bus.read_word(sp) as i16;
let load_if_missing = trap_num == 0x189;
let purgeable = trap_num == 0x18A;
self.cascade_dialog_resource_purgeability(
bus,
*b"ALRT",
alert_id,
purgeable,
load_if_missing,
);
bus.write_word(0x0A60, 0);
cpu.write_reg(Register::A7, sp + 2);
Ok(())
}
// ErrorSound ($A98C)
// Sets the error-sound procedure for alerts.
// PROCEDURE ErrorSound(soundProc: ProcPtr);
// Inside Macintosh Volume I, I-411
//
// Per IM:I I-411: "The address of the sound procedure
// being used is stored in the global variable DABeeper."
// ErrorSound replaces the Dialog Manager's standard
// sound procedure (installed by InitDialogs $A97B) with
// the caller's `soundProc`. NIL is documented as "no
// sound (or menu bar blinking) at all" — same encoding
// as a NIL InitDialogs default.
//
// HLE compromise: Systemless doesn't invoke DABeeper from
// any Alert family path (no menu-bar-blink emulation),
// so this trap's only observable side effect is the
// DABeeper global itself. Apps that probe DABeeper
// before/after ErrorSound to detect "did the previous
// app's sound proc get installed?" still see the
// correct value. Future iterations that wire up
// sound-proc invocation from Alert dispatch will pick
// up the stored ProcPtr automatically.
//
// Pop = 4 bytes (soundProc ProcPtr).
// 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.
(true, 0x18C) => {
let sp = cpu.read_reg(Register::A7);
let sound_proc = bus.read_long(sp);
bus.write_long(crate::memory::globals::addr::DA_BEEPER, sound_proc);
cpu.write_reg(Register::A7, sp + 4);
Ok(())
}
// SetDAFont ($A98D variant — actually uses DlgFont low-mem global)
// Sets the font for subsequently created dialogs and alerts.
// PROCEDURE SetDAFont(fontNum: INTEGER); [Not in ROM]
// Inside Macintosh Volume I, I-412
// Assembly-language: sets DlgFont ($0AFA) directly.
//
// Note: $A98D is GetDItem; SetDAFont is not a ROM trap but a
// glue routine that writes DlgFont. We handle it via the
// dedicated trap word $A97F (Pack13 slot repurposed).
// If apps call it directly, they just write DlgFont.
// SetDialogItemText ($A98F)
// Sets the text of a dialog item (statText or editText).
// PROCEDURE SetDialogItemText(item: Handle; text: Str255);
// Inside Macintosh Volume I, I-422; Macintosh Toolbox Essentials 1992, 6-131
(true, 0x18F) => {
let sp = cpu.read_reg(Register::A7);
let pc = cpu.read_reg(Register::PC);
let text_str_ptr = bus.read_long(sp);
let item_handle = bus.read_long(sp + 4);
let mut redraw_text_item = None;
if text_str_ptr != 0 {
let bytes = bus.read_pstring(text_str_ptr);
let len = bytes.len();
let text = decode_mac_roman_for_render(&bytes);
if item_handle != 0 {
let data_ptr = Self::ensure_text_handle_size(bus, item_handle, len);
if data_ptr != 0 {
bus.write_bytes(data_ptr, &bytes);
}
}
// Also update the item in dialog_items so ModalDialog picks it up
if let Some((dlg_ptr, idx)) =
self.dialog_item_handles.get(&item_handle).copied()
{
let item_no = (idx + 1) as i16;
if trace_dialog_items_enabled() {
eprintln!(
"[DIALOG-ITEM] SetDialogItemText pc=${:08X} dialog=${:08X} item={} handle=${:08X} text={:?}",
pc,
dlg_ptr,
item_no,
item_handle,
text
);
}
if let Some(items) = self.dialog_items.get_mut(&dlg_ptr) {
if idx < items.len() {
items[idx].text = text.clone();
}
}
let mut refresh_tracking = false;
if let Some(tracking) = self.dialog_tracking.as_mut() {
if tracking.dialog_ptr == dlg_ptr && idx < tracking.items.len() {
tracking.items[idx].text = text.clone();
if tracking.edit_item == item_no {
tracking.edit_text = text;
tracking.edit_text_modified = false;
}
refresh_tracking = !tracking.game_managed;
}
}
if refresh_tracking {
self.refresh_dialog_tracking_snapshot(bus);
} else {
redraw_text_item = Some((dlg_ptr, item_no));
}
} else if trace_dialog_items_enabled() {
eprintln!(
"[DIALOG-ITEM] SetDialogItemText pc=${:08X} dialog=<unknown> handle=${:08X} text={:?}",
pc,
item_handle,
text
);
}
}
if let Some((dlg_ptr, item_no)) = redraw_text_item {
self.redraw_dialog_text_item(bus, dlg_ptr, item_no);
}
cpu.write_reg(Register::A7, sp + 8);
Ok(())
}
// GetDialogItemText ($A990)
// Returns the text of a dialog item (statText or editText).
// PROCEDURE GetDialogItemText(item: Handle; VAR text: Str255);
// Inside Macintosh Volume I, I-422
(true, 0x190) => {
let sp = cpu.read_reg(Register::A7);
let text_ptr = bus.read_long(sp);
let item_handle = bus.read_long(sp + 4);
if text_ptr != 0 {
// If dialog tracking is active, return the current edit text
let mut wrote = false;
if let Some(ref tracking) = self.dialog_tracking {
let current_edit_handle =
self.dialog_item_handles.get(&item_handle).copied().filter(
|(dlg_ptr, idx)| {
*dlg_ptr == tracking.dialog_ptr
&& (*idx as i16 + 1) == tracking.edit_item
},
);
if current_edit_handle.is_some() {
let bytes = tracking.edit_text.as_bytes();
let len = bytes.len().min(255);
bus.write_byte(text_ptr, len as u8);
for (i, byte) in bytes.iter().take(len).enumerate() {
bus.write_byte(text_ptr + 1 + i as u32, *byte);
}
wrote = true;
}
}
if !wrote {
// Text item handles store raw bytes, not a Pascal-length byte.
// Inside Macintosh Volume I, I-422; Executor dialManip.cpp
if item_handle != 0 {
let master = bus.read_long(item_handle);
if master != 0 {
let len = bus.get_alloc_size(master).unwrap_or(0).min(255) as usize;
bus.write_byte(text_ptr, len as u8);
for i in 0..len {
bus.write_byte(
text_ptr + 1 + i as u32,
bus.read_byte(master + i as u32),
);
}
wrote = true;
}
}
if !wrote {
bus.write_byte(text_ptr, 0);
}
}
}
cpu.write_reg(Register::A7, sp + 8);
Ok(())
}
// DialogDispatch ($AA68)
// Selector-based dispatch for extended Dialog Manager routines.
// Selector is in D0 (not on stack) per the MPW THREEWORDINLINE
// sequence: MOVE.W #selector, D0 / _DialogDispatch. The low
// byte is the routine number; the high byte encodes param-
// bytes / 2 which the dispatcher uses to pop args.
// Macintosh Toolbox Essentials 1992, 6-162
// 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.
(true, 0x268) => {
let sp = cpu.read_reg(Register::A7);
let d0 = cpu.read_reg(Register::D0) as u16;
let param_bytes = (((d0 >> 8) & 0xFF) as u32) * 2;
let routine = d0 & 0xFF;
match routine {
// NewFeaturesDialog (selector $0C, param_bytes=34)
// FUNCTION NewFeaturesDialog(inStorage: Ptr;
// inBoundsRect: Rect; inTitle: ConstStr255Param;
// inIsVisible: Boolean; inProcID: SInt16;
// inBehind: WindowPtr; inGoAwayFlag: Boolean;
// inRefCon: SInt32; inItems: Handle;
// inFeatures: UInt32): DialogPtr;
// Mac Toolbox: Appearance Manager (Apple, 1997).
//
// Same shape as NewCDialog with one extra LongInt
// for Appearance feature flags. Mid-90s titles
// (e.g. Meteor Storm) call this unconditionally —
// even when Gestalt('appr') reports no features —
// to allocate their main dialog. Delegate to the
// existing NewCDialog path so the dialog is created
// without the Appearance theming, then drop the
// unused inFeatures word.
0x0C => {
// Stack (low to high), 34 bytes of params + 4 result:
// SP+0: inFeatures(4)
// SP+4: inItems(4)
// SP+8: inRefCon(4)
// SP+12: inGoAwayFlag(2)
// SP+14: inBehind(4)
// SP+18: inProcID(2)
// SP+20: inIsVisible(2)
// SP+22: inTitle(4)
// SP+26: inBoundsRect(4)
// SP+30: inStorage(4)
// SP+34: result DialogPtr(4)
let _features = bus.read_long(sp);
let items_handle = bus.read_long(sp + 4);
let ref_con = bus.read_long(sp + 8);
let go_away_flag = bus.read_byte(sp + 12) != 0;
let behind = bus.read_long(sp + 14);
let proc_id = bus.read_word(sp + 18) as i16;
let visible = bus.read_byte(sp + 20) != 0;
let title_ptr = bus.read_long(sp + 22);
let bounds_rect_ptr = bus.read_long(sp + 26);
let storage_ptr = bus.read_long(sp + 30);
let bounds = if bounds_rect_ptr != 0 {
(
bus.read_word(bounds_rect_ptr) as i16,
bus.read_word(bounds_rect_ptr + 2) as i16,
bus.read_word(bounds_rect_ptr + 4) as i16,
bus.read_word(bounds_rect_ptr + 6) as i16,
)
} else {
(0, 0, 0, 0)
};
let title = if title_ptr != 0 {
decode_mac_roman_for_render(&bus.read_pstring(title_ptr))
} else {
String::new()
};
let items_ptr = if items_handle != 0 {
bus.read_long(items_handle)
} else {
0
};
let items_len = if items_ptr != 0 {
bus.get_alloc_size(items_ptr).unwrap_or(0)
} else {
0
};
let items = if items_ptr != 0 && items_len != 0 {
Self::parse_ditl(bus, items_ptr, items_len)
} else {
Vec::new()
};
let dlg_ptr = self.finish_dialog_creation(
bus,
cpu,
storage_ptr,
bounds,
&title,
visible,
proc_id,
go_away_flag,
ref_con,
items_handle,
items,
);
self.apply_behind_parameter(bus, dlg_ptr, behind);
bus.write_long(sp + param_bytes, dlg_ptr);
cpu.write_reg(Register::A7, sp + param_bytes);
}
// GetStdFilterProc (selector 3, param_bytes=4)
// FUNCTION GetStdFilterProc(VAR theProc: ProcPtr): OSErr;
// Macintosh Toolbox Essentials 1992, 6-163
//
// Returns a ProcPtr to the system's standard
// event filter for modal dialogs. The public
// Dialogs.h / Dialogs.p declarations expose
// GetStdFilterProc as returning a ModalFilterUPP
// through a VAR/out pointer. MTb 1992 documents
// that NIL filterProc passed to ModalDialog uses
// the standard filter, and BasiliskII returns a
// non-NIL callable proc here.
//
// HLE compromise: return a tiny guest-resident
// Pascal FUNCTION shim that always returns FALSE:
//
// JMP shimBody ; recognizable callback entry for
// runner-side filter-proc firing
// MOVEQ #0,D0 ; Boolean result = FALSE in D0 too
// CLR.W 16(SP) ; Boolean result = FALSE
// RTD #12 ; pop dialog/event/itemHit args
//
// This makes the proc SAFE and callable for guest
// code (better than the older NIL placeholder) while
// still preserving Systemless's own ModalDialog default
// handling for Return/Escape when apps pass the proc
// straight back to ModalDialog.
//
// NIL VAR ptr (the caller passed NULL for theProc)
// is a defensive no-op — the impl skips the write
// rather than dereffing NIL.
0x03 => {
let proc_ptr = bus.read_long(sp);
let shim = if self.dialog_std_filter_proc != 0 {
self.dialog_std_filter_proc
} else {
let shim = bus.alloc(16);
bus.write_word(shim, 0x4EF9); // JMP abs.L shimBody
bus.write_long(shim + 2, shim + 6);
// Pascal FUNCTION ModalFilterProc(dialog, event, itemHit): Boolean
// entry stack layout:
// +0 return address
// +4 itemHit ptr
// +8 EventRecord ptr
// +12 DialogPtr
// +16 Boolean result slot
bus.write_word(shim + 6, 0x7000); // MOVEQ #0, D0
bus.write_word(shim + 8, 0x426F); // CLR.W 16(SP)
bus.write_word(shim + 10, 0x0010);
bus.write_word(shim + 12, 0x4E74); // RTD #12
bus.write_word(shim + 14, 0x000C);
self.dialog_std_filter_proc = shim;
shim
};
if proc_ptr != 0 {
bus.write_long(proc_ptr, shim);
}
// pop params; leave result in place
bus.write_word(sp + param_bytes, 0); // noErr
cpu.write_reg(Register::A7, sp + param_bytes);
}
// SetDialogDefaultItem (selector 4, param_bytes=6)
// FUNCTION SetDialogDefaultItem(theDialog: DialogPtr;
// newItem: INTEGER): OSErr;
// Macintosh Toolbox Essentials 1992, 6-164
//
// Stack: SP+0=newItem(2), SP+2=theDialog(4),
// SP+6=result OSErr slot (caller pre-pushed).
//
// Marks `newItem` as the dialog's default
// button — pressed when user hits Return per
// IM:MTb 6-164. Stored in DialogRecord.aDefItem
// at offset 168 so subsequent ModalDialog
// redraws can outline the default button. If
// dialog tracking is active for this dialog,
// mirror the value into DialogTrackingState
// .default_item so the active redraw path
// sees it without re-reading guest memory.
0x04 => {
let new_item = bus.read_word(sp) as i16;
let dialog_ptr = bus.read_long(sp + 2);
if dialog_ptr != 0 {
// DialogRecord.aDefItem is at offset 168
// per the existing GetNewDialog impl
// (dialog.rs:1961 writes "aDefItem" here).
bus.write_word(dialog_ptr + 168, new_item as u16);
// Mirror into active tracking state if
// this dialog is currently being tracked.
if let Some(tracking) = self.dialog_tracking.as_mut() {
if tracking.dialog_ptr == dialog_ptr {
tracking.default_item = new_item;
}
}
}
bus.write_word(sp + param_bytes, 0); // noErr
cpu.write_reg(Register::A7, sp + param_bytes);
}
// SetDialogCancelItem (selector 5, param_bytes=6)
// FUNCTION SetDialogCancelItem(theDialog: DialogPtr;
// newItem: INTEGER): OSErr;
// Macintosh Toolbox Essentials 1992, 6-165
//
// System 7 addition (no canonical DialogRecord
// field — Apple added cancel-item tracking to
// the Dialog Manager AFTER the original
// DialogRecord layout was finalized). Stored in
// a host-side per-dialog map and mirrored into
// active tracking state when ModalDialog is
// already running. NIL theDialog is a defensive
// no-op.
0x05 => {
let new_item = bus.read_word(sp) as i16;
let dialog_ptr = bus.read_long(sp + 2);
if dialog_ptr != 0 {
self.dialog_cancel_items.insert(dialog_ptr, new_item);
if let Some(tracking) = self.dialog_tracking.as_mut() {
if tracking.dialog_ptr == dialog_ptr {
tracking.cancel_item = new_item;
}
}
}
bus.write_word(sp + param_bytes, 0); // noErr
cpu.write_reg(Register::A7, sp + param_bytes);
}
// SetDialogTracksCursor (selector 6, param_bytes=6)
// FUNCTION SetDialogTracksCursor(theDialog: DialogPtr;
// tracks: BOOLEAN): OSErr;
// Macintosh Toolbox Essentials 1992, 6-166
//
// System 7 addition that controls whether the
// Dialog Manager auto-changes the cursor (to
// an I-beam) when the mouse is over an
// editText item. HLE compromise: Systemless
// doesn't model interactive cursor tracking
// (no continuous mouse-poll stream from the
// scripted event source), so the trap is a
// no-op noErr. Apps that defensively call this
// at dialog setup time get noErr and proceed.
0x06 => {
bus.write_word(sp + param_bytes, 0); // noErr
cpu.write_reg(Register::A7, sp + param_bytes);
}
_ => {
eprintln!(
"[TRAP] DialogDispatch unknown routine=${:02X} d0=${:04X}",
routine, d0
);
cpu.write_reg(Register::A7, sp + param_bytes);
}
}
Ok(())
}
_ => return None,
})
}
}
#[cfg(test)]
mod tests {
use super::super::test_helpers::{setup, setup_with_port, MockCpu, TEST_SP};
use crate::cpu::{CpuOps, Register};
use crate::memory::{MacMemoryBus, MemoryBus};
use crate::trap::dispatch::{
DialogButtonTrackingState, DialogItem, DialogPopupDraw, DialogTrackingState,
PendingDialogPopupMenu, PersistentDialogSnapshot, RetainedModalDialogClickState,
};
use crate::trap::menu::{Menu, MenuItem};
use crate::trap::TrapDispatcher;
use crate::ui_theme::UiThemeId;
use std::collections::VecDeque;
fn screen_pixel_is_set(bus: &MacMemoryBus, base: u32, row_bytes: u32, x: i16, y: i16) -> bool {
let byte = bus.read_byte(base + (y as u32 * row_bytes) + ((x as u32) / 8));
byte & (0x80u8 >> ((x as u8) & 7)) != 0
}
fn count_set_pixels(
bus: &MacMemoryBus,
base: u32,
row_bytes: u32,
top: i16,
left: i16,
bottom: i16,
right: i16,
) -> u32 {
let mut count = 0;
for y in top..bottom {
for x in left..right {
if screen_pixel_is_set(bus, base, row_bytes, x, y) {
count += 1;
}
}
}
count
}
fn count_pixel_index(
bus: &MacMemoryBus,
base: u32,
row_bytes: u32,
top: i16,
left: i16,
bottom: i16,
right: i16,
pixel_index: u8,
) -> u32 {
let mut count = 0;
for y in top..bottom {
for x in left..right {
if bus.read_byte(base + y as u32 * row_bytes + x as u32) == pixel_index {
count += 1;
}
}
}
count
}
fn sum_screen_bytes(
bus: &MacMemoryBus,
base: u32,
row_bytes: u32,
top: i16,
left: i16,
bottom: i16,
right: i16,
) -> u64 {
let mut sum = 0;
for y in top..bottom {
let row = base + (y as u32 * row_bytes);
for x in left..right {
sum += u64::from(bus.read_byte(row + x as u32));
}
}
sum
}
fn make_style_scrap(
bus: &mut MacMemoryBus,
styles: &[(u32, i16, i16, i16, i16, i16, (u16, u16, u16))],
) -> u32 {
let handle = TrapDispatcher::allocate_handle_with_data(
bus,
TrapDispatcher::SCRAP_STYLE_TAB_OFFSET
+ (styles.len() as u32 * TrapDispatcher::SCRAP_STYLE_ELEMENT_SIZE),
);
let ptr = bus.read_long(handle);
bus.write_word(
ptr + TrapDispatcher::SCRAP_N_STYLES_OFFSET,
styles.len() as u16,
);
for (index, (start, height, ascent, font, face, size, color)) in styles.iter().enumerate() {
let base = ptr
+ TrapDispatcher::SCRAP_STYLE_TAB_OFFSET
+ (index as u32 * TrapDispatcher::SCRAP_STYLE_ELEMENT_SIZE);
bus.write_long(base + TrapDispatcher::SCRAP_STYLE_START_CHAR_OFFSET, *start);
bus.write_word(
base + TrapDispatcher::SCRAP_STYLE_HEIGHT_OFFSET,
*height as u16,
);
bus.write_word(
base + TrapDispatcher::SCRAP_STYLE_ASCENT_OFFSET,
*ascent as u16,
);
bus.write_word(base + TrapDispatcher::SCRAP_STYLE_FONT_OFFSET, *font as u16);
bus.write_word(base + TrapDispatcher::SCRAP_STYLE_FACE_OFFSET, *face as u16);
bus.write_word(base + TrapDispatcher::SCRAP_STYLE_SIZE_OFFSET, *size as u16);
bus.write_word(base + TrapDispatcher::SCRAP_STYLE_COLOR_OFFSET, color.0);
bus.write_word(base + TrapDispatcher::SCRAP_STYLE_COLOR_OFFSET + 2, color.1);
bus.write_word(base + TrapDispatcher::SCRAP_STYLE_COLOR_OFFSET + 4, color.2);
}
handle
}
// ---- InitDialogs ($A97B) ----
#[test]
fn init_dialogs_pops_4_bytes() {
let (mut disp, mut cpu, mut bus) = setup();
// SP+0: ptr (4 bytes)
bus.write_long(TEST_SP, 0x00000000);
let result = disp.dispatch_dialog(true, 0x17B, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
}
#[test]
fn dialog_edit_state_falls_back_to_first_valid_edit_text_item() {
let (_disp, _cpu, mut bus) = setup();
let dialog_ptr = bus.alloc(170);
bus.write_word(dialog_ptr + 164, 0); // stale/default editField points at item 1
bus.write_word(dialog_ptr + 168, 1); // default OK button
let items = vec![
DialogItem {
item_type: 4,
text: "OK".to_string(),
..Default::default()
},
DialogItem {
item_type: 4,
text: "Cancel".to_string(),
..Default::default()
},
DialogItem {
item_type: 16,
text: "answer".to_string(),
..Default::default()
},
];
let (text, edit_item, default_item) =
TrapDispatcher::dialog_edit_state(&bus, dialog_ptr, &items);
assert_eq!(text, "answer");
assert_eq!(edit_item, 3);
assert_eq!(default_item, 1);
}
#[test]
fn dialog_edit_state_empty_live_handle_does_not_fallback_to_template_text() {
let (_disp, _cpu, mut bus) = setup();
let dialog_ptr = bus.alloc(170);
let items_handle = bus.alloc(4);
let ditl_ptr = bus.alloc(16);
let text_handle = bus.alloc(4);
bus.write_long(text_handle, 0); // valid empty text handle
bus.write_long(items_handle, ditl_ptr);
bus.write_long(dialog_ptr + 156, items_handle);
bus.write_word(dialog_ptr + 164, 0); // item 1 is the active edit field
bus.write_word(dialog_ptr + 168, 1);
bus.write_word(ditl_ptr, 0);
bus.write_long(ditl_ptr + 2, text_handle);
bus.write_word(ditl_ptr + 6, 10);
bus.write_word(ditl_ptr + 8, 10);
bus.write_word(ditl_ptr + 10, 26);
bus.write_word(ditl_ptr + 12, 120);
bus.write_byte(ditl_ptr + 14, 16);
bus.write_byte(ditl_ptr + 15, 0);
let items = vec![DialogItem {
item_type: 16,
rect: (10, 10, 26, 120),
text: "Edit Text".to_string(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
}];
let (text, edit_item, default_item) =
TrapDispatcher::dialog_edit_state(&bus, dialog_ptr, &items);
assert_eq!(text, "");
assert_eq!(edit_item, 1);
assert_eq!(default_item, 1);
}
#[test]
fn parse_dlog_ignores_position_word_when_resource_is_classic_length() {
let (_disp, _cpu, mut bus) = setup();
let ptr = bus.alloc(24);
bus.write_word(ptr, 228); // top
bus.write_word(ptr + 2, 198); // left
bus.write_word(ptr + 4, 372); // bottom
bus.write_word(ptr + 6, 455); // right
bus.write_word(ptr + 8, 2); // procID
bus.write_byte(ptr + 10, 1); // visible
bus.write_byte(ptr + 12, 0); // goAway
bus.write_long(ptr + 14, 0); // refCon
bus.write_word(ptr + 18, 1013); // itemsID
bus.write_byte(ptr + 20, 0); // empty title
bus.write_byte(ptr + 21, 0); // padding only, not a position field
bus.write_word(ptr + 22, 0x280A); // poison: centerMainScreen if over-read
let (bounds, proc_id, visible, items_id, title, position) =
TrapDispatcher::parse_dlog(&bus, ptr, 22);
assert_eq!(bounds, (228, 198, 372, 455));
assert_eq!(proc_id, 2);
assert!(visible);
assert_eq!(items_id, 1013);
assert_eq!(title, "");
assert_eq!(position, 0);
}
#[test]
fn parse_dlog_reads_position_word_when_resource_contains_it() {
let (_disp, _cpu, mut bus) = setup();
let ptr = bus.alloc(24);
bus.write_word(ptr, 10);
bus.write_word(ptr + 2, 20);
bus.write_word(ptr + 4, 110);
bus.write_word(ptr + 6, 220);
bus.write_word(ptr + 8, 2);
bus.write_byte(ptr + 10, 1);
bus.write_long(ptr + 14, 0);
bus.write_word(ptr + 18, 42);
bus.write_byte(ptr + 20, 0);
bus.write_byte(ptr + 21, 0);
bus.write_word(ptr + 22, 0x280A);
let (_, _, _, _, _, position) = TrapDispatcher::parse_dlog(&bus, ptr, 24);
assert_eq!(position, 0x280A);
}
#[test]
fn dlog_parse_title_alignment_even_and_odd() {
let (_disp, _cpu, mut bus) = setup();
let odd = build_test_dlog_with_title((10, 20, 110, 220), 701, "Odd", 0x300A);
let even = build_test_dlog_with_title((11, 21, 111, 221), 702, "Even", 0x700A);
let odd_ptr = bus.alloc(odd.len() as u32);
bus.write_bytes(odd_ptr, &odd);
let even_ptr = bus.alloc(even.len() as u32);
bus.write_bytes(even_ptr, &even);
// MTE 1992 p. 6-148: the optional position word follows the Pascal
// title string after a 0-or-1-byte alignment pad.
let (odd_bounds, _, _, odd_items, odd_title, odd_position) =
TrapDispatcher::parse_dlog(&bus, odd_ptr, odd.len() as u32);
let (even_bounds, _, _, even_items, even_title, even_position) =
TrapDispatcher::parse_dlog(&bus, even_ptr, even.len() as u32);
assert_eq!(odd_bounds, (10, 20, 110, 220));
assert_eq!(odd_items, 701);
assert_eq!(odd_title, "Odd");
assert_eq!(odd_position, 0x300A);
assert_eq!(odd.len(), 26);
assert_eq!(even_bounds, (11, 21, 111, 221));
assert_eq!(even_items, 702);
assert_eq!(even_title, "Even");
assert_eq!(even_position, 0x700A);
assert_eq!(even.len(), 28);
}
#[test]
fn dlog_parse_position_constants() {
let (_disp, _cpu, mut bus) = setup();
// MTE 1992 p. 6-148 documents the compiled DLOG position constants
// stored after the aligned title string.
for position in [0x0000u16, 0xB00A, 0x300A, 0x700A] {
let dlog = build_test_dlog_with_title((10, 20, 110, 220), 711, "P", position);
let ptr = bus.alloc(dlog.len() as u32);
bus.write_bytes(ptr, &dlog);
let (_, _, _, _, _, parsed_position) =
TrapDispatcher::parse_dlog(&bus, ptr, dlog.len() as u32);
assert_eq!(parsed_position, position);
}
}
#[test]
fn alrt_parse_stage_nibbles_and_position() {
let (_disp, _cpu, mut bus) = setup();
let mut alrt = build_alrt_template(-321, 0xF721);
alrt.extend_from_slice(&0xB00Au16.to_be_bytes());
let ptr = bus.alloc(alrt.len() as u32);
bus.write_bytes(ptr, &alrt);
let (bounds, items_id, stages, position) =
TrapDispatcher::parse_alrt(&bus, ptr, alrt.len() as u32);
assert_eq!(bounds, (0, 0, 80, 200));
assert_eq!(items_id, -321);
assert_eq!(stages, 0xF721);
// IM:I I-425 to I-426: low nibble is stage 1, high nibble is stage 4.
assert_eq!(stages & 0x000F, 0x0001);
assert_eq!((stages >> 4) & 0x000F, 0x0002);
assert_eq!((stages >> 8) & 0x000F, 0x0007);
assert_eq!((stages >> 12) & 0x000F, 0x000F);
assert_eq!(position, 0xB00A);
let classic_alrt = build_alrt_template(123, 0x0008);
let classic_ptr = bus.alloc(classic_alrt.len() as u32);
bus.write_bytes(classic_ptr, &classic_alrt);
let (_, classic_items_id, classic_stages, classic_position) =
TrapDispatcher::parse_alrt(&bus, classic_ptr, classic_alrt.len() as u32);
assert_eq!(classic_items_id, 123);
assert_eq!(classic_stages, 0x0008);
assert_eq!(classic_position, 0);
}
fn push_ditl_count(data: &mut Vec<u8>, count_minus_one: i16) {
data.extend_from_slice(&count_minus_one.to_be_bytes());
}
fn push_ditl_item(
data: &mut Vec<u8>,
item_handle: u32,
rect: (i16, i16, i16, i16),
item_type: u8,
length_or_reserved: u8,
payload: &[u8],
) {
data.extend_from_slice(&item_handle.to_be_bytes());
data.extend_from_slice(&rect.0.to_be_bytes());
data.extend_from_slice(&rect.1.to_be_bytes());
data.extend_from_slice(&rect.2.to_be_bytes());
data.extend_from_slice(&rect.3.to_be_bytes());
data.push(item_type);
data.push(length_or_reserved);
data.extend_from_slice(payload);
if payload.len() % 2 != 0 {
data.push(0);
}
}
fn parse_ditl_bytes(bus: &mut MacMemoryBus, data: &[u8]) -> Vec<DialogItem> {
let ptr = bus.alloc(data.len() as u32);
bus.write_bytes(ptr, data);
TrapDispatcher::parse_ditl(bus, ptr, data.len() as u32)
}
#[test]
fn ditl_iter_empty_count_minus_one() {
let (_disp, _cpu, mut bus) = setup();
let mut ditl = Vec::new();
// IM:I I-427: the first word stores the number of items minus 1.
push_ditl_count(&mut ditl, -1);
let items = parse_ditl_bytes(&mut bus, &ditl);
assert!(items.is_empty());
}
#[test]
fn ditl_iter_button_checkbox_radio_static_edit_text() {
let (_disp, _cpu, mut bus) = setup();
let mut ditl = Vec::new();
push_ditl_count(&mut ditl, 4);
for (item_type, rect, text) in [
(4, (1, 2, 11, 42), "OK"),
(5, (12, 2, 22, 72), "Check"),
(6, (23, 2, 33, 72), "Radio"),
(8, (34, 2, 44, 92), "Static"),
(16, (45, 2, 57, 122), "Edit"),
] {
push_ditl_item(
&mut ditl,
0,
rect,
item_type,
text.len() as u8,
text.as_bytes(),
);
}
let items = parse_ditl_bytes(&mut bus, &ditl);
assert_eq!(items.len(), 5);
assert_eq!(items[0].item_type, 4);
assert_eq!(items[0].rect, (1, 2, 11, 42));
assert_eq!(items[0].text, "OK");
assert_eq!(items[1].item_type, 5);
assert_eq!(items[1].text, "Check");
assert_eq!(items[2].item_type, 6);
assert_eq!(items[2].text, "Radio");
assert_eq!(items[3].item_type, 8);
assert_eq!(items[3].text, "Static");
assert_eq!(items[4].item_type, 16);
assert_eq!(items[4].rect, (45, 2, 57, 122));
assert_eq!(items[4].text, "Edit");
}
#[test]
fn ditl_iter_control_icon_picture_resource_id() {
let (_disp, _cpu, mut bus) = setup();
let mut ditl = Vec::new();
push_ditl_count(&mut ditl, 2);
// MTE 1992 p. 6-153 documents resource-backed DITL items as a
// reserved byte followed by a signed 16-bit resource ID.
push_ditl_item(&mut ditl, 0, (1, 2, 11, 42), 7, 0, &128i16.to_be_bytes());
// IM:I I-427 describes the same slot as a length byte of 2.
push_ditl_item(
&mut ditl,
0,
(12, 2, 28, 34),
32,
2,
&(-42i16).to_be_bytes(),
);
push_ditl_item(&mut ditl, 0, (29, 2, 70, 90), 64, 0, &300i16.to_be_bytes());
let items = parse_ditl_bytes(&mut bus, &ditl);
assert_eq!(items.len(), 3);
assert_eq!(items[0].item_type, 7);
assert_eq!(items[0].resource_id, 128);
assert_eq!(items[1].item_type, 32);
assert_eq!(items[1].resource_id, -42);
assert_eq!(items[2].item_type, 64);
assert_eq!(items[2].resource_id, 300);
}
#[test]
fn ditl_iter_user_item_without_data_bytes() {
let (_disp, _cpu, mut bus) = setup();
let mut ditl = Vec::new();
push_ditl_count(&mut ditl, 0);
push_ditl_item(&mut ditl, 0x12345678, (10, 20, 30, 40), 0, 0, &[]);
let items = parse_ditl_bytes(&mut bus, &ditl);
assert_eq!(items.len(), 1);
assert_eq!(items[0].item_type, 0);
assert_eq!(items[0].rect, (10, 20, 30, 40));
assert_eq!(items[0].proc_ptr, 0x12345678);
}
#[test]
fn ditl_iter_help_item_sized_payload() {
let (_disp, _cpu, mut bus) = setup();
let mut ditl = Vec::new();
push_ditl_count(&mut ditl, 1);
// MTE 1992 p. 6-154: help items use a 4- or 6-byte payload.
push_ditl_item(
&mut ditl,
0,
(0, 0, 0, 0),
1,
6,
&[0x00, 0x08, 0x01, 0x2C, 0x00, 0x02],
);
push_ditl_item(&mut ditl, 0, (5, 6, 17, 50), 4, 2, b"OK");
let items = parse_ditl_bytes(&mut bus, &ditl);
assert_eq!(items.len(), 2);
assert_eq!(items[0].item_type, 1);
assert_eq!(items[1].item_type, 4);
assert_eq!(items[1].text, "OK");
}
#[test]
fn ditl_iter_odd_pascal_text_alignment() {
let (_disp, _cpu, mut bus) = setup();
let mut ditl = Vec::new();
push_ditl_count(&mut ditl, 1);
push_ditl_item(&mut ditl, 0, (1, 2, 11, 42), 8, 3, b"Odd");
push_ditl_item(&mut ditl, 0, (12, 2, 22, 42), 4, 2, b"OK");
let items = parse_ditl_bytes(&mut bus, &ditl);
assert_eq!(items.len(), 2);
assert_eq!(items[0].text, "Odd");
assert_eq!(items[1].rect, (12, 2, 22, 42));
assert_eq!(items[1].text, "OK");
}
#[test]
fn ditl_iter_even_pascal_text_alignment() {
let (_disp, _cpu, mut bus) = setup();
let mut ditl = Vec::new();
push_ditl_count(&mut ditl, 1);
push_ditl_item(&mut ditl, 0, (1, 2, 11, 42), 8, 4, b"Even");
push_ditl_item(&mut ditl, 0, (12, 2, 22, 42), 4, 2, b"OK");
let items = parse_ditl_bytes(&mut bus, &ditl);
assert_eq!(items.len(), 2);
assert_eq!(items[0].text, "Even");
assert_eq!(items[1].rect, (12, 2, 22, 42));
assert_eq!(items[1].text, "OK");
}
#[test]
fn ditl_iter_rejects_or_stops_at_truncated_item() {
let (_disp, _cpu, mut bus) = setup();
let mut ditl = Vec::new();
push_ditl_count(&mut ditl, 1);
push_ditl_item(&mut ditl, 0, (1, 2, 11, 42), 4, 2, b"OK");
ditl.extend_from_slice(&0u32.to_be_bytes());
for coord in [12i16, 2, 22, 72] {
ditl.extend_from_slice(&coord.to_be_bytes());
}
ditl.push(8);
ditl.push(6);
ditl.extend_from_slice(b"Bad");
let items = parse_ditl_bytes(&mut bus, &ditl);
assert_eq!(items.len(), 1);
assert_eq!(items[0].text, "OK");
}
fn build_test_dlog(bounds: (i16, i16, i16, i16), items_id: i16, position: u16) -> Vec<u8> {
build_test_dlog_with_title(bounds, items_id, "", position)
}
fn build_test_dlog_with_title(
bounds: (i16, i16, i16, i16),
items_id: i16,
title: &str,
position: u16,
) -> Vec<u8> {
let mut data = Vec::with_capacity(22 + title.len() + (title.len() & 1));
data.extend_from_slice(&bounds.0.to_be_bytes());
data.extend_from_slice(&bounds.1.to_be_bytes());
data.extend_from_slice(&bounds.2.to_be_bytes());
data.extend_from_slice(&bounds.3.to_be_bytes());
data.extend_from_slice(&2i16.to_be_bytes()); // procID: plainDBox
data.push(0); // invisible; geometry-only tests do not need drawing
data.push(0); // filler
data.push(0); // goAwayFlag
data.push(0); // filler
data.extend_from_slice(&0u32.to_be_bytes()); // refCon
data.extend_from_slice(&items_id.to_be_bytes());
data.push(title.len() as u8);
data.extend_from_slice(title.as_bytes());
if (1 + title.len()) % 2 != 0 {
data.push(0); // title alignment padding
}
data.extend_from_slice(&position.to_be_bytes());
data
}
fn build_test_ditl_item(
item_type: u8,
rect: (i16, i16, i16, i16),
item_data: &[u8],
) -> Vec<u8> {
build_test_ditl_items(&[(item_type, rect, item_data)])
}
fn build_test_ditl_items(items: &[(u8, (i16, i16, i16, i16), &[u8])]) -> Vec<u8> {
let total_len = items.iter().fold(2usize, |len, (_, _, item_data)| {
len + 14 + item_data.len() + (item_data.len() & 1)
});
let mut data = Vec::with_capacity(total_len);
data.extend_from_slice(&((items.len() as u16).saturating_sub(1)).to_be_bytes());
for (item_type, rect, item_data) in items {
append_test_ditl_item(&mut data, *item_type, *rect, item_data);
}
data
}
fn append_test_ditl_item(
data: &mut Vec<u8>,
item_type: u8,
rect: (i16, i16, i16, i16),
item_data: &[u8],
) {
data.extend_from_slice(&0u32.to_be_bytes()); // itmHandle / userItem proc ptr
data.extend_from_slice(&rect.0.to_be_bytes());
data.extend_from_slice(&rect.1.to_be_bytes());
data.extend_from_slice(&rect.2.to_be_bytes());
data.extend_from_slice(&rect.3.to_be_bytes());
data.push(item_type);
data.push(item_data.len() as u8);
data.extend_from_slice(item_data);
if item_data.len() % 2 != 0 {
data.push(0);
}
}
fn build_test_alrt(bounds: (i16, i16, i16, i16), items_id: i16, stages: u16) -> Vec<u8> {
let mut data = Vec::with_capacity(12);
data.extend_from_slice(&bounds.0.to_be_bytes());
data.extend_from_slice(&bounds.1.to_be_bytes());
data.extend_from_slice(&bounds.2.to_be_bytes());
data.extend_from_slice(&bounds.3.to_be_bytes());
data.extend_from_slice(&items_id.to_be_bytes());
data.extend_from_slice(&stages.to_be_bytes());
data
}
#[test]
fn multi_button_alert_tracks_click_and_returns_selected_item() {
// Multi-choice Alert resources are real modal dialogs, not merely
// OK-only notices. Keep the Alert function frame pending until a
// button is selected, then write the selected item number into the
// function result slot at SP+6.
let (mut disp, mut cpu, mut bus) = setup();
let alert_id = 3300;
let ditl_id = 3301;
let alrt = build_test_alrt((100, 100, 210, 400), ditl_id, 0x5555);
let ditl = build_test_ditl_items(&[
(4, (70, 230, 90, 290), b"Choice 1"),
(4, (70, 130, 90, 210), b"Choice 2"),
(4, (70, 20, 90, 100), b"Cancel"),
(
8,
(16, 20, 56, 280),
b"Select one of these options before continuing.",
),
]);
disp.install_test_resource(&mut bus, *b"ALRT", alert_id, &alrt);
disp.install_test_resource(&mut bus, *b"DITL", ditl_id, &ditl);
bus.write_long(TEST_SP, 0); // filterProc
bus.write_word(TEST_SP + 4, alert_id as u16);
bus.write_word(TEST_SP + 6, 0xCAFE);
let result = disp.dispatch_dialog(true, 0x185, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP);
assert_eq!(bus.read_word(TEST_SP + 6), 0);
assert!(disp.dialog_tracking.is_some());
assert!(disp.is_tracking_refire(0xA985));
disp.push_mouse_down(180, 250);
for _ in 0..4 {
let result = disp.dispatch_dialog(true, 0x185, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
if disp
.dialog_tracking
.as_ref()
.and_then(|tracking| tracking.active_button.as_ref())
.is_some()
{
break;
}
}
assert!(disp
.dialog_tracking
.as_ref()
.and_then(|tracking| tracking.active_button.as_ref())
.is_some_and(|button| button.item_no == 2));
disp.push_mouse_up(180, 250);
let result = disp.dispatch_dialog(true, 0x185, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
for _ in 0..64 {
if disp.dialog_tracking.is_none() {
break;
}
let result = disp.dispatch_dialog(true, 0x185, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
}
assert!(disp.dialog_tracking.is_none());
assert_eq!(bus.read_word(TEST_SP + 6), 2);
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
}
fn loaded_resource_handle_for_test(
disp: &TrapDispatcher,
res_type: [u8; 4],
res_id: i16,
) -> u32 {
disp.loaded_handles
.iter()
.find_map(|(&handle, &(_, loaded_type, loaded_id))| {
if loaded_type == res_type && loaded_id == res_id {
Some(handle)
} else {
None
}
})
.unwrap_or_else(|| {
panic!(
"expected loaded {} resource {}",
String::from_utf8_lossy(&res_type),
res_id
)
})
}
fn assert_resource_nonpurgeable(
disp: &TrapDispatcher,
bus: &MacMemoryBus,
res_type: [u8; 4],
res_id: i16,
) {
let handle = loaded_resource_handle_for_test(disp, res_type, res_id);
assert_ne!(
bus.read_long(handle),
0,
"{} {} should have a non-NIL master pointer",
String::from_utf8_lossy(&res_type),
res_id
);
assert_eq!(
disp.handle_state_bits.get(&handle).copied().unwrap_or(0) & 0x40,
0,
"{} {} should be nonpurgeable",
String::from_utf8_lossy(&res_type),
res_id
);
}
fn assert_resource_purgeable(disp: &TrapDispatcher, res_type: [u8; 4], res_id: i16) {
let handle = loaded_resource_handle_for_test(disp, res_type, res_id);
assert_ne!(
disp.handle_state_bits.get(&handle).copied().unwrap_or(0) & 0x40,
0,
"{} {} should be purgeable",
String::from_utf8_lossy(&res_type),
res_id
);
}
fn ditl_item_handle_field(bus: &MacMemoryBus, ditl_ptr: u32, item_no: i16) -> u32 {
if item_no <= 0 || ditl_ptr == 0 {
return 0;
}
let max_index = bus.read_word(ditl_ptr) as i16;
if item_no > max_index + 1 {
return 0;
}
let mut offset = 2u32;
for current_item in 1..=max_index + 1 {
let handle_addr = ditl_ptr + offset;
offset += 4; // itmHandle / userItem ProcPtr
offset += 8; // item display rectangle
let data_len = bus.read_byte(ditl_ptr + offset + 1) as u32;
offset += 2; // item type + data length
let padded = (data_len + 1) & !1;
if current_item == item_no {
return bus.read_long(handle_addr);
}
offset += padded;
}
0
}
// ---- DialogDispatch ($AA68) ----
// Macintosh Toolbox Essentials (1992), pp. 6-162 to 6-167.
#[test]
fn dialogdispatch_getstdfilterproc_selector_03_writes_non_nil_shim_and_returns_noerr() {
let (mut disp, mut cpu, mut bus) = setup();
let proc_storage_ptr = 0x300000u32;
bus.write_long(proc_storage_ptr, 0x00AB_CDEF);
bus.write_long(TEST_SP, proc_storage_ptr); // VAR theProc
bus.write_word(TEST_SP + 4, 0xBEEF); // OSErr result slot
cpu.write_reg(Register::D0, 0x0203); // selector 3, 4 param bytes
let result = disp.dispatch_dialog(true, 0x268, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
let shim = bus.read_long(proc_storage_ptr);
assert_ne!(shim, 0);
assert_eq!(shim, disp.dialog_std_filter_proc);
assert_eq!(bus.read_word(shim), 0x4EF9); // JMP abs.L shimBody
assert_eq!(bus.read_long(shim + 2), shim + 6);
assert_eq!(bus.read_word(shim + 6), 0x7000); // MOVEQ #0, D0
assert_eq!(bus.read_word(shim + 8), 0x426F); // CLR.W 16(SP)
assert_eq!(bus.read_word(shim + 10), 0x0010);
assert_eq!(bus.read_word(shim + 12), 0x4E74); // RTD #12
assert_eq!(bus.read_word(shim + 14), 0x000C);
assert_eq!(bus.read_word(TEST_SP + 4), 0);
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
}
#[test]
fn dialogdispatch_getstdfilterproc_selector_03_reuses_cached_shim() {
let (mut disp, mut cpu, mut bus) = setup();
let slot_a = 0x300000u32;
let slot_b = 0x300004u32;
bus.write_long(TEST_SP, slot_a);
bus.write_word(TEST_SP + 4, 0xBEEF);
cpu.write_reg(Register::D0, 0x0203);
let first = disp.dispatch_dialog(true, 0x268, &mut cpu, &mut bus);
assert!(first.unwrap().is_ok());
let shim_a = bus.read_long(slot_a);
assert_ne!(shim_a, 0);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, slot_b);
bus.write_word(TEST_SP + 4, 0xCAFE);
cpu.write_reg(Register::D0, 0x0203);
let second = disp.dispatch_dialog(true, 0x268, &mut cpu, &mut bus);
assert!(second.unwrap().is_ok());
let shim_b = bus.read_long(slot_b);
assert_eq!(shim_a, shim_b);
assert_eq!(shim_b, disp.dialog_std_filter_proc);
assert_eq!(bus.read_word(shim_b), 0x4EF9);
assert_eq!(bus.read_word(shim_b + 6), 0x7000);
assert_eq!(bus.read_word(TEST_SP + 4), 0);
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
}
#[test]
fn dialogdispatch_setdialogdefaultitem_selector_04_writes_adefitem_and_updates_tracking() {
let (mut disp, mut cpu, mut bus) = setup();
let dialog_ptr = bus.alloc(256);
disp.dialog_tracking = Some(crate::trap::dispatch::DialogTrackingState {
dialog_ptr,
bounds: (0, 0, 0, 0),
title: String::new(),
proc_id: 0,
items: Vec::new(),
default_item: 1,
cancel_item: 2,
edit_text: String::new(),
edit_item: 0,
saved_pixels: Vec::new(),
stack_ptr: TEST_SP,
item_hit_ptr: 0,
rendered_pixels: Vec::new(),
flash_remaining: 0,
flash_delay: 0,
flash_item: 0,
edit_text_modified: false,
draw_proc_queue: VecDeque::new(),
draw_procs_done: true,
rendered_pixels_final: true,
filter_proc: 0,
game_managed: false,
last_filter_event: None,
popup_draws: Vec::new(),
active_popup: None,
active_button: None,
active_user_item: None,
});
bus.write_word(TEST_SP, 9); // newItem
bus.write_long(TEST_SP + 2, dialog_ptr); // theDialog
bus.write_word(TEST_SP + 6, 0xBEEF); // OSErr result slot
cpu.write_reg(Register::D0, 0x0304); // selector 4, 6 param bytes
let result = disp.dispatch_dialog(true, 0x268, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(dialog_ptr + 168), 9);
assert_eq!(
disp.dialog_tracking.as_ref().map(|t| t.default_item),
Some(9)
);
assert_eq!(bus.read_word(TEST_SP + 6), 0);
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
}
#[test]
fn dialogdispatch_setdialogcancelitem_selector_05_updates_tracking_cancel_item_and_returns_noerr(
) {
let (mut disp, mut cpu, mut bus) = setup();
let dialog_ptr = bus.alloc(256);
disp.dialog_tracking = Some(crate::trap::dispatch::DialogTrackingState {
dialog_ptr,
bounds: (0, 0, 0, 0),
title: String::new(),
proc_id: 0,
items: Vec::new(),
default_item: 1,
cancel_item: 2,
edit_text: String::new(),
edit_item: 0,
saved_pixels: Vec::new(),
stack_ptr: TEST_SP,
item_hit_ptr: 0,
rendered_pixels: Vec::new(),
flash_remaining: 0,
flash_delay: 0,
flash_item: 0,
edit_text_modified: false,
draw_proc_queue: VecDeque::new(),
draw_procs_done: true,
rendered_pixels_final: true,
filter_proc: 0,
game_managed: false,
last_filter_event: None,
popup_draws: Vec::new(),
active_popup: None,
active_button: None,
active_user_item: None,
});
bus.write_word(TEST_SP, 7); // newItem
bus.write_long(TEST_SP + 2, dialog_ptr); // theDialog
bus.write_word(TEST_SP + 6, 0xBEEF); // OSErr result slot
cpu.write_reg(Register::D0, 0x0305); // selector 5, 6 param bytes
let result = disp.dispatch_dialog(true, 0x268, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(disp.dialog_cancel_items.get(&dialog_ptr), Some(&7));
assert_eq!(
disp.dialog_tracking.as_ref().map(|t| t.cancel_item),
Some(7)
);
assert_eq!(bus.read_word(TEST_SP + 6), 0);
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
}
#[test]
fn dialogdispatch_modal_dialog_first_entry_honors_preserved_default_and_cancel_items() {
let (mut disp, mut cpu, mut bus) = setup();
let dialog_ptr = 0x200000u32;
let item_hit_ptr = 0x300000u32;
disp.front_window = dialog_ptr;
disp.window_bounds = (92, 95, 240, 320);
disp.window_proc_id = 1;
disp.window_title = "AA68".to_string();
disp.dialog_items.insert(
dialog_ptr,
vec![
DialogItem {
item_type: 4,
rect: (20, 20, 40, 90),
text: "OK".to_string(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
},
DialogItem {
item_type: 4,
rect: (20, 110, 40, 200),
text: "Cancel".to_string(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
},
],
);
bus.write_word(dialog_ptr + 168, 2);
disp.dialog_cancel_items.insert(dialog_ptr, 1);
bus.write_long(TEST_SP, item_hit_ptr);
bus.write_long(TEST_SP + 4, 0);
let result = disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP);
let tracking = disp.dialog_tracking.as_ref().unwrap();
assert_eq!(tracking.default_item, 2);
assert_eq!(tracking.cancel_item, 1);
}
#[test]
fn dialogdispatch_setdialogtrackscursor_selector_06_returns_noerr_and_pops_arguments() {
let (mut disp, mut cpu, mut bus) = setup();
let dialog_ptr = bus.alloc(256);
bus.write_word(TEST_SP, 1); // tracks = TRUE
bus.write_long(TEST_SP + 2, dialog_ptr); // theDialog
bus.write_word(TEST_SP + 6, 0xBEEF); // OSErr result slot
cpu.write_reg(Register::D0, 0x0306); // selector 6, 6 param bytes
let result = disp.dispatch_dialog(true, 0x268, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(TEST_SP + 6), 0);
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
}
// ---- GetNewDialog ($A97C) ----
#[test]
fn get_new_dialog_missing_dlog_sets_reserr_and_returns_nil() {
// MTE 1992 p. 6-114: GetNewDialog returns NIL when the DLOG
// resource can't be read; the failed resource read surfaces through
// Resource Manager ResErr as resNotFound (-192).
let (mut disp, mut cpu, mut bus) = setup();
bus.write_word(0x0A60, 0);
let result = disp.dispatch_dialog(true, 0x17C, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 10);
let dlg_ptr = bus.read_long(TEST_SP + 10);
assert_eq!(
dlg_ptr, 0,
"GetNewDialog must return NIL when DLOG is missing"
);
assert_eq!(bus.read_word(0x0A60) as i16, -192);
assert!(disp.window_list.is_empty());
}
#[test]
fn get_new_dialog_missing_ditl_sets_reserr_and_returns_nil() {
// MTE 1992 p. 6-114: GetNewDialog also returns NIL when the item-list
// resource named by the DLOG cannot be read.
let (mut disp, mut cpu, mut bus) = setup();
let dlog = build_test_dlog((10, 20, 110, 220), 1909, 0);
disp.install_test_resource(&mut bus, *b"DLOG", 1908, &dlog);
bus.write_word(0x0A60, 0);
bus.write_word(TEST_SP + 8, 1908);
let result = disp.dispatch_dialog(true, 0x17C, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 10);
assert_eq!(bus.read_long(TEST_SP + 10), 0);
assert_eq!(bus.read_word(0x0A60) as i16, -192);
assert!(disp.window_list.is_empty());
assert!(disp.dialog_items.is_empty());
}
#[test]
fn get_new_dialog_nil_storage_allocates_low_byte_clean_dialog_record() {
let (mut disp, mut cpu, mut bus) = setup();
let screen_base = bus.alloc((640 * 480) as u32);
bus.write_long(0x0824, screen_base);
disp.screen_mode = (screen_base, 640, 640, 480, 8);
let dlog = build_test_dlog((40, 50, 120, 240), 1911, 0);
let ditl = build_test_ditl_item(4, (50, 80, 70, 140), b"OK");
disp.install_test_resource(&mut bus, *b"DLOG", 1910, &dlog);
disp.install_test_resource(&mut bus, *b"DITL", 1911, &ditl);
let skew = bus.alloc(5);
assert_ne!(
(skew + MacMemoryBus::allocation_bucket_size(5)) & 0xFF,
0,
"test precondition should leave the heap skewed before GetNewDialog"
);
bus.write_long(TEST_SP, 0xFFFF_FFFF); // behind
bus.write_long(TEST_SP + 4, 0); // dStorage = NIL
bus.write_word(TEST_SP + 8, 1910);
bus.write_long(TEST_SP + 10, 0xDEAD_BEEF);
disp.dispatch_dialog(true, 0x17C, &mut cpu, &mut bus)
.unwrap()
.unwrap();
let dialog_ptr = bus.read_long(TEST_SP + 10);
assert_ne!(dialog_ptr, 0);
assert_eq!(
dialog_ptr & 0xFF,
0,
"manager-owned DialogRecord pointers should keep the low byte clear"
);
assert_eq!(
bus.get_alloc_size(dialog_ptr),
Some(170),
"DialogRecord allocation should keep its logical size"
);
}
#[test]
fn get_new_dialog_copies_ditl_not_aliases_resource() {
// IM:I I-403 and MTE 1992 p. 6-114: GetNewDialog reads the DITL
// resource, makes a copy, and uses that copy so several dialogs can
// share identical resource-backed items without aliasing the resource.
let (mut disp, mut cpu, mut bus) = setup();
let screen_base = bus.alloc((640 * 480) as u32);
bus.write_long(0x0824, screen_base);
disp.screen_mode = (screen_base, 640, 640, 480, 8);
let dlog = build_test_dlog((40, 50, 110, 230), 2201, 0);
let ditl = build_test_ditl_item(8, (10, 12, 28, 120), b"Shared");
disp.install_test_resource(&mut bus, *b"DLOG", 2200, &dlog);
let original_ditl_ptr = disp.install_test_resource(&mut bus, *b"DITL", 2201, &ditl);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, 0xFFFF_FFFF); // behind
bus.write_long(TEST_SP + 4, 0); // dStorage
bus.write_word(TEST_SP + 8, 2200); // dialogID
bus.write_long(TEST_SP + 10, 0xDEAD_BEEF);
disp.dispatch_dialog(true, 0x17C, &mut cpu, &mut bus)
.unwrap()
.unwrap();
let first_dialog = bus.read_long(TEST_SP + 10);
let first_ditl_ptr = bus.read_long(bus.read_long(first_dialog + 156));
assert_ne!(first_dialog, 0);
assert_ne!(first_ditl_ptr, 0);
assert_ne!(first_ditl_ptr, original_ditl_ptr);
assert_eq!(ditl_item_handle_field(&bus, original_ditl_ptr, 1), 0);
assert_ne!(ditl_item_handle_field(&bus, first_ditl_ptr, 1), 0);
bus.write_long(first_ditl_ptr + 2, 0xAABB_CCDD);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, 0xFFFF_FFFF); // behind
bus.write_long(TEST_SP + 4, 0); // dStorage
bus.write_word(TEST_SP + 8, 2200); // dialogID
bus.write_long(TEST_SP + 10, 0xDEAD_BEEF);
disp.dispatch_dialog(true, 0x17C, &mut cpu, &mut bus)
.unwrap()
.unwrap();
let second_dialog = bus.read_long(TEST_SP + 10);
let second_ditl_ptr = bus.read_long(bus.read_long(second_dialog + 156));
let second_text_handle = ditl_item_handle_field(&bus, second_ditl_ptr, 1);
assert_ne!(second_dialog, 0);
assert_ne!(second_ditl_ptr, original_ditl_ptr);
assert_ne!(second_ditl_ptr, first_ditl_ptr);
assert_ne!(second_text_handle, 0);
assert_ne!(second_text_handle, 0xAABB_CCDD);
assert_eq!(ditl_item_handle_field(&bus, original_ditl_ptr, 1), 0);
}
#[test]
fn get_new_dialog_rewrites_reserved_item_handles() {
// IM:I I-405: an item list in memory contains item handles for text,
// controls, icons, and pictures. GetNewDialog must rewrite the copied
// DITL's reserved handle fields, not the resource's fields.
let (mut disp, mut cpu, mut bus) = setup();
let screen_base = bus.alloc((640 * 480) as u32);
bus.write_long(0x0824, screen_base);
disp.screen_mode = (screen_base, 640, 640, 480, 8);
let cntl_id = 2301i16.to_be_bytes();
let icon_id = 2302i16.to_be_bytes();
let pict_id = 2303i16.to_be_bytes();
let dlog = build_test_dlog((40, 50, 130, 260), 2300, 0);
let ditl = build_test_ditl_items(&[
(8, (10, 12, 24, 140), b"Static".as_slice()),
(16, (30, 12, 44, 140), b"Edit".as_slice()),
(7, (50, 12, 70, 120), cntl_id.as_slice()),
(32, (10, 150, 42, 182), icon_id.as_slice()),
(64, (48, 150, 80, 220), pict_id.as_slice()),
]);
let mut cntl = Vec::new();
for word in [0i16, 0, 20, 110, 1, -1, 3, 0, 0] {
cntl.extend_from_slice(&(word as u16).to_be_bytes());
}
cntl.extend_from_slice(&0x1234_5678u32.to_be_bytes());
cntl.push(4);
cntl.extend_from_slice(b"Pick");
disp.install_test_resource(&mut bus, *b"DLOG", 2299, &dlog);
disp.install_test_resource(&mut bus, *b"CNTL", 2301, &cntl);
let icon_ptr = disp.install_test_resource(&mut bus, *b"ICON", 2302, &[0xAA; 128]);
let pict_ptr = disp.install_test_resource(&mut bus, *b"PICT", 2303, &[0x11; 32]);
let original_ditl_ptr = disp.install_test_resource(&mut bus, *b"DITL", 2300, &ditl);
bus.write_long(TEST_SP, 0xFFFF_FFFF); // behind
bus.write_long(TEST_SP + 4, 0); // dStorage
bus.write_word(TEST_SP + 8, 2299); // dialogID
disp.dispatch_dialog(true, 0x17C, &mut cpu, &mut bus)
.unwrap()
.unwrap();
let dialog_ptr = bus.read_long(TEST_SP + 10);
let copied_ditl_ptr = bus.read_long(bus.read_long(dialog_ptr + 156));
assert_ne!(dialog_ptr, 0);
assert_ne!(copied_ditl_ptr, original_ditl_ptr);
for item_no in 1..=5 {
assert_eq!(
ditl_item_handle_field(&bus, original_ditl_ptr, item_no),
0,
"resource DITL item {} handle field must stay reserved",
item_no
);
}
let static_handle = ditl_item_handle_field(&bus, copied_ditl_ptr, 1);
let edit_handle = ditl_item_handle_field(&bus, copied_ditl_ptr, 2);
let control_handle = ditl_item_handle_field(&bus, copied_ditl_ptr, 3);
let icon_handle = ditl_item_handle_field(&bus, copied_ditl_ptr, 4);
let pict_handle = ditl_item_handle_field(&bus, copied_ditl_ptr, 5);
assert_eq!(
bus.read_bytes(bus.read_long(static_handle), 6),
b"Static".to_vec()
);
assert_eq!(
bus.read_bytes(bus.read_long(edit_handle), 4),
b"Edit".to_vec()
);
assert_eq!(bus.read_long(bus.read_long(control_handle) + 4), dialog_ptr);
assert_eq!(
disp.dialog_control_handles.get(&control_handle),
Some(&(dialog_ptr, 3))
);
assert_eq!(bus.read_long(icon_handle), icon_ptr);
assert_eq!(bus.read_long(pict_handle), pict_ptr);
}
#[test]
fn get_new_dialog_centers_standard_dlog_with_position_constant() {
let (mut disp, mut cpu, mut bus) = setup();
let screen_base = bus.alloc((800 * 600) as u32);
bus.write_long(0x0824, screen_base);
disp.screen_mode = (screen_base, 800, 800, 600, 8);
let dlog = build_test_dlog((10, 20, 110, 220), 1503, 0x280A);
let ditl = build_test_ditl_item(4, (20, 20, 40, 80), b"OK");
disp.install_test_resource(&mut bus, *b"DLOG", 1502, &dlog);
disp.install_test_resource(&mut bus, *b"DITL", 1503, &ditl);
bus.write_long(TEST_SP, 0); // behind
bus.write_long(TEST_SP + 4, 0); // dStorage
bus.write_word(TEST_SP + 8, 1502); // dialogID
let result = disp.dispatch_dialog(true, 0x17C, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
let dlg_ptr = bus.read_long(TEST_SP + 10);
assert_ne!(dlg_ptr, 0);
assert_eq!(disp.window_bounds, (250, 300, 350, 500));
}
#[test]
fn get_new_dialog_uses_system7_alert_position_near_top() {
// MTE 1992 p. 4-126: alert position leaves about one-fifth of
// the unused vertical screen space above the new window. EVO's
// startup registration DLOG has these dimensions; a one-third
// placement draws it visibly too low compared with BasiliskII.
let (mut disp, mut cpu, mut bus) = setup();
let screen_base = bus.alloc((800 * 600) as u32);
bus.write_long(0x0824, screen_base);
disp.screen_mode = (screen_base, 800, 800, 600, 8);
let dlog = build_test_dlog((40, 40, 310, 530), 1513, 0x300A);
let ditl = build_test_ditl_item(4, (241, 291, 261, 381), b"Not Yet");
disp.install_test_resource(&mut bus, *b"DLOG", 1512, &dlog);
disp.install_test_resource(&mut bus, *b"DITL", 1513, &ditl);
bus.write_long(TEST_SP, 0); // behind
bus.write_long(TEST_SP + 4, 0); // dStorage
bus.write_word(TEST_SP + 8, 1512); // dialogID
let result = disp.dispatch_dialog(true, 0x17C, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
let dlg_ptr = bus.read_long(TEST_SP + 10);
assert_ne!(dlg_ptr, 0);
assert_eq!(disp.window_bounds, (66, 155, 336, 645));
}
#[test]
fn get_new_dialog_draws_visible_shell_with_unresolved_user_item_proc() {
// Visible dialogs appear immediately on a real Mac. userItem contents
// are application-owned and may be installed later with SetDItem, but
// the dialog shell and standard items must not be suppressed.
// Inside Macintosh Volume I, I-405, I-412, I-421.
let (mut disp, mut cpu, mut bus) = setup();
let screen_base = bus.alloc((800 * 600) as u32);
bus.write_bytes(screen_base, &vec![0x77; 800 * 600]);
bus.write_long(0x0824, screen_base);
disp.screen_mode = (screen_base, 800, 800, 600, 8);
let mut dlog = build_test_dlog((100, 100, 180, 260), 1701, 0);
dlog[10] = 1; // visible
let ditl = build_test_ditl_items(&[
(0x80, (8, 8, 30, 80), b"".as_slice()),
(4, (44, 20, 64, 90), b"OK".as_slice()),
]);
disp.install_test_resource(&mut bus, *b"DLOG", 1700, &dlog);
disp.install_test_resource(&mut bus, *b"DITL", 1701, &ditl);
bus.write_long(TEST_SP, 0xFFFF_FFFF); // behind
bus.write_long(TEST_SP + 4, 0); // dStorage
bus.write_word(TEST_SP + 8, 1700); // dialogID
let result = disp.dispatch_dialog(true, 0x17C, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
let dlg_ptr = bus.read_long(TEST_SP + 10);
assert_ne!(dlg_ptr, 0);
assert_eq!(bus.read_byte(dlg_ptr + 110), 0xFF);
assert!(disp.dialog_items.contains_key(&dlg_ptr));
assert!(
disp.dialog_saved_pixels.contains_key(&dlg_ptr),
"visible dialogs save the covered pixels before drawing"
);
assert!(
disp.dialog_visible_snapshots.contains_key(&dlg_ptr),
"visible dialogs retain their clean initial shell for first ModalDialog entry"
);
assert!(!disp.dialog_initial_draw_deferred.contains(&dlg_ptr));
assert!(
disp.event_queue
.iter()
.any(|event| event.what == 6 && event.message == dlg_ptr),
"visible dialog should still get an update event"
);
let probe_addr = screen_base + 120 * 800 + 120;
assert_ne!(
bus.read_byte(probe_addr),
0x77,
"visible GetNewDialog should draw the standard dialog background immediately"
);
}
#[test]
fn hidden_dialog_quickdraw_shape_is_clipped_by_empty_vis_region() {
// A real invisible window has an empty visRgn. Drawing through its
// port must be clipped away instead of touching the screen-backed
// PixMap. This covers hidden dialogs whose userItem setup draws into
// the current dialog port before the dialog is ever shown.
let (mut disp, mut cpu, mut bus) = setup();
let screen_base = bus.alloc((800 * 600) as u32);
bus.write_bytes(screen_base, &vec![0x77; 800 * 600]);
bus.write_long(0x0824, screen_base);
disp.screen_mode = (screen_base, 800, 800, 600, 8);
let dlog = build_test_dlog((100, 100, 180, 260), 1705, 0);
let ditl = build_test_ditl_item(8, (8, 8, 24, 90), b"Hidden");
disp.install_test_resource(&mut bus, *b"DLOG", 1704, &dlog);
disp.install_test_resource(&mut bus, *b"DITL", 1705, &ditl);
bus.write_long(TEST_SP, 0xFFFF_FFFF); // behind
bus.write_long(TEST_SP + 4, 0); // dStorage
bus.write_word(TEST_SP + 8, 1704); // dialogID
let result = disp.dispatch_dialog(true, 0x17C, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
let dlg_ptr = bus.read_long(TEST_SP + 10);
assert_ne!(dlg_ptr, 0);
assert_eq!(bus.read_byte(dlg_ptr + 110), 0);
assert_eq!(
TrapDispatcher::region_handle_rect(&bus, bus.read_long(dlg_ptr + 24)),
None
);
let rect_ptr = bus.alloc(8);
bus.write_word(rect_ptr, 20);
bus.write_word(rect_ptr + 2, 20);
bus.write_word(rect_ptr + 4, 30);
bus.write_word(rect_ptr + 6, 30);
bus.write_long(TEST_SP, rect_ptr);
let result = disp.dispatch_quickdraw(true, 0x0A2, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
let probe_addr = screen_base + 125 * 800 + 125;
assert_eq!(
bus.read_byte(probe_addr),
0x77,
"QuickDraw drawing through a hidden dialog port must be clipped"
);
assert!(
!disp.dialog_saved_pixels.contains_key(&dlg_ptr),
"no visible draw means no saved-under snapshot"
);
}
#[test]
fn dispos_dialog_skips_saved_pixels_for_never_drawn_deferred_dialog() {
// Defensive coverage for the deferred-state guard: if a dialog is
// marked never drawn, DisposDialog must not restore stale saved-under
// pixels. Real visible dialogs normally draw at creation.
// Inside Macintosh Volume I, I-425.
let (mut disp, mut cpu, mut bus) = setup();
let screen_base = bus.alloc((800 * 600) as u32);
bus.write_bytes(screen_base, &vec![0x77; 800 * 600]);
bus.write_long(0x0824, screen_base);
disp.screen_mode = (screen_base, 800, 800, 600, 8);
let mut dlog = build_test_dlog((100, 100, 180, 260), 1703, 0);
dlog[10] = 1; // visible
let ditl = build_test_ditl_items(&[
(0x80, (8, 8, 30, 80), b"".as_slice()),
(4, (44, 20, 64, 90), b"OK".as_slice()),
]);
disp.install_test_resource(&mut bus, *b"DLOG", 1702, &dlog);
disp.install_test_resource(&mut bus, *b"DITL", 1703, &ditl);
bus.write_long(TEST_SP, 0xFFFF_FFFF); // behind
bus.write_long(TEST_SP + 4, 0); // dStorage
bus.write_word(TEST_SP + 8, 1702); // dialogID
let result = disp.dispatch_dialog(true, 0x17C, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
let dlg_ptr = bus.read_long(TEST_SP + 10);
disp.dialog_initial_draw_deferred.insert(dlg_ptr);
// Replace the saved snapshot with a distinct dirty pattern. If
// DisposDialog restores it, the probe byte will change to 0x33.
disp.dialog_saved_pixels
.insert(dlg_ptr, vec![0x33; 90 * 170]);
let probe_addr = screen_base + 120 * 800 + 120;
bus.write_byte(probe_addr, 0x77);
bus.write_long(TEST_SP, dlg_ptr);
let result = disp.dispatch_dialog(true, 0x183, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(
bus.read_byte(probe_addr),
0x77,
"never-drawn deferred dialog must not restore saved background"
);
assert!(!disp.dialog_initial_draw_deferred.contains(&dlg_ptr));
assert!(!disp.dialog_saved_pixels.contains_key(&dlg_ptr));
}
// save_dialog_pixels / restore_dialog_pixels must guard against off-screen y
// (negative after the dBoxProc structure margin, or beyond screen_h).
// Without the guard, (y as u32) sign-extends a negative i16 and
// multiply-with-overflow panics in debug.
#[test]
fn save_dialog_pixels_handles_top_below_dbox_margin_without_overflow() {
let (disp, _cpu, mut bus) = setup();
let screen_base = bus.alloc((800 * 600) as u32);
for i in 0..800u32 * 600 {
bus.write_byte(screen_base + i, 0x42);
}
let mut d = disp;
bus.write_long(0x0824, screen_base);
d.screen_mode = (screen_base, 800, 800, 600, 8);
// Bounds with top=2 → save_top = -6 (negative).
let saved = d.save_dialog_pixels(&bus, (2, 2, 50, 50));
// Row width = (58 - (-6)) = 64; row count = 64.
let row_width = 64usize;
let row_count = 64usize;
assert_eq!(saved.len(), row_width * row_count);
// First 6 rows are off-screen (y = -6..-1) → zero-padded.
for row in 0..6 {
for col in 0..row_width {
assert_eq!(
saved[row * row_width + col],
0x00,
"off-screen row {} col {} must be zero-padded",
row,
col
);
}
}
// y=0 row, save_left=-6 so cols 0..6 are off-screen → 0,
// cols 6..64 read from the 0x42-filled framebuffer.
let row0 = &saved[6 * row_width..7 * row_width];
for (col, &px) in row0.iter().enumerate().take(6) {
assert_eq!(
px, 0x00,
"off-screen column {} within on-screen row must be zero",
col
);
}
for (col, &px) in row0.iter().enumerate().skip(6) {
assert_eq!(
px, 0x42,
"on-screen pixel at col {} must round-trip via framebuffer",
col
);
}
}
// save_rect_pixels / restore_rect_pixels guard the same off-screen y
// overflow hazard as save_dialog_pixels.
#[test]
fn save_rect_pixels_handles_negative_top_without_overflow() {
let (disp, _cpu, mut bus) = setup();
let screen_base = bus.alloc((800 * 600) as u32);
for i in 0..800u32 * 600 {
bus.write_byte(screen_base + i, 0x55);
}
let mut d = disp;
bus.write_long(0x0824, screen_base);
d.screen_mode = (screen_base, 800, 800, 600, 8);
// Rect spanning above the screen top.
let saved = d.save_rect_pixels(&bus, (-3, 0, 5, 10));
let row_width = 10usize;
let row_count = (5 - (-3)) as usize;
assert_eq!(saved.len(), row_width * row_count);
// First 3 rows off-screen (y = -3..0) → zero-padded.
for row in 0..3 {
for col in 0..row_width {
assert_eq!(saved[row * row_width + col], 0x00);
}
}
// Next 5 rows on-screen → 0x55.
for row in 3..8 {
for col in 0..row_width {
assert_eq!(saved[row * row_width + col], 0x55);
}
}
}
#[test]
fn save_dialog_pixels_handles_top_above_screen_height_without_overflow() {
let (disp, _cpu, mut bus) = setup();
let screen_base = bus.alloc((800 * 600) as u32);
let mut d = disp;
bus.write_long(0x0824, screen_base);
d.screen_mode = (screen_base, 800, 800, 600, 8);
// Bounds way below screen → save_top = 595+ and save_bottom
// = 700+, many rows off-screen on the bottom side.
let saved = d.save_dialog_pixels(&bus, (600, 0, 700, 50));
let row_count = (700 + TrapDispatcher::DBOX_FRAME_MARGIN
- (600 - TrapDispatcher::DBOX_FRAME_MARGIN)) as usize;
let row_width = (50 + TrapDispatcher::DBOX_FRAME_MARGIN
- (0 - TrapDispatcher::DBOX_FRAME_MARGIN)) as usize;
assert_eq!(saved.len(), row_count * row_width);
}
fn install_new_dialog_test_screen(
disp: &mut TrapDispatcher,
bus: &mut MacMemoryBus,
fill: u8,
) -> u32 {
let screen_base = bus.alloc((640 * 480) as u32);
bus.write_bytes(screen_base, &vec![fill; 640 * 480]);
bus.write_long(0x0824, screen_base);
disp.screen_mode = (screen_base, 640, 640, 480, 8);
screen_base
}
fn call_new_dialog_for_test(
disp: &mut TrapDispatcher,
cpu: &mut MockCpu,
bus: &mut MacMemoryBus,
storage_ptr: u32,
bounds: (i16, i16, i16, i16),
visible: bool,
proc_id: i16,
behind: u32,
items_handle: u32,
) -> (u32, u32) {
let title_ptr = bus.alloc(32);
bus.write_pstring(title_ptr, b"Lifecycle");
let bounds_ptr = bus.alloc(8);
bus.write_word(bounds_ptr, bounds.0 as u16);
bus.write_word(bounds_ptr + 2, bounds.1 as u16);
bus.write_word(bounds_ptr + 4, bounds.2 as u16);
bus.write_word(bounds_ptr + 6, bounds.3 as u16);
let sp = TEST_SP - 30;
cpu.write_reg(Register::A7, sp);
for offset in 0..34u32 {
bus.write_byte(sp + offset, 0);
}
bus.write_long(sp, items_handle);
bus.write_long(sp + 4, 0x1234_5678); // refCon
bus.write_byte(sp + 8, 0xFF); // goAwayFlag
bus.write_long(sp + 10, behind);
bus.write_word(sp + 14, proc_id as u16);
bus.write_byte(sp + 16, if visible { 0xFF } else { 0 });
bus.write_long(sp + 18, title_ptr);
bus.write_long(sp + 22, bounds_ptr);
bus.write_long(sp + 26, storage_ptr);
bus.write_long(sp + 30, 0xDEAD_BEEF);
disp.dispatch_dialog(true, 0x17D, cpu, bus)
.unwrap()
.unwrap();
(sp, bus.read_long(sp + 30))
}
fn install_ditl_handle_for_test(bus: &mut MacMemoryBus, ditl: &[u8]) -> (u32, u32) {
let items_ptr = bus.alloc(ditl.len() as u32);
bus.write_bytes(items_ptr, ditl);
let items_handle = bus.alloc(4);
bus.write_long(items_handle, items_ptr);
(items_handle, items_ptr)
}
#[test]
fn new_dialog_with_storage_uses_caller_record() {
// IM:I I-412: dStorage supplies the DialogRecord storage; NIL asks
// the Dialog Manager to allocate the record instead.
let (mut disp, mut cpu, mut bus) = setup();
install_new_dialog_test_screen(&mut disp, &mut bus, 0x77);
let storage_ptr = bus.alloc(170);
let (sp, dialog_ptr) = call_new_dialog_for_test(
&mut disp,
&mut cpu,
&mut bus,
storage_ptr,
(80, 90, 150, 260),
false,
4,
0xFFFF_FFFF,
0,
);
assert_eq!(dialog_ptr, storage_ptr);
assert_eq!(bus.read_long(sp + 30), storage_ptr);
assert_eq!(cpu.read_reg(Register::A7), sp + 30);
assert_eq!(disp.window_list, vec![storage_ptr]);
}
#[test]
fn new_dialog_without_storage_allocates_record() {
// IM:I I-412 and MTE 1992 p. 6-118: passing NIL for dStorage
// allocates the DialogRecord in the heap.
let (mut disp, mut cpu, mut bus) = setup();
install_new_dialog_test_screen(&mut disp, &mut bus, 0x77);
let (_sp, dialog_ptr) = call_new_dialog_for_test(
&mut disp,
&mut cpu,
&mut bus,
0,
(80, 90, 150, 260),
false,
2,
0xFFFF_FFFF,
0,
);
assert_ne!(dialog_ptr, 0);
assert_eq!(bus.get_alloc_size(dialog_ptr), Some(170));
assert_eq!(disp.window_list, vec![dialog_ptr]);
}
#[test]
fn new_dialog_sets_window_kind_dialog_kind() {
// IM:I I-407 and I-412: a DialogPtr is a WindowPtr whose
// WindowRecord.windowKind is dialogKind, independent of the WDEF
// procID used to draw modeless or modal chrome.
let (mut disp, mut cpu, mut bus) = setup();
install_new_dialog_test_screen(&mut disp, &mut bus, 0x77);
let (_sp, dialog_ptr) = call_new_dialog_for_test(
&mut disp,
&mut cpu,
&mut bus,
0,
(80, 90, 150, 260),
false,
4,
0xFFFF_FFFF,
0,
);
assert_eq!(bus.read_word(dialog_ptr + 108) as i16, 2);
assert_eq!(disp.window_proc_ids.get(&dialog_ptr), Some(&4));
}
#[test]
fn new_dialog_sets_dialog_port_font_to_dialog_font() {
// IM:I I-412 and MTE 1992 p. 6-104: SetDAFont/SetDialogFont set
// DlgFont for subsequently created dialog and alert grafPorts.
let (mut disp, mut cpu, mut bus) = setup();
install_new_dialog_test_screen(&mut disp, &mut bus, 0x77);
bus.write_word(crate::memory::globals::addr::DLG_FONT, 3); // Geneva
let (_sp, dialog_ptr) = call_new_dialog_for_test(
&mut disp,
&mut cpu,
&mut bus,
0,
(80, 90, 150, 260),
false,
2,
0xFFFF_FFFF,
0,
);
assert_eq!(bus.read_word(dialog_ptr + 68) as i16, 3);
assert_eq!(disp.tx_font, 3);
assert_eq!(
disp.port_draw_states
.get(&dialog_ptr)
.map(|state| state.tx_font),
Some(3)
);
}
#[test]
fn new_dialog_visible_queues_or_draws_expected_update_path() {
// IM:I I-412 and I-287: visible NewDialog draws the dialog window and
// generates an update event for the contents.
let (mut disp, mut cpu, mut bus) = setup();
let screen_base = install_new_dialog_test_screen(&mut disp, &mut bus, 0x77);
let (_sp, dialog_ptr) = call_new_dialog_for_test(
&mut disp,
&mut cpu,
&mut bus,
0,
(80, 90, 150, 260),
true,
2,
0xFFFF_FFFF,
0,
);
assert_eq!(bus.read_byte(dialog_ptr + 110), 0xFF);
assert!(disp
.event_queue
.iter()
.any(|event| event.what == 6 && event.message == dialog_ptr));
assert!(
TrapDispatcher::region_handle_rect(&bus, bus.read_long(dialog_ptr + 122)).is_some()
);
assert_ne!(bus.read_byte(screen_base + 100 * 640 + 110), 0x77);
}
#[test]
fn new_dialog_invisible_defers_screen_pixels() {
// IM:I I-412: if visible is FALSE, the window is initially invisible
// and may later be shown with ShowWindow.
let (mut disp, mut cpu, mut bus) = setup();
let screen_base = install_new_dialog_test_screen(&mut disp, &mut bus, 0x77);
let (_sp, dialog_ptr) = call_new_dialog_for_test(
&mut disp,
&mut cpu,
&mut bus,
0,
(80, 90, 150, 260),
false,
2,
0xFFFF_FFFF,
0,
);
assert_eq!(bus.read_byte(dialog_ptr + 110), 0);
assert!(!disp
.event_queue
.iter()
.any(|event| event.what == 6 && event.message == dialog_ptr));
assert_eq!(
TrapDispatcher::region_handle_rect(&bus, bus.read_long(dialog_ptr + 122)),
None
);
assert_eq!(bus.read_byte(screen_base + 100 * 640 + 110), 0x77);
}
#[test]
fn new_dialog_honors_behind_nil_and_inserts_at_back() {
// Inside Macintosh Volume I, I-412: NewDialog returns a DialogPtr
// and honors the `behind` parameter for plane order.
let (mut disp, mut cpu, mut bus) = setup();
// Seed an existing window that will stay in front.
let existing = 0x200040u32;
disp.window_list = vec![existing];
disp.front_window = existing;
bus.write_byte(existing + 110u32, 0xFF); // visible
// init_cgraf_window reads screen_mode for bounds math; a
// zero-initialized mode multiplies-with-overflow later.
let screen_base = bus.alloc((800 * 600) as u32);
bus.write_long(0x0824, screen_base);
disp.screen_mode = (screen_base, 800, 800, 600, 8);
// Bounds at the origin are now safe via the off-screen y overflow guard
// in save_dialog_pixels. Keep a non-origin placement for realism.
let bounds_rect_ptr = 0x301200u32;
bus.write_word(bounds_rect_ptr, 100);
bus.write_word(bounds_rect_ptr + 2, 100);
bus.write_word(bounds_rect_ptr + 4, 300);
bus.write_word(bounds_rect_ptr + 6, 400);
let sp = TEST_SP - 30;
cpu.write_reg(Register::A7, sp);
for i in 0..34u32 {
bus.write_byte(sp + i, 0);
}
bus.write_long(sp + 22, bounds_rect_ptr);
bus.write_word(sp + 16, 1); // visible
bus.write_word(sp + 14, 1); // dBoxProc WDEF
bus.write_long(sp + 10, 0); // behind = NIL (backmost)
let pre_a7 = cpu.read_reg(Register::A7);
let result = disp.dispatch_dialog(true, 0x17D, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(
cpu.read_reg(Register::A7),
pre_a7 + 30,
"NewDialog must pop 30 bytes of parameters and leave result at new SP+0"
);
let dlg_ptr = bus.read_long(sp + 30);
assert_ne!(dlg_ptr, 0);
assert_eq!(
bus.read_word(dlg_ptr + 108),
2,
"DialogRecord.window.windowKind must be dialogKind"
);
assert_eq!(
disp.window_proc_ids.get(&dlg_ptr),
Some(&1),
"dialog WDEF procID must be tracked separately from windowKind"
);
assert_eq!(
disp.window_list,
vec![existing, dlg_ptr],
"NewDialog(behind=NIL) must insert dialog at the back"
);
assert_eq!(
disp.front_window, existing,
"front must stay on the pre-existing visible window"
);
}
#[test]
fn new_cdialog_honors_behind_nil_and_inserts_at_back() {
// Inside Macintosh Volume V, V-243: NewCDialog follows the same
// creation path as NewDialog but returns a color dialog pointer.
let (mut disp, mut cpu, mut bus) = setup();
let existing = 0x200040u32;
disp.window_list = vec![existing];
disp.front_window = existing;
bus.write_byte(existing + 110u32, 0xFF); // visible
let screen_base = bus.alloc((800 * 600) as u32);
bus.write_long(0x0824, screen_base);
disp.screen_mode = (screen_base, 800, 800, 600, 8);
let bounds_rect_ptr = 0x301280u32;
bus.write_word(bounds_rect_ptr, 120);
bus.write_word(bounds_rect_ptr + 2, 120);
bus.write_word(bounds_rect_ptr + 4, 320);
bus.write_word(bounds_rect_ptr + 6, 420);
let sp = TEST_SP - 30;
cpu.write_reg(Register::A7, sp);
for i in 0..34u32 {
bus.write_byte(sp + i, 0);
}
bus.write_long(sp + 22, bounds_rect_ptr);
bus.write_word(sp + 16, 1); // visible
bus.write_long(sp + 10, 0); // behind = NIL (backmost)
let pre_a7 = cpu.read_reg(Register::A7);
let result = disp.dispatch_dialog(true, 0x24B, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(
cpu.read_reg(Register::A7),
pre_a7 + 30,
"NewCDialog must pop 30 bytes of parameters and leave result at new SP+0"
);
let dlg_ptr = bus.read_long(sp + 30);
assert_ne!(dlg_ptr, 0);
assert_eq!(
disp.window_list,
vec![existing, dlg_ptr],
"NewCDialog(behind=NIL) must insert dialog at the back"
);
assert_eq!(
disp.front_window, existing,
"front must stay on the pre-existing visible window"
);
}
#[test]
fn get_new_dialog_honors_behind_specific_window() {
// GetNewDialog with no DLOG resource hits the fallback branch
// that does NOT call finish_dialog_creation — so behind can't
// reshuffle a list entry that was never added. This test
// verifies the stack slot is at least READ without panicking.
// The main contract test is new_dialog_honors_behind_nil_
// and_inserts_at_back above, since that exercises the
// primary post-finish_dialog_creation path.
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
// Write a specific non-trivial behind pointer at SP+0.
bus.write_long(sp, 0xDEAD0000);
let result = disp.dispatch_dialog(true, 0x17C, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 10);
}
// ---- SelectDialogItemText ($A97E) ----
#[test]
fn select_dialog_item_text_zero_to_32767_selects_entire_text_and_sets_editfield() {
// Macintosh Toolbox Essentials 1992, 6-131: selecting the whole
// editable text item uses strtSel=0 and endSel=32767.
let (mut disp, mut cpu, mut bus) = setup();
let dialog_ptr = bus.alloc(170);
bus.write_word(dialog_ptr + 164, 0xFFFF); // editField = -1 (none)
disp.dialog_items.insert(
dialog_ptr,
vec![DialogItem {
item_type: 16,
rect: (10, 20, 30, 40),
text: "ABCDE".to_string(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
}],
);
bus.write_word(TEST_SP, 32767u16); // endSel
bus.write_word(TEST_SP + 2, 0); // strtSel
bus.write_word(TEST_SP + 4, 1); // itemNo (1-based)
bus.write_long(TEST_SP + 6, dialog_ptr); // theDialog
let result = disp.dispatch_dialog(true, 0x17E, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 10);
let item = &disp.dialog_items[&dialog_ptr][0];
assert_eq!(item.sel_start, 0);
assert_eq!(item.sel_end, 5);
assert_eq!(bus.read_word(dialog_ptr + 164), 0);
}
#[test]
fn select_dialog_item_text_clamps_and_normalizes_selection_bounds() {
// IM:I I-414 defines a [strtSel,endSel) range; callers can pass
// out-of-range values, so HLE clamps to text bounds and normalizes
// reversed inputs.
let (mut disp, mut cpu, mut bus) = setup();
let dialog_ptr = bus.alloc(170);
disp.dialog_items.insert(
dialog_ptr,
vec![DialogItem {
item_type: 16,
rect: (10, 20, 30, 40),
text: "ABCDE".to_string(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
}],
);
bus.write_word(TEST_SP, 2); // endSel
bus.write_word(TEST_SP + 2, 9); // strtSel (> text length)
bus.write_word(TEST_SP + 4, 1); // itemNo
bus.write_long(TEST_SP + 6, dialog_ptr); // theDialog
let result = disp.dispatch_dialog(true, 0x17E, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 10);
let item = &disp.dialog_items[&dialog_ptr][0];
assert_eq!(item.sel_start, 2);
assert_eq!(item.sel_end, 5);
}
#[test]
fn select_dialog_item_text_non_edit_item_is_noop() {
// Macintosh Toolbox Essentials 1992, 6-131: selection applies to
// editable text items only.
let (mut disp, mut cpu, mut bus) = setup();
let dialog_ptr = bus.alloc(170);
bus.write_word(dialog_ptr + 164, 0x7FFF); // sentinel
disp.dialog_items.insert(
dialog_ptr,
vec![DialogItem {
item_type: 8, // statText
rect: (10, 20, 30, 40),
text: "STATIC".to_string(),
resource_id: 0,
proc_ptr: 0,
sel_start: 1,
sel_end: 3,
}],
);
bus.write_word(TEST_SP, 4); // endSel
bus.write_word(TEST_SP + 2, 0); // strtSel
bus.write_word(TEST_SP + 4, 1); // itemNo
bus.write_long(TEST_SP + 6, dialog_ptr); // theDialog
let result = disp.dispatch_dialog(true, 0x17E, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 10);
let item = &disp.dialog_items[&dialog_ptr][0];
assert_eq!(item.sel_start, 1);
assert_eq!(item.sel_end, 3);
assert_eq!(bus.read_word(dialog_ptr + 164), 0x7FFF);
}
// ---- Alert ($A985) ----
// Alert with no matching ALRT resource must return -1 per IM:I-412
// ("Alert returns -1 and does nothing").
#[test]
fn alert_returns_minus_one_when_alrt_resource_missing() {
let (mut disp, mut cpu, mut bus) = setup();
// SP+4: alertID = 128 (no ALRT resource loaded in the test
// dispatcher).
bus.write_word(TEST_SP + 4, 128);
let result = disp.dispatch_dialog(true, 0x185, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
assert_eq!(bus.read_word(TEST_SP + 6) as i16, -1);
}
// ---- te_line_origin_x (TextEdit alignment) ----
//
// Per IM:Text 1993 lines 7320-7323 and the MPW Universal Headers:
// teJustLeft = 0, teJustCenter = 1, teJustRight = -1, teForceLeft = -2.
#[test]
fn te_line_origin_te_just_left_flush_left() {
// teJustLeft (0): origin = box_left + TextEdit's left inset.
assert_eq!(
TrapDispatcher::te_line_origin_x(0, 20, 200, 80),
21,
"teJustLeft (0) must anchor one pixel inside box_left"
);
}
#[test]
fn te_line_origin_te_just_center_midpoint() {
// teJustCenter (1): origin = box_left + (box_w - line_w) / 2.
// (200-20 - 80) / 2 = 50 → origin = 20 + 50 = 70.
assert_eq!(
TrapDispatcher::te_line_origin_x(1, 20, 200, 80),
70,
"teJustCenter must midpoint the slack"
);
}
#[test]
fn te_line_origin_te_just_right_flush_right() {
// teJustRight (-1): origin = box_right - line_width.
// 200 - 80 = 120.
assert_eq!(
TrapDispatcher::te_line_origin_x(-1, 20, 200, 80),
120,
"teJustRight (-1) must anchor at box_right - line_width"
);
}
#[test]
fn te_line_origin_te_force_left_flush_left() {
// teForceLeft (-2): origin = box_left + TextEdit's left inset
// (overrides any
// localised right-to-left default).
assert_eq!(
TrapDispatcher::te_line_origin_x(-2, 20, 200, 80),
21,
"teForceLeft (-2) must anchor one pixel inside box_left"
);
}
#[test]
fn te_line_origin_te_just_system_defaults_left() {
// teJustSystem (0): localised default = left for LTR, with the
// same TextEdit left inset.
assert_eq!(
TrapDispatcher::te_line_origin_x(0, 20, 200, 80),
21,
"teJustSystem must default to flush left inside box_left"
);
}
// ---- StopAlert ($A986) ----
#[test]
fn stop_alert_returns_minus_one_when_alrt_resource_missing() {
let (mut disp, mut cpu, mut bus) = setup();
bus.write_word(crate::memory::globals::addr::ALERT_STAGE, 2);
bus.write_word(crate::memory::globals::addr::ANUMBER, 0xC0DE);
bus.write_word(TEST_SP + 4, 128);
let result = disp.dispatch_dialog(true, 0x186, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
assert_eq!(bus.read_word(TEST_SP + 6) as i16, -1);
assert_eq!(bus.read_word(crate::memory::globals::addr::ALERT_STAGE), 2);
assert_eq!(bus.read_word(crate::memory::globals::addr::ANUMBER), 0xC0DE);
}
// ---- NoteAlert ($A987) ----
#[test]
fn note_alert_returns_minus_one_when_alrt_resource_missing() {
let (mut disp, mut cpu, mut bus) = setup();
bus.write_word(crate::memory::globals::addr::ALERT_STAGE, 1);
bus.write_word(crate::memory::globals::addr::ANUMBER, 0xBEEF);
bus.write_word(TEST_SP + 4, 128);
let result = disp.dispatch_dialog(true, 0x187, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
assert_eq!(bus.read_word(TEST_SP + 6) as i16, -1);
assert_eq!(bus.read_word(crate::memory::globals::addr::ALERT_STAGE), 1);
assert_eq!(bus.read_word(crate::memory::globals::addr::ANUMBER), 0xBEEF);
}
// ---- CautionAlert ($A988) ----
#[test]
fn caution_alert_returns_minus_one_when_alrt_resource_missing() {
let (mut disp, mut cpu, mut bus) = setup();
bus.write_word(crate::memory::globals::addr::ALERT_STAGE, 3);
bus.write_word(crate::memory::globals::addr::ANUMBER, 0xFACE);
bus.write_word(TEST_SP + 4, 128);
let result = disp.dispatch_dialog(true, 0x188, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
assert_eq!(bus.read_word(TEST_SP + 6) as i16, -1);
assert_eq!(bus.read_word(crate::memory::globals::addr::ALERT_STAGE), 3);
assert_eq!(bus.read_word(crate::memory::globals::addr::ANUMBER), 0xFACE);
}
// ---- Alert family stages-driven default item (IM:I I-417 / I-422) ----
//
// Each test installs an ALRT resource with a controlled
// 16-byte template (8-byte boundsRect + 2-byte itemsID + 2-byte
// stages + padding) and verifies the trap returns the bold
// (default) item for the current AlertStage low-mem byte.
//
// ALRT stages encoding (IM:I I-422): 16-bit word, 4 nibbles
// (low to high = stage 1..4). Within each nibble, bit 3 is
// boldItmNum (`okDismissal = 8`), bit 2 is boxDrwn (`alBit = 4`),
// and bits 0..1 are the sound number.
/// Build a 12-byte ALRT template: bounds=(0,0,80,200) +
/// itemsID + stages + 0 padding. Real ALRTs are typically
/// 12 bytes; we install at least 12.
fn build_alrt_template(items_id: i16, stages: u16) -> Vec<u8> {
let mut v = Vec::with_capacity(12);
v.extend_from_slice(&0u16.to_be_bytes()); // top
v.extend_from_slice(&0u16.to_be_bytes()); // left
v.extend_from_slice(&80u16.to_be_bytes()); // bottom
v.extend_from_slice(&200u16.to_be_bytes()); // right
v.extend_from_slice(&items_id.to_be_bytes()); // itemsID
v.extend_from_slice(&stages.to_be_bytes()); // stages
v
}
// ---- CouldDialog ($A979) / FreeDialog ($A97A) ----
#[test]
fn could_dialog_present_resource_sets_reserr_noerr() {
// Inside Macintosh Volume I, I-415: CouldDialog targets a DLOG by
// resource ID and prepares it for later use. BasiliskII leaves
// ResErr at noErr on the missing-resource path.
let (mut disp, mut cpu, mut bus) = setup();
let dlog = vec![0u8; 20];
disp.install_test_resource(&mut bus, *b"DLOG", 300, &dlog);
bus.write_word(0x0A60, 0x7FFF);
bus.write_word(TEST_SP, 300);
let pre_a7 = cpu.read_reg(Register::A7);
let result = disp.dispatch_dialog(true, 0x179, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), pre_a7 + 2);
assert_eq!(bus.read_word(0x0A60) as i16, 0);
}
#[test]
fn could_dialog_missing_resource_sets_reserr_resnotfound() {
// BasiliskII leaves ResErr at noErr even when the DLOG is missing.
let (mut disp, mut cpu, mut bus) = setup();
bus.write_word(0x0A60, 0);
bus.write_word(TEST_SP, 301);
let pre_a7 = cpu.read_reg(Register::A7);
let result = disp.dispatch_dialog(true, 0x179, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), pre_a7 + 2);
assert_eq!(bus.read_word(0x0A60) as i16, 0);
}
#[test]
fn free_dialog_present_resource_sets_reserr_noerr() {
// Inside Macintosh Volume I, I-415: FreeDialog reverses CouldDialog
// for previously targeted DLOG templates. BasiliskII leaves
// ResErr at noErr on the missing-resource path.
let (mut disp, mut cpu, mut bus) = setup();
let dlog = vec![0u8; 20];
disp.install_test_resource(&mut bus, *b"DLOG", 302, &dlog);
bus.write_word(0x0A60, 0x7FFF);
bus.write_word(TEST_SP, 302);
let pre_a7 = cpu.read_reg(Register::A7);
let result = disp.dispatch_dialog(true, 0x17A, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), pre_a7 + 2);
assert_eq!(bus.read_word(0x0A60) as i16, 0);
}
#[test]
fn free_dialog_missing_resource_sets_reserr_resnotfound() {
// BasiliskII leaves ResErr at noErr even when the DLOG is missing.
let (mut disp, mut cpu, mut bus) = setup();
bus.write_word(0x0A60, 0);
bus.write_word(TEST_SP, 303);
let pre_a7 = cpu.read_reg(Register::A7);
let result = disp.dispatch_dialog(true, 0x17A, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), pre_a7 + 2);
assert_eq!(bus.read_word(0x0A60) as i16, 0);
}
#[test]
fn could_dialog_free_dialog_purgeability() {
// IM:I I-415: CouldDialog reads the dialog resource family into
// memory if needed and makes it unpurgeable; FreeDialog reverses
// the purgeability state for those already-loaded resources.
let (mut disp, mut cpu, mut bus) = setup();
let dlog_id: i16 = 3100;
let ditl_id: i16 = 3101;
let cntl_id: i16 = 3102;
let icon_id: i16 = 3103;
let pict_id: i16 = 3104;
let dlog = build_test_dlog((10, 20, 110, 220), ditl_id, 0);
let ditl = build_test_ditl_items(&[
(7, (1, 2, 12, 82), &cntl_id.to_be_bytes()),
(32, (14, 2, 46, 34), &icon_id.to_be_bytes()),
(64, (48, 2, 88, 82), &pict_id.to_be_bytes()),
]);
disp.install_test_resource(&mut bus, *b"DLOG", dlog_id, &dlog);
disp.install_test_resource(&mut bus, *b"DITL", ditl_id, &ditl);
disp.install_test_resource(&mut bus, *b"CNTL", cntl_id, &[0u8; 32]);
disp.install_test_resource(&mut bus, *b"ICON", icon_id, &[0xAA; 128]);
disp.install_test_resource(&mut bus, *b"PICT", pict_id, &[0x11; 32]);
disp.res_load = false;
bus.write_word(0x0A60, 0x7FFF);
bus.write_word(TEST_SP, dlog_id as u16);
let pre_a7 = cpu.read_reg(Register::A7);
assert!(disp
.dispatch_dialog(true, 0x179, &mut cpu, &mut bus)
.unwrap()
.is_ok());
assert_eq!(cpu.read_reg(Register::A7), pre_a7 + 2);
assert_eq!(bus.read_word(0x0A60) as i16, 0);
for (res_type, res_id) in [
(*b"DLOG", dlog_id),
(*b"DITL", ditl_id),
(*b"CNTL", cntl_id),
(*b"ICON", icon_id),
(*b"PICT", pict_id),
] {
assert_resource_nonpurgeable(&disp, &bus, res_type, res_id);
}
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, dlog_id as u16);
assert!(disp
.dispatch_dialog(true, 0x17A, &mut cpu, &mut bus)
.unwrap()
.is_ok());
assert_eq!(bus.read_word(0x0A60) as i16, 0);
for (res_type, res_id) in [
(*b"DLOG", dlog_id),
(*b"DITL", ditl_id),
(*b"CNTL", cntl_id),
(*b"ICON", icon_id),
(*b"PICT", pict_id),
] {
assert_resource_purgeable(&disp, res_type, res_id);
}
}
// ---- CouldAlert ($A989) / FreeAlert ($A98A) ----
#[test]
fn could_alert_present_resource_sets_reserr_noerr() {
// IM:I I-420: CouldAlert targets an ALRT template by ID.
// Systemless's HLE compromise writes Resource Manager error
// state at ResErr ($0A60): noErr for present resources.
let (mut disp, mut cpu, mut bus) = setup();
let alrt = build_alrt_template(200, 0x0000);
disp.install_test_resource(&mut bus, *b"ALRT", 400, &alrt);
bus.write_word(0x0A60, 0x7FFF);
bus.write_word(TEST_SP, 400);
let pre_a7 = cpu.read_reg(Register::A7);
let result = disp.dispatch_dialog(true, 0x189, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), pre_a7 + 2);
assert_eq!(bus.read_word(0x0A60) as i16, 0);
}
#[test]
fn could_alert_missing_resource_sets_reserr_resnotfound() {
// IM:I I-420: missing ALRT IDs are ignored.
let (mut disp, mut cpu, mut bus) = setup();
bus.write_word(0x0A60, 0);
bus.write_word(TEST_SP, 401);
let pre_a7 = cpu.read_reg(Register::A7);
let result = disp.dispatch_dialog(true, 0x189, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), pre_a7 + 2);
assert_eq!(bus.read_word(0x0A60) as i16, 0);
}
#[test]
fn free_alert_present_resource_sets_reserr_noerr() {
// IM:I I-420: FreeAlert undoes a prior CouldAlert target.
// HLE compromise reports success via ResErr for present ALRT.
let (mut disp, mut cpu, mut bus) = setup();
let alrt = build_alrt_template(200, 0x0000);
disp.install_test_resource(&mut bus, *b"ALRT", 402, &alrt);
bus.write_word(0x0A60, 0x7FFF);
bus.write_word(TEST_SP, 402);
let pre_a7 = cpu.read_reg(Register::A7);
let result = disp.dispatch_dialog(true, 0x18A, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), pre_a7 + 2);
assert_eq!(bus.read_word(0x0A60) as i16, 0);
}
#[test]
fn free_alert_missing_resource_sets_reserr_resnotfound() {
// Missing ALRT IDs in FreeAlert are ignored.
let (mut disp, mut cpu, mut bus) = setup();
bus.write_word(0x0A60, 0);
bus.write_word(TEST_SP, 403);
let pre_a7 = cpu.read_reg(Register::A7);
let result = disp.dispatch_dialog(true, 0x18A, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), pre_a7 + 2);
assert_eq!(bus.read_word(0x0A60) as i16, 0);
}
#[test]
fn could_alert_and_free_alert_leave_loaded_alert_attrs_unchanged() {
let (mut disp, mut cpu, mut bus) = setup();
let alrt = build_alrt_template(200, 0x0000);
let data_ptr = disp.install_test_resource(&mut bus, *b"ALRT", 404, &alrt);
let handle = disp.get_or_create_resource_handle(&mut bus, *b"ALRT", 404, data_ptr);
let attrs_before = disp.resource_attributes_for_handle(handle).unwrap();
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 404);
assert!(disp
.dispatch_dialog(true, 0x189, &mut cpu, &mut bus)
.unwrap()
.is_ok());
assert_eq!(bus.read_word(0x0A60) as i16, 0);
assert_eq!(
disp.resource_attributes_for_handle(handle).unwrap(),
attrs_before
);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 404);
assert!(disp
.dispatch_dialog(true, 0x18A, &mut cpu, &mut bus)
.unwrap()
.is_ok());
assert_eq!(bus.read_word(0x0A60) as i16, 0);
assert_eq!(
disp.resource_attributes_for_handle(handle).unwrap(),
attrs_before
);
}
#[test]
fn could_alert_free_alert_purgeability() {
// IM:I I-420 gives CouldAlert/FreeAlert the same resource-family
// purgeability contract as CouldDialog/FreeDialog, rooted at ALRT.
let (mut disp, mut cpu, mut bus) = setup();
let alrt_id: i16 = 3200;
let ditl_id: i16 = 3201;
let cntl_id: i16 = 3202;
let icon_id: i16 = 3203;
let pict_id: i16 = 3204;
let alrt = build_alrt_template(ditl_id, 0x0000);
let ditl = build_test_ditl_items(&[
(7, (1, 2, 12, 82), &cntl_id.to_be_bytes()),
(32, (14, 2, 46, 34), &icon_id.to_be_bytes()),
(64, (48, 2, 88, 82), &pict_id.to_be_bytes()),
]);
disp.install_test_resource(&mut bus, *b"ALRT", alrt_id, &alrt);
disp.install_test_resource(&mut bus, *b"DITL", ditl_id, &ditl);
disp.install_test_resource(&mut bus, *b"CNTL", cntl_id, &[0u8; 32]);
disp.install_test_resource(&mut bus, *b"ICON", icon_id, &[0xAA; 128]);
disp.install_test_resource(&mut bus, *b"PICT", pict_id, &[0x11; 32]);
disp.res_load = false;
bus.write_word(0x0A60, 0x7FFF);
bus.write_word(TEST_SP, alrt_id as u16);
let pre_a7 = cpu.read_reg(Register::A7);
assert!(disp
.dispatch_dialog(true, 0x189, &mut cpu, &mut bus)
.unwrap()
.is_ok());
assert_eq!(cpu.read_reg(Register::A7), pre_a7 + 2);
assert_eq!(bus.read_word(0x0A60) as i16, 0);
for (res_type, res_id) in [
(*b"ALRT", alrt_id),
(*b"DITL", ditl_id),
(*b"CNTL", cntl_id),
(*b"ICON", icon_id),
(*b"PICT", pict_id),
] {
assert_resource_nonpurgeable(&disp, &bus, res_type, res_id);
}
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, alrt_id as u16);
assert!(disp
.dispatch_dialog(true, 0x18A, &mut cpu, &mut bus)
.unwrap()
.is_ok());
assert_eq!(bus.read_word(0x0A60) as i16, 0);
for (res_type, res_id) in [
(*b"ALRT", alrt_id),
(*b"DITL", ditl_id),
(*b"CNTL", cntl_id),
(*b"ICON", icon_id),
(*b"PICT", pict_id),
] {
assert_resource_purgeable(&disp, res_type, res_id);
}
}
#[test]
fn alert_returns_item_1_when_stage_1_bold_flag_clear() {
// stages = 0x4444 → all 4 nibbles have boxDrwn set and
// bold item clear, so item 1 (OK button) is the default.
let (mut disp, mut cpu, mut bus) = setup();
let alrt = build_alrt_template(/*itemsID*/ 200, /*stages*/ 0x4444);
disp.install_test_resource(&mut bus, *b"ALRT", 128, &alrt);
bus.write_word(crate::memory::globals::addr::ALERT_STAGE, 0); // stage 1
bus.write_word(TEST_SP + 4, 128);
let result = disp.dispatch_dialog(true, 0x185, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(
bus.read_word(TEST_SP + 6) as i16,
1,
"Alert with stage 1 + bold flag clear must return item 1 (OK)"
);
}
#[test]
fn alert_returns_item_2_when_stage_1_bold_flag_set() {
// stages = 0xCCCC → all 4 nibbles have boxDrwn and bit 3 set
// (okDismissal mask per IM:I I-424), so item 2 is default.
let (mut disp, mut cpu, mut bus) = setup();
let alrt = build_alrt_template(200, 0xCCCC);
disp.install_test_resource(&mut bus, *b"ALRT", 129, &alrt);
bus.write_word(crate::memory::globals::addr::ALERT_STAGE, 0);
bus.write_word(TEST_SP + 4, 129);
let result = disp.dispatch_dialog(true, 0x185, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(
bus.read_word(TEST_SP + 6) as i16,
2,
"Alert with stage 1 + bold flag set must return item 2"
);
}
#[test]
fn alert_steps_through_stages_and_caps_at_stage_4() {
// stages = 0xC4C4 → boxDrwn set in every nibble; bit 3 of
// stage 1/3 nibble = 0 (item 1), bit 3 of stage 2/4 nibble
// = 1 (item 2). Per IM:I I-422 okDismissal mask = 8.
let (mut disp, mut cpu, mut bus) = setup();
let alrt = build_alrt_template(200, 0xC4C4);
disp.install_test_resource(&mut bus, *b"ALRT", 130, &alrt);
bus.write_word(crate::memory::globals::addr::ALERT_STAGE, 0);
bus.write_word(TEST_SP + 4, 130);
let expected = [1i16, 2, 1, 2, 2, 2];
for (i, want) in expected.iter().enumerate() {
cpu.write_reg(Register::A7, TEST_SP);
let result = disp.dispatch_dialog(true, 0x185, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(
bus.read_word(TEST_SP + 6) as i16,
*want,
"call #{} (stage {}): expected item {}",
i + 1,
(i + 1).min(4),
want
);
}
// After at least 4 calls the AlertStage word is capped at 3.
assert_eq!(
bus.read_word(crate::memory::globals::addr::ALERT_STAGE),
3,
"AlertStage must cap at 3 (stages are 1..4 → word 0..3)"
);
}
#[test]
fn alert_increments_alert_stage_word_after_dispatch() {
// First call from stage 0 must leave AlertStage = 1. Read
// and write as 16-bit WORD per IM:I I-423 + MTb 1992 22620.
let (mut disp, mut cpu, mut bus) = setup();
let alrt = build_alrt_template(200, 0x4444);
disp.install_test_resource(&mut bus, *b"ALRT", 131, &alrt);
bus.write_word(crate::memory::globals::addr::ALERT_STAGE, 0);
bus.write_word(TEST_SP + 4, 131);
let _ = disp.dispatch_dialog(true, 0x185, &mut cpu, &mut bus);
assert_eq!(
bus.read_word(crate::memory::globals::addr::ALERT_STAGE),
1,
"Alert must increment AlertStage 0 → 1 after dispatch"
);
}
#[test]
fn alert_does_not_increment_alert_stage_when_alrt_missing() {
// No ALRT → -1 path must NOT touch AlertStage so the next
// call (with a real ALRT installed) sees the original
// stage. This is critical for apps that defensively call
// Alert(missingID) before Alert(realID).
let (mut disp, mut cpu, mut bus) = setup();
bus.write_word(crate::memory::globals::addr::ALERT_STAGE, 2);
bus.write_word(TEST_SP + 4, 999); // no ALRT 999
let result = disp.dispatch_dialog(true, 0x185, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(TEST_SP + 6) as i16, -1);
assert_eq!(
bus.read_word(crate::memory::globals::addr::ALERT_STAGE),
2,
"missing-ALRT must NOT increment AlertStage"
);
}
#[test]
fn alert_returns_minus_one_when_stage_box_drawn_clear_but_updates_stage() {
// IM:I I-418 and MTE 1992 p. 6-106: Alert calls the alert
// sound procedure for the stage and returns -1 when boxDrwn
// is clear, but it is still an occurrence of that alert.
let (mut disp, mut cpu, mut bus) = setup();
let alrt = build_alrt_template(200, 0x0000);
disp.install_test_resource(&mut bus, *b"ALRT", 133, &alrt);
bus.write_word(crate::memory::globals::addr::ALERT_STAGE, 0);
bus.write_word(crate::memory::globals::addr::ANUMBER, 0xCAFE);
bus.write_word(TEST_SP + 4, 133);
let result = disp.dispatch_dialog(true, 0x185, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
assert_eq!(
bus.read_word(TEST_SP + 6) as i16,
-1,
"boxDrwn clear must suppress the alert box and return -1"
);
assert_eq!(
bus.read_word(crate::memory::globals::addr::ALERT_STAGE),
1,
"suppressed present ALRT must still advance ACount"
);
assert_eq!(
bus.read_word(crate::memory::globals::addr::ANUMBER) as i16,
133,
"suppressed present ALRT must still record ANumber"
);
}
#[test]
fn alert_writes_anumber_with_alert_id_after_successful_dispatch() {
// ANumber at $0A98 records the resource ID of the last
// alert that occurred per IM:I I-423.
let (mut disp, mut cpu, mut bus) = setup();
let alrt = build_alrt_template(200, 0x4444);
disp.install_test_resource(&mut bus, *b"ALRT", 250, &alrt);
bus.write_word(crate::memory::globals::addr::ANUMBER, 0xCAFE);
bus.write_word(crate::memory::globals::addr::ALERT_STAGE, 0);
bus.write_word(TEST_SP + 4, 250);
let _ = disp.dispatch_dialog(true, 0x185, &mut cpu, &mut bus);
assert_eq!(
bus.read_word(crate::memory::globals::addr::ANUMBER) as i16,
250,
"Alert must write the alertID to ANumber per IM:I I-423"
);
}
#[test]
fn alert_does_not_overwrite_anumber_when_alrt_missing() {
// Missing-ALRT path returns -1 without overwriting ANumber
// (defensive: callers reading ANumber after a probe call
// must see the prior real value, not the failed probe ID).
let (mut disp, mut cpu, mut bus) = setup();
bus.write_word(crate::memory::globals::addr::ANUMBER, 0xC0DE);
bus.write_word(TEST_SP + 4, 999); // no ALRT 999
let _ = disp.dispatch_dialog(true, 0x185, &mut cpu, &mut bus);
assert_eq!(
bus.read_word(crate::memory::globals::addr::ANUMBER),
0xC0DE,
"missing-ALRT must NOT overwrite ANumber"
);
}
#[test]
fn alert_family_share_dispatch_path_so_stop_note_caution_match_alert() {
// The four icon variants ($A985 Alert / $A986 StopAlert /
// $A987 NoteAlert / $A988 CautionAlert) differ only by
// displayed icon — their dispatch into the ALRT template
// is identical. With the same ALRT installed and the same
// AlertStage all four must return the same item number.
let alrt = build_alrt_template(200, 0x4444);
let trap_words = [
(0x185, "Alert"),
(0x186, "StopAlert"),
(0x187, "NoteAlert"),
(0x188, "CautionAlert"),
];
for (trap, name) in trap_words.iter() {
let (mut disp, mut cpu, mut bus) = setup();
disp.install_test_resource(&mut bus, *b"ALRT", 132, &alrt);
bus.write_word(crate::memory::globals::addr::ALERT_STAGE, 0);
bus.write_word(TEST_SP + 4, 132);
let result = disp.dispatch_dialog(true, *trap, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(
bus.read_word(TEST_SP + 6) as i16,
1,
"{name} must return item 1 (bold flag clear, stage 1) — \
same as Alert"
);
}
}
#[test]
fn alert_pops_eight_bytes_per_pascal_signature() {
// FUNCTION Alert(alertID: INTEGER; filterProc: ProcPtr): INTEGER;
// Pascal stack frame: result(2) + filterProc(4) +
// alertID(2) + return PC slot mock at SP+0 (caller-set
// here). Trap pops 6 bytes (filterProc + alertID + return)
// leaving result at new SP+0 = TEST_SP+6.
let (mut disp, mut cpu, mut bus) = setup();
bus.write_word(TEST_SP + 4, 999); // alertID
let pre_a7 = cpu.read_reg(Register::A7);
let _ = disp.dispatch_dialog(true, 0x185, &mut cpu, &mut bus);
let post_a7 = cpu.read_reg(Register::A7);
assert_eq!(
post_a7,
pre_a7 + 6,
"Alert must advance A7 by 6 bytes (filterProc + alertID + return)"
);
}
// ---- InitDialogs ($A97B) — IM:I I-411 init contract ----
#[test]
fn init_dialogs_stores_resume_proc_at_lowmem_a8c() {
// PROCEDURE InitDialogs(resumeProc: ProcPtr);
// The non-NIL ProcPtr argument must land at $0A8C
// (ResumeProc global) per IM:I I-411 + the assembly note.
let (mut disp, mut cpu, mut bus) = setup();
bus.write_long(crate::memory::globals::addr::RESUME_PROC, 0xCAFEBABE);
bus.write_long(TEST_SP, 0x00123456); // resumeProc parameter
let _ = disp.dispatch_dialog(true, 0x17B, &mut cpu, &mut bus);
assert_eq!(
bus.read_long(crate::memory::globals::addr::RESUME_PROC),
0x00123456,
"InitDialogs must store resumeProc at $0A8C"
);
}
#[test]
fn init_dialogs_accepts_nil_resume_proc() {
// The IM-canonical default is NIL ("no resume procedure
// is desired"). Pin that NIL is stored as 0 — not a
// sentinel like -1 — so the System Error Handler's
// `if (resume) call(*resume)` path takes the no-op branch.
let (mut disp, mut cpu, mut bus) = setup();
bus.write_long(crate::memory::globals::addr::RESUME_PROC, 0xDEADBEEF);
bus.write_long(TEST_SP, 0); // NIL
let _ = disp.dispatch_dialog(true, 0x17B, &mut cpu, &mut bus);
assert_eq!(
bus.read_long(crate::memory::globals::addr::RESUME_PROC),
0,
"InitDialogs must store NIL resumeProc as 0 at $0A8C"
);
}
#[test]
fn init_dialogs_zeros_dabeeper_at_lowmem_a9c() {
// IM:I I-411: "It installs the standard sound procedure."
// Systemless's HLE has no menu-bar-blink sound, so we install
// NIL (== silent) per the IM:I I-411 ErrorSound semantic
// ("If you pass NIL for soundProc, there will be no sound
// ... at all"). A subsequent ErrorSound ($A98C) call can
// override.
let (mut disp, mut cpu, mut bus) = setup();
bus.write_long(crate::memory::globals::addr::DA_BEEPER, 0xDEADBEEF);
bus.write_long(TEST_SP, 0);
let _ = disp.dispatch_dialog(true, 0x17B, &mut cpu, &mut bus);
assert_eq!(
bus.read_long(crate::memory::globals::addr::DA_BEEPER),
0,
"InitDialogs must store NIL (silent default) at DABeeper $0A9C"
);
}
#[test]
fn init_dialogs_zeros_alert_stage_at_lowmem_a9a() {
// First call to Alert/StopAlert/NoteAlert/CautionAlert
// after InitDialogs must start at stage 1 (= AlertStage
// word value 0). This is critical for apps that call
// InitDialogs at re-launch but already have a stale stage
// byte from a prior crashed run.
let (mut disp, mut cpu, mut bus) = setup();
bus.write_word(crate::memory::globals::addr::ALERT_STAGE, 3);
bus.write_long(TEST_SP, 0);
let _ = disp.dispatch_dialog(true, 0x17B, &mut cpu, &mut bus);
assert_eq!(
bus.read_word(crate::memory::globals::addr::ALERT_STAGE),
0,
"InitDialogs must zero AlertStage at $0A9A so the next Alert starts at stage 1"
);
}
#[test]
fn init_dialogs_zeros_dastrings_array_at_lowmem_aa0() {
// IM:I I-411: "It passes empty strings to ParamText."
// The DAStrings global at $0AA0 is a 16-byte array of 4
// Handles; zeroing all 4 = ParamText('','','','').
let (mut disp, mut cpu, mut bus) = setup();
for i in 0..4u32 {
bus.write_long(
crate::memory::globals::addr::DA_STRINGS + i * 4,
0xDEAD0000 | i,
);
}
bus.write_long(TEST_SP, 0);
let _ = disp.dispatch_dialog(true, 0x17B, &mut cpu, &mut bus);
for i in 0..4u32 {
assert_eq!(
bus.read_long(crate::memory::globals::addr::DA_STRINGS + i * 4),
0,
"InitDialogs must zero DAStrings[{}] at $0AA0+{}",
i,
i * 4
);
}
}
#[test]
fn init_dialogs_pops_four_bytes_resume_proc_param() {
// PROCEDURE → no result slot, single 4-byte ProcPtr arg.
let (mut disp, mut cpu, mut bus) = setup();
bus.write_long(TEST_SP, 0);
let pre_a7 = cpu.read_reg(Register::A7);
let _ = disp.dispatch_dialog(true, 0x17B, &mut cpu, &mut bus);
assert_eq!(
cpu.read_reg(Register::A7),
pre_a7 + 4,
"InitDialogs must pop 4 bytes (resumeProc ProcPtr)"
);
}
// ---- ErrorSound ($A98C) ----
#[test]
fn error_sound_stores_sound_proc_pointer_in_dabeeper() {
// Inside Macintosh Volume I, I-411: ErrorSound sets the current
// alert sound procedure, and the assembly-language note says this
// pointer is stored in DABeeper.
let (mut disp, mut cpu, mut bus) = setup();
let sound_proc = 0x0012_3456u32;
bus.write_long(crate::memory::globals::addr::DA_BEEPER, 0);
bus.write_long(TEST_SP, sound_proc);
let pre_a7 = cpu.read_reg(Register::A7);
let result = disp.dispatch_dialog(true, 0x18C, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(
bus.read_long(crate::memory::globals::addr::DA_BEEPER),
sound_proc
);
assert_eq!(cpu.read_reg(Register::A7), pre_a7 + 4);
}
#[test]
fn error_sound_nil_clears_dabeeper_to_disable_alert_sound() {
// Inside Macintosh Volume I, I-411: passing NIL for soundProc means
// "no sound (or menu bar blinking) at all".
let (mut disp, mut cpu, mut bus) = setup();
bus.write_long(crate::memory::globals::addr::DA_BEEPER, 0xDEADBEEF);
bus.write_long(TEST_SP, 0);
let result = disp.dispatch_dialog(true, 0x18C, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_long(crate::memory::globals::addr::DA_BEEPER), 0);
}
#[test]
fn error_sound_overrides_init_dialogs_default_sound_proc() {
// IM:I I-411: InitDialogs installs the default alert sound procedure,
// and ErrorSound replaces it with the caller's soundProc.
let (mut disp, mut cpu, mut bus) = setup();
let sound_proc = 0x0012_3456u32;
bus.write_long(crate::memory::globals::addr::DA_BEEPER, 0xDEADBEEF);
bus.write_long(TEST_SP, 0);
let result = disp.dispatch_dialog(true, 0x17B, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_long(crate::memory::globals::addr::DA_BEEPER), 0);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, sound_proc);
let result = disp.dispatch_dialog(true, 0x18C, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(
bus.read_long(crate::memory::globals::addr::DA_BEEPER),
sound_proc
);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, 0);
let result = disp.dispatch_dialog(true, 0x18C, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_long(crate::memory::globals::addr::DA_BEEPER), 0);
}
// ---- IsDialogEvent ($A97F) ----
#[test]
fn is_dialog_event_returns_false() {
let (mut disp, mut cpu, mut bus) = setup();
// SP+0: event_ptr (4 bytes)
bus.write_long(TEST_SP, 0x300000);
let result = disp.dispatch_dialog(true, 0x17F, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
assert_eq!(bus.read_word(TEST_SP + 4), 0);
}
#[test]
fn is_dialog_event_true_for_mouse_down_in_front_dialog() {
// Inside Macintosh Volume I, I-417: mouse-down in content region of
// active dialog window is a dialog event (TRUE).
let (mut disp, mut cpu, mut bus) = setup();
let dialog_ptr = bus.alloc(170);
let event_ptr = 0x300000u32;
disp.front_window = dialog_ptr;
disp.enable_input_trace_capture();
bus.write_word(dialog_ptr + 8, (-100i16) as u16);
bus.write_word(dialog_ptr + 10, (-200i16) as u16);
bus.write_word(dialog_ptr + 16, 0);
bus.write_word(dialog_ptr + 18, 0);
bus.write_word(dialog_ptr + 20, 80);
bus.write_word(dialog_ptr + 22, 120);
disp.dialog_items.insert(dialog_ptr, Vec::new());
bus.write_word(event_ptr, 1);
bus.write_long(event_ptr + 2, 0);
bus.write_long(event_ptr + 6, 0);
bus.write_word(event_ptr + 10, 120);
bus.write_word(event_ptr + 12, 240);
bus.write_word(event_ptr + 14, 0x0080);
bus.write_long(TEST_SP, event_ptr);
let result = disp.dispatch_dialog(true, 0x17F, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(TEST_SP + 4), 0xFFFF);
let trace = disp.input_trace_text();
assert!(trace.contains("A97F action=guest_event:mouseDown"));
assert!(trace.contains("tracking=menu:idle dialog:idle control:idle"));
assert!(trace.contains("result=true outcome=is_dialog_event"));
}
#[test]
fn is_dialog_event_false_for_mouse_down_outside_front_dialog() {
// Inside Macintosh Volume I, I-417: only mouse-down events in the
// content region of an active dialog window return TRUE.
let (mut disp, mut cpu, mut bus) = setup();
let dialog_ptr = bus.alloc(170);
let event_ptr = 0x300000u32;
disp.front_window = dialog_ptr;
disp.enable_input_trace_capture();
bus.write_word(dialog_ptr + 8, (-100i16) as u16);
bus.write_word(dialog_ptr + 10, (-200i16) as u16);
bus.write_word(dialog_ptr + 16, 0);
bus.write_word(dialog_ptr + 18, 0);
bus.write_word(dialog_ptr + 20, 80);
bus.write_word(dialog_ptr + 22, 120);
disp.dialog_items.insert(dialog_ptr, Vec::new());
bus.write_word(event_ptr, 1); // mouseDown
bus.write_long(event_ptr + 2, 0);
bus.write_long(event_ptr + 6, 0);
bus.write_word(event_ptr + 10, 40); // well outside dialog content
bus.write_word(event_ptr + 12, 40);
bus.write_word(event_ptr + 14, 0x0080);
bus.write_long(TEST_SP, event_ptr);
let result = disp.dispatch_dialog(true, 0x17F, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(TEST_SP + 4), 0);
}
#[test]
fn is_dialog_event_true_for_update_event_targeting_dialog_window() {
// Inside Macintosh Volume I, I-417: activate/update events for a
// dialog window are dialog events.
let (mut disp, mut cpu, mut bus) = setup();
let dialog_ptr = bus.alloc(170);
let event_ptr = 0x300000u32;
disp.dialog_items.insert(dialog_ptr, Vec::new());
bus.write_word(event_ptr, 6); // updateEvt
bus.write_long(event_ptr + 2, dialog_ptr);
bus.write_long(TEST_SP, event_ptr);
let result = disp.dispatch_dialog(true, 0x17F, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(TEST_SP + 4), 0xFFFF);
}
#[test]
fn is_dialog_event_true_for_activate_event_targeting_dialog_window() {
// Inside Macintosh Volume I, I-417: activate events for a dialog
// window are dialog events as well.
let (mut disp, mut cpu, mut bus) = setup();
let dialog_ptr = bus.alloc(170);
let event_ptr = 0x300000u32;
disp.dialog_items.insert(dialog_ptr, Vec::new());
bus.write_word(event_ptr, 8); // activateEvt
bus.write_long(event_ptr + 2, dialog_ptr);
bus.write_long(TEST_SP, event_ptr);
let result = disp.dispatch_dialog(true, 0x17F, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(TEST_SP + 4), 0xFFFF);
}
// ---- DialogSelect ($A980) ----
#[test]
fn dialog_select_returns_false() {
let (mut disp, mut cpu, mut bus) = setup();
let result = disp.dispatch_dialog(true, 0x180, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 12);
assert_eq!(bus.read_word(TEST_SP + 12), 0);
}
#[test]
fn dialog_select_returns_hit_for_enabled_user_item() {
let (mut disp, mut cpu, mut bus) = setup();
let dialog_ptr = bus.alloc(170);
let event_ptr = 0x300000u32;
let dialog_out_ptr = 0x300100u32;
let item_hit_ptr = 0x300104u32;
disp.front_window = dialog_ptr;
disp.enable_input_trace_capture();
bus.write_word(dialog_ptr + 8, (-100i16) as u16);
bus.write_word(dialog_ptr + 10, (-200i16) as u16);
bus.write_word(dialog_ptr + 16, 0);
bus.write_word(dialog_ptr + 18, 0);
bus.write_word(dialog_ptr + 20, 100);
bus.write_word(dialog_ptr + 22, 160);
disp.dialog_items.insert(
dialog_ptr,
vec![DialogItem {
item_type: 0,
rect: (20, 30, 60, 110),
text: String::new(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
}],
);
bus.write_word(event_ptr, 1);
bus.write_long(event_ptr + 2, 0);
bus.write_long(event_ptr + 6, 0);
bus.write_word(event_ptr + 10, 130);
bus.write_word(event_ptr + 12, 240);
bus.write_word(event_ptr + 14, 0x0080);
bus.write_long(TEST_SP, item_hit_ptr);
bus.write_long(TEST_SP + 4, dialog_out_ptr);
bus.write_long(TEST_SP + 8, event_ptr);
let result = disp.dispatch_dialog(true, 0x180, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 12);
assert_eq!(bus.read_word(TEST_SP + 12), 0xFFFF);
assert_eq!(bus.read_long(dialog_out_ptr), dialog_ptr);
assert_eq!(bus.read_word(item_hit_ptr), 1);
let trace = disp.input_trace_text();
assert!(trace.contains("A980 action=guest_event:mouseDown"));
assert!(trace.contains("item_hit=1 item_type=$00 disabled=false outcome=enabled_item"));
}
fn dialog_select_enabled_user_item_hit_for_theme(theme_id: UiThemeId) -> (u16, u32, u16, u32) {
let (mut disp, mut cpu, mut bus) = setup();
disp.set_ui_theme_id(theme_id);
let dialog_ptr = bus.alloc(170);
let event_ptr = 0x300000u32;
let dialog_out_ptr = 0x300100u32;
let item_hit_ptr = 0x300104u32;
disp.front_window = dialog_ptr;
bus.write_word(dialog_ptr + 8, (-100i16) as u16);
bus.write_word(dialog_ptr + 10, (-200i16) as u16);
bus.write_word(dialog_ptr + 16, 0);
bus.write_word(dialog_ptr + 18, 0);
bus.write_word(dialog_ptr + 20, 100);
bus.write_word(dialog_ptr + 22, 160);
disp.dialog_items.insert(
dialog_ptr,
vec![DialogItem {
item_type: 0,
rect: (20, 30, 60, 110),
text: String::new(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
}],
);
bus.write_word(event_ptr, 1);
bus.write_long(event_ptr + 2, 0);
bus.write_long(event_ptr + 6, 0);
bus.write_word(event_ptr + 10, 130);
bus.write_word(event_ptr + 12, 240);
bus.write_word(event_ptr + 14, 0x0080);
bus.write_long(TEST_SP, item_hit_ptr);
bus.write_long(TEST_SP + 4, dialog_out_ptr);
bus.write_long(TEST_SP + 8, event_ptr);
disp.dispatch_dialog(true, 0x180, &mut cpu, &mut bus)
.unwrap()
.unwrap();
(
bus.read_word(TEST_SP + 12),
bus.read_long(dialog_out_ptr),
bus.read_word(item_hit_ptr),
cpu.read_reg(Register::A7),
)
}
fn dialog_select_enabled_checkbox_hit_for_theme(
theme_id: UiThemeId,
) -> (u16, u32, u16, u32, u16, u16) {
let (mut disp, mut cpu, mut bus) = setup();
disp.set_ui_theme_id(theme_id);
let dialog_ptr = bus.alloc(170);
let event_ptr = 0x300000u32;
let dialog_out_ptr = 0x300100u32;
let item_hit_ptr = 0x300104u32;
let box_ptr = 0x300108u32;
let item_ptr = 0x300110u32;
let type_ptr = 0x300114u32;
disp.front_window = dialog_ptr;
bus.write_word(dialog_ptr + 8, 0);
bus.write_word(dialog_ptr + 10, 0);
bus.write_word(dialog_ptr + 16, 0);
bus.write_word(dialog_ptr + 18, 0);
bus.write_word(dialog_ptr + 20, 100);
bus.write_word(dialog_ptr + 22, 160);
disp.dialog_items.insert(
dialog_ptr,
vec![DialogItem {
item_type: 5,
rect: (20, 30, 40, 130),
text: "Enabled".to_string(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
}],
);
disp.dialog_control_values.insert((dialog_ptr, 1), 1);
bus.write_long(TEST_SP, box_ptr);
bus.write_long(TEST_SP + 4, item_ptr);
bus.write_long(TEST_SP + 8, type_ptr);
bus.write_word(TEST_SP + 12, 1);
bus.write_long(TEST_SP + 14, dialog_ptr);
disp.dispatch_dialog(true, 0x18D, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 18);
let ctrl_handle = bus.read_long(item_ptr);
let ctrl_ptr = bus.read_long(ctrl_handle);
assert_ne!(ctrl_ptr, 0);
assert_eq!(bus.read_word(type_ptr), 5);
assert_eq!(bus.read_word(ctrl_ptr + 18), 1);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(event_ptr, 1);
bus.write_long(event_ptr + 2, 0);
bus.write_long(event_ptr + 6, 0);
bus.write_word(event_ptr + 10, 30);
bus.write_word(event_ptr + 12, 40);
bus.write_word(event_ptr + 14, 0x0080);
bus.write_long(TEST_SP, item_hit_ptr);
bus.write_long(TEST_SP + 4, dialog_out_ptr);
bus.write_long(TEST_SP + 8, event_ptr);
disp.dispatch_dialog(true, 0x180, &mut cpu, &mut bus)
.unwrap()
.unwrap();
(
bus.read_word(TEST_SP + 12),
bus.read_long(dialog_out_ptr),
bus.read_word(item_hit_ptr),
cpu.read_reg(Register::A7),
bus.read_word(ctrl_ptr + 18),
disp.dialog_control_values
.get(&(dialog_ptr, 1))
.copied()
.unwrap_or(-1) as u16,
)
}
fn findditem_results_for_theme(theme_id: UiThemeId) -> Vec<(i16, u32)> {
let (mut disp, mut cpu, mut bus) = setup();
disp.set_ui_theme_id(theme_id);
let dialog_ptr = 0x220400u32;
disp.dialog_items.insert(
dialog_ptr,
vec![
DialogItem {
item_type: 0x80 | 4,
rect: (10, 20, 40, 60),
text: "DisabledFirst".into(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
},
DialogItem {
item_type: 4,
rect: (20, 30, 55, 80),
text: "EnabledSecond".into(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
},
DialogItem {
item_type: 16,
rect: (60, 16384 + 20, 80, 16384 + 100),
text: "HiddenEdit".into(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
},
],
);
let mut out = Vec::new();
for (pt_v, pt_h) in [(25i16, 35i16), (45, 70), (65, 40), (90, 110)] {
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, pt_v as u16);
bus.write_word(TEST_SP + 2, pt_h as u16);
bus.write_long(TEST_SP + 4, dialog_ptr);
bus.write_word(TEST_SP + 8, 0xBEEF);
disp.dispatch_dialog(true, 0x184, &mut cpu, &mut bus)
.unwrap()
.unwrap();
out.push((
bus.read_word(TEST_SP + 8) as i16,
cpu.read_reg(Register::A7),
));
}
out
}
#[derive(Debug, PartialEq, Eq)]
struct DItemGetSnapshot {
item_type: u16,
item: u32,
rect: (u16, u16, u16, u16),
stack_after: u32,
}
#[derive(Debug, PartialEq, Eq)]
struct DItemUserItemRecordSnapshot {
initial_get: DItemGetSnapshot,
set_stack_after: u32,
stored_type: u8,
stored_proc_ptr: u32,
stored_rect: (i16, i16, i16, i16),
post_set_get: DItemGetSnapshot,
}
#[derive(Debug, PartialEq, Eq)]
struct DItemTextControlRecordSnapshot {
text_initial_get: DItemGetSnapshot,
text_set_stack_after: u32,
text_old_handle_mapped_after: bool,
text_new_handle_map_value_after: Option<(u32, usize)>,
text_ditl_handle_after: u32,
text_ditl_rect_after: (u16, u16, u16, u16),
text_ditl_type_after: u8,
text_stored_type_after: u8,
text_stored_rect_after: (i16, i16, i16, i16),
text_cached_text_after: String,
text_handle_bytes_after: Vec<u8>,
text_post_set_get: DItemGetSnapshot,
control_initial_get: DItemGetSnapshot,
control_set_stack_after: u32,
control_old_handle_mapped_after: bool,
control_new_handle_map_value_after: Option<(u32, i16)>,
control_ditl_handle_after: u32,
control_ditl_rect_after: (u16, u16, u16, u16),
control_ditl_type_after: u8,
control_stored_type_after: u8,
control_stored_rect_after: (i16, i16, i16, i16),
control_record_rect_after: (u16, u16, u16, u16),
control_value_after: Option<i16>,
control_post_set_get: DItemGetSnapshot,
}
#[derive(Debug, PartialEq, Eq)]
struct DItemVisibilitySnapshot {
initial_get: DItemGetSnapshot,
hide_stack_after: u32,
hidden_item_rect: (i16, i16, i16, i16),
hidden_saved_rect: Option<(i16, i16, i16, i16)>,
hidden_control_rect: (u16, u16, u16, u16),
hidden_get: DItemGetSnapshot,
hidden_find: (i16, u32),
show_stack_after: u32,
restored_item_rect: (i16, i16, i16, i16),
restored_saved_rect: Option<(i16, i16, i16, i16)>,
restored_control_rect: (u16, u16, u16, u16),
restored_get: DItemGetSnapshot,
restored_find: (i16, u32),
}
#[derive(Debug, PartialEq, Eq)]
struct ParamTextSnapshot {
initial_slots: [Vec<u8>; 4],
initial_da_handles: [u32; 4],
initial_da_strings: [Vec<u8>; 4],
initial_stack_after: u32,
initial_substitution: String,
nil_slots: [Vec<u8>; 4],
nil_da_handles: [u32; 4],
nil_da_strings: [Vec<u8>; 4],
nil_stack_after: u32,
nil_substitution: String,
}
#[derive(Debug, PartialEq, Eq)]
struct DialogItemTextSnapshot {
initial_get_text: Vec<u8>,
initial_get_stack_after: u32,
set_stack_after: u32,
handle_size_after_set: Option<u32>,
handle_bytes_after_set: Vec<u8>,
cached_text_after_set: String,
post_set_get_text: Vec<u8>,
post_set_get_stack_after: u32,
}
#[derive(Debug, PartialEq, Eq)]
struct DialogItemTextSelectionSnapshot {
whole_selection: (i16, i16),
whole_edit_field: u16,
whole_te_selection: (u16, u16),
whole_stack_after: u32,
normalized_selection: (i16, i16),
normalized_edit_field: u16,
normalized_te_selection: (u16, u16),
normalized_stack_after: u32,
non_edit_selection: (i16, i16),
non_edit_edit_field: u16,
non_edit_te_selection: (u16, u16),
non_edit_stack_after: u32,
}
#[derive(Debug, PartialEq, Eq)]
struct IsDialogEventSnapshot {
null_event: (u16, u32),
key_down: (u16, u32),
mouse_inside: (u16, u32),
mouse_outside: (u16, u32),
update_target: (u16, u32),
activate_target: (u16, u32),
}
#[derive(Debug, PartialEq, Eq)]
struct DialogSelectWindowEventSnapshot {
dialog_ptr: u32,
update_result: u16,
update_dialog_out: u32,
update_item_hit: u16,
update_stack_after: u32,
update_deferred_after: bool,
update_region_after: Option<(i16, i16, i16, i16)>,
update_vis_region_after: Option<(i16, i16, i16, i16)>,
update_the_port_after: u32,
update_current_port_after: u32,
update_saved_vis_after: bool,
update_queued_draw_procs: Vec<(u32, u32, i16)>,
update_item_count_after: usize,
activate_result: u16,
activate_dialog_out: u32,
activate_item_hit: u16,
activate_stack_after: u32,
}
#[derive(Debug, PartialEq, Eq)]
struct DialogSelectEditTextMouseSnapshot {
mouse_result: u16,
mouse_dialog_out: u32,
mouse_item_hit: u16,
mouse_stack_after: u32,
mouse_edit_field: u16,
mouse_item_selection: (i16, i16),
mouse_te_text: Vec<u8>,
mouse_te_length: u16,
mouse_te_selection: (u16, u16),
null_result: u16,
null_dialog_out: u32,
null_item_hit: u16,
null_stack_after: u32,
null_edit_field: u16,
null_te_text: Vec<u8>,
null_te_length: u16,
null_te_selection: (u16, u16),
}
#[derive(Debug, PartialEq, Eq)]
struct ModalDialogEditTextMouseSnapshot {
item_hit: u16,
stack_after: u32,
tracking_finished: bool,
retained_visible_snapshot: bool,
saved_background_retained: bool,
queued_mouse_up_consumed: bool,
edit_field: u16,
item_selection: (i16, i16),
te_text: Vec<u8>,
te_length: u16,
te_selection: (u16, u16),
handle_bytes: Vec<u8>,
}
#[derive(Debug, PartialEq, Eq)]
struct ModalDialogKeyboardButtonSnapshot {
return_key: ModalDialogKeyboardButtonCase,
enter_key: ModalDialogKeyboardButtonCase,
escape_key: ModalDialogKeyboardButtonCase,
command_period: ModalDialogKeyboardButtonCase,
}
#[derive(Debug, PartialEq, Eq)]
struct ModalDialogKeyboardButtonCase {
flash_item_after_key: i16,
stack_after_key: u32,
item_hit_after_key: u16,
item_hit_after_flash: u16,
stack_after_flash: u32,
tracking_finished: bool,
}
#[derive(Debug, PartialEq, Eq)]
struct ModalDialogUpdateEventSnapshot {
dialog_ptr: u32,
item_hit_after: u16,
stack_after: u32,
tracking_finished: bool,
rendered_pixels_final: bool,
retained_pixel_after: u8,
retained_snapshot_pixel_after: u8,
update_region_after: Option<(i16, i16, i16, i16)>,
vis_region_after: Option<(i16, i16, i16, i16)>,
the_port_after: u32,
current_port_after: u32,
saved_vis_after: bool,
}
#[derive(Debug, PartialEq, Eq)]
struct DialogLifecycleCleanupSnapshot {
close_stack_after: u32,
close_tracking_cleared: bool,
close_retained_click_cleared: bool,
close_visible_snapshot_cleared: bool,
close_modal_entered_cleared: bool,
close_saved_pixels_cleared: bool,
close_window_list_contains_dialog: bool,
close_front_window_after: u32,
close_the_port_after: u32,
close_current_port_after: u32,
close_update_event_for_previous_window: bool,
close_dialog_items_present_after: bool,
dispose_stack_after: u32,
dispose_tracking_cleared: bool,
dispose_retained_click_cleared: bool,
dispose_visible_snapshot_cleared: bool,
dispose_modal_entered_cleared: bool,
dispose_saved_pixels_cleared: bool,
dispose_window_list_contains_dialog: bool,
dispose_front_window_after: u32,
dispose_the_port_after: u32,
dispose_current_port_after: u32,
dispose_update_event_for_previous_window: bool,
dispose_dialog_items_present_after: bool,
dispose_other_dialog_items_present_after: bool,
dispose_dialog_item_handle_present_after: bool,
dispose_other_dialog_item_handle_present_after: bool,
dispose_dialog_control_handle_present_after: bool,
dispose_other_dialog_control_handle_present_after: bool,
dispose_dialog_control_value_present_after: bool,
dispose_other_dialog_control_value_present_after: bool,
dispose_hidden_rect_present_after: bool,
dispose_other_hidden_rect_present_after: bool,
dispose_cancel_item_present_after: bool,
dispose_other_cancel_item_present_after: bool,
dispose_pending_popup_cleared: bool,
}
#[derive(Debug, PartialEq, Eq)]
struct DialogCreationSnapshot {
new_dialog_ptr: u32,
new_stack_after: u32,
new_result_slot: u32,
new_window_list: Vec<u32>,
new_front_window: u32,
new_current_port: u32,
new_the_port: u32,
new_visible_byte: u8,
new_goaway_byte: u8,
new_refcon: u32,
new_window_kind: i16,
new_proc_id: i16,
new_port_rect: (i16, i16, i16, i16),
new_items_handle: u32,
new_items_data_ptr: u32,
new_first_item_handle_nonzero: bool,
new_cached_items: Vec<(u8, (i16, i16, i16, i16), String, i16)>,
new_update_region: Option<(i16, i16, i16, i16)>,
new_update_event_queued: bool,
new_edit_field: u16,
new_default_item: u16,
get_dialog_ptr: u32,
get_stack_after: u32,
get_result_slot: u32,
get_window_list: Vec<u32>,
get_front_window: u32,
get_current_port: u32,
get_the_port: u32,
get_visible_byte: u8,
get_refcon: u32,
get_window_kind: i16,
get_proc_id: i16,
get_port_rect: (i16, i16, i16, i16),
get_items_handle: u32,
get_items_data_ptr: u32,
get_uses_distinct_ditl_copy: bool,
get_original_ditl_handle_field: u32,
get_first_item_handle_nonzero: bool,
get_cached_items: Vec<(u8, (i16, i16, i16, i16), String, i16)>,
get_update_region: Option<(i16, i16, i16, i16)>,
get_update_event_queued: bool,
get_edit_field: u16,
get_default_item: u16,
}
#[derive(Debug, PartialEq, Eq)]
struct AlertTrapCallSnapshot {
trap_word: u16,
result: i16,
stack_after: u32,
alert_stage_after: u16,
anumber_after: u16,
}
#[derive(Debug, PartialEq, Eq)]
struct AlertResourcePrepSnapshot {
could_present: (i16, u32),
free_present: (i16, u32),
could_missing: (i16, u32),
free_missing: (i16, u32),
}
#[derive(Debug, PartialEq, Eq)]
struct AlertFamilySnapshot {
staged_calls: Vec<AlertTrapCallSnapshot>,
missing_call: AlertTrapCallSnapshot,
resource_prep: AlertResourcePrepSnapshot,
}
#[derive(Debug, PartialEq, Eq)]
struct DialogInitSoundSnapshot {
init_stack_after: u32,
resume_proc_after_init: u32,
da_beeper_after_init: u32,
alert_stage_after_init: u16,
anumber_after_init: u16,
da_strings_after_init: [u32; 4],
error_sound_stack_after: u32,
da_beeper_after_error_sound: u32,
nil_error_sound_stack_after: u32,
da_beeper_after_nil_error_sound: u32,
}
#[derive(Debug, PartialEq, Eq)]
struct DialogDispatchDefaultCancelSnapshot {
default_result: u16,
default_stack_after: u32,
default_adef_item: u16,
default_tracking_item: i16,
cancel_result: u16,
cancel_stack_after: u32,
cancel_map_item: Option<i16>,
cancel_tracking_item: i16,
tracks_result: u16,
tracks_stack_after: u32,
tracks_adef_item: u16,
tracks_cancel_map_item: Option<i16>,
tracks_tracking_items: (i16, i16),
}
#[derive(Debug, PartialEq, Eq)]
struct UpdtDialogUpdateRegionSnapshot {
stack_after: u32,
the_port_after: u32,
current_port_after: u32,
deferred_after: bool,
queued_draw_procs: Vec<(u32, u32, i16)>,
item_count_after: usize,
inside_update_pixel_after: u8,
outside_update_pixel_after: u8,
outside_item_pixel_after: u8,
}
#[derive(Debug, PartialEq, Eq)]
struct DialogSelectEditTextKeySnapshot {
keydown_result: u16,
keydown_dialog_out: u32,
keydown_item_hit: u16,
keydown_stack_after: u32,
keydown_handle_bytes: Vec<u8>,
keydown_cached_text: String,
keydown_item_selection: (i16, i16),
keydown_te_text: Vec<u8>,
keydown_te_length: u16,
keydown_te_selection: (u16, u16),
autokey_result: u16,
autokey_dialog_out: u32,
autokey_item_hit: u16,
autokey_stack_after: u32,
autokey_handle_bytes: Vec<u8>,
autokey_cached_text: String,
autokey_item_selection: (i16, i16),
autokey_te_text: Vec<u8>,
autokey_te_length: u16,
autokey_te_selection: (u16, u16),
}
#[derive(Debug, PartialEq, Eq)]
struct TextEditScrapEditCase {
text: Vec<u8>,
selection: (u16, u16),
scrap_length: u16,
scrap_bytes: Vec<u8>,
stack_after: u32,
}
#[derive(Debug, PartialEq, Eq)]
struct TextEditPrivateScrapEditingSnapshot {
copy: TextEditScrapEditCase,
cut: TextEditScrapEditCase,
paste: TextEditScrapEditCase,
delete: TextEditScrapEditCase,
}
fn paramtext_da_strings(bus: &MacMemoryBus) -> ([u32; 4], [Vec<u8>; 4]) {
let mut handles = [0u32; 4];
let mut strings: [Vec<u8>; 4] = std::array::from_fn(|_| Vec::new());
for i in 0..4 {
let handle = bus.read_long(crate::memory::globals::addr::DA_STRINGS + i as u32 * 4);
handles[i] = handle;
if handle != 0 {
let data_ptr = bus.read_long(handle);
if data_ptr != 0 {
strings[i] = bus.read_pstring(data_ptr);
}
}
}
(handles, strings)
}
fn paramtext_results_for_theme(theme_id: UiThemeId) -> ParamTextSnapshot {
let (mut disp, mut cpu, mut bus) = setup();
disp.set_ui_theme_id(theme_id);
let p0 = bus.alloc(32);
let p1 = bus.alloc(32);
let p2 = bus.alloc(32);
let p3 = bus.alloc(32);
bus.write_pstring(p0, b"Doc");
bus.write_pstring(p1, b"42");
bus.write_pstring(p2, b"");
bus.write_pstring(p3, b"Tail");
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, p3);
bus.write_long(TEST_SP + 4, p2);
bus.write_long(TEST_SP + 8, p1);
bus.write_long(TEST_SP + 12, p0);
disp.dispatch_dialog(true, 0x18B, &mut cpu, &mut bus)
.unwrap()
.unwrap();
let initial_slots = [
disp.param_text[0].clone(),
disp.param_text[1].clone(),
disp.param_text[2].clone(),
disp.param_text[3].clone(),
];
let (initial_da_handles, initial_da_strings) = paramtext_da_strings(&bus);
let initial_stack_after = cpu.read_reg(Register::A7);
let initial_substitution = disp
.apply_param_text("Cannot open ^0 (^1)^2^3 ^9")
.into_owned();
let new0 = bus.alloc(32);
bus.write_pstring(new0, b"NewDoc");
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, 0); // param3 = NIL; preserve previous slot
bus.write_long(TEST_SP + 4, 0); // param2 = NIL; preserve previous slot
bus.write_long(TEST_SP + 8, 0); // param1 = NIL; preserve previous slot
bus.write_long(TEST_SP + 12, new0);
disp.dispatch_dialog(true, 0x18B, &mut cpu, &mut bus)
.unwrap()
.unwrap();
let nil_slots = [
disp.param_text[0].clone(),
disp.param_text[1].clone(),
disp.param_text[2].clone(),
disp.param_text[3].clone(),
];
let (nil_da_handles, nil_da_strings) = paramtext_da_strings(&bus);
let nil_stack_after = cpu.read_reg(Register::A7);
let nil_substitution = disp
.apply_param_text("Cannot open ^0 (^1)^2^3 ^9")
.into_owned();
ParamTextSnapshot {
initial_slots,
initial_da_handles,
initial_da_strings,
initial_stack_after,
initial_substitution,
nil_slots,
nil_da_handles,
nil_da_strings,
nil_stack_after,
nil_substitution,
}
}
fn dialog_item_text_results_for_theme(theme_id: UiThemeId) -> DialogItemTextSnapshot {
let (mut disp, mut cpu, mut bus) = setup();
disp.set_ui_theme_id(theme_id);
let dialog_ptr = bus.alloc(170);
let text_handle = bus.alloc(4);
let text_ptr = bus.alloc(3);
let out_ptr = bus.alloc(256);
let new_text_ptr = bus.alloc(32);
bus.write_long(text_handle, text_ptr);
bus.write_bytes(text_ptr, b"Old");
disp.dialog_items.insert(
dialog_ptr,
vec![DialogItem {
item_type: 16,
rect: (10, 20, 30, 120),
text: "Old".to_string(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
}],
);
disp.dialog_item_handles
.insert(text_handle, (dialog_ptr, 0));
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, out_ptr);
bus.write_long(TEST_SP + 4, text_handle);
disp.dispatch_dialog(true, 0x190, &mut cpu, &mut bus)
.unwrap()
.unwrap();
let initial_get_text = bus.read_pstring(out_ptr);
let initial_get_stack_after = cpu.read_reg(Register::A7);
bus.write_pstring(new_text_ptr, b"Edited Text");
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, new_text_ptr);
bus.write_long(TEST_SP + 4, text_handle);
disp.dispatch_dialog(true, 0x18F, &mut cpu, &mut bus)
.unwrap()
.unwrap();
let set_stack_after = cpu.read_reg(Register::A7);
let data_ptr_after_set = bus.read_long(text_handle);
let handle_size_after_set = bus.get_alloc_size(data_ptr_after_set);
let handle_bytes_after_set = bus.read_bytes(
data_ptr_after_set,
handle_size_after_set.unwrap_or(0) as usize,
);
let cached_text_after_set = disp.dialog_items.get(&dialog_ptr).unwrap()[0].text.clone();
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, out_ptr);
bus.write_long(TEST_SP + 4, text_handle);
disp.dispatch_dialog(true, 0x190, &mut cpu, &mut bus)
.unwrap()
.unwrap();
let post_set_get_text = bus.read_pstring(out_ptr);
let post_set_get_stack_after = cpu.read_reg(Register::A7);
DialogItemTextSnapshot {
initial_get_text,
initial_get_stack_after,
set_stack_after,
handle_size_after_set,
handle_bytes_after_set,
cached_text_after_set,
post_set_get_text,
post_set_get_stack_after,
}
}
fn select_dialog_item_text_results_for_theme(
theme_id: UiThemeId,
) -> DialogItemTextSelectionSnapshot {
let (mut disp, mut cpu, mut bus) = setup();
disp.set_ui_theme_id(theme_id);
let dialog_ptr = bus.alloc(170);
let text_handle = bus.alloc(4);
let te_ptr = bus.alloc(0x40);
bus.write_long(dialog_ptr + 160, text_handle);
bus.write_long(text_handle, te_ptr);
bus.write_word(dialog_ptr + 164, 0xFFFF);
disp.dialog_items.insert(
dialog_ptr,
vec![
DialogItem {
item_type: 16,
rect: (10, 20, 30, 120),
text: "ABCDE".to_string(),
resource_id: 0,
proc_ptr: 0,
sel_start: 1,
sel_end: 1,
},
DialogItem {
item_type: 8,
rect: (34, 20, 50, 120),
text: "STATIC".to_string(),
resource_id: 0,
proc_ptr: 0,
sel_start: 1,
sel_end: 3,
},
],
);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 32767); // endSel
bus.write_word(TEST_SP + 2, 0); // strtSel
bus.write_word(TEST_SP + 4, 1); // itemNo
bus.write_long(TEST_SP + 6, dialog_ptr); // theDialog
disp.dispatch_dialog(true, 0x17E, &mut cpu, &mut bus)
.unwrap()
.unwrap();
let item = &disp.dialog_items[&dialog_ptr][0];
let whole_selection = (item.sel_start, item.sel_end);
let whole_edit_field = bus.read_word(dialog_ptr + 164);
let whole_te_selection = (
bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET),
);
let whole_stack_after = cpu.read_reg(Register::A7);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 2); // endSel
bus.write_word(TEST_SP + 2, 9); // strtSel, beyond text length
bus.write_word(TEST_SP + 4, 1); // itemNo
bus.write_long(TEST_SP + 6, dialog_ptr); // theDialog
disp.dispatch_dialog(true, 0x17E, &mut cpu, &mut bus)
.unwrap()
.unwrap();
let item = &disp.dialog_items[&dialog_ptr][0];
let normalized_selection = (item.sel_start, item.sel_end);
let normalized_edit_field = bus.read_word(dialog_ptr + 164);
let normalized_te_selection = (
bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET),
);
let normalized_stack_after = cpu.read_reg(Register::A7);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 4); // endSel
bus.write_word(TEST_SP + 2, 0); // strtSel
bus.write_word(TEST_SP + 4, 2); // non-edit itemNo
bus.write_long(TEST_SP + 6, dialog_ptr); // theDialog
disp.dispatch_dialog(true, 0x17E, &mut cpu, &mut bus)
.unwrap()
.unwrap();
let item = &disp.dialog_items[&dialog_ptr][1];
let non_edit_selection = (item.sel_start, item.sel_end);
let non_edit_edit_field = bus.read_word(dialog_ptr + 164);
let non_edit_te_selection = (
bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET),
);
let non_edit_stack_after = cpu.read_reg(Register::A7);
DialogItemTextSelectionSnapshot {
whole_selection,
whole_edit_field,
whole_te_selection,
whole_stack_after,
normalized_selection,
normalized_edit_field,
normalized_te_selection,
normalized_stack_after,
non_edit_selection,
non_edit_edit_field,
non_edit_te_selection,
non_edit_stack_after,
}
}
fn is_dialog_event_results_for_theme(theme_id: UiThemeId) -> IsDialogEventSnapshot {
fn dispatch_is_dialog_event(
disp: &mut TrapDispatcher,
cpu: &mut impl CpuOps,
bus: &mut MacMemoryBus,
event_ptr: u32,
what: u16,
message: u32,
where_v: i16,
where_h: i16,
) -> (u16, u32) {
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(event_ptr, what);
bus.write_long(event_ptr + 2, message);
bus.write_long(event_ptr + 6, 0);
bus.write_word(event_ptr + 10, where_v as u16);
bus.write_word(event_ptr + 12, where_h as u16);
bus.write_word(event_ptr + 14, 0);
bus.write_word(TEST_SP + 4, 0xBEEF);
bus.write_long(TEST_SP, event_ptr);
disp.dispatch_dialog(true, 0x17F, cpu, bus)
.unwrap()
.unwrap();
(bus.read_word(TEST_SP + 4), cpu.read_reg(Register::A7))
}
let (mut disp, mut cpu, mut bus) = setup();
disp.set_ui_theme_id(theme_id);
let dialog_ptr = bus.alloc(170);
let event_ptr = 0x300000u32;
disp.front_window = dialog_ptr;
bus.write_word(dialog_ptr + 8, (-100i16) as u16);
bus.write_word(dialog_ptr + 10, (-200i16) as u16);
bus.write_word(dialog_ptr + 16, 0);
bus.write_word(dialog_ptr + 18, 0);
bus.write_word(dialog_ptr + 20, 80);
bus.write_word(dialog_ptr + 22, 120);
disp.dialog_items.insert(dialog_ptr, Vec::new());
let null_event =
dispatch_is_dialog_event(&mut disp, &mut cpu, &mut bus, event_ptr, 0, 0, 0, 0);
let key_down = dispatch_is_dialog_event(
&mut disp,
&mut cpu,
&mut bus,
event_ptr,
3,
u32::from(b'A'),
0,
0,
);
let mouse_inside =
dispatch_is_dialog_event(&mut disp, &mut cpu, &mut bus, event_ptr, 1, 0, 120, 240);
let mouse_outside =
dispatch_is_dialog_event(&mut disp, &mut cpu, &mut bus, event_ptr, 1, 0, 40, 40);
let update_target = dispatch_is_dialog_event(
&mut disp, &mut cpu, &mut bus, event_ptr, 6, dialog_ptr, 0, 0,
);
let activate_target = dispatch_is_dialog_event(
&mut disp, &mut cpu, &mut bus, event_ptr, 8, dialog_ptr, 0, 0,
);
IsDialogEventSnapshot {
null_event,
key_down,
mouse_inside,
mouse_outside,
update_target,
activate_target,
}
}
fn text_handle_bytes(bus: &MacMemoryBus, handle: u32) -> Vec<u8> {
let ptr = bus.read_long(handle);
if ptr == 0 {
return Vec::new();
}
let len = bus.get_alloc_size(ptr).unwrap_or(0) as usize;
bus.read_bytes(ptr, len)
}
fn textedit_scrap_contents(bus: &MacMemoryBus) -> (u16, Vec<u8>) {
let scrap_length = bus.read_word(crate::memory::globals::addr::TE_SCRP_LENGTH);
let scrap_handle = bus.read_long(crate::memory::globals::addr::TE_SCRP_HANDLE);
if scrap_length == 0 || scrap_handle == 0 {
return (scrap_length, Vec::new());
}
let scrap_ptr = bus.read_long(scrap_handle);
if scrap_ptr == 0 {
return (scrap_length, Vec::new());
}
(
scrap_length,
bus.read_bytes(scrap_ptr, scrap_length as usize),
)
}
fn textedit_scrap_edit_case(
bus: &MacMemoryBus,
te_handle: u32,
stack_after: u32,
) -> TextEditScrapEditCase {
let te_ptr = bus.read_long(te_handle);
let (scrap_length, scrap_bytes) = textedit_scrap_contents(bus);
TextEditScrapEditCase {
text: TrapDispatcher::te_text_bytes(bus, te_handle),
selection: (
bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET),
),
scrap_length,
scrap_bytes,
stack_after,
}
}
fn dialog_select_window_event_results_for_theme(
theme_id: UiThemeId,
) -> DialogSelectWindowEventSnapshot {
fn install_region(
bus: &mut MacMemoryBus,
handle: u32,
data: u32,
rect: (i16, i16, i16, i16),
) {
bus.write_long(handle, data);
bus.write_word(data, 10);
bus.write_word(data + 2, rect.0 as u16);
bus.write_word(data + 4, rect.1 as u16);
bus.write_word(data + 6, rect.2 as u16);
bus.write_word(data + 8, rect.3 as u16);
}
fn dispatch_dialog_select(
disp: &mut TrapDispatcher,
cpu: &mut impl CpuOps,
bus: &mut MacMemoryBus,
event_ptr: u32,
dialog_out_ptr: u32,
item_hit_ptr: u32,
what: u16,
message: u32,
) -> (u16, u32, u16, u32) {
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(event_ptr, what);
bus.write_long(event_ptr + 2, message);
bus.write_long(event_ptr + 6, 0);
bus.write_word(event_ptr + 10, 0);
bus.write_word(event_ptr + 12, 0);
bus.write_word(event_ptr + 14, 0);
bus.write_word(TEST_SP + 12, 0xBEEF);
bus.write_long(dialog_out_ptr, 0xDEAD_BEEF);
bus.write_word(item_hit_ptr, 0xCAFE);
bus.write_long(TEST_SP, item_hit_ptr);
bus.write_long(TEST_SP + 4, dialog_out_ptr);
bus.write_long(TEST_SP + 8, event_ptr);
disp.dispatch_dialog(true, 0x180, cpu, bus)
.unwrap()
.unwrap();
(
bus.read_word(TEST_SP + 12),
bus.read_long(dialog_out_ptr),
bus.read_word(item_hit_ptr),
cpu.read_reg(Register::A7),
)
}
let (mut disp, mut cpu, mut bus) = setup_with_port();
disp.set_ui_theme_id(theme_id);
let initial_port = 0x181000;
disp.set_current_port_state(&mut bus, &mut cpu, initial_port, None);
let screen_base = bus.alloc(100 * 100);
bus.write_long(0x0824, screen_base);
disp.screen_mode = (screen_base, 100, 100, 100, 8);
let dialog_ptr = bus.alloc(256);
let event_ptr = 0x300000u32;
let dialog_out_ptr = 0x300100u32;
let item_hit_ptr = 0x300104u32;
let vis_rgn = bus.alloc(10);
let vis_rgn_handle = bus.alloc(4);
install_region(&mut bus, vis_rgn_handle, vis_rgn, (0, 0, 100, 100));
let clip_rgn = bus.alloc(10);
let clip_rgn_handle = bus.alloc(4);
install_region(&mut bus, clip_rgn_handle, clip_rgn, (0, 0, 100, 100));
let update_rgn = bus.alloc(10);
let update_rgn_handle = bus.alloc(4);
install_region(&mut bus, update_rgn_handle, update_rgn, (0, 0, 40, 40));
disp.front_window = dialog_ptr;
disp.window_list.push(dialog_ptr);
bus.write_word(dialog_ptr, 0);
bus.write_long(dialog_ptr + 2, screen_base);
bus.write_word(dialog_ptr + 6, 100);
bus.write_word(dialog_ptr + 8, 0);
bus.write_word(dialog_ptr + 10, 0);
bus.write_word(dialog_ptr + 12, 100);
bus.write_word(dialog_ptr + 14, 100);
bus.write_word(dialog_ptr + 16, 0);
bus.write_word(dialog_ptr + 18, 0);
bus.write_word(dialog_ptr + 20, 100);
bus.write_word(dialog_ptr + 22, 100);
bus.write_long(dialog_ptr + 24, vis_rgn_handle);
bus.write_long(dialog_ptr + 28, clip_rgn_handle);
bus.write_word(dialog_ptr + 108, 2);
bus.write_long(dialog_ptr + 122, update_rgn_handle);
disp.dialog_items.insert(
dialog_ptr,
vec![
DialogItem {
item_type: 0,
rect: (8, 8, 24, 32),
proc_ptr: 0x500000,
..Default::default()
},
DialogItem {
item_type: 0,
rect: (60, 60, 80, 80),
proc_ptr: 0x600000,
..Default::default()
},
DialogItem {
item_type: 8,
rect: (90, 90, 96, 96),
text: "x".to_string(),
..Default::default()
},
],
);
disp.dialog_initial_draw_deferred.insert(dialog_ptr);
let (update_result, update_dialog_out, update_item_hit, update_stack_after) =
dispatch_dialog_select(
&mut disp,
&mut cpu,
&mut bus,
event_ptr,
dialog_out_ptr,
item_hit_ptr,
6,
dialog_ptr,
);
let update_deferred_after = disp.dialog_initial_draw_deferred.contains(&dialog_ptr);
let update_region_after =
TrapDispatcher::region_handle_rect(&bus, bus.read_long(dialog_ptr + 122));
let update_vis_region_after =
TrapDispatcher::region_handle_rect(&bus, bus.read_long(dialog_ptr + 24));
let update_the_port_after = bus.read_long(crate::memory::globals::addr::THE_PORT);
let update_current_port_after = disp.current_port;
let update_saved_vis_after = disp.saved_vis_regions.contains_key(&dialog_ptr);
let update_queued_draw_procs = disp
.modeless_dialog_draw_proc_queue
.iter()
.copied()
.collect();
let update_item_count_after = disp
.dialog_items
.get(&dialog_ptr)
.map(Vec::len)
.unwrap_or_default();
let (activate_result, activate_dialog_out, activate_item_hit, activate_stack_after) =
dispatch_dialog_select(
&mut disp,
&mut cpu,
&mut bus,
event_ptr,
dialog_out_ptr,
item_hit_ptr,
8,
dialog_ptr,
);
DialogSelectWindowEventSnapshot {
dialog_ptr,
update_result,
update_dialog_out,
update_item_hit,
update_stack_after,
update_deferred_after,
update_region_after,
update_vis_region_after,
update_the_port_after,
update_current_port_after,
update_saved_vis_after,
update_queued_draw_procs,
update_item_count_after,
activate_result,
activate_dialog_out,
activate_item_hit,
activate_stack_after,
}
}
fn dialog_select_edit_text_mouse_results_for_theme(
theme_id: UiThemeId,
) -> DialogSelectEditTextMouseSnapshot {
fn dispatch_dialog_select(
disp: &mut TrapDispatcher,
cpu: &mut impl CpuOps,
bus: &mut MacMemoryBus,
event_ptr: u32,
dialog_out_ptr: u32,
item_hit_ptr: u32,
what: u16,
where_v: i16,
where_h: i16,
) -> (u16, u32, u16, u32) {
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(event_ptr, what);
bus.write_long(event_ptr + 2, 0);
bus.write_long(event_ptr + 6, 0);
bus.write_word(event_ptr + 10, where_v as u16);
bus.write_word(event_ptr + 12, where_h as u16);
bus.write_word(event_ptr + 14, 0);
bus.write_word(TEST_SP + 12, 0xBEEF);
bus.write_long(dialog_out_ptr, 0xDEAD_BEEF);
bus.write_word(item_hit_ptr, 0xCAFE);
bus.write_long(TEST_SP, item_hit_ptr);
bus.write_long(TEST_SP + 4, dialog_out_ptr);
bus.write_long(TEST_SP + 8, event_ptr);
disp.dispatch_dialog(true, 0x180, cpu, bus)
.unwrap()
.unwrap();
(
bus.read_word(TEST_SP + 12),
bus.read_long(dialog_out_ptr),
bus.read_word(item_hit_ptr),
cpu.read_reg(Register::A7),
)
}
fn write_ditl_item(
bus: &mut MacMemoryBus,
ditl_ptr: u32,
offset: u32,
handle: u32,
rect: (i16, i16, i16, i16),
item_type: u8,
) {
bus.write_long(ditl_ptr + offset, handle);
bus.write_word(ditl_ptr + offset + 4, rect.0 as u16);
bus.write_word(ditl_ptr + offset + 6, rect.1 as u16);
bus.write_word(ditl_ptr + offset + 8, rect.2 as u16);
bus.write_word(ditl_ptr + offset + 10, rect.3 as u16);
bus.write_byte(ditl_ptr + offset + 12, item_type);
bus.write_byte(ditl_ptr + offset + 13, 0);
}
let (mut disp, mut cpu, mut bus) = setup();
disp.set_ui_theme_id(theme_id);
let dialog_ptr = bus.alloc(170);
let event_ptr = 0x300000u32;
let dialog_out_ptr = 0x300100u32;
let item_hit_ptr = 0x300104u32;
let items_handle = bus.alloc(4);
let ditl_ptr = bus.alloc(30);
let item1_text_handle = bus.alloc(4);
let item1_text_ptr = bus.alloc(5);
let item2_text_handle = bus.alloc(4);
let item2_text_ptr = bus.alloc(6);
let text_h = TrapDispatcher::allocate_te_handle(&mut bus);
disp.front_window = dialog_ptr;
bus.write_word(dialog_ptr + 8, (-100i16) as u16);
bus.write_word(dialog_ptr + 10, (-200i16) as u16);
bus.write_word(dialog_ptr + 16, 0);
bus.write_word(dialog_ptr + 18, 0);
bus.write_word(dialog_ptr + 20, 100);
bus.write_word(dialog_ptr + 22, 160);
bus.write_long(dialog_ptr + 156, items_handle);
bus.write_long(dialog_ptr + 160, text_h);
bus.write_word(dialog_ptr + 164, 0); // editField = first item
bus.write_long(items_handle, ditl_ptr);
bus.write_word(ditl_ptr, 1); // two items
write_ditl_item(
&mut bus,
ditl_ptr,
2,
item1_text_handle,
(20, 20, 38, 120),
16,
);
write_ditl_item(
&mut bus,
ditl_ptr,
16,
item2_text_handle,
(42, 20, 62, 120),
16,
);
bus.write_long(item1_text_handle, item1_text_ptr);
bus.write_bytes(item1_text_ptr, b"First");
bus.write_long(item2_text_handle, item2_text_ptr);
bus.write_bytes(item2_text_ptr, b"Second");
disp.te_set_text_contents(&mut bus, text_h, b"First");
let te_ptr = bus.read_long(text_h);
bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 1);
bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 1);
disp.dialog_items.insert(
dialog_ptr,
vec![
DialogItem {
item_type: 16,
rect: (20, 20, 38, 120),
text: "First".to_string(),
resource_id: 0,
proc_ptr: 0,
sel_start: 1,
sel_end: 1,
},
DialogItem {
item_type: 16,
rect: (42, 20, 62, 120),
text: "Second".to_string(),
resource_id: 0,
proc_ptr: 0,
sel_start: 2,
sel_end: 4,
},
],
);
let (mouse_result, mouse_dialog_out, mouse_item_hit, mouse_stack_after) =
dispatch_dialog_select(
&mut disp,
&mut cpu,
&mut bus,
event_ptr,
dialog_out_ptr,
item_hit_ptr,
1,
150,
240,
);
let mouse_edit_field = bus.read_word(dialog_ptr + 164);
let mouse_item = &disp.dialog_items[&dialog_ptr][1];
let mouse_item_selection = (mouse_item.sel_start, mouse_item.sel_end);
let mouse_te_text = TrapDispatcher::te_text_bytes(&bus, text_h);
let mouse_te_length = bus.read_word(te_ptr + TrapDispatcher::TE_LENGTH_OFFSET);
let mouse_te_selection = (
bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET),
);
let (null_result, null_dialog_out, null_item_hit, null_stack_after) =
dispatch_dialog_select(
&mut disp,
&mut cpu,
&mut bus,
event_ptr,
dialog_out_ptr,
item_hit_ptr,
0,
0,
0,
);
let null_edit_field = bus.read_word(dialog_ptr + 164);
let null_te_text = TrapDispatcher::te_text_bytes(&bus, text_h);
let null_te_length = bus.read_word(te_ptr + TrapDispatcher::TE_LENGTH_OFFSET);
let null_te_selection = (
bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET),
);
DialogSelectEditTextMouseSnapshot {
mouse_result,
mouse_dialog_out,
mouse_item_hit,
mouse_stack_after,
mouse_edit_field,
mouse_item_selection,
mouse_te_text,
mouse_te_length,
mouse_te_selection,
null_result,
null_dialog_out,
null_item_hit,
null_stack_after,
null_edit_field,
null_te_text,
null_te_length,
null_te_selection,
}
}
fn modal_dialog_edit_text_mouse_results_for_theme(
theme_id: UiThemeId,
) -> ModalDialogEditTextMouseSnapshot {
let (mut disp, mut cpu, mut bus) = setup();
disp.set_ui_theme_id(theme_id);
let dialog_ptr = bus.alloc(170);
let item_hit_ptr = 0x300100u32;
let items_handle = bus.alloc(4);
let ditl_ptr = bus.alloc(30);
let item1_text_handle = bus.alloc(4);
let item1_text_ptr = bus.alloc(5);
let item2_text_handle = bus.alloc(4);
let item2_text_ptr = bus.alloc(6);
let text_h = TrapDispatcher::allocate_te_handle(&mut bus);
bus.write_word(dialog_ptr + 8, (-100i16) as u16);
bus.write_word(dialog_ptr + 10, (-200i16) as u16);
bus.write_word(dialog_ptr + 16, 0);
bus.write_word(dialog_ptr + 18, 0);
bus.write_word(dialog_ptr + 20, 100);
bus.write_word(dialog_ptr + 22, 160);
bus.write_long(dialog_ptr + 156, items_handle);
bus.write_long(dialog_ptr + 160, text_h);
bus.write_word(dialog_ptr + 164, 0); // editField = first item
bus.write_long(items_handle, ditl_ptr);
bus.write_word(ditl_ptr, 1); // two items
bus.write_long(ditl_ptr + 2, item1_text_handle);
bus.write_word(ditl_ptr + 6, 20);
bus.write_word(ditl_ptr + 8, 20);
bus.write_word(ditl_ptr + 10, 38);
bus.write_word(ditl_ptr + 12, 120);
bus.write_byte(ditl_ptr + 14, 16);
bus.write_byte(ditl_ptr + 15, 0);
bus.write_long(ditl_ptr + 16, item2_text_handle);
bus.write_word(ditl_ptr + 20, 42);
bus.write_word(ditl_ptr + 22, 20);
bus.write_word(ditl_ptr + 24, 62);
bus.write_word(ditl_ptr + 26, 120);
bus.write_byte(ditl_ptr + 28, 16);
bus.write_byte(ditl_ptr + 29, 0);
bus.write_long(item1_text_handle, item1_text_ptr);
bus.write_bytes(item1_text_ptr, b"First");
bus.write_long(item2_text_handle, item2_text_ptr);
bus.write_bytes(item2_text_ptr, b"Second");
disp.te_set_text_contents(&mut bus, text_h, b"First");
let te_ptr = bus.read_long(text_h);
bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 1);
bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 1);
let items = vec![
DialogItem {
item_type: 16,
rect: (20, 20, 38, 120),
text: "First".to_string(),
resource_id: 0,
proc_ptr: 0,
sel_start: 1,
sel_end: 1,
},
DialogItem {
item_type: 16,
rect: (42, 20, 62, 120),
text: "Second".to_string(),
resource_id: 0,
proc_ptr: 0,
sel_start: 2,
sel_end: 4,
},
];
disp.dialog_items.insert(dialog_ptr, items.clone());
disp.dialog_tracking = Some(crate::trap::dispatch::DialogTrackingState {
dialog_ptr,
bounds: (100, 200, 200, 360),
title: String::new(),
proc_id: 2,
items,
default_item: 0,
cancel_item: 0,
edit_text: "First".to_string(),
edit_item: 1,
saved_pixels: vec![0x11, 0x22, 0x33],
stack_ptr: TEST_SP,
item_hit_ptr,
rendered_pixels: Vec::new(),
flash_remaining: 0,
flash_delay: 0,
flash_item: 0,
edit_text_modified: false,
draw_proc_queue: VecDeque::new(),
draw_procs_done: true,
rendered_pixels_final: true,
filter_proc: 0,
game_managed: false,
last_filter_event: None,
popup_draws: Vec::new(),
active_popup: None,
active_button: None,
active_user_item: None,
});
bus.write_word(item_hit_ptr, 0xCAFE);
cpu.write_reg(Register::A7, TEST_SP);
disp.mouse_button = true;
disp.mouse_pos = (150, 240);
disp.event_queue
.push_back(crate::trap::dispatch::QueuedEvent {
what: 1,
message: 0,
where_v: 150,
where_h: 240,
modifiers: 0,
});
disp.event_queue
.push_back(crate::trap::dispatch::QueuedEvent {
what: 2,
message: 0,
where_v: 150,
where_h: 240,
modifiers: 0,
});
disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus)
.unwrap()
.unwrap();
let item = &disp.dialog_items[&dialog_ptr][1];
ModalDialogEditTextMouseSnapshot {
item_hit: bus.read_word(item_hit_ptr),
stack_after: cpu.read_reg(Register::A7),
tracking_finished: disp.dialog_tracking.is_none(),
retained_visible_snapshot: disp.dialog_visible_snapshots.contains_key(&dialog_ptr),
saved_background_retained: disp.dialog_saved_pixels.contains_key(&dialog_ptr),
queued_mouse_up_consumed: !disp.event_queue.iter().any(|event| event.what == 2),
edit_field: bus.read_word(dialog_ptr + 164),
item_selection: (item.sel_start, item.sel_end),
te_text: TrapDispatcher::te_text_bytes(&bus, text_h),
te_length: bus.read_word(te_ptr + TrapDispatcher::TE_LENGTH_OFFSET),
te_selection: (
bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET),
),
handle_bytes: text_handle_bytes(&bus, item2_text_handle),
}
}
fn modal_dialog_keyboard_button_results_for_theme(
theme_id: UiThemeId,
) -> ModalDialogKeyboardButtonSnapshot {
fn run_key(
theme_id: UiThemeId,
key_code: u8,
char_code: u8,
modifiers: u16,
) -> ModalDialogKeyboardButtonCase {
let (mut disp, mut cpu, mut bus) = setup_with_port();
disp.set_ui_theme_id(theme_id);
let dialog_ptr = 0x200000u32;
let item_hit_ptr = 0x300000u32;
let items = vec![
DialogItem {
item_type: 4,
rect: (20, 30, 60, 110),
text: "OK".to_string(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
},
DialogItem {
item_type: 4,
rect: (20, 124, 60, 214),
text: "Cancel".to_string(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
},
];
disp.dialog_tracking = Some(crate::trap::dispatch::DialogTrackingState {
dialog_ptr,
bounds: (100, 200, 200, 440),
title: String::new(),
proc_id: 2,
items,
default_item: 1,
cancel_item: 2,
edit_text: String::new(),
edit_item: 0,
saved_pixels: Vec::new(),
stack_ptr: TEST_SP,
item_hit_ptr,
rendered_pixels: Vec::new(),
flash_remaining: 0,
flash_delay: 0,
flash_item: 0,
edit_text_modified: false,
draw_proc_queue: VecDeque::new(),
draw_procs_done: true,
rendered_pixels_final: true,
filter_proc: 0,
game_managed: false,
last_filter_event: None,
popup_draws: Vec::new(),
active_popup: None,
active_button: None,
active_user_item: None,
});
disp.event_queue
.push_back(crate::trap::dispatch::QueuedEvent {
what: 3,
message: (u32::from(key_code) << 8) | u32::from(char_code),
where_v: 0,
where_h: 0,
modifiers,
});
bus.write_word(item_hit_ptr, 0xCAFE);
cpu.write_reg(Register::A7, TEST_SP);
disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus)
.unwrap()
.unwrap();
let flash_item_after_key = disp
.dialog_tracking
.as_ref()
.map(|tracking| tracking.flash_item)
.unwrap_or(0);
let stack_after_key = cpu.read_reg(Register::A7);
let item_hit_after_key = bus.read_word(item_hit_ptr);
{
let tracking = disp.dialog_tracking.as_mut().unwrap();
tracking.flash_remaining = 1;
tracking.flash_delay = 0;
}
disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus)
.unwrap()
.unwrap();
ModalDialogKeyboardButtonCase {
flash_item_after_key,
stack_after_key,
item_hit_after_key,
item_hit_after_flash: bus.read_word(item_hit_ptr),
stack_after_flash: cpu.read_reg(Register::A7),
tracking_finished: disp.dialog_tracking.is_none(),
}
}
ModalDialogKeyboardButtonSnapshot {
return_key: run_key(theme_id, 0x24, 0x0D, 0),
enter_key: run_key(theme_id, 0x4C, 0x03, 0),
escape_key: run_key(theme_id, 0x35, 0x1B, 0),
command_period: run_key(theme_id, 0x2F, b'.', 0x0100),
}
}
fn modal_dialog_update_event_results_for_theme(
theme_id: UiThemeId,
) -> ModalDialogUpdateEventSnapshot {
fn install_region(
bus: &mut MacMemoryBus,
handle: u32,
data: u32,
rect: (i16, i16, i16, i16),
) {
bus.write_long(handle, data);
bus.write_word(data, 10);
bus.write_word(data + 2, rect.0 as u16);
bus.write_word(data + 4, rect.1 as u16);
bus.write_word(data + 6, rect.2 as u16);
bus.write_word(data + 8, rect.3 as u16);
}
let (mut disp, mut cpu, mut bus) = setup_with_port();
disp.set_ui_theme_id(theme_id);
let initial_port = 0x181000;
disp.set_current_port_state(&mut bus, &mut cpu, initial_port, None);
let screen_base = bus.alloc(80 * 80);
bus.write_long(0x0824, screen_base);
disp.screen_mode = (screen_base, 80, 80, 80, 8);
let dialog_ptr = bus.alloc(256);
let item_hit_ptr = 0x300100u32;
let bounds = (10, 10, 40, 40);
let snapshot_width = (bounds.3 - bounds.1 + TrapDispatcher::DBOX_FRAME_MARGIN * 2) as usize;
let snapshot_height =
(bounds.2 - bounds.0 + TrapDispatcher::DBOX_FRAME_MARGIN * 2) as usize;
let snapshot_index = (17 - (bounds.0 - TrapDispatcher::DBOX_FRAME_MARGIN)) as usize
* snapshot_width
+ (19 - (bounds.1 - TrapDispatcher::DBOX_FRAME_MARGIN)) as usize;
let mut rendered_pixels = vec![0x00; snapshot_width * snapshot_height];
rendered_pixels[snapshot_index] = 0x44;
let vis_rgn = bus.alloc(10);
let vis_rgn_handle = bus.alloc(4);
install_region(&mut bus, vis_rgn_handle, vis_rgn, (0, 0, 80, 80));
let clip_rgn = bus.alloc(10);
let clip_rgn_handle = bus.alloc(4);
install_region(&mut bus, clip_rgn_handle, clip_rgn, (0, 0, 80, 80));
let update_rgn = bus.alloc(10);
let update_rgn_handle = bus.alloc(4);
install_region(&mut bus, update_rgn_handle, update_rgn, (0, 0, 40, 40));
bus.write_word(dialog_ptr, 0);
bus.write_long(dialog_ptr + 2, screen_base);
bus.write_word(dialog_ptr + 6, 80);
bus.write_word(dialog_ptr + 8, 0);
bus.write_word(dialog_ptr + 10, 0);
bus.write_word(dialog_ptr + 12, 80);
bus.write_word(dialog_ptr + 14, 80);
bus.write_word(dialog_ptr + 16, 0);
bus.write_word(dialog_ptr + 18, 0);
bus.write_word(dialog_ptr + 20, 80);
bus.write_word(dialog_ptr + 22, 80);
bus.write_long(dialog_ptr + 24, vis_rgn_handle);
bus.write_long(dialog_ptr + 28, clip_rgn_handle);
bus.write_long(dialog_ptr + 122, update_rgn_handle);
bus.write_word(item_hit_ptr, 0xCAFE);
disp.front_window = dialog_ptr;
disp.window_bounds = bounds;
disp.dialog_tracking = Some(crate::trap::dispatch::DialogTrackingState {
dialog_ptr,
bounds,
title: String::new(),
proc_id: 2,
items: Vec::new(),
default_item: 0,
cancel_item: 0,
edit_text: String::new(),
edit_item: 0,
saved_pixels: Vec::new(),
stack_ptr: TEST_SP,
item_hit_ptr,
rendered_pixels,
flash_remaining: 0,
flash_delay: 0,
flash_item: 0,
edit_text_modified: false,
draw_proc_queue: VecDeque::new(),
draw_procs_done: true,
rendered_pixels_final: true,
filter_proc: 0,
game_managed: false,
last_filter_event: None,
popup_draws: Vec::new(),
active_popup: None,
active_button: None,
active_user_item: None,
});
disp.event_queue
.push_back(crate::trap::dispatch::QueuedEvent {
what: 6,
message: dialog_ptr,
where_v: 0,
where_h: 0,
modifiers: 0,
});
cpu.write_reg(Register::A7, TEST_SP);
disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus)
.unwrap()
.unwrap();
let tracking = disp.dialog_tracking.as_ref().unwrap();
ModalDialogUpdateEventSnapshot {
dialog_ptr,
item_hit_after: bus.read_word(item_hit_ptr),
stack_after: cpu.read_reg(Register::A7),
tracking_finished: disp.dialog_tracking.is_none(),
rendered_pixels_final: tracking.rendered_pixels_final,
retained_pixel_after: bus.read_byte(screen_base + 17 * 80 + 19),
retained_snapshot_pixel_after: tracking.rendered_pixels[snapshot_index],
update_region_after: TrapDispatcher::region_handle_rect(
&bus,
bus.read_long(dialog_ptr + 122),
),
vis_region_after: TrapDispatcher::region_handle_rect(
&bus,
bus.read_long(dialog_ptr + 24),
),
the_port_after: bus.read_long(crate::memory::globals::addr::THE_PORT),
current_port_after: disp.current_port,
saved_vis_after: disp.saved_vis_regions.contains_key(&dialog_ptr),
}
}
fn dialog_select_edit_text_key_results_for_theme(
theme_id: UiThemeId,
) -> DialogSelectEditTextKeySnapshot {
let (mut disp, mut cpu, mut bus) = setup();
disp.set_ui_theme_id(theme_id);
let dialog_ptr = bus.alloc(170);
let event_ptr = 0x300000u32;
let dialog_out_ptr = 0x300100u32;
let item_hit_ptr = 0x300104u32;
let items_handle = bus.alloc(4);
let ditl_ptr = bus.alloc(16);
let item_text_handle = bus.alloc(4);
let item_text_ptr = bus.alloc(5);
let text_h = TrapDispatcher::allocate_te_handle(&mut bus);
disp.front_window = dialog_ptr;
bus.write_word(dialog_ptr + 8, 0);
bus.write_word(dialog_ptr + 10, 0);
bus.write_word(dialog_ptr + 16, 0);
bus.write_word(dialog_ptr + 18, 0);
bus.write_word(dialog_ptr + 20, 100);
bus.write_word(dialog_ptr + 22, 160);
bus.write_long(dialog_ptr + 156, items_handle);
bus.write_long(dialog_ptr + 160, text_h);
bus.write_word(dialog_ptr + 164, 0); // editField = first item
bus.write_long(items_handle, ditl_ptr);
bus.write_word(ditl_ptr, 0); // one item
bus.write_long(ditl_ptr + 2, item_text_handle);
bus.write_word(ditl_ptr + 6, 20);
bus.write_word(ditl_ptr + 8, 30);
bus.write_word(ditl_ptr + 10, 60);
bus.write_word(ditl_ptr + 12, 110);
bus.write_byte(ditl_ptr + 14, 16); // editText
bus.write_byte(ditl_ptr + 15, 0);
bus.write_long(item_text_handle, item_text_ptr);
bus.write_bytes(item_text_ptr, b"Hello");
disp.te_set_text_contents(&mut bus, text_h, b"Hello");
let te_ptr = bus.read_long(text_h);
bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 1);
bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 4);
disp.dialog_items.insert(
dialog_ptr,
vec![DialogItem {
item_type: 16,
rect: (20, 30, 60, 110),
text: "Hello".to_string(),
resource_id: 0,
proc_ptr: 0,
sel_start: 1,
sel_end: 4,
}],
);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(event_ptr, 3); // keyDown
bus.write_long(event_ptr + 2, u32::from(b'Y'));
bus.write_long(event_ptr + 6, 0);
bus.write_word(event_ptr + 10, 0);
bus.write_word(event_ptr + 12, 0);
bus.write_word(event_ptr + 14, 0);
bus.write_word(TEST_SP + 12, 0xBEEF);
bus.write_long(dialog_out_ptr, 0xDEAD_BEEF);
bus.write_word(item_hit_ptr, 0xCAFE);
bus.write_long(TEST_SP, item_hit_ptr);
bus.write_long(TEST_SP + 4, dialog_out_ptr);
bus.write_long(TEST_SP + 8, event_ptr);
disp.dispatch_dialog(true, 0x180, &mut cpu, &mut bus)
.unwrap()
.unwrap();
let keydown_result = bus.read_word(TEST_SP + 12);
let keydown_dialog_out = bus.read_long(dialog_out_ptr);
let keydown_item_hit = bus.read_word(item_hit_ptr);
let keydown_stack_after = cpu.read_reg(Register::A7);
let keydown_handle_bytes = text_handle_bytes(&bus, item_text_handle);
let keydown_cached_text = disp.dialog_items[&dialog_ptr][0].text.clone();
let keydown_item_selection = (
disp.dialog_items[&dialog_ptr][0].sel_start,
disp.dialog_items[&dialog_ptr][0].sel_end,
);
let keydown_te_text = TrapDispatcher::te_text_bytes(&bus, text_h);
let keydown_te_length = bus.read_word(te_ptr + TrapDispatcher::TE_LENGTH_OFFSET);
let keydown_te_selection = (
bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET),
);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(event_ptr, 5); // autoKey
bus.write_long(event_ptr + 2, 0x0000_0008); // backspace
bus.write_word(TEST_SP + 12, 0xBEEF);
bus.write_long(dialog_out_ptr, 0xDEAD_BEEF);
bus.write_word(item_hit_ptr, 0xCAFE);
bus.write_long(TEST_SP, item_hit_ptr);
bus.write_long(TEST_SP + 4, dialog_out_ptr);
bus.write_long(TEST_SP + 8, event_ptr);
disp.dispatch_dialog(true, 0x180, &mut cpu, &mut bus)
.unwrap()
.unwrap();
let autokey_result = bus.read_word(TEST_SP + 12);
let autokey_dialog_out = bus.read_long(dialog_out_ptr);
let autokey_item_hit = bus.read_word(item_hit_ptr);
let autokey_stack_after = cpu.read_reg(Register::A7);
let autokey_handle_bytes = text_handle_bytes(&bus, item_text_handle);
let autokey_cached_text = disp.dialog_items[&dialog_ptr][0].text.clone();
let autokey_item_selection = (
disp.dialog_items[&dialog_ptr][0].sel_start,
disp.dialog_items[&dialog_ptr][0].sel_end,
);
let autokey_te_text = TrapDispatcher::te_text_bytes(&bus, text_h);
let autokey_te_length = bus.read_word(te_ptr + TrapDispatcher::TE_LENGTH_OFFSET);
let autokey_te_selection = (
bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET),
);
DialogSelectEditTextKeySnapshot {
keydown_result,
keydown_dialog_out,
keydown_item_hit,
keydown_stack_after,
keydown_handle_bytes,
keydown_cached_text,
keydown_item_selection,
keydown_te_text,
keydown_te_length,
keydown_te_selection,
autokey_result,
autokey_dialog_out,
autokey_item_hit,
autokey_stack_after,
autokey_handle_bytes,
autokey_cached_text,
autokey_item_selection,
autokey_te_text,
autokey_te_length,
autokey_te_selection,
}
}
fn getsetditem_useritem_record_results_for_theme(
theme_id: UiThemeId,
) -> DItemUserItemRecordSnapshot {
let (mut disp, mut cpu, mut bus) = setup();
disp.set_ui_theme_id(theme_id);
let dialog_ptr = bus.alloc(170);
let items_handle = bus.alloc(4);
let ditl_ptr = bus.alloc(16);
let initial_proc_ptr = 0x0012_3456u32;
let new_proc_ptr = 0x00AB_CDEFu32;
let set_box_ptr = bus.alloc(8);
let get_box_ptr = bus.alloc(8);
let get_item_ptr = bus.alloc(4);
let get_type_ptr = bus.alloc(2);
bus.write_long(items_handle, ditl_ptr);
bus.write_long(dialog_ptr + 156, items_handle);
bus.write_word(ditl_ptr, 0); // one item
bus.write_long(ditl_ptr + 2, initial_proc_ptr);
bus.write_word(ditl_ptr + 6, 12);
bus.write_word(ditl_ptr + 8, 24);
bus.write_word(ditl_ptr + 10, 36);
bus.write_word(ditl_ptr + 12, 48);
bus.write_byte(ditl_ptr + 14, 0);
bus.write_byte(ditl_ptr + 15, 0);
disp.dialog_items.insert(
dialog_ptr,
vec![DialogItem {
item_type: 0,
rect: (12, 24, 36, 48),
text: String::new(),
resource_id: 0,
proc_ptr: initial_proc_ptr,
sel_start: 0,
sel_end: 0,
}],
);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, get_box_ptr);
bus.write_long(TEST_SP + 4, get_item_ptr);
bus.write_long(TEST_SP + 8, get_type_ptr);
bus.write_word(TEST_SP + 12, 1);
bus.write_long(TEST_SP + 14, dialog_ptr);
disp.dispatch_dialog(true, 0x18D, &mut cpu, &mut bus)
.unwrap()
.unwrap();
let initial_get = DItemGetSnapshot {
item_type: bus.read_word(get_type_ptr),
item: bus.read_long(get_item_ptr),
rect: (
bus.read_word(get_box_ptr),
bus.read_word(get_box_ptr + 2),
bus.read_word(get_box_ptr + 4),
bus.read_word(get_box_ptr + 6),
),
stack_after: cpu.read_reg(Register::A7),
};
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(set_box_ptr, 50);
bus.write_word(set_box_ptr + 2, 60);
bus.write_word(set_box_ptr + 4, 70);
bus.write_word(set_box_ptr + 6, 80);
bus.write_long(TEST_SP, set_box_ptr);
bus.write_long(TEST_SP + 4, new_proc_ptr);
bus.write_word(TEST_SP + 8, 0);
bus.write_word(TEST_SP + 10, 1);
bus.write_long(TEST_SP + 12, dialog_ptr);
disp.dispatch_dialog(true, 0x18E, &mut cpu, &mut bus)
.unwrap()
.unwrap();
let set_stack_after = cpu.read_reg(Register::A7);
let stored_item = disp
.dialog_items
.get(&dialog_ptr)
.and_then(|items| items.first())
.cloned()
.unwrap();
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, get_box_ptr);
bus.write_long(TEST_SP + 4, get_item_ptr);
bus.write_long(TEST_SP + 8, get_type_ptr);
bus.write_word(TEST_SP + 12, 1);
bus.write_long(TEST_SP + 14, dialog_ptr);
disp.dispatch_dialog(true, 0x18D, &mut cpu, &mut bus)
.unwrap()
.unwrap();
let post_set_get = DItemGetSnapshot {
item_type: bus.read_word(get_type_ptr),
item: bus.read_long(get_item_ptr),
rect: (
bus.read_word(get_box_ptr),
bus.read_word(get_box_ptr + 2),
bus.read_word(get_box_ptr + 4),
bus.read_word(get_box_ptr + 6),
),
stack_after: cpu.read_reg(Register::A7),
};
DItemUserItemRecordSnapshot {
initial_get,
set_stack_after,
stored_type: stored_item.item_type,
stored_proc_ptr: stored_item.proc_ptr,
stored_rect: stored_item.rect,
post_set_get,
}
}
fn get_ditem_snapshot_for_test(
disp: &mut TrapDispatcher,
cpu: &mut MockCpu,
bus: &mut MacMemoryBus,
dialog_ptr: u32,
item_no: i16,
box_ptr: u32,
item_ptr: u32,
type_ptr: u32,
) -> DItemGetSnapshot {
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, box_ptr);
bus.write_long(TEST_SP + 4, item_ptr);
bus.write_long(TEST_SP + 8, type_ptr);
bus.write_word(TEST_SP + 12, item_no as u16);
bus.write_long(TEST_SP + 14, dialog_ptr);
disp.dispatch_dialog(true, 0x18D, cpu, bus)
.unwrap()
.unwrap();
DItemGetSnapshot {
item_type: bus.read_word(type_ptr),
item: bus.read_long(item_ptr),
rect: (
bus.read_word(box_ptr),
bus.read_word(box_ptr + 2),
bus.read_word(box_ptr + 4),
bus.read_word(box_ptr + 6),
),
stack_after: cpu.read_reg(Register::A7),
}
}
fn write_control_record_for_ditem_test(
bus: &mut MacMemoryBus,
handle: u32,
ctrl_ptr: u32,
dialog_ptr: u32,
rect: (i16, i16, i16, i16),
value: i16,
title: &[u8],
) {
bus.write_long(handle, ctrl_ptr);
bus.write_long(ctrl_ptr, 0);
bus.write_long(ctrl_ptr + 4, dialog_ptr);
bus.write_word(ctrl_ptr + 8, rect.0 as u16);
bus.write_word(ctrl_ptr + 10, rect.1 as u16);
bus.write_word(ctrl_ptr + 12, rect.2 as u16);
bus.write_word(ctrl_ptr + 14, rect.3 as u16);
bus.write_byte(ctrl_ptr + 16, 255);
bus.write_byte(ctrl_ptr + 17, 0);
bus.write_word(ctrl_ptr + 18, value as u16);
bus.write_word(ctrl_ptr + 20, 0);
bus.write_word(ctrl_ptr + 22, 1);
bus.write_byte(ctrl_ptr + 40, title.len() as u8);
bus.write_bytes(ctrl_ptr + 41, title);
}
fn getsetditem_text_control_record_results_for_theme(
theme_id: UiThemeId,
) -> DItemTextControlRecordSnapshot {
let (mut disp, mut cpu, mut bus) = setup();
disp.set_ui_theme_id(theme_id);
let dialog_ptr = bus.alloc(170);
let items_handle = bus.alloc(4);
let ditl_ptr = bus.alloc(32);
let old_text_handle = bus.alloc(4);
let old_text_ptr = bus.alloc(3);
let new_text_handle = bus.alloc(4);
let new_text_ptr = bus.alloc(7);
let old_control_handle = bus.alloc(4);
let old_control_ptr = bus.alloc(44);
let new_control_handle = bus.alloc(4);
let new_control_ptr = bus.alloc(47);
let text_set_box_ptr = bus.alloc(8);
let control_set_box_ptr = bus.alloc(8);
let get_box_ptr = bus.alloc(8);
let get_item_ptr = bus.alloc(4);
let get_type_ptr = bus.alloc(2);
let text_entry = ditl_ptr + 2;
let control_entry = ditl_ptr + 16;
bus.write_bytes(old_text_ptr, b"Old");
bus.write_bytes(new_text_ptr, b"Updated");
bus.write_long(old_text_handle, old_text_ptr);
bus.write_long(new_text_handle, new_text_ptr);
write_control_record_for_ditem_test(
&mut bus,
old_control_handle,
old_control_ptr,
dialog_ptr,
(30, 40, 50, 140),
1,
b"OK",
);
write_control_record_for_ditem_test(
&mut bus,
new_control_handle,
new_control_ptr,
dialog_ptr,
(1, 2, 3, 4),
2,
b"Apply",
);
bus.write_long(items_handle, ditl_ptr);
bus.write_long(dialog_ptr + 156, items_handle);
bus.write_word(ditl_ptr, 1); // two items
bus.write_long(text_entry, old_text_handle);
bus.write_word(text_entry + 4, 10);
bus.write_word(text_entry + 6, 20);
bus.write_word(text_entry + 8, 24);
bus.write_word(text_entry + 10, 160);
bus.write_byte(text_entry + 12, 16);
bus.write_byte(text_entry + 13, 0);
bus.write_long(control_entry, old_control_handle);
bus.write_word(control_entry + 4, 30);
bus.write_word(control_entry + 6, 40);
bus.write_word(control_entry + 8, 50);
bus.write_word(control_entry + 10, 140);
bus.write_byte(control_entry + 12, 4);
bus.write_byte(control_entry + 13, 2);
bus.write_bytes(control_entry + 14, b"OK");
disp.dialog_items.insert(
dialog_ptr,
vec![
DialogItem {
item_type: 16,
rect: (10, 20, 24, 160),
text: "Old".to_string(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
},
DialogItem {
item_type: 4,
rect: (30, 40, 50, 140),
text: "OK".to_string(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
},
],
);
disp.dialog_item_handles
.insert(old_text_handle, (dialog_ptr, 0));
disp.dialog_control_handles
.insert(old_control_handle, (dialog_ptr, 2));
disp.dialog_control_values.insert((dialog_ptr, 2), 1);
let text_initial_get = get_ditem_snapshot_for_test(
&mut disp,
&mut cpu,
&mut bus,
dialog_ptr,
1,
get_box_ptr,
get_item_ptr,
get_type_ptr,
);
let control_initial_get = get_ditem_snapshot_for_test(
&mut disp,
&mut cpu,
&mut bus,
dialog_ptr,
2,
get_box_ptr,
get_item_ptr,
get_type_ptr,
);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(text_set_box_ptr, 12);
bus.write_word(text_set_box_ptr + 2, 22);
bus.write_word(text_set_box_ptr + 4, 28);
bus.write_word(text_set_box_ptr + 6, 168);
bus.write_long(TEST_SP, text_set_box_ptr);
bus.write_long(TEST_SP + 4, new_text_handle);
bus.write_word(TEST_SP + 8, 16);
bus.write_word(TEST_SP + 10, 1);
bus.write_long(TEST_SP + 12, dialog_ptr);
disp.dispatch_dialog(true, 0x18E, &mut cpu, &mut bus)
.unwrap()
.unwrap();
let text_set_stack_after = cpu.read_reg(Register::A7);
let text_stored_item = disp.dialog_items.get(&dialog_ptr).unwrap()[0].clone();
let text_post_set_get = get_ditem_snapshot_for_test(
&mut disp,
&mut cpu,
&mut bus,
dialog_ptr,
1,
get_box_ptr,
get_item_ptr,
get_type_ptr,
);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(control_set_box_ptr, 42);
bus.write_word(control_set_box_ptr + 2, 52);
bus.write_word(control_set_box_ptr + 4, 62);
bus.write_word(control_set_box_ptr + 6, 172);
bus.write_long(TEST_SP, control_set_box_ptr);
bus.write_long(TEST_SP + 4, new_control_handle);
bus.write_word(TEST_SP + 8, 5);
bus.write_word(TEST_SP + 10, 2);
bus.write_long(TEST_SP + 12, dialog_ptr);
disp.dispatch_dialog(true, 0x18E, &mut cpu, &mut bus)
.unwrap()
.unwrap();
let control_set_stack_after = cpu.read_reg(Register::A7);
let control_stored_item = disp.dialog_items.get(&dialog_ptr).unwrap()[1].clone();
let control_post_set_get = get_ditem_snapshot_for_test(
&mut disp,
&mut cpu,
&mut bus,
dialog_ptr,
2,
get_box_ptr,
get_item_ptr,
get_type_ptr,
);
DItemTextControlRecordSnapshot {
text_initial_get,
text_set_stack_after,
text_old_handle_mapped_after: disp.dialog_item_handles.contains_key(&old_text_handle),
text_new_handle_map_value_after: disp
.dialog_item_handles
.get(&new_text_handle)
.copied(),
text_ditl_handle_after: bus.read_long(text_entry),
text_ditl_rect_after: (
bus.read_word(text_entry + 4),
bus.read_word(text_entry + 6),
bus.read_word(text_entry + 8),
bus.read_word(text_entry + 10),
),
text_ditl_type_after: bus.read_byte(text_entry + 12),
text_stored_type_after: text_stored_item.item_type,
text_stored_rect_after: text_stored_item.rect,
text_cached_text_after: text_stored_item.text,
text_handle_bytes_after: text_handle_bytes(&bus, new_text_handle),
text_post_set_get,
control_initial_get,
control_set_stack_after,
control_old_handle_mapped_after: disp
.dialog_control_handles
.contains_key(&old_control_handle),
control_new_handle_map_value_after: disp
.dialog_control_handles
.get(&new_control_handle)
.copied(),
control_ditl_handle_after: bus.read_long(control_entry),
control_ditl_rect_after: (
bus.read_word(control_entry + 4),
bus.read_word(control_entry + 6),
bus.read_word(control_entry + 8),
bus.read_word(control_entry + 10),
),
control_ditl_type_after: bus.read_byte(control_entry + 12),
control_stored_type_after: control_stored_item.item_type,
control_stored_rect_after: control_stored_item.rect,
control_record_rect_after: (
bus.read_word(new_control_ptr + 8),
bus.read_word(new_control_ptr + 10),
bus.read_word(new_control_ptr + 12),
bus.read_word(new_control_ptr + 14),
),
control_value_after: disp.dialog_control_values.get(&(dialog_ptr, 2)).copied(),
control_post_set_get,
}
}
fn hide_show_ditem_visibility_results_for_theme(
theme_id: UiThemeId,
) -> DItemVisibilitySnapshot {
let (mut disp, mut cpu, mut bus) = setup();
disp.set_ui_theme_id(theme_id);
let dialog_ptr = bus.alloc(170);
let items_handle = bus.alloc(4);
let ditl_ptr = bus.alloc(18);
let get_box_ptr = bus.alloc(8);
let get_item_ptr = bus.alloc(4);
let get_type_ptr = bus.alloc(2);
bus.write_long(items_handle, ditl_ptr);
bus.write_long(dialog_ptr + 156, items_handle);
bus.write_word(ditl_ptr, 0); // one item
bus.write_long(ditl_ptr + 2, 0);
bus.write_word(ditl_ptr + 6, 10);
bus.write_word(ditl_ptr + 8, 20);
bus.write_word(ditl_ptr + 10, 30);
bus.write_word(ditl_ptr + 12, 120);
bus.write_byte(ditl_ptr + 14, 4);
bus.write_byte(ditl_ptr + 15, 2);
bus.write_bytes(ditl_ptr + 16, b"OK");
disp.dialog_items.insert(
dialog_ptr,
vec![DialogItem {
item_type: 4,
rect: (10, 20, 30, 120),
text: "OK".into(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
}],
);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, get_box_ptr);
bus.write_long(TEST_SP + 4, get_item_ptr);
bus.write_long(TEST_SP + 8, get_type_ptr);
bus.write_word(TEST_SP + 12, 1);
bus.write_long(TEST_SP + 14, dialog_ptr);
disp.dispatch_dialog(true, 0x18D, &mut cpu, &mut bus)
.unwrap()
.unwrap();
let initial_get = DItemGetSnapshot {
item_type: bus.read_word(get_type_ptr),
item: bus.read_long(get_item_ptr),
rect: (
bus.read_word(get_box_ptr),
bus.read_word(get_box_ptr + 2),
bus.read_word(get_box_ptr + 4),
bus.read_word(get_box_ptr + 6),
),
stack_after: cpu.read_reg(Register::A7),
};
assert_ne!(initial_get.item, 0);
let ctrl_ptr = bus.read_long(initial_get.item);
assert_ne!(ctrl_ptr, 0);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 1);
bus.write_long(TEST_SP + 2, dialog_ptr);
disp.dispatch_dialog(true, 0x027, &mut cpu, &mut bus)
.unwrap()
.unwrap();
let hide_stack_after = cpu.read_reg(Register::A7);
let hidden_item_rect = disp.dialog_items.get(&dialog_ptr).unwrap()[0].rect;
let hidden_saved_rect = disp.hidden_dialog_item_rects.get(&(dialog_ptr, 1)).copied();
let hidden_control_rect = (
bus.read_word(ctrl_ptr + 8),
bus.read_word(ctrl_ptr + 10),
bus.read_word(ctrl_ptr + 12),
bus.read_word(ctrl_ptr + 14),
);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, get_box_ptr);
bus.write_long(TEST_SP + 4, get_item_ptr);
bus.write_long(TEST_SP + 8, get_type_ptr);
bus.write_word(TEST_SP + 12, 1);
bus.write_long(TEST_SP + 14, dialog_ptr);
disp.dispatch_dialog(true, 0x18D, &mut cpu, &mut bus)
.unwrap()
.unwrap();
let hidden_get = DItemGetSnapshot {
item_type: bus.read_word(get_type_ptr),
item: bus.read_long(get_item_ptr),
rect: (
bus.read_word(get_box_ptr),
bus.read_word(get_box_ptr + 2),
bus.read_word(get_box_ptr + 4),
bus.read_word(get_box_ptr + 6),
),
stack_after: cpu.read_reg(Register::A7),
};
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 20);
bus.write_word(TEST_SP + 2, 40);
bus.write_long(TEST_SP + 4, dialog_ptr);
bus.write_word(TEST_SP + 8, 0xBEEF);
disp.dispatch_dialog(true, 0x184, &mut cpu, &mut bus)
.unwrap()
.unwrap();
let hidden_find = (
bus.read_word(TEST_SP + 8) as i16,
cpu.read_reg(Register::A7),
);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 1);
bus.write_long(TEST_SP + 2, dialog_ptr);
disp.dispatch_dialog(true, 0x028, &mut cpu, &mut bus)
.unwrap()
.unwrap();
let show_stack_after = cpu.read_reg(Register::A7);
let restored_item_rect = disp.dialog_items.get(&dialog_ptr).unwrap()[0].rect;
let restored_saved_rect = disp.hidden_dialog_item_rects.get(&(dialog_ptr, 1)).copied();
let restored_control_rect = (
bus.read_word(ctrl_ptr + 8),
bus.read_word(ctrl_ptr + 10),
bus.read_word(ctrl_ptr + 12),
bus.read_word(ctrl_ptr + 14),
);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, get_box_ptr);
bus.write_long(TEST_SP + 4, get_item_ptr);
bus.write_long(TEST_SP + 8, get_type_ptr);
bus.write_word(TEST_SP + 12, 1);
bus.write_long(TEST_SP + 14, dialog_ptr);
disp.dispatch_dialog(true, 0x18D, &mut cpu, &mut bus)
.unwrap()
.unwrap();
let restored_get = DItemGetSnapshot {
item_type: bus.read_word(get_type_ptr),
item: bus.read_long(get_item_ptr),
rect: (
bus.read_word(get_box_ptr),
bus.read_word(get_box_ptr + 2),
bus.read_word(get_box_ptr + 4),
bus.read_word(get_box_ptr + 6),
),
stack_after: cpu.read_reg(Register::A7),
};
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 20);
bus.write_word(TEST_SP + 2, 40);
bus.write_long(TEST_SP + 4, dialog_ptr);
bus.write_word(TEST_SP + 8, 0xBEEF);
disp.dispatch_dialog(true, 0x184, &mut cpu, &mut bus)
.unwrap()
.unwrap();
let restored_find = (
bus.read_word(TEST_SP + 8) as i16,
cpu.read_reg(Register::A7),
);
DItemVisibilitySnapshot {
initial_get,
hide_stack_after,
hidden_item_rect,
hidden_saved_rect,
hidden_control_rect,
hidden_get,
hidden_find,
show_stack_after,
restored_item_rect,
restored_saved_rect,
restored_control_rect,
restored_get,
restored_find,
}
}
fn drawdialog_useritem_pixel_results_for_theme(theme_id: UiThemeId) -> (u8, u32, u32) {
let (mut disp, mut cpu, mut bus) = setup();
disp.set_ui_theme_id(theme_id);
let dialog_ptr = bus.alloc(170);
let (screen_base, row_bytes, _w, _h, pixel_size) = disp.screen_mode;
assert_eq!(pixel_size, 8);
bus.write_word(dialog_ptr + 8, 0);
bus.write_word(dialog_ptr + 10, 0);
bus.write_word(dialog_ptr + 16, 0);
bus.write_word(dialog_ptr + 18, 0);
bus.write_word(dialog_ptr + 20, 40);
bus.write_word(dialog_ptr + 22, 80);
disp.dialog_items.insert(
dialog_ptr,
vec![
DialogItem {
item_type: 0, // enabled userItem
rect: (8, 8, 20, 32),
text: String::new(),
resource_id: 0,
proc_ptr: 0x00C0_FFEE,
sel_start: 0,
sel_end: 0,
},
DialogItem {
item_type: 4,
rect: (24, 8, 36, 40),
text: "OK".to_string(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
},
],
);
let user_x = 10u32;
let user_y = 10u32;
let user_pixel = screen_base + user_y * row_bytes + user_x;
bus.write_byte(user_pixel, 0xA5);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, dialog_ptr);
bus.write_long(TEST_SP + 4, 0xCAFE_BABE);
disp.dispatch_dialog(true, 0x181, &mut cpu, &mut bus)
.unwrap()
.unwrap();
(
bus.read_byte(user_pixel),
cpu.read_reg(Register::A7),
bus.read_long(TEST_SP + 4),
)
}
fn monochrome_icon_resource_with_points(points: &[(u8, u8)]) -> Vec<u8> {
let mut icon = vec![0u8; 128];
for &(x, y) in points {
let byte = y as usize * 4 + x as usize / 8;
icon[byte] |= 0x80 >> (x & 7);
}
icon
}
fn write_be_word(data: &mut [u8], offset: usize, value: u16) {
data[offset] = (value >> 8) as u8;
data[offset + 1] = value as u8;
}
fn cicn_resource_with_points(width: u16, height: u16, points: &[(u8, u8, u8)]) -> Vec<u8> {
let pixel_row_bytes = u32::from(width);
let mask_row_bytes = u32::from(width).div_ceil(8);
let mask_size = mask_row_bytes * u32::from(height);
let bmap_size = mask_row_bytes * u32::from(height);
let ctab_size = 16u32;
let pixel_size = pixel_row_bytes * u32::from(height);
let bmap_offset = 82 + mask_size as usize;
let ctab_offset = bmap_offset + bmap_size as usize;
let pixel_offset = ctab_offset + ctab_size as usize;
let mut data = vec![0u8; pixel_offset + pixel_size as usize];
write_be_word(&mut data, 4, pixel_row_bytes as u16);
write_be_word(&mut data, 10, height);
write_be_word(&mut data, 12, width);
write_be_word(&mut data, 32, 8);
write_be_word(&mut data, 54, mask_row_bytes as u16);
write_be_word(&mut data, 60, height);
write_be_word(&mut data, 62, width);
write_be_word(&mut data, 68, mask_row_bytes as u16);
write_be_word(&mut data, 74, height);
write_be_word(&mut data, 76, width);
write_be_word(&mut data, ctab_offset + 6, 0);
for &(x, y, pixel) in points {
let mask_row = 82 + y as usize * mask_row_bytes as usize;
data[mask_row + x as usize / 8] |= 0x80 >> (x & 7);
let pixel_row = pixel_offset + y as usize * pixel_row_bytes as usize;
data[pixel_row + x as usize] = pixel;
}
data
}
fn drawdialog_icon_item_pixel_results_for_theme(theme_id: UiThemeId) -> ([u8; 3], u32, u32) {
let (mut disp, mut cpu, mut bus) = setup();
disp.set_ui_theme_id(theme_id);
let dialog_ptr = bus.alloc(170);
let (screen_base, row_bytes, _w, _h, pixel_size) = disp.screen_mode;
assert_eq!(pixel_size, 8);
bus.write_word(dialog_ptr + 8, 0);
bus.write_word(dialog_ptr + 10, 0);
bus.write_word(dialog_ptr + 16, 0);
bus.write_word(dialog_ptr + 18, 0);
bus.write_word(dialog_ptr + 20, 56);
bus.write_word(dialog_ptr + 22, 88);
disp.install_test_resource(
&mut bus,
*b"ICON",
421,
&monochrome_icon_resource_with_points(&[(0, 0), (15, 15), (31, 31)]),
);
disp.dialog_items.insert(
dialog_ptr,
vec![
DialogItem {
item_type: 32, // icon item
rect: (8, 8, 40, 40),
text: String::new(),
resource_id: 421,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
},
DialogItem {
item_type: 4,
rect: (44, 8, 54, 40),
text: "OK".to_string(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
},
],
);
for (x, y) in [(8u32, 8u32), (23, 23), (39, 39)] {
bus.write_byte(screen_base + y * row_bytes + x, 0x42);
}
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, dialog_ptr);
bus.write_long(TEST_SP + 4, 0xCAFE_BABE);
disp.dispatch_dialog(true, 0x181, &mut cpu, &mut bus)
.unwrap()
.unwrap();
(
[
bus.read_byte(screen_base + 8 * row_bytes + 8),
bus.read_byte(screen_base + 23 * row_bytes + 23),
bus.read_byte(screen_base + 39 * row_bytes + 39),
],
cpu.read_reg(Register::A7),
bus.read_long(TEST_SP + 4),
)
}
fn drawdialog_cicn_item_pixel_results_for_theme(theme_id: UiThemeId) -> ([u8; 3], u32, u32) {
let (mut disp, mut cpu, mut bus) = setup();
disp.set_ui_theme_id(theme_id);
let dialog_ptr = bus.alloc(170);
let (screen_base, row_bytes, _w, _h, pixel_size) = disp.screen_mode;
assert_eq!(pixel_size, 8);
bus.write_word(dialog_ptr + 8, 0);
bus.write_word(dialog_ptr + 10, 0);
bus.write_word(dialog_ptr + 16, 0);
bus.write_word(dialog_ptr + 18, 0);
bus.write_word(dialog_ptr + 20, 56);
bus.write_word(dialog_ptr + 22, 88);
let sample_points = [(0, 0), (15, 15), (31, 31)];
disp.install_test_resource(
&mut bus,
*b"ICON",
422,
&monochrome_icon_resource_with_points(&sample_points),
);
disp.install_test_resource(
&mut bus,
*b"cicn",
422,
&cicn_resource_with_points(32, 32, &[(0, 0, 0x33), (15, 15, 0x55), (31, 31, 0x77)]),
);
disp.dialog_items.insert(
dialog_ptr,
vec![
DialogItem {
item_type: 32, // icon item with same-ID cicn override
rect: (8, 8, 40, 40),
text: String::new(),
resource_id: 422,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
},
DialogItem {
item_type: 4,
rect: (44, 8, 54, 40),
text: "OK".to_string(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
},
],
);
for (x, y) in [(8u32, 8u32), (23, 23), (39, 39)] {
bus.write_byte(screen_base + y * row_bytes + x, 0x42);
}
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, dialog_ptr);
bus.write_long(TEST_SP + 4, 0xCAFE_BABE);
disp.dispatch_dialog(true, 0x181, &mut cpu, &mut bus)
.unwrap()
.unwrap();
(
[
bus.read_byte(screen_base + 8 * row_bytes + 8),
bus.read_byte(screen_base + 23 * row_bytes + 23),
bus.read_byte(screen_base + 39 * row_bytes + 39),
],
cpu.read_reg(Register::A7),
bus.read_long(TEST_SP + 4),
)
}
fn solid_fill_pict_resource(width: i16, height: i16) -> Vec<u8> {
let mut data = Vec::new();
data.extend_from_slice(&0u16.to_be_bytes()); // picSize placeholder
for value in [0i16, 0, height, width] {
data.extend_from_slice(&(value as u16).to_be_bytes());
}
data.push(0x11); // versionOp
data.push(0x01); // PICT v1
data.push(0x0A); // FillPat
data.extend_from_slice(&[0xFF; 8]);
data.push(0x34); // fillRect
for value in [0i16, 0, height, width] {
data.extend_from_slice(&(value as u16).to_be_bytes());
}
data.push(0xFF); // EndOfPicture
let size = data.len() as u16;
data[0..2].copy_from_slice(&size.to_be_bytes());
data
}
fn drawdialog_picture_item_pixel_results_for_theme(theme_id: UiThemeId) -> ([u8; 3], u32, u32) {
let (mut disp, mut cpu, mut bus) = setup();
disp.set_ui_theme_id(theme_id);
let dialog_ptr = bus.alloc(170);
let (screen_base, row_bytes, _w, _h, pixel_size) = disp.screen_mode;
assert_eq!(pixel_size, 8);
bus.write_word(dialog_ptr + 8, 0);
bus.write_word(dialog_ptr + 10, 0);
bus.write_word(dialog_ptr + 16, 0);
bus.write_word(dialog_ptr + 18, 0);
bus.write_word(dialog_ptr + 20, 48);
bus.write_word(dialog_ptr + 22, 80);
disp.install_test_resource(&mut bus, *b"PICT", 420, &solid_fill_pict_resource(16, 16));
disp.dialog_items.insert(
dialog_ptr,
vec![
DialogItem {
item_type: 64, // picture item
rect: (8, 8, 24, 24),
text: String::new(),
resource_id: 420,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
},
DialogItem {
item_type: 4,
rect: (30, 8, 42, 40),
text: "OK".to_string(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
},
],
);
for (x, y) in [(9u32, 9u32), (16, 16), (22, 22)] {
bus.write_byte(screen_base + y * row_bytes + x, 0x42);
}
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, dialog_ptr);
bus.write_long(TEST_SP + 4, 0xCAFE_BABE);
disp.dispatch_dialog(true, 0x181, &mut cpu, &mut bus)
.unwrap()
.unwrap();
(
[
bus.read_byte(screen_base + 9 * row_bytes + 9),
bus.read_byte(screen_base + 16 * row_bytes + 16),
bus.read_byte(screen_base + 22 * row_bytes + 22),
],
cpu.read_reg(Register::A7),
bus.read_long(TEST_SP + 4),
)
}
fn dialog_tracking_state_for_test(dialog_ptr: u32) -> DialogTrackingState {
DialogTrackingState {
dialog_ptr,
bounds: (0, 0, 0, 0),
title: String::new(),
proc_id: 0,
items: Vec::new(),
default_item: 1,
cancel_item: 2,
edit_text: String::new(),
edit_item: 0,
saved_pixels: Vec::new(),
stack_ptr: TEST_SP,
item_hit_ptr: 0,
rendered_pixels: Vec::new(),
flash_remaining: 0,
flash_delay: 0,
flash_item: 0,
edit_text_modified: false,
draw_proc_queue: VecDeque::new(),
draw_procs_done: true,
rendered_pixels_final: true,
filter_proc: 0,
game_managed: false,
last_filter_event: None,
popup_draws: Vec::new(),
active_popup: None,
active_button: None,
active_user_item: None,
}
}
fn dialog_lifecycle_cleanup_results_for_theme(
theme_id: UiThemeId,
) -> DialogLifecycleCleanupSnapshot {
let bounds = (100, 100, 150, 200);
let previous_bounds = (0, 0, 342, 512);
let saved_pixels = vec![0x33; 66 * 116];
let (mut close_disp, mut close_cpu, mut close_bus) = setup();
close_disp.set_ui_theme_id(theme_id);
close_disp.set_screen_mode_for_test(0x300000, 640, 640, 480, 8);
let close_dialog_ptr = close_bus.alloc(170);
let close_previous_window = close_bus.alloc(170);
seed_window_regions(&mut close_bus, close_dialog_ptr, bounds);
seed_window_regions(&mut close_bus, close_previous_window, previous_bounds);
close_bus.write_word(close_dialog_ptr + 108, 2);
close_disp.front_window = close_dialog_ptr;
close_disp.current_port = close_dialog_ptr;
close_disp.window_bounds = bounds;
close_disp.window_proc_id = 2;
close_disp.window_list = vec![close_dialog_ptr, close_previous_window];
close_disp.window_stack.push((
close_previous_window,
previous_bounds,
0,
"Previous".to_string(),
));
close_disp.dialog_items.insert(
close_dialog_ptr,
vec![DialogItem {
item_type: 4,
rect: (20, 40, 40, 100),
text: "OK".to_string(),
..Default::default()
}],
);
close_disp.dialog_tracking = Some(dialog_tracking_state_for_test(close_dialog_ptr));
close_disp.retained_modal_dialog_click = Some(RetainedModalDialogClickState {
dialog_ptr: close_dialog_ptr,
item_no: 1,
rect: (20, 40, 40, 100),
title: "OK".to_string(),
is_default: true,
highlighted: true,
delivered_to_app: false,
});
close_disp.dialog_visible_snapshots.insert(
close_dialog_ptr,
PersistentDialogSnapshot {
bounds,
pixels: saved_pixels.clone(),
},
);
close_disp.dialog_modal_entered.insert(close_dialog_ptr);
close_disp
.dialog_saved_pixels
.insert(close_dialog_ptr, saved_pixels.clone());
close_bus.write_long(crate::memory::globals::addr::THE_PORT, close_dialog_ptr);
let close_global_ptr = close_bus.read_long(close_cpu.read_reg(Register::A5));
close_bus.write_long(close_global_ptr, close_dialog_ptr);
close_cpu.write_reg(Register::A7, TEST_SP);
close_bus.write_long(TEST_SP, close_dialog_ptr);
close_disp
.dispatch_dialog(true, 0x182, &mut close_cpu, &mut close_bus)
.unwrap()
.unwrap();
let close_stack_after = close_cpu.read_reg(Register::A7);
let close_tracking_cleared = close_disp.dialog_tracking.is_none();
let close_retained_click_cleared = close_disp.retained_modal_dialog_click.is_none();
let close_visible_snapshot_cleared = !close_disp
.dialog_visible_snapshots
.contains_key(&close_dialog_ptr);
let close_modal_entered_cleared =
!close_disp.dialog_modal_entered.contains(&close_dialog_ptr);
let close_saved_pixels_cleared = !close_disp
.dialog_saved_pixels
.contains_key(&close_dialog_ptr);
let close_window_list_contains_dialog = close_disp.window_list.contains(&close_dialog_ptr);
let close_front_window_after = close_disp.front_window;
let close_the_port_after = close_bus.read_long(crate::memory::globals::addr::THE_PORT);
let close_current_port_after = close_disp.current_port;
let close_update_event_for_previous_window = close_disp
.event_queue
.iter()
.any(|event| event.what == 6 && event.message == close_previous_window);
let close_dialog_items_present_after =
close_disp.dialog_items.contains_key(&close_dialog_ptr);
let (mut dispose_disp, mut dispose_cpu, mut dispose_bus) = setup();
dispose_disp.set_ui_theme_id(theme_id);
dispose_disp.set_screen_mode_for_test(0x300000, 640, 640, 480, 8);
let dispose_dialog_ptr = dispose_bus.alloc(170);
let dispose_other_dialog_ptr = dispose_bus.alloc(170);
let dispose_previous_window = dispose_bus.alloc(170);
let dispose_text_handle = dispose_bus.alloc(4);
let dispose_other_text_handle = dispose_bus.alloc(4);
let dispose_ctrl_handle = dispose_bus.alloc(4);
let dispose_other_ctrl_handle = dispose_bus.alloc(4);
seed_window_regions(&mut dispose_bus, dispose_dialog_ptr, bounds);
seed_window_regions(&mut dispose_bus, dispose_previous_window, previous_bounds);
dispose_bus.write_word(dispose_dialog_ptr + 108, 2);
dispose_disp.front_window = dispose_dialog_ptr;
dispose_disp.current_port = dispose_dialog_ptr;
dispose_disp.window_bounds = bounds;
dispose_disp.window_proc_id = 2;
dispose_disp.window_list = vec![
dispose_dialog_ptr,
dispose_previous_window,
dispose_other_dialog_ptr,
];
dispose_disp.window_stack.push((
dispose_previous_window,
previous_bounds,
0,
"Previous".to_string(),
));
dispose_disp
.dialog_items
.insert(dispose_dialog_ptr, vec![DialogItem::default()]);
dispose_disp
.dialog_items
.insert(dispose_other_dialog_ptr, vec![DialogItem::default()]);
dispose_disp.dialog_tracking = Some(dialog_tracking_state_for_test(dispose_dialog_ptr));
dispose_disp.retained_modal_dialog_click = Some(RetainedModalDialogClickState {
dialog_ptr: dispose_dialog_ptr,
item_no: 1,
rect: (20, 40, 40, 100),
title: "OK".to_string(),
is_default: true,
highlighted: true,
delivered_to_app: false,
});
dispose_disp.dialog_visible_snapshots.insert(
dispose_dialog_ptr,
PersistentDialogSnapshot {
bounds,
pixels: saved_pixels.clone(),
},
);
dispose_disp.dialog_modal_entered.insert(dispose_dialog_ptr);
dispose_disp
.dialog_saved_pixels
.insert(dispose_dialog_ptr, saved_pixels);
dispose_disp
.dialog_item_handles
.insert(dispose_text_handle, (dispose_dialog_ptr, 0));
dispose_disp
.dialog_item_handles
.insert(dispose_other_text_handle, (dispose_other_dialog_ptr, 0));
dispose_disp
.dialog_control_handles
.insert(dispose_ctrl_handle, (dispose_dialog_ptr, 1));
dispose_disp
.dialog_control_handles
.insert(dispose_other_ctrl_handle, (dispose_other_dialog_ptr, 1));
dispose_disp
.dialog_control_values
.insert((dispose_dialog_ptr, 1), 1);
dispose_disp
.dialog_control_values
.insert((dispose_other_dialog_ptr, 1), 1);
dispose_disp
.hidden_dialog_item_rects
.insert((dispose_dialog_ptr, 1), (10, 20, 30, 40));
dispose_disp
.hidden_dialog_item_rects
.insert((dispose_other_dialog_ptr, 1), (50, 60, 70, 80));
dispose_disp
.dialog_cancel_items
.insert(dispose_dialog_ptr, 2);
dispose_disp
.dialog_cancel_items
.insert(dispose_other_dialog_ptr, 3);
dispose_disp.pending_dialog_popup_menu = Some(PendingDialogPopupMenu {
dialog_ptr: dispose_dialog_ptr,
item_no: 1,
menu_id: 900,
rect: (10, 20, 30, 130),
});
dispose_bus.write_long(crate::memory::globals::addr::THE_PORT, dispose_dialog_ptr);
let dispose_global_ptr = dispose_bus.read_long(dispose_cpu.read_reg(Register::A5));
dispose_bus.write_long(dispose_global_ptr, dispose_dialog_ptr);
dispose_cpu.write_reg(Register::A7, TEST_SP);
dispose_bus.write_long(TEST_SP, dispose_dialog_ptr);
dispose_disp
.dispatch_dialog(true, 0x183, &mut dispose_cpu, &mut dispose_bus)
.unwrap()
.unwrap();
DialogLifecycleCleanupSnapshot {
close_stack_after,
close_tracking_cleared,
close_retained_click_cleared,
close_visible_snapshot_cleared,
close_modal_entered_cleared,
close_saved_pixels_cleared,
close_window_list_contains_dialog,
close_front_window_after,
close_the_port_after,
close_current_port_after,
close_update_event_for_previous_window,
close_dialog_items_present_after,
dispose_stack_after: dispose_cpu.read_reg(Register::A7),
dispose_tracking_cleared: dispose_disp.dialog_tracking.is_none(),
dispose_retained_click_cleared: dispose_disp.retained_modal_dialog_click.is_none(),
dispose_visible_snapshot_cleared: !dispose_disp
.dialog_visible_snapshots
.contains_key(&dispose_dialog_ptr),
dispose_modal_entered_cleared: !dispose_disp
.dialog_modal_entered
.contains(&dispose_dialog_ptr),
dispose_saved_pixels_cleared: !dispose_disp
.dialog_saved_pixels
.contains_key(&dispose_dialog_ptr),
dispose_window_list_contains_dialog: dispose_disp
.window_list
.contains(&dispose_dialog_ptr),
dispose_front_window_after: dispose_disp.front_window,
dispose_the_port_after: dispose_bus.read_long(crate::memory::globals::addr::THE_PORT),
dispose_current_port_after: dispose_disp.current_port,
dispose_update_event_for_previous_window: dispose_disp
.event_queue
.iter()
.any(|event| event.what == 6 && event.message == dispose_previous_window),
dispose_dialog_items_present_after: dispose_disp
.dialog_items
.contains_key(&dispose_dialog_ptr),
dispose_other_dialog_items_present_after: dispose_disp
.dialog_items
.contains_key(&dispose_other_dialog_ptr),
dispose_dialog_item_handle_present_after: dispose_disp
.dialog_item_handles
.contains_key(&dispose_text_handle),
dispose_other_dialog_item_handle_present_after: dispose_disp
.dialog_item_handles
.contains_key(&dispose_other_text_handle),
dispose_dialog_control_handle_present_after: dispose_disp
.dialog_control_handles
.contains_key(&dispose_ctrl_handle),
dispose_other_dialog_control_handle_present_after: dispose_disp
.dialog_control_handles
.contains_key(&dispose_other_ctrl_handle),
dispose_dialog_control_value_present_after: dispose_disp
.dialog_control_values
.contains_key(&(dispose_dialog_ptr, 1)),
dispose_other_dialog_control_value_present_after: dispose_disp
.dialog_control_values
.contains_key(&(dispose_other_dialog_ptr, 1)),
dispose_hidden_rect_present_after: dispose_disp
.hidden_dialog_item_rects
.contains_key(&(dispose_dialog_ptr, 1)),
dispose_other_hidden_rect_present_after: dispose_disp
.hidden_dialog_item_rects
.contains_key(&(dispose_other_dialog_ptr, 1)),
dispose_cancel_item_present_after: dispose_disp
.dialog_cancel_items
.contains_key(&dispose_dialog_ptr),
dispose_other_cancel_item_present_after: dispose_disp
.dialog_cancel_items
.contains_key(&dispose_other_dialog_ptr),
dispose_pending_popup_cleared: dispose_disp.pending_dialog_popup_menu.is_none(),
}
}
fn cached_dialog_items(
disp: &TrapDispatcher,
dialog_ptr: u32,
) -> Vec<(u8, (i16, i16, i16, i16), String, i16)> {
disp.dialog_items
.get(&dialog_ptr)
.into_iter()
.flatten()
.map(|item| {
(
item.item_type,
item.rect,
item.text.clone(),
item.resource_id,
)
})
.collect()
}
fn dialog_port_rect(bus: &MacMemoryBus, dialog_ptr: u32) -> (i16, i16, i16, i16) {
(
bus.read_word(dialog_ptr + 16) as i16,
bus.read_word(dialog_ptr + 18) as i16,
bus.read_word(dialog_ptr + 20) as i16,
bus.read_word(dialog_ptr + 22) as i16,
)
}
fn dialog_creation_results_for_theme(theme_id: UiThemeId) -> DialogCreationSnapshot {
let (mut new_disp, mut new_cpu, mut new_bus) = setup();
new_disp.set_ui_theme_id(theme_id);
let new_screen_base = new_bus.alloc((640 * 480) as u32);
new_bus.write_long(0x0824, new_screen_base);
new_disp.screen_mode = (new_screen_base, 640, 640, 480, 8);
let new_ditl = build_test_ditl_items(&[
(16, (10, 12, 28, 120), b"Alpha".as_slice()),
(4, (44, 70, 66, 132), b"OK".as_slice()),
]);
let new_items_data_ptr = new_bus.alloc(new_ditl.len() as u32);
new_bus.write_bytes(new_items_data_ptr, &new_ditl);
let new_items_handle = new_bus.alloc(4);
new_bus.write_long(new_items_handle, new_items_data_ptr);
let new_title = new_bus.alloc(32);
new_bus.write_pstring(new_title, b"Modeless");
let new_bounds = new_bus.alloc(8);
new_bus.write_word(new_bounds, 60);
new_bus.write_word(new_bounds + 2, 70);
new_bus.write_word(new_bounds + 4, 140);
new_bus.write_word(new_bounds + 6, 220);
let new_storage = new_bus.alloc(170);
let new_sp = TEST_SP - 30;
new_cpu.write_reg(Register::A7, new_sp);
new_bus.write_long(new_sp, new_items_handle);
new_bus.write_long(new_sp + 4, 0x1234_5678);
new_bus.write_byte(new_sp + 8, 0xFF); // goAwayFlag
new_bus.write_long(new_sp + 10, 0xFFFF_FFFF); // behind = front
new_bus.write_word(new_sp + 14, 4); // noGrowDocProc/modeless
new_bus.write_byte(new_sp + 16, 0xFF); // visible
new_bus.write_long(new_sp + 18, new_title);
new_bus.write_long(new_sp + 22, new_bounds);
new_bus.write_long(new_sp + 26, new_storage);
new_bus.write_long(new_sp + 30, 0xDEAD_BEEF);
new_disp
.dispatch_dialog(true, 0x17D, &mut new_cpu, &mut new_bus)
.unwrap()
.unwrap();
let new_dialog_ptr = new_bus.read_long(new_sp + 30);
let new_update_region =
TrapDispatcher::region_handle_rect(&new_bus, new_bus.read_long(new_dialog_ptr + 122));
let new_update_event_queued = new_disp
.event_queue
.iter()
.any(|event| event.what == 6 && event.message == new_dialog_ptr);
let (mut get_disp, mut get_cpu, mut get_bus) = setup();
get_disp.set_ui_theme_id(theme_id);
let get_screen_base = get_bus.alloc((640 * 480) as u32);
get_bus.write_long(0x0824, get_screen_base);
get_disp.screen_mode = (get_screen_base, 640, 640, 480, 8);
let mut dlog = build_test_dlog((80, 90, 160, 240), 1901, 0);
dlog[10] = 1; // visible
let get_ditl = build_test_ditl_items(&[
(16, (12, 18, 30, 128), b"Beta".as_slice()),
(4, (46, 78, 68, 138), b"OK".as_slice()),
]);
get_disp.install_test_resource(&mut get_bus, *b"DLOG", 1900, &dlog);
let original_ditl_ptr =
get_disp.install_test_resource(&mut get_bus, *b"DITL", 1901, &get_ditl);
let get_storage = get_bus.alloc(170);
get_cpu.write_reg(Register::A7, TEST_SP);
get_bus.write_long(TEST_SP, 0xFFFF_FFFF); // behind = front
get_bus.write_long(TEST_SP + 4, get_storage);
get_bus.write_word(TEST_SP + 8, 1900);
get_bus.write_long(TEST_SP + 10, 0xDEAD_BEEF);
get_disp
.dispatch_dialog(true, 0x17C, &mut get_cpu, &mut get_bus)
.unwrap()
.unwrap();
let get_dialog_ptr = get_bus.read_long(TEST_SP + 10);
let get_items_handle = get_bus.read_long(get_dialog_ptr + 156);
let get_items_data_ptr = get_bus.read_long(get_items_handle);
let get_update_region =
TrapDispatcher::region_handle_rect(&get_bus, get_bus.read_long(get_dialog_ptr + 122));
let get_update_event_queued = get_disp
.event_queue
.iter()
.any(|event| event.what == 6 && event.message == get_dialog_ptr);
DialogCreationSnapshot {
new_dialog_ptr,
new_stack_after: new_cpu.read_reg(Register::A7),
new_result_slot: new_bus.read_long(new_sp + 30),
new_window_list: new_disp.window_list.clone(),
new_front_window: new_disp.front_window,
new_current_port: new_disp.current_port,
new_the_port: new_bus.read_long(crate::memory::globals::addr::THE_PORT),
new_visible_byte: new_bus.read_byte(new_dialog_ptr + 110),
new_goaway_byte: new_bus.read_byte(new_dialog_ptr + 112),
new_refcon: new_bus.read_long(new_dialog_ptr + 152),
new_window_kind: new_bus.read_word(new_dialog_ptr + 108) as i16,
new_proc_id: new_disp
.window_proc_ids
.get(&new_dialog_ptr)
.copied()
.unwrap_or_default(),
new_port_rect: dialog_port_rect(&new_bus, new_dialog_ptr),
new_items_handle: new_bus.read_long(new_dialog_ptr + 156),
new_items_data_ptr: new_bus.read_long(new_items_handle),
new_first_item_handle_nonzero: new_bus.read_long(new_items_data_ptr + 2) != 0,
new_cached_items: cached_dialog_items(&new_disp, new_dialog_ptr),
new_update_region,
new_update_event_queued,
new_edit_field: new_bus.read_word(new_dialog_ptr + 164),
new_default_item: new_bus.read_word(new_dialog_ptr + 168),
get_dialog_ptr,
get_stack_after: get_cpu.read_reg(Register::A7),
get_result_slot: get_bus.read_long(TEST_SP + 10),
get_window_list: get_disp.window_list.clone(),
get_front_window: get_disp.front_window,
get_current_port: get_disp.current_port,
get_the_port: get_bus.read_long(crate::memory::globals::addr::THE_PORT),
get_visible_byte: get_bus.read_byte(get_dialog_ptr + 110),
get_refcon: get_bus.read_long(get_dialog_ptr + 152),
get_window_kind: get_bus.read_word(get_dialog_ptr + 108) as i16,
get_proc_id: get_disp
.window_proc_ids
.get(&get_dialog_ptr)
.copied()
.unwrap_or_default(),
get_port_rect: dialog_port_rect(&get_bus, get_dialog_ptr),
get_items_handle,
get_items_data_ptr,
get_uses_distinct_ditl_copy: get_items_data_ptr != original_ditl_ptr,
get_original_ditl_handle_field: get_bus.read_long(original_ditl_ptr + 2),
get_first_item_handle_nonzero: get_bus.read_long(get_items_data_ptr + 2) != 0,
get_cached_items: cached_dialog_items(&get_disp, get_dialog_ptr),
get_update_region,
get_update_event_queued,
get_edit_field: get_bus.read_word(get_dialog_ptr + 164),
get_default_item: get_bus.read_word(get_dialog_ptr + 168),
}
}
fn alert_family_results_for_theme(theme_id: UiThemeId) -> AlertFamilySnapshot {
let (mut disp, mut cpu, mut bus) = setup();
disp.set_ui_theme_id(theme_id);
let ditl = build_test_ditl_items(&[
(4, (48, 70, 70, 132), b"OK".as_slice()),
// Keep this fixture on the auto-return path while still verifying
// that stage defaults can report item 2.
(0x84, (48, 144, 70, 212), b"Cancel".as_slice()),
]);
let alrt = build_alrt_template(2100, 0xC4C4);
disp.install_test_resource(&mut bus, *b"DITL", 2100, &ditl);
disp.install_test_resource(&mut bus, *b"ALRT", 2099, &alrt);
bus.write_word(crate::memory::globals::addr::ALERT_STAGE, 0);
bus.write_word(crate::memory::globals::addr::ANUMBER, 0xCAFE);
let mut staged_calls = Vec::new();
for trap_word in [0x185u16, 0x186, 0x187, 0x188, 0x185] {
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP + 4, 2099);
disp.dispatch_dialog(true, trap_word, &mut cpu, &mut bus)
.unwrap()
.unwrap();
staged_calls.push(AlertTrapCallSnapshot {
trap_word,
result: bus.read_word(TEST_SP + 6) as i16,
stack_after: cpu.read_reg(Register::A7),
alert_stage_after: bus.read_word(crate::memory::globals::addr::ALERT_STAGE),
anumber_after: bus.read_word(crate::memory::globals::addr::ANUMBER),
});
}
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(crate::memory::globals::addr::ALERT_STAGE, 2);
bus.write_word(crate::memory::globals::addr::ANUMBER, 0xBEEF);
bus.write_word(TEST_SP + 4, 2999);
disp.dispatch_dialog(true, 0x187, &mut cpu, &mut bus)
.unwrap()
.unwrap();
let missing_call = AlertTrapCallSnapshot {
trap_word: 0x187,
result: bus.read_word(TEST_SP + 6) as i16,
stack_after: cpu.read_reg(Register::A7),
alert_stage_after: bus.read_word(crate::memory::globals::addr::ALERT_STAGE),
anumber_after: bus.read_word(crate::memory::globals::addr::ANUMBER),
};
let (mut prep_disp, mut prep_cpu, mut prep_bus) = setup();
prep_disp.set_ui_theme_id(theme_id);
let prep_alrt = build_alrt_template(2101, 0);
prep_disp.install_test_resource(&mut prep_bus, *b"ALRT", 2102, &prep_alrt);
let mut prep_call = |trap_word: u16, alert_id: i16| -> (i16, u32) {
prep_cpu.write_reg(Register::A7, TEST_SP);
prep_bus.write_word(0x0A60, 0x7FFF);
prep_bus.write_word(TEST_SP, alert_id as u16);
prep_disp
.dispatch_dialog(true, trap_word, &mut prep_cpu, &mut prep_bus)
.unwrap()
.unwrap();
(
prep_bus.read_word(0x0A60) as i16,
prep_cpu.read_reg(Register::A7),
)
};
let resource_prep = AlertResourcePrepSnapshot {
could_present: prep_call(0x189, 2102),
free_present: prep_call(0x18A, 2102),
could_missing: prep_call(0x189, 2998),
free_missing: prep_call(0x18A, 2998),
};
AlertFamilySnapshot {
staged_calls,
missing_call,
resource_prep,
}
}
fn dialog_init_sound_results_for_theme(theme_id: UiThemeId) -> DialogInitSoundSnapshot {
let (mut disp, mut cpu, mut bus) = setup();
disp.set_ui_theme_id(theme_id);
use crate::memory::globals::addr;
bus.write_long(addr::RESUME_PROC, 0xDEAD_BEEF);
bus.write_long(addr::DA_BEEPER, 0x00AA_BBCC);
bus.write_word(addr::ALERT_STAGE, 3);
bus.write_word(addr::ANUMBER, 0xCAFE);
for i in 0..4u32 {
bus.write_long(addr::DA_STRINGS + i * 4, 0x00D0_0000 | i);
}
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, 0x0012_3456);
disp.dispatch_dialog(true, 0x17B, &mut cpu, &mut bus)
.unwrap()
.unwrap();
let init_stack_after = cpu.read_reg(Register::A7);
let resume_proc_after_init = bus.read_long(addr::RESUME_PROC);
let da_beeper_after_init = bus.read_long(addr::DA_BEEPER);
let alert_stage_after_init = bus.read_word(addr::ALERT_STAGE);
let anumber_after_init = bus.read_word(addr::ANUMBER);
let da_strings_after_init =
std::array::from_fn(|i| bus.read_long(addr::DA_STRINGS + i as u32 * 4));
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, 0x00AB_CDEF);
disp.dispatch_dialog(true, 0x18C, &mut cpu, &mut bus)
.unwrap()
.unwrap();
let error_sound_stack_after = cpu.read_reg(Register::A7);
let da_beeper_after_error_sound = bus.read_long(addr::DA_BEEPER);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, 0);
disp.dispatch_dialog(true, 0x18C, &mut cpu, &mut bus)
.unwrap()
.unwrap();
let nil_error_sound_stack_after = cpu.read_reg(Register::A7);
let da_beeper_after_nil_error_sound = bus.read_long(addr::DA_BEEPER);
DialogInitSoundSnapshot {
init_stack_after,
resume_proc_after_init,
da_beeper_after_init,
alert_stage_after_init,
anumber_after_init,
da_strings_after_init,
error_sound_stack_after,
da_beeper_after_error_sound,
nil_error_sound_stack_after,
da_beeper_after_nil_error_sound,
}
}
fn dialogdispatch_default_cancel_results_for_theme(
theme_id: UiThemeId,
) -> DialogDispatchDefaultCancelSnapshot {
let (mut disp, mut cpu, mut bus) = setup();
disp.set_ui_theme_id(theme_id);
let dialog_ptr = bus.alloc(256);
bus.write_word(dialog_ptr + 168, 1);
disp.dialog_tracking = Some(dialog_tracking_state_for_test(dialog_ptr));
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 9); // newItem
bus.write_long(TEST_SP + 2, dialog_ptr); // theDialog
bus.write_word(TEST_SP + 6, 0xBEEF); // OSErr result slot
cpu.write_reg(Register::D0, 0x0304); // selector 4, 6 param bytes
disp.dispatch_dialog(true, 0x268, &mut cpu, &mut bus)
.unwrap()
.unwrap();
let default_result = bus.read_word(TEST_SP + 6);
let default_stack_after = cpu.read_reg(Register::A7);
let default_adef_item = bus.read_word(dialog_ptr + 168);
let default_tracking_item = disp
.dialog_tracking
.as_ref()
.map(|tracking| tracking.default_item)
.unwrap_or(-1);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 7); // newItem
bus.write_long(TEST_SP + 2, dialog_ptr); // theDialog
bus.write_word(TEST_SP + 6, 0xCAFE); // OSErr result slot
cpu.write_reg(Register::D0, 0x0305); // selector 5, 6 param bytes
disp.dispatch_dialog(true, 0x268, &mut cpu, &mut bus)
.unwrap()
.unwrap();
let cancel_result = bus.read_word(TEST_SP + 6);
let cancel_stack_after = cpu.read_reg(Register::A7);
let cancel_map_item = disp.dialog_cancel_items.get(&dialog_ptr).copied();
let cancel_tracking_item = disp
.dialog_tracking
.as_ref()
.map(|tracking| tracking.cancel_item)
.unwrap_or(-1);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 1); // tracks = TRUE
bus.write_long(TEST_SP + 2, dialog_ptr); // theDialog
bus.write_word(TEST_SP + 6, 0xCAFE); // OSErr result slot
cpu.write_reg(Register::D0, 0x0306); // selector 6, 6 param bytes
disp.dispatch_dialog(true, 0x268, &mut cpu, &mut bus)
.unwrap()
.unwrap();
let tracks_result = bus.read_word(TEST_SP + 6);
let tracks_stack_after = cpu.read_reg(Register::A7);
let tracks_adef_item = bus.read_word(dialog_ptr + 168);
let tracks_cancel_map_item = disp.dialog_cancel_items.get(&dialog_ptr).copied();
let tracks_tracking_items = disp
.dialog_tracking
.as_ref()
.map(|tracking| (tracking.default_item, tracking.cancel_item))
.unwrap_or((-1, -1));
DialogDispatchDefaultCancelSnapshot {
default_result,
default_stack_after,
default_adef_item,
default_tracking_item,
cancel_result,
cancel_stack_after,
cancel_map_item,
cancel_tracking_item,
tracks_result,
tracks_stack_after,
tracks_adef_item,
tracks_cancel_map_item,
tracks_tracking_items,
}
}
fn updtdialog_update_region_results_for_theme(
theme_id: UiThemeId,
) -> UpdtDialogUpdateRegionSnapshot {
let (mut disp, mut cpu, mut bus) = setup_with_port();
disp.set_ui_theme_id(theme_id);
let initial_port = 0x181000;
disp.set_current_port_state(&mut bus, &mut cpu, initial_port, None);
let screen_base = bus.alloc(100 * 100);
bus.write_long(0x0824, screen_base);
disp.screen_mode = (screen_base, 100, 100, 100, 8);
let dialog_ptr = bus.alloc(256);
bus.write_word(dialog_ptr, 0);
bus.write_long(dialog_ptr + 2, screen_base);
bus.write_word(dialog_ptr + 6, 100);
bus.write_word(dialog_ptr + 8, 0);
bus.write_word(dialog_ptr + 10, 0);
bus.write_word(dialog_ptr + 12, 100);
bus.write_word(dialog_ptr + 14, 100);
bus.write_word(dialog_ptr + 16, 0);
bus.write_word(dialog_ptr + 18, 0);
bus.write_word(dialog_ptr + 20, 100);
bus.write_word(dialog_ptr + 22, 100);
bus.write_word(dialog_ptr + 108, 2);
disp.window_list.push(dialog_ptr);
disp.front_window = dialog_ptr;
disp.dialog_initial_draw_deferred.insert(dialog_ptr);
disp.dialog_items.insert(
dialog_ptr,
vec![
DialogItem {
item_type: 0,
rect: (8, 8, 24, 32),
proc_ptr: 0x500000,
..Default::default()
},
DialogItem {
item_type: 0,
rect: (60, 60, 80, 80),
proc_ptr: 0x600000,
..Default::default()
},
DialogItem {
item_type: 8,
rect: (90, 90, 96, 96),
text: "x".to_string(),
..Default::default()
},
DialogItem {
item_type: 4,
rect: (52, 8, 72, 44),
text: "OK".to_string(),
..Default::default()
},
],
);
let inside_update_pixel = screen_base + 4 * 100 + 4;
let outside_update_pixel = screen_base + 70 * 100 + 70;
let outside_item_pixel = screen_base + 52 * 100 + 9;
bus.write_byte(inside_update_pixel, 0xA5);
bus.write_byte(outside_update_pixel, 0x5A);
bus.write_byte(outside_item_pixel, 0x3C);
let update_rgn_ptr = bus.alloc(10);
let update_rgn = bus.alloc(4);
bus.write_long(update_rgn, update_rgn_ptr);
bus.write_word(update_rgn_ptr, 10);
bus.write_word(update_rgn_ptr + 2, 0);
bus.write_word(update_rgn_ptr + 4, 0);
bus.write_word(update_rgn_ptr + 6, 40);
bus.write_word(update_rgn_ptr + 8, 40);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, update_rgn);
bus.write_long(TEST_SP + 4, dialog_ptr);
disp.dispatch_dialog(true, 0x178, &mut cpu, &mut bus)
.unwrap()
.unwrap();
UpdtDialogUpdateRegionSnapshot {
stack_after: cpu.read_reg(Register::A7),
the_port_after: bus.read_long(crate::memory::globals::addr::THE_PORT),
current_port_after: disp.current_port,
deferred_after: disp.dialog_initial_draw_deferred.contains(&dialog_ptr),
queued_draw_procs: disp
.modeless_dialog_draw_proc_queue
.iter()
.copied()
.collect(),
item_count_after: disp
.dialog_items
.get(&dialog_ptr)
.map(Vec::len)
.unwrap_or_default(),
inside_update_pixel_after: bus.read_byte(inside_update_pixel),
outside_update_pixel_after: bus.read_byte(outside_update_pixel),
outside_item_pixel_after: bus.read_byte(outside_item_pixel),
}
}
#[test]
fn systemless_theme_does_not_change_dialogselect_item_hits() {
// IM:I I-417: DialogSelect reports enabled dialog item hits through
// its Boolean result, dialog pointer, and itemHit. Theme rendering is
// outside that guest-visible event contract.
let classic = dialog_select_enabled_user_item_hit_for_theme(UiThemeId::ClassicSystem7);
let themed = dialog_select_enabled_user_item_hit_for_theme(UiThemeId::SystemlessDefault);
assert_eq!(classic.0, 0xFFFF);
assert_eq!(classic.2, 1);
assert_eq!(classic.3, TEST_SP + 12);
assert_eq!(
themed, classic,
"systemless-default must not change DialogSelect item-hit semantics"
);
}
#[test]
fn systemless_theme_does_not_change_dialogselect_control_item_hits() {
// IM:I I-417: for mouseDown in an enabled control, DialogSelect
// returns TRUE and itemHit after Control Manager tracking succeeds.
// The app owns checkbox value changes, so theme chrome must not alter
// the ControlRecord value or dialog-scoped control-value mirror.
let classic = dialog_select_enabled_checkbox_hit_for_theme(UiThemeId::ClassicSystem7);
let themed = dialog_select_enabled_checkbox_hit_for_theme(UiThemeId::SystemlessDefault);
assert_eq!(classic.0, 0xFFFF);
assert_eq!(classic.2, 1);
assert_eq!(classic.3, TEST_SP + 12);
assert_eq!(classic.4, 1);
assert_eq!(classic.5, 1);
assert_eq!(
themed, classic,
"systemless-default must not change DialogSelect control item-hit or value semantics"
);
}
#[test]
fn systemless_theme_does_not_change_dialogselect_window_events() {
// MTE 1992 p. 6-139 and IM:I I-417: activate/update events for a
// dialog window are handled by DialogSelect and return FALSE rather
// than reporting an enabled item through theDialog/itemHit.
// MTE 1992 pp. 6-141 and 6-143 plus IM:I I-291: update handling
// brackets the redraw with BeginUpdate/EndUpdate, uses the dialog
// port, redraws the dialog, and leaves app-owned userItem drawing
// to the guest draw procs.
// Theme rendering can change pixels, but not this function ABI,
// output-slot behavior, stack protocol, or update bookkeeping.
let classic = dialog_select_window_event_results_for_theme(UiThemeId::ClassicSystem7);
let themed = dialog_select_window_event_results_for_theme(UiThemeId::SystemlessDefault);
assert_eq!(classic.update_result, 0);
assert_eq!(classic.update_dialog_out, 0xDEAD_BEEF);
assert_eq!(classic.update_item_hit, 0xCAFE);
assert_eq!(classic.update_stack_after, TEST_SP + 12);
assert!(!classic.update_deferred_after);
assert_eq!(classic.update_region_after, None);
assert_eq!(classic.update_vis_region_after, Some((0, 0, 100, 100)));
assert_eq!(
classic.update_the_port_after,
classic.update_current_port_after
);
assert_eq!(classic.update_current_port_after, classic.dialog_ptr);
assert!(!classic.update_saved_vis_after);
assert_eq!(
classic.update_queued_draw_procs,
vec![(classic.dialog_ptr, 0x500000, 1)]
);
assert_eq!(classic.update_item_count_after, 3);
assert_eq!(classic.activate_result, 0);
assert_eq!(classic.activate_dialog_out, 0xDEAD_BEEF);
assert_eq!(classic.activate_item_hit, 0xCAFE);
assert_eq!(classic.activate_stack_after, TEST_SP + 12);
assert_eq!(
themed, classic,
"systemless-default must not change DialogSelect update/activate handling"
);
}
#[test]
fn systemless_theme_does_not_change_modaldialog_edit_text_mouse_handling() {
// IM:I I-415: ModalDialog handles mouseDown in an editText item using
// TextEdit and returns the item when it is enabled. Theme rendering
// must not change the returned item, retained-dialog state, active
// editField, TERecord mirroring, queued mouse-up consumption, or stack
// protocol.
let classic = modal_dialog_edit_text_mouse_results_for_theme(UiThemeId::ClassicSystem7);
let themed = modal_dialog_edit_text_mouse_results_for_theme(UiThemeId::SystemlessDefault);
assert_eq!(classic.item_hit, 2);
assert_eq!(classic.stack_after, TEST_SP + 8);
assert!(classic.tracking_finished);
assert!(classic.retained_visible_snapshot);
assert!(classic.saved_background_retained);
assert!(classic.queued_mouse_up_consumed);
assert_eq!(classic.edit_field, 1);
assert_eq!(classic.item_selection, (2, 4));
assert_eq!(classic.te_text, b"Second".to_vec());
assert_eq!(classic.te_length, 6);
assert_eq!(classic.te_selection, (2, 4));
assert_eq!(classic.handle_bytes, b"Second".to_vec());
assert_eq!(
themed, classic,
"systemless-default must not change ModalDialog editText mouse handling"
);
}
#[test]
fn systemless_theme_does_not_change_modaldialog_keyboard_default_cancel_items() {
// MTE 1992 p. 6-30 and p. 6-138: Return/Enter activate the default
// button; Esc and Command-period activate the Cancel button. Theme
// chrome must not change the key-to-item mapping, flash lifecycle,
// returned itemHit, or Pascal stack protocol.
let classic = modal_dialog_keyboard_button_results_for_theme(UiThemeId::ClassicSystem7);
let themed = modal_dialog_keyboard_button_results_for_theme(UiThemeId::SystemlessDefault);
for case in [&classic.return_key, &classic.enter_key] {
assert_eq!(case.flash_item_after_key, 1);
assert_eq!(case.stack_after_key, TEST_SP);
assert_eq!(case.item_hit_after_key, 0xCAFE);
assert_eq!(case.item_hit_after_flash, 1);
assert_eq!(case.stack_after_flash, TEST_SP + 8);
assert!(case.tracking_finished);
}
for case in [&classic.escape_key, &classic.command_period] {
assert_eq!(case.flash_item_after_key, 2);
assert_eq!(case.stack_after_key, TEST_SP);
assert_eq!(case.item_hit_after_key, 0xCAFE);
assert_eq!(case.item_hit_after_flash, 2);
assert_eq!(case.stack_after_flash, TEST_SP + 8);
assert!(case.tracking_finished);
}
assert_eq!(
themed, classic,
"systemless-default must not change ModalDialog keyboard default/cancel handling"
);
}
#[test]
fn systemless_theme_does_not_change_modaldialog_update_events() {
// MTE 1992 pp. 6-135 and 6-141: ModalDialog routes update events
// through IsDialogEvent/DialogSelect-style handling. That brackets
// the redraw with BeginUpdate/EndUpdate and makes the dialog the
// current graphics port, while ModalDialog itself does not return
// an item for update events. Theme chrome must not alter this ABI or
// the retained visible dialog snapshot used across modal refires.
let classic = modal_dialog_update_event_results_for_theme(UiThemeId::ClassicSystem7);
let themed = modal_dialog_update_event_results_for_theme(UiThemeId::SystemlessDefault);
assert_eq!(classic.item_hit_after, 0xCAFE);
assert_eq!(classic.stack_after, TEST_SP);
assert!(!classic.tracking_finished);
assert!(classic.rendered_pixels_final);
assert_eq!(classic.retained_pixel_after, 0x44);
assert_eq!(classic.retained_snapshot_pixel_after, 0x44);
assert_eq!(classic.update_region_after, None);
assert_eq!(classic.vis_region_after, Some((0, 0, 80, 80)));
assert_eq!(classic.the_port_after, classic.current_port_after);
assert_eq!(classic.current_port_after, classic.dialog_ptr);
assert!(!classic.saved_vis_after);
assert_eq!(
themed, classic,
"systemless-default must not change ModalDialog update-event handling"
);
}
#[test]
fn systemless_theme_does_not_change_dialog_lifecycle_cleanup() {
// MTE 1992 pp. 6-119..6-120: CloseDialog removes the dialog from
// the screen/window list, while DisposeDialog calls CloseDialog and
// additionally releases dialog-owned item-list/dialog-record storage.
// Theme chrome must not alter Pascal stack discipline, active
// ModalDialog cleanup, retained-click cleanup, saved snapshot
// cleanup, front-window promotion, current-port side effects, or
// dialog-scoped side-map cleanup.
let classic = dialog_lifecycle_cleanup_results_for_theme(UiThemeId::ClassicSystem7);
let themed = dialog_lifecycle_cleanup_results_for_theme(UiThemeId::SystemlessDefault);
assert_eq!(classic.close_stack_after, TEST_SP + 4);
assert!(classic.close_tracking_cleared);
assert!(classic.close_retained_click_cleared);
assert!(classic.close_visible_snapshot_cleared);
assert!(classic.close_modal_entered_cleared);
assert!(classic.close_saved_pixels_cleared);
assert!(!classic.close_window_list_contains_dialog);
assert_eq!(
classic.close_front_window_after,
classic.close_the_port_after
);
assert_eq!(
classic.close_front_window_after,
classic.close_current_port_after
);
assert!(classic.close_update_event_for_previous_window);
assert!(!classic.close_dialog_items_present_after);
assert_eq!(classic.dispose_stack_after, TEST_SP + 4);
assert!(classic.dispose_tracking_cleared);
assert!(classic.dispose_retained_click_cleared);
assert!(classic.dispose_visible_snapshot_cleared);
assert!(classic.dispose_modal_entered_cleared);
assert!(classic.dispose_saved_pixels_cleared);
assert!(!classic.dispose_window_list_contains_dialog);
assert_eq!(
classic.dispose_front_window_after,
classic.dispose_the_port_after
);
assert_eq!(
classic.dispose_front_window_after,
classic.dispose_current_port_after
);
assert!(classic.dispose_update_event_for_previous_window);
assert!(!classic.dispose_dialog_items_present_after);
assert!(classic.dispose_other_dialog_items_present_after);
assert!(!classic.dispose_dialog_item_handle_present_after);
assert!(classic.dispose_other_dialog_item_handle_present_after);
assert!(!classic.dispose_dialog_control_handle_present_after);
assert!(classic.dispose_other_dialog_control_handle_present_after);
assert!(!classic.dispose_dialog_control_value_present_after);
assert!(classic.dispose_other_dialog_control_value_present_after);
assert!(!classic.dispose_hidden_rect_present_after);
assert!(classic.dispose_other_hidden_rect_present_after);
assert!(!classic.dispose_cancel_item_present_after);
assert!(classic.dispose_other_cancel_item_present_after);
assert!(classic.dispose_pending_popup_cleared);
assert_eq!(
themed, classic,
"systemless-default must not change CloseDialog/DisposeDialog lifecycle cleanup"
);
}
#[test]
fn systemless_theme_does_not_change_dialog_creation_records() {
// IM:I I-412 and MTE 1992 pp. 6-118..6-119: NewDialog creates a
// dialog from caller-supplied parameters and an item-list handle,
// sets dialogKind/default dialog fields, and returns a DialogPtr.
// IM:I I-412 and I-424: GetNewDialog reads DLOG/DITL resources and
// uses a copy of the item list. Theme chrome must not alter resource
// parsing, item-list ownership, DialogRecord fields, visible/update
// bookkeeping, current-port side effects, or Pascal stack ABI.
let classic = dialog_creation_results_for_theme(UiThemeId::ClassicSystem7);
let themed = dialog_creation_results_for_theme(UiThemeId::SystemlessDefault);
assert_ne!(classic.new_dialog_ptr, 0);
assert_eq!(classic.new_stack_after, TEST_SP);
assert_eq!(classic.new_result_slot, classic.new_dialog_ptr);
assert_eq!(classic.new_window_list, vec![classic.new_dialog_ptr]);
assert_eq!(classic.new_front_window, classic.new_dialog_ptr);
assert_eq!(classic.new_current_port, classic.new_dialog_ptr);
assert_eq!(classic.new_the_port, classic.new_dialog_ptr);
assert_eq!(classic.new_visible_byte, 0xFF);
assert_eq!(classic.new_goaway_byte, 0xFF);
assert_eq!(classic.new_refcon, 0x1234_5678);
assert_eq!(classic.new_window_kind, 2);
assert_eq!(classic.new_proc_id, 4);
assert_eq!(classic.new_port_rect, (0, 0, 80, 150));
assert_ne!(classic.new_items_handle, 0);
assert_ne!(classic.new_items_data_ptr, 0);
assert!(classic.new_first_item_handle_nonzero);
assert_eq!(
classic.new_cached_items,
vec![
(16, (10, 12, 28, 120), "Alpha".to_string(), 0),
(4, (44, 70, 66, 132), "OK".to_string(), 0),
]
);
assert!(classic.new_update_region.is_some());
assert!(classic.new_update_event_queued);
assert_eq!(classic.new_edit_field, 0xFFFF);
assert_eq!(classic.new_default_item, 1);
assert_ne!(classic.get_dialog_ptr, 0);
assert_eq!(classic.get_stack_after, TEST_SP + 10);
assert_eq!(classic.get_result_slot, classic.get_dialog_ptr);
assert_eq!(classic.get_window_list, vec![classic.get_dialog_ptr]);
assert_eq!(classic.get_front_window, classic.get_dialog_ptr);
assert_eq!(classic.get_current_port, classic.get_dialog_ptr);
assert_eq!(classic.get_the_port, classic.get_dialog_ptr);
assert_eq!(classic.get_visible_byte, 0xFF);
assert_eq!(classic.get_refcon, 0);
assert_eq!(classic.get_window_kind, 2);
assert_eq!(classic.get_proc_id, 2);
assert_eq!(classic.get_port_rect, (0, 0, 80, 150));
assert_ne!(classic.get_items_handle, 0);
assert_ne!(classic.get_items_data_ptr, 0);
assert!(classic.get_uses_distinct_ditl_copy);
assert_eq!(classic.get_original_ditl_handle_field, 0);
assert!(classic.get_first_item_handle_nonzero);
assert_eq!(
classic.get_cached_items,
vec![
(16, (12, 18, 30, 128), "Beta".to_string(), 0),
(4, (46, 78, 68, 138), "OK".to_string(), 0),
]
);
assert!(classic.get_update_region.is_some());
assert!(classic.get_update_event_queued);
assert_eq!(classic.get_edit_field, 0xFFFF);
assert_eq!(classic.get_default_item, 1);
assert_eq!(
themed, classic,
"systemless-default must not change NewDialog/GetNewDialog creation semantics"
);
}
#[test]
fn systemless_theme_does_not_change_alert_family_resource_state() {
// IM:I I-417..I-423 and MTE 1992 pp. 6-105..6-112: Alert,
// StopAlert, NoteAlert, and CautionAlert read ALRT/DITL resources,
// use the current alert stage to choose the default item, update
// ACount/ANumber low-memory state, and share behavior apart from
// icon drawing. IM:I I-420 also defines CouldAlert/FreeAlert as
// resource-preparation routines. Theme chrome must not alter those
// resource, low-memory, result-slot, or stack contracts.
let classic = alert_family_results_for_theme(UiThemeId::ClassicSystem7);
let themed = alert_family_results_for_theme(UiThemeId::SystemlessDefault);
assert_eq!(
classic.staged_calls,
vec![
AlertTrapCallSnapshot {
trap_word: 0x185,
result: 1,
stack_after: TEST_SP + 6,
alert_stage_after: 1,
anumber_after: 2099,
},
AlertTrapCallSnapshot {
trap_word: 0x186,
result: 2,
stack_after: TEST_SP + 6,
alert_stage_after: 2,
anumber_after: 2099,
},
AlertTrapCallSnapshot {
trap_word: 0x187,
result: 1,
stack_after: TEST_SP + 6,
alert_stage_after: 3,
anumber_after: 2099,
},
AlertTrapCallSnapshot {
trap_word: 0x188,
result: 2,
stack_after: TEST_SP + 6,
alert_stage_after: 3,
anumber_after: 2099,
},
AlertTrapCallSnapshot {
trap_word: 0x185,
result: 2,
stack_after: TEST_SP + 6,
alert_stage_after: 3,
anumber_after: 2099,
},
]
);
assert_eq!(
classic.missing_call,
AlertTrapCallSnapshot {
trap_word: 0x187,
result: -1,
stack_after: TEST_SP + 6,
alert_stage_after: 2,
anumber_after: 0xBEEF,
}
);
assert_eq!(
classic.resource_prep,
AlertResourcePrepSnapshot {
could_present: (0, TEST_SP + 2),
free_present: (0, TEST_SP + 2),
could_missing: (0, TEST_SP + 2),
free_missing: (0, TEST_SP + 2),
}
);
assert_eq!(
themed, classic,
"systemless-default must not change Alert family resource or low-memory semantics"
);
}
#[test]
fn systemless_theme_does_not_change_initdialogs_errorsound_state() {
// IM:I I-411 and MTE 1992 pp. 6-103..6-104: InitDialogs stores
// ResumeProc, initializes alert sound state, and prepares the
// ParamText/DAStrings globals; ErrorSound stores the current alert
// sound ProcPtr in DABeeper, with NIL disabling sound/menu-bar blink.
// Those low-memory globals and procedure stack pops are app-visible
// Dialog Manager state and must not depend on the selected renderer.
let classic = dialog_init_sound_results_for_theme(UiThemeId::ClassicSystem7);
let themed = dialog_init_sound_results_for_theme(UiThemeId::SystemlessDefault);
assert_eq!(classic.init_stack_after, TEST_SP + 4);
assert_eq!(classic.resume_proc_after_init, 0x0012_3456);
assert_eq!(classic.da_beeper_after_init, 0);
assert_eq!(classic.alert_stage_after_init, 0);
assert_eq!(classic.anumber_after_init, 0xCAFE);
assert_eq!(classic.da_strings_after_init, [0, 0, 0, 0]);
assert_eq!(classic.error_sound_stack_after, TEST_SP + 4);
assert_eq!(classic.da_beeper_after_error_sound, 0x00AB_CDEF);
assert_eq!(classic.nil_error_sound_stack_after, TEST_SP + 4);
assert_eq!(classic.da_beeper_after_nil_error_sound, 0);
assert_eq!(
themed, classic,
"systemless-default must not change InitDialogs/ErrorSound low-memory state"
);
}
#[test]
fn systemless_theme_does_not_change_dialogdispatch_default_cancel_selectors() {
// MTE 1992 pp. 6-162..6-166: DialogDispatch selectors extend the
// Dialog Manager with default-item, cancel-item, and cursor-tracking
// state. MTE 1992 p. 6-138 ties Return/Enter and Esc/Command-period
// handling to the default and Cancel items, so theme chrome must not
// alter selector OSErr results, stack ABI, DialogRecord.aDefItem, the
// cancel-item side state, or active ModalDialog tracking mirrors.
let classic = dialogdispatch_default_cancel_results_for_theme(UiThemeId::ClassicSystem7);
let themed = dialogdispatch_default_cancel_results_for_theme(UiThemeId::SystemlessDefault);
assert_eq!(classic.default_result, 0);
assert_eq!(classic.default_stack_after, TEST_SP + 6);
assert_eq!(classic.default_adef_item, 9);
assert_eq!(classic.default_tracking_item, 9);
assert_eq!(classic.cancel_result, 0);
assert_eq!(classic.cancel_stack_after, TEST_SP + 6);
assert_eq!(classic.cancel_map_item, Some(7));
assert_eq!(classic.cancel_tracking_item, 7);
assert_eq!(classic.tracks_result, 0);
assert_eq!(classic.tracks_stack_after, TEST_SP + 6);
assert_eq!(classic.tracks_adef_item, 9);
assert_eq!(classic.tracks_cancel_map_item, Some(7));
assert_eq!(classic.tracks_tracking_items, (9, 7));
assert_eq!(
themed, classic,
"systemless-default must not change DialogDispatch default/cancel selector semantics"
);
}
#[test]
fn systemless_theme_does_not_change_updtdialog_update_region_semantics() {
// MTE 1992 pp. 6-142..6-143: UpdateDialog redraws only items in the
// supplied update region, calls application-defined item draw procs,
// and uses SetPort to make the dialog box current before updating.
// Theme chrome can change pixels but must not change that update-item
// selection, current-port side effect, deferred redraw state, or stack
// ABI.
let classic = updtdialog_update_region_results_for_theme(UiThemeId::ClassicSystem7);
let themed = updtdialog_update_region_results_for_theme(UiThemeId::SystemlessDefault);
assert_eq!(classic.stack_after, TEST_SP + 8);
assert_ne!(classic.the_port_after, 0x181000);
assert_eq!(classic.the_port_after, classic.current_port_after);
assert!(!classic.deferred_after);
assert_eq!(classic.queued_draw_procs.len(), 1);
assert_eq!(classic.queued_draw_procs[0].0, classic.the_port_after);
assert_eq!(classic.queued_draw_procs[0].1, 0x500000);
assert_eq!(classic.queued_draw_procs[0].2, 1);
assert_eq!(classic.item_count_after, 4);
assert_eq!(classic.inside_update_pixel_after, 0);
assert_eq!(classic.outside_update_pixel_after, 0x5A);
assert_eq!(classic.outside_item_pixel_after, 0x3C);
assert_eq!(
themed, classic,
"systemless-default must not change UpdtDialog update-region semantics"
);
}
#[test]
fn systemless_theme_does_not_change_dialogselect_edit_text_mouse_and_null_events() {
// MTE 1992 p. 6-139 and IM:I I-417: mouseDown in an enabled
// editText item is handled through TextEdit and reports the item,
// while null events with an editText item call TEIdle and return
// FALSE. Systemless TEClick/TEIdle preserve the TERec text/selection,
// so theme rendering must not alter the active editField, result slots,
// output-slot behavior, TERecord mirroring, or stack protocol.
let classic = dialog_select_edit_text_mouse_results_for_theme(UiThemeId::ClassicSystem7);
let themed = dialog_select_edit_text_mouse_results_for_theme(UiThemeId::SystemlessDefault);
assert_eq!(classic.mouse_result, 0xFFFF);
assert_ne!(classic.mouse_dialog_out, 0);
assert_eq!(classic.mouse_item_hit, 2);
assert_eq!(classic.mouse_stack_after, TEST_SP + 12);
assert_eq!(classic.mouse_edit_field, 1);
assert_eq!(classic.mouse_item_selection, (2, 4));
assert_eq!(classic.mouse_te_text, b"Second".to_vec());
assert_eq!(classic.mouse_te_length, 6);
assert_eq!(classic.mouse_te_selection, (2, 4));
assert_eq!(classic.null_result, 0);
assert_eq!(classic.null_dialog_out, 0xDEAD_BEEF);
assert_eq!(classic.null_item_hit, 0xCAFE);
assert_eq!(classic.null_stack_after, TEST_SP + 12);
assert_eq!(classic.null_edit_field, 1);
assert_eq!(classic.null_te_text, b"Second".to_vec());
assert_eq!(classic.null_te_length, 6);
assert_eq!(classic.null_te_selection, (2, 4));
assert_eq!(
themed, classic,
"systemless-default must not change DialogSelect editText mouse/null handling"
);
}
#[test]
fn systemless_theme_does_not_change_dialogselect_edit_text_key_handling() {
// MTE 1992 p. 6-139: for keyDown/autoKey events in editable text
// items, DialogSelect uses TextEdit to handle text entry/editing and
// reports the edit item through itemHit. Text mutation, selection
// collapse, shared TERecord state, result slots, and stack ABI are
// guest-visible Dialog Manager behavior, not theme rendering choices.
let classic = dialog_select_edit_text_key_results_for_theme(UiThemeId::ClassicSystem7);
let themed = dialog_select_edit_text_key_results_for_theme(UiThemeId::SystemlessDefault);
assert_eq!(classic.keydown_result, 0xFFFF);
assert_ne!(classic.keydown_dialog_out, 0);
assert_eq!(classic.keydown_item_hit, 1);
assert_eq!(classic.keydown_stack_after, TEST_SP + 12);
assert_eq!(classic.keydown_handle_bytes, b"HYo".to_vec());
assert_eq!(classic.keydown_cached_text, "HYo");
assert_eq!(classic.keydown_item_selection, (2, 2));
assert_eq!(classic.keydown_te_text, b"HYo".to_vec());
assert_eq!(classic.keydown_te_length, 3);
assert_eq!(classic.keydown_te_selection, (2, 2));
assert_eq!(classic.autokey_result, 0xFFFF);
assert_eq!(classic.autokey_dialog_out, classic.keydown_dialog_out);
assert_eq!(classic.autokey_item_hit, 1);
assert_eq!(classic.autokey_stack_after, TEST_SP + 12);
assert_eq!(classic.autokey_handle_bytes, b"Ho".to_vec());
assert_eq!(classic.autokey_cached_text, "Ho");
assert_eq!(classic.autokey_item_selection, (1, 1));
assert_eq!(classic.autokey_te_text, b"Ho".to_vec());
assert_eq!(classic.autokey_te_length, 2);
assert_eq!(classic.autokey_te_selection, (1, 1));
assert_eq!(
themed, classic,
"systemless-default must not change DialogSelect editText key handling"
);
}
#[test]
fn systemless_theme_does_not_change_isdialogevent_active_dialog_routing() {
// MTE 1992 p. 6-138: IsDialogEvent returns TRUE for events that
// belong to an active dialog, including null events; IM:I I-416 also
// calls out active-dialog key events, mouse-downs in the content
// region, and update/activate events targeting a dialog window.
// Themes must not alter that event-routing or function-stack ABI.
let classic = is_dialog_event_results_for_theme(UiThemeId::ClassicSystem7);
let themed = is_dialog_event_results_for_theme(UiThemeId::SystemlessDefault);
assert_eq!(classic.null_event, (0xFFFF, TEST_SP + 4));
assert_eq!(classic.key_down, (0xFFFF, TEST_SP + 4));
assert_eq!(classic.mouse_inside, (0xFFFF, TEST_SP + 4));
assert_eq!(classic.mouse_outside, (0, TEST_SP + 4));
assert_eq!(classic.update_target, (0xFFFF, TEST_SP + 4));
assert_eq!(classic.activate_target, (0xFFFF, TEST_SP + 4));
assert_eq!(
themed, classic,
"systemless-default must not change IsDialogEvent routing"
);
}
#[test]
fn systemless_theme_does_not_change_findditem_hit_testing() {
// MTE 1992 p. 6-125: FindDialogItem/FindDItem returns the first
// item-list index containing the local point, including disabled
// items; themes must not alter this guest-visible hit test.
let classic = findditem_results_for_theme(UiThemeId::ClassicSystem7);
let themed = findditem_results_for_theme(UiThemeId::SystemlessDefault);
assert_eq!(
classic,
vec![
(0, TEST_SP + 8),
(1, TEST_SP + 8),
(-1, TEST_SP + 8),
(-1, TEST_SP + 8),
]
);
assert_eq!(
themed, classic,
"systemless-default must not change FindDItem hit-test semantics"
);
}
#[test]
fn systemless_theme_does_not_change_getsetditem_useritem_records() {
// MTE 1992 p. 6-121..6-123: GetDialogItem and SetDialogItem expose
// item type, item handle/ProcPtr, and display rectangle. For
// application-defined userItems, `item` is the app's draw ProcPtr;
// theme chrome must not alter that record ABI or stack contract.
let classic = getsetditem_useritem_record_results_for_theme(UiThemeId::ClassicSystem7);
let themed = getsetditem_useritem_record_results_for_theme(UiThemeId::SystemlessDefault);
assert_eq!(classic.initial_get.item_type, 0);
assert_eq!(classic.initial_get.item, 0x0012_3456);
assert_eq!(classic.initial_get.rect, (12, 24, 36, 48));
assert_eq!(classic.initial_get.stack_after, TEST_SP + 18);
assert_eq!(classic.set_stack_after, TEST_SP + 16);
assert_eq!(classic.stored_type, 0);
assert_eq!(classic.stored_proc_ptr, 0x00AB_CDEF);
assert_eq!(classic.stored_rect, (50, 60, 70, 80));
assert_eq!(classic.post_set_get.item_type, 0);
assert_eq!(classic.post_set_get.item, 0x00AB_CDEF);
assert_eq!(classic.post_set_get.rect, (50, 60, 70, 80));
assert_eq!(classic.post_set_get.stack_after, TEST_SP + 18);
assert_eq!(
themed, classic,
"systemless-default must not change GetDItem/SetDItem userItem record semantics"
);
}
#[test]
fn systemless_theme_does_not_change_getsetditem_text_control_records() {
// MTE 1992 p. 6-121..6-123 and IM:I I-421: GetDialogItem and
// SetDialogItem expose item type, item handle, and display rectangle.
// Text items use handles consumed by Get/SetDialogItemText; control
// items use ControlRecord handles. Theme chrome must not alter that
// guest ABI, DITL storage, side-map relinking, or stack contract.
let classic = getsetditem_text_control_record_results_for_theme(UiThemeId::ClassicSystem7);
let themed =
getsetditem_text_control_record_results_for_theme(UiThemeId::SystemlessDefault);
assert_eq!(classic.text_initial_get.item_type, 16);
assert_ne!(classic.text_initial_get.item, 0);
assert_eq!(classic.text_initial_get.rect, (10, 20, 24, 160));
assert_eq!(classic.text_initial_get.stack_after, TEST_SP + 18);
assert_eq!(classic.text_set_stack_after, TEST_SP + 16);
assert!(!classic.text_old_handle_mapped_after);
let mapped_dialog = classic.text_new_handle_map_value_after.unwrap().0;
let text_new_handle = classic.text_post_set_get.item;
assert_ne!(mapped_dialog, 0);
assert_eq!(classic.text_new_handle_map_value_after.unwrap().1, 0);
assert_ne!(text_new_handle, classic.text_initial_get.item);
assert_eq!(classic.text_ditl_handle_after, text_new_handle);
assert_eq!(classic.text_ditl_rect_after, (12, 22, 28, 168));
assert_eq!(classic.text_ditl_type_after, 16);
assert_eq!(classic.text_stored_type_after, 16);
assert_eq!(classic.text_stored_rect_after, (12, 22, 28, 168));
assert_eq!(classic.text_cached_text_after, "Updated");
assert_eq!(classic.text_handle_bytes_after, b"Updated".to_vec());
assert_eq!(classic.text_post_set_get.item_type, 16);
assert_eq!(classic.text_post_set_get.item, text_new_handle);
assert_eq!(classic.text_post_set_get.rect, (12, 22, 28, 168));
assert_eq!(classic.text_post_set_get.stack_after, TEST_SP + 18);
assert_eq!(classic.control_initial_get.item_type, 4);
assert_ne!(classic.control_initial_get.item, 0);
assert_eq!(classic.control_initial_get.rect, (30, 40, 50, 140));
assert_eq!(classic.control_initial_get.stack_after, TEST_SP + 18);
assert_eq!(classic.control_set_stack_after, TEST_SP + 16);
assert!(!classic.control_old_handle_mapped_after);
let control_new_handle = classic.control_post_set_get.item;
assert_eq!(
classic.control_new_handle_map_value_after.unwrap().0,
mapped_dialog
);
assert_eq!(classic.control_new_handle_map_value_after.unwrap().1, 2);
assert_ne!(control_new_handle, classic.control_initial_get.item);
assert_eq!(classic.control_ditl_handle_after, control_new_handle);
assert_eq!(classic.control_ditl_rect_after, (42, 52, 62, 172));
assert_eq!(classic.control_ditl_type_after, 5);
assert_eq!(classic.control_stored_type_after, 5);
assert_eq!(classic.control_stored_rect_after, (42, 52, 62, 172));
assert_eq!(classic.control_record_rect_after, (42, 52, 62, 172));
assert_eq!(classic.control_value_after, Some(2));
assert_eq!(classic.control_post_set_get.item_type, 5);
assert_eq!(classic.control_post_set_get.item, control_new_handle);
assert_eq!(classic.control_post_set_get.rect, (42, 52, 62, 172));
assert_eq!(classic.control_post_set_get.stack_after, TEST_SP + 18);
assert_eq!(
themed, classic,
"systemless-default must not change GetDItem/SetDItem text/control record semantics"
);
}
#[test]
fn systemless_theme_does_not_change_hide_show_ditem_visibility_records() {
// MTE 1992 p. 6-123..6-124: HideDialogItem moves an item's display
// rectangle offscreen by offsetting left/right, while ShowDialogItem
// restores the visible rectangle and queues redraw. Themes must not
// alter that item/control record state, hit testing, or stack ABI.
let classic = hide_show_ditem_visibility_results_for_theme(UiThemeId::ClassicSystem7);
let themed = hide_show_ditem_visibility_results_for_theme(UiThemeId::SystemlessDefault);
assert_eq!(classic.initial_get.item_type, 4);
assert_eq!(classic.initial_get.rect, (10, 20, 30, 120));
assert_eq!(classic.initial_get.stack_after, TEST_SP + 18);
assert_eq!(classic.hide_stack_after, TEST_SP + 6);
assert_eq!(classic.hidden_item_rect, (10, 16404, 30, 16504));
assert_eq!(classic.hidden_saved_rect, Some((10, 20, 30, 120)));
assert_eq!(classic.hidden_control_rect, (10, 16404, 30, 16504));
assert_eq!(classic.hidden_get.item_type, 4);
assert_eq!(classic.hidden_get.item, classic.initial_get.item);
assert_eq!(classic.hidden_get.rect, (10, 16404, 30, 16504));
assert_eq!(classic.hidden_get.stack_after, TEST_SP + 18);
assert_eq!(classic.hidden_find, (-1, TEST_SP + 8));
assert_eq!(classic.show_stack_after, TEST_SP + 6);
assert_eq!(classic.restored_item_rect, (10, 20, 30, 120));
assert_eq!(classic.restored_saved_rect, None);
assert_eq!(classic.restored_control_rect, (10, 20, 30, 120));
assert_eq!(classic.restored_get.item_type, 4);
assert_eq!(classic.restored_get.item, classic.initial_get.item);
assert_eq!(classic.restored_get.rect, (10, 20, 30, 120));
assert_eq!(classic.restored_get.stack_after, TEST_SP + 18);
assert_eq!(classic.restored_find, (0, TEST_SP + 8));
assert_eq!(
themed, classic,
"systemless-default must not change HideDItem/ShowDItem visibility semantics"
);
}
#[test]
fn systemless_theme_does_not_change_paramtext_storage_and_substitution() {
// MTE 1992 p. 6-129..6-130: ParamText stores up to four strings in
// DAStrings and substitutes ^0..^3 in subsequently created dialog or
// alert text. This low-memory/global text contract is app-visible and
// must not depend on the selected rendering provider.
let classic = paramtext_results_for_theme(UiThemeId::ClassicSystem7);
let themed = paramtext_results_for_theme(UiThemeId::SystemlessDefault);
assert_eq!(
classic.initial_slots,
[
b"Doc".to_vec(),
b"42".to_vec(),
Vec::<u8>::new(),
b"Tail".to_vec(),
]
);
assert!(classic.initial_da_handles.iter().all(|handle| *handle != 0));
assert_eq!(classic.initial_da_strings, classic.initial_slots);
assert_eq!(classic.initial_stack_after, TEST_SP + 16);
assert_eq!(classic.initial_substitution, "Cannot open Doc (42)Tail ^9");
assert_eq!(
classic.nil_slots,
[
b"NewDoc".to_vec(),
b"42".to_vec(),
Vec::<u8>::new(),
b"Tail".to_vec(),
]
);
assert!(classic.nil_da_handles.iter().all(|handle| *handle != 0));
assert_eq!(classic.nil_da_strings, classic.nil_slots);
assert_eq!(classic.nil_da_handles[1], classic.initial_da_handles[1]);
assert_eq!(classic.nil_da_handles[2], classic.initial_da_handles[2]);
assert_eq!(classic.nil_da_handles[3], classic.initial_da_handles[3]);
assert_eq!(classic.nil_stack_after, TEST_SP + 16);
assert_eq!(classic.nil_substitution, "Cannot open NewDoc (42)Tail ^9");
assert_eq!(
themed, classic,
"systemless-default must not change ParamText storage or substitution semantics"
);
}
#[test]
fn systemless_theme_does_not_change_dialog_item_text_storage() {
// MTE 1992 p. 6-130..6-131: GetDialogItemText/GetIText returns the
// text in a statText/editText item, and SetDialogItemText/SetIText
// replaces that text. Theme rendering must not alter the text handle,
// cached item text, Pascal-string output, or stack ABI.
let classic = dialog_item_text_results_for_theme(UiThemeId::ClassicSystem7);
let themed = dialog_item_text_results_for_theme(UiThemeId::SystemlessDefault);
assert_eq!(classic.initial_get_text, b"Old".to_vec());
assert_eq!(classic.initial_get_stack_after, TEST_SP + 8);
assert_eq!(classic.set_stack_after, TEST_SP + 8);
assert_eq!(classic.handle_size_after_set, Some(11));
assert_eq!(classic.handle_bytes_after_set, b"Edited Text".to_vec());
assert_eq!(classic.cached_text_after_set, "Edited Text");
assert_eq!(classic.post_set_get_text, b"Edited Text".to_vec());
assert_eq!(classic.post_set_get_stack_after, TEST_SP + 8);
assert_eq!(
themed, classic,
"systemless-default must not change Get/SetDialogItemText storage semantics"
);
}
#[test]
fn systemless_theme_does_not_change_select_dialog_item_text_selection() {
// MTE 1992 p. 6-131..6-132: SelectDialogItemText/SelIText sets the
// selection range in an editable text item and supports the 0..32767
// whole-text selection convention. The active editField, TERecord
// mirrored selection, non-edit no-op, and stack ABI are guest-visible
// Dialog Manager behavior, not theme rendering choices.
let classic = select_dialog_item_text_results_for_theme(UiThemeId::ClassicSystem7);
let themed = select_dialog_item_text_results_for_theme(UiThemeId::SystemlessDefault);
assert_eq!(classic.whole_selection, (0, 5));
assert_eq!(classic.whole_edit_field, 0);
assert_eq!(classic.whole_te_selection, (0, 5));
assert_eq!(classic.whole_stack_after, TEST_SP + 10);
assert_eq!(classic.normalized_selection, (2, 5));
assert_eq!(classic.normalized_edit_field, 0);
assert_eq!(classic.normalized_te_selection, (2, 5));
assert_eq!(classic.normalized_stack_after, TEST_SP + 10);
assert_eq!(classic.non_edit_selection, (1, 3));
assert_eq!(classic.non_edit_edit_field, 0);
assert_eq!(classic.non_edit_te_selection, (2, 5));
assert_eq!(classic.non_edit_stack_after, TEST_SP + 10);
assert_eq!(
themed, classic,
"systemless-default must not change SelectDialogItemText selection semantics"
);
}
#[test]
fn systemless_theme_does_not_change_drawdialog_useritem_pixels() {
// MTE 1992 p. 6-142: DrawDialog calls application-defined item
// draw procedures when their rectangles are in the update region.
// MTE 1992 p. 6-155 defines application-defined items as userItem
// records; theme chrome must not erase the app-owned rectangle.
let classic = drawdialog_useritem_pixel_results_for_theme(UiThemeId::ClassicSystem7);
let themed = drawdialog_useritem_pixel_results_for_theme(UiThemeId::SystemlessDefault);
assert_eq!(classic.0, 0xA5);
assert_eq!(classic.1, TEST_SP + 4);
assert_eq!(classic.2, 0xCAFE_BABE);
assert_eq!(
themed, classic,
"systemless-default must not change DrawDialog userItem pixel preservation"
);
}
#[test]
fn systemless_theme_does_not_change_drawdialog_icon_item_pixels() {
// MTE 1992 p. 6-155: an icon item's item-list record names an
// `ICON` resource. MTE 1992 p. 7-63 defines `ICON` as application
// resource content used in dialog boxes; theme chrome must not alter
// those pixels inside the icon item rectangle.
let classic = drawdialog_icon_item_pixel_results_for_theme(UiThemeId::ClassicSystem7);
let themed = drawdialog_icon_item_pixel_results_for_theme(UiThemeId::SystemlessDefault);
assert_eq!(classic.0, [0xFF, 0xFF, 0xFF]);
assert_eq!(classic.1, TEST_SP + 4);
assert_eq!(classic.2, 0xCAFE_BABE);
assert_eq!(
themed, classic,
"systemless-default must not change DrawDialog ICON item pixels"
);
}
#[test]
fn systemless_theme_does_not_change_drawdialog_cicn_item_pixels() {
// MTE 1992 p. 6-155: an icon item's item-list record names an
// `ICON` resource and optionally a same-ID `cicn` resource. MTE 1992
// p. 7-64 specifies that the Dialog Manager displays that color icon
// instead of the black-and-white icon on color displays.
let classic = drawdialog_cicn_item_pixel_results_for_theme(UiThemeId::ClassicSystem7);
let themed = drawdialog_cicn_item_pixel_results_for_theme(UiThemeId::SystemlessDefault);
assert_eq!(classic.0, [0x33, 0x55, 0x77]);
assert_eq!(classic.1, TEST_SP + 4);
assert_eq!(classic.2, 0xCAFE_BABE);
assert_eq!(
themed, classic,
"systemless-default must not change DrawDialog cicn item pixels"
);
}
#[test]
fn systemless_theme_does_not_change_drawdialog_picture_item_pixels() {
// MTE 1992 p. 6-155: a picture item's item-list record names a
// `PICT` resource. That resource is application-supplied picture
// content, so theme chrome must not alter the pixels DrawDialog
// renders inside the picture item rectangle.
let classic = drawdialog_picture_item_pixel_results_for_theme(UiThemeId::ClassicSystem7);
let themed = drawdialog_picture_item_pixel_results_for_theme(UiThemeId::SystemlessDefault);
assert_eq!(classic.0, [0xFF, 0xFF, 0xFF]);
assert_eq!(classic.1, TEST_SP + 4);
assert_eq!(classic.2, 0xCAFE_BABE);
assert_eq!(
themed, classic,
"systemless-default must not change DrawDialog PICT item pixels"
);
}
#[test]
fn dialog_select_disabled_item_hit_returns_false_and_leaves_outputs_unchanged() {
// Inside Macintosh Volume I, I-417: disabled items do nothing and
// DialogSelect returns FALSE.
let (mut disp, mut cpu, mut bus) = setup();
let dialog_ptr = bus.alloc(170);
let event_ptr = 0x300000u32;
let dialog_out_ptr = 0x300100u32;
let item_hit_ptr = 0x300104u32;
disp.front_window = dialog_ptr;
bus.write_word(dialog_ptr + 8, 0);
bus.write_word(dialog_ptr + 10, 0);
bus.write_word(dialog_ptr + 16, 0);
bus.write_word(dialog_ptr + 18, 0);
bus.write_word(dialog_ptr + 20, 100);
bus.write_word(dialog_ptr + 22, 160);
disp.dialog_items.insert(
dialog_ptr,
vec![DialogItem {
item_type: 0x80, // disabled userItem
rect: (20, 30, 60, 110),
text: String::new(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
}],
);
bus.write_word(event_ptr, 1); // mouseDown
bus.write_long(event_ptr + 2, 0);
bus.write_long(event_ptr + 6, 0);
bus.write_word(event_ptr + 10, 130);
bus.write_word(event_ptr + 12, 240);
bus.write_word(event_ptr + 14, 0x0080);
bus.write_long(dialog_out_ptr, 0x12345678);
bus.write_word(item_hit_ptr, 0x7F7F);
bus.write_long(TEST_SP, item_hit_ptr);
bus.write_long(TEST_SP + 4, dialog_out_ptr);
bus.write_long(TEST_SP + 8, event_ptr);
let result = disp.dispatch_dialog(true, 0x180, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 12);
assert_eq!(bus.read_word(TEST_SP + 12), 0);
assert_eq!(bus.read_long(dialog_out_ptr), 0x12345678);
assert_eq!(bus.read_word(item_hit_ptr), 0x7F7F);
}
#[test]
fn dialog_select_keydown_without_edit_text_item_returns_false() {
// Inside Macintosh Volume I, I-417: keyDown handling returns TRUE only
// when an enabled editText item is active.
let (mut disp, mut cpu, mut bus) = setup();
let dialog_ptr = bus.alloc(170);
let event_ptr = 0x300000u32;
let dialog_out_ptr = 0x300100u32;
let item_hit_ptr = 0x300104u32;
disp.front_window = dialog_ptr;
bus.write_word(dialog_ptr + 8, 0);
bus.write_word(dialog_ptr + 10, 0);
bus.write_word(dialog_ptr + 16, 0);
bus.write_word(dialog_ptr + 18, 0);
bus.write_word(dialog_ptr + 20, 100);
bus.write_word(dialog_ptr + 22, 160);
bus.write_word(dialog_ptr + 164, 0); // editField points at item 1
disp.dialog_items.insert(
dialog_ptr,
vec![DialogItem {
item_type: 0, // userItem, not editText
rect: (20, 30, 60, 110),
text: String::new(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
}],
);
bus.write_word(event_ptr, 3); // keyDown
bus.write_long(event_ptr + 2, 0x00000041); // 'A'
bus.write_long(event_ptr + 6, 0);
bus.write_word(event_ptr + 10, 0);
bus.write_word(event_ptr + 12, 0);
bus.write_word(event_ptr + 14, 0);
bus.write_long(TEST_SP, item_hit_ptr);
bus.write_long(TEST_SP + 4, dialog_out_ptr);
bus.write_long(TEST_SP + 8, event_ptr);
let result = disp.dispatch_dialog(true, 0x180, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(TEST_SP + 12), 0);
}
#[test]
fn dialog_select_keydown_with_enabled_edit_text_returns_hit() {
// Inside Macintosh Volume I, I-417: for keyDown/autoKey with an
// enabled editText item, DialogSelect returns TRUE and item number.
let (mut disp, mut cpu, mut bus) = setup();
let dialog_ptr = bus.alloc(170);
let event_ptr = 0x300000u32;
let dialog_out_ptr = 0x300100u32;
let item_hit_ptr = 0x300104u32;
disp.front_window = dialog_ptr;
bus.write_word(dialog_ptr + 8, 0);
bus.write_word(dialog_ptr + 10, 0);
bus.write_word(dialog_ptr + 16, 0);
bus.write_word(dialog_ptr + 18, 0);
bus.write_word(dialog_ptr + 20, 100);
bus.write_word(dialog_ptr + 22, 160);
bus.write_word(dialog_ptr + 164, 0); // editField = first item
disp.dialog_items.insert(
dialog_ptr,
vec![DialogItem {
item_type: 16, // editText
rect: (20, 30, 60, 110),
text: "Hello".to_string(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
}],
);
bus.write_word(event_ptr, 3); // keyDown
bus.write_long(event_ptr + 2, 0x00000041); // 'A'
bus.write_long(event_ptr + 6, 0);
bus.write_word(event_ptr + 10, 0);
bus.write_word(event_ptr + 12, 0);
bus.write_word(event_ptr + 14, 0);
bus.write_long(TEST_SP, item_hit_ptr);
bus.write_long(TEST_SP + 4, dialog_out_ptr);
bus.write_long(TEST_SP + 8, event_ptr);
let result = disp.dispatch_dialog(true, 0x180, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(TEST_SP + 12), 0xFFFF);
assert_eq!(bus.read_long(dialog_out_ptr), dialog_ptr);
assert_eq!(bus.read_word(item_hit_ptr), 1);
}
#[test]
fn dialog_select_null_event_blinks_active_edit_text_insertion_caret() {
// MTE 1992 p. 6-139 and IM:I I-417: DialogSelect mouse-down in an
// enabled editText item displays the insertion point, and later null
// events call TEIdle so the insertion point blinks.
let (mut disp, mut cpu, mut bus) = setup_with_port();
let dialog_ptr = bus.alloc(170);
let event_ptr = bus.alloc(16);
let dialog_out_ptr = bus.alloc(4);
let item_hit_ptr = bus.alloc(2);
let items_handle = bus.alloc(4);
let ditl_ptr = bus.alloc(16);
let text_item_handle = TrapDispatcher::allocate_handle_with_data(&mut bus, 0);
let text_h = make_te_with_text(&mut disp, &mut bus, b"");
let te_ptr = bus.read_long(text_h);
let (screen_base, row_bytes, _screen_w, _screen_h, _pixel_size) = disp.screen_mode;
for i in 0..(row_bytes * 80) {
bus.write_byte(screen_base + i, 0);
}
disp.front_window = dialog_ptr;
bus.write_word(dialog_ptr + 8, 0);
bus.write_word(dialog_ptr + 10, 0);
bus.write_word(dialog_ptr + 16, 0);
bus.write_word(dialog_ptr + 18, 0);
bus.write_word(dialog_ptr + 20, 100);
bus.write_word(dialog_ptr + 22, 160);
bus.write_long(dialog_ptr + 156, items_handle);
bus.write_long(dialog_ptr + 160, text_h);
bus.write_word(dialog_ptr + 164, 0);
bus.write_long(items_handle, ditl_ptr);
bus.write_word(ditl_ptr, 0);
bus.write_long(ditl_ptr + 2, text_item_handle);
bus.write_word(ditl_ptr + 6, 20);
bus.write_word(ditl_ptr + 8, 20);
bus.write_word(ditl_ptr + 10, 38);
bus.write_word(ditl_ptr + 12, 120);
bus.write_byte(ditl_ptr + 14, 16);
bus.write_byte(ditl_ptr + 15, 0);
disp.dialog_items.insert(
dialog_ptr,
vec![DialogItem {
item_type: 16,
rect: (20, 20, 38, 120),
text: String::new(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
}],
);
let dispatch_dialog_select_event = |disp: &mut TrapDispatcher,
cpu: &mut MockCpu,
bus: &mut MacMemoryBus,
what: u16,
tick: u32| {
disp.tick_count = tick;
bus.write_long(crate::memory::globals::addr::TICKS, tick);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(event_ptr, what);
bus.write_long(event_ptr + 2, 0);
bus.write_long(event_ptr + 6, 0);
bus.write_word(event_ptr + 10, 25);
bus.write_word(event_ptr + 12, 25);
bus.write_word(event_ptr + 14, 0);
bus.write_long(dialog_out_ptr, 0xDEAD_BEEF);
bus.write_word(item_hit_ptr, 0xCAFE);
bus.write_long(TEST_SP, item_hit_ptr);
bus.write_long(TEST_SP + 4, dialog_out_ptr);
bus.write_long(TEST_SP + 8, event_ptr);
disp.dispatch_dialog(true, 0x180, cpu, bus)
.unwrap()
.unwrap();
};
dispatch_dialog_select_event(&mut disp, &mut cpu, &mut bus, 1, 100);
assert_eq!(bus.read_word(TEST_SP + 12), 0xFFFF);
assert_eq!(bus.read_long(dialog_out_ptr), dialog_ptr);
assert_eq!(bus.read_word(item_hit_ptr), 1);
assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_ACTIVE_OFFSET), 1);
assert_eq!(
bus.read_long(te_ptr + TrapDispatcher::TE_CARET_TIME_OFFSET),
100
);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_CARET_STATE_OFFSET),
0
);
assert!(
screen_pixel_is_set(&bus, screen_base, row_bytes, 1, 0),
"DialogSelect editText mouse-down should display the insertion caret"
);
dispatch_dialog_select_event(&mut disp, &mut cpu, &mut bus, 0, 131);
assert_eq!(bus.read_word(TEST_SP + 12), 0);
assert_eq!(bus.read_long(dialog_out_ptr), 0xDEAD_BEEF);
assert_eq!(bus.read_word(item_hit_ptr), 0xCAFE);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_CARET_STATE_OFFSET),
0
);
assert!(
screen_pixel_is_set(&bus, screen_base, row_bytes, 1, 0),
"DialogSelect null event before 32 ticks should keep the caret visible"
);
dispatch_dialog_select_event(&mut disp, &mut cpu, &mut bus, 0, 132);
assert_eq!(
bus.read_long(te_ptr + TrapDispatcher::TE_CARET_TIME_OFFSET),
132
);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_CARET_STATE_OFFSET),
1
);
assert!(
!screen_pixel_is_set(&bus, screen_base, row_bytes, 1, 0),
"DialogSelect null event at the 32-tick boundary should hide the caret"
);
dispatch_dialog_select_event(&mut disp, &mut cpu, &mut bus, 0, 164);
assert_eq!(
bus.read_long(te_ptr + TrapDispatcher::TE_CARET_TIME_OFFSET),
164
);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_CARET_STATE_OFFSET),
0
);
assert!(
screen_pixel_is_set(&bus, screen_base, row_bytes, 1, 0),
"the next DialogSelect null-event blink interval should show the caret again"
);
}
// ---- DrawDialog ($A981) ----
#[test]
fn draw_dialog_pops_4_bytes() {
let (mut disp, mut cpu, mut bus) = setup();
// SP+0: dialog ptr (4 bytes)
bus.write_long(TEST_SP, 0x200000);
let result = disp.dispatch_dialog(true, 0x181, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
}
#[test]
fn draw_dialog_repaints_dialog_background_for_known_dialog() {
// Inside Macintosh Volume I, I-417: DrawDialog draws the contents of
// the given dialog box.
let (mut disp, mut cpu, mut bus) = setup();
let dialog_ptr = bus.alloc(170);
let (screen_base, row_bytes, _w, _h, pixel_size) = disp.screen_mode;
bus.write_word(dialog_ptr + 8, 0);
bus.write_word(dialog_ptr + 10, 0);
bus.write_word(dialog_ptr + 16, 0);
bus.write_word(dialog_ptr + 18, 0);
bus.write_word(dialog_ptr + 20, 30);
bus.write_word(dialog_ptr + 22, 40);
disp.dialog_items.insert(
dialog_ptr,
vec![DialogItem {
item_type: 4, // btnCtrl draws a deterministic border and label
rect: (8, 8, 20, 32),
text: "OK".to_string(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
}],
);
let probe_x = 4u32;
let probe_y = 4u32;
let probe_addr = if pixel_size == 8 {
screen_base + probe_y * row_bytes + probe_x
} else {
screen_base + probe_y * row_bytes + (probe_x / 8)
};
let probe_bit = 1 << (7 - (probe_x % 8));
if pixel_size == 8 {
bus.write_byte(probe_addr, 0xFF); // black in 8bpp CLUT
} else {
bus.write_byte(probe_addr, bus.read_byte(probe_addr) | probe_bit);
}
bus.write_long(TEST_SP, dialog_ptr);
let result = disp.dispatch_dialog(true, 0x181, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
if pixel_size == 8 {
assert_eq!(bus.read_byte(probe_addr), 0); // white in 8bpp CLUT
} else {
assert_eq!(bus.read_byte(probe_addr) & probe_bit, 0);
}
}
#[test]
fn draw_dialog_repaints_document_proc_title_chrome() {
// DrawDialog is also valid for modeless documentProc dialogs. It must
// redraw the WDEF title bar from the WindowRecord title instead of
// falling back to modal-box border chrome (IM:I I-299/I-417).
let screen_base = 0x300000u32;
let row_bytes = 64u32;
let (mut disp, mut cpu, mut bus) = setup();
disp.set_screen_mode_for_test(screen_base, row_bytes, 512, 342, 1);
let dialog_ptr = bus.alloc(170);
bus.write_word(dialog_ptr + 6, 0);
bus.write_word(dialog_ptr + 8, (-40i16) as u16);
bus.write_word(dialog_ptr + 10, (-80i16) as u16);
bus.write_word(dialog_ptr + 16, 0);
bus.write_word(dialog_ptr + 18, 0);
bus.write_word(dialog_ptr + 20, 80);
bus.write_word(dialog_ptr + 22, 160);
bus.write_byte(dialog_ptr + 112, 0);
let title_ptr = bus.alloc(16);
bus.write_pstring(title_ptr, b"Modeless");
let title_handle = bus.alloc(4);
bus.write_long(title_handle, title_ptr);
bus.write_long(dialog_ptr + 134, title_handle);
disp.window_proc_ids.insert(dialog_ptr, 0);
disp.dialog_items.insert(dialog_ptr, Vec::new());
bus.write_long(TEST_SP, dialog_ptr);
let result = disp.dispatch_dialog(true, 0x181, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert!(
screen_pixel_is_set(&bus, screen_base, row_bytes, 85, 24),
"documentProc DrawDialog should redraw active title-bar stripes"
);
assert!(
screen_pixel_is_set(&bus, screen_base, row_bytes, 79, 21),
"documentProc DrawDialog should redraw the title-bar border"
);
}
#[test]
fn draw_dialog_radio_items_use_control_value_state() {
// SetCtlValue stores value 1 for a selected radio control (IM:I
// I-317/I-328). DrawDialog must honor the dialog control value table
// for radio items, just as it does for checkboxes.
let screen_base = 0x300000u32;
let row_bytes = 64u32;
let bounds = (40, 40, 120, 220);
let dialog_ptr = 0x2000;
let items = vec![DialogItem {
item_type: 6,
rect: (20, 20, 40, 160),
text: "Selected".to_string(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
}];
let (mut disp, _cpu, mut bus) = setup();
disp.set_screen_mode_for_test(screen_base, row_bytes, 512, 342, 1);
disp.dialog_control_values.insert((dialog_ptr, 1), 1);
disp.draw_dialog(&mut bus, bounds, 1, "", &items, 0, "", 0, false, dialog_ptr);
assert!(
screen_pixel_is_set(&bus, screen_base, row_bytes, 67, 69),
"selected radio item should draw the central dot"
);
}
#[test]
fn draw_dialog_queues_user_item_draw_proc_for_known_dialog() {
// MTE 1992 p. 6-142: DrawDialog calls application-defined item
// draw procs whose userItem display rectangles are in the dialog.
let (mut disp, mut cpu, mut bus) = setup();
let dialog_ptr = bus.alloc(170);
bus.write_word(dialog_ptr + 16, 0);
bus.write_word(dialog_ptr + 18, 0);
bus.write_word(dialog_ptr + 20, 80);
bus.write_word(dialog_ptr + 22, 120);
disp.dialog_items.insert(
dialog_ptr,
vec![
DialogItem {
item_type: 0,
rect: (8, 8, 24, 48),
proc_ptr: 0x500000,
..Default::default()
},
DialogItem {
item_type: 0,
rect: (100, 8, 120, 48),
proc_ptr: 0x600000,
..Default::default()
},
DialogItem {
item_type: 4,
rect: (40, 8, 60, 48),
text: "OK".to_string(),
..Default::default()
},
],
);
bus.write_long(TEST_SP, dialog_ptr);
let result = disp.dispatch_dialog(true, 0x181, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
assert_eq!(
disp.modeless_dialog_draw_proc_queue
.iter()
.copied()
.collect::<Vec<_>>(),
vec![(dialog_ptr, 0x500000, 1)]
);
}
#[test]
fn draw_button_systemless_theme_routes_dialog_chrome_through_provider() {
let screen_base = 0x300000u32;
let row_bytes = 64u32;
let (mut classic, _classic_cpu, mut classic_bus) = setup();
classic.set_screen_mode_for_test(screen_base, row_bytes, 512, 342, 1);
classic.draw_button(&mut classic_bus, 20, 20, 40, 80, "", true);
let (mut themed, _themed_cpu, mut themed_bus) = setup();
themed.set_ui_theme_id(UiThemeId::SystemlessDefault);
themed.set_screen_mode_for_test(screen_base, row_bytes, 512, 342, 1);
themed.draw_button(&mut themed_bus, 20, 20, 40, 80, "", true);
assert!(
!screen_pixel_is_set(&classic_bus, screen_base, row_bytes, 16, 16),
"classic default-button round rect should leave its outer corner as background"
);
assert!(
screen_pixel_is_set(&themed_bus, screen_base, row_bytes, 16, 16),
"systemless-default provider chrome should own the default-button outline corner"
);
}
#[test]
fn draw_checkbox_and_radio_systemless_theme_route_dialog_chrome_through_provider() {
let screen_base = 0x300000u32;
let row_bytes = 64u32;
let (mut classic, _classic_cpu, mut classic_bus) = setup();
classic.set_screen_mode_for_test(screen_base, row_bytes, 512, 342, 1);
classic.draw_checkbox(&mut classic_bus, 20, 20, 40, 120, "", true);
classic.draw_radio(&mut classic_bus, 50, 20, 70, 120, "", true);
let (mut themed, _themed_cpu, mut themed_bus) = setup();
themed.set_ui_theme_id(UiThemeId::SystemlessDefault);
themed.set_screen_mode_for_test(screen_base, row_bytes, 512, 342, 1);
themed.draw_checkbox(&mut themed_bus, 20, 20, 40, 120, "", true);
themed.draw_radio(&mut themed_bus, 50, 20, 70, 120, "", true);
let classic_checkbox_pixels =
count_set_pixels(&classic_bus, screen_base, row_bytes, 21, 20, 33, 32);
let themed_checkbox_pixels =
count_set_pixels(&themed_bus, screen_base, row_bytes, 21, 20, 33, 32);
assert!(
themed_checkbox_pixels > classic_checkbox_pixels,
"systemless-default dialog checkbox provider should draw a denser selected mark ({themed_checkbox_pixels} <= {classic_checkbox_pixels})"
);
let classic_radio_pixels =
count_set_pixels(&classic_bus, screen_base, row_bytes, 51, 20, 63, 32);
let themed_radio_pixels =
count_set_pixels(&themed_bus, screen_base, row_bytes, 51, 20, 63, 32);
assert!(
themed_radio_pixels > classic_radio_pixels,
"systemless-default dialog radio provider should draw a denser selected mark ({themed_radio_pixels} <= {classic_radio_pixels})"
);
}
#[test]
fn draw_popup_control_systemless_theme_routes_dialog_chrome_through_provider() {
let screen_base = 0x300000u32;
let row_bytes = 64u32;
let (mut classic, _classic_cpu, mut classic_bus) = setup();
classic.set_screen_mode_for_test(screen_base, row_bytes, 512, 342, 1);
classic.draw_popup_control(&mut classic_bus, 20, 20, 40, 120, "");
let (mut themed, _themed_cpu, mut themed_bus) = setup();
themed.set_ui_theme_id(UiThemeId::SystemlessDefault);
themed.set_screen_mode_for_test(screen_base, row_bytes, 512, 342, 1);
themed.draw_popup_control(&mut themed_bus, 20, 20, 40, 120, "");
assert!(
screen_pixel_is_set(&classic_bus, screen_base, row_bytes, 120, 39),
"classic popup CDEF should draw its one-pixel offset shadow"
);
assert!(
!screen_pixel_is_set(&themed_bus, screen_base, row_bytes, 120, 39),
"systemless-default popup provider should not draw the classic offset shadow"
);
}
#[test]
fn draw_edit_text_systemless_theme_routes_caret_through_provider() {
let screen_base = 0x300000u32;
let row_bytes = 64u32;
let (mut classic, _classic_cpu, mut classic_bus) = setup();
classic.set_screen_mode_for_test(screen_base, row_bytes, 512, 342, 1);
classic.draw_edit_text(&mut classic_bus, 20, 20, 40, 100, "", false);
let (mut themed, _themed_cpu, mut themed_bus) = setup();
themed.set_ui_theme_id(UiThemeId::SystemlessDefault);
themed.set_screen_mode_for_test(screen_base, row_bytes, 512, 342, 1);
themed.draw_edit_text(&mut themed_bus, 20, 20, 40, 100, "", false);
assert!(
!screen_pixel_is_set(&classic_bus, screen_base, row_bytes, 22, 22),
"classic editText caret should remain a single vertical bar"
);
assert!(
screen_pixel_is_set(&themed_bus, screen_base, row_bytes, 22, 22),
"systemless-default provider should draw the caret cap"
);
}
#[test]
fn draw_edit_text_systemless_theme_routes_field_frame_through_provider() {
let screen_base = 0x300000u32;
let row_bytes = 64u32;
let (mut classic, _classic_cpu, mut classic_bus) = setup();
classic.set_screen_mode_for_test(screen_base, row_bytes, 512, 342, 1);
classic.draw_edit_text(&mut classic_bus, 20, 20, 40, 100, "", false);
let (mut themed, _themed_cpu, mut themed_bus) = setup();
themed.set_ui_theme_id(UiThemeId::SystemlessDefault);
themed.set_screen_mode_for_test(screen_base, row_bytes, 512, 342, 1);
themed.draw_edit_text(&mut themed_bus, 20, 20, 40, 100, "", false);
assert!(
!screen_pixel_is_set(&classic_bus, screen_base, row_bytes, 19, 22),
"classic editText field should keep the upper interior clear"
);
assert!(
screen_pixel_is_set(&themed_bus, screen_base, row_bytes, 19, 22),
"systemless-default provider should draw focused editText field chrome"
);
}
#[test]
fn draw_dialog_systemless_theme_routes_disabled_edit_text_state_through_provider() {
let screen_base = 0x300000u32;
let row_bytes = 64u32;
let bounds = (40, 40, 110, 190);
let items = vec![DialogItem {
item_type: 0x80 | 16,
rect: (20, 20, 44, 130),
text: "Disabled".to_string(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
}];
let (mut classic, _classic_cpu, mut classic_bus) = setup();
classic.set_screen_mode_for_test(screen_base, row_bytes, 512, 342, 1);
classic.draw_dialog(
&mut classic_bus,
bounds,
1,
"",
&items,
0,
"",
0,
false,
0x2000,
);
let (mut themed, _themed_cpu, mut themed_bus) = setup();
themed.set_ui_theme_id(UiThemeId::SystemlessDefault);
themed.set_screen_mode_for_test(screen_base, row_bytes, 512, 342, 1);
themed.draw_dialog(
&mut themed_bus,
bounds,
1,
"",
&items,
0,
"",
0,
false,
0x2000,
);
let disabled_inner_x =
bounds.1 + items[0].rect.1 - TrapDispatcher::EDIT_TEXT_FRAME_OUTSET + 2;
let disabled_inner_y = bounds.0 + items[0].rect.0 + 2;
assert!(
!screen_pixel_is_set(
&classic_bus,
screen_base,
row_bytes,
disabled_inner_x,
disabled_inner_y
),
"classic editText field should keep the disabled-state inner frame pixel clear"
);
assert!(
screen_pixel_is_set(
&themed_bus,
screen_base,
row_bytes,
disabled_inner_x,
disabled_inner_y
),
"systemless-default provider should receive disabled editText field state"
);
}
#[test]
fn draw_dialog_systemless_theme_routes_disabled_standard_controls_through_provider() {
let screen_base = 0x300000u32;
let row_bytes = 64u32;
let bounds = (40, 40, 140, 210);
let items = vec![
DialogItem {
item_type: 0x80 | 4,
rect: (20, 20, 40, 82),
text: String::new(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
},
DialogItem {
item_type: 0x80 | 5,
rect: (52, 20, 72, 120),
text: String::new(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
},
DialogItem {
item_type: 0x80 | 6,
rect: (84, 20, 104, 120),
text: String::new(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
},
];
let (mut classic, _classic_cpu, mut classic_bus) = setup();
classic.set_screen_mode_for_test(screen_base, row_bytes, 512, 342, 1);
classic.draw_dialog(
&mut classic_bus,
bounds,
1,
"",
&items,
0,
"",
0,
false,
0x2000,
);
let (mut themed, _themed_cpu, mut themed_bus) = setup();
themed.set_ui_theme_id(UiThemeId::SystemlessDefault);
themed.set_screen_mode_for_test(screen_base, row_bytes, 512, 342, 1);
themed.draw_dialog(
&mut themed_bus,
bounds,
1,
"",
&items,
0,
"",
0,
false,
0x2000,
);
// The unselected disabled checkbox is intentionally pixel-equivalent
// after classic CDEF mark alignment; selected checkbox routing is
// covered by draw_checkbox_and_radio_systemless_theme_route_dialog_chrome_through_provider.
let probes = [
(
bounds.1 + items[0].rect.1 + 2,
bounds.0 + items[0].rect.0 + 2,
),
(
bounds.1 + items[2].rect.1 + 2,
bounds.0
+ items[2].rect.0
+ (items[2].rect.2
- items[2].rect.0
- TrapDispatcher::STANDARD_CONTROL_MARK_SIZE)
/ 2,
),
];
for (x, y) in probes {
assert!(
!screen_pixel_is_set(&classic_bus, screen_base, row_bytes, x, y),
"classic disabled DITL controls should keep standard appearance at ({x},{y})"
);
assert!(
screen_pixel_is_set(&themed_bus, screen_base, row_bytes, x, y),
"systemless-default provider should receive disabled DITL control state at ({x},{y})"
);
}
}
#[test]
fn draw_dialog_classic_keeps_disabled_checkbox_and_radio_titles_undimmed() {
let screen_base = 0x300000u32;
let row_bytes = 64u32;
let bounds = (40, 40, 190, 230);
let items = vec![
DialogItem {
item_type: 5,
rect: (20, 20, 40, 150),
text: "Sound".to_string(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
},
DialogItem {
item_type: 0x80 | 5,
rect: (52, 20, 72, 150),
text: "Sound".to_string(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
},
DialogItem {
item_type: 6,
rect: (84, 20, 104, 150),
text: "Music".to_string(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
},
DialogItem {
item_type: 0x80 | 6,
rect: (116, 20, 136, 150),
text: "Music".to_string(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
},
];
let (mut dispatcher, _cpu, mut bus) = setup();
dispatcher.set_screen_mode_for_test(screen_base, row_bytes, 512, 342, 1);
dispatcher.draw_dialog(&mut bus, bounds, 1, "", &items, 0, "", 0, false, 0x2000);
let label_left = bounds.1
+ items[0].rect.1
+ TrapDispatcher::STANDARD_CONTROL_MARK_LEFT_INSET
+ TrapDispatcher::STANDARD_CONTROL_MARK_SIZE
+ TrapDispatcher::STANDARD_CONTROL_TITLE_GAP;
let enabled_checkbox = count_set_pixels(
&bus,
screen_base,
row_bytes,
bounds.0 + items[0].rect.0,
label_left,
bounds.0 + items[0].rect.2,
bounds.1 + items[0].rect.3,
);
let disabled_checkbox = count_set_pixels(
&bus,
screen_base,
row_bytes,
bounds.0 + items[1].rect.0,
label_left,
bounds.0 + items[1].rect.2,
bounds.1 + items[1].rect.3,
);
let enabled_radio = count_set_pixels(
&bus,
screen_base,
row_bytes,
bounds.0 + items[2].rect.0,
label_left,
bounds.0 + items[2].rect.2,
bounds.1 + items[2].rect.3,
);
let disabled_radio = count_set_pixels(
&bus,
screen_base,
row_bytes,
bounds.0 + items[3].rect.0,
label_left,
bounds.0 + items[3].rect.2,
bounds.1 + items[3].rect.3,
);
assert!(
enabled_checkbox > 0,
"enabled checkbox title should be visible"
);
assert_eq!(
enabled_checkbox, disabled_checkbox,
"classic itemDisable checkbox title must not be visually dimmed"
);
assert!(enabled_radio > 0, "enabled radio title should be visible");
assert_eq!(
enabled_radio, disabled_radio,
"classic itemDisable radio title must not be visually dimmed"
);
}
#[test]
fn draw_dialog_standard_control_hilite_255_titles_use_gray_device_index() {
let screen_base = 0x300000u32;
let row_bytes = 160u32;
let bounds = (20, 20, 120, 220);
let dialog_ptr = 0x2400u32;
let items = vec![
DialogItem {
item_type: 5,
rect: (20, 20, 40, 150),
text: "Sound".to_string(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
},
DialogItem {
item_type: 6,
rect: (52, 20, 72, 150),
text: "Music".to_string(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
},
];
let (mut dispatcher, _cpu, mut bus) = setup();
dispatcher.set_screen_mode_for_test(screen_base, row_bytes, 160, 140, 8);
dispatcher.device_clut = [[0x2020, 0x4040, 0x6060]; 256];
dispatcher.device_clut[0] = [0xFFFF, 0xFFFF, 0xFFFF];
dispatcher.device_clut[42] = [0xA1A1, 0xA1A1, 0xA1A1];
dispatcher.device_clut[255] = [0, 0, 0];
for offset in 0..(row_bytes * 140) {
bus.write_byte(screen_base + offset, 0);
}
for (item_no, proc_id, item) in [(1i16, 1i16, &items[0]), (2, 2, &items[1])] {
let ctrl_ptr = bus.alloc(296);
let ctrl_handle = bus.alloc(4);
bus.write_long(ctrl_handle, ctrl_ptr);
dispatcher.initialize_control_record(
&mut bus,
ctrl_ptr,
dialog_ptr,
item.rect,
item.text.as_bytes(),
true,
0,
0,
1,
proc_id,
0,
);
bus.write_byte(ctrl_ptr + 17, 255);
dispatcher
.dialog_control_handles
.insert(ctrl_handle, (dialog_ptr, item_no));
dispatcher
.dialog_control_values
.insert((dialog_ptr, item_no), 0);
}
dispatcher.draw_dialog(&mut bus, bounds, 1, "", &items, 0, "", 0, false, dialog_ptr);
for (label, item) in [("checkbox", &items[0]), ("radio", &items[1])] {
let label_left = bounds.1
+ item.rect.1
+ TrapDispatcher::STANDARD_CONTROL_MARK_LEFT_INSET
+ TrapDispatcher::STANDARD_CONTROL_MARK_SIZE
+ TrapDispatcher::STANDARD_CONTROL_TITLE_GAP;
let top = bounds.0 + item.rect.0;
let bottom = bounds.0 + item.rect.2;
let right = bounds.1 + item.rect.3;
let gray = count_pixel_index(
&bus,
screen_base,
row_bytes,
top,
label_left,
bottom,
right,
42,
);
let black = count_pixel_index(
&bus,
screen_base,
row_bytes,
top,
label_left,
bottom,
right,
255,
);
assert!(
gray > 12,
"inactive standard DITL {label} title should draw with the device gray index"
);
assert_eq!(
black, 0,
"inactive standard DITL {label} title must not leave black glyph pixels"
);
}
}
#[test]
fn draw_dialog_systemless_theme_routes_frame_through_provider() {
let screen_base = 0x300000u32;
let row_bytes = 64u32;
let bounds = (40, 40, 100, 140);
let (mut classic, _classic_cpu, mut classic_bus) = setup();
classic.set_screen_mode_for_test(screen_base, row_bytes, 512, 342, 1);
classic.draw_dialog(
&mut classic_bus,
bounds,
1,
"",
&[],
0,
"",
0,
false,
0x2000,
);
let (mut themed, _themed_cpu, mut themed_bus) = setup();
themed.set_ui_theme_id(UiThemeId::SystemlessDefault);
themed.set_screen_mode_for_test(screen_base, row_bytes, 512, 342, 1);
themed.draw_dialog(&mut themed_bus, bounds, 1, "", &[], 0, "", 0, false, 0x2000);
assert!(
!screen_pixel_is_set(&classic_bus, screen_base, row_bytes, 50, 38),
"classic dBoxProc leaves the gap between outer and inner borders clear"
);
assert!(
screen_pixel_is_set(&themed_bus, screen_base, row_bytes, 50, 38),
"systemless-default provider should own dialog frame accent pixels"
);
}
#[test]
fn draw_window_frame_systemless_theme_routes_border_through_provider() {
let screen_base = 0x300000u32;
let row_bytes = 64u32;
let (mut classic, _classic_cpu, mut classic_bus) = setup();
classic.set_screen_mode_for_test(screen_base, row_bytes, 512, 342, 1);
classic.window_bounds = (40, 40, 100, 140);
classic.window_proc_id = 1;
classic.draw_window_frame(&mut classic_bus);
let (mut themed, _themed_cpu, mut themed_bus) = setup();
themed.set_ui_theme_id(UiThemeId::SystemlessDefault);
themed.set_screen_mode_for_test(screen_base, row_bytes, 512, 342, 1);
themed.window_bounds = (40, 40, 100, 140);
themed.window_proc_id = 1;
themed.draw_window_frame(&mut themed_bus);
assert!(
!screen_pixel_is_set(&classic_bus, screen_base, row_bytes, 50, 34),
"classic dBoxProc window frame keeps the structure gap clear"
);
assert!(
screen_pixel_is_set(&themed_bus, screen_base, row_bytes, 50, 34),
"systemless-default provider should own window frame accent pixels"
);
}
#[test]
fn draw_dialog_preserves_existing_user_item_pixels() {
// UserItems are application-owned drawing areas. Games often draw
// into them before calling DrawDialog; the Dialog Manager redraw
// must not erase that content while refreshing the standard items.
let (mut disp, mut cpu, mut bus) = setup();
let dialog_ptr = bus.alloc(170);
let (screen_base, row_bytes, _w, _h, pixel_size) = disp.screen_mode;
bus.write_word(dialog_ptr + 8, 0);
bus.write_word(dialog_ptr + 10, 0);
bus.write_word(dialog_ptr + 16, 0);
bus.write_word(dialog_ptr + 18, 0);
bus.write_word(dialog_ptr + 20, 40);
bus.write_word(dialog_ptr + 22, 80);
disp.dialog_items.insert(
dialog_ptr,
vec![
DialogItem {
item_type: 0x80, // disabled userItem
rect: (8, 8, 20, 32),
text: String::new(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
},
DialogItem {
item_type: 4,
rect: (24, 8, 36, 40),
text: "OK".to_string(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
},
],
);
let user_x = 10u32;
let user_y = 10u32;
let background_x = 4u32;
let background_y = 4u32;
if pixel_size == 8 {
bus.write_byte(screen_base + user_y * row_bytes + user_x, 0xFF);
bus.write_byte(screen_base + background_y * row_bytes + background_x, 0xFF);
} else {
for (x, y) in [(user_x, user_y), (background_x, background_y)] {
let addr = screen_base + y * row_bytes + (x / 8);
let bit = 1 << (7 - (x % 8));
bus.write_byte(addr, bus.read_byte(addr) | bit);
}
}
bus.write_long(TEST_SP, dialog_ptr);
let result = disp.dispatch_dialog(true, 0x181, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
if pixel_size == 8 {
assert_eq!(
bus.read_byte(screen_base + user_y * row_bytes + user_x),
0xFF,
"DrawDialog should preserve pre-drawn userItem pixels"
);
assert_eq!(
bus.read_byte(screen_base + background_y * row_bytes + background_x),
0,
"DrawDialog should still repaint normal dialog background"
);
} else {
let user_addr = screen_base + user_y * row_bytes + (user_x / 8);
let user_bit = 1 << (7 - (user_x % 8));
let background_addr = screen_base + background_y * row_bytes + (background_x / 8);
let background_bit = 1 << (7 - (background_x % 8));
assert_ne!(bus.read_byte(user_addr) & user_bit, 0);
assert_eq!(bus.read_byte(background_addr) & background_bit, 0);
}
}
#[test]
fn modal_dialog_first_entry_does_not_preserve_invisible_user_item_background() {
// A newly shown modal dialog has no meaningful userItem pixels yet.
// Dialog Manager fills the dialog background; it must not treat the
// previous screen contents as application-owned userItem drawing.
let (mut disp, mut cpu, mut bus) = setup();
let dialog_ptr = bus.alloc(170);
let item_hit_ptr = bus.alloc(2);
let (screen_base, row_bytes, _w, _h, pixel_size) = disp.screen_mode;
assert_eq!(pixel_size, 8);
bus.write_word(dialog_ptr + 8, 0);
bus.write_word(dialog_ptr + 10, 0);
bus.write_word(dialog_ptr + 16, 0);
bus.write_word(dialog_ptr + 18, 0);
bus.write_word(dialog_ptr + 20, 40);
bus.write_word(dialog_ptr + 22, 80);
bus.write_word(dialog_ptr + 108, 2);
disp.front_window = dialog_ptr;
disp.window_bounds = (0, 0, 40, 80);
disp.window_proc_id = 2;
disp.dialog_items.insert(
dialog_ptr,
vec![
DialogItem {
item_type: 0x80, // disabled userItem placeholder
rect: (8, 8, 20, 32),
text: String::new(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
},
DialogItem {
item_type: 4,
rect: (24, 8, 36, 40),
text: "OK".to_string(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
},
],
);
let user_x = 10u32;
let user_y = 10u32;
bus.write_byte(screen_base + user_y * row_bytes + user_x, 0xFF);
bus.write_long(TEST_SP, item_hit_ptr);
bus.write_long(TEST_SP + 4, 0);
let result = disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(
bus.read_byte(screen_base + user_y * row_bytes + user_x),
0,
"ModalDialog should paint a clean dialog background through untouched userItems"
);
assert!(disp.dialog_tracking.is_some());
}
#[test]
fn modal_dialog_first_entry_restores_visible_shell_before_user_item_preservation() {
// Visible GetNewDialog draws a clean shell before the first ModalDialog
// call. If later screen/chrome drawing leaves unrelated pixels in a
// userItem rect, ModalDialog must restore that clean shell before
// preserving application-owned userItem pixels. EVO's startup
// shareware notice exposes this when title/menu art leaks through its
// large userItem body.
let (mut disp, mut cpu, mut bus) = setup();
let screen_base = bus.alloc((800 * 600) as u32);
bus.write_bytes(screen_base, &vec![0x77; 800 * 600]);
bus.write_long(0x0824, screen_base);
disp.screen_mode = (screen_base, 800, 800, 600, 8);
let mut dlog = build_test_dlog((100, 100, 180, 260), 1711, 0);
dlog[10] = 1; // visible
let ditl = build_test_ditl_items(&[
(0, (8, 8, 30, 80), b"".as_slice()),
(4, (44, 20, 64, 90), b"OK".as_slice()),
]);
disp.install_test_resource(&mut bus, *b"DLOG", 1710, &dlog);
disp.install_test_resource(&mut bus, *b"DITL", 1711, &ditl);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, 0xFFFF_FFFF); // behind
bus.write_long(TEST_SP + 4, 0); // dStorage
bus.write_word(TEST_SP + 8, 1710); // dialogID
disp.dispatch_dialog(true, 0x17C, &mut cpu, &mut bus)
.unwrap()
.unwrap();
let dlg_ptr = bus.read_long(TEST_SP + 10);
assert_ne!(dlg_ptr, 0);
assert!(disp.dialog_visible_snapshots.contains_key(&dlg_ptr));
let probe_v = 120u32;
let probe_h = 120u32;
let probe_addr = screen_base + probe_v * 800 + probe_h;
assert_eq!(
bus.read_byte(probe_addr),
0,
"visible dialog creation should paint the userItem area as dialog background"
);
bus.write_byte(probe_addr, 0x55);
let item_hit_ptr = bus.alloc(2);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, item_hit_ptr);
bus.write_long(TEST_SP + 4, 0);
disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(
bus.read_byte(probe_addr),
0,
"first ModalDialog entry should not preserve stale non-dialog pixels in userItems"
);
assert!(disp.dialog_tracking.is_some());
assert!(
!disp.dialog_visible_snapshots.contains_key(&dlg_ptr),
"first ModalDialog entry consumes the clean visible shell snapshot"
);
}
#[test]
fn premodal_visible_dialog_snapshot_ignores_bulk_screen_blits() {
// Background windows may bulk-blit menu/title art after a visible
// dialog is already on screen. Until ModalDialog or a userItem draw
// proc owns the dialog, those bulk blits are not dialog content and
// must not replace the retained visible-dialog snapshot.
let (mut disp, _cpu, mut bus) = setup();
let screen_base = bus.alloc((64 * 64) as u32);
bus.write_bytes(screen_base, &vec![0x00; 64 * 64]);
bus.write_long(0x0824, screen_base);
disp.screen_mode = (screen_base, 64, 64, 64, 8);
let dialog_ptr = 0x200000u32;
let bounds = (10, 10, 30, 30);
let snapshot_width = (bounds.3 - bounds.1 + TrapDispatcher::DBOX_FRAME_MARGIN * 2) as usize;
let snapshot_height =
(bounds.2 - bounds.0 + TrapDispatcher::DBOX_FRAME_MARGIN * 2) as usize;
disp.front_window = dialog_ptr;
disp.dialog_items.insert(dialog_ptr, Vec::new());
disp.dialog_visible_snapshots.insert(
dialog_ptr,
PersistentDialogSnapshot {
bounds,
pixels: vec![0x11; snapshot_width * snapshot_height],
},
);
let probe = screen_base + 20 * 64 + 20;
bus.write_byte(probe, 0x77);
disp.refresh_visible_dialog_snapshot_after_bulk_port_draw(&bus, dialog_ptr);
bus.write_byte(probe, 0x00);
disp.restore_visible_dialog_snapshots(&mut bus);
assert_eq!(
bus.read_byte(probe),
0x11,
"pre-modal bulk blits must not replace a visible dialog snapshot"
);
disp.dialog_modal_entered.insert(dialog_ptr);
bus.write_byte(probe, 0x88);
disp.refresh_visible_dialog_snapshot_after_bulk_port_draw(&bus, dialog_ptr);
bus.write_byte(probe, 0x00);
disp.restore_visible_dialog_snapshots(&mut bus);
assert_eq!(
bus.read_byte(probe),
0x88,
"retained modal dialogs may refresh snapshots from later bulk drawing"
);
let game_dialog_ptr = 0x200100u32;
let game_bounds = (34, 10, 54, 30);
bus.write_word(game_dialog_ptr + 6, 0xC000);
let pixmap_handle = bus.alloc(4);
let pixmap_ptr = bus.alloc(50);
bus.write_long(pixmap_handle, pixmap_ptr);
bus.write_long(pixmap_ptr, screen_base);
bus.write_word(pixmap_ptr + 4, 0x8000 | 64);
bus.write_word(pixmap_ptr + 6, (-(game_bounds.0)) as u16);
bus.write_word(pixmap_ptr + 8, (-(game_bounds.1)) as u16);
bus.write_word(pixmap_ptr + 32, 8);
bus.write_long(game_dialog_ptr + 2, pixmap_handle);
bus.write_word(game_dialog_ptr + 16, 0);
bus.write_word(game_dialog_ptr + 18, 0);
bus.write_word(game_dialog_ptr + 20, 20);
bus.write_word(game_dialog_ptr + 22, 20);
bus.write_byte(game_dialog_ptr + 110, 0xFF);
disp.dialog_items.insert(
game_dialog_ptr,
vec![DialogItem {
item_type: 0,
rect: (0, 0, 20, 20),
text: String::new(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
}],
);
disp.front_window = game_dialog_ptr;
bus.write_byte(screen_base + 40 * 64 + 20, 0x99);
disp.refresh_visible_dialog_snapshot_after_bulk_port_draw(&bus, game_dialog_ptr);
bus.write_byte(screen_base + 40 * 64 + 20, 0x00);
disp.restore_visible_dialog_snapshots(&mut bus);
assert_eq!(
bus.read_byte(screen_base + 40 * 64 + 20),
0x99,
"game-managed dialogs should create snapshots from app-owned bulk drawing"
);
}
#[test]
fn updtdialog_pops_eight_bytes() {
// Inside Macintosh Volume I, I-415: UpdtDialog is a Pascal
// procedure taking theDialog and updateRgn.
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
bus.write_long(sp, 0x300100); // updateRgn
bus.write_long(sp + 4, 0x200000); // theDialog
let result = disp.dispatch_dialog(true, 0x178, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), sp + 8);
}
#[test]
fn updtdialog_nil_update_region_is_a_noop() {
// Inside Macintosh Volume I, I-415 names updateRgn as the dialog's
// update region. A NIL region should not trigger redraw work.
let (mut disp, mut cpu, mut bus) = setup();
let existing = 0x200040u32;
disp.window_list = vec![existing];
disp.front_window = existing;
bus.write_byte(existing + 110, 0xFF);
let screen_base = bus.alloc((800 * 600) as u32);
bus.write_long(0x0824, screen_base);
disp.screen_mode = (screen_base, 800, 800, 600, 8);
let bounds_rect_ptr = 0x301200u32;
bus.write_word(bounds_rect_ptr, 100);
bus.write_word(bounds_rect_ptr + 2, 100);
bus.write_word(bounds_rect_ptr + 4, 300);
bus.write_word(bounds_rect_ptr + 6, 400);
let sp = TEST_SP - 30;
cpu.write_reg(Register::A7, sp);
for i in 0..34u32 {
bus.write_byte(sp + i, 0);
}
bus.write_long(sp + 22, bounds_rect_ptr);
bus.write_word(sp + 16, 1); // visible
bus.write_long(sp + 10, 0); // behind = NIL (backmost)
let result = disp.dispatch_dialog(true, 0x17D, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
let dlg_ptr = bus.read_long(sp + 30);
assert_ne!(dlg_ptr, 0);
let probe_x = 120u32;
let probe_y = 120u32;
let probe_addr = screen_base + probe_y * 800 + probe_x;
bus.write_byte(probe_addr, 0xFF);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, 0); // NIL update region
bus.write_long(TEST_SP + 4, dlg_ptr);
let result = disp.dispatch_dialog(true, 0x178, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
assert_eq!(bus.read_byte(probe_addr), 0xFF);
}
#[test]
fn updtdialog_queues_user_item_draw_proc_for_intersecting_update_region() {
// MTE 1992, 6-142 to 6-143: UpdateDialog redraws only items in
// the supplied update region and calls application-defined item
// draw procedures.
let (mut disp, mut cpu, mut bus) = setup();
let screen_base = bus.alloc(100 * 100);
bus.write_long(0x0824, screen_base);
disp.screen_mode = (screen_base, 100, 100, 100, 8);
let dialog_ptr = bus.alloc(256);
bus.write_word(dialog_ptr + 16, 0);
bus.write_word(dialog_ptr + 18, 0);
bus.write_word(dialog_ptr + 20, 100);
bus.write_word(dialog_ptr + 22, 100);
bus.write_word(dialog_ptr + 108, 2); // plain dialog box
disp.dialog_items.insert(
dialog_ptr,
vec![
DialogItem {
item_type: 0,
rect: (8, 8, 24, 32),
proc_ptr: 0x500000,
..Default::default()
},
DialogItem {
item_type: 0,
rect: (60, 60, 80, 80),
proc_ptr: 0x600000,
..Default::default()
},
],
);
let update_rgn_ptr = bus.alloc(10);
let update_rgn = bus.alloc(4);
bus.write_long(update_rgn, update_rgn_ptr);
bus.write_word(update_rgn_ptr, 10);
bus.write_word(update_rgn_ptr + 2, 0);
bus.write_word(update_rgn_ptr + 4, 0);
bus.write_word(update_rgn_ptr + 6, 40);
bus.write_word(update_rgn_ptr + 8, 40);
bus.write_long(TEST_SP, update_rgn);
bus.write_long(TEST_SP + 4, dialog_ptr);
let result = disp.dispatch_dialog(true, 0x178, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
assert_eq!(
disp.modeless_dialog_draw_proc_queue
.iter()
.copied()
.collect::<Vec<_>>(),
vec![(dialog_ptr, 0x500000, 1)]
);
}
#[test]
fn te_text_box_erases_box_before_drawing_text() {
// Inside Macintosh: Text 1993, p. 2-88: TETextBox erases the box
// and then draws wrapped/aligned text into it.
let (mut disp, mut cpu, mut bus) = setup_with_port();
let port_ptr = 0x181000u32;
let screen_base = bus.read_long(0x0824);
// Fill the target area black so a missing EraseRect is visible.
for y in 0..12u32 {
for byte in 0..3u32 {
bus.write_byte(screen_base + y * 64 + byte, 0xFF);
}
}
// Make sure the text renderer has a usable size and port state.
disp.current_port = port_ptr;
disp.tx_size = 12;
bus.write_word(port_ptr + 74, 12);
let text_ptr = 0x200000u32;
bus.write_byte(text_ptr, b'A');
let box_ptr = 0x200100u32;
bus.write_word(box_ptr, 0); // top
bus.write_word(box_ptr + 2, 0); // left
bus.write_word(box_ptr + 4, 12); // bottom
bus.write_word(box_ptr + 6, 24); // right
let sp = TEST_SP - 14;
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, 0); // teFlushLeft
bus.write_long(sp + 2, box_ptr);
bus.write_long(sp + 6, 1); // length
bus.write_long(sp + 10, text_ptr);
let result = disp.dispatch_dialog(true, 0x1CE, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP);
// Pixel (20, 10) lies inside the box but outside the glyph; it should
// be white after the implicit EraseRect.
let probe_addr = screen_base + 10 * 64 + 2;
assert_eq!(bus.read_byte(probe_addr) & (1 << 3), 0);
}
#[test]
fn te_text_box_erases_box_for_zero_length_text() {
// Inside Macintosh: Text 1993, p. 2-88: the erase precedes text
// drawing and is not conditional on the text length.
let (mut disp, mut cpu, mut bus) = setup_with_port();
let port_ptr = 0x181000u32;
let screen_base = bus.read_long(0x0824);
for y in 0..12u32 {
for byte in 0..3u32 {
bus.write_byte(screen_base + y * 64 + byte, 0xFF);
}
}
disp.current_port = port_ptr;
disp.tx_size = 12;
bus.write_word(port_ptr + 74, 12);
let text_ptr = 0x200000u32;
let box_ptr = 0x200100u32;
bus.write_word(box_ptr, 0);
bus.write_word(box_ptr + 2, 0);
bus.write_word(box_ptr + 4, 12);
bus.write_word(box_ptr + 6, 24);
let sp = TEST_SP - 14;
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, 0);
bus.write_long(sp + 2, box_ptr);
bus.write_long(sp + 6, 0);
bus.write_long(sp + 10, text_ptr);
let result = disp.dispatch_dialog(true, 0x1CE, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP);
let probe_addr = screen_base + 10 * 64 + 2;
assert_eq!(bus.read_byte(probe_addr) & (1 << 3), 0);
}
#[test]
fn te_text_box_consumes_align_box_length_text_arguments() {
// Inside Macintosh: Text 1993, p. 2-88 declares TETextBox as a
// procedure taking text/length/box/align arguments.
let (mut disp, mut cpu, mut bus) = setup_with_port();
let port_ptr = 0x181000u32;
disp.current_port = port_ptr;
disp.tx_size = 12;
bus.write_word(port_ptr + 74, 12);
let text_ptr = 0x200200u32;
bus.write_byte(text_ptr, b'A');
let box_ptr = 0x200240u32;
bus.write_word(box_ptr, 0);
bus.write_word(box_ptr + 2, 0);
bus.write_word(box_ptr + 4, 20);
bus.write_word(box_ptr + 6, 60);
let sp = TEST_SP - 14;
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, 0);
bus.write_long(sp + 2, box_ptr);
bus.write_long(sp + 6, 1);
bus.write_long(sp + 10, text_ptr);
let result = disp.dispatch_dialog(true, 0x1CE, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP);
}
#[test]
fn te_text_box_align_parameter_controls_rendered_line_origin() {
// Inside Macintosh: Text 1993, p. 2-87 defines teJustLeft(0),
// teJustCenter(1), and teJustRight(-1) alignment values.
let (mut disp, mut cpu, mut bus) = setup_with_port();
let port_ptr = 0x181000u32;
disp.current_port = port_ptr;
disp.tx_size = 12;
bus.write_word(port_ptr + 74, 12);
let text = b"ABC";
let text_ptr = 0x200280u32;
bus.write_bytes(text_ptr, text);
let box_ptr = 0x2002C0u32;
let box_top = 0i16;
let box_left = 10i16;
let box_bottom = 40i16;
let box_right = 110i16;
bus.write_word(box_ptr, box_top as u16);
bus.write_word(box_ptr + 2, box_left as u16);
bus.write_word(box_ptr + 4, box_bottom as u16);
bus.write_word(box_ptr + 6, box_right as u16);
let advance_extra = disp.advance_extra();
let missing_advance = disp.missing_glyph_advance();
let mut line_width = 0i16;
for &byte in text {
if let Some((glyph, _)) =
crate::quickdraw::text::get_glyph(disp.tx_font, disp.tx_size, byte as char)
{
line_width += glyph.advance as i16 + advance_extra;
} else {
line_width += missing_advance;
}
}
let mut run_align = |align: i16| -> i16 {
let sp = TEST_SP - 14;
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, align as u16);
bus.write_long(sp + 2, box_ptr);
bus.write_long(sp + 6, text.len() as u32);
bus.write_long(sp + 10, text_ptr);
let result = disp.dispatch_dialog(true, 0x1CE, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP);
disp.pn_loc.1
};
let x_left = run_align(0);
let x_center = run_align(1);
let x_right = run_align(-1);
let expected_left =
TrapDispatcher::te_line_origin_x(0, box_left, box_right, line_width) + line_width;
let expected_center =
TrapDispatcher::te_line_origin_x(1, box_left, box_right, line_width) + line_width;
let expected_right =
TrapDispatcher::te_line_origin_x(-1, box_left, box_right, line_width) + line_width;
assert_eq!(x_left, expected_left);
assert_eq!(x_center, expected_center);
assert_eq!(x_right, expected_right);
assert!(x_left < x_center && x_center < x_right);
}
#[test]
fn te_text_box_pascal_procedure_protocol_does_not_overwrite_past_arg_frame() {
// Per Inside Macintosh: Text 1993, p. 2-88 TETextBox is a
// Pascal PROCEDURE. The MPW Universal Headers C declaration
// uses `const Rect *box`, so the caller pushes exactly 14
// bytes (2-byte just + 4-byte Rect* + 4-byte long + 4-byte
// text Ptr) and the trap must pop exactly 14 bytes with no
// result slot written.
let (mut disp, mut cpu, mut bus) = setup_with_port();
let port_ptr = 0x181000u32;
disp.current_port = port_ptr;
disp.tx_size = 12;
bus.write_word(port_ptr + 74, 12);
let text_ptr = 0x200400u32;
bus.write_byte(text_ptr, b'X');
let box_ptr = 0x200440u32;
bus.write_word(box_ptr, 0);
bus.write_word(box_ptr + 2, 0);
bus.write_word(box_ptr + 4, 12);
bus.write_word(box_ptr + 6, 64);
// Pre-poison memory immediately past the 14-byte arg frame.
// After the trap pops 14 bytes, A7 must equal TEST_SP and the
// sentinel words at TEST_SP, TEST_SP+2 must survive untouched.
let sp = TEST_SP - 14;
bus.write_word(TEST_SP, 0xCAFE);
bus.write_word(TEST_SP + 2, 0xBABE);
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, 0); // align = teJustLeft
bus.write_long(sp + 2, box_ptr);
bus.write_long(sp + 6, 1); // length
bus.write_long(sp + 10, text_ptr);
let result = disp.dispatch_dialog(true, 0x1CE, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP);
assert_eq!(bus.read_word(TEST_SP), 0xCAFE);
assert_eq!(bus.read_word(TEST_SP + 2), 0xBABE);
}
#[test]
fn te_text_box_wraps_when_text_exceeds_box_width() {
// Inside Macintosh: Text 1993, p. 2-88: TETextBox word-wraps text
// in the destination rectangle.
let (mut disp, mut cpu, mut bus) = setup_with_port();
let port_ptr = 0x181000u32;
disp.current_port = port_ptr;
disp.tx_size = 12;
bus.write_word(port_ptr + 74, 12);
let text = b"A A A A";
let text_ptr = 0x200300u32;
bus.write_bytes(text_ptr, text);
let mut glyph_w = disp.missing_glyph_advance();
if let Some((glyph, _)) = crate::quickdraw::text::get_glyph(disp.tx_font, disp.tx_size, 'A')
{
glyph_w = glyph.advance as i16 + disp.advance_extra();
}
let wide_box_ptr = 0x200340u32;
bus.write_word(wide_box_ptr, 0);
bus.write_word(wide_box_ptr + 2, 0);
bus.write_word(wide_box_ptr + 4, 80);
bus.write_word(wide_box_ptr + 6, 140);
let narrow_box_ptr = 0x200380u32;
bus.write_word(narrow_box_ptr, 0);
bus.write_word(narrow_box_ptr + 2, 0);
bus.write_word(narrow_box_ptr + 4, 80);
bus.write_word(narrow_box_ptr + 6, glyph_w as u16);
let mut run_box = |box_ptr: u32| -> i16 {
let sp = TEST_SP - 14;
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, 0);
bus.write_long(sp + 2, box_ptr);
bus.write_long(sp + 6, text.len() as u32);
bus.write_long(sp + 10, text_ptr);
let result = disp.dispatch_dialog(true, 0x1CE, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP);
disp.pn_loc.0
};
let y_wide = run_box(wide_box_ptr);
let y_narrow = run_box(narrow_box_ptr);
let metrics = crate::quickdraw::text::get_font_metrics(disp.tx_font, disp.tx_size);
let line_height = metrics.ascent + metrics.descent + metrics.leading.max(2);
assert!(
y_narrow >= y_wide + line_height,
"narrow box should force at least one additional wrapped line"
);
}
// ---- CloseDialog ($A982) ----
fn alloc_region_handle(bus: &mut MacMemoryBus, rect: Option<(i16, i16, i16, i16)>) -> u32 {
let rgn_ptr = bus.alloc(10);
bus.write_word(rgn_ptr, 10);
if let Some((top, left, bottom, right)) = rect.filter(|r| r.2 > r.0 && r.3 > r.1) {
bus.write_word(rgn_ptr + 2, top as u16);
bus.write_word(rgn_ptr + 4, left as u16);
bus.write_word(rgn_ptr + 6, bottom as u16);
bus.write_word(rgn_ptr + 8, right as u16);
} else {
bus.write_long(rgn_ptr + 2, 0);
bus.write_long(rgn_ptr + 6, 0);
}
let handle = bus.alloc(4);
bus.write_long(handle, rgn_ptr);
handle
}
fn seed_window_regions(
bus: &mut MacMemoryBus,
window_ptr: u32,
content_rect: (i16, i16, i16, i16),
) {
// Minimal WindowRecord region setup for invalidate_window_rect:
// contRgn @ +118 and updateRgn @ +122.
bus.write_word(window_ptr + 16, content_rect.0 as u16);
bus.write_word(window_ptr + 18, content_rect.1 as u16);
bus.write_word(window_ptr + 20, content_rect.2 as u16);
bus.write_word(window_ptr + 22, content_rect.3 as u16);
let cont_rgn = alloc_region_handle(bus, Some(content_rect));
let update_rgn = alloc_region_handle(bus, None);
bus.write_long(window_ptr + 118, cont_rgn);
bus.write_long(window_ptr + 122, update_rgn);
bus.write_byte(window_ptr + 110, 0xFF);
}
fn seed_app_owned_modal_dialog(
disp: &mut TrapDispatcher,
bus: &mut MacMemoryBus,
dialog_ptr: u32,
prev_window: u32,
proc_id: i16,
) {
seed_window_regions(bus, dialog_ptr, (0, 0, 100, 220));
seed_window_regions(bus, prev_window, (0, 0, 342, 512));
bus.write_word(dialog_ptr + 8, 0);
bus.write_word(dialog_ptr + 10, 0);
bus.write_word(dialog_ptr + 108, 2);
disp.window_proc_ids.insert(dialog_ptr, proc_id);
disp.front_window = dialog_ptr;
disp.current_port = dialog_ptr;
disp.window_bounds = (0, 0, 100, 220);
disp.window_proc_id = proc_id;
disp.window_list = vec![dialog_ptr, prev_window];
disp.window_stack
.push((prev_window, (0, 0, 342, 512), 0, "Game".to_string()));
disp.dialog_items.insert(
dialog_ptr,
vec![DialogItem {
item_type: 4,
rect: (60, 130, 82, 200),
text: "OK".to_string(),
..DialogItem::default()
}],
);
disp.sent_open_app_event = true;
}
fn seed_retained_modal_dialog(
disp: &mut TrapDispatcher,
bus: &mut MacMemoryBus,
dialog_ptr: u32,
prev_window: u32,
proc_id: i16,
) {
seed_app_owned_modal_dialog(disp, bus, dialog_ptr, prev_window, proc_id);
disp.dialog_modal_entered.insert(dialog_ptr);
}
#[test]
fn close_dialog_pops_4_bytes() {
// Inside Macintosh Volume I, I-413: CloseDialog takes one
// DialogPtr argument.
let (mut disp, mut cpu, mut bus) = setup();
bus.write_long(TEST_SP, 0x200000);
let result = disp.dispatch_dialog(true, 0x182, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
}
#[test]
fn close_dialog_front_dialog_restores_previous_port_state() {
// IM:I I-413 says CloseDialog behaves like CloseWindow; when the
// front dialog closes, the window behind it becomes frontmost.
let (mut disp, mut cpu, mut bus) = setup();
let dialog_ptr = 0x200000u32;
let prev_window = 0x181000u32;
seed_window_regions(&mut bus, prev_window, (0, 0, 342, 512));
disp.front_window = dialog_ptr;
disp.current_port = dialog_ptr;
disp.window_bounds = (100, 120, 220, 320);
bus.write_long(crate::memory::globals::addr::THE_PORT, dialog_ptr);
let a5 = cpu.read_reg(Register::A5);
let global_ptr = bus.read_long(a5);
bus.write_long(global_ptr, dialog_ptr);
disp.window_stack
.push((prev_window, (0, 0, 342, 512), 2, "Prev".to_string()));
bus.write_long(TEST_SP, dialog_ptr);
let result = disp.dispatch_dialog(true, 0x182, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(disp.front_window, prev_window);
assert_eq!(disp.current_port, prev_window);
assert_eq!(
bus.read_long(crate::memory::globals::addr::THE_PORT),
prev_window
);
assert_eq!(bus.read_long(global_ptr), prev_window);
assert_eq!(
TrapDispatcher::region_handle_rect(&bus, bus.read_long(prev_window + 122)),
Some((100, 120, 220, 320)),
"CloseDialog should invalidate the dialog-exposed area on the promoted window"
);
assert!(
disp.event_queue.iter().any(|event| {
event.what == 8 && event.message == prev_window && (event.modifiers & 1) != 0
}),
"CloseDialog should queue activateEvt for the promoted front window"
);
assert!(
disp.event_queue
.iter()
.any(|event| event.what == 6 && event.message == prev_window),
"CloseDialog must queue updateEvt for the newly exposed front window"
);
}
#[test]
fn close_dialog_restores_saved_background_pixels() {
let (mut disp, mut cpu, mut bus) = setup();
let screen_base = 0x300000u32;
let row_bytes: u32 = 640;
disp.set_screen_mode_for_test(screen_base, row_bytes, 640, 480, 8);
let bounds = (100i16, 100i16, 150i16, 200i16);
let dialog_ptr = 0x200000u32;
disp.front_window = dialog_ptr;
disp.window_bounds = bounds;
for y in 92u32..158 {
for x in 92u32..208 {
bus.write_byte(screen_base + y * row_bytes + x, 0xCC);
}
}
disp.dialog_saved_pixels
.insert(dialog_ptr, vec![0x33; 66 * 116]);
bus.write_long(TEST_SP, dialog_ptr);
let result = disp.dispatch_dialog(true, 0x182, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
for y in 92u32..158 {
for x in 92u32..208 {
assert_eq!(
bus.read_byte(screen_base + y * row_bytes + x),
0x33,
"CloseDialog should restore saved background at ({x},{y})"
);
}
}
assert!(
!disp.dialog_saved_pixels.contains_key(&dialog_ptr),
"saved pixels must be consumed after CloseDialog"
);
}
#[test]
fn close_dialog_non_front_dialog_leaves_front_window_and_port_unchanged() {
// CloseWindow front-promotion only applies when the closed window
// was frontmost (IM:I I-283); CloseDialog inherits that behavior.
let (mut disp, mut cpu, mut bus) = setup();
let dialog_ptr = 0x200000u32;
let other_front = 0x181000u32;
disp.front_window = other_front;
disp.current_port = other_front;
bus.write_long(crate::memory::globals::addr::THE_PORT, other_front);
let a5 = cpu.read_reg(Register::A5);
let global_ptr = bus.read_long(a5);
bus.write_long(global_ptr, other_front);
disp.window_stack
.push((0x170000, (1, 2, 3, 4), 3, "Prev".to_string()));
bus.write_long(TEST_SP, dialog_ptr);
let result = disp.dispatch_dialog(true, 0x182, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(disp.front_window, other_front);
assert_eq!(disp.current_port, other_front);
assert_eq!(
bus.read_long(crate::memory::globals::addr::THE_PORT),
other_front
);
assert_eq!(bus.read_long(global_ptr), other_front);
assert_eq!(
disp.window_stack.len(),
1,
"non-front CloseDialog should not consume the saved front-window stack entry"
);
}
#[test]
fn close_dialog_removes_window_but_preserves_caller_storage() {
// MTE 1992 pp. 6-119..6-120: CloseDialog releases dialog-owned
// items but does not dispose caller-supplied DialogRecord or item-list
// storage.
let (mut disp, mut cpu, mut bus) = setup();
install_new_dialog_test_screen(&mut disp, &mut bus, 0x77);
let storage_ptr = bus.alloc(170);
let ditl = build_test_ditl_item(8, (10, 12, 24, 140), b"Static");
let (items_handle, items_ptr) = install_ditl_handle_for_test(&mut bus, &ditl);
let (_sp, dialog_ptr) = call_new_dialog_for_test(
&mut disp,
&mut cpu,
&mut bus,
storage_ptr,
(80, 90, 150, 260),
false,
4,
0xFFFF_FFFF,
items_handle,
);
let text_handle = ditl_item_handle_field(&bus, items_ptr, 1);
let text_ptr = bus.read_long(text_handle);
let te_handle = bus.read_long(dialog_ptr + 160);
let te_ptr = bus.read_long(te_handle);
assert_ne!(text_handle, 0);
assert_ne!(text_ptr, 0);
assert_ne!(te_handle, 0);
assert_ne!(te_ptr, 0);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, dialog_ptr);
disp.dispatch_dialog(true, 0x182, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
assert!(!disp.window_list.contains(&dialog_ptr));
assert_eq!(bus.get_alloc_size(storage_ptr), Some(170));
assert_eq!(bus.get_alloc_size(items_handle), Some(4));
assert_eq!(bus.get_alloc_size(items_ptr), Some(ditl.len() as u32));
assert_eq!(bus.get_alloc_size(text_ptr), None);
assert_eq!(bus.get_alloc_size(text_handle), None);
assert_eq!(bus.get_alloc_size(te_ptr), None);
assert_eq!(bus.get_alloc_size(te_handle), None);
assert!(!disp.dialog_items.contains_key(&dialog_ptr));
assert!(!disp.dialog_item_handles.contains_key(&text_handle));
}
// ---- DisposDialog ($A983) ----
#[test]
fn dispos_dialog_pops_4_bytes() {
let (mut disp, mut cpu, mut bus) = setup();
// SP+0: dialog ptr (4 bytes)
bus.write_long(TEST_SP, 0x200000);
let result = disp.dispatch_dialog(true, 0x183, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
}
#[test]
fn dispos_dialog_restores_previous_port_state() {
let (mut disp, mut cpu, mut bus) = setup();
let dialog_ptr = 0x200000u32;
let prev_window = 0x181000u32;
seed_window_regions(&mut bus, prev_window, (0, 0, 342, 512));
disp.front_window = dialog_ptr;
disp.current_port = dialog_ptr;
disp.window_bounds = (100, 120, 220, 320);
bus.write_long(crate::memory::globals::addr::THE_PORT, dialog_ptr);
let a5 = cpu.read_reg(Register::A5);
let global_ptr = bus.read_long(a5);
bus.write_long(global_ptr, dialog_ptr);
disp.window_stack
.push((prev_window, (0, 0, 342, 512), 2, "Prev".to_string()));
bus.write_long(TEST_SP, dialog_ptr);
let result = disp.dispatch_dialog(true, 0x183, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(disp.front_window, prev_window);
assert_eq!(disp.current_port, prev_window);
assert_eq!(
bus.read_long(crate::memory::globals::addr::THE_PORT),
prev_window
);
assert_eq!(bus.read_long(global_ptr), prev_window);
assert_eq!(
TrapDispatcher::region_handle_rect(&bus, bus.read_long(prev_window + 122)),
Some((100, 120, 220, 320)),
"DisposDialog should invalidate the dialog-exposed area on the promoted window"
);
assert!(
disp.event_queue.iter().any(|event| {
event.what == 8 && event.message == prev_window && (event.modifiers & 1) != 0
}),
"DisposDialog should queue activateEvt for the promoted front window"
);
assert!(
disp.event_queue
.iter()
.any(|event| event.what == 6 && event.message == prev_window),
"DisposDialog must queue updateEvt for the newly exposed front window"
);
}
#[test]
fn dispos_dialog_removes_dialog_from_window_list() {
// IM:I I-425: DisposDialog closes/disposes the dialog and
// IM:I I-283 CloseWindow semantics remove it from window list.
let (mut disp, mut cpu, mut bus) = setup();
let dialog_ptr = 0x200000u32;
let prev_window = 0x181000u32;
disp.window_list = vec![dialog_ptr, prev_window];
disp.front_window = dialog_ptr;
disp.current_port = dialog_ptr;
bus.write_byte(dialog_ptr + 110, 0xFF);
bus.write_byte(prev_window + 110, 0xFF);
disp.window_stack
.push((prev_window, (0, 0, 342, 512), 2, "Prev".to_string()));
bus.write_long(TEST_SP, dialog_ptr);
let result = disp.dispatch_dialog(true, 0x183, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(disp.window_list, vec![prev_window]);
assert_eq!(disp.front_window, prev_window);
}
#[test]
fn dispos_dialog_with_user_item_proc_argument_does_not_close_dialog() {
// SetDItem stores a ProcPtr for userItem entries. A ProcPtr is not a
// DialogPtr, so DisposDialog must not translate it into the current
// front dialog. Doing so suppresses legitimate visible prompts whose
// app code is installing or calling userItem procedures.
// Inside Macintosh Volume I, I-421, I-425.
let (mut disp, mut cpu, mut bus) = setup();
let dialog_ptr = 0x200000u32;
let prev_window = 0x181000u32;
let user_item_proc = 0x00016178u32;
seed_window_regions(&mut bus, prev_window, (0, 0, 342, 512));
disp.window_list = vec![dialog_ptr, prev_window];
disp.front_window = dialog_ptr;
disp.current_port = dialog_ptr;
disp.window_bounds = (110, 155, 380, 645);
disp.dialog_items.insert(
dialog_ptr,
vec![DialogItem {
item_type: 0x80,
proc_ptr: user_item_proc,
..DialogItem::default()
}],
);
disp.dialog_saved_pixels
.insert(dialog_ptr, vec![0x33; 66 * 116]);
disp.window_stack
.push((prev_window, (0, 0, 342, 512), 2, "Prev".to_string()));
bus.write_long(TEST_SP, user_item_proc);
let result = disp.dispatch_dialog(true, 0x183, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
assert_eq!(disp.window_list, vec![dialog_ptr, prev_window]);
assert_eq!(disp.front_window, dialog_ptr);
assert!(disp.dialog_items.contains_key(&dialog_ptr));
assert!(disp.dialog_saved_pixels.contains_key(&dialog_ptr));
}
#[test]
fn dispos_dialog_after_modal_button_hit_recovers_stale_proc_argument() {
// When ModalDialog has just returned an enabled push button, a
// following DisposeDialog is part of the standard modal teardown
// pattern. If the app hands back a stale ProcPtr-shaped value in that
// exact one-shot window, close the retained modal dialog rather than
// leaving the button-accepted dialog visible forever.
let (mut disp, mut cpu, mut bus) = setup();
let dialog_ptr = 0x200000u32;
let prev_window = 0x181000u32;
let stale_proc_arg = 0x00016178u32;
seed_window_regions(&mut bus, prev_window, (0, 0, 342, 512));
disp.window_list = vec![dialog_ptr, prev_window];
disp.front_window = dialog_ptr;
disp.current_port = dialog_ptr;
disp.window_bounds = (110, 155, 380, 645);
disp.dialog_modal_entered.insert(dialog_ptr);
disp.pending_modal_button_dispose_dialog = Some(dialog_ptr);
disp.dialog_items.insert(
dialog_ptr,
vec![DialogItem {
item_type: 4,
text: String::from("OK"),
..DialogItem::default()
}],
);
disp.window_stack
.push((prev_window, (0, 0, 342, 512), 2, "Prev".to_string()));
bus.write_long(TEST_SP, stale_proc_arg);
let result = disp.dispatch_dialog(true, 0x183, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
assert_eq!(disp.window_list, vec![prev_window]);
assert_eq!(disp.front_window, prev_window);
assert!(!disp.dialog_items.contains_key(&dialog_ptr));
assert!(disp.pending_modal_button_dispose_dialog.is_none());
}
#[test]
fn dispose_dialog_disposes_allocated_storage_and_items() {
// MTE 1992 p. 6-120: DisposeDialog calls CloseDialog, then releases
// the copied item list and DialogRecord allocated for a NIL dStorage.
let (mut disp, mut cpu, mut bus) = setup();
install_new_dialog_test_screen(&mut disp, &mut bus, 0x77);
let ditl = build_test_ditl_item(8, (10, 12, 24, 140), b"Static");
let (items_handle, items_ptr) = install_ditl_handle_for_test(&mut bus, &ditl);
let (_sp, dialog_ptr) = call_new_dialog_for_test(
&mut disp,
&mut cpu,
&mut bus,
0,
(80, 90, 150, 260),
false,
4,
0xFFFF_FFFF,
items_handle,
);
let text_handle = ditl_item_handle_field(&bus, items_ptr, 1);
let text_ptr = bus.read_long(text_handle);
let te_handle = bus.read_long(dialog_ptr + 160);
let te_ptr = bus.read_long(te_handle);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, dialog_ptr);
disp.dispatch_dialog(true, 0x183, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
assert_eq!(bus.get_alloc_size(text_ptr), None);
assert_eq!(bus.get_alloc_size(text_handle), None);
assert_eq!(bus.get_alloc_size(te_ptr), None);
assert_eq!(bus.get_alloc_size(te_handle), None);
assert_eq!(bus.get_alloc_size(items_ptr), None);
assert_eq!(bus.get_alloc_size(items_handle), None);
assert_eq!(bus.get_alloc_size(dialog_ptr), None);
assert!(!disp.dialog_items.contains_key(&dialog_ptr));
}
#[test]
fn dispose_dialog_releases_text_handles() {
// IM:I I-413 and I-425: CloseDialog releases standard dialog items;
// DisposeDialog inherits that item cleanup before freeing the record.
let (mut disp, mut cpu, mut bus) = setup();
install_new_dialog_test_screen(&mut disp, &mut bus, 0x77);
let ditl = build_test_ditl_item(16, (10, 12, 24, 140), b"Edit");
let (items_handle, items_ptr) = install_ditl_handle_for_test(&mut bus, &ditl);
let (_sp, dialog_ptr) = call_new_dialog_for_test(
&mut disp,
&mut cpu,
&mut bus,
0,
(80, 90, 150, 260),
false,
4,
0xFFFF_FFFF,
items_handle,
);
let item_text_handle = ditl_item_handle_field(&bus, items_ptr, 1);
let item_text_ptr = bus.read_long(item_text_handle);
let dialog_te_handle = bus.read_long(dialog_ptr + 160);
let dialog_te_ptr = bus.read_long(dialog_te_handle);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, dialog_ptr);
disp.dispatch_dialog(true, 0x183, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(bus.get_alloc_size(item_text_ptr), None);
assert_eq!(bus.get_alloc_size(item_text_handle), None);
assert_eq!(bus.get_alloc_size(dialog_te_ptr), None);
assert_eq!(bus.get_alloc_size(dialog_te_handle), None);
assert!(!disp.dialog_item_handles.contains_key(&item_text_handle));
}
#[test]
fn dispose_dialog_releases_control_mappings() {
// IM:I I-413 and I-425: dialog-owned controls are released with the
// dialog, including dispatcher-side control mappings.
let (mut disp, mut cpu, mut bus) = setup();
install_new_dialog_test_screen(&mut disp, &mut bus, 0x77);
let ditl = build_test_ditl_item(4, (36, 40, 58, 120), b"OK");
let (items_handle, _items_ptr) = install_ditl_handle_for_test(&mut bus, &ditl);
let (_sp, dialog_ptr) = call_new_dialog_for_test(
&mut disp,
&mut cpu,
&mut bus,
0,
(80, 90, 150, 260),
false,
4,
0xFFFF_FFFF,
items_handle,
);
let box_ptr = bus.alloc(8);
let item_ptr = bus.alloc(4);
let type_ptr = bus.alloc(2);
let snapshot = get_ditem_snapshot_for_test(
&mut disp, &mut cpu, &mut bus, dialog_ptr, 1, box_ptr, item_ptr, type_ptr,
);
let control_handle = snapshot.item;
let control_ptr = bus.read_long(control_handle);
assert_ne!(control_handle, 0);
assert_ne!(control_ptr, 0);
assert_eq!(
disp.dialog_control_handles.get(&control_handle),
Some(&(dialog_ptr, 1))
);
assert!(disp.control_proc_ids.contains_key(&control_ptr));
let aux_handle = disp.ensure_control_aux_record(&mut bus, control_handle);
let aux_ptr = bus.read_long(aux_handle);
assert_ne!(aux_handle, 0);
assert_ne!(aux_ptr, 0);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, dialog_ptr);
disp.dispatch_dialog(true, 0x183, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(bus.get_alloc_size(control_ptr), None);
assert_eq!(bus.get_alloc_size(control_handle), None);
assert_eq!(bus.get_alloc_size(aux_ptr), None);
assert_eq!(bus.get_alloc_size(aux_handle), None);
assert!(disp.control_aux_state(control_handle).is_none());
assert!(!disp.dialog_control_handles.contains_key(&control_handle));
assert!(!disp.control_proc_ids.contains_key(&control_ptr));
}
#[test]
fn dispose_dialog_clears_visible_snapshot() {
let (mut disp, mut cpu, mut bus) = setup();
let dialog_ptr = bus.alloc(170);
disp.window_list = vec![dialog_ptr];
disp.dialog_visible_snapshots.insert(
dialog_ptr,
PersistentDialogSnapshot {
bounds: (10, 20, 30, 40),
pixels: vec![0x55; 400],
},
);
bus.write_long(TEST_SP, dialog_ptr);
disp.dispatch_dialog(true, 0x183, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert!(!disp.dialog_visible_snapshots.contains_key(&dialog_ptr));
}
#[test]
fn dispose_dialog_clears_retained_click_state() {
let (mut disp, mut cpu, mut bus) = setup();
let dialog_ptr = bus.alloc(170);
disp.window_list = vec![dialog_ptr];
disp.retained_modal_dialog_click = Some(RetainedModalDialogClickState {
dialog_ptr,
item_no: 1,
rect: (10, 20, 30, 40),
title: "OK".to_string(),
is_default: true,
highlighted: true,
delivered_to_app: false,
});
bus.write_long(TEST_SP, dialog_ptr);
disp.dispatch_dialog(true, 0x183, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert!(disp.retained_modal_dialog_click.is_none());
}
#[test]
fn dispose_dialog_with_active_tracking_cancels_tracking() {
let (mut disp, mut cpu, mut bus) = setup();
let dialog_ptr = 0x200000u32;
disp.dialog_tracking = Some(DialogTrackingState {
dialog_ptr,
..Default::default()
});
bus.write_long(TEST_SP, dialog_ptr);
disp.dispatch_dialog(true, 0x183, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert!(disp.dialog_tracking.is_none());
}
#[test]
fn retained_modal_dialog_consumes_outside_mouse_down() {
let (mut disp, mut cpu, mut bus) = setup();
let dialog_ptr = bus.alloc(170);
let prev_window = bus.alloc(170);
seed_retained_modal_dialog(&mut disp, &mut bus, dialog_ptr, prev_window, 1);
disp.push_mouse_down(140, 260);
let (what, _message, _where_v, _where_h, _modifiers, has_event) =
disp.dequeue_toolbox_event(&mut cpu, &mut bus, 0xFFFF);
assert!(!has_event);
assert_eq!(what, 0);
assert!(disp.event_queue.is_empty());
assert_eq!(disp.front_window, dialog_ptr);
assert!(
disp.retained_modal_dialog_click.is_some(),
"outside click must be captured until mouseUp so it cannot leak behind the dialog"
);
disp.push_mouse_up(140, 260);
let (_what, _message, _where_v, _where_h, _modifiers, has_event) =
disp.dequeue_toolbox_event(&mut cpu, &mut bus, 0xFFFF);
assert!(!has_event);
assert_eq!(disp.front_window, dialog_ptr);
assert!(disp.retained_modal_dialog_click.is_none());
assert!(
disp.dialog_items.contains_key(&dialog_ptr),
"outside clicks do not dismiss modal dialogs"
);
}
#[test]
fn app_owned_modal_dialog_mouse_down_is_delivered_to_event_loop() {
// A DLOG can be created and shown with GetNewDialog while the
// application owns the WaitNextEvent/DialogSelect loop. The Dialog
// Manager reports enabled item clicks to that application code; it
// does not dismiss the dialog from the Event Manager dequeue path.
// Macintosh Toolbox Essentials 1992, pp. 6-138..6-141.
let (mut disp, mut cpu, mut bus) = setup();
let dialog_ptr = bus.alloc(170);
let prev_window = bus.alloc(170);
let screen_base = 0x300000u32;
let row_bytes = 320u32;
disp.set_screen_mode_for_test(screen_base, row_bytes, 320, 200, 8);
let probe = screen_base + 70 * row_bytes + 150;
bus.write_byte(probe, 17);
seed_app_owned_modal_dialog(&mut disp, &mut bus, dialog_ptr, prev_window, 1);
disp.push_mouse_down(70, 150);
let (what, _message, where_v, where_h, _modifiers, has_event) =
disp.dequeue_toolbox_event(&mut cpu, &mut bus, 0xFFFF);
assert!(has_event);
assert_eq!(what, 1);
assert_eq!((where_v, where_h), (70, 150));
assert_eq!(
disp.retained_modal_dialog_click
.as_ref()
.map(|click| (click.item_no, click.delivered_to_app)),
Some((1, true))
);
assert_eq!(
bus.read_byte(probe),
238,
"app-owned modal button should still show the pressed state"
);
assert_eq!(disp.front_window, dialog_ptr);
assert!(
disp.dialog_items.contains_key(&dialog_ptr),
"application-owned dialog should not be closed by event dequeue"
);
}
#[test]
fn app_owned_modal_dialog_button_mouse_up_dismisses_after_delivery() {
// Some apps drive modal DLOGs with their own WaitNextEvent loop and
// only ask the Window Manager which window was clicked. Keep the
// mouseDown deliverable, but finish the standard modal button press
// on mouseUp if app code has not called DialogSelect.
// Macintosh Toolbox Essentials 1992, pp. 6-136, 6-138..6-141.
let (mut disp, mut cpu, mut bus) = setup();
let dialog_ptr = bus.alloc(170);
let prev_window = bus.alloc(170);
let screen_base = 0x300000u32;
let row_bytes = 320u32;
disp.set_screen_mode_for_test(screen_base, row_bytes, 320, 200, 8);
let probe = screen_base + 70 * row_bytes + 150;
bus.write_byte(probe, 17);
seed_app_owned_modal_dialog(&mut disp, &mut bus, dialog_ptr, prev_window, 1);
disp.push_mouse_down(70, 150);
let (what, _message, where_v, where_h, _modifiers, has_event) =
disp.dequeue_toolbox_event(&mut cpu, &mut bus, 0xFFFF);
assert!(has_event);
assert_eq!(what, 1);
assert_eq!((where_v, where_h), (70, 150));
assert_eq!(disp.front_window, dialog_ptr);
disp.push_mouse_up(70, 150);
let (what, _message, where_v, where_h, _modifiers, has_event) =
disp.dequeue_toolbox_event(&mut cpu, &mut bus, 0xFFFF);
assert!(
has_event,
"app-owned modal mouseUp remains deliverable after compatibility close"
);
assert_eq!(what, 2);
assert_eq!((where_v, where_h), (70, 150));
assert_eq!(bus.read_byte(probe), 17);
assert_eq!(disp.front_window, prev_window);
assert!(!disp.dialog_items.contains_key(&dialog_ptr));
assert!(disp.retained_modal_dialog_click.is_none());
assert!(
disp.event_queue
.iter()
.any(|event| event.what == 6 && event.message == prev_window),
"closing the dialog must invalidate/update the exposed window"
);
}
#[test]
fn retained_modal_dialog_button_click_highlights_then_dismisses() {
let (mut disp, mut cpu, mut bus) = setup();
let dialog_ptr = bus.alloc(170);
let prev_window = bus.alloc(170);
let screen_base = 0x300000u32;
let row_bytes = 320u32;
disp.set_screen_mode_for_test(screen_base, row_bytes, 320, 200, 8);
seed_retained_modal_dialog(&mut disp, &mut bus, dialog_ptr, prev_window, 1);
let probe = screen_base + 70 * row_bytes + 150;
bus.write_byte(probe, 17);
let bounds = TrapDispatcher::dialog_screen_bounds(&bus, dialog_ptr);
assert_eq!(bounds, (0, 0, 100, 220));
let base_pixels = disp.save_dialog_pixels(&bus, bounds);
disp.dialog_visible_snapshots.insert(
dialog_ptr,
PersistentDialogSnapshot {
bounds,
pixels: base_pixels,
},
);
let items = disp.dialog_items.get(&dialog_ptr).cloned().unwrap();
assert_eq!(
disp.dialog_item_hit_test(
&bus,
&items,
bounds,
70,
150,
&disp.dialog_popup_original_rects,
dialog_ptr,
),
1
);
disp.push_mouse_down(70, 150);
let (_what, _message, _where_v, _where_h, _modifiers, has_event) =
disp.dequeue_toolbox_event(&mut cpu, &mut bus, 0xFFFF);
assert!(!has_event);
assert_eq!(
disp.retained_modal_dialog_click
.as_ref()
.map(|click| click.item_no),
Some(1)
);
assert_eq!(
bus.read_byte(probe),
238,
"button interior should invert on mouseDown"
);
disp.restore_visible_dialog_snapshots(&mut bus);
disp.redraw_retained_modal_dialog_click(&mut bus);
assert_eq!(
bus.read_byte(probe),
238,
"pressed retained-modal button must survive visible-dialog snapshot redraw"
);
assert_eq!(disp.front_window, dialog_ptr);
disp.push_mouse_up(70, 150);
let (_what, _message, _where_v, _where_h, _modifiers, has_event) =
disp.dequeue_toolbox_event(&mut cpu, &mut bus, 0xFFFF);
assert!(!has_event);
assert_eq!(
bus.read_byte(probe),
17,
"button should be unhighlighted before close"
);
assert_eq!(disp.front_window, prev_window);
assert!(!disp.dialog_items.contains_key(&dialog_ptr));
assert!(disp.retained_modal_dialog_click.is_none());
assert!(
!disp
.event_queue
.iter()
.any(|event| matches!(event.what, 1 | 2)),
"consumed dialog click must not leave mouse events for the game loop"
);
assert!(
disp.event_queue
.iter()
.any(|event| event.what == 6 && event.message == prev_window),
"closing the dialog must invalidate/update the exposed window"
);
}
#[test]
fn modal_dialog_button_tracking_systemless_theme_routes_pressed_state_through_provider() {
let (mut disp, _cpu, mut bus) = setup();
let screen_base = 0x300000u32;
let row_bytes = 64u32;
let bounds = (0, 0, 100, 220);
let item_rect = (60, 120, 80, 180);
let screen_rect = TrapDispatcher::dialog_item_screen_rect(bounds, item_rect);
disp.set_ui_theme_id(UiThemeId::SystemlessDefault);
disp.set_screen_mode_for_test(screen_base, row_bytes, 512, 342, 1);
disp.draw_button(
&mut bus,
screen_rect.0,
screen_rect.1,
screen_rect.2,
screen_rect.3,
"",
true,
);
let probe_x = 150;
let probe_y = 70;
assert!(
!screen_pixel_is_set(&bus, screen_base, row_bytes, probe_x, probe_y),
"unpressed provider button interior should start clear"
);
let dialog_ptr = bus.alloc(170);
disp.dialog_tracking = Some(DialogTrackingState {
dialog_ptr,
bounds,
items: vec![DialogItem {
item_type: 4,
rect: item_rect,
text: String::new(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
}],
default_item: 1,
active_button: Some(DialogButtonTrackingState {
item_no: 1,
rect: item_rect,
title: String::new(),
is_default: true,
highlighted: false,
}),
..Default::default()
});
disp.mouse_button = true;
disp.mouse_pos = (probe_y, probe_x);
disp.handle_dialog_button_tracking(&mut bus);
assert!(
screen_pixel_is_set(&bus, screen_base, row_bytes, probe_x, probe_y),
"inside tracking should paint the provider pressed fill"
);
assert_eq!(
disp.dialog_tracking
.as_ref()
.and_then(|tracking| tracking.active_button.as_ref())
.map(|button| button.highlighted),
Some(true)
);
disp.mouse_pos = (40, 50);
disp.handle_dialog_button_tracking(&mut bus);
assert!(
!screen_pixel_is_set(&bus, screen_base, row_bytes, probe_x, probe_y),
"outside tracking should redraw unpressed provider chrome"
);
assert_eq!(
disp.dialog_tracking
.as_ref()
.and_then(|tracking| tracking.active_button.as_ref())
.map(|button| button.highlighted),
Some(false)
);
}
#[test]
fn retained_modal_dialog_capture_does_not_apply_to_modeless_dialogs() {
let (mut disp, mut cpu, mut bus) = setup();
let dialog_ptr = bus.alloc(170);
let prev_window = bus.alloc(170);
seed_retained_modal_dialog(&mut disp, &mut bus, dialog_ptr, prev_window, 4);
disp.push_mouse_down(70, 150);
let (what, _message, where_v, where_h, _modifiers, has_event) =
disp.dequeue_toolbox_event(&mut cpu, &mut bus, 0xFFFF);
assert!(has_event);
assert_eq!(what, 1);
assert_eq!((where_v, where_h), (70, 150));
assert!(disp.retained_modal_dialog_click.is_none());
assert_eq!(disp.front_window, dialog_ptr);
}
#[test]
fn dispos_dialog_clears_tracking_for_disposed_dialog() {
let (mut disp, mut cpu, mut bus) = setup();
let dialog_ptr = 0x200000u32;
disp.dialog_tracking = Some(crate::trap::dispatch::DialogTrackingState {
dialog_ptr,
bounds: (0, 0, 32, 32),
title: String::new(),
proc_id: 1,
items: Vec::new(),
default_item: 0,
cancel_item: 0,
edit_text: String::new(),
edit_item: 0,
saved_pixels: Vec::new(),
stack_ptr: 0,
item_hit_ptr: 0,
rendered_pixels: Vec::new(),
flash_remaining: 0,
flash_delay: 0,
flash_item: 0,
edit_text_modified: false,
draw_proc_queue: VecDeque::new(),
draw_procs_done: true,
rendered_pixels_final: true,
filter_proc: 0,
game_managed: false,
last_filter_event: None,
popup_draws: Vec::new(),
active_popup: None,
active_button: None,
active_user_item: None,
});
bus.write_long(TEST_SP, dialog_ptr);
let result = disp.dispatch_dialog(true, 0x183, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert!(disp.dialog_tracking.is_none());
}
#[test]
fn dispos_dialog_clears_dialog_scoped_side_maps_for_disposed_dialog() {
let (mut disp, mut cpu, mut bus) = setup();
let dialog_ptr = 0x200000u32;
let other_dialog_ptr = 0x210000u32;
let text_handle = 0x300000u32;
let other_text_handle = 0x300010u32;
let ctrl_handle = 0x300020u32;
let other_ctrl_handle = 0x300030u32;
disp.dialog_items
.insert(dialog_ptr, vec![DialogItem::default()]);
disp.dialog_items
.insert(other_dialog_ptr, vec![DialogItem::default()]);
disp.dialog_item_handles
.insert(text_handle, (dialog_ptr, 0));
disp.dialog_item_handles
.insert(other_text_handle, (other_dialog_ptr, 0));
disp.dialog_control_handles
.insert(ctrl_handle, (dialog_ptr, 1));
disp.dialog_control_handles
.insert(other_ctrl_handle, (other_dialog_ptr, 1));
disp.dialog_control_values.insert((dialog_ptr, 1), 1);
disp.dialog_control_values.insert((other_dialog_ptr, 1), 1);
disp.hidden_dialog_item_rects
.insert((dialog_ptr, 1), (10, 20, 30, 40));
disp.hidden_dialog_item_rects
.insert((other_dialog_ptr, 1), (50, 60, 70, 80));
disp.dialog_item_popup_menus.insert((dialog_ptr, 1), 900);
disp.dialog_item_popup_menus
.insert((other_dialog_ptr, 1), 901);
disp.dialog_popup_original_rects
.insert((dialog_ptr, 1), (10, 20, 30, 130));
disp.dialog_popup_original_rects
.insert((other_dialog_ptr, 1), (50, 60, 70, 180));
disp.dialog_popup_candidate_items.insert((dialog_ptr, 1));
disp.dialog_popup_candidate_items
.insert((other_dialog_ptr, 1));
disp.dialog_cancel_items.insert(dialog_ptr, 2);
disp.dialog_cancel_items.insert(other_dialog_ptr, 3);
disp.pending_dialog_popup_menu = Some(PendingDialogPopupMenu {
dialog_ptr,
item_no: 1,
menu_id: 900,
rect: (10, 20, 30, 130),
});
bus.write_long(TEST_SP, dialog_ptr);
disp.dispatch_dialog(true, 0x183, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert!(!disp.dialog_items.contains_key(&dialog_ptr));
assert!(disp.dialog_items.contains_key(&other_dialog_ptr));
assert!(!disp.dialog_item_handles.contains_key(&text_handle));
assert!(disp.dialog_item_handles.contains_key(&other_text_handle));
assert!(!disp.dialog_control_handles.contains_key(&ctrl_handle));
assert!(disp.dialog_control_handles.contains_key(&other_ctrl_handle));
assert!(!disp.dialog_control_values.contains_key(&(dialog_ptr, 1)));
assert!(disp
.dialog_control_values
.contains_key(&(other_dialog_ptr, 1)));
assert!(!disp.hidden_dialog_item_rects.contains_key(&(dialog_ptr, 1)));
assert!(disp
.hidden_dialog_item_rects
.contains_key(&(other_dialog_ptr, 1)));
assert!(!disp.dialog_item_popup_menus.contains_key(&(dialog_ptr, 1)));
assert!(disp
.dialog_item_popup_menus
.contains_key(&(other_dialog_ptr, 1)));
assert!(!disp
.dialog_popup_original_rects
.contains_key(&(dialog_ptr, 1)));
assert!(disp
.dialog_popup_original_rects
.contains_key(&(other_dialog_ptr, 1)));
assert!(!disp.dialog_popup_candidate_items.contains(&(dialog_ptr, 1)));
assert!(disp
.dialog_popup_candidate_items
.contains(&(other_dialog_ptr, 1)));
assert!(!disp.dialog_cancel_items.contains_key(&dialog_ptr));
assert!(disp.dialog_cancel_items.contains_key(&other_dialog_ptr));
assert!(disp.pending_dialog_popup_menu.is_none());
}
// Regression: games that run their own event loop (e.g. Escape
// Velocity's "enter pilot/ship name" text dialogs) call
// GetNewDialog → custom event loop → DisposDialog without ever
// invoking ModalDialog. Before the fix, DisposDialog discarded
// the saved-background pixels without blitting them back to the
// screen, leaving a dialog-shaped hole over the window behind.
// IM:I I-425 says DisposDialog internally calls CloseWindow,
// whose PaintBehind/CalcVisBehind is supposed to restore the
// underlying content. These three tests pin that contract.
//
// Save/restore geometry note: save_dialog_pixels adds the dBoxProc
// structure margin around the bounds. The tests use bounds
// (100,100,150,200) → save area (92,92)..(158,208) =
// 66 rows × 116 cols = 7656 bytes.
#[test]
fn disposdialog_restores_saved_background_pixels() {
let (mut disp, mut cpu, mut bus) = setup();
// 8bpp test screen. Must be inside the 4MB test bus.
let screen_base = 0x300000u32;
let row_bytes: u32 = 640;
disp.set_screen_mode_for_test(screen_base, row_bytes, 640, 480, 8);
let bounds = (100i16, 100i16, 150i16, 200i16);
let dialog_ptr = 0x200000u32;
disp.front_window = dialog_ptr;
disp.window_bounds = bounds;
// Paint the save area with 0xCC — the "dialog pixels" that
// should be overwritten on dispose.
for y in 92u32..158 {
for x in 92u32..208 {
bus.write_byte(screen_base + y * row_bytes + x, 0xCC);
}
}
// Install a saved background snapshot filled with 0x33 (the
// "what was behind the dialog" pattern). 66*116=7656 bytes.
disp.dialog_saved_pixels
.insert(dialog_ptr, vec![0x33; 66 * 116]);
bus.write_long(TEST_SP, dialog_ptr);
let result = disp.dispatch_dialog(true, 0x183, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
// Every byte in the save area should now be 0x33, not 0xCC.
for y in 92u32..158 {
for x in 92u32..208 {
let addr = screen_base + y * row_bytes + x;
let got = bus.read_byte(addr);
assert_eq!(
got, 0x33,
"byte at ({},{}) must be restored background 0x33, got 0x{:02X}",
x, y, got
);
}
}
assert!(
!disp.dialog_saved_pixels.contains_key(&dialog_ptr),
"saved pixels must be consumed after DisposDialog"
);
}
#[test]
fn visible_dialog_saved_background_tracks_screen_draws_behind_it() {
// Some applications keep animating their screen-backed main window
// while a visible DLOG is frontmost. The Window Manager still closes
// the dialog via CloseWindow/DisposDialog (IM:I I-283, I-425), so the
// saved-under pixels must reflect those later behind-dialog draws.
let (mut disp, mut cpu, mut bus) = setup();
let screen_base = 0x300000u32;
let row_bytes: u32 = 640;
disp.set_screen_mode_for_test(screen_base, row_bytes, 640, 480, 8);
let bounds = (100i16, 100i16, 150i16, 200i16);
let dialog_ptr = bus.alloc(170);
let background_port = bus.alloc(170);
let save_top = 92u32;
let save_left = 92u32;
let save_bottom = 158u32;
let save_right = 208u32;
let row_width = (save_right - save_left) as usize;
for y in save_top..save_bottom {
for x in save_left..save_right {
bus.write_byte(screen_base + y * row_bytes + x, 0xEE);
}
}
disp.dialog_saved_pixels.insert(
dialog_ptr,
vec![0x11; row_width * (save_bottom - save_top) as usize],
);
disp.dialog_visible_snapshots.insert(
dialog_ptr,
PersistentDialogSnapshot {
bounds,
pixels: vec![0xEE; row_width * (save_bottom - save_top) as usize],
},
);
for y in 120u32..123 {
for x in 130u32..139 {
bus.write_byte(screen_base + y * row_bytes + x, 0x44);
}
}
disp.refresh_dialog_saved_pixels_after_screen_draw(
&bus,
background_port,
(120, 130, 123, 139),
);
let saved = disp.dialog_saved_pixels.get(&dialog_ptr).unwrap();
let touched_idx = (120 - save_top) as usize * row_width + (130 - save_left) as usize;
let untouched_idx = (100 - save_top) as usize * row_width + (100 - save_left) as usize;
assert_eq!(saved[touched_idx], 0x44);
assert_eq!(saved[untouched_idx], 0x11);
for y in save_top..save_bottom {
for x in save_left..save_right {
bus.write_byte(screen_base + y * row_bytes + x, 0xEE);
}
}
disp.front_window = dialog_ptr;
disp.current_port = dialog_ptr;
disp.window_bounds = bounds;
disp.window_list = vec![dialog_ptr];
disp.window_stack.push((0, (0, 0, 0, 0), -1, String::new()));
bus.write_long(TEST_SP, dialog_ptr);
disp.dispatch_dialog(true, 0x183, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(bus.read_byte(screen_base + 120 * row_bytes + 130), 0x44);
assert_eq!(bus.read_byte(screen_base + 100 * row_bytes + 100), 0x11);
}
#[test]
fn active_modal_dialog_saved_background_tracks_screen_draws_behind_it() {
// ModalDialog moves the visible dialog snapshot into dialog_tracking
// while the modal loop is active. Background screen draws behind that
// front modal still need to update the saved-under pixels that
// DisposDialog/CloseWindow will restore.
let (mut disp, _cpu, mut bus) = setup();
let screen_base = 0x300000u32;
let row_bytes: u32 = 640;
disp.set_screen_mode_for_test(screen_base, row_bytes, 640, 480, 8);
let bounds = (100i16, 100i16, 150i16, 200i16);
let dialog_ptr = bus.alloc(170);
let background_port = bus.alloc(170);
let save_top = 92u32;
let save_left = 92u32;
let save_bottom = 158u32;
let save_right = 208u32;
let row_width = (save_right - save_left) as usize;
for y in save_top..save_bottom {
for x in save_left..save_right {
bus.write_byte(screen_base + y * row_bytes + x, 0xEE);
}
}
disp.dialog_saved_pixels.insert(
dialog_ptr,
vec![0x11; row_width * (save_bottom - save_top) as usize],
);
let mut tracking = dialog_tracking_state_for_test(dialog_ptr);
tracking.bounds = bounds;
disp.dialog_tracking = Some(tracking);
for y in 120u32..123 {
for x in 130u32..139 {
bus.write_byte(screen_base + y * row_bytes + x, 0x44);
}
}
disp.refresh_dialog_saved_pixels_after_screen_draw(
&bus,
background_port,
(120, 130, 123, 139),
);
let saved = disp.dialog_saved_pixels.get(&dialog_ptr).unwrap();
let touched_idx = (120 - save_top) as usize * row_width + (130 - save_left) as usize;
let untouched_idx = (100 - save_top) as usize * row_width + (100 - save_left) as usize;
assert_eq!(saved[touched_idx], 0x44);
assert_eq!(saved[untouched_idx], 0x11);
}
#[test]
fn retained_modal_dialog_saved_background_tracks_same_port_screen_draws() {
// After ModalDialog returns with a retained visible modal, the app may
// immediately redraw the background before calling DisposDialog while
// the dialog remains the current screen-backed port. Treat those
// same-port screen writes as background, not as dialog content.
let (mut disp, _cpu, mut bus) = setup();
let screen_base = 0x300000u32;
let row_bytes: u32 = 640;
disp.set_screen_mode_for_test(screen_base, row_bytes, 640, 480, 8);
let bounds = (100i16, 100i16, 150i16, 200i16);
let dialog_ptr = bus.alloc(170);
let save_top = 92u32;
let save_left = 92u32;
let save_bottom = 158u32;
let save_right = 208u32;
let row_width = (save_right - save_left) as usize;
disp.dialog_saved_pixels.insert(
dialog_ptr,
vec![0x11; row_width * (save_bottom - save_top) as usize],
);
disp.dialog_visible_snapshots.insert(
dialog_ptr,
PersistentDialogSnapshot {
bounds,
pixels: vec![0xEE; row_width * (save_bottom - save_top) as usize],
},
);
let touched_idx = (120 - save_top) as usize * row_width + (130 - save_left) as usize;
bus.write_byte(screen_base + 120 * row_bytes + 130, 0x22);
disp.refresh_dialog_saved_pixels_after_screen_draw(&bus, dialog_ptr, (120, 130, 121, 131));
assert_eq!(
disp.dialog_saved_pixels.get(&dialog_ptr).unwrap()[touched_idx],
0x11,
"pre-modal same-port dialog drawing must not become saved-under background"
);
disp.dialog_modal_entered.insert(dialog_ptr);
bus.write_byte(screen_base + 120 * row_bytes + 130, 0x44);
disp.refresh_dialog_saved_pixels_after_screen_draw(&bus, dialog_ptr, (120, 130, 121, 131));
assert_eq!(
disp.dialog_saved_pixels.get(&dialog_ptr).unwrap()[touched_idx],
0x44,
"retained modal same-port background drawing should update saved-under pixels"
);
}
#[test]
fn stale_fullscreen_dialog_exposure_restores_from_offscreen_scene_port() {
let (mut disp, _cpu, mut bus) = setup();
let screen_base = bus.alloc(100 * 80);
bus.write_long(0x0824, screen_base);
disp.screen_mode = (screen_base, 100, 100, 80, 8);
disp.device_clut[0x22] = [0x3333, 0x7777, 0x2222];
disp.color_manager_clut = disp.device_clut;
let offscreen_base = bus.alloc(100 * 80);
bus.write_bytes(offscreen_base, &vec![0x22; 100 * 80]);
let port = bus.alloc(170);
let pixmap_handle = bus.alloc(4);
let pixmap_ptr = bus.alloc(50);
bus.write_word(port + 6, 0xC000);
bus.write_long(port + 2, pixmap_handle);
bus.write_long(pixmap_handle, pixmap_ptr);
bus.write_long(pixmap_ptr, offscreen_base);
bus.write_word(pixmap_ptr + 4, 0x8000 | 100);
bus.write_word(pixmap_ptr + 6, 0);
bus.write_word(pixmap_ptr + 8, 0);
bus.write_word(pixmap_ptr + 10, 80);
bus.write_word(pixmap_ptr + 12, 100);
bus.write_word(pixmap_ptr + 32, 8);
disp.cport_ports.insert(port);
bus.write_bytes(screen_base, &vec![0x00; 100 * 80]);
assert!(
disp.restore_dialog_exposure_from_fullscreen_offscreen_port(&mut bus, (20, 20, 50, 70))
);
assert_eq!(bus.read_byte(screen_base + 20 * 100 + 20), 0x22);
assert_eq!(bus.read_byte(screen_base + 49 * 100 + 69), 0x22);
assert_eq!(bus.read_byte(screen_base + 2 * 100 + 2), 0x00);
}
#[test]
fn disposdialog_restore_is_bounded_by_save_margin() {
// Pin that the restore writes exactly the dBoxProc structure-margin
// rectangle and does not stomp adjacent bytes. This catches
// off-by-one errors in the save/restore geometry.
let (mut disp, mut cpu, mut bus) = setup();
let screen_base = 0x300000u32;
let row_bytes: u32 = 640;
disp.set_screen_mode_for_test(screen_base, row_bytes, 640, 480, 8);
let bounds = (100i16, 100i16, 150i16, 200i16);
let dialog_ptr = 0x200000u32;
disp.front_window = dialog_ptr;
disp.window_bounds = bounds;
// Paint the entire screen area of interest (including a
// generous border around the save rect) with 0xAA.
for y in 85u32..165 {
for x in 85u32..215 {
bus.write_byte(screen_base + y * row_bytes + x, 0xAA);
}
}
disp.dialog_saved_pixels
.insert(dialog_ptr, vec![0x33; 66 * 116]);
bus.write_long(TEST_SP, dialog_ptr);
disp.dispatch_dialog(true, 0x183, &mut cpu, &mut bus)
.unwrap()
.unwrap();
// Bytes OUTSIDE the save area must still be 0xAA.
// Above the save rect:
for x in 85u32..215 {
assert_eq!(bus.read_byte(screen_base + 91 * row_bytes + x), 0xAA);
}
// Below the save rect:
for x in 85u32..215 {
assert_eq!(bus.read_byte(screen_base + 158 * row_bytes + x), 0xAA);
}
// Left of the save rect:
for y in 85u32..165 {
assert_eq!(bus.read_byte(screen_base + y * row_bytes + 91), 0xAA);
}
// Right of the save rect:
for y in 85u32..165 {
assert_eq!(bus.read_byte(screen_base + y * row_bytes + 208), 0xAA);
}
// Bytes INSIDE the save area must now be 0x33.
for y in 92u32..158 {
for x in 92u32..208 {
assert_eq!(bus.read_byte(screen_base + y * row_bytes + x), 0x33);
}
}
}
#[test]
fn disposdialog_without_saved_pixels_leaves_screen_untouched() {
// If no saved pixels exist for this dialog (e.g. ModalDialog
// already consumed them on flash completion), DisposDialog
// must be a no-op on the screen — no panic, no accidental
// fill.
let (mut disp, mut cpu, mut bus) = setup();
let screen_base = 0x300000u32;
let row_bytes: u32 = 640;
disp.set_screen_mode_for_test(screen_base, row_bytes, 640, 480, 8);
let bounds = (100i16, 100i16, 150i16, 200i16);
let dialog_ptr = 0x200000u32;
disp.front_window = dialog_ptr;
disp.window_bounds = bounds;
for y in 95u32..155 {
for x in 95u32..205 {
bus.write_byte(screen_base + y * row_bytes + x, 0x77);
}
}
assert!(!disp.dialog_saved_pixels.contains_key(&dialog_ptr));
bus.write_long(TEST_SP, dialog_ptr);
disp.dispatch_dialog(true, 0x183, &mut cpu, &mut bus)
.unwrap()
.unwrap();
for y in 95u32..155 {
for x in 95u32..205 {
let addr = screen_base + y * row_bytes + x;
assert_eq!(
bus.read_byte(addr),
0x77,
"DisposDialog without saved pixels must not write to screen"
);
}
}
}
#[test]
fn disposdialog_non_front_does_not_restore() {
// If the dialog being disposed is NOT the front window, we
// don't know its correct bounds (self.window_bounds belongs
// to whatever is currently front). Restoring at the wrong
// coords would corrupt the screen over the actual front
// window. Expected behavior: discard saved pixels silently.
let (mut disp, mut cpu, mut bus) = setup();
let screen_base = 0x300000u32;
let row_bytes: u32 = 640;
disp.set_screen_mode_for_test(screen_base, row_bytes, 640, 480, 8);
let dialog_ptr = 0x200000u32;
let other_front = 0x280000u32;
disp.front_window = other_front; // dialog_ptr is NOT front
disp.window_bounds = (200, 200, 300, 400); // matches other_front
for y in 95u32..155 {
for x in 95u32..205 {
bus.write_byte(screen_base + y * row_bytes + x, 0x55);
}
}
disp.dialog_saved_pixels
.insert(dialog_ptr, vec![0x99; 66 * 116]);
bus.write_long(TEST_SP, dialog_ptr);
disp.dispatch_dialog(true, 0x183, &mut cpu, &mut bus)
.unwrap()
.unwrap();
for y in 95u32..155 {
for x in 95u32..205 {
let addr = screen_base + y * row_bytes + x;
assert_eq!(
bus.read_byte(addr),
0x55,
"non-front DisposDialog must not restore over current front window"
);
}
}
assert!(
!disp.dialog_saved_pixels.contains_key(&dialog_ptr),
"saved pixels must still be removed for non-front dispose"
);
}
// ---- ParamText ($A98B) ----
fn write_pascal_str(bus: &mut crate::memory::MacMemoryBus, addr: u32, s: &[u8]) {
bus.write_byte(addr, s.len() as u8);
for (i, b) in s.iter().enumerate() {
bus.write_byte(addr + 1 + i as u32, *b);
}
}
#[test]
fn param_text_saves_all_four_strings_and_pops_16_bytes() {
let (mut disp, mut cpu, mut bus) = setup();
let p0 = 0x300000u32;
let p1 = 0x300100u32;
let p2 = 0x300200u32;
let p3 = 0x300300u32;
write_pascal_str(&mut bus, p0, b"alpha");
write_pascal_str(&mut bus, p1, b"bravo");
write_pascal_str(&mut bus, p2, b"chi");
write_pascal_str(&mut bus, p3, b"d");
bus.write_long(TEST_SP, p3);
bus.write_long(TEST_SP + 4, p2);
bus.write_long(TEST_SP + 8, p1);
bus.write_long(TEST_SP + 12, p0);
disp.dispatch_dialog(true, 0x18B, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 16);
assert_eq!(disp.param_text[0], b"alpha");
assert_eq!(disp.param_text[1], b"bravo");
assert_eq!(disp.param_text[2], b"chi");
assert_eq!(disp.param_text[3], b"d");
}
#[test]
fn param_text_empty_pascal_string_clears_slot() {
// Per Inside Macintosh Volume I, I-422, an empty Pascal string
// (length-byte = 0) IS a valid value — the caret placeholder
// gets replaced by nothing. This is the explicit "clear this
// slot" idiom. Distinguishes from NIL (preserves prior).
let (mut disp, mut cpu, mut bus) = setup();
disp.param_text[0] = b"stale".to_vec();
disp.param_text[1] = b"stale".to_vec();
disp.param_text[2] = b"stale".to_vec();
disp.param_text[3] = b"stale".to_vec();
let empty_ptr = 0x300000u32;
write_pascal_str(&mut bus, empty_ptr, b"");
for off in [0u32, 4, 8, 12] {
bus.write_long(TEST_SP + off, empty_ptr);
}
disp.dispatch_dialog(true, 0x18B, &mut cpu, &mut bus)
.unwrap()
.unwrap();
for i in 0..4 {
assert_eq!(
disp.param_text[i],
Vec::<u8>::new(),
"empty Pascal string must clear slot {}",
i
);
}
}
#[test]
fn apply_param_text_substitutes_caret_placeholders() {
let (mut disp, _cpu, _bus) = setup();
disp.param_text[0] = b"MS UserKey".to_vec();
disp.param_text[1] = b"42".to_vec();
assert_eq!(
disp.apply_param_text("Unable to open the \"^0\" file."),
"Unable to open the \"MS UserKey\" file."
);
assert_eq!(disp.apply_param_text("count: ^1"), "count: 42");
assert_eq!(
disp.apply_param_text("plain text without placeholders"),
"plain text without placeholders"
);
assert_eq!(disp.apply_param_text("^0 ^1 ^2 ^3"), "MS UserKey 42 ");
assert_eq!(
disp.apply_param_text("^A literal caret"),
"^A literal caret"
);
// Edge cases: lone trailing caret, double caret, caret at boundary,
// out-of-range digit (^9 has no slot 9 → kept literal).
assert_eq!(disp.apply_param_text("trailing^"), "trailing^");
assert_eq!(disp.apply_param_text("^^0"), "^MS UserKey");
assert_eq!(disp.apply_param_text("^9 unknown slot"), "^9 unknown slot");
assert_eq!(disp.apply_param_text(""), "");
}
#[test]
fn param_text_nil_pointer_preserves_previous_slot_value() {
// Per Inside Macintosh Volume I, I-422, passing NIL for any
// ParamText slot must leave the prior value unchanged — apps
// commonly stage one parameter at a time before opening an
// alert, expecting the others to retain whatever they were
// last set to.
let (mut disp, mut cpu, mut bus) = setup();
disp.param_text[0] = b"old0".to_vec();
disp.param_text[1] = b"old1".to_vec();
disp.param_text[2] = b"old2".to_vec();
disp.param_text[3] = b"old3".to_vec();
let new0 = 0x300000u32;
write_pascal_str(&mut bus, new0, b"new0");
bus.write_long(TEST_SP, 0); // param3 = NIL
bus.write_long(TEST_SP + 4, 0); // param2 = NIL
bus.write_long(TEST_SP + 8, 0); // param1 = NIL
bus.write_long(TEST_SP + 12, new0);
disp.dispatch_dialog(true, 0x18B, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(disp.param_text[0], b"new0", "param0 should be replaced");
assert_eq!(disp.param_text[1], b"old1", "NIL must preserve param1");
assert_eq!(disp.param_text[2], b"old2", "NIL must preserve param2");
assert_eq!(disp.param_text[3], b"old3", "NIL must preserve param3");
}
#[test]
fn apply_param_text_returns_borrowed_when_no_placeholders() {
// Pin the no-allocation contract: the common case (DITL text
// without any `^N` placeholders) must return Cow::Borrowed so
// draw_static_text doesn't allocate per item.
use std::borrow::Cow;
let (mut disp, _cpu, _bus) = setup();
disp.param_text[0] = b"value".to_vec();
let plain = disp.apply_param_text("hello world");
assert!(
matches!(plain, Cow::Borrowed(_)),
"no-placeholder input must skip the allocation path"
);
let substituted = disp.apply_param_text("hello ^0");
assert!(
matches!(substituted, Cow::Owned(_)),
"with-placeholder input takes the allocation path"
);
assert_eq!(substituted, "hello value");
}
// ---- GetDItem ($A98D) ----
#[test]
fn get_ditem_clears_outputs_and_pops_18_bytes() {
let (mut disp, mut cpu, mut bus) = setup();
// Set up output pointers
let box_ptr = 0x300000u32;
let item_ptr = 0x300100u32;
let type_ptr = 0x300200u32;
// Pre-fill output locations with non-zero to verify they get cleared
bus.write_long(box_ptr, 0xDEADBEEF);
bus.write_long(box_ptr + 4, 0xDEADBEEF);
bus.write_long(item_ptr, 0xDEADBEEF);
bus.write_word(type_ptr, 0xBEEF);
// Stack layout: SP+0: box(4), SP+4: item(4), SP+8: type(4), SP+12: itemNo(2), SP+14: dialog(4)
bus.write_long(TEST_SP, box_ptr);
bus.write_long(TEST_SP + 4, item_ptr);
bus.write_long(TEST_SP + 8, type_ptr);
bus.write_word(TEST_SP + 12, 1); // item number
bus.write_long(TEST_SP + 14, 0x200000); // dialog ptr
let result = disp.dispatch_dialog(true, 0x18D, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 18);
// Verify outputs were cleared to 0
assert_eq!(bus.read_word(type_ptr), 0);
assert_eq!(bus.read_long(item_ptr), 0);
assert_eq!(bus.read_long(box_ptr), 0);
assert_eq!(bus.read_long(box_ptr + 4), 0);
}
#[test]
fn get_ditem_text_handle_returns_existing_raw_text_handle() {
let (mut disp, mut cpu, mut bus) = setup();
let dialog_ptr = bus.alloc(170);
let box_ptr = 0x300000u32;
let item_ptr = 0x300100u32;
let type_ptr = 0x300200u32;
let text_handle = bus.alloc(4);
let text_ptr = bus.alloc(5);
let items_handle = bus.alloc(4);
let ditl_ptr = bus.alloc(22);
bus.write_bytes(text_ptr, b"Hello");
bus.write_long(text_handle, text_ptr);
bus.write_long(items_handle, ditl_ptr);
bus.write_long(dialog_ptr + 156, items_handle);
bus.write_word(ditl_ptr, 0);
bus.write_long(ditl_ptr + 2, text_handle);
bus.write_word(ditl_ptr + 6, 10);
bus.write_word(ditl_ptr + 8, 20);
bus.write_word(ditl_ptr + 10, 30);
bus.write_word(ditl_ptr + 12, 40);
bus.write_byte(ditl_ptr + 14, 8);
bus.write_byte(ditl_ptr + 15, 5);
bus.write_bytes(ditl_ptr + 16, b"Hello");
disp.dialog_items.insert(
dialog_ptr,
vec![DialogItem {
item_type: 8,
rect: (10, 20, 30, 40),
text: "Hello".to_string(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
}],
);
bus.write_long(TEST_SP, box_ptr);
bus.write_long(TEST_SP + 4, item_ptr);
bus.write_long(TEST_SP + 8, type_ptr);
bus.write_word(TEST_SP + 12, 1);
bus.write_long(TEST_SP + 14, dialog_ptr);
let result = disp.dispatch_dialog(true, 0x18D, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_long(item_ptr), text_handle);
assert_eq!(bus.read_long(ditl_ptr + 2), text_handle);
assert_eq!(bus.get_alloc_size(text_ptr), Some(5));
assert_eq!(bus.read_bytes(text_ptr, 5), b"Hello".to_vec());
}
#[test]
fn get_ditem_records_pending_popup_candidate_without_associating() {
let (mut disp, mut cpu, mut bus) = setup();
let dialog_ptr = bus.alloc(170);
let box_ptr = bus.alloc(8);
let item_ptr = bus.alloc(4);
let type_ptr = bus.alloc(2);
disp.dialog_items.insert(
dialog_ptr,
vec![DialogItem {
item_type: 0,
rect: (10, 20, 30, 40),
text: String::new(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
}],
);
disp.last_inserted_menu_id = Some(1234);
bus.write_long(TEST_SP, box_ptr);
bus.write_long(TEST_SP + 4, item_ptr);
bus.write_long(TEST_SP + 8, type_ptr);
bus.write_word(TEST_SP + 12, 1);
bus.write_long(TEST_SP + 14, dialog_ptr);
disp.dispatch_dialog(true, 0x18D, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert!(!disp.dialog_item_popup_menus.contains_key(&(dialog_ptr, 1)));
assert!(!disp
.dialog_popup_original_rects
.contains_key(&(dialog_ptr, 1)));
let pending = disp
.pending_dialog_popup_menu
.expect("enabled userItem should leave a pending popup candidate");
assert_eq!(pending.dialog_ptr, dialog_ptr);
assert_eq!(pending.item_no, 1);
assert_eq!(pending.menu_id, 1234);
assert_eq!(pending.rect, (10, 20, 30, 40));
assert_eq!(disp.last_inserted_menu_id, None);
}
#[test]
fn get_ditem_ignores_recent_inserted_menu_for_disabled_user_item() {
let (mut disp, mut cpu, mut bus) = setup();
let dialog_ptr = bus.alloc(170);
let box_ptr = bus.alloc(8);
let item_ptr = bus.alloc(4);
let type_ptr = bus.alloc(2);
disp.dialog_items.insert(
dialog_ptr,
vec![DialogItem {
item_type: 0x80,
rect: (10, 20, 30, 40),
text: String::new(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
}],
);
disp.last_inserted_menu_id = Some(1234);
bus.write_long(TEST_SP, box_ptr);
bus.write_long(TEST_SP + 4, item_ptr);
bus.write_long(TEST_SP + 8, type_ptr);
bus.write_word(TEST_SP + 12, 1);
bus.write_long(TEST_SP + 14, dialog_ptr);
disp.dispatch_dialog(true, 0x18D, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert!(!disp.dialog_item_popup_menus.contains_key(&(dialog_ptr, 1)));
assert!(!disp
.dialog_popup_original_rects
.contains_key(&(dialog_ptr, 1)));
assert!(disp.pending_dialog_popup_menu.is_none());
assert_eq!(disp.last_inserted_menu_id, None);
}
#[test]
fn set_ditem_confirms_pending_popup_user_item_when_proc_installed() {
let (mut disp, mut cpu, mut bus) = setup();
let dialog_ptr = bus.alloc(170);
let get_box_ptr = bus.alloc(8);
let get_item_ptr = bus.alloc(4);
let get_type_ptr = bus.alloc(2);
let set_box_ptr = bus.alloc(8);
let proc_ptr = 0x00AB_CDEF;
disp.dialog_items.insert(
dialog_ptr,
vec![DialogItem {
item_type: 0,
rect: (10, 20, 30, 40),
text: String::new(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
}],
);
disp.last_inserted_menu_id = Some(1234);
bus.write_long(TEST_SP, get_box_ptr);
bus.write_long(TEST_SP + 4, get_item_ptr);
bus.write_long(TEST_SP + 8, get_type_ptr);
bus.write_word(TEST_SP + 12, 1);
bus.write_long(TEST_SP + 14, dialog_ptr);
disp.dispatch_dialog(true, 0x18D, &mut cpu, &mut bus)
.unwrap()
.unwrap();
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(set_box_ptr, 12);
bus.write_word(set_box_ptr + 2, 22);
bus.write_word(set_box_ptr + 4, 32);
bus.write_word(set_box_ptr + 6, 42);
bus.write_long(TEST_SP, set_box_ptr);
bus.write_long(TEST_SP + 4, proc_ptr);
bus.write_word(TEST_SP + 8, 0);
bus.write_word(TEST_SP + 10, 1);
bus.write_long(TEST_SP + 12, dialog_ptr);
disp.dispatch_dialog(true, 0x18E, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(
disp.dialog_item_popup_menus.get(&(dialog_ptr, 1)),
Some(&1234)
);
assert_eq!(
disp.dialog_popup_original_rects.get(&(dialog_ptr, 1)),
Some(&(10, 20, 30, 40))
);
assert!(disp.pending_dialog_popup_menu.is_none());
}
#[test]
fn set_ditem_narrowing_user_item_records_popup_candidate_without_proc() {
let (mut disp, mut cpu, mut bus) = setup();
let dialog_ptr = bus.alloc(170);
let set_box_ptr = bus.alloc(8);
disp.dialog_items.insert(
dialog_ptr,
vec![DialogItem {
item_type: 0,
rect: (10, 20, 30, 150),
text: String::new(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
}],
);
bus.write_word(set_box_ptr, 10);
bus.write_word(set_box_ptr + 2, 20);
bus.write_word(set_box_ptr + 4, 30);
bus.write_word(set_box_ptr + 6, 35);
bus.write_long(TEST_SP, set_box_ptr);
bus.write_long(TEST_SP + 4, 0);
bus.write_word(TEST_SP + 8, 0);
bus.write_word(TEST_SP + 10, 1);
bus.write_long(TEST_SP + 12, dialog_ptr);
disp.dispatch_dialog(true, 0x18E, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert!(!disp.dialog_item_popup_menus.contains_key(&(dialog_ptr, 1)));
assert_eq!(
disp.dialog_popup_original_rects.get(&(dialog_ptr, 1)),
Some(&(10, 20, 30, 150))
);
assert!(disp.dialog_popup_candidate_items.contains(&(dialog_ptr, 1)));
assert_eq!(
disp.dialog_items
.get(&dialog_ptr)
.and_then(|items| items.first())
.map(|item| item.rect),
Some((10, 20, 30, 35))
);
}
// ---- SetDItem ($A98E) ----
#[test]
fn set_ditem_pops_16_bytes() {
let (mut disp, mut cpu, mut bus) = setup();
let result = disp.dispatch_dialog(true, 0x18E, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 16);
}
#[test]
fn set_ditem_updates_item_fields_visible_through_get_ditem() {
// Inside Macintosh Volume I, I-421: SetDItem changes itemType, item,
// and box for the specified item; GetDItem must then report those
// updated fields.
let (mut disp, mut cpu, mut bus) = setup();
let dialog_ptr = bus.alloc(170);
let items_handle = bus.alloc(4);
let ditl_ptr = bus.alloc(22);
let old_handle = bus.alloc(4);
let old_ptr = bus.alloc(4);
let new_handle = bus.alloc(4);
let new_ptr = bus.alloc(3);
let set_box_ptr = bus.alloc(8);
let get_box_ptr = bus.alloc(8);
let get_item_ptr = bus.alloc(4);
let get_type_ptr = bus.alloc(2);
bus.write_bytes(old_ptr, b"Old!");
bus.write_bytes(new_ptr, b"New");
bus.write_long(old_handle, old_ptr);
bus.write_long(new_handle, new_ptr);
bus.write_long(items_handle, ditl_ptr);
bus.write_long(dialog_ptr + 156, items_handle);
bus.write_word(ditl_ptr, 0); // one item
bus.write_long(ditl_ptr + 2, old_handle);
bus.write_word(ditl_ptr + 6, 10);
bus.write_word(ditl_ptr + 8, 20);
bus.write_word(ditl_ptr + 10, 30);
bus.write_word(ditl_ptr + 12, 40);
bus.write_byte(ditl_ptr + 14, 8);
bus.write_byte(ditl_ptr + 15, 4);
bus.write_bytes(ditl_ptr + 16, b"Old!");
disp.dialog_items.insert(
dialog_ptr,
vec![DialogItem {
item_type: 8,
rect: (10, 20, 30, 40),
text: "Old!".to_string(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
}],
);
disp.dialog_item_handles.insert(old_handle, (dialog_ptr, 0));
bus.write_word(set_box_ptr, 111);
bus.write_word(set_box_ptr + 2, 222);
bus.write_word(set_box_ptr + 4, 333);
bus.write_word(set_box_ptr + 6, 444);
bus.write_long(TEST_SP, set_box_ptr);
bus.write_long(TEST_SP + 4, new_handle);
bus.write_word(TEST_SP + 8, 16);
bus.write_word(TEST_SP + 10, 1);
bus.write_long(TEST_SP + 12, dialog_ptr);
disp.dispatch_dialog(true, 0x18E, &mut cpu, &mut bus)
.unwrap()
.unwrap();
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, get_box_ptr);
bus.write_long(TEST_SP + 4, get_item_ptr);
bus.write_long(TEST_SP + 8, get_type_ptr);
bus.write_word(TEST_SP + 12, 1);
bus.write_long(TEST_SP + 14, dialog_ptr);
disp.dispatch_dialog(true, 0x18D, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(bus.read_word(get_type_ptr), 16);
assert_eq!(bus.read_long(get_item_ptr), new_handle);
assert_eq!(bus.read_word(get_box_ptr), 111);
assert_eq!(bus.read_word(get_box_ptr + 2), 222);
assert_eq!(bus.read_word(get_box_ptr + 4), 333);
assert_eq!(bus.read_word(get_box_ptr + 6), 444);
}
#[test]
fn set_ditem_user_item_treats_item_parameter_as_proc_ptr() {
// Inside Macintosh Volume I, I-421: for userItem, SetDItem's `item`
// parameter is a draw procedure pointer (ProcPtr), not a text/control
// handle. GetDItem should report the same ProcPtr value.
let (mut disp, mut cpu, mut bus) = setup();
let dialog_ptr = bus.alloc(170);
let items_handle = bus.alloc(4);
let ditl_ptr = bus.alloc(16);
let proc_ptr = 0x00AB_CDEFu32;
let set_box_ptr = bus.alloc(8);
let get_box_ptr = bus.alloc(8);
let get_item_ptr = bus.alloc(4);
let get_type_ptr = bus.alloc(2);
bus.write_long(items_handle, ditl_ptr);
bus.write_long(dialog_ptr + 156, items_handle);
bus.write_word(ditl_ptr, 0); // one item
bus.write_long(ditl_ptr + 2, 0);
bus.write_word(ditl_ptr + 6, 10);
bus.write_word(ditl_ptr + 8, 20);
bus.write_word(ditl_ptr + 10, 30);
bus.write_word(ditl_ptr + 12, 40);
bus.write_byte(ditl_ptr + 14, 0);
bus.write_byte(ditl_ptr + 15, 0);
disp.dialog_items.insert(
dialog_ptr,
vec![DialogItem {
item_type: 0,
rect: (10, 20, 30, 40),
text: String::new(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
}],
);
bus.write_word(set_box_ptr, 50);
bus.write_word(set_box_ptr + 2, 60);
bus.write_word(set_box_ptr + 4, 70);
bus.write_word(set_box_ptr + 6, 80);
bus.write_long(TEST_SP, set_box_ptr);
bus.write_long(TEST_SP + 4, proc_ptr);
bus.write_word(TEST_SP + 8, 0);
bus.write_word(TEST_SP + 10, 1);
bus.write_long(TEST_SP + 12, dialog_ptr);
disp.dispatch_dialog(true, 0x18E, &mut cpu, &mut bus)
.unwrap()
.unwrap();
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, get_box_ptr);
bus.write_long(TEST_SP + 4, get_item_ptr);
bus.write_long(TEST_SP + 8, get_type_ptr);
bus.write_word(TEST_SP + 12, 1);
bus.write_long(TEST_SP + 14, dialog_ptr);
disp.dispatch_dialog(true, 0x18D, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(bus.read_word(get_type_ptr), 0);
assert_eq!(bus.read_long(get_item_ptr), proc_ptr);
assert_eq!(bus.read_word(get_box_ptr), 50);
assert_eq!(bus.read_word(get_box_ptr + 2), 60);
assert_eq!(bus.read_word(get_box_ptr + 4), 70);
assert_eq!(bus.read_word(get_box_ptr + 6), 80);
}
#[test]
fn set_dialog_item_text_resizes_existing_text_handle() {
let (mut disp, mut cpu, mut bus) = setup();
let dialog_ptr = 0x200000u32;
let text_handle = bus.alloc(4);
let text_ptr = bus.alloc(2);
let new_text_ptr = 0x300000u32;
bus.write_long(text_handle, text_ptr);
bus.write_byte(text_ptr, 1);
bus.write_byte(text_ptr + 1, b'A');
bus.write_byte(new_text_ptr, 5);
bus.write_bytes(new_text_ptr + 1, b"Hello");
disp.dialog_items.insert(
dialog_ptr,
vec![DialogItem {
item_type: 8,
rect: (0, 0, 10, 10),
text: "A".to_string(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
}],
);
disp.dialog_item_handles
.insert(text_handle, (dialog_ptr, 0));
bus.write_long(TEST_SP, new_text_ptr);
bus.write_long(TEST_SP + 4, text_handle);
let result = disp.dispatch_dialog(true, 0x18F, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
let resized_ptr = bus.read_long(text_handle);
assert_eq!(bus.get_alloc_size(resized_ptr), Some(5));
assert_eq!(bus.read_bytes(resized_ptr, 5), b"Hello".to_vec());
}
#[test]
fn set_dialog_item_text_redraws_visible_text_item() {
let (mut disp, mut cpu, mut bus) = setup();
let screen_base = bus.alloc((200 * 100) as u32);
let row_bytes = 200u32;
disp.set_screen_mode_for_test(screen_base, row_bytes, 200, 100, 8);
bus.write_long(0x0824, screen_base);
TrapDispatcher::fb_fill_rect(
&mut bus,
screen_base,
row_bytes,
8,
200,
100,
0,
0,
100,
200,
false,
);
let white = bus.read_byte(screen_base);
let bounds = (10, 20, 70, 180);
let dialog_ptr = bus.alloc(170);
bus.write_word(dialog_ptr + 6, 0); // GrafPort, not CGrafPort
bus.write_word(dialog_ptr + 8, (-bounds.0) as u16);
bus.write_word(dialog_ptr + 10, (-bounds.1) as u16);
bus.write_word(dialog_ptr + 16, 0);
bus.write_word(dialog_ptr + 18, 0);
bus.write_word(dialog_ptr + 20, (bounds.2 - bounds.0) as u16);
bus.write_word(dialog_ptr + 22, (bounds.3 - bounds.1) as u16);
bus.write_word(dialog_ptr + 108, 2);
bus.write_byte(dialog_ptr + 110, 0xFF);
bus.write_word(dialog_ptr + 164, 0xFFFF);
bus.write_word(dialog_ptr + 168, 1);
let items_handle = bus.alloc(4);
let ditl_ptr = bus.alloc(16);
let text_handle = bus.alloc(4);
let text_ptr = bus.alloc(1);
bus.write_bytes(text_ptr, b"A");
bus.write_long(text_handle, text_ptr);
bus.write_long(items_handle, ditl_ptr);
bus.write_long(dialog_ptr + 156, items_handle);
bus.write_word(ditl_ptr, 0);
bus.write_long(ditl_ptr + 2, text_handle);
bus.write_word(ditl_ptr + 6, 10);
bus.write_word(ditl_ptr + 8, 10);
bus.write_word(ditl_ptr + 10, 26);
bus.write_word(ditl_ptr + 12, 140);
bus.write_byte(ditl_ptr + 14, 8);
bus.write_byte(ditl_ptr + 15, 0);
let items = vec![DialogItem {
item_type: 8,
rect: (10, 10, 26, 140),
text: "A".to_string(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
}];
disp.dialog_items.insert(dialog_ptr, items.clone());
disp.dialog_item_handles
.insert(text_handle, (dialog_ptr, 0));
disp.draw_dialog(&mut bus, bounds, 2, "", &items, 1, "", 0, false, dialog_ptr);
let count_nonwhite = |bus: &MacMemoryBus| -> usize {
let mut count = 0;
for y in 20..36u32 {
for x in 30..160u32 {
if bus.read_byte(screen_base + y * row_bytes + x) != white {
count += 1;
}
}
}
count
};
let before = count_nonwhite(&bus);
let pstr = bus.alloc(9);
bus.write_byte(pstr, 8);
bus.write_bytes(pstr + 1, b"WWWWWWWW");
bus.write_long(TEST_SP, pstr);
bus.write_long(TEST_SP + 4, text_handle);
let result = disp.dispatch_dialog(true, 0x18F, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
let after = count_nonwhite(&bus);
assert!(
after > before + 20,
"SetDialogItemText must draw the updated text item; before={} after={}",
before,
after
);
}
#[test]
fn set_dialog_item_text_redraw_clips_repaint_to_dialog_bounds() {
let (mut disp, mut cpu, mut bus) = setup();
let screen_base = bus.alloc((200 * 100) as u32);
let row_bytes = 200u32;
disp.set_screen_mode_for_test(screen_base, row_bytes, 200, 100, 8);
bus.write_long(0x0824, screen_base);
TrapDispatcher::fb_fill_rect(
&mut bus,
screen_base,
row_bytes,
8,
200,
100,
0,
0,
100,
200,
true,
);
let bounds = (10, 20, 70, 120);
let dialog_ptr = bus.alloc(170);
bus.write_word(dialog_ptr + 6, 0); // GrafPort, not CGrafPort
bus.write_word(dialog_ptr + 8, (-bounds.0) as u16);
bus.write_word(dialog_ptr + 10, (-bounds.1) as u16);
bus.write_word(dialog_ptr + 16, 0);
bus.write_word(dialog_ptr + 18, 0);
bus.write_word(dialog_ptr + 20, (bounds.2 - bounds.0) as u16);
bus.write_word(dialog_ptr + 22, (bounds.3 - bounds.1) as u16);
bus.write_word(dialog_ptr + 108, 2);
bus.write_byte(dialog_ptr + 110, 0xFF);
bus.write_word(dialog_ptr + 164, 1); // item 2 is the active edit field
bus.write_word(dialog_ptr + 168, 1);
let items_handle = bus.alloc(4);
let ditl_ptr = bus.alloc(30);
let edit_handle = bus.alloc(4);
let edit_ptr = bus.alloc(3);
bus.write_bytes(edit_ptr, b"Old");
bus.write_long(edit_handle, edit_ptr);
bus.write_long(items_handle, ditl_ptr);
bus.write_long(dialog_ptr + 156, items_handle);
bus.write_word(ditl_ptr, 1); // two items
bus.write_long(ditl_ptr + 2, 0);
bus.write_word(ditl_ptr + 6, 5);
bus.write_word(ditl_ptr + 8, 20);
bus.write_word(ditl_ptr + 10, 20);
bus.write_word(ditl_ptr + 12, 160); // extends beyond dialog right
bus.write_byte(ditl_ptr + 14, 8);
bus.write_byte(ditl_ptr + 15, 0);
bus.write_long(ditl_ptr + 16, edit_handle);
bus.write_word(ditl_ptr + 20, 25);
bus.write_word(ditl_ptr + 22, 20);
bus.write_word(ditl_ptr + 24, 41);
bus.write_word(ditl_ptr + 26, 80);
bus.write_byte(ditl_ptr + 28, 16);
bus.write_byte(ditl_ptr + 29, 0);
let items = vec![
DialogItem {
item_type: 8,
rect: (5, 20, 20, 160),
text: String::new(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
},
DialogItem {
item_type: 16,
rect: (25, 20, 41, 80),
text: "Old".to_string(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
},
];
disp.dialog_items.insert(dialog_ptr, items.clone());
disp.dialog_item_handles
.insert(edit_handle, (dialog_ptr, 1));
disp.draw_dialog(
&mut bus, bounds, 2, "", &items, 1, "Old", 2, false, dialog_ptr,
);
let sample = screen_base + 17 * row_bytes + 150;
let outside_before = bus.read_byte(sample);
let pstr = bus.alloc(4);
bus.write_byte(pstr, 3);
bus.write_bytes(pstr + 1, b"New");
bus.write_long(TEST_SP, pstr);
bus.write_long(TEST_SP + 4, edit_handle);
let result = disp.dispatch_dialog(true, 0x18F, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(
bus.read_byte(sample),
outside_before,
"SetDialogItemText repaint must not clear outside the dialog port"
);
}
#[test]
fn get_dialog_item_text_returns_pascal_string_from_raw_handle() {
let (mut disp, mut cpu, mut bus) = setup();
let text_handle = bus.alloc(4);
let text_ptr = bus.alloc(5);
let out_ptr = 0x300000u32;
bus.write_long(text_handle, text_ptr);
bus.write_bytes(text_ptr, b"Hello");
bus.write_long(TEST_SP, out_ptr);
bus.write_long(TEST_SP + 4, text_handle);
let result = disp.dispatch_dialog(true, 0x190, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_byte(out_ptr), 5);
assert_eq!(bus.read_bytes(out_ptr + 1, 5), b"Hello".to_vec());
}
#[test]
fn initialize_dialog_item_handles_rewrites_ditl_text_item_storage() {
let (mut disp, _cpu, mut bus) = setup();
let dialog_ptr = bus.alloc(170);
let items_handle = bus.alloc(4);
let ditl_ptr = bus.alloc(22);
bus.write_long(items_handle, ditl_ptr);
bus.write_long(dialog_ptr + 156, items_handle);
bus.write_word(ditl_ptr, 0);
bus.write_long(ditl_ptr + 2, 0);
bus.write_word(ditl_ptr + 6, 10);
bus.write_word(ditl_ptr + 8, 20);
bus.write_word(ditl_ptr + 10, 30);
bus.write_word(ditl_ptr + 12, 40);
bus.write_byte(ditl_ptr + 14, 8);
bus.write_byte(ditl_ptr + 15, 5);
bus.write_bytes(ditl_ptr + 16, b"Hello");
let items = vec![DialogItem {
item_type: 8,
rect: (10, 20, 30, 40),
text: "Hello".to_string(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
}];
disp.initialize_dialog_item_handles(&mut bus, dialog_ptr, &items);
let created_handle = bus.read_long(ditl_ptr + 2);
let created_ptr = bus.read_long(created_handle);
assert_ne!(created_handle, 0);
assert_eq!(bus.get_alloc_size(created_ptr), Some(5));
assert_eq!(bus.read_bytes(created_ptr, 5), b"Hello".to_vec());
assert_eq!(
disp.dialog_item_handles.get(&created_handle),
Some(&(dialog_ptr, 0))
);
}
#[test]
fn initialize_dialog_item_handles_materializes_icon_and_picture_handles() {
let (mut disp, _cpu, mut bus) = setup();
let dialog_ptr = bus.alloc(170);
let items_handle = bus.alloc(4);
let ditl_ptr = bus.alloc(34);
let icon_ptr = disp.install_test_resource(&mut bus, *b"ICON", 421, &[0xFF; 128]);
let pict_ptr = disp.install_test_resource(&mut bus, *b"PICT", 422, &[0x11; 32]);
bus.write_long(items_handle, ditl_ptr);
bus.write_long(dialog_ptr + 156, items_handle);
bus.write_word(ditl_ptr, 1);
bus.write_long(ditl_ptr + 2, 0);
bus.write_word(ditl_ptr + 6, 8);
bus.write_word(ditl_ptr + 8, 8);
bus.write_word(ditl_ptr + 10, 40);
bus.write_word(ditl_ptr + 12, 40);
bus.write_byte(ditl_ptr + 14, 32);
bus.write_byte(ditl_ptr + 15, 2);
bus.write_word(ditl_ptr + 16, 421);
bus.write_long(ditl_ptr + 18, 0);
bus.write_word(ditl_ptr + 22, 8);
bus.write_word(ditl_ptr + 24, 48);
bus.write_word(ditl_ptr + 26, 40);
bus.write_word(ditl_ptr + 28, 112);
bus.write_byte(ditl_ptr + 30, 64);
bus.write_byte(ditl_ptr + 31, 2);
bus.write_word(ditl_ptr + 32, 422);
let items = vec![
DialogItem {
item_type: 32,
rect: (8, 8, 40, 40),
text: String::new(),
resource_id: 421,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
},
DialogItem {
item_type: 64,
rect: (8, 48, 40, 112),
text: String::new(),
resource_id: 422,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
},
];
disp.initialize_dialog_item_handles(&mut bus, dialog_ptr, &items);
let icon_handle = bus.read_long(ditl_ptr + 2);
let pict_handle = bus.read_long(ditl_ptr + 18);
assert_ne!(icon_handle, 0);
assert_ne!(pict_handle, 0);
assert_eq!(bus.read_long(icon_handle), icon_ptr);
assert_eq!(bus.read_long(pict_handle), pict_ptr);
}
#[test]
fn initialize_dialog_item_handles_clears_user_item_placeholder_proc_ptr() {
let (mut disp, _cpu, mut bus) = setup();
let dialog_ptr = bus.alloc(170);
let items_handle = bus.alloc(4);
let ditl_ptr = bus.alloc(16);
bus.write_long(items_handle, ditl_ptr);
bus.write_long(dialog_ptr + 156, items_handle);
bus.write_word(ditl_ptr, 0);
bus.write_long(ditl_ptr + 2, 0x12345678);
bus.write_word(ditl_ptr + 6, 10);
bus.write_word(ditl_ptr + 8, 20);
bus.write_word(ditl_ptr + 10, 30);
bus.write_word(ditl_ptr + 12, 40);
bus.write_byte(ditl_ptr + 14, 0);
bus.write_byte(ditl_ptr + 15, 0);
let items = vec![DialogItem {
item_type: 0,
rect: (10, 20, 30, 40),
text: String::new(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
}];
disp.initialize_dialog_item_handles(&mut bus, dialog_ptr, &items);
assert_eq!(bus.read_long(ditl_ptr + 2), 0);
}
#[test]
fn initialize_dialog_item_handles_creates_standard_control_handle() {
let (mut disp, _cpu, mut bus) = setup();
let dialog_ptr = bus.alloc(170);
let items_handle = bus.alloc(4);
let ditl_ptr = bus.alloc(22);
bus.write_long(items_handle, ditl_ptr);
bus.write_long(dialog_ptr + 156, items_handle);
bus.write_word(ditl_ptr, 0);
bus.write_long(ditl_ptr + 2, 0);
bus.write_word(ditl_ptr + 6, 10);
bus.write_word(ditl_ptr + 8, 20);
bus.write_word(ditl_ptr + 10, 30);
bus.write_word(ditl_ptr + 12, 120);
bus.write_byte(ditl_ptr + 14, 5);
bus.write_byte(ditl_ptr + 15, 5);
bus.write_bytes(ditl_ptr + 16, b"Sound");
let items = vec![DialogItem {
item_type: 5,
rect: (10, 20, 30, 120),
text: "Sound".to_string(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
}];
disp.initialize_dialog_item_handles(&mut bus, dialog_ptr, &items);
let control_handle = bus.read_long(ditl_ptr + 2);
let control_ptr = bus.read_long(control_handle);
assert_ne!(control_handle, 0);
assert_ne!(control_ptr, 0);
assert_eq!(bus.read_long(dialog_ptr + 140), control_handle);
assert_eq!(bus.read_long(control_ptr + 4), dialog_ptr);
assert_eq!(bus.read_word(control_ptr + 8) as i16, 10);
assert_eq!(bus.read_word(control_ptr + 10) as i16, 20);
assert_eq!(bus.read_byte(control_ptr + 17), 0);
assert_eq!(
disp.dialog_control_handles.get(&control_handle),
Some(&(dialog_ptr, 1))
);
assert_eq!(disp.dialog_control_values.get(&(dialog_ptr, 1)), Some(&0));
assert_eq!(disp.control_proc_ids.get(&control_ptr), Some(&1));
}
#[test]
fn initialize_dialog_item_handles_creates_resctrl_control_handle() {
let (mut disp, _cpu, mut bus) = setup();
let dialog_ptr = bus.alloc(170);
let items_handle = bus.alloc(4);
let ditl_ptr = bus.alloc(18);
bus.write_long(items_handle, ditl_ptr);
bus.write_long(dialog_ptr + 156, items_handle);
bus.write_word(ditl_ptr, 0);
bus.write_long(ditl_ptr + 2, 0);
bus.write_word(ditl_ptr + 6, 10);
bus.write_word(ditl_ptr + 8, 20);
bus.write_word(ditl_ptr + 10, 30);
bus.write_word(ditl_ptr + 12, 140);
bus.write_byte(ditl_ptr + 14, 7);
bus.write_byte(ditl_ptr + 15, 2);
bus.write_word(ditl_ptr + 16, 128);
let mut cntl = Vec::new();
for word in [0i16, 0, 20, 110, 2, -1, 4, 1300, 1009] {
cntl.extend_from_slice(&(word as u16).to_be_bytes());
}
cntl.extend_from_slice(&0x1234_5678u32.to_be_bytes());
cntl.push(4);
cntl.extend_from_slice(b"Team");
disp.install_test_resource(&mut bus, *b"CNTL", 128, &cntl);
let items = vec![DialogItem {
item_type: 7,
rect: (10, 20, 30, 140),
text: String::new(),
resource_id: 128,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
}];
disp.initialize_dialog_item_handles(&mut bus, dialog_ptr, &items);
let handle = bus.read_long(ditl_ptr + 2);
let ctrl_ptr = bus.read_long(handle);
assert_ne!(handle, 0);
assert_ne!(ctrl_ptr, 0);
assert_eq!(bus.read_long(dialog_ptr + 140), handle);
assert_eq!(bus.read_long(ctrl_ptr + 4), dialog_ptr);
assert_eq!(bus.read_word(ctrl_ptr + 8) as i16, 10);
assert_eq!(bus.read_word(ctrl_ptr + 10) as i16, 20);
assert_eq!(bus.read_word(ctrl_ptr + 12) as i16, 30);
assert_eq!(bus.read_word(ctrl_ptr + 14) as i16, 140);
assert_eq!(bus.read_word(ctrl_ptr + 18) as i16, 2);
assert_eq!(bus.read_word(ctrl_ptr + 20) as i16, 1300);
assert_eq!(disp.control_proc_ids.get(&ctrl_ptr), Some(&1009));
assert_eq!(
disp.dialog_control_handles.get(&handle),
Some(&(dialog_ptr, 1))
);
assert_eq!(disp.dialog_control_values.get(&(dialog_ptr, 1)), Some(&2));
}
#[test]
fn popup_resctrl_initializes_contrl_data_private_menu_record() {
let (mut disp, _cpu, mut bus) = setup();
let dialog_ptr = bus.alloc(170);
let items_handle = bus.alloc(4);
let ditl_ptr = bus.alloc(18);
bus.write_long(items_handle, ditl_ptr);
bus.write_long(dialog_ptr + 156, items_handle);
bus.write_word(ditl_ptr, 0);
bus.write_long(ditl_ptr + 2, 0);
bus.write_word(ditl_ptr + 6, 10);
bus.write_word(ditl_ptr + 8, 20);
bus.write_word(ditl_ptr + 10, 30);
bus.write_word(ditl_ptr + 12, 140);
bus.write_byte(ditl_ptr + 14, 7);
bus.write_byte(ditl_ptr + 15, 2);
bus.write_word(ditl_ptr + 16, 128);
let mut cntl = Vec::new();
for word in [0i16, 0, 20, 110, 1, -1, 0, 4000, 1009] {
cntl.extend_from_slice(&(word as u16).to_be_bytes());
}
cntl.extend_from_slice(&0u32.to_be_bytes());
cntl.push(0);
disp.install_test_resource(&mut bus, *b"CNTL", 128, &cntl);
let items = vec![DialogItem {
item_type: 7,
rect: (10, 20, 30, 140),
text: String::new(),
resource_id: 128,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
}];
disp.initialize_dialog_item_handles(&mut bus, dialog_ptr, &items);
let ctrl_handle = bus.read_long(ditl_ptr + 2);
let ctrl_ptr = bus.read_long(ctrl_handle);
let private_handle = bus.read_long(ctrl_ptr + 28);
let private_ptr = bus.read_long(private_handle);
let menu_handle = bus.read_long(private_ptr);
let menu_ptr = bus.read_long(menu_handle);
assert_ne!(private_handle, 0);
assert_ne!(private_ptr, 0);
assert_ne!(menu_handle, 0);
assert_eq!(bus.read_word(private_ptr + 4) as i16, 4000);
assert_eq!(bus.read_word(menu_ptr) as i16, 4000);
assert_eq!(disp.popup_control_menu_id(&bus, ctrl_ptr, -1), 4000);
assert!(disp
.menus
.iter()
.any(|menu| menu.id == 4000 && menu.handle == menu_handle));
}
#[test]
fn append_ditl_overlay_preserves_existing_handles_and_initializes_resctrl() {
let (mut disp, _cpu, mut bus) = setup();
let dialog_ptr = bus.alloc(170);
let items_handle = bus.alloc(4);
let old_ditl_ptr = bus.alloc(18);
let preserved_handle = 0x00A1_B2C3;
bus.write_long(items_handle, old_ditl_ptr);
bus.write_long(dialog_ptr + 156, items_handle);
bus.write_word(old_ditl_ptr, 0);
bus.write_long(old_ditl_ptr + 2, preserved_handle);
bus.write_word(old_ditl_ptr + 6, 10);
bus.write_word(old_ditl_ptr + 8, 20);
bus.write_word(old_ditl_ptr + 10, 30);
bus.write_word(old_ditl_ptr + 12, 60);
bus.write_byte(old_ditl_ptr + 14, 4);
bus.write_byte(old_ditl_ptr + 15, 2);
bus.write_bytes(old_ditl_ptr + 16, b"OK");
disp.dialog_items.insert(
dialog_ptr,
vec![DialogItem {
item_type: 4,
rect: (10, 20, 30, 60),
text: "OK".to_string(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
}],
);
let mut cntl = Vec::new();
for word in [0i16, 0, 20, 110, 3, -1, 1, 5, 1008] {
cntl.extend_from_slice(&(word as u16).to_be_bytes());
}
cntl.extend_from_slice(&0x1234_5678u32.to_be_bytes());
cntl.push(4);
cntl.extend_from_slice(b"Pane");
disp.install_test_resource(&mut bus, *b"CNTL", 131, &cntl);
let append_handle = bus.alloc(4);
let append_ditl_ptr = bus.alloc(18);
bus.write_long(append_handle, append_ditl_ptr);
bus.write_word(append_ditl_ptr, 0);
bus.write_long(append_ditl_ptr + 2, 0);
bus.write_word(append_ditl_ptr + 6, 40);
bus.write_word(append_ditl_ptr + 8, 50);
bus.write_word(append_ditl_ptr + 10, 60);
bus.write_word(append_ditl_ptr + 12, 170);
bus.write_byte(append_ditl_ptr + 14, 7);
bus.write_byte(append_ditl_ptr + 15, 2);
bus.write_word(append_ditl_ptr + 16, 131);
let count = disp.append_ditl_to_dialog(&mut bus, dialog_ptr, append_handle, 0);
assert_eq!(count, 2);
let new_ditl_ptr = bus.read_long(items_handle);
assert_ne!(new_ditl_ptr, old_ditl_ptr);
assert_eq!(bus.read_word(new_ditl_ptr), 1);
assert_eq!(
bus.read_long(new_ditl_ptr + 2),
preserved_handle,
"AppendDITL must not recreate existing item handles"
);
let ctrl_handle = bus.read_long(new_ditl_ptr + 18);
let ctrl_ptr = bus.read_long(ctrl_handle);
assert_ne!(ctrl_handle, 0);
assert_ne!(ctrl_ptr, 0);
assert_eq!(bus.read_long(dialog_ptr + 140), ctrl_handle);
assert_eq!(bus.read_word(ctrl_ptr + 8) as i16, 40);
assert_eq!(bus.read_word(ctrl_ptr + 10) as i16, 50);
assert_eq!(bus.read_word(ctrl_ptr + 12) as i16, 60);
assert_eq!(bus.read_word(ctrl_ptr + 14) as i16, 170);
assert_eq!(
disp.dialog_control_handles.get(&ctrl_handle),
Some(&(dialog_ptr, 2))
);
assert_eq!(disp.dialog_control_values.get(&(dialog_ptr, 2)), Some(&3));
}
#[test]
fn shorten_ditl_erases_removed_retained_item_rects_without_wiping_header_pixels() {
let (mut disp, _cpu, mut bus) = setup();
let screen_base = bus.alloc((400 * 300) as u32);
for i in 0..400u32 * 300 {
bus.write_byte(screen_base + i, 0x00);
}
bus.write_long(0x0824, screen_base);
disp.screen_mode = (screen_base, 400, 400, 300, 8);
let dialog_ptr = bus.alloc(200);
bus.write_word(dialog_ptr + 6, 0);
bus.write_word(dialog_ptr + 8, (-100i16) as u16);
bus.write_word(dialog_ptr + 10, (-100i16) as u16);
bus.write_word(dialog_ptr + 16, 0);
bus.write_word(dialog_ptr + 18, 0);
bus.write_word(dialog_ptr + 20, 120);
bus.write_word(dialog_ptr + 22, 200);
let bounds = TrapDispatcher::dialog_screen_bounds(&bus, dialog_ptr);
let items_handle = bus.alloc(4);
let ditl_ptr = bus.alloc(38);
bus.write_long(items_handle, ditl_ptr);
bus.write_long(dialog_ptr + 156, items_handle);
bus.write_word(ditl_ptr, 1);
bus.write_long(ditl_ptr + 2, 0);
bus.write_word(ditl_ptr + 6, 10);
bus.write_word(ditl_ptr + 8, 10);
bus.write_word(ditl_ptr + 10, 30);
bus.write_word(ditl_ptr + 12, 80);
bus.write_byte(ditl_ptr + 14, 8);
bus.write_byte(ditl_ptr + 15, 2);
bus.write_bytes(ditl_ptr + 16, b"OK");
bus.write_long(ditl_ptr + 20, 0);
bus.write_word(ditl_ptr + 24, 40);
bus.write_word(ditl_ptr + 26, 40);
bus.write_word(ditl_ptr + 28, 60);
bus.write_word(ditl_ptr + 30, 160);
bus.write_byte(ditl_ptr + 32, 8);
bus.write_byte(ditl_ptr + 33, 3);
bus.write_bytes(ditl_ptr + 34, b"Old");
disp.dialog_items.insert(
dialog_ptr,
vec![
DialogItem {
item_type: 8,
rect: (10, 10, 30, 80),
text: "OK".to_string(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
},
DialogItem {
item_type: 8,
rect: (40, 40, 60, 160),
text: "Old".to_string(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
},
],
);
disp.fill_rect_clipped_to_dialog(&mut bus, bounds, (106, 106, 114, 170), true);
disp.fill_rect_clipped_to_dialog(&mut bus, bounds, (140, 140, 160, 260), true);
let pixels = disp.save_dialog_pixels(&bus, bounds);
disp.dialog_visible_snapshots
.insert(dialog_ptr, PersistentDialogSnapshot { bounds, pixels });
let count = disp.shorten_ditl_in_dialog(&mut bus, dialog_ptr, 1);
assert_eq!(count, 1);
assert_eq!(bus.read_word(ditl_ptr), 0);
assert_eq!(bus.read_byte(screen_base + 110 * 400 + 120), 0xFF);
assert_eq!(bus.read_byte(screen_base + 150 * 400 + 150), 0x00);
let retained = disp.dialog_visible_snapshots.get(&dialog_ptr).unwrap();
assert!(
retained.pixels == disp.save_dialog_pixels(&bus, bounds),
"ShortenDITL retained snapshot must match the erased framebuffer"
);
}
#[test]
fn dialog_item_handle_addr_skips_zero_len_resctrl_resource_id() {
let (mut disp, _cpu, mut bus) = setup();
let dialog_ptr = bus.alloc(170);
let items_handle = bus.alloc(4);
let ditl_ptr = bus.alloc(36);
bus.write_long(items_handle, ditl_ptr);
bus.write_long(dialog_ptr + 156, items_handle);
bus.write_word(ditl_ptr, 1);
bus.write_long(ditl_ptr + 2, 0);
bus.write_word(ditl_ptr + 6, 10);
bus.write_word(ditl_ptr + 8, 20);
bus.write_word(ditl_ptr + 10, 30);
bus.write_word(ditl_ptr + 12, 140);
bus.write_byte(ditl_ptr + 14, 7);
bus.write_byte(ditl_ptr + 15, 0);
bus.write_word(ditl_ptr + 16, 128);
bus.write_long(ditl_ptr + 18, 0);
bus.write_word(ditl_ptr + 22, 40);
bus.write_word(ditl_ptr + 24, 20);
bus.write_word(ditl_ptr + 26, 52);
bus.write_word(ditl_ptr + 28, 140);
bus.write_byte(ditl_ptr + 30, 8);
bus.write_byte(ditl_ptr + 31, 3);
bus.write_bytes(ditl_ptr + 32, b"abc");
bus.write_byte(ditl_ptr + 35, 0);
let mut cntl = Vec::new();
for word in [0i16, 0, 20, 110, 2, -1, 4, 1300, 1009] {
cntl.extend_from_slice(&(word as u16).to_be_bytes());
}
cntl.extend_from_slice(&0x1234_5678u32.to_be_bytes());
cntl.push(4);
cntl.extend_from_slice(b"Team");
disp.install_test_resource(&mut bus, *b"CNTL", 128, &cntl);
let items = vec![
DialogItem {
item_type: 7,
rect: (10, 20, 30, 140),
text: String::new(),
resource_id: 128,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
},
DialogItem {
item_type: 8,
rect: (40, 20, 52, 140),
text: "abc".to_string(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
},
];
disp.initialize_dialog_item_handles(&mut bus, dialog_ptr, &items);
assert_eq!(
bus.read_word(ditl_ptr + 16),
128,
"resCtrl resource ID must not be overwritten by the next item handle"
);
assert_ne!(bus.read_long(ditl_ptr + 2), 0);
assert_ne!(bus.read_long(ditl_ptr + 18), 0);
assert_eq!(
TrapDispatcher::dialog_item_handle(&bus, dialog_ptr, 2),
bus.read_long(ditl_ptr + 18)
);
}
#[test]
fn draw_dialog_resctrl_uses_cntl_proc_id_not_always_popup() {
let (mut disp, _cpu, mut bus) = setup();
let screen_base = 0x300000u32;
let row_bytes: u32 = 640;
disp.set_screen_mode_for_test(screen_base, row_bytes, 640, 480, 8);
let dialog_ptr = bus.alloc(170);
let items_handle = bus.alloc(4);
let ditl_ptr = bus.alloc(18);
bus.write_long(items_handle, ditl_ptr);
bus.write_long(dialog_ptr + 156, items_handle);
bus.write_word(ditl_ptr, 0);
bus.write_long(ditl_ptr + 2, 0);
bus.write_word(ditl_ptr + 6, 20);
bus.write_word(ditl_ptr + 8, 20);
bus.write_word(ditl_ptr + 10, 40);
bus.write_word(ditl_ptr + 12, 120);
bus.write_byte(ditl_ptr + 14, 7);
bus.write_byte(ditl_ptr + 15, 2);
bus.write_word(ditl_ptr + 16, 128);
let mut cntl = Vec::new();
for word in [0i16, 0, 20, 100, 0, -1, 0, 0, 0] {
cntl.extend_from_slice(&(word as u16).to_be_bytes());
}
cntl.extend_from_slice(&0u32.to_be_bytes());
cntl.push(2);
cntl.extend_from_slice(b"OK");
disp.install_test_resource(&mut bus, *b"CNTL", 128, &cntl);
let items = vec![DialogItem {
item_type: 7,
rect: (20, 20, 40, 120),
text: String::new(),
resource_id: 128,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
}];
disp.initialize_dialog_item_handles(&mut bus, dialog_ptr, &items);
disp.draw_dialog(
&mut bus,
(100, 100, 180, 260),
3,
"",
&items,
0,
"",
0,
false,
dialog_ptr,
);
let old_popup_arrow_pixel = screen_base + 130 * row_bytes + 210;
assert_ne!(
bus.read_byte(old_popup_arrow_pixel),
0xFF,
"push-button resCtrl must not draw popup-menu arrow pixels"
);
}
#[test]
fn show_dialog_item_updates_live_resctrl_control_rect() {
let (mut disp, mut cpu, mut bus) = setup();
let dialog_ptr = bus.alloc(170);
let items_handle = bus.alloc(4);
let ditl_ptr = bus.alloc(18);
let hidden_rect = (20, 20 + 16384, 40, 120 + 16384);
let visible_rect = (20, 20, 40, 120);
bus.write_long(items_handle, ditl_ptr);
bus.write_long(dialog_ptr + 156, items_handle);
bus.write_word(ditl_ptr, 0);
bus.write_long(ditl_ptr + 2, 0);
bus.write_word(ditl_ptr + 6, hidden_rect.0 as u16);
bus.write_word(ditl_ptr + 8, hidden_rect.1 as u16);
bus.write_word(ditl_ptr + 10, hidden_rect.2 as u16);
bus.write_word(ditl_ptr + 12, hidden_rect.3 as u16);
bus.write_byte(ditl_ptr + 14, 7);
bus.write_byte(ditl_ptr + 15, 2);
bus.write_word(ditl_ptr + 16, 128);
let mut cntl = Vec::new();
for word in [0i16, 0, 20, 100, 0, -1, 0, 0, 0] {
cntl.extend_from_slice(&(word as u16).to_be_bytes());
}
cntl.extend_from_slice(&0u32.to_be_bytes());
cntl.push(2);
cntl.extend_from_slice(b"OK");
disp.install_test_resource(&mut bus, *b"CNTL", 128, &cntl);
let items = vec![DialogItem {
item_type: 7,
rect: hidden_rect,
text: String::new(),
resource_id: 128,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
}];
disp.initialize_dialog_item_handles(&mut bus, dialog_ptr, &items);
disp.dialog_items.insert(dialog_ptr, items);
disp.hidden_dialog_item_rects
.insert((dialog_ptr, 1), visible_rect);
bus.write_word(TEST_SP, 1);
bus.write_long(TEST_SP + 2, dialog_ptr);
disp.dispatch_dialog(true, 0x028, &mut cpu, &mut bus)
.unwrap()
.unwrap();
let ctrl_handle = disp.dialog_control_handle_for_item(dialog_ptr, 1).unwrap();
let ctrl_ptr = bus.read_long(ctrl_handle);
assert_eq!(bus.read_word(ctrl_ptr + 8) as i16, visible_rect.0);
assert_eq!(bus.read_word(ctrl_ptr + 10) as i16, visible_rect.1);
assert_eq!(bus.read_word(ctrl_ptr + 12) as i16, visible_rect.2);
assert_eq!(bus.read_word(ctrl_ptr + 14) as i16, visible_rect.3);
}
#[test]
fn parse_ditl_preserves_user_item_proc_ptr() {
let (_disp, _cpu, mut bus) = setup();
let ditl_ptr = bus.alloc(16);
bus.write_word(ditl_ptr, 0);
bus.write_long(ditl_ptr + 2, 0x12345678);
bus.write_word(ditl_ptr + 6, 10);
bus.write_word(ditl_ptr + 8, 20);
bus.write_word(ditl_ptr + 10, 30);
bus.write_word(ditl_ptr + 12, 40);
bus.write_byte(ditl_ptr + 14, 0);
bus.write_byte(ditl_ptr + 15, 0);
let items = TrapDispatcher::parse_ditl(&bus, ditl_ptr, 16);
assert_eq!(items.len(), 1);
assert_eq!(items[0].proc_ptr, 0x12345678);
}
#[test]
fn initialize_dialog_item_handles_preserves_user_item_proc_ptr() {
let (mut disp, _cpu, mut bus) = setup();
let dialog_ptr = bus.alloc(170);
let items_handle = bus.alloc(4);
let ditl_ptr = bus.alloc(16);
bus.write_long(items_handle, ditl_ptr);
bus.write_long(dialog_ptr + 156, items_handle);
bus.write_word(ditl_ptr, 0);
bus.write_long(ditl_ptr + 2, 0);
bus.write_word(ditl_ptr + 6, 10);
bus.write_word(ditl_ptr + 8, 20);
bus.write_word(ditl_ptr + 10, 30);
bus.write_word(ditl_ptr + 12, 40);
bus.write_byte(ditl_ptr + 14, 0);
bus.write_byte(ditl_ptr + 15, 0);
let items = vec![DialogItem {
item_type: 0,
rect: (10, 20, 30, 40),
text: String::new(),
resource_id: 0,
proc_ptr: 0x12345678,
sel_start: 0,
sel_end: 0,
}];
disp.initialize_dialog_item_handles(&mut bus, dialog_ptr, &items);
assert_eq!(bus.read_long(ditl_ptr + 2), 0x12345678);
}
#[test]
fn refresh_ditl_proc_ptrs_clears_stale_user_item_proc_ptr() {
let (_disp, _cpu, mut bus) = setup();
let dialog_ptr = bus.alloc(170);
let items_handle = bus.alloc(4);
let ditl_ptr = bus.alloc(16);
bus.write_long(items_handle, ditl_ptr);
bus.write_long(dialog_ptr + 156, items_handle);
bus.write_word(ditl_ptr, 0);
bus.write_long(ditl_ptr + 2, 0);
bus.write_word(ditl_ptr + 6, 10);
bus.write_word(ditl_ptr + 8, 20);
bus.write_word(ditl_ptr + 10, 30);
bus.write_word(ditl_ptr + 12, 40);
bus.write_byte(ditl_ptr + 14, 0);
bus.write_byte(ditl_ptr + 15, 0);
let mut items = vec![DialogItem {
item_type: 0,
rect: (10, 20, 30, 40),
text: String::new(),
resource_id: 0,
proc_ptr: 0x12345678,
sel_start: 0,
sel_end: 0,
}];
TrapDispatcher::refresh_ditl_proc_ptrs(&bus, dialog_ptr, &mut items);
assert_eq!(items[0].proc_ptr, 0);
}
// ---- ModalDialog ($A991) ----
#[test]
fn modal_dialog_writes_item_hit_and_pops_8_bytes() {
let (mut disp, mut cpu, mut bus) = setup();
let item_hit_addr = 0x300000u32;
bus.write_word(item_hit_addr, 0); // pre-clear
// SP+0: item_hit_ptr (4), SP+4: filterProc (4)
bus.write_long(TEST_SP, item_hit_addr);
bus.write_long(TEST_SP + 4, 0); // nil filterProc
let result = disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
assert_eq!(bus.read_word(item_hit_addr), 1);
}
#[test]
fn modal_dialog_retained_edit_text_appends_across_reentry_and_command_a_selects() {
let (mut disp, mut cpu, mut bus) = setup_with_port();
let screen_base = bus.alloc(320 * 240);
for offset in 0..320u32 * 240 {
bus.write_byte(screen_base + offset, 0xFF);
}
bus.write_long(0x0824, screen_base);
disp.set_screen_mode_for_test(screen_base, 320, 320, 240, 8);
let dialog_ptr = bus.alloc(256);
let item_hit_addr = 0x300000u32;
let bounds = (40, 40, 130, 280);
disp.front_window = dialog_ptr;
disp.current_port = dialog_ptr;
disp.window_bounds = bounds;
disp.window_proc_id = 2;
disp.window_title.clear();
disp.window_list = vec![dialog_ptr];
bus.write_word(dialog_ptr + 108, 2);
bus.write_word(dialog_ptr + 164, 0);
bus.write_word(dialog_ptr + 168, 0);
disp.dialog_items.insert(
dialog_ptr,
vec![DialogItem {
item_type: 16,
rect: (24, 24, 42, 190),
text: String::new(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
}],
);
fn enter_modal(
disp: &mut TrapDispatcher,
cpu: &mut MockCpu,
bus: &mut MacMemoryBus,
item_hit_addr: u32,
) {
bus.write_word(item_hit_addr, 0xCAFE);
bus.write_long(TEST_SP, item_hit_addr);
bus.write_long(TEST_SP + 4, 0);
cpu.write_reg(Register::A7, TEST_SP);
disp.dispatch_dialog(true, 0x191, cpu, bus)
.unwrap()
.unwrap();
assert!(disp.dialog_tracking.is_some());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP);
}
fn key_down(
disp: &mut TrapDispatcher,
cpu: &mut MockCpu,
bus: &mut MacMemoryBus,
item_hit_addr: u32,
key_code: u8,
char_code: u8,
modifiers: u16,
) {
disp.event_queue
.push_back(crate::trap::dispatch::QueuedEvent {
what: 3,
message: (u32::from(key_code) << 8) | u32::from(char_code),
where_v: 0,
where_h: 0,
modifiers,
});
disp.dispatch_dialog(true, 0x191, cpu, bus)
.unwrap()
.unwrap();
assert!(disp.dialog_tracking.is_none());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
assert_eq!(bus.read_word(item_hit_addr), 1);
}
for (key_code, char_code) in [(0x02, b'd'), (0x20, b'u'), (0x02, b'd')] {
enter_modal(&mut disp, &mut cpu, &mut bus, item_hit_addr);
key_down(
&mut disp,
&mut cpu,
&mut bus,
item_hit_addr,
key_code,
char_code,
0,
);
}
let item = &disp.dialog_items[&dialog_ptr][0];
assert_eq!(item.text, "dud");
assert_eq!((item.sel_start, item.sel_end), (3, 3));
assert!(disp
.dialog_edit_text_modified_items
.contains(&(dialog_ptr, 1)));
enter_modal(&mut disp, &mut cpu, &mut bus, item_hit_addr);
key_down(
&mut disp,
&mut cpu,
&mut bus,
item_hit_addr,
0x00,
b'a',
0x0100,
);
let item = &disp.dialog_items[&dialog_ptr][0];
assert_eq!(item.text, "dud");
assert_eq!((item.sel_start, item.sel_end), (0, 3));
assert!(!disp
.dialog_edit_text_modified_items
.contains(&(dialog_ptr, 1)));
enter_modal(&mut disp, &mut cpu, &mut bus, item_hit_addr);
key_down(&mut disp, &mut cpu, &mut bus, item_hit_addr, 0x33, 0x08, 0);
let item = &disp.dialog_items[&dialog_ptr][0];
assert_eq!(item.text, "");
assert_eq!((item.sel_start, item.sel_end), (0, 0));
}
#[test]
fn modal_dialog_popup_resctrl_tracks_selection_and_returns_item() {
let (mut disp, mut cpu, mut bus) = setup();
let screen_base = bus.alloc((800 * 600) as u32);
for i in 0..800u32 * 600 {
bus.write_byte(screen_base + i, 0xFF);
}
bus.write_long(0x0824, screen_base);
disp.screen_mode = (screen_base, 800, 800, 600, 8);
let dialog_ptr = bus.alloc(256);
bus.write_word(dialog_ptr + 6, 0);
bus.write_word(dialog_ptr + 8, (-100i16) as u16);
bus.write_word(dialog_ptr + 10, (-100i16) as u16);
bus.write_word(dialog_ptr + 16, 0);
bus.write_word(dialog_ptr + 18, 0);
bus.write_word(dialog_ptr + 20, 180);
bus.write_word(dialog_ptr + 22, 220);
let dialog_bounds = (100, 100, 280, 320);
let ctrl_ptr = bus.alloc(296);
let ctrl_handle = bus.alloc(4);
bus.write_long(ctrl_handle, ctrl_ptr);
disp.initialize_control_record(
&mut bus,
ctrl_ptr,
dialog_ptr,
(10, 20, 30, 130),
b"",
true,
1,
900,
0,
1009,
0,
);
disp.dialog_control_handles
.insert(ctrl_handle, (dialog_ptr, 1));
disp.dialog_control_values.insert((dialog_ptr, 1), 1);
disp.menus.push(Menu {
id: 900,
title: "Squadies".to_string(),
items: vec![
MenuItem {
text: "Duke".to_string(),
icon: 0,
key_equiv: 0,
mark: 0,
style: 0,
enabled: true,
},
MenuItem {
text: "Carnage".to_string(),
icon: 0,
key_equiv: 0,
mark: 0,
style: 0,
enabled: true,
},
],
enabled: true,
handle: 0,
in_menu_bar: false,
hierarchical: false,
visible_in_menu_bar: false,
});
let item_hit_addr = 0x300000u32;
bus.write_word(item_hit_addr, 0);
cpu.write_reg(Register::A7, TEST_SP);
disp.dialog_tracking = Some(crate::trap::dispatch::DialogTrackingState {
dialog_ptr,
bounds: dialog_bounds,
title: String::new(),
proc_id: 2,
items: vec![DialogItem {
item_type: 7,
rect: (10, 20, 30, 130),
text: String::new(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
}],
default_item: 0,
cancel_item: 0,
edit_text: String::new(),
edit_item: 0,
saved_pixels: Vec::new(),
stack_ptr: TEST_SP,
item_hit_ptr: item_hit_addr,
rendered_pixels: Vec::new(),
flash_remaining: 0,
flash_delay: 0,
flash_item: 0,
edit_text_modified: false,
draw_proc_queue: VecDeque::new(),
draw_procs_done: true,
rendered_pixels_final: true,
filter_proc: 0,
game_managed: false,
last_filter_event: None,
popup_draws: Vec::new(),
active_popup: None,
active_button: None,
active_user_item: None,
});
disp.mouse_button = true;
disp.mouse_pos = (115, 125);
disp.event_queue
.push_back(crate::trap::dispatch::QueuedEvent {
what: 1,
message: 0,
where_v: 115,
where_h: 125,
modifiers: 0,
});
disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert!(disp
.dialog_tracking
.as_ref()
.and_then(|tracking| tracking.active_popup.as_ref())
.is_some());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP);
let stale_open_popup_pixels = disp.save_dialog_pixels(&bus, dialog_bounds);
let tracking = disp.dialog_tracking.as_mut().unwrap();
tracking.rendered_pixels = stale_open_popup_pixels.clone();
tracking.rendered_pixels_final = true;
let (dropdown_top, dropdown_left, _, _) = disp
.dialog_tracking
.as_ref()
.and_then(|tracking| tracking.active_popup.as_ref())
.map(|popup| popup.dropdown_rect)
.expect("popup tracking should expose the live dropdown rect");
disp.mouse_pos = (dropdown_top + 1 + 16 + 1, dropdown_left + 5);
disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(
disp.dialog_tracking
.as_ref()
.and_then(|tracking| tracking.active_popup.as_ref())
.map(|popup| popup.highlighted_item),
Some(2)
);
disp.mouse_button = false;
disp.event_queue
.push_back(crate::trap::dispatch::QueuedEvent {
what: 2,
message: 0,
where_v: 148,
where_h: 125,
modifiers: 0,
});
disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert!(disp.dialog_tracking.is_none());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
assert_eq!(bus.read_word(item_hit_addr), 1);
assert_eq!(bus.read_word(ctrl_ptr + 18) as i16, 2);
assert_eq!(disp.dialog_control_values.get(&(dialog_ptr, 1)), Some(&2));
let retained = disp.dialog_visible_snapshots.get(&dialog_ptr).unwrap();
let current_pixels = disp.save_dialog_pixels(&bus, dialog_bounds);
assert!(
retained.pixels == current_pixels,
"retained snapshot must match the closed popup framebuffer"
);
assert!(
retained.pixels != stale_open_popup_pixels,
"retained snapshot kept stale open-popup pixels"
);
}
#[test]
fn modal_dialog_game_managed_dialog_still_queues_user_item_draw_procs() {
let (mut disp, mut cpu, mut bus) = setup();
let dialog_ptr = 0x200000u32;
let item_hit_addr = 0x300000u32;
disp.front_window = dialog_ptr;
disp.window_bounds = (92, 95, 415, 704);
disp.window_proc_id = 2;
disp.window_title.clear();
disp.dialog_items.insert(
dialog_ptr,
vec![
DialogItem {
item_type: 0,
rect: (8, 353, 148, 603),
text: String::new(),
resource_id: 0,
proc_ptr: 0x500000,
sel_start: 0,
sel_end: 0,
},
DialogItem {
item_type: 0x80,
rect: (159, 381, 184, 581),
text: String::new(),
resource_id: 0,
proc_ptr: 0x500100,
sel_start: 0,
sel_end: 0,
},
DialogItem {
item_type: 0x80,
rect: (324, 650, 349, 771),
text: String::new(),
resource_id: 0,
proc_ptr: 0x500200,
sel_start: 0,
sel_end: 0,
},
],
);
bus.write_long(TEST_SP, item_hit_addr);
bus.write_long(TEST_SP + 4, 0x149F0);
let result = disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP);
let tracking = disp.dialog_tracking.as_ref().unwrap();
assert!(tracking.game_managed);
assert_eq!(tracking.filter_proc, 0x149F0);
assert_eq!(tracking.draw_proc_queue.len(), 2);
assert_eq!(
tracking
.draw_proc_queue
.iter()
.map(|(_, item_no)| *item_no)
.collect::<Vec<_>>(),
vec![1, 2]
);
assert!(!tracking.draw_procs_done);
assert!(!tracking.rendered_pixels_final);
}
#[test]
fn modal_dialog_retained_reentry_reuses_visible_snapshot_without_user_item_redraw() {
let (mut disp, mut cpu, mut bus) = setup();
let dialog_ptr = 0x200000u32;
let item_hit_addr = 0x300000u32;
let screen_base = bus.alloc((64 * 64) as u32);
let row_bytes = 64u32;
for i in 0..row_bytes * 64 {
bus.write_byte(screen_base + i, 0x11);
}
bus.write_long(0x0824, screen_base);
disp.screen_mode = (screen_base, row_bytes, 64, 64, 8);
let bounds = (10, 12, 30, 34);
let snapshot_width = (bounds.3 - bounds.1 + TrapDispatcher::DBOX_FRAME_MARGIN * 2) as usize;
let snapshot_height =
(bounds.2 - bounds.0 + TrapDispatcher::DBOX_FRAME_MARGIN * 2) as usize;
let visible_pixels = vec![0x44; snapshot_width * snapshot_height];
disp.dialog_visible_snapshots.insert(
dialog_ptr,
PersistentDialogSnapshot {
bounds,
pixels: visible_pixels,
},
);
disp.dialog_saved_pixels
.insert(dialog_ptr, vec![0x22; snapshot_width * snapshot_height]);
disp.dialog_modal_entered.insert(dialog_ptr);
disp.front_window = dialog_ptr;
disp.window_bounds = bounds;
disp.window_proc_id = 1;
disp.window_title.clear();
disp.dialog_items.insert(
dialog_ptr,
vec![DialogItem {
item_type: 0x80,
rect: (0, 0, 20, 22),
text: String::new(),
resource_id: 0,
proc_ptr: 0x500000,
sel_start: 0,
sel_end: 0,
}],
);
bus.write_long(TEST_SP, item_hit_addr);
bus.write_long(TEST_SP + 4, 0);
let result = disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP);
let tracking = disp.dialog_tracking.as_ref().unwrap();
assert!(tracking.draw_proc_queue.is_empty());
assert!(tracking.draw_procs_done);
assert!(tracking.rendered_pixels_final);
assert_eq!(
bus.read_byte(screen_base + 20 * row_bytes + 20),
0x44,
"retained visible snapshot should be restored on re-entry"
);
}
#[test]
fn modal_dialog_first_entry_with_showwindow_snapshot_still_queues_user_item_draw_proc() {
let (mut disp, mut cpu, mut bus) = setup();
let dialog_ptr = 0x200000u32;
let item_hit_addr = 0x300000u32;
let screen_base = bus.alloc((64 * 64) as u32);
let row_bytes = 64u32;
bus.write_long(0x0824, screen_base);
disp.screen_mode = (screen_base, row_bytes, 64, 64, 8);
let bounds = (10, 12, 30, 34);
let snapshot_width = (bounds.3 - bounds.1 + TrapDispatcher::DBOX_FRAME_MARGIN * 2) as usize;
let snapshot_height =
(bounds.2 - bounds.0 + TrapDispatcher::DBOX_FRAME_MARGIN * 2) as usize;
disp.dialog_visible_snapshots.insert(
dialog_ptr,
PersistentDialogSnapshot {
bounds,
pixels: vec![0x44; snapshot_width * snapshot_height],
},
);
disp.front_window = dialog_ptr;
disp.window_bounds = bounds;
disp.window_proc_id = 1;
disp.window_title.clear();
disp.dialog_items.insert(
dialog_ptr,
vec![DialogItem {
item_type: 0x80,
rect: (0, 0, 20, 22),
text: String::new(),
resource_id: 0,
proc_ptr: 0x500000,
sel_start: 0,
sel_end: 0,
}],
);
bus.write_long(TEST_SP, item_hit_addr);
bus.write_long(TEST_SP + 4, 0);
let result = disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
let tracking = disp.dialog_tracking.as_ref().unwrap();
assert_eq!(tracking.draw_proc_queue.len(), 1);
assert_eq!(tracking.draw_proc_queue[0], (0x500000, 1));
assert!(!tracking.draw_procs_done);
assert!(!tracking.rendered_pixels_final);
}
#[test]
fn dialog_game_managed_ignores_offscreen_placeholder_items() {
let bounds = (200, 146, 400, 510);
let items = vec![
DialogItem {
item_type: 0,
rect: (167, 279, 192, 357),
text: String::new(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
},
DialogItem {
item_type: 0xC0, // disabled picture placeholder outside bounds
rect: (217, 179, 247, 247),
text: String::new(),
resource_id: 1431,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
},
DialogItem {
item_type: 0x80, // disabled userItem inside bounds
rect: (7, 8, 157, 358),
text: String::new(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
},
];
assert!(TrapDispatcher::dialog_is_game_managed(bounds, &items));
let mut standard_items = items;
standard_items.push(DialogItem {
item_type: 8,
rect: (20, 20, 40, 100),
text: "Standard".to_string(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
});
assert!(!TrapDispatcher::dialog_is_game_managed(
bounds,
&standard_items
));
}
#[test]
fn redraw_dialog_window_contents_does_not_snapshot_game_managed_shell() {
let (mut disp, _cpu, mut bus) = setup();
let dialog_ptr = 0x200000u32;
let screen_base = bus.alloc(64 * 64);
for offset in 0..64u32 * 64 {
bus.write_byte(screen_base + offset, 0x11);
}
bus.write_long(0x0824, screen_base);
disp.screen_mode = (screen_base, 64, 64, 64, 8);
bus.write_long(dialog_ptr + 2, screen_base);
bus.write_word(dialog_ptr + 6, 64);
bus.write_word(dialog_ptr + 8, 0);
bus.write_word(dialog_ptr + 10, 0);
bus.write_word(dialog_ptr + 12, 20);
bus.write_word(dialog_ptr + 14, 20);
bus.write_word(dialog_ptr + 16, 0);
bus.write_word(dialog_ptr + 18, 0);
bus.write_word(dialog_ptr + 20, 20);
bus.write_word(dialog_ptr + 22, 20);
bus.write_word(dialog_ptr + 108, 2);
bus.write_byte(dialog_ptr + 110, 0xFF);
disp.window_proc_ids.insert(dialog_ptr, 2);
disp.dialog_items.insert(
dialog_ptr,
vec![DialogItem {
item_type: 0x80,
rect: (2, 2, 10, 10),
text: String::new(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
}],
);
disp.dialog_visible_snapshots.insert(
dialog_ptr,
PersistentDialogSnapshot {
bounds: (0, 0, 20, 20),
pixels: vec![0x44; 30 * 30],
},
);
disp.redraw_dialog_window_contents(&mut bus, dialog_ptr);
assert!(
!disp.dialog_visible_snapshots.contains_key(&dialog_ptr),
"ShowWindow redraw must not retain a stale shell for app-drawn all-userItem dialogs"
);
}
#[test]
fn modal_dialog_resnapshot_redraws_popup_controls_after_user_item_draw_procs() {
let (mut disp, mut cpu, mut bus) = setup();
let screen_base = bus.alloc((800 * 600) as u32);
for i in 0..800u32 * 600 {
bus.write_byte(screen_base + i, 0xFF);
}
bus.write_long(0x0824, screen_base);
disp.screen_mode = (screen_base, 800, 800, 600, 8);
disp.dialog_tracking = Some(crate::trap::dispatch::DialogTrackingState {
dialog_ptr: 0x200000,
bounds: (0, 0, 100, 220),
title: String::new(),
proc_id: 2,
items: Vec::new(),
default_item: 0,
cancel_item: 0,
edit_text: String::new(),
edit_item: 0,
saved_pixels: Vec::new(),
stack_ptr: TEST_SP,
item_hit_ptr: 0,
rendered_pixels: Vec::new(),
flash_remaining: 0,
flash_delay: 0,
flash_item: 0,
edit_text_modified: false,
draw_proc_queue: VecDeque::new(),
draw_procs_done: true,
rendered_pixels_final: false,
filter_proc: 0,
game_managed: false,
last_filter_event: None,
popup_draws: vec![DialogPopupDraw {
rect: (20, 30, 42, 180),
title: String::new(),
enabled: true,
pressed: false,
}],
active_popup: None,
active_button: None,
active_user_item: None,
});
let result = disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
let interior = screen_base + 30 * 800 + 80;
assert_eq!(
bus.read_byte(interior),
0,
"popup interior must be redrawn over the guest userItem pixels"
);
assert!(
disp.dialog_tracking
.as_ref()
.is_some_and(|tracking| tracking.rendered_pixels_final),
"ModalDialog should re-snapshot after popup redraw"
);
}
#[test]
fn modal_dialog_popup_resnapshot_preserves_theme_pressed_and_inactive_states() {
let (mut disp, mut cpu, mut bus) = setup();
disp.set_ui_theme_id(UiThemeId::SystemlessDefault);
let row_bytes = 64u32;
let screen_base = bus.alloc(row_bytes * 160);
for i in 0..row_bytes * 160 {
bus.write_byte(screen_base + i, 0);
}
disp.set_screen_mode_for_test(screen_base, row_bytes, 512, 160, 1);
cpu.write_reg(Register::A7, TEST_SP);
disp.dialog_tracking = Some(crate::trap::dispatch::DialogTrackingState {
dialog_ptr: 0x200000,
bounds: (0, 0, 120, 220),
title: String::new(),
proc_id: 2,
items: Vec::new(),
default_item: 0,
cancel_item: 0,
edit_text: String::new(),
edit_item: 0,
saved_pixels: Vec::new(),
stack_ptr: TEST_SP,
item_hit_ptr: 0,
rendered_pixels: Vec::new(),
flash_remaining: 0,
flash_delay: 0,
flash_item: 0,
edit_text_modified: false,
draw_proc_queue: VecDeque::new(),
draw_procs_done: true,
rendered_pixels_final: false,
filter_proc: 0,
game_managed: false,
last_filter_event: None,
popup_draws: vec![
DialogPopupDraw {
rect: (20, 30, 42, 180),
title: String::new(),
enabled: true,
pressed: true,
},
DialogPopupDraw {
rect: (60, 30, 82, 180),
title: String::new(),
enabled: false,
pressed: false,
},
],
active_popup: None,
active_button: None,
active_user_item: None,
});
let result = disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert!(
screen_pixel_is_set(&bus, screen_base, row_bytes, 35, 25),
"pressed popup redraw should preserve the provider pressed fill"
);
assert!(
screen_pixel_is_set(&bus, screen_base, row_bytes, 32, 62),
"inactive popup redraw should preserve the provider inactive frame"
);
assert!(
disp.dialog_tracking
.as_ref()
.is_some_and(|tracking| tracking.rendered_pixels_final),
"ModalDialog should re-snapshot after stateful popup redraw"
);
}
#[test]
fn modal_dialog_resnapshot_blits_dialog_port_after_user_item_draw_procs() {
let (mut disp, mut cpu, mut bus) = setup();
let screen_base = bus.alloc((32 * 32) as u32);
for i in 0..32u32 * 32 {
bus.write_byte(screen_base + i, 0xFF);
}
bus.write_long(0x0824, screen_base);
disp.screen_mode = (screen_base, 32, 32, 32, 8);
let dialog_ptr = bus.alloc(64);
let offscreen_base = bus.alloc(20 * 20);
for i in 0..20u32 * 20 {
bus.write_byte(offscreen_base + i, 0x00);
}
let pixmap_handle = bus.alloc(4);
let pixmap_ptr = bus.alloc(50);
bus.write_long(pixmap_handle, pixmap_ptr);
bus.write_long(pixmap_ptr, offscreen_base);
bus.write_word(pixmap_ptr + 4, 0x8000 | 20);
bus.write_word(pixmap_ptr + 32, 8);
bus.write_long(dialog_ptr + 2, pixmap_handle);
bus.write_word(dialog_ptr + 6, 0xC000);
bus.write_word(dialog_ptr + 16, 0);
bus.write_word(dialog_ptr + 18, 0);
bus.write_word(dialog_ptr + 20, 20);
bus.write_word(dialog_ptr + 22, 20);
bus.write_byte(offscreen_base + 7 * 20 + 9, 0x44);
disp.front_window = dialog_ptr;
disp.window_bounds = (0, 0, 20, 20);
disp.dialog_tracking = Some(crate::trap::dispatch::DialogTrackingState {
dialog_ptr,
bounds: (0, 0, 20, 20),
title: String::new(),
proc_id: 2,
items: Vec::new(),
default_item: 0,
cancel_item: 0,
edit_text: String::new(),
edit_item: 0,
saved_pixels: Vec::new(),
stack_ptr: TEST_SP,
item_hit_ptr: 0,
rendered_pixels: Vec::new(),
flash_remaining: 0,
flash_delay: 0,
flash_item: 0,
edit_text_modified: false,
draw_proc_queue: VecDeque::new(),
draw_procs_done: true,
rendered_pixels_final: false,
filter_proc: 0,
game_managed: false,
last_filter_event: None,
popup_draws: Vec::new(),
active_popup: None,
active_button: None,
active_user_item: None,
});
let result = disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
let screen_pixel = screen_base + 7 * 32 + 9;
assert_eq!(
bus.read_byte(screen_pixel),
0x44,
"ModalDialog should composite dialog-port userItem pixels before snapshot"
);
let tracking = disp.dialog_tracking.as_ref().unwrap();
assert!(tracking.rendered_pixels_final);
let snapshot_width = 20 + (TrapDispatcher::DBOX_FRAME_MARGIN as usize * 2);
let snapshot_height = 20 + (TrapDispatcher::DBOX_FRAME_MARGIN as usize * 2);
assert_eq!(
tracking.rendered_pixels.len(),
snapshot_width * snapshot_height
);
let snapshot_index = (7 + TrapDispatcher::DBOX_FRAME_MARGIN as usize) * snapshot_width
+ (9 + TrapDispatcher::DBOX_FRAME_MARGIN as usize);
assert_eq!(tracking.rendered_pixels[snapshot_index], 0x44);
}
#[test]
fn modal_dialog_update_event_restores_existing_snapshot_before_resnapshot() {
let (mut disp, mut cpu, mut bus) = setup();
let screen_base = bus.alloc((32 * 32) as u32);
for i in 0..32u32 * 32 {
bus.write_byte(screen_base + i, 0x00);
}
bus.write_long(0x0824, screen_base);
disp.screen_mode = (screen_base, 32, 32, 32, 8);
let dialog_ptr = bus.alloc(64);
disp.front_window = dialog_ptr;
disp.window_bounds = (0, 0, 20, 20);
let snapshot_width = 20 + (TrapDispatcher::DBOX_FRAME_MARGIN as usize * 2);
let snapshot_height = 20 + (TrapDispatcher::DBOX_FRAME_MARGIN as usize * 2);
let mut rendered_pixels = vec![0x00; snapshot_width * snapshot_height];
let snapshot_index = (7 + TrapDispatcher::DBOX_FRAME_MARGIN as usize) * snapshot_width
+ (9 + TrapDispatcher::DBOX_FRAME_MARGIN as usize);
rendered_pixels[snapshot_index] = 0x44;
disp.dialog_tracking = Some(crate::trap::dispatch::DialogTrackingState {
dialog_ptr,
bounds: (0, 0, 20, 20),
title: String::new(),
proc_id: 2,
items: Vec::new(),
default_item: 0,
cancel_item: 0,
edit_text: String::new(),
edit_item: 0,
saved_pixels: Vec::new(),
stack_ptr: TEST_SP,
item_hit_ptr: 0,
rendered_pixels,
flash_remaining: 0,
flash_delay: 0,
flash_item: 0,
edit_text_modified: false,
draw_proc_queue: VecDeque::new(),
draw_procs_done: true,
rendered_pixels_final: true,
filter_proc: 0,
game_managed: false,
last_filter_event: None,
popup_draws: Vec::new(),
active_popup: None,
active_button: None,
active_user_item: None,
});
disp.event_queue
.push_back(crate::trap::dispatch::QueuedEvent {
what: 6,
message: dialog_ptr,
where_v: 0,
where_h: 0,
modifiers: 0,
});
let result = disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
let screen_pixel = screen_base + 7 * 32 + 9;
assert_eq!(
bus.read_byte(screen_pixel),
0x44,
"ModalDialog update events must preserve userItem-rendered dialog pixels"
);
let tracking = disp.dialog_tracking.as_ref().unwrap();
assert_eq!(tracking.rendered_pixels[snapshot_index], 0x44);
}
#[test]
fn finalize_dialog_draw_procs_snapshots_completed_user_item_pixels() {
let (mut disp, _cpu, mut bus) = setup();
let screen_base = bus.alloc((32 * 32) as u32);
for i in 0..32u32 * 32 {
bus.write_byte(screen_base + i, 0x00);
}
bus.write_long(0x0824, screen_base);
disp.screen_mode = (screen_base, 32, 32, 32, 8);
let dialog_ptr = bus.alloc(64);
let screen_pixel = screen_base + 7 * 32 + 9;
bus.write_byte(screen_pixel, 0x44);
disp.front_window = dialog_ptr;
disp.window_bounds = (0, 0, 20, 20);
disp.dialog_tracking = Some(crate::trap::dispatch::DialogTrackingState {
dialog_ptr,
bounds: (0, 0, 20, 20),
title: String::new(),
proc_id: 2,
items: Vec::new(),
default_item: 0,
cancel_item: 0,
edit_text: String::new(),
edit_item: 0,
saved_pixels: Vec::new(),
stack_ptr: TEST_SP,
item_hit_ptr: 0,
rendered_pixels: Vec::new(),
flash_remaining: 0,
flash_delay: 0,
flash_item: 0,
edit_text_modified: false,
draw_proc_queue: VecDeque::new(),
draw_procs_done: false,
rendered_pixels_final: false,
filter_proc: 0,
game_managed: false,
last_filter_event: None,
popup_draws: Vec::new(),
active_popup: None,
active_button: None,
active_user_item: None,
});
disp.finalize_dialog_draw_procs_if_idle(&mut bus);
let tracking = disp.dialog_tracking.as_ref().unwrap();
assert!(tracking.draw_procs_done);
assert!(tracking.rendered_pixels_final);
let snapshot_width = 20 + (TrapDispatcher::DBOX_FRAME_MARGIN as usize * 2);
let snapshot_index = (7 + TrapDispatcher::DBOX_FRAME_MARGIN as usize) * snapshot_width
+ (9 + TrapDispatcher::DBOX_FRAME_MARGIN as usize);
assert_eq!(tracking.rendered_pixels[snapshot_index], 0x44);
}
#[test]
fn modal_dialog_mouse_down_return_consumes_queued_mouse_up() {
let (mut disp, mut cpu, mut bus) = setup();
let dialog_ptr = 0x200000u32;
let item_hit_ptr = 0x300000u32;
disp.dialog_tracking = Some(crate::trap::dispatch::DialogTrackingState {
dialog_ptr,
bounds: (100, 200, 200, 360),
title: String::new(),
proc_id: 2,
items: vec![DialogItem {
item_type: 0,
rect: (20, 30, 60, 110),
text: String::new(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
}],
default_item: 1,
cancel_item: 2,
edit_text: String::new(),
edit_item: 0,
saved_pixels: Vec::new(),
stack_ptr: TEST_SP,
item_hit_ptr,
rendered_pixels: Vec::new(),
flash_remaining: 0,
flash_delay: 0,
flash_item: 0,
edit_text_modified: false,
draw_proc_queue: VecDeque::new(),
draw_procs_done: true,
rendered_pixels_final: true,
filter_proc: 0,
game_managed: true,
last_filter_event: None,
popup_draws: Vec::new(),
active_popup: None,
active_button: None,
active_user_item: None,
});
disp.event_queue
.push_back(crate::trap::dispatch::QueuedEvent {
what: 1,
message: 0,
where_v: 130,
where_h: 240,
modifiers: 0,
});
disp.event_queue
.push_back(crate::trap::dispatch::QueuedEvent {
what: 2,
message: 0,
where_v: 130,
where_h: 240,
modifiers: 0x0080,
});
let result = disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
assert_eq!(bus.read_word(item_hit_ptr), 1);
assert!(disp.event_queue.iter().all(|event| event.what != 2));
}
#[test]
fn modal_dialog_plain_user_item_waits_for_mouse_up_before_returning() {
let (mut disp, mut cpu, mut bus) = setup();
let dialog_ptr = 0x200000u32;
let item_hit_ptr = 0x300000u32;
disp.dialog_tracking = Some(crate::trap::dispatch::DialogTrackingState {
dialog_ptr,
bounds: (100, 200, 200, 360),
title: String::new(),
proc_id: 2,
items: vec![DialogItem {
item_type: 0,
rect: (20, 30, 60, 110),
text: String::new(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
}],
default_item: 1,
cancel_item: 2,
edit_text: String::new(),
edit_item: 0,
saved_pixels: Vec::new(),
stack_ptr: TEST_SP,
item_hit_ptr,
rendered_pixels: Vec::new(),
flash_remaining: 0,
flash_delay: 0,
flash_item: 0,
edit_text_modified: false,
draw_proc_queue: VecDeque::new(),
draw_procs_done: true,
rendered_pixels_final: true,
filter_proc: 0,
game_managed: false,
last_filter_event: None,
popup_draws: Vec::new(),
active_popup: None,
active_button: None,
active_user_item: None,
});
disp.mouse_button = true;
disp.mouse_pos = (130, 240);
disp.event_queue
.push_back(crate::trap::dispatch::QueuedEvent {
what: 1,
message: 0,
where_v: 130,
where_h: 240,
modifiers: 0,
});
let result = disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP);
assert_eq!(bus.read_word(item_hit_ptr), 0);
assert!(disp
.dialog_tracking
.as_ref()
.and_then(|tracking| tracking.active_user_item.as_ref())
.is_some());
disp.mouse_button = false;
disp.event_queue
.push_back(crate::trap::dispatch::QueuedEvent {
what: 2,
message: 0,
where_v: 130,
where_h: 240,
modifiers: 0x0080,
});
let result = disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
assert_eq!(bus.read_word(item_hit_ptr), 1);
assert!(disp.event_queue.iter().all(|event| event.what != 2));
}
#[test]
fn modal_dialog_popup_candidate_user_item_returns_on_mouse_down_in_original_rect() {
let (mut disp, mut cpu, mut bus) = setup();
let dialog_ptr = 0x200000u32;
let item_hit_ptr = 0x300000u32;
disp.dialog_tracking = Some(crate::trap::dispatch::DialogTrackingState {
dialog_ptr,
bounds: (100, 200, 200, 420),
title: String::new(),
proc_id: 2,
items: vec![DialogItem {
item_type: 0,
rect: (20, 90, 40, 110),
text: String::new(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
}],
default_item: 1,
cancel_item: 2,
edit_text: String::new(),
edit_item: 0,
saved_pixels: Vec::new(),
stack_ptr: TEST_SP,
item_hit_ptr,
rendered_pixels: Vec::new(),
flash_remaining: 0,
flash_delay: 0,
flash_item: 0,
edit_text_modified: false,
draw_proc_queue: VecDeque::new(),
draw_procs_done: true,
rendered_pixels_final: true,
filter_proc: 0,
game_managed: false,
last_filter_event: None,
popup_draws: Vec::new(),
active_popup: None,
active_button: None,
active_user_item: None,
});
disp.dialog_popup_original_rects
.insert((dialog_ptr, 1), (20, 30, 40, 150));
disp.dialog_popup_candidate_items.insert((dialog_ptr, 1));
disp.mouse_button = true;
disp.mouse_pos = (130, 260);
disp.event_queue
.push_back(crate::trap::dispatch::QueuedEvent {
what: 1,
message: 0,
where_v: 130,
where_h: 260,
modifiers: 0,
});
let result = disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
assert_eq!(bus.read_word(item_hit_ptr), 1);
assert_eq!(
disp.dialog_tracking
.as_ref()
.and_then(|tracking| tracking.active_user_item.as_ref())
.map(|active| active.item_no),
None,
"popup-candidate userItems must return on mouse-down so the app can run PopUpMenuSelect"
);
}
#[test]
fn modal_dialog_button_waits_for_late_mouse_up_before_returning() {
let (mut disp, mut cpu, mut bus) = setup();
let dialog_ptr = 0x200000u32;
let item_hit_ptr = 0x300000u32;
disp.dialog_tracking = Some(crate::trap::dispatch::DialogTrackingState {
dialog_ptr,
bounds: (100, 200, 200, 360),
title: String::new(),
proc_id: 2,
items: vec![DialogItem {
item_type: 4,
rect: (20, 30, 60, 110),
text: String::from("OK"),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
}],
default_item: 1,
cancel_item: 0,
edit_text: String::new(),
edit_item: 0,
saved_pixels: Vec::new(),
stack_ptr: TEST_SP,
item_hit_ptr,
rendered_pixels: Vec::new(),
flash_remaining: 0,
flash_delay: 0,
flash_item: 0,
edit_text_modified: false,
draw_proc_queue: VecDeque::new(),
draw_procs_done: true,
rendered_pixels_final: true,
filter_proc: 0,
game_managed: true,
last_filter_event: None,
popup_draws: Vec::new(),
active_popup: None,
active_button: None,
active_user_item: None,
});
disp.mouse_button = true;
disp.mouse_pos = (130, 240);
disp.event_queue
.push_back(crate::trap::dispatch::QueuedEvent {
what: 1,
message: 0,
where_v: 130,
where_h: 240,
modifiers: 0,
});
let result = disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP);
assert_eq!(bus.read_word(item_hit_ptr), 0);
assert!(disp
.dialog_tracking
.as_ref()
.and_then(|tracking| tracking.active_button.as_ref())
.is_some());
disp.mouse_button = false;
disp.event_queue
.push_back(crate::trap::dispatch::QueuedEvent {
what: 2,
message: 0,
where_v: 130,
where_h: 240,
modifiers: 0x0080,
});
let result = disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert!(disp.event_queue.iter().all(|event| event.what != 2));
assert!(disp
.dialog_tracking
.as_ref()
.and_then(|tracking| tracking.active_button.as_ref())
.is_none());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP);
{
let tracking = disp.dialog_tracking.as_mut().unwrap();
tracking.flash_remaining = 1;
tracking.flash_delay = 0;
}
let result = disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
assert_eq!(bus.read_word(item_hit_ptr), 1);
}
#[test]
fn modal_dialog_filter_handled_mouse_down_consumes_queued_mouse_up() {
let (mut disp, mut cpu, mut bus) = setup();
let dialog_ptr = 0x200000u32;
let item_hit_ptr = 0x300000u32;
let result_addr = 0x300100u32;
bus.write_word(result_addr, 0xFFFF);
bus.write_word(item_hit_ptr, 12);
disp.dialog_filter_result_addr = result_addr;
disp.dialog_tracking = Some(crate::trap::dispatch::DialogTrackingState {
dialog_ptr,
bounds: (100, 200, 200, 360),
title: String::new(),
proc_id: 2,
items: vec![DialogItem {
item_type: 0,
rect: (20, 30, 60, 110),
text: String::new(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
}],
default_item: 1,
cancel_item: 2,
edit_text: String::new(),
edit_item: 0,
saved_pixels: Vec::new(),
stack_ptr: TEST_SP,
item_hit_ptr,
rendered_pixels: Vec::new(),
flash_remaining: 0,
flash_delay: 0,
flash_item: 0,
edit_text_modified: false,
draw_proc_queue: VecDeque::new(),
draw_procs_done: true,
rendered_pixels_final: true,
filter_proc: 0x149F0,
game_managed: true,
last_filter_event: Some(crate::trap::dispatch::QueuedEvent {
what: 1,
message: 0,
where_v: 130,
where_h: 240,
modifiers: 0,
}),
popup_draws: Vec::new(),
active_popup: None,
active_button: None,
active_user_item: None,
});
disp.event_queue
.push_back(crate::trap::dispatch::QueuedEvent {
what: 2,
message: 0,
where_v: 130,
where_h: 240,
modifiers: 0x0080,
});
let result = disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
assert!(disp.event_queue.iter().all(|event| event.what != 2));
}
#[test]
fn modal_dialog_return_key_activates_default_button_after_filter_false() {
let (mut disp, mut cpu, mut bus) = setup();
let dialog_ptr = 0x200000u32;
let item_hit_ptr = 0x300000u32;
let result_addr = 0x300100u32;
bus.write_word(result_addr, 0);
bus.write_word(item_hit_ptr, 0);
disp.dialog_filter_result_addr = result_addr;
disp.dialog_tracking = Some(crate::trap::dispatch::DialogTrackingState {
dialog_ptr,
bounds: (100, 200, 200, 360),
title: String::new(),
proc_id: 2,
items: vec![DialogItem {
item_type: 4,
rect: (20, 30, 60, 110),
text: String::from("OK"),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
}],
default_item: 1,
cancel_item: 0,
edit_text: String::new(),
edit_item: 0,
saved_pixels: Vec::new(),
stack_ptr: TEST_SP,
item_hit_ptr,
rendered_pixels: Vec::new(),
flash_remaining: 0,
flash_delay: 0,
flash_item: 0,
edit_text_modified: false,
draw_proc_queue: VecDeque::new(),
draw_procs_done: true,
rendered_pixels_final: true,
filter_proc: 0x149F0,
game_managed: true,
last_filter_event: Some(crate::trap::dispatch::QueuedEvent {
what: 3,
message: 0x0000_240D,
where_v: 130,
where_h: 240,
modifiers: 0,
}),
popup_draws: Vec::new(),
active_popup: None,
active_button: None,
active_user_item: None,
});
let result = disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP);
assert_eq!(bus.read_word(item_hit_ptr), 0);
{
let tracking = disp.dialog_tracking.as_ref().unwrap();
assert_eq!(tracking.flash_item, 1);
assert!(tracking.flash_remaining > 0);
}
{
let tracking = disp.dialog_tracking.as_mut().unwrap();
tracking.flash_remaining = 1;
tracking.flash_delay = 0;
}
let result = disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
assert_eq!(bus.read_word(item_hit_ptr), 1);
assert!(disp.dialog_tracking.is_none());
}
#[test]
fn modal_dialog_filter_low_byte_nonzero_result_is_false() {
let (mut disp, mut cpu, mut bus) = setup();
let dialog_ptr = 0x200000u32;
let item_hit_ptr = 0x300000u32;
let result_addr = 0x300100u32;
bus.write_word(result_addr, 0x0001);
bus.write_word(item_hit_ptr, 0);
disp.dialog_filter_result_addr = result_addr;
disp.dialog_tracking = Some(crate::trap::dispatch::DialogTrackingState {
dialog_ptr,
bounds: (100, 200, 200, 360),
title: String::new(),
proc_id: 2,
items: vec![DialogItem {
item_type: 4,
rect: (20, 30, 60, 110),
text: String::from("OK"),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
}],
default_item: 1,
cancel_item: 0,
edit_text: String::new(),
edit_item: 0,
saved_pixels: Vec::new(),
stack_ptr: TEST_SP,
item_hit_ptr,
rendered_pixels: Vec::new(),
flash_remaining: 0,
flash_delay: 0,
flash_item: 0,
edit_text_modified: false,
draw_proc_queue: VecDeque::new(),
draw_procs_done: true,
rendered_pixels_final: true,
filter_proc: 0x149F0,
game_managed: false,
last_filter_event: Some(crate::trap::dispatch::QueuedEvent {
what: 1,
message: 0,
where_v: 130,
where_h: 240,
modifiers: 0,
}),
popup_draws: Vec::new(),
active_popup: None,
active_button: None,
active_user_item: None,
});
disp.mouse_button = true;
disp.mouse_pos = (130, 240);
let result = disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP);
assert_eq!(bus.read_word(item_hit_ptr), 0);
assert!(disp
.dialog_tracking
.as_ref()
.and_then(|tracking| tracking.active_button.as_ref())
.is_some());
}
#[test]
fn modal_dialog_filter_true_zero_hit_returns_to_app() {
let (mut disp, mut cpu, mut bus) = setup();
let dialog_ptr = 0x200000u32;
let item_hit_ptr = 0x300000u32;
let result_addr = 0x300100u32;
bus.write_word(result_addr, 0xFFFF);
bus.write_word(item_hit_ptr, 0);
disp.dialog_filter_result_addr = result_addr;
disp.dialog_tracking = Some(crate::trap::dispatch::DialogTrackingState {
dialog_ptr,
bounds: (100, 200, 200, 360),
title: String::new(),
proc_id: 2,
items: vec![DialogItem {
item_type: 4,
rect: (20, 30, 60, 110),
text: String::from("OK"),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
}],
default_item: 1,
cancel_item: 2,
edit_text: String::new(),
edit_item: 0,
saved_pixels: Vec::new(),
stack_ptr: TEST_SP,
item_hit_ptr,
rendered_pixels: Vec::new(),
flash_remaining: 0,
flash_delay: 0,
flash_item: 0,
edit_text_modified: false,
draw_proc_queue: VecDeque::new(),
draw_procs_done: true,
rendered_pixels_final: true,
filter_proc: 0x149F0,
game_managed: false,
last_filter_event: Some(crate::trap::dispatch::QueuedEvent {
what: 3,
message: 0x0000_240D,
where_v: 130,
where_h: 240,
modifiers: 0,
}),
popup_draws: Vec::new(),
active_popup: None,
active_button: None,
active_user_item: None,
});
let result = disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
assert_eq!(bus.read_word(item_hit_ptr), 0);
assert!(disp.dialog_tracking.is_none());
}
// ---- TEInit ($A9CC) ----
#[test]
fn teinit_first_call_allocates_empty_scrap_handle_and_zeros_length() {
// IM:I I-376 + I-389: TEInit creates an empty TextEdit scrap
// handle and sets TEScrpLength to 0.
let (mut disp, mut cpu, mut bus) = setup();
let sp_before = cpu.read_reg(Register::A7);
bus.write_word(crate::memory::globals::addr::TE_SCRP_LENGTH, 0xFFFF);
bus.write_long(crate::memory::globals::addr::TE_SCRP_HANDLE, 0);
let result = disp.dispatch_dialog(true, 0x1CC, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), sp_before);
let scrap_handle = bus.read_long(crate::memory::globals::addr::TE_SCRP_HANDLE);
assert_ne!(scrap_handle, 0, "TEInit must allocate TEScrpHandle");
assert_eq!(
bus.read_long(scrap_handle),
0,
"TEInit scrap handle should initially reference empty data"
);
assert_eq!(
bus.read_word(crate::memory::globals::addr::TE_SCRP_LENGTH),
0
);
}
#[test]
fn teinit_reuses_existing_scrap_handle_and_resets_length() {
// IM:I I-376 + I-389: TEInit restores empty-scrap state; HLE must
// keep an existing handle stable to avoid churn/leaks on repeat calls.
let (mut disp, mut cpu, mut bus) = setup();
let existing_handle = TrapDispatcher::allocate_handle_with_data(&mut bus, 3);
let existing_ptr = bus.read_long(existing_handle);
bus.write_bytes(existing_ptr, b"xyz");
bus.write_long(
crate::memory::globals::addr::TE_SCRP_HANDLE,
existing_handle,
);
bus.write_word(crate::memory::globals::addr::TE_SCRP_LENGTH, 3);
let result = disp.dispatch_dialog(true, 0x1CC, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP);
assert_eq!(
bus.read_long(crate::memory::globals::addr::TE_SCRP_HANDLE),
existing_handle
);
assert_eq!(
bus.read_word(crate::memory::globals::addr::TE_SCRP_LENGTH),
0
);
assert_eq!(bus.read_long(existing_handle), existing_ptr);
assert_eq!(bus.read_bytes(existing_ptr, 3), b"xyz".to_vec());
}
fn make_te_with_text(
disp: &mut TrapDispatcher,
bus: &mut crate::memory::MacMemoryBus,
text: &[u8],
) -> u32 {
let te_handle = TrapDispatcher::allocate_te_handle(bus);
disp.current_port = 0x181000;
disp.tx_font = 4;
disp.tx_face = 0;
disp.tx_mode = 0;
disp.tx_size = 10;
disp.initialize_te_record(bus, te_handle, (0, 0, 60, 160), (0, 0, 60, 160));
disp.te_set_text_contents(bus, te_handle, text);
te_handle
}
// ---- TENew ----
#[test]
fn tenew_pointer_arg_convention_initializes_destrect_viewrect_and_returns_non_nil_handle() {
// IM:I I-373..I-374 + TextEdit.h ONEWORDINLINE(0xA9D2):
// FUNCTION TENew(destRect, viewRect: Rect): TEHandle. Modern MPW
// Universal Headers pass two `const Rect *` pointers (8 bytes of
// args). After dispatch (**hTE).destRect and (**hTE).viewRect
// must round-trip the caller's input rects exactly. Defeats stubs
// that swap the args, zero the rects, or copy only a subset of
// the four 2-byte fields.
let (mut disp, mut cpu, mut bus) = setup();
// Build two DISTINCT Rect inputs in guest memory.
let dest_rect_ptr: u32 = 0x190000;
bus.write_word(dest_rect_ptr, (-1000i16) as u16); // top
bus.write_word(dest_rect_ptr + 2, (-1000i16) as u16); // left
bus.write_word(dest_rect_ptr + 4, (-700i16) as u16); // bottom
bus.write_word(dest_rect_ptr + 6, (-800i16) as u16); // right
let view_rect_ptr: u32 = 0x190010;
bus.write_word(view_rect_ptr, (-900i16) as u16); // top
bus.write_word(view_rect_ptr + 2, (-1100i16) as u16); // left
bus.write_word(view_rect_ptr + 4, (-600i16) as u16); // bottom
bus.write_word(view_rect_ptr + 6, (-700i16) as u16); // right
// Pascal FUNCTION stack frame (pointer-arg convention).
// Pascal pushes args left-to-right, so the FIRST arg (destRect_ptr)
// is DEEPEST on stack at trap entry. With 4-byte pointers:
// sp+0..3 viewRect_ptr (last pushed, shallowest)
// sp+4..7 destRect_ptr (first pushed, deepest)
// sp+8..11 TEHandle result slot (poisoned)
bus.write_long(TEST_SP, view_rect_ptr);
bus.write_long(TEST_SP + 4, dest_rect_ptr);
bus.write_long(TEST_SP + 8, 0xDEAD_BEEF);
let result = disp.dispatch_dialog(true, 0x1D2, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
let te_handle = bus.read_long(TEST_SP + 8);
assert_ne!(te_handle, 0, "TENew returned NIL TEHandle");
let te_ptr = bus.read_long(te_handle);
assert_ne!(te_ptr, 0, "TEHandle master pointer is NIL");
// destRect round-trip (top, left, bottom, right at offset 0..6).
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET) as i16,
-1000
);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 2) as i16,
-1000
);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 4) as i16,
-700
);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 6) as i16,
-800
);
// viewRect round-trip (top, left, bottom, right at offset 8..14).
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET) as i16,
-900
);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 2) as i16,
-1100
);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 4) as i16,
-600
);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 6) as i16,
-700
);
}
#[test]
fn tenew_fresh_terec_has_zero_telength_and_empty_selection_per_im_i_373() {
// IM:I I-373: a fresh TERec has no text and an empty selection
// range. Witness teLength == 0, selStart == 0, selEnd == 0, and
// hText is a non-NIL Handle.
let (mut disp, mut cpu, mut bus) = setup();
let rect_ptr: u32 = 0x190020;
bus.write_word(rect_ptr, 10); // top
bus.write_word(rect_ptr + 2, 20); // left
bus.write_word(rect_ptr + 4, 110); // bottom
bus.write_word(rect_ptr + 6, 220); // right
bus.write_long(TEST_SP, rect_ptr);
bus.write_long(TEST_SP + 4, rect_ptr);
let result = disp.dispatch_dialog(true, 0x1D2, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
let te_handle = bus.read_long(TEST_SP + 8);
let te_ptr = bus.read_long(te_handle);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_LENGTH_OFFSET),
0,
"fresh TERec must have teLength == 0"
);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
0,
"fresh TERec must have selStart == 0"
);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET),
0,
"fresh TERec must have selEnd == 0"
);
let h_text = bus.read_long(te_ptr + TrapDispatcher::TE_HTEXT_OFFSET);
assert_ne!(h_text, 0, "fresh TERec must have a non-NIL hText handle");
}
#[test]
fn tenew_function_protocol_consumes_two_pointer_args_and_writes_4_byte_result() {
// Pascal FUNCTION calling convention: 2 pointer args (8 bytes)
// are popped, the 4-byte TEHandle result is written into the
// caller's pre-allocated slot at the former SP+8. Sentinels
// around the result slot must survive the trap call.
let (mut disp, mut cpu, mut bus) = setup();
let rect_ptr: u32 = 0x190030;
bus.write_word(rect_ptr, 0);
bus.write_word(rect_ptr + 2, 0);
bus.write_word(rect_ptr + 4, 100);
bus.write_word(rect_ptr + 6, 200);
bus.write_long(TEST_SP, rect_ptr);
bus.write_long(TEST_SP + 4, rect_ptr);
bus.write_long(TEST_SP + 8, 0xDEAD_BEEF);
bus.write_long(TEST_SP + 12, 0xCAFE_BABE); // sentinel past result
let result = disp.dispatch_dialog(true, 0x1D2, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
let te_handle = bus.read_long(TEST_SP + 8);
assert_ne!(te_handle, 0xDEAD_BEEF, "result slot must be overwritten");
assert_ne!(te_handle, 0);
assert_eq!(
bus.read_long(TEST_SP + 12),
0xCAFE_BABE,
"sentinel past 4-byte result slot must survive"
);
}
#[test]
fn tenew_pointer_arg_convention_accepts_wide_ordered_destrect() {
// Some MPW callers pass a Rect embedded at the start of an
// application record. TextEdit accepts the pointer convention
// even when the destination Rect is wider than the visible
// screen; the trap must still pop only the two pointer args.
let (mut disp, mut cpu, mut bus) = setup();
let dest_record_ptr: u32 = 0x190040;
bus.write_word(dest_record_ptr, 159);
bus.write_word(dest_record_ptr + 2, 500);
bus.write_word(dest_record_ptr + 4, 171);
bus.write_word(dest_record_ptr + 6, 10500);
let view_rect_ptr: u32 = 0x190060;
bus.write_word(view_rect_ptr, 159);
bus.write_word(view_rect_ptr + 2, 500);
bus.write_word(view_rect_ptr + 4, 171);
bus.write_word(view_rect_ptr + 6, 10500);
bus.write_long(TEST_SP, view_rect_ptr);
bus.write_long(TEST_SP + 4, dest_record_ptr);
bus.write_long(TEST_SP + 8, 0xDEAD_BEEF);
bus.write_long(TEST_SP + 12, 0xCAFE_BABE);
let result = disp.dispatch_dialog(true, 0x1D2, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(
cpu.read_reg(Register::A7),
TEST_SP + 8,
"TENew pointer convention must pop exactly two pointers"
);
let te_handle = bus.read_long(TEST_SP + 8);
assert_ne!(te_handle, 0);
assert_ne!(te_handle, 0xDEAD_BEEF);
assert_eq!(
bus.read_long(TEST_SP + 12),
0xCAFE_BABE,
"TENew must not write past the 4-byte function-result slot"
);
let te_ptr = bus.read_long(te_handle);
assert_ne!(te_ptr, 0);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 6) as i16,
10500,
"wide destRect.right must round-trip from caller memory"
);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 6) as i16,
10500,
"wide viewRect.right must round-trip from caller memory"
);
}
#[test]
fn tenew_pointer_arg_convention_is_based_on_valid_pointers_not_rect_shape() {
// The calling convention is determined by the stack containing
// two Rect pointers. Unusual caller Rect contents must not make
// the trap consume a legacy by-value frame and corrupt the
// caller's saved registers.
let (mut disp, mut cpu, mut bus) = setup();
let dest_rect_ptr: u32 = 0x190080;
bus.write_word(dest_rect_ptr, 552);
bus.write_word(dest_rect_ptr + 2, 525);
bus.write_word(dest_rect_ptr + 4, 520);
bus.write_word(dest_rect_ptr + 6, 10525);
let view_rect_ptr: u32 = 0x1900A0;
bus.write_word(view_rect_ptr, 552);
bus.write_word(view_rect_ptr + 2, 525);
bus.write_word(view_rect_ptr + 4, 520);
bus.write_word(view_rect_ptr + 6, 10525);
bus.write_long(TEST_SP, view_rect_ptr);
bus.write_long(TEST_SP + 4, dest_rect_ptr);
bus.write_long(TEST_SP + 8, 0xDEAD_BEEF);
bus.write_long(TEST_SP + 12, 0xCAFE_BABE);
let result = disp.dispatch_dialog(true, 0x1D2, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
let te_handle = bus.read_long(TEST_SP + 8);
assert_ne!(te_handle, 0);
assert_ne!(te_handle, 0xDEAD_BEEF);
assert_eq!(bus.read_long(TEST_SP + 12), 0xCAFE_BABE);
let te_ptr = bus.read_long(te_handle);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET) as i16,
552
);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 4) as i16,
520
);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 6) as i16,
10525
);
}
// ---- TEDispose / TECalText / TESetSelect / TEDelete ----
#[test]
fn tedispose_releases_te_record_text_handle_and_pops_arg() {
// IM:I I-383..I-384.
let (mut disp, mut cpu, mut bus) = setup();
let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
let te_ptr = bus.read_long(te_handle);
let text_handle = bus.read_long(te_ptr + TrapDispatcher::TE_HTEXT_OFFSET);
let text_ptr = bus.read_long(text_handle);
assert_ne!(te_handle, 0);
assert_ne!(te_ptr, 0);
assert_ne!(text_handle, 0);
assert_ne!(text_ptr, 0);
bus.write_long(TEST_SP, te_handle);
let result = disp.dispatch_dialog(true, 0x1CD, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
assert_eq!(bus.get_alloc_size(text_ptr), None);
assert_eq!(bus.get_alloc_size(text_handle), None);
assert_eq!(bus.get_alloc_size(te_ptr), None);
assert_eq!(bus.get_alloc_size(te_handle), None);
}
#[test]
fn tecaltext_recomputes_line_metadata_and_pops_arg() {
// IM:I I-390: lineStarts are recomputed from current text layout.
let (mut disp, mut cpu, mut bus) = setup();
let te_handle = make_te_with_text(&mut disp, &mut bus, b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
let te_ptr = bus.read_long(te_handle);
let text_len = TrapDispatcher::te_text_bytes(&bus, te_handle).len() as u16;
assert_eq!(text_len, 32);
// Force stale metadata and a narrow destination width so wrapped
// layout produces multiple lines.
bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 6, 12);
bus.write_word(te_ptr + TrapDispatcher::TE_LENGTH_OFFSET, 0);
bus.write_word(te_ptr + TrapDispatcher::TE_N_LINES_OFFSET, 0x7777);
bus.write_word(te_ptr + TrapDispatcher::TE_LINE_STARTS_OFFSET, 0x7777);
bus.write_long(TEST_SP, te_handle);
let result = disp.dispatch_dialog(true, 0x1D0, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
let te_ptr = bus.read_long(te_handle);
let n_lines = bus.read_word(te_ptr + TrapDispatcher::TE_N_LINES_OFFSET);
assert_ne!(n_lines, 0x7777);
assert!(n_lines > 1);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_LENGTH_OFFSET),
text_len
);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_LINE_STARTS_OFFSET),
0
);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_LINE_STARTS_OFFSET + u32::from(n_lines) * 2),
text_len
);
}
#[test]
fn tegettext_returns_htext_handle_from_terec_and_pops_te_handle_arg() {
// IM:I I-384: "TEGetText returns a handle to the text of the
// specified edit record. The result is the same as the handle in
// the hText field of the edit record."
let (mut disp, mut cpu, mut bus) = setup();
let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
let te_ptr = bus.read_long(te_handle);
let expected_h_text = bus.read_long(te_ptr + TrapDispatcher::TE_HTEXT_OFFSET);
assert_ne!(expected_h_text, 0);
bus.write_long(TEST_SP, te_handle);
bus.write_long(TEST_SP + 4, 0xDEAD_BEEF); // poison result slot
let result = disp.dispatch_dialog(true, 0x1CB, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
// The returned CharsHandle equals (**hTE).hText exactly.
assert_eq!(bus.read_long(TEST_SP + 4), expected_h_text);
// The handle dereferences to the 5-byte payload "HELLO".
let text_ptr = bus.read_long(expected_h_text);
assert_ne!(text_ptr, 0);
assert_eq!(bus.read_bytes(text_ptr, 5), b"HELLO".to_vec());
}
#[test]
fn tegettext_returns_nil_handle_when_te_handle_is_zero() {
// Defensive: a NIL TEHandle has no TERec to read hText from, so
// the result is NIL. Guards against any future change that would
// crash or read past invalid memory when handed a NIL argument.
let (mut disp, mut cpu, mut bus) = setup();
bus.write_long(TEST_SP, 0); // NIL TEHandle
bus.write_long(TEST_SP + 4, 0xCAFE_BABE); // poison result slot
let result = disp.dispatch_dialog(true, 0x1CB, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
assert_eq!(bus.read_long(TEST_SP + 4), 0);
}
#[test]
fn tesetselect_clamps_selend_to_text_length_and_pops_args() {
// IM:I I-385: selEnd past end-of-text clamps to teLength.
let (mut disp, mut cpu, mut bus) = setup();
let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
let te_ptr = bus.read_long(te_handle);
assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_LENGTH_OFFSET), 5);
bus.write_long(TEST_SP, te_handle); // hTE
bus.write_long(TEST_SP + 4, 999); // selEnd
bus.write_long(TEST_SP + 8, 2); // selStart
let result = disp.dispatch_dialog(true, 0x1D1, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 12);
let te_ptr = bus.read_long(te_handle);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
2
);
assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET), 5);
}
#[test]
fn tedelete_removes_selection_without_touching_scrap_and_pops_arg() {
// IM:I I-387: TEDelete deletes selection without writing TextEdit scrap.
let (mut disp, mut cpu, mut bus) = setup();
let existing_scrap = TrapDispatcher::allocate_handle_with_data(&mut bus, 2);
let existing_ptr = bus.read_long(existing_scrap);
bus.write_bytes(existing_ptr, b"QQ");
bus.write_long(crate::memory::globals::addr::TE_SCRP_HANDLE, existing_scrap);
bus.write_word(crate::memory::globals::addr::TE_SCRP_LENGTH, 2);
let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
let te_ptr = bus.read_long(te_handle);
bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 1);
bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 4);
bus.write_long(TEST_SP, te_handle);
let result = disp.dispatch_dialog(true, 0x1D7, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
assert_eq!(
TrapDispatcher::te_text_bytes(&bus, te_handle),
b"HO".to_vec()
);
let te_ptr = bus.read_long(te_handle);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
1
);
assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET), 1);
assert_eq!(
bus.read_long(crate::memory::globals::addr::TE_SCRP_HANDLE),
existing_scrap
);
assert_eq!(
bus.read_word(crate::memory::globals::addr::TE_SCRP_LENGTH),
2
);
assert_eq!(bus.read_bytes(existing_ptr, 2), b"QQ".to_vec());
}
#[test]
fn tedelete_insertion_point_selection_is_noop_and_pops_arg() {
// IM:I I-387: insertion-point selection means "nothing happens".
let (mut disp, mut cpu, mut bus) = setup();
let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
let te_ptr = bus.read_long(te_handle);
bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 3);
bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 3);
bus.write_long(TEST_SP, te_handle);
let result = disp.dispatch_dialog(true, 0x1D7, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
assert_eq!(
TrapDispatcher::te_text_bytes(&bus, te_handle),
b"HELLO".to_vec()
);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
3
);
assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET), 3);
}
#[test]
fn teupdate_pascal_form_redraws_text_and_pops_inline_rect_and_tehandle() {
// IM:I I-387; Inside Macintosh: Text 1993, 2-88.
let (mut disp, mut cpu, mut bus) = setup_with_port();
let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
let (screen_base, row_bytes, _screen_w, _screen_h, _pixel_size) = disp.screen_mode;
for i in 0..(row_bytes * 80) {
bus.write_byte(screen_base + i, 0);
}
bus.write_long(TEST_SP, te_handle);
bus.write_word(TEST_SP + 4, 0); // rUpdate.top
bus.write_word(TEST_SP + 6, 0); // rUpdate.left
bus.write_word(TEST_SP + 8, 40); // rUpdate.bottom
bus.write_word(TEST_SP + 10, 120); // rUpdate.right
let result = disp.dispatch_dialog(true, 0x1D3, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 12);
assert_eq!(
TrapDispatcher::te_text_bytes(&bus, te_handle),
b"HELLO".to_vec()
);
let drew_any_pixel =
(0..(row_bytes * 80)).any(|offset| bus.read_byte(screen_base + offset) != 0);
assert!(
drew_any_pixel,
"TEUpdate should draw at least one non-white pixel for visible text"
);
}
#[test]
fn teupdate_systemless_theme_routes_active_caret_through_provider() {
let (mut classic, mut classic_cpu, mut classic_bus) = setup_with_port();
let classic_te_handle = make_te_with_text(&mut classic, &mut classic_bus, b"");
let classic_te_ptr = classic_bus.read_long(classic_te_handle);
classic_bus.write_word(classic_te_ptr + TrapDispatcher::TE_ACTIVE_OFFSET, 1);
classic_bus.write_word(classic_te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 0);
classic_bus.write_word(classic_te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 0);
let (screen_base, row_bytes, _screen_w, _screen_h, _pixel_size) = classic.screen_mode;
for i in 0..(row_bytes * 80) {
classic_bus.write_byte(screen_base + i, 0);
}
classic_bus.write_long(TEST_SP, classic_te_handle);
classic_bus.write_word(TEST_SP + 4, 0);
classic_bus.write_word(TEST_SP + 6, 0);
classic_bus.write_word(TEST_SP + 8, 40);
classic_bus.write_word(TEST_SP + 10, 120);
let result = classic.dispatch_dialog(true, 0x1D3, &mut classic_cpu, &mut classic_bus);
assert!(result.unwrap().is_ok());
let (mut themed, mut themed_cpu, mut themed_bus) = setup_with_port();
themed.set_ui_theme_id(UiThemeId::SystemlessDefault);
let themed_te_handle = make_te_with_text(&mut themed, &mut themed_bus, b"");
let themed_te_ptr = themed_bus.read_long(themed_te_handle);
themed_bus.write_word(themed_te_ptr + TrapDispatcher::TE_ACTIVE_OFFSET, 1);
themed_bus.write_word(themed_te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 0);
themed_bus.write_word(themed_te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 0);
for i in 0..(row_bytes * 80) {
themed_bus.write_byte(screen_base + i, 0);
}
themed_bus.write_long(TEST_SP, themed_te_handle);
themed_bus.write_word(TEST_SP + 4, 0);
themed_bus.write_word(TEST_SP + 6, 0);
themed_bus.write_word(TEST_SP + 8, 40);
themed_bus.write_word(TEST_SP + 10, 120);
let result = themed.dispatch_dialog(true, 0x1D3, &mut themed_cpu, &mut themed_bus);
assert!(result.unwrap().is_ok());
assert!(
screen_pixel_is_set(&classic_bus, screen_base, row_bytes, 1, 0),
"classic TEUpdate should draw a one-pixel caret for an active empty insertion point"
);
assert!(
screen_pixel_is_set(&themed_bus, screen_base, row_bytes, 0, 0),
"systemless-default TEUpdate should draw provider-owned caret chrome"
);
}
#[test]
fn teactivate_and_tedeactivate_repaint_empty_insertion_caret() {
// IM:I I-385 and Text 1993 p. 2-80: TEActivate displays the caret
// when the active selection is an insertion point; TEDeactivate
// removes it without changing the selection offsets.
let (mut disp, mut cpu, mut bus) = setup_with_port();
let te_handle = make_te_with_text(&mut disp, &mut bus, b"");
let te_ptr = bus.read_long(te_handle);
let (screen_base, row_bytes, _screen_w, _screen_h, _pixel_size) = disp.screen_mode;
for i in 0..(row_bytes * 80) {
bus.write_byte(screen_base + i, 0);
}
bus.write_word(te_ptr + TrapDispatcher::TE_ACTIVE_OFFSET, 0);
bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 0);
bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 0);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, te_handle);
let result = disp.dispatch_dialog(true, 0x1D8, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_ACTIVE_OFFSET), 1);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
0
);
assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET), 0);
assert!(
screen_pixel_is_set(&bus, screen_base, row_bytes, 1, 0),
"TEActivate should paint the visible insertion caret"
);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, te_handle);
let result = disp.dispatch_dialog(true, 0x1D9, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_ACTIVE_OFFSET), 0);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
0
);
assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET), 0);
assert!(
!screen_pixel_is_set(&bus, screen_base, row_bytes, 1, 0),
"TEDeactivate should erase the insertion caret"
);
}
#[test]
fn teupdate_classic_active_selection_inverts_selected_text_range() {
fn render_classic_selection(selected: bool) -> u64 {
let (mut disp, mut cpu, mut bus) = setup_with_port();
let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
let te_ptr = bus.read_long(te_handle);
bus.write_word(te_ptr + TrapDispatcher::TE_ACTIVE_OFFSET, 1);
bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 0);
bus.write_word(
te_ptr + TrapDispatcher::TE_SEL_END_OFFSET,
if selected { 3 } else { 0 },
);
let line_height = bus.read_word(te_ptr + TrapDispatcher::TE_LINE_HEIGHT_OFFSET) as i16;
let port = bus.read_long(te_ptr + TrapDispatcher::TE_IN_PORT_OFFSET);
let base = bus.read_long(port + 2);
let row_bytes = u32::from(bus.read_word(port + 6) & 0x3FFF);
for i in 0..(row_bytes * 80) {
bus.write_byte(base + i, 0);
}
bus.write_long(TEST_SP, te_handle);
bus.write_word(TEST_SP + 4, 0);
bus.write_word(TEST_SP + 6, 0);
bus.write_word(TEST_SP + 8, 40);
bus.write_word(TEST_SP + 10, 120);
let result = disp.dispatch_dialog(true, 0x1D3, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
sum_screen_bytes(&bus, base, row_bytes, 0, 0, line_height, 32)
}
// IM:I I-375/I-385 and Text 1993 p. 2-80: active non-empty TextEdit
// selections are highlighted. Classic highlighting is inverse video
// over the selected character range, so the selected render must
// differ from the same active insertion-point redraw.
let selected = render_classic_selection(true);
let unselected = render_classic_selection(false);
assert!(
selected != unselected,
"classic TEUpdate should invert active non-empty selection pixels; selected={selected}, unselected={unselected}"
);
}
#[test]
fn teupdate_systemless_theme_routes_multiline_selection_through_provider() {
fn render_themed_multiline_selection(selected: bool) -> (u16, u16, i16, u64, u64) {
let (mut disp, mut cpu, mut bus) = setup_with_port();
disp.set_ui_theme_id(UiThemeId::SystemlessDefault);
let te_handle = make_te_with_text(&mut disp, &mut bus, b"A\rB");
let te_ptr = bus.read_long(te_handle);
bus.write_word(te_ptr + TrapDispatcher::TE_ACTIVE_OFFSET, 1);
bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 0);
bus.write_word(
te_ptr + TrapDispatcher::TE_SEL_END_OFFSET,
if selected { 3 } else { 0 },
);
let line_height = bus.read_word(te_ptr + TrapDispatcher::TE_LINE_HEIGHT_OFFSET) as i16;
let (base, row_bytes, _screen_w, _screen_h, _pixel_size) = disp.screen_mode;
for i in 0..(row_bytes * 80) {
bus.write_byte(base + i, 0);
}
bus.write_long(TEST_SP, te_handle);
bus.write_word(TEST_SP + 4, 0);
bus.write_word(TEST_SP + 6, 0);
bus.write_word(TEST_SP + 8, 40);
bus.write_word(TEST_SP + 10, 120);
let result = disp.dispatch_dialog(true, 0x1D3, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
let first_line = sum_screen_bytes(&bus, base, row_bytes, 0, 0, line_height, 24);
let second_line =
sum_screen_bytes(&bus, base, row_bytes, line_height, 0, line_height * 2, 24);
(
bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET),
line_height,
first_line,
second_line,
)
}
let selected = render_themed_multiline_selection(true);
let unselected = render_themed_multiline_selection(false);
// Text 1993, appendix C: TERec.active marks whether the selection
// range is highlighted or the caret is displayed; selStart/selEnd are
// byte offsets into the contiguous selection range.
assert_eq!((selected.0, selected.1), (0, 3));
assert_eq!((unselected.0, unselected.1), (0, 0));
assert_eq!(selected.2, unselected.2);
assert!(
selected.4 != unselected.4,
"systemless-default TEUpdate should draw provider selection chrome on the second selected line"
);
}
#[test]
fn teupdate_systemless_theme_renders_reversed_selection_without_rewriting_offsets() {
fn render_themed_selection(sel_start: u16, sel_end: u16) -> (u16, u16, u16, i16, u32, u32) {
let (mut disp, mut cpu, mut bus) = setup_with_port();
disp.set_ui_theme_id(UiThemeId::SystemlessDefault);
let te_handle = make_te_with_text(&mut disp, &mut bus, b"A\rB");
let te_ptr = bus.read_long(te_handle);
bus.write_word(te_ptr + TrapDispatcher::TE_ACTIVE_OFFSET, 1);
bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, sel_start);
bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, sel_end);
let line_height = bus.read_word(te_ptr + TrapDispatcher::TE_LINE_HEIGHT_OFFSET) as i16;
let (base, row_bytes, _screen_w, _screen_h, _pixel_size) = disp.screen_mode;
for i in 0..(row_bytes * 80) {
bus.write_byte(base + i, 0);
}
bus.write_long(TEST_SP, te_handle);
bus.write_word(TEST_SP + 4, 0);
bus.write_word(TEST_SP + 6, 0);
bus.write_word(TEST_SP + 8, 40);
bus.write_word(TEST_SP + 10, 120);
let result = disp.dispatch_dialog(true, 0x1D3, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 12);
let first_line_bottom_band =
count_set_pixels(&bus, base, row_bytes, line_height - 2, 0, line_height, 24);
let second_line_bottom_band = count_set_pixels(
&bus,
base,
row_bytes,
line_height * 2 - 2,
0,
line_height * 2,
24,
);
(
bus.read_word(te_ptr + TrapDispatcher::TE_LENGTH_OFFSET),
bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET),
line_height,
first_line_bottom_band,
second_line_bottom_band,
)
}
let forward = render_themed_selection(0, 3);
let reversed = render_themed_selection(3, 0);
// IM:I I-385 says TESetSelect owns selection-range changes, while
// IM:I I-387 / Text 1993 p. 2-88 define TEUpdate as redraw. The theme
// path can normalize reversed endpoints for drawing but must not
// rewrite the guest TERec.
assert_eq!(forward.0, 3);
assert_eq!(reversed.0, 3);
assert_eq!((forward.1, forward.2), (0, 3));
assert_eq!((reversed.1, reversed.2), (3, 0));
assert_eq!(forward.3, reversed.3);
assert!(forward.4 > 0 && forward.5 > 0);
assert_eq!(
(reversed.4, reversed.5),
(forward.4, forward.5),
"systemless-default TEUpdate should render reversed selection ranges like their normalized range without rewriting selStart/selEnd"
);
}
#[test]
fn teupdate_systemless_theme_routes_inactive_outline_selection_through_provider() {
let (mut inactive_plain, mut inactive_plain_cpu, mut inactive_plain_bus) =
setup_with_port();
inactive_plain.set_ui_theme_id(UiThemeId::SystemlessDefault);
let inactive_plain_te_handle =
make_te_with_text(&mut inactive_plain, &mut inactive_plain_bus, b"HELLO");
let inactive_plain_te_ptr = inactive_plain_bus.read_long(inactive_plain_te_handle);
inactive_plain_bus.write_word(inactive_plain_te_ptr + TrapDispatcher::TE_ACTIVE_OFFSET, 0);
inactive_plain_bus.write_word(
inactive_plain_te_ptr + TrapDispatcher::TE_SEL_START_OFFSET,
0,
);
inactive_plain_bus.write_word(inactive_plain_te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 3);
let (plain_base, plain_row_bytes, _screen_w, _screen_h, _pixel_size) =
inactive_plain.screen_mode;
for i in 0..(plain_row_bytes * 80) {
inactive_plain_bus.write_byte(plain_base + i, 0);
}
inactive_plain_bus.write_long(TEST_SP, inactive_plain_te_handle);
inactive_plain_bus.write_word(TEST_SP + 4, 0);
inactive_plain_bus.write_word(TEST_SP + 6, 0);
inactive_plain_bus.write_word(TEST_SP + 8, 40);
inactive_plain_bus.write_word(TEST_SP + 10, 120);
let result = inactive_plain.dispatch_dialog(
true,
0x1D3,
&mut inactive_plain_cpu,
&mut inactive_plain_bus,
);
assert!(result.unwrap().is_ok());
let (mut inactive_outline, mut inactive_outline_cpu, mut inactive_outline_bus) =
setup_with_port();
inactive_outline.set_ui_theme_id(UiThemeId::SystemlessDefault);
let inactive_outline_te_handle =
make_te_with_text(&mut inactive_outline, &mut inactive_outline_bus, b"HELLO");
let inactive_outline_te_ptr = inactive_outline_bus.read_long(inactive_outline_te_handle);
inactive_outline_bus.write_word(
inactive_outline_te_ptr + TrapDispatcher::TE_ACTIVE_OFFSET,
0,
);
inactive_outline_bus.write_word(
inactive_outline_te_ptr + TrapDispatcher::TE_SEL_START_OFFSET,
0,
);
inactive_outline_bus.write_word(
inactive_outline_te_ptr + TrapDispatcher::TE_SEL_END_OFFSET,
3,
);
let inactive_line_height = inactive_outline_bus
.read_word(inactive_outline_te_ptr + TrapDispatcher::TE_LINE_HEIGHT_OFFSET)
as i16;
let (outline_base, outline_row_bytes, _screen_w, _screen_h, _pixel_size) =
inactive_outline.screen_mode;
for i in 0..(outline_row_bytes * 80) {
inactive_outline_bus.write_byte(outline_base + i, 0);
}
inactive_outline_cpu.write_reg(Register::A7, TEST_SP);
inactive_outline_bus.write_word(TEST_SP, 0x000E);
inactive_outline_bus.write_long(TEST_SP + 2, inactive_outline_te_handle);
inactive_outline_bus.write_word(TEST_SP + 6, TrapDispatcher::TE_BIT_SET as u16);
inactive_outline_bus.write_word(TEST_SP + 8, TrapDispatcher::TE_FEATURE_OUTLINE_HILITE);
inactive_outline_bus.write_word(TEST_SP + 10, 0xBEEF);
let result = inactive_outline.dispatch_dialog(
true,
0x03D,
&mut inactive_outline_cpu,
&mut inactive_outline_bus,
);
assert!(result.unwrap().is_ok());
assert_eq!(inactive_outline_bus.read_word(TEST_SP + 10), 0);
inactive_outline_cpu.write_reg(Register::A7, TEST_SP);
inactive_outline_bus.write_long(TEST_SP, inactive_outline_te_handle);
inactive_outline_bus.write_word(TEST_SP + 4, 0);
inactive_outline_bus.write_word(TEST_SP + 6, 0);
inactive_outline_bus.write_word(TEST_SP + 8, 40);
inactive_outline_bus.write_word(TEST_SP + 10, 120);
let result = inactive_outline.dispatch_dialog(
true,
0x1D3,
&mut inactive_outline_cpu,
&mut inactive_outline_bus,
);
assert!(result.unwrap().is_ok());
let (mut active, mut active_cpu, mut active_bus) = setup_with_port();
active.set_ui_theme_id(UiThemeId::SystemlessDefault);
let active_te_handle = make_te_with_text(&mut active, &mut active_bus, b"HELLO");
let active_te_ptr = active_bus.read_long(active_te_handle);
active_bus.write_word(active_te_ptr + TrapDispatcher::TE_ACTIVE_OFFSET, 1);
active_bus.write_word(active_te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 0);
active_bus.write_word(active_te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 3);
let active_line_height =
active_bus.read_word(active_te_ptr + TrapDispatcher::TE_LINE_HEIGHT_OFFSET) as i16;
let (active_base, active_row_bytes, _screen_w, _screen_h, _pixel_size) = active.screen_mode;
for i in 0..(active_row_bytes * 80) {
active_bus.write_byte(active_base + i, 0);
}
active_bus.write_long(TEST_SP, active_te_handle);
active_bus.write_word(TEST_SP + 4, 0);
active_bus.write_word(TEST_SP + 6, 0);
active_bus.write_word(TEST_SP + 8, 40);
active_bus.write_word(TEST_SP + 10, 120);
let result = active.dispatch_dialog(true, 0x1D3, &mut active_cpu, &mut active_bus);
assert!(result.unwrap().is_ok());
// Text 1993 describes outline highlighting as framing an inactive
// selection; TEActivate/TEDeactivate tie it to teFOutlineHilite.
let inactive_plain_top_band = count_set_pixels(
&inactive_plain_bus,
plain_base,
plain_row_bytes,
0,
0,
1,
24,
);
let inactive_outline_top_band = count_set_pixels(
&inactive_outline_bus,
outline_base,
outline_row_bytes,
0,
0,
1,
24,
);
assert_eq!(
inactive_plain_top_band, 0,
"inactive TextEdit selection without teFOutlineHilite should not draw provider selection chrome"
);
assert!(
inactive_outline_top_band > 0,
"inactive TextEdit selection with teFOutlineHilite should draw provider outline chrome"
);
assert_eq!(
inactive_outline_bus
.read_word(inactive_outline_te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
0
);
assert_eq!(
inactive_outline_bus
.read_word(inactive_outline_te_ptr + TrapDispatcher::TE_SEL_END_OFFSET),
3
);
assert_eq!(inactive_line_height, active_line_height);
let inactive_bottom_band = count_set_pixels(
&inactive_outline_bus,
outline_base,
outline_row_bytes,
inactive_line_height - 2,
0,
inactive_line_height,
24,
);
let active_bottom_band = count_set_pixels(
&active_bus,
active_base,
active_row_bytes,
active_line_height - 2,
0,
active_line_height,
24,
);
assert!(
active_bottom_band > inactive_bottom_band,
"inactive outline selection should stay outline-only instead of drawing active bottom-stripe chrome"
);
}
#[test]
fn teupdate_c_rect_pointer_form_is_accepted_and_pops_eight_bytes() {
// Inside Macintosh: Text 1993, 2-88 update semantics plus
// MPW C call-shape compatibility (Rect* + TEHandle).
let (mut disp, mut cpu, mut bus) = setup_with_port();
let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
let rect_ptr = bus.alloc(8);
bus.write_word(rect_ptr, 10);
bus.write_word(rect_ptr + 2, 10);
bus.write_word(rect_ptr + 4, 40);
bus.write_word(rect_ptr + 6, 120);
bus.write_long(TEST_SP, te_handle);
bus.write_long(TEST_SP + 4, rect_ptr);
let result = disp.dispatch_dialog(true, 0x1D3, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
assert_eq!(
TrapDispatcher::te_text_bytes(&bus, te_handle),
b"HELLO".to_vec()
);
}
#[test]
fn teupdate_preserves_te_record_state_across_redraw() {
// TEUpdate is a redraw operation and MUST NOT mutate hText,
// teLength, selStart, or selEnd. Per IM:I I-387 + IM:Text 1993
// p. 2-88: text bytes "HELLO" preserved, teLength==5,
// selStart==1, selEnd==4 after both C-form and Pascal-form
// TEUpdate calls.
let (mut disp, mut cpu, mut bus) = setup_with_port();
let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
let te_ptr = bus.read_long(te_handle);
bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 1);
bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 4);
// C-form dispatch (8-byte pop).
let rect_ptr = bus.alloc(8);
bus.write_word(rect_ptr, 10);
bus.write_word(rect_ptr + 2, 10);
bus.write_word(rect_ptr + 4, 40);
bus.write_word(rect_ptr + 6, 120);
bus.write_long(TEST_SP, te_handle);
bus.write_long(TEST_SP + 4, rect_ptr);
let result = disp.dispatch_dialog(true, 0x1D3, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
// Pascal-form dispatch on the same TE record (12-byte pop):
// hTE at sp+0, inlined rect at sp+4..sp+11.
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, te_handle);
bus.write_word(TEST_SP + 4, 10);
bus.write_word(TEST_SP + 6, 10);
bus.write_word(TEST_SP + 8, 40);
bus.write_word(TEST_SP + 10, 120);
let result = disp.dispatch_dialog(true, 0x1D3, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 12);
// State preservation contract.
assert_eq!(
TrapDispatcher::te_text_bytes(&bus, te_handle),
b"HELLO".to_vec()
);
assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_LENGTH_OFFSET), 5);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
1
);
assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET), 4);
}
#[test]
fn teactivate_sets_active_flag_preserves_selection_and_pops_arg() {
// IM:I I-385; Inside Macintosh: Text 1993, 2-80.
let (mut disp, mut cpu, mut bus) = setup();
let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
let te_ptr = bus.read_long(te_handle);
bus.write_word(te_ptr + TrapDispatcher::TE_ACTIVE_OFFSET, 0);
bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 1);
bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 4);
bus.write_long(TEST_SP, te_handle);
let result = disp.dispatch_dialog(true, 0x1D8, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_ACTIVE_OFFSET), 1);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
1
);
assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET), 4);
}
#[test]
fn tedeactivate_clears_active_flag_preserves_selection_and_pops_arg() {
// IM:I I-385; Inside Macintosh: Text 1993, 2-80.
let (mut disp, mut cpu, mut bus) = setup();
let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
let te_ptr = bus.read_long(te_handle);
bus.write_word(te_ptr + TrapDispatcher::TE_ACTIVE_OFFSET, 1);
bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 2);
bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 5);
bus.write_long(TEST_SP, te_handle);
let result = disp.dispatch_dialog(true, 0x1D9, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_ACTIVE_OFFSET), 0);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
2
);
assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET), 5);
}
#[test]
fn teclick_consumes_point_extend_and_tehandle_arguments() {
// Inside Macintosh Volume I (1985), p. I-376 and
// Inside Macintosh: Text (1993), p. 2-85.
let (mut disp, mut cpu, mut bus) = setup();
let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
bus.write_long(TEST_SP, te_handle); // hTE
bus.write_word(TEST_SP + 4, 0xFF00); // extend=TRUE in high byte
bus.write_word(TEST_SP + 6, 18); // pt.v
bus.write_word(TEST_SP + 8, 27); // pt.h
let result = disp.dispatch_dialog(true, 0x1D4, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 10);
}
#[test]
fn teclick_empty_text_keeps_insertion_point_at_zero() {
// Inside Macintosh Volume I (1985), p. I-376: TEClick places the
// insertion point from mouse position; with empty text only offset 0
// is valid.
let (mut disp, mut cpu, mut bus) = setup();
let te_handle = make_te_with_text(&mut disp, &mut bus, b"");
let te_ptr = bus.read_long(te_handle);
bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 0);
bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 0);
bus.write_long(TEST_SP, te_handle); // hTE
bus.write_word(TEST_SP + 4, 0x0000); // extend=FALSE
bus.write_word(TEST_SP + 6, 12); // pt.v
bus.write_word(TEST_SP + 8, 40); // pt.h
let result = disp.dispatch_dialog(true, 0x1D4, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 10);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
0
);
assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET), 0);
}
#[test]
fn teclick_preserves_terec_active_flag_and_telength() {
// Both BasiliskII System 7.5.3 ROM and Systemless HLE agree that
// TEClick must not mutate the active flag (owned by
// TEActivate/TEDeactivate per IM:I I-385) or teLength
// (owned by TESetText/TEKey/TEDelete/TEInsert). Inside
// Macintosh Volume I (1985), p. I-376; Inside Macintosh:
// Text (1993), p. 2-85.
let (mut disp, mut cpu, mut bus) = setup();
let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
let te_ptr = bus.read_long(te_handle);
bus.write_word(te_ptr + TrapDispatcher::TE_ACTIVE_OFFSET, 0xFF);
bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 2);
bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 4);
let pre_active = bus.read_word(te_ptr + TrapDispatcher::TE_ACTIVE_OFFSET);
let pre_telength = bus.read_word(te_ptr + TrapDispatcher::TE_LENGTH_OFFSET);
bus.write_long(TEST_SP, te_handle);
bus.write_word(TEST_SP + 4, 0x0000); // extend=FALSE
bus.write_word(TEST_SP + 6, 8); // pt.v
bus.write_word(TEST_SP + 8, 32); // pt.h
let result = disp.dispatch_dialog(true, 0x1D4, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 10);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_ACTIVE_OFFSET),
pre_active
);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_LENGTH_OFFSET),
pre_telength
);
}
#[test]
fn teclick_repeated_calls_balance_stack_no_drift() {
// An 8-call composition of TEClick keeps the A7 advance balanced at
// exactly 8 * 10 = 80 bytes; per-call pop errors that cancel
// within a single call but drift over many invocations are
// detected here. A7 is reset to TEST_SP between iterations so
// each call exercises the SP discipline independently.
let (mut disp, mut cpu, mut bus) = setup();
let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
for k in 0..8i16 {
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, te_handle);
bus.write_word(TEST_SP + 4, 0x0000); // extend=FALSE
bus.write_word(TEST_SP + 6, (4 + k) as u16); // pt.v varied
bus.write_word(TEST_SP + 8, (16 + k * 3) as u16); // pt.h varied
let result = disp.dispatch_dialog(true, 0x1D4, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 10);
}
}
fn textedit_selection_replacement_for_theme(
theme_id: UiThemeId,
) -> (Vec<u8>, u16, u16, u16, u32) {
let (mut disp, mut cpu, mut bus) = setup_with_port();
disp.set_ui_theme_id(theme_id);
let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
let te_ptr = bus.read_long(te_handle);
bus.write_word(te_ptr + TrapDispatcher::TE_ACTIVE_OFFSET, 1);
bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 1);
bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 4);
bus.write_long(TEST_SP, te_handle);
bus.write_word(TEST_SP + 4, 0);
bus.write_word(TEST_SP + 6, 0);
bus.write_word(TEST_SP + 8, 40);
bus.write_word(TEST_SP + 10, 120);
let result = disp.dispatch_dialog(true, 0x1D3, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 12);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, te_handle);
bus.write_word(TEST_SP + 4, u16::from(b'Y'));
let result = disp.dispatch_dialog(true, 0x1DC, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
(
TrapDispatcher::te_text_bytes(&bus, te_handle),
bus.read_word(te_ptr + TrapDispatcher::TE_LENGTH_OFFSET),
bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET),
cpu.read_reg(Register::A7),
)
}
fn textedit_private_scrap_editing_results_for_theme(
theme_id: UiThemeId,
) -> TextEditPrivateScrapEditingSnapshot {
let (mut disp, mut cpu, mut bus) = setup();
disp.set_ui_theme_id(theme_id);
let copy_te = make_te_with_text(&mut disp, &mut bus, b"HELLO");
let copy_ptr = bus.read_long(copy_te);
bus.write_word(copy_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 1);
bus.write_word(copy_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 4);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, copy_te);
disp.dispatch_dialog(true, 0x1D5, &mut cpu, &mut bus)
.unwrap()
.unwrap();
let copy = textedit_scrap_edit_case(&bus, copy_te, cpu.read_reg(Register::A7));
let cut_te = make_te_with_text(&mut disp, &mut bus, b"HELLO");
let cut_ptr = bus.read_long(cut_te);
bus.write_word(cut_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 1);
bus.write_word(cut_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 4);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, cut_te);
disp.dispatch_dialog(true, 0x1D6, &mut cpu, &mut bus)
.unwrap()
.unwrap();
let cut = textedit_scrap_edit_case(&bus, cut_te, cpu.read_reg(Register::A7));
let paste_te = make_te_with_text(&mut disp, &mut bus, b"HEXXO");
let paste_ptr = bus.read_long(paste_te);
bus.write_word(paste_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 2);
bus.write_word(paste_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 4);
let paste_scrap = TrapDispatcher::allocate_handle_with_data(&mut bus, 2);
let paste_scrap_ptr = bus.read_long(paste_scrap);
bus.write_bytes(paste_scrap_ptr, b"LL");
bus.write_long(crate::memory::globals::addr::TE_SCRP_HANDLE, paste_scrap);
bus.write_word(crate::memory::globals::addr::TE_SCRP_LENGTH, 2);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, paste_te);
disp.dispatch_dialog(true, 0x1DB, &mut cpu, &mut bus)
.unwrap()
.unwrap();
let paste = textedit_scrap_edit_case(&bus, paste_te, cpu.read_reg(Register::A7));
let delete_te = make_te_with_text(&mut disp, &mut bus, b"HELLO");
let delete_ptr = bus.read_long(delete_te);
bus.write_word(delete_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 1);
bus.write_word(delete_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 4);
let delete_scrap = TrapDispatcher::allocate_handle_with_data(&mut bus, 2);
let delete_scrap_ptr = bus.read_long(delete_scrap);
bus.write_bytes(delete_scrap_ptr, b"QQ");
bus.write_long(crate::memory::globals::addr::TE_SCRP_HANDLE, delete_scrap);
bus.write_word(crate::memory::globals::addr::TE_SCRP_LENGTH, 2);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, delete_te);
disp.dispatch_dialog(true, 0x1D7, &mut cpu, &mut bus)
.unwrap()
.unwrap();
let delete = textedit_scrap_edit_case(&bus, delete_te, cpu.read_reg(Register::A7));
TextEditPrivateScrapEditingSnapshot {
copy,
cut,
paste,
delete,
}
}
#[test]
fn systemless_theme_does_not_change_textedit_selection_offsets() {
// Text 1993 appendix C defines selStart/selEnd as byte offsets into
// the selection range; Text 1993 p. 2-81 says TEKey replaces that
// range and positions the insertion point just past the inserted byte.
// Theme selection chrome must not change those guest-visible fields.
let classic = textedit_selection_replacement_for_theme(UiThemeId::ClassicSystem7);
let themed = textedit_selection_replacement_for_theme(UiThemeId::SystemlessDefault);
assert_eq!(classic.0, b"HYO".to_vec());
assert_eq!(classic.1, 3);
assert_eq!(classic.2, 2);
assert_eq!(classic.3, 2);
assert_eq!(classic.4, TEST_SP + 6);
assert_eq!(
themed, classic,
"systemless-default must not change TextEdit replacement selection offsets"
);
}
#[test]
fn systemless_theme_does_not_change_textedit_private_scrap_editing() {
// IM:I I-373 says TextEdit's scrap is private to TextEdit, not the
// Scrap Manager desk scrap. IM:I I-385..I-387 define TECopy, TECut,
// TEPaste, and TEDelete selection/scrap effects; MTE 1992 p.
// 6-132..6-134 routes dialog edit commands through those TextEdit
// operations. Theme chrome must not change the private-scrap globals,
// TERec text/selection fields, or stack ABI for these edit actions.
let classic = textedit_private_scrap_editing_results_for_theme(UiThemeId::ClassicSystem7);
let themed = textedit_private_scrap_editing_results_for_theme(UiThemeId::SystemlessDefault);
assert_eq!(classic.copy.text, b"HELLO".to_vec());
assert_eq!(classic.copy.selection, (1, 4));
assert_eq!(classic.copy.scrap_length, 3);
assert_eq!(classic.copy.scrap_bytes, b"ELL".to_vec());
assert_eq!(classic.copy.stack_after, TEST_SP + 4);
assert_eq!(classic.cut.text, b"HO".to_vec());
assert_eq!(classic.cut.selection, (1, 1));
assert_eq!(classic.cut.scrap_length, 3);
assert_eq!(classic.cut.scrap_bytes, b"ELL".to_vec());
assert_eq!(classic.cut.stack_after, TEST_SP + 4);
assert_eq!(classic.paste.text, b"HELLO".to_vec());
assert_eq!(classic.paste.selection, (4, 4));
assert_eq!(classic.paste.scrap_length, 2);
assert_eq!(classic.paste.scrap_bytes, b"LL".to_vec());
assert_eq!(classic.paste.stack_after, TEST_SP + 4);
assert_eq!(classic.delete.text, b"HO".to_vec());
assert_eq!(classic.delete.selection, (1, 1));
assert_eq!(classic.delete.scrap_length, 2);
assert_eq!(classic.delete.scrap_bytes, b"QQ".to_vec());
assert_eq!(classic.delete.stack_after, TEST_SP + 4);
assert_eq!(
themed, classic,
"systemless-default must not change TextEdit private-scrap edit semantics"
);
}
#[test]
fn teidle_consumes_tehandle_argument() {
// Inside Macintosh Volume I (1985), p. I-374 and
// Inside Macintosh: Text (1993), p. 2-84.
let (mut disp, mut cpu, mut bus) = setup();
let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
bus.write_long(TEST_SP, te_handle);
let result = disp.dispatch_dialog(true, 0x1DA, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
}
#[test]
fn teidle_toggles_visible_insertion_caret_after_blink_interval() {
// IM:I I-374 and Text 1993 p. 2-84: TEIdle blinks an active
// insertion caret, but only after the minimum blink interval
// initially set to 32 ticks.
let (mut disp, mut cpu, mut bus) = setup_with_port();
let te_handle = make_te_with_text(&mut disp, &mut bus, b"");
let te_ptr = bus.read_long(te_handle);
let (screen_base, row_bytes, _screen_w, _screen_h, _pixel_size) = disp.screen_mode;
for i in 0..(row_bytes * 80) {
bus.write_byte(screen_base + i, 0);
}
bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 0);
bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 0);
disp.tick_count = 100;
bus.write_long(crate::memory::globals::addr::TICKS, 100);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, te_handle);
let result = disp.dispatch_dialog(true, 0x1D8, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
assert!(
screen_pixel_is_set(&bus, screen_base, row_bytes, 1, 0),
"TEActivate should show the insertion caret before TEIdle"
);
assert_eq!(
bus.read_long(te_ptr + TrapDispatcher::TE_CARET_TIME_OFFSET),
100
);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_CARET_STATE_OFFSET),
0
);
disp.tick_count = 131;
bus.write_long(crate::memory::globals::addr::TICKS, 131);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, te_handle);
let result = disp.dispatch_dialog(true, 0x1DA, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
assert!(
screen_pixel_is_set(&bus, screen_base, row_bytes, 1, 0),
"TEIdle before 32 ticks should leave the caret visible"
);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_CARET_STATE_OFFSET),
0
);
disp.tick_count = 132;
bus.write_long(crate::memory::globals::addr::TICKS, 132);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, te_handle);
let result = disp.dispatch_dialog(true, 0x1DA, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
assert!(
!screen_pixel_is_set(&bus, screen_base, row_bytes, 1, 0),
"TEIdle at the 32-tick boundary should hide the caret"
);
assert_eq!(
bus.read_long(te_ptr + TrapDispatcher::TE_CARET_TIME_OFFSET),
132
);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_CARET_STATE_OFFSET),
1
);
disp.tick_count = 164;
bus.write_long(crate::memory::globals::addr::TICKS, 164);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, te_handle);
let result = disp.dispatch_dialog(true, 0x1DA, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
assert!(
screen_pixel_is_set(&bus, screen_base, row_bytes, 1, 0),
"the next elapsed interval should show the caret again"
);
assert_eq!(
bus.read_long(te_ptr + TrapDispatcher::TE_CARET_TIME_OFFSET),
164
);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_CARET_STATE_OFFSET),
0
);
}
#[test]
fn teidle_preserves_active_flag_and_selection_offsets() {
// Inside Macintosh Volume I (1985), pp. I-374 and I-385: TEIdle
// blinks caret for an active record, while TEActivate/TEDeactivate
// control active state. The text length must also remain
// unchanged across an idle call.
let (mut disp, mut cpu, mut bus) = setup();
let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
let te_ptr = bus.read_long(te_handle);
bus.write_word(te_ptr + TrapDispatcher::TE_ACTIVE_OFFSET, 0xFF);
bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 2);
bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 4);
let pre_telength = bus.read_word(te_ptr + TrapDispatcher::TE_LENGTH_OFFSET);
bus.write_long(TEST_SP, te_handle);
let result = disp.dispatch_dialog(true, 0x1DA, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_ACTIVE_OFFSET),
0xFF
);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
2
);
assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET), 4);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_LENGTH_OFFSET),
pre_telength
);
}
#[test]
fn teidle_repeated_calls_balance_stack_and_preserve_terec_state() {
// Inside Macintosh Volume I (1985), p. I-374: TEIdle is intended
// to be called on every null event from the application's event
// loop, so per-call pop discipline must compose cleanly across
// many invocations and the TERec must remain bit-for-bit
// identical across the sequence: 8 successive TEIdle calls,
// assert A7 advanced exactly 8 * 4 = 32 bytes AND active /
// selStart / selEnd / teLength are all preserved.
let (mut disp, mut cpu, mut bus) = setup();
let te_handle = make_te_with_text(&mut disp, &mut bus, b"WORLD");
let te_ptr = bus.read_long(te_handle);
bus.write_word(te_ptr + TrapDispatcher::TE_ACTIVE_OFFSET, 0xFF);
bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 1);
bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 3);
let pre_active = bus.read_word(te_ptr + TrapDispatcher::TE_ACTIVE_OFFSET);
let pre_sel_start = bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET);
let pre_sel_end = bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET);
let pre_telength = bus.read_word(te_ptr + TrapDispatcher::TE_LENGTH_OFFSET);
for _ in 0..8 {
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, te_handle);
let result = disp.dispatch_dialog(true, 0x1DA, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
}
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_ACTIVE_OFFSET),
pre_active
);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
pre_sel_start
);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET),
pre_sel_end
);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_LENGTH_OFFSET),
pre_telength
);
}
#[test]
fn tescroll_offsets_destrect_by_requested_delta_and_pops_args() {
// IM:I I-388; Inside Macintosh: Text 1993, 2-91.
let (mut disp, mut cpu, mut bus) = setup_with_port();
let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
let te_ptr = bus.read_long(te_handle);
bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET, 10);
bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 2, 20);
bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 4, 70);
bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 6, 180);
bus.write_long(TEST_SP, te_handle);
bus.write_word(TEST_SP + 4, (-3i16) as u16); // dv
bus.write_word(TEST_SP + 6, 5); // dh
let result = disp.dispatch_dialog(true, 0x1DD, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET),
7
);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 2),
25
);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 4),
67
);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 6),
185
);
}
#[test]
fn teinsert_inserts_before_selection_shifts_range_and_preserves_scrap() {
// IM:I I-387; Inside Macintosh: Text 1993, 2-94. TEInsert splices
// the supplied bytes at the selStart offset (BEFORE the selection)
// without replacing the selected range. The selection range is
// preserved logically — selStart and selEnd shift forward by the
// inserted length so the original selected characters still fall
// inside the range.
let (mut disp, mut cpu, mut bus) = setup();
let existing_scrap = TrapDispatcher::allocate_handle_with_data(&mut bus, 2);
let existing_scrap_ptr = bus.read_long(existing_scrap);
bus.write_bytes(existing_scrap_ptr, b"QQ");
bus.write_long(crate::memory::globals::addr::TE_SCRP_HANDLE, existing_scrap);
bus.write_word(crate::memory::globals::addr::TE_SCRP_LENGTH, 2);
let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
let te_ptr = bus.read_long(te_handle);
bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 1);
bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 4);
let insert_ptr = bus.alloc(3);
bus.write_bytes(insert_ptr, b"XYZ");
bus.write_long(TEST_SP, te_handle);
bus.write_long(TEST_SP + 4, 3);
bus.write_long(TEST_SP + 8, insert_ptr);
let result = disp.dispatch_dialog(true, 0x1DE, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 12);
assert_eq!(
TrapDispatcher::te_text_bytes(&bus, te_handle),
b"HXYZELLO".to_vec()
);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
4
);
assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET), 7);
assert_eq!(
bus.read_long(crate::memory::globals::addr::TE_SCRP_HANDLE),
existing_scrap
);
assert_eq!(
bus.read_word(crate::memory::globals::addr::TE_SCRP_LENGTH),
2
);
assert_eq!(bus.read_bytes(existing_scrap_ptr, 2), b"QQ".to_vec());
}
#[test]
fn teinsert_insertion_point_inserts_before_caret_and_shifts_caret() {
// TEInsert inserts immediately before the current selection/insertion
// point per IM:I I-387; Inside Macintosh: Text 1993, 2-94. The
// selection range is preserved logically — the insertion point shifts
// forward by the inserted length so it continues to mark the same
// place relative to the surrounding original characters.
let (mut disp, mut cpu, mut bus) = setup();
let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
let te_ptr = bus.read_long(te_handle);
bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 2);
bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 2);
let insert_ptr = bus.alloc(1);
bus.write_byte(insert_ptr, b'X');
bus.write_long(TEST_SP, te_handle);
bus.write_long(TEST_SP + 4, 1);
bus.write_long(TEST_SP + 8, insert_ptr);
let result = disp.dispatch_dialog(true, 0x1DE, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 12);
assert_eq!(
TrapDispatcher::te_text_bytes(&bus, te_handle),
b"HEXLLO".to_vec()
);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
3
);
assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET), 3);
}
#[test]
fn tesetalignment_writes_just_field_and_pops_args() {
// IM:I I-388 (TESetJust) and Inside Macintosh: Text 1993, 2-87.
let (mut disp, mut cpu, mut bus) = setup();
let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
let te_ptr = bus.read_long(te_handle);
bus.write_word(te_ptr + TrapDispatcher::TE_JUST_OFFSET, 0);
bus.write_long(TEST_SP, te_handle);
bus.write_word(TEST_SP + 4, (-1i16) as u16);
let result = disp.dispatch_dialog(true, 0x1DF, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_JUST_OFFSET) as i16,
-1
);
}
// ---- TECopy / TECut / TEPaste ($A9D5 / $A9D6 / $A9DB) ----
#[test]
fn tecopy_nonempty_selection_updates_textedit_scrap_globals() {
// IM:I I-386 + I-389.
let (mut disp, mut cpu, mut bus) = setup();
let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
let te_ptr = bus.read_long(te_handle);
bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 1);
bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 4);
bus.write_long(TEST_SP, te_handle);
let result = disp.dispatch_dialog(true, 0x1D5, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
let scrap_handle = bus.read_long(crate::memory::globals::addr::TE_SCRP_HANDLE);
let scrap_ptr = bus.read_long(scrap_handle);
assert_ne!(scrap_handle, 0);
assert_ne!(scrap_ptr, 0);
assert_eq!(
bus.read_word(crate::memory::globals::addr::TE_SCRP_LENGTH),
3
);
assert_eq!(bus.read_bytes(scrap_ptr, 3), b"ELL".to_vec());
}
#[test]
fn tecopy_empty_selection_preserves_existing_scrap_contents() {
// IM:I I-386: no selected range => no copy.
let (mut disp, mut cpu, mut bus) = setup();
let existing_scrap = TrapDispatcher::allocate_handle_with_data(&mut bus, 2);
let existing_ptr = bus.read_long(existing_scrap);
bus.write_bytes(existing_ptr, b"ZZ");
bus.write_long(crate::memory::globals::addr::TE_SCRP_HANDLE, existing_scrap);
bus.write_word(crate::memory::globals::addr::TE_SCRP_LENGTH, 2);
let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
let te_ptr = bus.read_long(te_handle);
bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 2);
bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 2);
bus.write_long(TEST_SP, te_handle);
let result = disp.dispatch_dialog(true, 0x1D5, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
assert_eq!(
bus.read_long(crate::memory::globals::addr::TE_SCRP_HANDLE),
existing_scrap
);
assert_eq!(
bus.read_word(crate::memory::globals::addr::TE_SCRP_LENGTH),
2
);
assert_eq!(bus.read_bytes(existing_ptr, 2), b"ZZ".to_vec());
}
#[test]
fn tecut_nonempty_selection_copies_to_scrap_and_deletes_selected_text() {
// IM:I I-391 + I-389.
let (mut disp, mut cpu, mut bus) = setup();
let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
let te_ptr = bus.read_long(te_handle);
bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 1);
bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 4);
bus.write_long(TEST_SP, te_handle);
let result = disp.dispatch_dialog(true, 0x1D6, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
assert_eq!(
TrapDispatcher::te_text_bytes(&bus, te_handle),
b"HO".to_vec()
);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
1
);
assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET), 1);
let scrap_handle = bus.read_long(crate::memory::globals::addr::TE_SCRP_HANDLE);
let scrap_ptr = bus.read_long(scrap_handle);
assert_eq!(
bus.read_word(crate::memory::globals::addr::TE_SCRP_LENGTH),
3
);
assert_eq!(bus.read_bytes(scrap_ptr, 3), b"ELL".to_vec());
}
#[test]
fn tecut_empty_selection_preserves_text_and_scrap_contents() {
// IM:I I-391: insertion-point selection performs no cut.
let (mut disp, mut cpu, mut bus) = setup();
let existing_scrap = TrapDispatcher::allocate_handle_with_data(&mut bus, 2);
let existing_ptr = bus.read_long(existing_scrap);
bus.write_bytes(existing_ptr, b"QQ");
bus.write_long(crate::memory::globals::addr::TE_SCRP_HANDLE, existing_scrap);
bus.write_word(crate::memory::globals::addr::TE_SCRP_LENGTH, 2);
let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
let te_ptr = bus.read_long(te_handle);
bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 3);
bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 3);
bus.write_long(TEST_SP, te_handle);
let result = disp.dispatch_dialog(true, 0x1D6, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
assert_eq!(
TrapDispatcher::te_text_bytes(&bus, te_handle),
b"HELLO".to_vec()
);
assert_eq!(
bus.read_long(crate::memory::globals::addr::TE_SCRP_HANDLE),
existing_scrap
);
assert_eq!(
bus.read_word(crate::memory::globals::addr::TE_SCRP_LENGTH),
2
);
assert_eq!(bus.read_bytes(existing_ptr, 2), b"QQ".to_vec());
}
#[test]
fn tepaste_nonempty_scrap_replaces_selection_and_advances_insertion_point() {
// IM:I I-387 + I-389.
let (mut disp, mut cpu, mut bus) = setup();
let te_handle = make_te_with_text(&mut disp, &mut bus, b"HLO");
let te_ptr = bus.read_long(te_handle);
bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 1);
bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 1);
let scrap_handle = TrapDispatcher::allocate_handle_with_data(&mut bus, 2);
let scrap_ptr = bus.read_long(scrap_handle);
bus.write_bytes(scrap_ptr, b"EL");
bus.write_long(crate::memory::globals::addr::TE_SCRP_HANDLE, scrap_handle);
bus.write_word(crate::memory::globals::addr::TE_SCRP_LENGTH, 2);
bus.write_long(TEST_SP, te_handle);
let result = disp.dispatch_dialog(true, 0x1DB, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
assert_eq!(
TrapDispatcher::te_text_bytes(&bus, te_handle),
b"HELLO".to_vec()
);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
3
);
assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET), 3);
}
#[test]
fn tepaste_empty_scrap_is_noop() {
// IM:I I-387: empty scrap inserts nothing.
let (mut disp, mut cpu, mut bus) = setup();
let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
let te_ptr = bus.read_long(te_handle);
bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 1);
bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 3);
let scrap_handle = TrapDispatcher::allocate_handle_with_data(&mut bus, 2);
let scrap_ptr = bus.read_long(scrap_handle);
bus.write_bytes(scrap_ptr, b"ZZ");
bus.write_long(crate::memory::globals::addr::TE_SCRP_HANDLE, scrap_handle);
bus.write_word(crate::memory::globals::addr::TE_SCRP_LENGTH, 0);
bus.write_long(TEST_SP, te_handle);
let result = disp.dispatch_dialog(true, 0x1DB, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
assert_eq!(
TrapDispatcher::te_text_bytes(&bus, te_handle),
b"HELLO".to_vec()
);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
1
);
assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET), 3);
}
#[test]
fn te_new_initializes_basic_record_fields() {
let (mut disp, mut cpu, mut bus) = setup();
bus.write_word(TEST_SP, 0); // viewRect.top
bus.write_word(TEST_SP + 2, 0); // viewRect.left
bus.write_word(TEST_SP + 4, 100); // viewRect.bottom
bus.write_word(TEST_SP + 6, 120); // viewRect.right
bus.write_word(TEST_SP + 8, 10); // destRect.top
bus.write_word(TEST_SP + 10, 20); // destRect.left
bus.write_word(TEST_SP + 12, 90); // destRect.bottom
bus.write_word(TEST_SP + 14, 80); // destRect.right
let result = disp.dispatch_dialog(true, 0x1D2, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 16);
let te_handle = bus.read_long(TEST_SP + 16);
let te_ptr = bus.read_long(te_handle);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET),
10
);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 2),
20
);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 4),
100
);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 6),
120
);
assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_LENGTH_OFFSET), 0);
let h_text = bus.read_long(te_ptr + TrapDispatcher::TE_HTEXT_OFFSET);
assert_ne!(h_text, 0);
assert_eq!(bus.read_long(h_text), 0);
}
// Inside Macintosh: Text (1993), p. 2-78: TEStyleNew returns a
// multistyled TEHandle, uses -1 sentinels in txSize/lineHeight/
// fontAscent, and installs style metadata.
#[test]
fn testylenew_returns_styled_handle_and_initializes_sentinel_fields() {
let (mut disp, mut cpu, mut bus) = setup();
bus.write_word(TEST_SP, 0); // viewRect.top
bus.write_word(TEST_SP + 2, 0); // viewRect.left
bus.write_word(TEST_SP + 4, 100); // viewRect.bottom
bus.write_word(TEST_SP + 6, 120); // viewRect.right
bus.write_word(TEST_SP + 8, 10); // destRect.top
bus.write_word(TEST_SP + 10, 20); // destRect.left
bus.write_word(TEST_SP + 12, 90); // destRect.bottom
bus.write_word(TEST_SP + 14, 80); // destRect.right
let result = disp.dispatch_dialog(true, 0x03E, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(
cpu.read_reg(Register::A7),
TEST_SP + 16,
"TEStyleNew should pop two Rect arguments (16 bytes)"
);
let te_handle = bus.read_long(TEST_SP + 16);
assert_ne!(te_handle, 0, "TEStyleNew should return a non-NIL TEHandle");
let te_ptr = bus.read_long(te_handle);
assert_ne!(te_ptr, 0, "returned TEHandle should point to a TERec");
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_TX_SIZE_OFFSET),
0xFFFF
);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_LINE_HEIGHT_OFFSET),
0xFFFF
);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_FONT_ASCENT_OFFSET),
0xFFFF
);
let style_handle = bus.read_long(te_ptr + TrapDispatcher::TE_TX_FONT_OFFSET);
assert_ne!(style_handle, 0, "styled record should carry a style handle");
let style_ptr = bus.read_long(style_handle);
assert_ne!(
style_ptr, 0,
"style handle should dereference to a style record"
);
assert_ne!(
bus.read_long(style_ptr + TrapDispatcher::TE_STYLE_NULL_STYLE_OFFSET),
0,
"style record should include a non-NIL null-style handle"
);
}
// ---- TEStyleNew (pointer-arg convention) ----
// Inside Macintosh: Text (1993), p. 2-78: TEStyleNew accepts a
// pointer-arg convention `const Rect *destRect, const Rect *viewRect`
// per MPW Universal Headers. Pascal LR push: destRect_ptr at SP+4
// (deepest, pushed first), viewRect_ptr at SP+0 (shallowest, pushed
// last). Net pop is 8 bytes.
#[test]
fn testylenew_pointer_arg_convention_initializes_destrect_viewrect_and_styled_sentinels() {
let (mut disp, mut cpu, mut bus) = setup();
// Build distinct destRect and viewRect in guest memory.
let dest_rect_ptr: u32 = 0x1A0000;
let view_rect_ptr: u32 = 0x1A0010;
bus.write_word(dest_rect_ptr, (-1200_i16) as u16);
bus.write_word(dest_rect_ptr + 2, (-1200_i16) as u16);
bus.write_word(dest_rect_ptr + 4, (-900_i16) as u16);
bus.write_word(dest_rect_ptr + 6, (-1000_i16) as u16);
bus.write_word(view_rect_ptr, (-1100_i16) as u16);
bus.write_word(view_rect_ptr + 2, (-1300_i16) as u16);
bus.write_word(view_rect_ptr + 4, (-800_i16) as u16);
bus.write_word(view_rect_ptr + 6, (-900_i16) as u16);
// Pascal LR push: viewRect_ptr at SP+0 (last pushed),
// destRect_ptr at SP+4 (first pushed).
bus.write_long(TEST_SP, view_rect_ptr);
bus.write_long(TEST_SP + 4, dest_rect_ptr);
let result = disp.dispatch_dialog(true, 0x03E, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(
cpu.read_reg(Register::A7),
TEST_SP + 8,
"TEStyleNew pointer convention should pop 8 bytes (two pointer args)"
);
let te_handle = bus.read_long(TEST_SP + 8);
assert_ne!(te_handle, 0, "TEStyleNew should return a non-NIL TEHandle");
let te_ptr = bus.read_long(te_handle);
assert_ne!(te_ptr, 0, "TEHandle should dereference to a TERec");
// destRect at offset 0x00 round-trips from the destRect_ptr.
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET) as i16,
-1200,
"destRect.top must round-trip from caller-supplied destRect_ptr"
);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 2) as i16,
-1200,
"destRect.left must round-trip from caller-supplied destRect_ptr"
);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 4) as i16,
-900,
"destRect.bottom must round-trip from caller-supplied destRect_ptr"
);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 6) as i16,
-1000,
"destRect.right must round-trip from caller-supplied destRect_ptr"
);
// viewRect at offset 0x08 round-trips from the viewRect_ptr.
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET) as i16,
-1100,
"viewRect.top must round-trip from caller-supplied viewRect_ptr"
);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 2) as i16,
-1300,
"viewRect.left must round-trip from caller-supplied viewRect_ptr"
);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 4) as i16,
-800,
"viewRect.bottom must round-trip from caller-supplied viewRect_ptr"
);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 6) as i16,
-900,
"viewRect.right must round-trip from caller-supplied viewRect_ptr"
);
// Styled sentinels per IM:Text 1993 p. 2-78.
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_TX_SIZE_OFFSET),
0xFFFF,
"TEStyleNew must set txSize to -1 (styled-record sentinel)"
);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_LINE_HEIGHT_OFFSET),
0xFFFF,
"TEStyleNew must set lineHeight to -1 (styled-record sentinel)"
);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_FONT_ASCENT_OFFSET),
0xFFFF,
"TEStyleNew must set fontAscent to -1 (styled-record sentinel)"
);
// Style handle at txFont/txFace overlay (4 bytes at offset 0x4A).
let style_handle = bus.read_long(te_ptr + TrapDispatcher::TE_TX_FONT_OFFSET);
assert_ne!(
style_handle, 0,
"TEStyleNew must install a non-NIL TEStyleHandle in the txFont/txFace overlay"
);
let style_ptr = bus.read_long(style_handle);
assert_ne!(
style_ptr, 0,
"TEStyleHandle should dereference to an allocated TEStyleRec"
);
}
// Inside Macintosh: Text (1993), p. 2-78 + MPW TextEdit.h
// ONEWORDINLINE(0xA83E): TEStyleNew is `EXTERN_API(TEHandle)`.
// Pascal FUNCTION protocol: caller pre-allocates 4-byte TEHandle
// result slot, pushes 2 pointer args (8 bytes); trap pops 8 +
// writes 4-byte result.
#[test]
fn testylenew_function_protocol_consumes_two_pointer_args_and_writes_4_byte_result() {
let (mut disp, mut cpu, mut bus) = setup();
let dest_rect_ptr: u32 = 0x1A0020;
let view_rect_ptr: u32 = 0x1A0030;
// Both rects: non-empty distinct values so te_new_rect_args
// selects the pointer convention (8 bytes).
bus.write_word(dest_rect_ptr, (-500_i16) as u16);
bus.write_word(dest_rect_ptr + 2, (-500_i16) as u16);
bus.write_word(dest_rect_ptr + 4, (-300_i16) as u16);
bus.write_word(dest_rect_ptr + 6, (-400_i16) as u16);
bus.write_word(view_rect_ptr, (-450_i16) as u16);
bus.write_word(view_rect_ptr + 2, (-550_i16) as u16);
bus.write_word(view_rect_ptr + 4, (-250_i16) as u16);
bus.write_word(view_rect_ptr + 6, (-350_i16) as u16);
bus.write_long(TEST_SP, view_rect_ptr);
bus.write_long(TEST_SP + 4, dest_rect_ptr);
// Poison the result slot at sp+8 and a sentinel past it.
bus.write_long(TEST_SP + 8, 0xDEADBEEF);
bus.write_long(TEST_SP + 12, 0xCAFEBABE);
let result = disp.dispatch_dialog(true, 0x03E, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
// A7 advanced exactly 8 bytes.
assert_eq!(
cpu.read_reg(Register::A7),
TEST_SP + 8,
"TEStyleNew pointer-arg convention must pop exactly 8 bytes"
);
// The function result slot at the former sp+8 was overwritten
// by a non-poison TEHandle.
let te_handle = bus.read_long(TEST_SP + 8);
assert_ne!(
te_handle, 0xDEADBEEF,
"TEStyleNew result slot must be overwritten by a real TEHandle, not the poison sentinel"
);
assert_ne!(te_handle, 0, "TEStyleNew result must be a non-NIL TEHandle");
// The sentinel at sp+12 (4 bytes past the 4-byte result slot)
// must survive — TEStyleNew must not write past its result.
assert_eq!(
bus.read_long(TEST_SP + 12),
0xCAFEBABE,
"TEStyleNew must not write past the 4-byte function-result slot"
);
}
#[test]
fn testyleinsert_applies_style_scrap_runs_and_line_metrics() {
let (mut disp, mut cpu, mut bus) = setup();
disp.tx_font = 0;
disp.tx_face = 0;
disp.tx_size = 12;
let te_handle = TrapDispatcher::allocate_te_handle(&mut bus);
disp.initialize_styled_te_record(&mut bus, te_handle, (0, 0, 80, 200), (0, 0, 80, 200));
let text = b"small\rLARGE";
let text_ptr = bus.alloc(text.len() as u32);
bus.write_bytes(text_ptr, text);
let style_scrap = make_style_scrap(
&mut bus,
&[
(0, 10, 8, 0, 1, 9, (0, 0, 0)),
(6, 14, 11, 0, 0, 12, (0x1111, 0x2222, 0x3333)),
],
);
bus.write_word(TEST_SP, 0x0007);
bus.write_long(TEST_SP + 2, te_handle);
bus.write_long(TEST_SP + 6, style_scrap);
bus.write_long(TEST_SP + 10, text.len() as u32);
bus.write_long(TEST_SP + 14, text_ptr);
let result = disp.dispatch_dialog(true, 0x03D, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(
cpu.read_reg(Register::A7),
TEST_SP + 18,
"TEStyleInsert must consume selector + hTE + hST + length + text pointer"
);
let te_ptr = bus.read_long(te_handle);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_LENGTH_OFFSET) as usize,
text.len()
);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_N_LINES_OFFSET),
2,
"explicit CR should produce two TextEdit lines"
);
let style_handle = bus.read_long(te_ptr + TrapDispatcher::TE_TX_FONT_OFFSET);
let style_ptr = bus.read_long(style_handle);
assert_eq!(
bus.read_word(style_ptr + TrapDispatcher::TE_STYLE_N_RUNS_OFFSET),
2
);
assert_eq!(
bus.read_word(style_ptr + TrapDispatcher::TE_STYLE_N_STYLES_OFFSET),
2
);
assert_eq!(
bus.read_word(style_ptr + TrapDispatcher::TE_STYLE_RUNS_OFFSET),
0
);
assert_eq!(
bus.read_word(style_ptr + TrapDispatcher::TE_STYLE_RUNS_OFFSET + 4),
6
);
let style_table = bus.read_long(style_ptr + TrapDispatcher::TE_STYLE_STYLE_TABLE_OFFSET);
let style_table_ptr = bus.read_long(style_table);
assert_eq!(
bus.read_word(style_table_ptr + TrapDispatcher::ST_ELEMENT_FACE_OFFSET),
1,
"first style run should preserve the bold face from the style scrap"
);
assert_eq!(
bus.read_word(style_table_ptr + TrapDispatcher::ST_ELEMENT_SIZE_OFFSET),
9,
"first style run should preserve the smaller body size"
);
let second_style = style_table_ptr + TrapDispatcher::ST_ELEMENT_SIZE;
assert_eq!(
bus.read_word(second_style + TrapDispatcher::ST_ELEMENT_SIZE_OFFSET),
12
);
assert_eq!(
(
bus.read_word(second_style + TrapDispatcher::ST_ELEMENT_COLOR_OFFSET),
bus.read_word(second_style + TrapDispatcher::ST_ELEMENT_COLOR_OFFSET + 2),
bus.read_word(second_style + TrapDispatcher::ST_ELEMENT_COLOR_OFFSET + 4),
),
(0x1111, 0x2222, 0x3333)
);
let lh_table = bus.read_long(style_ptr + TrapDispatcher::TE_STYLE_LH_TABLE_OFFSET);
let lh_ptr = bus.read_long(lh_table);
assert_eq!(
bus.read_word(lh_ptr + TrapDispatcher::LH_ELEMENT_HEIGHT_OFFSET),
10,
"first TextEdit line should use first style's line height"
);
assert_eq!(
bus.read_word(
lh_ptr + TrapDispatcher::LH_ELEMENT_SIZE + TrapDispatcher::LH_ELEMENT_HEIGHT_OFFSET
),
14,
"second TextEdit line should use second style's line height"
);
let runs = disp.te_style_runs(&bus, te_handle, text.len());
assert_eq!(runs.len(), 2);
assert_eq!(runs[0].start, 0);
assert_eq!(runs[0].style_index, 0);
assert_eq!(runs[0].style.size, 9);
assert_eq!(runs[1].start, 6);
assert_eq!(runs[1].style_index, 1);
assert_eq!(runs[1].style.size, 12);
}
#[test]
fn testyleinsert_parses_stscrprec_style_table_after_count_word() {
// IM:V V-274 and MPW TextEdit.h: StScrpRec is `short scrpNStyles`
// immediately followed by `ScrpSTElement scrpStyleTab[]`.
let (mut disp, mut cpu, mut bus) = setup();
disp.tx_font = 0;
disp.tx_face = 0;
disp.tx_size = 12;
let te_handle = TrapDispatcher::allocate_te_handle(&mut bus);
disp.initialize_styled_te_record(&mut bus, te_handle, (0, 0, 80, 200), (0, 0, 80, 200));
let text = b"TITLE body";
let text_ptr = bus.alloc(text.len() as u32);
bus.write_bytes(text_ptr, text);
let style_scrap = TrapDispatcher::allocate_handle_with_data(&mut bus, 2 + 2 * 0x14);
let scrap_ptr = bus.read_long(style_scrap);
bus.write_word(scrap_ptr, 2);
let first = scrap_ptr + 2;
bus.write_long(first, 0);
bus.write_word(first + 4, 10);
bus.write_word(first + 6, 8);
bus.write_word(first + 8, 0);
bus.write_word(first + 10, 1);
bus.write_word(first + 12, 9);
bus.write_word(first + 14, 0);
bus.write_word(first + 16, 0);
bus.write_word(first + 18, 0);
let second = first + 0x14;
bus.write_long(second, 6);
bus.write_word(second + 4, 14);
bus.write_word(second + 6, 11);
bus.write_word(second + 8, 3);
bus.write_word(second + 10, 0);
bus.write_word(second + 12, 12);
bus.write_word(second + 14, 0x1111);
bus.write_word(second + 16, 0x2222);
bus.write_word(second + 18, 0x3333);
bus.write_word(TEST_SP, 0x0007);
bus.write_long(TEST_SP + 2, te_handle);
bus.write_long(TEST_SP + 6, style_scrap);
bus.write_long(TEST_SP + 10, text.len() as u32);
bus.write_long(TEST_SP + 14, text_ptr);
let result = disp.dispatch_dialog(true, 0x03D, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
let runs = disp.te_style_runs(&bus, te_handle, text.len());
assert_eq!(runs.len(), 2);
assert_eq!(runs[0].start, 0);
assert_eq!(runs[0].style.face, 1);
assert_eq!(runs[0].style.size, 9);
assert_eq!(runs[1].start, 6);
assert_eq!(runs[1].style.font, 3);
assert_eq!(runs[1].style.size, 12);
assert_eq!(runs[1].style.color, (0x1111, 0x2222, 0x3333));
}
// Inside Macintosh: Text (1993), p. 2-92: TEAutoView takes
// `fAuto: Boolean; hTE: TEHandle`. With Pascal calling convention, hTE
// (last parameter) is at SP+0 and fAuto at SP+4.
#[test]
fn teautoview_reads_hte_from_sp_plus_0_and_fauto_from_sp_plus_4() {
let (mut disp, mut cpu, mut bus) = setup();
let te_handle = TrapDispatcher::allocate_te_handle(&mut bus);
bus.write_long(TEST_SP, te_handle); // SP+0: hTE (last-pushed)
// Pascal BOOLEAN in high byte (MPW C convention).
bus.write_byte(TEST_SP + 4, 1); // SP+4: fAuto = TRUE
let result = disp.dispatch_dialog(true, 0x013, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
assert!(
disp.te_feature_bit(te_handle, TrapDispatcher::TE_FEATURE_AUTO_SCROLL),
"TEAutoView must enable auto-scroll on the TE handle at SP+0"
);
}
#[test]
fn teautoview_clears_feature_when_fauto_is_false() {
let (mut disp, mut cpu, mut bus) = setup();
let te_handle = TrapDispatcher::allocate_te_handle(&mut bus);
// First turn it on.
disp.set_te_feature_bit(te_handle, TrapDispatcher::TE_FEATURE_AUTO_SCROLL, true);
assert!(disp.te_feature_bit(te_handle, TrapDispatcher::TE_FEATURE_AUTO_SCROLL));
bus.write_long(TEST_SP, te_handle);
bus.write_word(TEST_SP + 4, 0); // fAuto = FALSE
let result = disp.dispatch_dialog(true, 0x013, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(
cpu.read_reg(Register::A7),
TEST_SP + 6,
"TEAutoView should pop one Boolean and one TEHandle"
);
assert!(
!disp.te_feature_bit(te_handle, TrapDispatcher::TE_FEATURE_AUTO_SCROLL),
"TEAutoView(FALSE) must clear the auto-scroll bit on the supplied hTE"
);
}
// IM:Text 2-92 + IM:VI 15-22: TEAutoView and TEFeatureFlag
// teFAutoScroll expose the same automatic-scroll state.
#[test]
fn teautoview_and_tefeatureflag_observe_shared_autoscroll_state() {
let (mut disp, mut cpu, mut bus) = setup();
let te_handle = TrapDispatcher::allocate_te_handle(&mut bus);
bus.write_long(TEST_SP, te_handle);
bus.write_byte(TEST_SP + 4, 1); // fAuto = TRUE
let result = disp.dispatch_dialog(true, 0x013, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 0x000E); // selector
bus.write_long(TEST_SP + 2, te_handle); // hTE
bus.write_word(TEST_SP + 6, (-1i16) as u16); // teBitTest
bus.write_word(TEST_SP + 8, TrapDispatcher::TE_FEATURE_AUTO_SCROLL);
bus.write_word(TEST_SP + 10, 0xBEEF);
let result = disp.dispatch_dialog(true, 0x03D, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(TEST_SP + 10), 1);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, te_handle);
bus.write_word(TEST_SP + 4, 0); // fAuto = FALSE
let result = disp.dispatch_dialog(true, 0x013, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 0x000E); // selector
bus.write_long(TEST_SP + 2, te_handle); // hTE
bus.write_word(TEST_SP + 6, (-1i16) as u16); // teBitTest
bus.write_word(TEST_SP + 8, TrapDispatcher::TE_FEATURE_AUTO_SCROLL);
bus.write_word(TEST_SP + 10, 0xBEEF);
let result = disp.dispatch_dialog(true, 0x03D, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(TEST_SP + 10), 0);
}
#[test]
fn te_dispatch_feature_flag_tracks_auto_scroll_state() {
let (mut disp, mut cpu, mut bus) = setup();
let te_handle = TrapDispatcher::allocate_te_handle(&mut bus);
bus.write_word(TEST_SP, 0x000E);
bus.write_long(TEST_SP + 2, te_handle);
bus.write_word(TEST_SP + 6, 1); // teBitSet
bus.write_word(TEST_SP + 8, 0); // teFAutoScroll
let result = disp.dispatch_dialog(true, 0x03D, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 10);
assert_eq!(bus.read_word(TEST_SP + 10), 0);
assert!(disp.te_feature_bit(te_handle, TrapDispatcher::TE_FEATURE_AUTO_SCROLL));
}
// Inside Macintosh Volume VI (1991), pp. 15-22 and 15-43: selector
// $000E dispatches to TEFeatureFlag; action TEBitTest (-1) reports
// current feature state without mutating it.
#[test]
fn te_dispatch_feature_flag_test_action_returns_current_state() {
let (mut disp, mut cpu, mut bus) = setup();
let te_handle = TrapDispatcher::allocate_te_handle(&mut bus);
disp.set_te_feature_bit(te_handle, TrapDispatcher::TE_FEATURE_AUTO_SCROLL, true);
bus.write_word(TEST_SP, 0x000E); // selector
bus.write_long(TEST_SP + 2, te_handle); // hTE
bus.write_word(TEST_SP + 6, (-1i16) as u16); // teBitTest
bus.write_word(TEST_SP + 8, TrapDispatcher::TE_FEATURE_AUTO_SCROLL);
bus.write_word(TEST_SP + 10, 0xBEEF);
let result = disp.dispatch_dialog(true, 0x03D, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 10);
assert_eq!(bus.read_word(TEST_SP + 10), 1);
assert!(
disp.te_feature_bit(te_handle, TrapDispatcher::TE_FEATURE_AUTO_SCROLL),
"teBitTest should not mutate the feature bit"
);
}
// Inside Macintosh Volume VI (1991), p. 15-22: TEFeatureFlag with
// action teBitClear=0 clears the selector bit and returns the prior
// state. Symmetric counterpart of `te_dispatch_feature_flag_tracks_auto_scroll_state`
// for the clear action.
#[test]
fn tefeatureflag_clear_action_returns_prior_one_state_and_clears_bit() {
let (mut disp, mut cpu, mut bus) = setup();
let te_handle = TrapDispatcher::allocate_te_handle(&mut bus);
// Pre-set autoscroll so teBitClear sees a prior-on state.
disp.set_te_feature_bit(te_handle, TrapDispatcher::TE_FEATURE_AUTO_SCROLL, true);
assert!(disp.te_feature_bit(te_handle, TrapDispatcher::TE_FEATURE_AUTO_SCROLL));
// THREEWORDINLINE(0x3F3C, 0x000E, 0xA83D) stack frame: selector
// pre-pushed at sp+0; Pascal LR push order leaves hTE shallowest
// (sp+2), action middle (sp+6), feature deepest (sp+8); short
// function-result slot at sp+10.
bus.write_word(TEST_SP, 0x000E); // selector $000E = TEFeatureFlag
bus.write_long(TEST_SP + 2, te_handle); // hTE
bus.write_word(TEST_SP + 6, 0); // action = teBitClear (0)
bus.write_word(TEST_SP + 8, TrapDispatcher::TE_FEATURE_AUTO_SCROLL);
bus.write_word(TEST_SP + 10, 0xBEEF); // poison result slot
let result = disp.dispatch_dialog(true, 0x03D, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(
cpu.read_reg(Register::A7),
TEST_SP + 10,
"TEFeatureFlag must pop 10 bytes (selector + hTE + action + feature)"
);
assert_eq!(
bus.read_word(TEST_SP + 10),
1,
"teBitClear must return PRIOR bit state (1 = was set), not the new state"
);
assert!(
!disp.te_feature_bit(te_handle, TrapDispatcher::TE_FEATURE_AUTO_SCROLL),
"teBitClear must clear the feature bit after returning its prior state"
);
}
// Inside Macintosh Volume VI (1991), p. 15-22: TEFeatureFlag is
// FUNCTION TEFeatureFlag(feature, action: INTEGER; hTE: TEHandle): INTEGER.
// Universal Headers TextEdit.h declares THREEWORDINLINE(0x3F3C, 0x000E, 0xA83D).
// The 10-byte stack frame (2 selector + 4 hTE + 2 action + 2 feature)
// is consumed atomically; the post-pop SP points at the 2-byte short
// function-result slot.
#[test]
fn tedispatch_function_protocol_consumes_threewordinline_stack_frame_for_tefeatureflag() {
let (mut disp, mut cpu, mut bus) = setup();
let te_handle = TrapDispatcher::allocate_te_handle(&mut bus);
bus.write_word(TEST_SP, 0x000E); // selector
bus.write_long(TEST_SP + 2, te_handle);
bus.write_word(TEST_SP + 6, (-1i16) as u16); // teBitTest
bus.write_word(TEST_SP + 8, TrapDispatcher::TE_FEATURE_AUTO_SCROLL);
// Poison both the result slot and a sentinel past it.
bus.write_word(TEST_SP + 10, 0xDEAD);
bus.write_word(TEST_SP + 12, 0xCAFE);
let result = disp.dispatch_dialog(true, 0x03D, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
// SP advanced exactly 10 bytes: selector(2) + hTE(4) + action(2) + feature(2).
assert_eq!(
cpu.read_reg(Register::A7),
TEST_SP + 10,
"TEFeatureFlag dispatch must advance A7 by exactly 10 bytes"
);
// Result slot was overwritten by the prior bit state (0 here).
assert_ne!(
bus.read_word(TEST_SP + 10),
0xDEAD,
"TEFeatureFlag must overwrite the 2-byte result slot with the prior bit state"
);
// Sentinel 2 bytes past the result slot must survive — TEFeatureFlag
// must not write beyond its function-result word.
assert_eq!(
bus.read_word(TEST_SP + 12),
0xCAFE,
"TEFeatureFlag must not write past the 2-byte short function-result slot"
);
}
#[test]
fn te_dispatch_continuous_style_returns_unstyled_record_style() {
let (mut disp, mut cpu, mut bus) = setup();
let te_handle = TrapDispatcher::allocate_te_handle(&mut bus);
disp.current_port = 0x181000;
disp.tx_font = 4;
disp.tx_face = 1;
disp.tx_mode = 2;
disp.tx_size = 12;
disp.initialize_te_record(&mut bus, te_handle, (0, 0, 40, 80), (0, 0, 40, 80));
let te_ptr = bus.read_long(te_handle);
bus.write_word(te_ptr + TrapDispatcher::TE_TX_FONT_OFFSET, 22);
bus.write_word(te_ptr + TrapDispatcher::TE_TX_FACE_OFFSET, 3);
bus.write_word(te_ptr + TrapDispatcher::TE_TX_SIZE_OFFSET, 18);
let style_ptr = 0x300000u32;
let mode_ptr = 0x300100u32;
bus.write_word(mode_ptr, 0x000F); // doAll
bus.write_word(TEST_SP, 0x000A);
bus.write_long(TEST_SP + 2, te_handle);
bus.write_long(TEST_SP + 6, style_ptr);
bus.write_long(TEST_SP + 10, mode_ptr);
let result = disp.dispatch_dialog(true, 0x03D, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 14);
assert_eq!(bus.read_word(TEST_SP + 14), 0xFFFF);
assert_eq!(bus.read_word(style_ptr), 22);
assert_eq!(bus.read_word(style_ptr + 2), 3);
assert_eq!(bus.read_word(style_ptr + 4), 18);
assert_eq!(bus.read_word(style_ptr + 6), 0);
assert_eq!(bus.read_word(style_ptr + 8), 0);
assert_eq!(bus.read_word(style_ptr + 10), 0);
}
#[test]
fn te_scroll_reads_handle_from_stack_top() {
let (mut disp, mut cpu, mut bus) = setup();
let te_handle = TrapDispatcher::allocate_te_handle(&mut bus);
disp.current_port = 0x181000;
disp.tx_font = 4;
disp.tx_face = 0;
disp.tx_mode = 0;
disp.tx_size = 10;
disp.initialize_te_record(&mut bus, te_handle, (10, 20, 50, 80), (10, 20, 50, 80));
bus.write_long(TEST_SP, te_handle);
bus.write_word(TEST_SP + 4, (-6i16) as u16);
bus.write_word(TEST_SP + 6, 4);
let result = disp.dispatch_dialog(true, 0x1DD, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
let te_ptr = bus.read_long(te_handle);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET),
4
);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 2),
24
);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 4),
44
);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 6),
84
);
}
#[test]
fn te_pin_scroll_reads_handle_from_stack_top() {
let (mut disp, mut cpu, mut bus) = setup();
let te_handle = TrapDispatcher::allocate_te_handle(&mut bus);
disp.current_port = 0x181000;
disp.tx_font = 4;
disp.tx_face = 0;
disp.tx_mode = 0;
disp.tx_size = 10;
disp.initialize_te_record(&mut bus, te_handle, (10, 20, 50, 80), (10, 20, 50, 80));
disp.te_set_text_contents(
&mut bus,
te_handle,
b"one two three four five six seven eight nine ten",
);
bus.write_long(TEST_SP, te_handle);
bus.write_word(TEST_SP + 4, (-6i16) as u16);
bus.write_word(TEST_SP + 6, 4);
let result = disp.dispatch_dialog(true, 0x012, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
let te_ptr = bus.read_long(te_handle);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET),
4
);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 2),
20
);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 4),
44
);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 6),
80
);
}
#[test]
fn tepinscroll_in_range_negative_dv_offsets_destrect_top_and_bottom_exactly_by_dv() {
// Inside Macintosh: Text 1993, p. 2-91: "The destination rectangle
// is offset by the amount scrolled." In-range up-scroll with
// multi-line text that overflows the view: with text
// "A\rB\rC\rD\rE\rF\rG\rH"
// and view height 10, any small negative dv is in-range so destRect.
// top and destRect.bottom must shift by exactly dv with destRect.
// left and destRect.right unchanged (dh=0).
let (mut disp, mut cpu, mut bus) = setup();
let te_handle = make_te_with_text(&mut disp, &mut bus, b"A\rB\rC\rD\rE\rF\rG\rH");
let te_ptr = bus.read_long(te_handle);
bus.write_word(
te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET,
(-1000i16) as u16,
);
bus.write_word(
te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 2,
(-1000i16) as u16,
);
bus.write_word(
te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 4,
(-990i16) as u16,
);
bus.write_word(
te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 6,
(-800i16) as u16,
);
bus.write_word(
te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET,
(-1000i16) as u16,
);
bus.write_word(
te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 2,
(-1000i16) as u16,
);
bus.write_word(
te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 4,
(-990i16) as u16,
);
bus.write_word(
te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 6,
(-800i16) as u16,
);
bus.write_long(TEST_SP, te_handle);
bus.write_word(TEST_SP + 4, (-3i16) as u16);
bus.write_word(TEST_SP + 6, 0);
let result = disp.dispatch_dialog(true, 0x012, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET) as i16,
-1003,
"destRect.top must shift by exactly dv=-3"
);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 2) as i16,
-1000,
"destRect.left must be unchanged (dh=0)"
);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 4) as i16,
-993,
"destRect.bottom must shift by exactly dv=-3"
);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 6) as i16,
-800,
"destRect.right must be unchanged (dh=0)"
);
}
#[test]
fn tepinscroll_pascal_lr_stack_layout_reads_dh_dv_and_hte_from_correct_offsets() {
// Per Pascal LR-push-first-arg-deepest convention (matching the
// EXTERN_API expansion in MPW Universal Headers TextEdit.h):
// sp+0 hTE: TEHandle (4) — last pushed, shallowest
// sp+4 dv: INTEGER (2) — middle
// sp+6 dh: INTEGER (2) — first pushed, deepest
// Total pop = 8 bytes; no function-result slot.
//
// Stage hTE at sp+0, dv=-2 at sp+4, dh=+7 at sp+6 with a 0xCAFE
// sentinel at sp+8 to detect over-reads. The HLE must consume
// exactly 8 bytes and the sentinel must survive.
let (mut disp, mut cpu, mut bus) = setup();
let te_handle = make_te_with_text(&mut disp, &mut bus, b"A\rB\rC\rD\rE\rF");
let te_ptr = bus.read_long(te_handle);
bus.write_word(
te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET,
(-500i16) as u16,
);
bus.write_word(
te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 2,
(-500i16) as u16,
);
bus.write_word(
te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 4,
(-490i16) as u16,
);
bus.write_word(
te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 6,
(-400i16) as u16,
);
bus.write_word(
te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET,
(-500i16) as u16,
);
// destRect.left = -510 (shifted left of view.left by 10, so positive
// dh can be in-range to actually scroll right)
bus.write_word(
te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 2,
(-510i16) as u16,
);
bus.write_word(
te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 4,
(-490i16) as u16,
);
bus.write_word(
te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 6,
(-310i16) as u16,
);
bus.write_long(TEST_SP, te_handle);
bus.write_word(TEST_SP + 4, (-2i16) as u16);
bus.write_word(TEST_SP + 6, 7);
bus.write_word(TEST_SP + 8, 0xCAFE);
let result = disp.dispatch_dialog(true, 0x012, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(
cpu.read_reg(Register::A7),
TEST_SP + 8,
"Pascal PROCEDURE must pop exactly 8 bytes (dh 2 + dv 2 + hTE 4)"
);
assert_eq!(
bus.read_word(TEST_SP + 8),
0xCAFE,
"sentinel past the arg frame must survive"
);
// Verify destRect was actually mutated to confirm the HLE read
// its args from the correct stack offsets (a swapped-arg stub
// would shift by wrong amounts or pick up the sentinel).
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET) as i16,
-502,
"destRect.top must shift by dv=-2"
);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 2) as i16,
-503,
"destRect.left must shift by dh=+7 (clamped to max_right=10)"
);
}
#[test]
fn tepinscroll_clamps_overscroll_when_last_line_is_already_visible() {
// Inside Macintosh: Text 1993, p. 2-91: TEPinScroll clamps movement
// so scrolling stops once the last line is visible.
let (mut disp, mut cpu, mut bus) = setup();
let te_handle = make_te_with_text(&mut disp, &mut bus, b"A");
let te_ptr = bus.read_long(te_handle);
bus.write_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET, 10);
bus.write_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 2, 20);
bus.write_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 4, 50);
bus.write_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 6, 80);
bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET, 10);
bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 2, 20);
bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 4, 50);
bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 6, 80);
bus.write_long(TEST_SP, te_handle);
bus.write_word(TEST_SP + 4, (-20i16) as u16);
bus.write_word(TEST_SP + 6, 0);
let result = disp.dispatch_dialog(true, 0x012, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET),
10
);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 2),
20
);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 4),
50
);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 6),
80
);
}
#[test]
fn teselview_autoscroll_disabled_leaves_destrect_unchanged() {
// Inside Macintosh: Text 1993, p. 2-92: TESelView only scrolls when
// auto-scroll is enabled for the TE record.
let (mut disp, mut cpu, mut bus) = setup();
let te_handle = make_te_with_text(
&mut disp,
&mut bus,
b"one two three four five six seven eight nine ten",
);
let te_ptr = bus.read_long(te_handle);
bus.write_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET, 10);
bus.write_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 2, 20);
bus.write_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 4, 50);
bus.write_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 6, 80);
bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET, 0);
bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 2, 20);
bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 4, 40);
bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 6, 80);
bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 0);
bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 0);
disp.set_te_feature_bit(te_handle, TrapDispatcher::TE_FEATURE_AUTO_SCROLL, false);
bus.write_long(TEST_SP, te_handle);
let result = disp.dispatch_dialog(true, 0x011, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET),
0
);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 2),
20
);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 4),
40
);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 6),
80
);
}
#[test]
fn teselview_autoscroll_enabled_scrolls_destrect_toward_selection() {
// Inside Macintosh: Text 1993, p. 2-92: TESelView scrolls selection
// into view when auto-scroll is enabled.
let (mut disp, mut cpu, mut bus) = setup();
let te_handle = make_te_with_text(
&mut disp,
&mut bus,
b"one two three four five six seven eight nine ten",
);
let te_ptr = bus.read_long(te_handle);
bus.write_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET, 10);
bus.write_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 2, 20);
bus.write_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 4, 50);
bus.write_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 6, 80);
bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET, 0);
bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 2, 20);
bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 4, 40);
bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 6, 80);
bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 0);
bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 0);
disp.set_te_feature_bit(te_handle, TrapDispatcher::TE_FEATURE_AUTO_SCROLL, true);
bus.write_long(TEST_SP, te_handle);
let result = disp.dispatch_dialog(true, 0x011, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET),
10
);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 2),
20
);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 4),
50
);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 6),
80
);
}
#[test]
fn teselview_apple_canonical_below_view_shifts_destrect_top_up() {
// Inside Macintosh: Text 1993, p. 2-92 (Apple canonical):
// "The top left part of the selection range is scrolled
// into view."
// BasiliskII System 7.5.3 ROM does not scroll destRect in
// this case (empirically verified); Systemless implements the
// Apple-canonical semantic.
//
// Setup: destRect = (0, 0, 100, 200) tall enough for the
// entire multi-line text, viewRect = (0, 0, 100, 30) showing
// only the first ~1.5 lines, selection at the last character
// (line 7 of "A\rB\r..\rH", well below viewRect.bottom).
let (mut disp, mut cpu, mut bus) = setup();
let te_handle = make_te_with_text(&mut disp, &mut bus, b"A\rB\rC\rD\rE\rF\rG\rH");
let te_ptr = bus.read_long(te_handle);
bus.write_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET, 0);
bus.write_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 2, 0);
bus.write_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 4, 30);
bus.write_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 6, 100);
bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET, 0);
bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 2, 0);
bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 4, 200);
bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 6, 100);
bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 14);
bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 14);
disp.set_te_feature_bit(te_handle, TrapDispatcher::TE_FEATURE_AUTO_SCROLL, true);
let pre_top = bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET) as i16;
bus.write_long(TEST_SP, te_handle);
let result = disp.dispatch_dialog(true, 0x011, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
let post_top = bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET) as i16;
assert!(
post_top < pre_top,
"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={}",
pre_top,
post_top
);
}
#[test]
fn teselview_procedure_protocol_consumes_only_tehandle_arg() {
// Pascal PROCEDURE: 4-byte hTE arg, no result. Sentinel guard
// at TEST_SP+4 must survive the call (trap must not write past
// its argument frame). A7 advances by exactly 4 bytes.
let (mut disp, mut cpu, mut bus) = setup();
let te_handle = make_te_with_text(&mut disp, &mut bus, b"hi");
// Auto-scroll disabled so the trap is a guaranteed destRect
// no-op and the only observable side-effect is the SP advance.
disp.set_te_feature_bit(te_handle, TrapDispatcher::TE_FEATURE_AUTO_SCROLL, false);
bus.write_long(TEST_SP, te_handle);
bus.write_word(TEST_SP + 4, 0xCAFE);
bus.write_word(TEST_SP + 6, 0xBABE);
let result = disp.dispatch_dialog(true, 0x011, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
assert_eq!(bus.read_word(TEST_SP + 4), 0xCAFE);
assert_eq!(bus.read_word(TEST_SP + 6), 0xBABE);
}
#[test]
fn tegetoffset_point_above_destrect_returns_zero() {
// Inside Macintosh Volume V (1986), p. V-172: TEGetOffset
// returns the character offset of the start of the first line
// when the point is above the first line. Pascal LR layout:
// sp+0..3 hTE (last pushed, shallowest)
// sp+4..5 pt.v
// sp+6..7 pt.h
// sp+8..9 INTEGER result slot
let (mut disp, mut cpu, mut bus) = setup();
let te_handle = make_te_with_text(&mut disp, &mut bus, b"ABC");
let te_ptr = bus.read_long(te_handle);
bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET, 10);
bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 2, 20);
bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 4, 40);
bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 6, 140);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, te_handle);
bus.write_word(TEST_SP + 4, 5);
bus.write_word(TEST_SP + 6, 30);
bus.write_word(TEST_SP + 8, 0xBEEF);
let result = disp.dispatch_dialog(true, 0x03C, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
assert_eq!(bus.read_word(TEST_SP + 8) as i16, 0);
}
#[test]
fn tegetoffset_point_below_last_line_returns_telength() {
// Inside Macintosh Volume V (1986), p. V-172: TEGetOffset
// returns the character offset of the end of the text when
// the point is below the last line. Witnesses that the HLE
// computes a value that varies with the point arg (vs the
// always-zero result the pre-fix off-by-2 read produced).
let (mut disp, mut cpu, mut bus) = setup();
let te_handle = make_te_with_text(&mut disp, &mut bus, b"ABC");
let te_ptr = bus.read_long(te_handle);
bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET, 10);
bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 2, 20);
bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 4, 40);
bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 6, 140);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, te_handle);
bus.write_word(TEST_SP + 4, 200);
bus.write_word(TEST_SP + 6, 30);
bus.write_word(TEST_SP + 8, 0xBEEF);
let result = disp.dispatch_dialog(true, 0x03C, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
assert_eq!(bus.read_word(TEST_SP + 8) as i16, 3);
}
#[test]
fn tegetoffset_function_protocol_consumes_point_and_tehandle_args_writes_integer_result() {
// Pascal FUNCTION protocol: 8 bytes of args (Point + TEHandle)
// popped, 2-byte INTEGER result written at sp+8. Sentinel at
// sp+10 must survive — the trap must not write past the
// 2-byte result slot.
let (mut disp, mut cpu, mut bus) = setup();
let te_handle = make_te_with_text(&mut disp, &mut bus, b"ABC");
let te_ptr = bus.read_long(te_handle);
bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET, 10);
bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 2, 20);
bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 4, 40);
bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 6, 140);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, te_handle);
bus.write_word(TEST_SP + 4, 5);
bus.write_word(TEST_SP + 6, 30);
bus.write_word(TEST_SP + 8, 0xDEAD);
bus.write_word(TEST_SP + 10, 0xCAFE);
let result = disp.dispatch_dialog(true, 0x03C, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
assert_ne!(bus.read_word(TEST_SP + 8), 0xDEAD);
assert_eq!(bus.read_word(TEST_SP + 10), 0xCAFE);
}
#[test]
fn tefindword_returns_word_bounds_for_interior_positions() {
// Inside Macintosh: Text (1993), pp. 2-60..2-61: TEFindWord
// reports the word boundaries surrounding the current position.
let (mut disp, mut cpu, mut bus) = setup();
let te_handle = make_te_with_text(&mut disp, &mut bus, b"alpha beta");
let te_ptr = bus.read_long(te_handle);
cpu.write_reg(Register::A7, TEST_SP);
cpu.write_reg(Register::D0, 1); // inside "alpha"
cpu.write_reg(Register::D2, 0x1111);
cpu.write_reg(Register::A3, te_ptr);
cpu.write_reg(Register::A4, te_handle);
let result = disp.dispatch_dialog(false, 0x0FE, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP);
assert_eq!(cpu.read_reg(Register::D0) as u16, 0);
assert_eq!(cpu.read_reg(Register::D1) as u16, 5);
}
#[test]
fn tefindword_returns_second_word_bounds_without_touching_stack() {
// Same TextEdit hook: a later position inside the second word
// should return that word's bounds, and the register-based hook
// must not consume any stack bytes.
let (mut disp, mut cpu, mut bus) = setup();
let te_handle = make_te_with_text(&mut disp, &mut bus, b"alpha beta");
let te_ptr = bus.read_long(te_handle);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 0xCAFE);
cpu.write_reg(Register::D0, 7); // inside "beta"
cpu.write_reg(Register::D2, 0x2222);
cpu.write_reg(Register::A3, te_ptr);
cpu.write_reg(Register::A4, te_handle);
let result = disp.dispatch_dialog(false, 0x0FE, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP);
assert_eq!(bus.read_word(TEST_SP), 0xCAFE);
assert_eq!(cpu.read_reg(Register::D0) as u16, 6);
assert_eq!(cpu.read_reg(Register::D1) as u16, 10);
}
#[test]
fn tesettext_copies_bytes_updates_length_and_pops_arguments() {
// Inside Macintosh Volume I (1985), p. I-378: TESetText replaces an
// edit record's text contents.
let (mut disp, mut cpu, mut bus) = setup();
let te_handle = make_te_with_text(&mut disp, &mut bus, b"OLD");
let te_ptr = bus.read_long(te_handle);
let source_ptr = bus.alloc(4);
bus.write_bytes(source_ptr, b"NEW!");
bus.write_long(TEST_SP, te_handle);
bus.write_long(TEST_SP + 4, 4);
bus.write_long(TEST_SP + 8, source_ptr);
let result = disp.dispatch_dialog(true, 0x1CF, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 12);
assert_eq!(
TrapDispatcher::te_text_bytes(&bus, te_handle),
b"NEW!".to_vec()
);
assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_LENGTH_OFFSET), 4);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
4
);
assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET), 4);
let n_lines = u32::from(bus.read_word(te_ptr + TrapDispatcher::TE_N_LINES_OFFSET));
assert!(n_lines > 0);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_LINE_STARTS_OFFSET + n_lines * 2),
4
);
}
#[test]
fn tesettext_nil_or_zero_length_input_clears_text() {
// Inside Macintosh Volume I (1985), p. I-378: TESetText sets current
// text contents; empty input yields empty text.
let (mut disp, mut cpu, mut bus) = setup();
let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
let te_ptr = bus.read_long(te_handle);
bus.write_long(TEST_SP, te_handle);
bus.write_long(TEST_SP + 4, 5);
bus.write_long(TEST_SP + 8, 0);
let result = disp.dispatch_dialog(true, 0x1CF, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 12);
assert_eq!(
TrapDispatcher::te_text_bytes(&bus, te_handle),
Vec::<u8>::new()
);
assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_LENGTH_OFFSET), 0);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
0
);
assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET), 0);
}
#[test]
fn tesettext_replaces_prior_contents_via_sequential_call() {
// Inside Macintosh Volume I (1985), p. I-378: TESetText *sets*
// (not appends to) the current text contents: TESetText("WORLD!", 6)
// followed by TESetText("HI", 2) on the same TERec yields
// teLength == 2 with the first two bytes equal to "HI".
let (mut disp, mut cpu, mut bus) = setup();
let te_handle = make_te_with_text(&mut disp, &mut bus, b"OLD");
let te_ptr = bus.read_long(te_handle);
let first_ptr = bus.alloc(6);
bus.write_bytes(first_ptr, b"WORLD!");
bus.write_long(TEST_SP, te_handle);
bus.write_long(TEST_SP + 4, 6);
bus.write_long(TEST_SP + 8, first_ptr);
let r1 = disp.dispatch_dialog(true, 0x1CF, &mut cpu, &mut bus);
assert!(r1.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 12);
assert_eq!(
TrapDispatcher::te_text_bytes(&bus, te_handle),
b"WORLD!".to_vec()
);
assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_LENGTH_OFFSET), 6);
cpu.write_reg(Register::A7, TEST_SP);
let second_ptr = bus.alloc(2);
bus.write_bytes(second_ptr, b"HI");
bus.write_long(TEST_SP, te_handle);
bus.write_long(TEST_SP + 4, 2);
bus.write_long(TEST_SP + 8, second_ptr);
let r2 = disp.dispatch_dialog(true, 0x1CF, &mut cpu, &mut bus);
assert!(r2.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 12);
assert_eq!(
TrapDispatcher::te_text_bytes(&bus, te_handle),
b"HI".to_vec()
);
assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_LENGTH_OFFSET), 2);
}
#[test]
fn tesettext_balances_stackspace_with_pascal_protocol() {
// Inside Macintosh Volume I (1985), p. I-378: TESetText is a
// PROCEDURE with three args (Ptr text, LONGINT length, TEHandle).
// Pascal LR push order yields a 12-byte arg frame and no result
// slot. A7 must advance by exactly 12 bytes regardless of arg
// values.
let (mut disp, mut cpu, mut bus) = setup();
let te_handle = make_te_with_text(&mut disp, &mut bus, b"SEED");
let source = bus.alloc(1);
bus.write_bytes(source, b"X");
bus.write_long(TEST_SP, te_handle);
bus.write_long(TEST_SP + 4, 1);
bus.write_long(TEST_SP + 8, source);
bus.write_word(TEST_SP + 12, 0xCAFE);
let result = disp.dispatch_dialog(true, 0x1CF, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 12);
assert_eq!(bus.read_word(TEST_SP + 12), 0xCAFE);
assert_eq!(
TrapDispatcher::te_text_bytes(&bus, te_handle),
b"X".to_vec()
);
}
#[test]
fn tekey_inserts_character_at_caret_and_advances_selection() {
// Inside Macintosh Volume I (1985), p. I-385 and Text 1993, p. 2-81:
// TEKey inserts typed characters at the insertion point.
let (mut disp, mut cpu, mut bus) = setup();
let te_handle = make_te_with_text(&mut disp, &mut bus, b"HLO");
let te_ptr = bus.read_long(te_handle);
bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 1);
bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 1);
bus.write_long(TEST_SP, te_handle);
bus.write_word(TEST_SP + 4, u16::from(b'E'));
let result = disp.dispatch_dialog(true, 0x1DC, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
assert_eq!(
TrapDispatcher::te_text_bytes(&bus, te_handle),
b"HELO".to_vec()
);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
2
);
assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET), 2);
}
#[test]
fn tekey_backspace_deletes_selection_or_previous_character() {
// Inside Macintosh Volume I (1985), p. I-385 and Text 1993, p. 2-81:
// backspace deletes current selection, or the previous character.
let (mut disp, mut cpu, mut bus) = setup();
let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
let te_ptr = bus.read_long(te_handle);
bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 1);
bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 3);
bus.write_long(TEST_SP, te_handle);
bus.write_word(TEST_SP + 4, 0x0008);
let result = disp.dispatch_dialog(true, 0x1DC, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
assert_eq!(
TrapDispatcher::te_text_bytes(&bus, te_handle),
b"HLO".to_vec()
);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
1
);
assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET), 1);
disp.te_set_text_contents(&mut bus, te_handle, b"HELLO");
bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 2);
bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 2);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, te_handle);
bus.write_word(TEST_SP + 4, 0x0008);
let result = disp.dispatch_dialog(true, 0x1DC, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
assert_eq!(
TrapDispatcher::te_text_bytes(&bus, te_handle),
b"HLLO".to_vec()
);
assert_eq!(
bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
1
);
assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET), 1);
}
// ---- HideDialogItem / ShowDialogItem ($A827 / $A828) ----
// Gate the move-offscreen-and-restore behaviour.
#[test]
fn hide_dialog_item_moves_rect_offscreen_and_saves_original() {
// MTE 1992, 6-123: HideDialogItem offsets left/right by +16384.
let (mut disp, mut cpu, mut bus) = setup();
let dialog_ptr = 0x210000u32;
disp.dialog_items.insert(
dialog_ptr,
vec![DialogItem {
item_type: 4,
rect: (10, 20, 30, 120),
text: "OK".into(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
}],
);
// Stack: SP+0=itemNo(2), SP+2=dialog_ptr(4). Pop 6.
bus.write_word(TEST_SP, 1);
bus.write_long(TEST_SP + 2, dialog_ptr);
let result = disp.dispatch_dialog(true, 0x027, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
// Rect is now offscreen.
let item_rect = disp.dialog_items.get(&dialog_ptr).unwrap()[0].rect;
assert!(
item_rect.0 == 10 && item_rect.2 == 30,
"vertical coordinates are unchanged by HideDialogItem, got {item_rect:?}"
);
assert!(
item_rect.1 == 20 + 16384 && item_rect.3 == 120 + 16384,
"hidden item rect must be offscreen, got {item_rect:?}"
);
// Original saved.
assert_eq!(
disp.hidden_dialog_item_rects.get(&(dialog_ptr, 1)).copied(),
Some((10, 20, 30, 120))
);
}
#[test]
fn show_dialog_item_restores_saved_rect() {
// MTE 1992, 6-124: ShowDialogItem restores pre-hide item rect.
let (mut disp, mut cpu, mut bus) = setup();
let dialog_ptr = 0x210000u32;
disp.dialog_items.insert(
dialog_ptr,
vec![DialogItem {
item_type: 4,
rect: (10, 20 + 16384, 30, 120 + 16384),
text: "OK".into(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
}],
);
disp.hidden_dialog_item_rects
.insert((dialog_ptr, 1), (10, 20, 30, 120));
bus.write_word(TEST_SP, 1);
bus.write_long(TEST_SP + 2, dialog_ptr);
let result = disp.dispatch_dialog(true, 0x028, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
assert_eq!(
disp.dialog_items.get(&dialog_ptr).unwrap()[0].rect,
(10, 20, 30, 120)
);
assert!(
!disp.hidden_dialog_item_rects.contains_key(&(dialog_ptr, 1)),
"saved rect must be removed after show"
);
}
#[test]
fn hide_then_show_is_idempotent_roundtrip() {
let (mut disp, mut cpu, mut bus) = setup();
let dialog_ptr = 0x210000u32;
let orig_rect = (50, 60, 80, 200);
disp.dialog_items.insert(
dialog_ptr,
vec![DialogItem {
item_type: 4,
rect: orig_rect,
text: "Cancel".into(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
}],
);
bus.write_word(TEST_SP, 1);
bus.write_long(TEST_SP + 2, dialog_ptr);
disp.dispatch_dialog(true, 0x027, &mut cpu, &mut bus)
.unwrap()
.unwrap();
// Reset SP + args for Show.
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 1);
bus.write_long(TEST_SP + 2, dialog_ptr);
disp.dispatch_dialog(true, 0x028, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(
disp.dialog_items.get(&dialog_ptr).unwrap()[0].rect,
orig_rect,
"round-trip hide→show must restore the exact original rect"
);
}
#[test]
fn hide_dialog_item_already_hidden_left_coord_is_noop() {
// MTE 1992, 6-123: left > 8192 means the item is already hidden.
let (mut disp, mut cpu, mut bus) = setup();
let dialog_ptr = 0x210000u32;
let already_hidden = (10, 9000, 30, 9200);
disp.dialog_items.insert(
dialog_ptr,
vec![DialogItem {
item_type: 4,
rect: already_hidden,
text: "Hidden".into(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
}],
);
bus.write_word(TEST_SP, 1);
bus.write_long(TEST_SP + 2, dialog_ptr);
let result = disp.dispatch_dialog(true, 0x027, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
assert_eq!(
disp.dialog_items.get(&dialog_ptr).unwrap()[0].rect,
already_hidden
);
assert!(
!disp.hidden_dialog_item_rects.contains_key(&(dialog_ptr, 1)),
"already hidden items must not record a new saved rect"
);
}
#[test]
fn show_dialog_item_without_saved_rect_subtracts_offset_when_hidden() {
// MTE 1992, 6-124: ShowDialogItem uses left > 8192 as hidden predicate.
let (mut disp, mut cpu, mut bus) = setup();
let dialog_ptr = 0x210000u32;
disp.dialog_items.insert(
dialog_ptr,
vec![DialogItem {
item_type: 4,
rect: (10, 30 + 16384, 30, 130 + 16384),
text: "ShowMe".into(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
}],
);
bus.write_word(TEST_SP, 1);
bus.write_long(TEST_SP + 2, dialog_ptr);
let result = disp.dispatch_dialog(true, 0x028, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
assert_eq!(
disp.dialog_items.get(&dialog_ptr).unwrap()[0].rect,
(10, 30, 30, 130)
);
}
#[test]
fn show_dialog_item_visible_left_coord_is_noop() {
// MTE 1992, 6-124: left < 8192 means already visible; no-op.
let (mut disp, mut cpu, mut bus) = setup();
let dialog_ptr = 0x210000u32;
let visible = (10, 20, 30, 120);
disp.dialog_items.insert(
dialog_ptr,
vec![DialogItem {
item_type: 4,
rect: visible,
text: "Visible".into(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
}],
);
bus.write_word(TEST_SP, 1);
bus.write_long(TEST_SP + 2, dialog_ptr);
let result = disp.dispatch_dialog(true, 0x028, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
assert_eq!(disp.dialog_items.get(&dialog_ptr).unwrap()[0].rect, visible);
}
// ---- FindDItem ($A984) ----
#[test]
fn findditem_returns_zero_based_index_for_hit_and_pops_stack() {
// MTE 1992, 6-125: FindDialogItem/FindDItem returns 0 for the first
// item, 1 for the second, etc.
let (mut disp, mut cpu, mut bus) = setup();
let dialog_ptr = 0x220000u32;
disp.dialog_items.insert(
dialog_ptr,
vec![
DialogItem {
item_type: 4,
rect: (10, 20, 30, 40),
text: "A".into(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
},
DialogItem {
item_type: 4,
rect: (40, 50, 70, 90),
text: "B".into(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
},
],
);
bus.write_word(TEST_SP, 45);
bus.write_word(TEST_SP + 2, 55);
bus.write_long(TEST_SP + 4, dialog_ptr);
let result = disp.dispatch_dialog(true, 0x184, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
assert_eq!(bus.read_word(TEST_SP + 8) as i16, 1);
}
#[test]
fn findditem_point_outside_all_items_returns_minus_one() {
// IM:IV 1986, p. IV-60: FindDItem returns -1 when the point does not
// lie within any item rectangle.
let (mut disp, mut cpu, mut bus) = setup();
let dialog_ptr = 0x220100u32;
disp.dialog_items.insert(
dialog_ptr,
vec![DialogItem {
item_type: 4,
rect: (10, 20, 30, 40),
text: "A".into(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
}],
);
bus.write_word(TEST_SP, 99);
bus.write_word(TEST_SP + 2, 99);
bus.write_long(TEST_SP + 4, dialog_ptr);
let result = disp.dispatch_dialog(true, 0x184, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
assert_eq!(bus.read_word(TEST_SP + 8) as i16, -1);
}
#[test]
fn findditem_returns_first_overlapping_item_and_includes_disabled_items() {
// IM:IV 1986, p. IV-60: overlap resolution is first item in list.
// IM:IV 1986, p. IV-60 note: disabled items are still returned.
let (mut disp, mut cpu, mut bus) = setup();
let dialog_ptr = 0x220200u32;
disp.dialog_items.insert(
dialog_ptr,
vec![
DialogItem {
item_type: 0x80 | 4, // disabled
rect: (10, 20, 40, 60),
text: "DisabledTop".into(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
},
DialogItem {
item_type: 4,
rect: (20, 30, 50, 70),
text: "EnabledBottom".into(),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
},
],
);
// Point lies within both rectangles.
bus.write_word(TEST_SP, 25);
bus.write_word(TEST_SP + 2, 35);
bus.write_long(TEST_SP + 4, dialog_ptr);
let result = disp.dispatch_dialog(true, 0x184, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
assert_eq!(bus.read_word(TEST_SP + 8) as i16, 0);
}
// ========== Cursor Manager ==========
// ---- InitCursor ($A850) ----
#[test]
fn init_cursor_resets_cursor_level_to_zero_and_sets_arrow_visible() {
// IM:I I-167: InitCursor sets arrow cursor, sets cursor level to 0,
// and makes cursor visible.
let (mut disp, mut cpu, mut bus) = setup();
// Start from a hidden nested level to prove InitCursor reset.
disp.cursor_data = None;
disp.cursor_level = -3;
disp.cursor_visible = false;
let result = disp.dispatch_dialog(true, 0x050, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert!(disp.cursor_data.is_some());
assert_eq!(disp.cursor_level, 0);
assert!(disp.cursor_visible);
}
// ---- SetCursor ($A851) ----
#[test]
fn set_cursor_reads_cursor_data_and_pops_4_bytes() {
let (mut disp, mut cpu, mut bus) = setup();
let crsr_ptr = 0x300000u32;
// Write cursor bitmap data (32 bytes)
for i in 0..32u32 {
bus.write_byte(crsr_ptr + i, 0xAA);
}
// Write cursor mask data (32 bytes)
for i in 0..32u32 {
bus.write_byte(crsr_ptr + 32 + i, 0xFF);
}
// Write hotspot
bus.write_word(crsr_ptr + 64, 5); // hot_v
bus.write_word(crsr_ptr + 66, 3); // hot_h
// SP+0: crsr_ptr (4 bytes)
bus.write_long(TEST_SP, crsr_ptr);
let result = disp.dispatch_dialog(true, 0x051, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
let (data, mask, hot_v, hot_h) = disp.cursor_data().unwrap();
assert!(data.iter().all(|&b| b == 0xAA));
assert!(mask.iter().all(|&b| b == 0xFF));
assert_eq!(hot_v, 5);
assert_eq!(hot_h, 3);
}
#[test]
fn set_cursor_does_not_force_visibility_when_cursor_is_hidden() {
// IM:I I-167: if the cursor is hidden, SetCursor changes the
// current cursor image but it remains hidden until uncovered.
let (mut disp, mut cpu, mut bus) = setup();
let crsr_ptr = 0x300100u32;
for i in 0..32u32 {
bus.write_byte(crsr_ptr + i, 0x11);
bus.write_byte(crsr_ptr + 32 + i, 0x22);
}
bus.write_word(crsr_ptr + 64, 9);
bus.write_word(crsr_ptr + 66, 4);
bus.write_long(TEST_SP, crsr_ptr);
disp.cursor_level = -1;
disp.cursor_visible = false;
let result = disp.dispatch_dialog(true, 0x051, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(disp.cursor_level, -1);
assert!(!disp.cursor_visible);
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
}
// ---- HideCursor ($A852) ----
#[test]
fn hide_cursor_decrements_level_and_hides_cursor() {
// IM:I I-168: HideCursor decrements cursor level (from 0 to -1)
// and removes the cursor from the screen.
let (mut disp, mut cpu, mut bus) = setup();
disp.cursor_level = 0;
disp.cursor_visible = true;
let result = disp.dispatch_dialog(true, 0x052, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(disp.cursor_level, -1);
assert!(!disp.cursor_visible);
}
// ---- ShowCursor ($A853) ----
#[test]
fn show_cursor_balances_hidecursor_and_reveals_at_level_zero() {
// IM:I I-168: ShowCursor increments toward 0 and only shows the
// cursor when level becomes 0.
let (mut disp, mut cpu, mut bus) = setup();
disp.cursor_level = -2;
disp.cursor_visible = false;
let result = disp.dispatch_dialog(true, 0x053, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(disp.cursor_level, -1);
assert!(!disp.cursor_visible);
let result = disp.dispatch_dialog(true, 0x053, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(disp.cursor_level, 0);
assert!(disp.cursor_visible);
}
#[test]
fn show_cursor_at_level_zero_is_noop() {
// IM:I I-168: extra ShowCursor calls have no effect and do not
// increment cursor level above 0.
let (mut disp, mut cpu, mut bus) = setup();
disp.cursor_level = 0;
disp.cursor_visible = true;
let result = disp.dispatch_dialog(true, 0x053, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(disp.cursor_level, 0);
assert!(disp.cursor_visible);
}
// ---- ObscureCursor ($A856) ----
#[test]
fn obscure_cursor_noop_preserves_cursor_level_visibility_and_stack() {
// IM:I I-168: ObscureCursor has no effect on cursor level and takes
// no arguments. Systemless's HLE compromise keeps it as a no-op because
// synthesized mouse-move events would immediately un-obscure anyway.
let (mut disp, mut cpu, mut bus) = setup();
disp.cursor_level = -1;
disp.cursor_visible = false;
let sp_before = cpu.read_reg(Register::A7);
let result = disp.dispatch_dialog(true, 0x056, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), sp_before);
assert_eq!(disp.cursor_level, -1);
assert!(!disp.cursor_visible);
}
#[test]
fn obscure_cursor_five_call_composition_preserves_stack_pointer_and_level() {
// 5 successive ObscureCursor dispatches inside one
// StackSpace sandwich leave SP unchanged. Per IM:I I-168 the
// PROCEDURE has no arguments and no result slot, so each call
// pops 0 bytes; the cumulative SP delta after N calls is zero.
// Also pins the cursor-level no-effect contract per IM:I I-168:
// ObscureCursor "has no effect on the cursor level and must
// not be balanced by a call to ShowCursor."
let (mut disp, mut cpu, mut bus) = setup();
disp.cursor_level = -2;
disp.cursor_visible = false;
let sp_before = cpu.read_reg(Register::A7);
for i in 0..5 {
let result = disp.dispatch_dialog(true, 0x056, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok(), "iteration {i} dispatch failed");
assert_eq!(
cpu.read_reg(Register::A7),
sp_before,
"iteration {i}: ObscureCursor must leave SP unchanged (0-byte pop)",
);
assert_eq!(
disp.cursor_level, -2,
"iteration {i}: ObscureCursor must NOT change cursor level per IM:I I-168",
);
}
}
// ---- GetCursor ($A9B9) — IM:I I-474 contract ----
#[test]
fn get_cursor_returns_handle_for_system_cursor() {
// crossCursor (ID 2) is one of the four standard system
// cursors per IM:I I-475..I-477. Systemless synthesises it via
// [`TrapDispatcher::synthesize_system_cursor`] because the
// System file's resource fork isn't loaded; the result must
// be a non-NIL handle whose master ptr's bitmap matches the
// synthesised crosshair.
let (mut disp, mut cpu, mut bus) = setup();
bus.write_word(TEST_SP, 2); // crossCursor
let _ = disp.dispatch_dialog(true, 0x1B9, &mut cpu, &mut bus);
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 2);
let handle = bus.read_long(TEST_SP + 2);
assert_ne!(
handle, 0,
"GetCursor(crossCursor) must return non-NIL handle for a built-in system cursor"
);
let crsr = bus.read_long(handle);
let hot_v = bus.read_word(crsr + 64) as i16;
let hot_h = bus.read_word(crsr + 66) as i16;
assert_eq!(
(hot_v, hot_h),
(7, 7),
"crossCursor's hotspot is documented at (7,7) in IM:I I-475"
);
}
#[test]
fn get_cursor_returns_nil_for_unknown_id_per_im_i_474() {
// IM:I I-474: "If the resource can't be read, GetCursor
// returns NIL." For an ID that's neither a CURS resource we
// have loaded nor one of the four standard built-ins, the
// miss path is NIL — not a fresh empty 68-byte block. Apps
// that defensively check `if handle = NIL then use_arrow
// else SetCursor(handle^^)` would otherwise SetCursor a
// blank cursor onto the screen.
let (mut disp, mut cpu, mut bus) = setup();
bus.write_word(TEST_SP, 999); // not a system cursor, no CURS 999 installed
let _ = disp.dispatch_dialog(true, 0x1B9, &mut cpu, &mut bus);
assert_eq!(
cpu.read_reg(Register::A7),
TEST_SP + 2,
"GetCursor must pop 2 bytes (cursorID INTEGER)"
);
assert_eq!(
bus.read_long(TEST_SP + 2),
0,
"GetCursor must return NIL when CURS resource is missing per IM:I I-474"
);
}
#[test]
fn get_cursor_returns_stable_handle_across_repeated_calls_for_system_cursor() {
// Apps cache GetCursor results at boot and pass them to
// SetCursor every frame; without handle stability the
// dispatcher would leak a 68-byte block per call. Pin the
// cache hit so a future "tighten alloc" change doesn't
// accidentally drop the cache.
let (mut disp, mut cpu, mut bus) = setup();
bus.write_word(TEST_SP, 4); // watchCursor
let _ = disp.dispatch_dialog(true, 0x1B9, &mut cpu, &mut bus);
let h1 = bus.read_long(TEST_SP + 2);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 4);
let _ = disp.dispatch_dialog(true, 0x1B9, &mut cpu, &mut bus);
let h2 = bus.read_long(TEST_SP + 2);
assert_eq!(
h1, h2,
"GetCursor must return the same handle for repeated calls on a system cursor ID"
);
}
#[test]
fn get_cursor_returns_handle_to_loaded_curs_resource() {
// With a CURS resource installed, GetCursor must return a
// handle whose master ptr equals the loaded resource bytes
// — same value-asserting gate as get_icon_returns_handle_to_loaded_icon_resource.
let (mut disp, mut cpu, mut bus) = setup();
// CURS records are 68 bytes (32 data + 32 mask + 4 hotSpot).
let curs_data: Vec<u8> = (0..68).map(|i| (i as u8).wrapping_mul(11)).collect();
let res_ptr = disp.install_test_resource(&mut bus, *b"CURS", 200, &curs_data);
bus.write_word(TEST_SP, 200);
let _ = disp.dispatch_dialog(true, 0x1B9, &mut cpu, &mut bus);
let handle = bus.read_long(TEST_SP + 2);
assert_ne!(
handle, 0,
"GetCursor must return non-NIL handle when CURS resource is loaded"
);
assert_eq!(
bus.read_long(handle),
res_ptr,
"GetCursor's handle must dereference to the loaded CURS resource bytes"
);
}
// ---- GetPattern ($A9B8) — IM:I I-473 contract ----
#[test]
fn get_pattern_returns_nil_for_missing_resource_per_im_i_473() {
// IM:I I-473: "If the resource can't be read, GetPattern
// returns NIL." The previous Stub returned a handle to a
// fresh all-0xFF (white) 8-byte block, strictly worse than
// NIL because callers branching on `handle = NIL` took the
// FillRect-with-white path instead of the recovery path.
let (mut disp, mut cpu, mut bus) = setup();
bus.write_word(TEST_SP, 1); // patID = 1, no PAT 1 installed
let _ = disp.dispatch_dialog(true, 0x1B8, &mut cpu, &mut bus);
assert_eq!(
cpu.read_reg(Register::A7),
TEST_SP + 2,
"GetPattern must pop 2 bytes (patID INTEGER)"
);
assert_eq!(
bus.read_long(TEST_SP + 2),
0,
"GetPattern must return NIL when PAT resource is missing per IM:I I-473"
);
}
#[test]
fn get_pattern_returns_handle_to_loaded_pat_resource() {
// With a PAT resource installed, GetPattern must return a
// handle whose master ptr equals the loaded resource bytes.
// Pin master-ptr-equality so a future regression that
// allocates fresh memory instead of reusing the loaded ptr
// fails here. Same gate shape as
// get_icon_returns_handle_to_loaded_icon_resource.
let (mut disp, mut cpu, mut bus) = setup();
let pat_data: [u8; 8] = [0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA];
let res_ptr = disp.install_test_resource(&mut bus, *b"PAT ", 16, &pat_data);
bus.write_word(TEST_SP, 16);
let _ = disp.dispatch_dialog(true, 0x1B8, &mut cpu, &mut bus);
let handle = bus.read_long(TEST_SP + 2);
assert_ne!(
handle, 0,
"GetPattern must return non-NIL handle when PAT resource is loaded"
);
assert_eq!(
bus.read_long(handle),
res_ptr,
"GetPattern's handle must dereference to the loaded PAT resource bytes \
(not a fresh all-0xFF allocation)"
);
// Verify the bytes through the handle deref are the
// installed sentinel pattern (50/AA stripes), not 0xFF.
let master = bus.read_long(handle);
for (i, want) in pat_data.iter().enumerate() {
assert_eq!(
bus.read_byte(master + i as u32),
*want,
"GetPattern PAT byte +{i} must match installed resource byte"
);
}
}
#[test]
fn get_pattern_returns_stable_handle_across_repeated_calls() {
// Apps that paint with a pattern in tight loops cache the
// GetPattern result; pin handle stability so a future
// regression that drops `get_or_create_resource_handle` for
// a fresh `bus.alloc(4)` per call surfaces here.
let (mut disp, mut cpu, mut bus) = setup();
let pat_data: [u8; 8] = [0x0F, 0xF0, 0x0F, 0xF0, 0x0F, 0xF0, 0x0F, 0xF0];
let _ = disp.install_test_resource(&mut bus, *b"PAT ", 100, &pat_data);
bus.write_word(TEST_SP, 100);
let _ = disp.dispatch_dialog(true, 0x1B8, &mut cpu, &mut bus);
let h1 = bus.read_long(TEST_SP + 2);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 100);
let _ = disp.dispatch_dialog(true, 0x1B8, &mut cpu, &mut bus);
let h2 = bus.read_long(TEST_SP + 2);
assert_eq!(
h1, h2,
"GetPattern must return the same handle for repeated calls on a loaded PAT resource"
);
}
// ---- GetIcon ($A9BB) — IM:I I-473 contract ----
#[test]
fn get_icon_returns_nil_when_resource_missing() {
// IM:I I-473: "If the resource can't be read, GetIcon
// returns NIL." Critical for apps that defensively check
// `if handle = NIL` before dereferencing — the prior Stub
// always returned a non-NIL handle to uninitialised
// memory which broke that branch.
let (mut disp, mut cpu, mut bus) = setup();
bus.write_word(TEST_SP, 1); // iconID = 1, no ICON 1 installed
let _ = disp.dispatch_dialog(true, 0x1BB, &mut cpu, &mut bus);
assert_eq!(
cpu.read_reg(Register::A7),
TEST_SP + 2,
"GetIcon must pop 2 bytes (iconID INTEGER)"
);
assert_eq!(
bus.read_long(TEST_SP + 2),
0,
"GetIcon must return NIL when ICON resource is missing per IM:I I-473"
);
}
#[test]
fn get_icon_returns_handle_to_loaded_icon_resource() {
// With an ICON resource installed, GetIcon must return a
// non-NIL handle whose master ptr points at the loaded
// resource bytes. Pin the master-ptr-equality so a future
// regression that allocates fresh memory instead of
// reusing the loaded ptr fails here.
let (mut disp, mut cpu, mut bus) = setup();
// Real ICON resources are 128 bytes (32x32 1bpp); fill
// with a recognisable sentinel pattern so handle deref
// assertions can verify "this is the right resource."
let icon_data: Vec<u8> = (0..128).map(|i| (i as u8).wrapping_mul(7)).collect();
let res_ptr = disp.install_test_resource(&mut bus, *b"ICON", 128, &icon_data);
bus.write_word(TEST_SP, 128);
let _ = disp.dispatch_dialog(true, 0x1BB, &mut cpu, &mut bus);
let handle = bus.read_long(TEST_SP + 2);
assert_ne!(
handle, 0,
"GetIcon must return a non-NIL handle when ICON resource is loaded"
);
assert_eq!(
bus.read_long(handle),
res_ptr,
"GetIcon's handle must dereference to the loaded ICON resource bytes \
(not a fresh uninitialised allocation)"
);
// Verify the bytes through the handle deref are the
// installed sentinel pattern.
let master = bus.read_long(handle);
for (i, want) in icon_data.iter().enumerate() {
assert_eq!(
bus.read_byte(master + i as u32),
*want,
"GetIcon ICON byte +{i} must match installed resource byte"
);
}
}
#[test]
fn get_icon_returns_stable_handle_across_repeated_calls() {
// Per IM:Resource Manager, GetResource returns the SAME
// handle on repeat calls (unless ReleaseResource has run).
// GetIcon delegates to GetResource per IM:I I-473, so it
// must inherit this stability — apps that cache the handle
// depend on it not changing between alert cycles.
let (mut disp, mut cpu, mut bus) = setup();
let icon_data: Vec<u8> = vec![0xCC; 128];
disp.install_test_resource(&mut bus, *b"ICON", 200, &icon_data);
bus.write_word(TEST_SP, 200);
let _ = disp.dispatch_dialog(true, 0x1BB, &mut cpu, &mut bus);
let h1 = bus.read_long(TEST_SP + 2);
// Reset SP and call again with the same iconID.
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 200);
let _ = disp.dispatch_dialog(true, 0x1BB, &mut cpu, &mut bus);
let h2 = bus.read_long(TEST_SP + 2);
assert_ne!(h1, 0, "first GetIcon must return non-NIL");
assert_eq!(
h1, h2,
"GetIcon must return the SAME handle on repeated calls per IM:Resource Mgr stability"
);
}
#[test]
fn get_icon_pops_two_bytes_per_pascal_signature() {
// FUNCTION GetIcon(iconID: INTEGER): Handle;
// Pascal stack frame: result Handle (4) pre-pushed by
// caller, iconID (2) on top. Trap pops 2 bytes (iconID),
// result lands at new SP+0 (= old SP+2).
let (mut disp, mut cpu, mut bus) = setup();
bus.write_word(TEST_SP, 999); // any iconID
let pre_a7 = cpu.read_reg(Register::A7);
let _ = disp.dispatch_dialog(true, 0x1BB, &mut cpu, &mut bus);
assert_eq!(
cpu.read_reg(Register::A7),
pre_a7 + 2,
"GetIcon must advance A7 by 2 bytes (iconID INTEGER)"
);
}
// ---- GetPicture ($A9BC) ----
#[test]
fn get_picture_returns_nil_without_resource() {
let (mut disp, mut cpu, mut bus) = setup();
// SP+0: picture id (2 bytes)
bus.write_word(TEST_SP, 1);
let result = disp.dispatch_dialog(true, 0x1BC, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 2);
let handle = bus.read_long(TEST_SP + 2);
assert_eq!(handle, 0); // NIL when no PICT resource loaded
}
#[test]
fn get_picture_reloads_released_resource_from_open_resource_file() {
fn make_resource_fork_bytes(resources: &[([u8; 4], i16, &[u8])]) -> Vec<u8> {
let mut type_groups: Vec<([u8; 4], Vec<(i16, &[u8], u32)>)> = Vec::new();
for (res_type, res_id, data) in resources {
let group_idx = type_groups
.iter()
.position(|(existing_type, _)| existing_type == res_type)
.unwrap_or_else(|| {
type_groups.push((*res_type, Vec::new()));
type_groups.len() - 1
});
type_groups[group_idx].1.push((*res_id, *data, 0));
}
type_groups.sort_by_key(|(res_type, _)| *res_type);
for (_, entries) in &mut type_groups {
entries.sort_by_key(|(res_id, _, _)| *res_id);
}
let data_offset = 16u32;
let mut data_section = Vec::new();
for (_, entries) in &mut type_groups {
for (_, data, data_pos) in entries {
*data_pos = data_section.len() as u32;
data_section.extend_from_slice(&(data.len() as u32).to_be_bytes());
data_section.extend_from_slice(data);
}
}
let map_offset = data_offset + data_section.len() as u32;
let type_list_offset = 30u16;
let type_count = type_groups.len();
let resource_count: usize = type_groups.iter().map(|(_, entries)| entries.len()).sum();
let ref_lists_offset = 2 + type_count * 8;
let name_list_offset =
type_list_offset as usize + ref_lists_offset + resource_count * 12;
let map_length = name_list_offset as u32;
let mut bytes = vec![0u8; (map_offset + map_length) as usize];
let mut header = [0u8; 16];
header[0..4].copy_from_slice(&data_offset.to_be_bytes());
header[4..8].copy_from_slice(&map_offset.to_be_bytes());
header[8..12].copy_from_slice(&(data_section.len() as u32).to_be_bytes());
header[12..16].copy_from_slice(&map_length.to_be_bytes());
bytes[0..16].copy_from_slice(&header);
bytes[data_offset as usize..data_offset as usize + data_section.len()]
.copy_from_slice(&data_section);
let map_start = map_offset as usize;
bytes[map_start..map_start + 16].copy_from_slice(&header);
bytes[map_start + 24..map_start + 26].copy_from_slice(&type_list_offset.to_be_bytes());
bytes[map_start + 26..map_start + 28]
.copy_from_slice(&(name_list_offset as u16).to_be_bytes());
bytes[map_start + 28..map_start + 30]
.copy_from_slice(&((type_count as u16) - 1).to_be_bytes());
let type_list_start = map_start + type_list_offset as usize;
bytes[type_list_start..type_list_start + 2]
.copy_from_slice(&((type_count as u16) - 1).to_be_bytes());
let mut next_ref_list_offset = ref_lists_offset;
for (i, (res_type, entries)) in type_groups.iter().enumerate() {
let type_entry = type_list_start + 2 + i * 8;
bytes[type_entry..type_entry + 4].copy_from_slice(res_type);
bytes[type_entry + 4..type_entry + 6]
.copy_from_slice(&((entries.len() as u16) - 1).to_be_bytes());
bytes[type_entry + 6..type_entry + 8]
.copy_from_slice(&(next_ref_list_offset as u16).to_be_bytes());
let ref_list_start = type_list_start + next_ref_list_offset;
for (j, (res_id, _, data_pos)) in entries.iter().enumerate() {
let ref_entry = ref_list_start + j * 12;
bytes[ref_entry..ref_entry + 2]
.copy_from_slice(&(*res_id as u16).to_be_bytes());
bytes[ref_entry + 2..ref_entry + 4].copy_from_slice(&0xFFFFu16.to_be_bytes());
bytes[ref_entry + 4] = 0;
let data_offset_bytes = data_pos.to_be_bytes();
bytes[ref_entry + 5..ref_entry + 8].copy_from_slice(&data_offset_bytes[1..4]);
}
next_ref_list_offset += entries.len() * 12;
}
bytes
}
let (mut disp, mut cpu, mut bus) = setup();
let pict_data = b"\x00\x11Wordtris reloadable picture data";
let fork = make_resource_fork_bytes(&[(*b"PICT", 1127, &pict_data[..])]);
disp.vfs_rsrc.insert("Pictures".to_string(), fork);
disp.open_resource_file_from_vfs_key(&mut bus, "Pictures", false);
bus.write_word(TEST_SP, 1127);
let first = disp.dispatch_dialog(true, 0x1BC, &mut cpu, &mut bus);
assert!(first.unwrap().is_ok());
let first_handle = bus.read_long(TEST_SP + 2);
assert_ne!(first_handle, 0, "first GetPicture must find PICT 1127");
let first_ptr = bus.read_long(first_handle);
assert_eq!(bus.read_bytes(first_ptr, pict_data.len()), pict_data);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, first_handle);
let release = disp.dispatch_resource(true, 0x1A3, &mut cpu, &mut bus);
assert!(release.unwrap().is_ok());
assert_eq!(
bus.read_long(first_handle),
0,
"ReleaseResource should clear the old master pointer"
);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 1127);
let second = disp.dispatch_dialog(true, 0x1BC, &mut cpu, &mut bus);
assert!(second.unwrap().is_ok());
let second_handle = bus.read_long(TEST_SP + 2);
assert_ne!(
second_handle, 0,
"GetPicture must reload a released PICT from the open resource file"
);
assert_ne!(
second_handle, first_handle,
"a released resource should be returned through a fresh handle"
);
let second_ptr = bus.read_long(second_handle);
assert_eq!(bus.read_bytes(second_ptr, pict_data.len()), pict_data);
}
// ---- GetString ($A9BA) ----
#[test]
fn get_string_returns_loaded_str_resource_handle() {
let (mut disp, mut cpu, mut bus) = setup();
let str_ptr = bus.alloc(6);
bus.write_byte(str_ptr, 5);
bus.write_bytes(str_ptr + 1, b"Hello");
disp.resources = Some(crate::trap::dispatch::LoadedResources {
files: std::collections::HashMap::from([(
0,
crate::trap::dispatch::ResourceFileMap {
loaded: std::collections::HashMap::from([((*b"STR ", 1), str_ptr)]),
named: std::collections::HashMap::new(),
names_by_id: std::collections::HashMap::new(),
attrs: std::collections::HashMap::new(),
map_attrs: 0,
},
)]),
names: std::collections::HashMap::new(),
search_order: vec![0],
current_file: 0,
});
// SP+0: string id (2 bytes)
bus.write_word(TEST_SP, 1);
let result = disp.dispatch_dialog(true, 0x1BA, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 2);
let handle = bus.read_long(TEST_SP + 2);
assert_ne!(handle, 0);
assert_eq!(bus.read_long(handle), str_ptr);
}
#[test]
fn get_string_returns_nil_when_resource_is_missing() {
let (mut disp, mut cpu, mut bus) = setup();
bus.write_word(TEST_SP, 999);
let result = disp.dispatch_dialog(true, 0x1BA, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 2);
assert_eq!(bus.read_long(TEST_SP + 2), 0);
}
// ---- Unhandled trap returns None ----
#[test]
fn unhandled_trap_returns_none() {
let (mut disp, mut cpu, mut bus) = setup();
let result = disp.dispatch_dialog(true, 0xFFFF, &mut cpu, &mut bus);
assert!(result.is_none());
}
}