use super::dispatch::{ControlAuxRecordState, ControlTrackingState};
use super::types::{Rect, ShapeOp};
use crate::cpu::{CpuOps, Register};
use crate::memory::{MacMemoryBus, MemoryBus};
use crate::quickdraw::text::get_font_metrics;
use crate::Result;
use std::sync::OnceLock;
static TRACE_CONTROLS: OnceLock<bool> = OnceLock::new();
fn trace_controls_enabled() -> bool {
*TRACE_CONTROLS.get_or_init(|| std::env::var_os("SYSTEMLESS_TRACE_CONTROLS").is_some())
}
impl super::TrapDispatcher {
const LOWMEM_AUX_CTL_HEAD: u32 = 0x0CD4;
const AUX_CTL_NEXT_OFFSET: u32 = 0;
const AUX_CTL_OWNER_OFFSET: u32 = 4;
const AUX_CTL_CTABLE_OFFSET: u32 = 8;
const AUX_CTL_FLAGS_OFFSET: u32 = 12;
const AUX_CTL_RESERVED_OFFSET: u32 = 14;
const AUX_CTL_REFCON_OFFSET: u32 = 18;
const AUX_CTL_RECORD_SIZE: u32 = 22;
fn control_record_ptr(bus: &MacMemoryBus, ctrl_handle: u32) -> u32 {
if ctrl_handle == 0 {
0
} else {
bus.read_long(ctrl_handle)
}
}
fn read_pascal_string(bus: &MacMemoryBus, str_ptr: u32) -> Vec<u8> {
if str_ptr == 0 {
return Vec::new();
}
let len = bus.read_byte(str_ptr) as usize;
(0..len)
.map(|i| bus.read_byte(str_ptr + 1 + i as u32))
.collect()
}
fn write_pascal_string(bus: &mut MacMemoryBus, str_ptr: u32, bytes: &[u8]) {
if str_ptr == 0 {
return;
}
let len = bytes.len().min(255);
bus.write_byte(str_ptr, len as u8);
for (index, &byte) in bytes.iter().take(len).enumerate() {
bus.write_byte(str_ptr + 1 + index as u32, byte);
}
}
pub(crate) fn control_title_bytes(bus: &MacMemoryBus, ctrl_ptr: u32) -> Vec<u8> {
Self::read_pascal_string(bus, ctrl_ptr + 40)
}
fn set_control_title_bytes(bus: &mut MacMemoryBus, ctrl_ptr: u32, bytes: &[u8]) {
Self::write_pascal_string(bus, ctrl_ptr + 40, bytes);
}
fn control_def_proc_handle(&mut self, bus: &mut MacMemoryBus, proc_id: i16) -> u32 {
if proc_id <= 0 {
return 0;
}
let cdef_id = proc_id >> 4;
self.find_resource_any(*b"CDEF", cdef_id)
.map(|(_, ptr)| ptr)
.map(|ptr| self.get_or_create_resource_handle(bus, *b"CDEF", cdef_id, ptr))
.unwrap_or(0)
}
fn control_aux_reserved_value(&self, bus: &MacMemoryBus, ctrl_handle: u32) -> u32 {
let ctrl_ptr = Self::control_record_ptr(bus, ctrl_handle);
let proc_id = self.control_proc_ids.get(&ctrl_ptr).copied().unwrap_or(0);
u32::from((proc_id & 0xF) as u16) << 24
}
fn standard_testcontrol_part_code(&self, ctrl_ptr: u32) -> u16 {
match self.control_proc_ids.get(&ctrl_ptr).copied().unwrap_or(0) {
1 | 2 => 11, _ => 10, }
}
pub(crate) fn write_control_value(
&mut self,
bus: &mut MacMemoryBus,
ctrl_handle: u32,
value: i16,
) {
let ctrl_ptr = Self::control_record_ptr(bus, ctrl_handle);
if ctrl_ptr == 0 {
return;
}
if let Some((dlg_ptr, item_no)) = self.dialog_control_handles.get(&ctrl_handle).copied() {
self.dialog_control_values.insert((dlg_ptr, item_no), value);
}
bus.write_word(ctrl_ptr + 18, value as u16);
}
fn sync_dialog_item_rect_for_control(&mut self, bus: &MacMemoryBus, ctrl_handle: u32) {
let ctrl_ptr = Self::control_record_ptr(bus, ctrl_handle);
if ctrl_ptr == 0 {
return;
}
let Some((dialog_ptr, item_no)) = self.dialog_control_handles.get(&ctrl_handle).copied()
else {
return;
};
let 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 idx = (item_no as usize).wrapping_sub(1);
if let Some(items) = self.dialog_items.get_mut(&dialog_ptr) {
if idx < items.len() {
items[idx].rect = rect;
}
}
if let Some(tracking) = self.dialog_tracking.as_mut() {
if tracking.dialog_ptr == dialog_ptr && idx < tracking.items.len() {
tracking.items[idx].rect = rect;
}
}
}
pub(crate) fn popup_control_dropdown_rect(
&self,
bus: &MacMemoryBus,
ctrl_ptr: u32,
menu_idx: usize,
) -> (i16, i16, i16, i16) {
let (_, _, screen_width, screen_height, _) = self.get_screen_params();
let owner = bus.read_long(ctrl_ptr + 4);
let (owner_top, owner_left, _, _) = if owner != 0 {
Self::dialog_screen_bounds(bus, owner)
} else {
(0, 0, 0, 0)
};
let r_left = bus.read_word(ctrl_ptr + 10) as i16;
let r_bottom = bus.read_word(ctrl_ptr + 12) as i16;
let r_right = bus.read_word(ctrl_ptr + 14) as i16;
let abs_left = owner_left + r_left;
let abs_bottom = owner_top + r_bottom;
let item_height: i16 = 16;
let mut width = (r_right - r_left).max(80);
if let Some(menu) = self.menus.get(menu_idx) {
for item in &menu.items {
let w = Self::fb_measure_string(&item.text, 0, 12) + 30;
width = width.max(w);
}
let bottom =
(abs_bottom + menu.items.len() as i16 * item_height + 2).min(screen_height);
return (
abs_bottom,
abs_left,
bottom,
(abs_left + width).min(screen_width),
);
}
(
abs_bottom,
abs_left,
abs_bottom + 2,
(abs_left + width).min(screen_width),
)
}
fn control_tracking_item_at_point(&self, mouse_x: i16, mouse_y: i16) -> i16 {
let Some(tracking) = self.control_tracking.as_ref() else {
return 0;
};
let (top, left, bottom, right) = tracking.dropdown_rect;
if mouse_x < left || mouse_x >= right || mouse_y < top || mouse_y >= bottom {
return 0;
}
let Some(menu) = self.menus.get(tracking.active_menu) else {
return 0;
};
let item_height: i16 = 16;
let item_idx = (mouse_y - top - 1) / item_height;
if item_idx < 0 || (item_idx as usize) >= menu.items.len() {
return 0;
}
let item = &menu.items[item_idx as usize];
if item.text == "-" || !item.enabled {
return 0;
}
item_idx + 1
}
fn invert_control_tracking_item(&self, bus: &mut MacMemoryBus, item: i16) {
let Some(tracking) = self.control_tracking.as_ref() else {
return;
};
self.invert_dropdown_item_rect(bus, tracking.dropdown_rect, item);
}
fn finish_popup_control_tracking<C: CpuOps>(
&mut self,
cpu: &mut C,
bus: &mut MacMemoryBus,
selected_item: i16,
) {
let Some(tracking) = self.control_tracking.take() else {
return;
};
self.restore_dropdown_pixels(bus, tracking.dropdown_rect, &tracking.saved_pixels);
let part = if selected_item > 0 {
self.write_control_value(bus, tracking.ctrl_handle, selected_item);
self.draw_control(cpu, bus, tracking.ctrl_ptr);
10u16
} else {
0u16
};
bus.write_word(tracking.stack_ptr + 12, part);
cpu.write_reg(Register::A7, tracking.stack_ptr + 12);
}
fn standard_scrollbar_testcontrol_part_code(
&self,
bus: &MacMemoryBus,
ctrl_ptr: u32,
pt_v: i16,
pt_h: i16,
) -> u16 {
let top = bus.read_word(ctrl_ptr + 8) as i16;
let left = bus.read_word(ctrl_ptr + 10) as i16;
let bottom = bus.read_word(ctrl_ptr + 12) as i16;
let right = bus.read_word(ctrl_ptr + 14) as i16;
let value = 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;
if min >= max {
return 0;
}
let is_vertical = (bottom - top) > (right - left);
let arrow_len = 16i16.min(if is_vertical {
bottom - top
} else {
right - left
});
let range = i32::from(max) - i32::from(min);
if range <= 0 {
return 0;
}
let value_clamped = (i32::from(value) - i32::from(min)).clamp(0, range);
let thumb_len = 16i16.min(if is_vertical {
(bottom - top) - arrow_len * 2
} else {
(right - left) - arrow_len * 2
});
if thumb_len <= 0 {
return 0;
}
if is_vertical {
let track_top = top + arrow_len;
let track_bottom = bottom - arrow_len;
let track_len = track_bottom - track_top;
let travel = i32::from(track_len - thumb_len);
if travel < 0 {
return 0;
}
let thumb_top = track_top + ((value_clamped * travel) / range) as i16;
let thumb_bottom = thumb_top + thumb_len;
if pt_v < track_top {
20
} else if pt_v >= track_bottom {
21
} else if pt_v < thumb_top {
22
} else if pt_v < thumb_bottom {
129
} else {
23
}
} else {
let track_left = left + arrow_len;
let track_right = right - arrow_len;
let track_len = track_right - track_left;
let travel = i32::from(track_len - thumb_len);
if travel < 0 {
return 0;
}
let thumb_left = track_left + ((value_clamped * travel) / range) as i16;
let thumb_right = thumb_left + thumb_len;
if pt_h < track_left {
20
} else if pt_h >= track_right {
21
} else if pt_h < thumb_left {
22
} else if pt_h < thumb_right {
129
} else {
23
}
}
}
fn sync_control_aux_head_lowmem(&self, bus: &mut MacMemoryBus) {
bus.write_long(Self::LOWMEM_AUX_CTL_HEAD, self.control_aux_head);
}
fn default_control_color_table_handle(&mut self, bus: &mut MacMemoryBus) -> u32 {
let gd_handle = self.ensure_main_gdevice(bus);
let gd_ptr = bus.read_long(gd_handle);
let gd_pixmap_handle = bus.read_long(gd_ptr + 22);
let gd_pixmap_ptr = bus.read_long(gd_pixmap_handle);
bus.read_long(gd_pixmap_ptr + 42)
}
pub(crate) fn ensure_control_aux_record(
&mut self,
bus: &mut MacMemoryBus,
ctrl_handle: u32,
) -> u32 {
if ctrl_handle == 0 {
return 0;
}
let default_ctab = self.default_control_color_table_handle(bus);
if let Some(state) = self.control_aux_records.get(&ctrl_handle).copied() {
let aux_ptr = bus.read_long(state.handle);
if aux_ptr != 0 {
bus.write_long(aux_ptr + Self::AUX_CTL_OWNER_OFFSET, ctrl_handle);
if bus.read_long(aux_ptr + Self::AUX_CTL_CTABLE_OFFSET) == 0 {
bus.write_long(aux_ptr + Self::AUX_CTL_CTABLE_OFFSET, default_ctab);
}
bus.write_word(aux_ptr + Self::AUX_CTL_FLAGS_OFFSET, 0);
bus.write_long(
aux_ptr + Self::AUX_CTL_RESERVED_OFFSET,
self.control_aux_reserved_value(bus, ctrl_handle),
);
}
return state.handle;
}
let aux_ptr = bus.alloc(Self::AUX_CTL_RECORD_SIZE);
let aux_handle = bus.alloc(4);
bus.write_long(aux_handle, aux_ptr);
bus.write_long(aux_ptr + Self::AUX_CTL_NEXT_OFFSET, self.control_aux_head);
bus.write_long(aux_ptr + Self::AUX_CTL_OWNER_OFFSET, ctrl_handle);
bus.write_long(aux_ptr + Self::AUX_CTL_CTABLE_OFFSET, default_ctab);
bus.write_word(aux_ptr + Self::AUX_CTL_FLAGS_OFFSET, 0);
bus.write_long(
aux_ptr + Self::AUX_CTL_RESERVED_OFFSET,
self.control_aux_reserved_value(bus, ctrl_handle),
);
bus.write_long(aux_ptr + Self::AUX_CTL_REFCON_OFFSET, 0);
self.control_aux_head = aux_handle;
self.sync_control_aux_head_lowmem(bus);
self.control_aux_records
.insert(ctrl_handle, ControlAuxRecordState { handle: aux_handle });
aux_handle
}
fn release_control_aux_record(&mut self, bus: &mut MacMemoryBus, ctrl_handle: u32) {
let Some(state) = self.control_aux_records.remove(&ctrl_handle) else {
return;
};
let mut prev_handle = 0u32;
let mut cur_handle = self.control_aux_head;
while cur_handle != 0 {
let cur_ptr = bus.read_long(cur_handle);
if cur_ptr == 0 {
break;
}
let next_handle = bus.read_long(cur_ptr + Self::AUX_CTL_NEXT_OFFSET);
if cur_handle == state.handle {
if prev_handle == 0 {
self.control_aux_head = next_handle;
} else {
let prev_ptr = bus.read_long(prev_handle);
if prev_ptr != 0 {
bus.write_long(prev_ptr + Self::AUX_CTL_NEXT_OFFSET, next_handle);
}
}
break;
}
prev_handle = cur_handle;
cur_handle = next_handle;
}
self.sync_control_aux_head_lowmem(bus);
let aux_ptr = bus.read_long(state.handle);
if aux_ptr != 0 {
bus.free(aux_ptr);
}
bus.free(state.handle);
}
pub(crate) fn control_aux_state(&self, ctrl_handle: u32) -> Option<ControlAuxRecordState> {
self.control_aux_records.get(&ctrl_handle).copied()
}
pub(crate) fn initialize_control_record(
&mut self,
bus: &mut MacMemoryBus,
ctrl_ptr: u32,
window_ptr: u32,
bounds: (i16, i16, i16, i16),
title: &[u8],
visible: bool,
value: i16,
min: i16,
max: i16,
proc_id: i16,
ref_con: u32,
) {
bus.write_long(ctrl_ptr, 0); bus.write_long(ctrl_ptr + 4, window_ptr); bus.write_word(ctrl_ptr + 8, bounds.0 as u16);
bus.write_word(ctrl_ptr + 10, bounds.1 as u16);
bus.write_word(ctrl_ptr + 12, bounds.2 as u16);
bus.write_word(ctrl_ptr + 14, bounds.3 as u16);
bus.write_byte(ctrl_ptr + 16, if visible { 255 } else { 0 });
bus.write_byte(ctrl_ptr + 17, 0); bus.write_word(ctrl_ptr + 18, value as u16);
bus.write_word(ctrl_ptr + 20, min as u16);
bus.write_word(ctrl_ptr + 22, max as u16);
let def_proc_handle = self.control_def_proc_handle(bus, proc_id);
bus.write_long(ctrl_ptr + 24, def_proc_handle);
bus.write_long(ctrl_ptr + 28, 0); bus.write_long(ctrl_ptr + 32, 0); bus.write_long(ctrl_ptr + 36, ref_con);
Self::set_control_title_bytes(bus, ctrl_ptr, title);
self.control_proc_ids.insert(ctrl_ptr, proc_id);
}
pub(crate) fn draw_control<C: CpuOps>(
&mut self,
cpu: &mut C,
bus: &mut MacMemoryBus,
ctrl_ptr: u32,
) {
if ctrl_ptr == 0 {
return;
}
let vis = bus.read_byte(ctrl_ptr + 16);
if vis != 255 {
return;
}
let r_top = bus.read_word(ctrl_ptr + 8) as i16;
let r_left = bus.read_word(ctrl_ptr + 10) as i16;
let r_bottom = bus.read_word(ctrl_ptr + 12) as i16;
let r_right = bus.read_word(ctrl_ptr + 14) as i16;
let hilite = bus.read_byte(ctrl_ptr + 17);
let value = 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 title_bytes = Self::control_title_bytes(bus, ctrl_ptr);
let title: String = title_bytes
.iter()
.map(|&b| match b {
0xD2 | 0xD4 => '\u{201C}', 0xD3 | 0xD5 => '\'', 0xC7 => '"', 0xC8 => '"',
0xCA => ' ', 0xD0 => '-', 0xD1 => '-', b if (0x20..=0x7E).contains(&b) => b as char,
_ => '?',
})
.collect();
let proc_id = self.control_proc_ids.get(&ctrl_ptr).copied().unwrap_or(0);
let saved_pn_size = self.pn_size;
let saved_pn_mode = self.pn_mode;
let saved_pn_pat = self.pn_pat;
self.pn_size = (1, 1);
self.pn_mode = 8; self.pn_pat = [0xFF; 8];
let r = Rect {
top: r_top,
left: r_left,
bottom: r_bottom,
right: r_right,
};
let window_ptr = bus.read_long(ctrl_ptr + 4);
let (scr_top, scr_left, _, _) = Self::dialog_screen_bounds(bus, window_ptr);
let abs_top = scr_top + r_top;
let abs_left = scr_left + r_left;
let abs_bottom = scr_top + r_bottom;
let abs_right = scr_left + r_right;
match proc_id {
0 => {
let oval: i16 = 10;
self.draw_round_rect(cpu, bus, &r, oval, oval, ShapeOp::Erase);
self.draw_round_rect(cpu, bus, &r, oval, oval, ShapeOp::Frame);
self.draw_control_text(bus, abs_top, abs_left, abs_bottom, abs_right, &title);
if hilite == 1 {
let inv = Rect {
top: r_top + 1,
left: r_left + 1,
bottom: r_bottom - 1,
right: r_right - 1,
};
let inv_oval = (oval - 2).max(0);
self.draw_round_rect(cpu, bus, &inv, inv_oval, inv_oval, ShapeOp::Invert);
} else if hilite == 255 {
self.dim_rect(bus, abs_top, abs_left, abs_bottom, abs_right);
}
}
1 => {
let checked = value != 0;
let height = r_bottom - r_top;
let box_size = 12i16;
let box_top = r_top + (height - box_size) / 2;
let box_left = r_left + 2;
let box_r = Rect {
top: box_top,
left: box_left,
bottom: box_top + box_size,
right: box_left + box_size,
};
self.draw_rect(cpu, bus, &box_r, ShapeOp::Erase);
self.draw_rect(cpu, bus, &box_r, ShapeOp::Frame);
if checked {
self.draw_checkbox_x(bus, scr_top + box_top, scr_left + box_left, box_size);
}
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_x = scr_left + box_left + box_size + 4;
let text_y = scr_top + r_top + (height + 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 hilite == 1 {
self.draw_rect(cpu, bus, &box_r, ShapeOp::Invert);
} else if hilite == 255 {
let label_left = scr_left + box_left + box_size + 1;
self.dim_rect(bus, abs_top, label_left, abs_bottom, abs_right);
}
}
2 => {
let selected = value != 0;
let height = r_bottom - r_top;
let circle_size = 12i16;
let circle_top = r_top + (height - circle_size) / 2;
let circle_left = r_left + 2;
let circle_r = Rect {
top: circle_top,
left: circle_left,
bottom: circle_top + circle_size,
right: circle_left + circle_size,
};
self.draw_oval(cpu, bus, &circle_r, ShapeOp::Erase);
self.draw_oval(cpu, bus, &circle_r, ShapeOp::Frame);
if selected {
let inner_r = Rect {
top: circle_top + 3,
left: circle_left + 3,
bottom: circle_top + circle_size - 3,
right: circle_left + circle_size - 3,
};
self.draw_oval(cpu, bus, &inner_r, ShapeOp::Paint);
}
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_x = scr_left + circle_left + circle_size + 4;
let text_y = scr_top + r_top + (height + 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 hilite == 1 {
self.draw_oval(cpu, bus, &circle_r, ShapeOp::Invert);
} else if hilite == 255 {
let label_left = scr_left + circle_left + circle_size + 1;
self.dim_rect(bus, abs_top, label_left, abs_bottom, abs_right);
}
}
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 selected = value.max(1) as usize;
let item_title = self.popup_menu_item_title(bus, min, selected);
self.draw_popup_control(
bus,
abs_top,
abs_left,
abs_bottom,
abs_right,
&item_title.unwrap_or_default(),
);
}
_ => {
if trace_controls_enabled() {
eprintln!(
"[CONTROL] Unknown procID {} for control at ${:08X}",
proc_id, ctrl_ptr
);
}
}
}
self.pn_size = saved_pn_size;
self.pn_mode = saved_pn_mode;
self.pn_pat = saved_pn_pat;
}
fn draw_control_text(
&self,
bus: &mut MacMemoryBus,
abs_top: i16,
abs_left: i16,
abs_bottom: i16,
abs_right: i16,
title: &str,
) {
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_w = Self::fb_measure_string(title, font_id, font_size);
let text_x = abs_left + (abs_right - abs_left - text_w) / 2;
let text_y = abs_top
+ (abs_bottom - abs_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_checkbox_x(
&self,
bus: &mut MacMemoryBus,
box_top_screen: i16,
box_left_screen: i16,
box_size: i16,
) {
let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
self.get_screen_params();
for i in 1..box_size - 1 {
Self::fb_set_pixel(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
box_left_screen + i,
box_top_screen + i,
true,
);
Self::fb_set_pixel(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
box_left_screen + box_size - 1 - i,
box_top_screen + i,
true,
);
}
}
fn dim_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();
for y in top..bottom {
if y < 0 || y >= screen_height {
continue;
}
for x in left..right {
if x < 0 || x >= screen_width {
continue;
}
if (x + y) & 1 == 0 {
Self::fb_set_pixel(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
x,
y,
false,
);
}
}
}
}
pub(crate) fn draw_scroll_bar(
&self,
bus: &mut MacMemoryBus,
top: i16,
left: i16,
bottom: i16,
right: i16,
value: i16,
min: i16,
max: i16,
hilite: u8,
) {
let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
self.get_screen_params();
let is_vertical = (bottom - top) > (right - left);
let is_inactive = hilite == 255 || min >= max;
self.draw_rect_border(bus, top, left, bottom, right);
if is_inactive {
Self::fb_fill_rect(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
top + 1,
left + 1,
bottom - 1,
right - 1,
false,
);
if is_vertical {
let arrow_h = 16i16.min(bottom - top);
self.draw_hline(bus, top + arrow_h - 1, left, right);
self.draw_hline(bus, bottom - arrow_h, left, right);
self.draw_scroll_arrow_inactive(bus, top + 1, left + 1, 0); self.draw_scroll_arrow_inactive(bus, bottom - arrow_h + 1, left + 1, 1);
} else {
let arrow_w = 16i16.min(right - left);
self.draw_vline(bus, left + arrow_w - 1, top, bottom);
self.draw_vline(bus, right - arrow_w, top, bottom);
self.draw_scroll_arrow_inactive(bus, top + 1, left + 1, 2); self.draw_scroll_arrow_inactive(bus, top + 1, right - arrow_w + 1, 3);
}
return;
}
if is_vertical {
let arrow_h = 16i16.min(bottom - top);
self.draw_rect_border(bus, top, left, top + arrow_h, right);
Self::fb_fill_rect(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
top + 1,
left + 1,
top + arrow_h - 1,
right - 1,
false,
);
Self::fb_fill_rect(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
top + 1,
right - 2,
top + arrow_h - 1,
right - 1,
true,
);
Self::fb_fill_rect(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
top + arrow_h - 2,
left + 1,
top + arrow_h - 1,
right - 1,
true,
);
self.draw_scroll_arrow(bus, top + 1, left + 1, 0);
let da_top = bottom - arrow_h;
self.draw_rect_border(bus, da_top, left, bottom, right);
Self::fb_fill_rect(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
da_top + 1,
left + 1,
bottom - 1,
right - 1,
false,
);
Self::fb_fill_rect(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
da_top + 1,
right - 2,
bottom - 1,
right - 1,
true,
);
Self::fb_fill_rect(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
bottom - 2,
left + 1,
bottom - 1,
right - 1,
true,
);
self.draw_scroll_arrow(bus, da_top + 1, left + 1, 1);
let track_top = top + arrow_h;
let track_bottom = bottom - arrow_h;
if track_top < track_bottom {
self.fill_scroll_track(bus, track_top, left + 1, track_bottom, right - 1);
let track_len = track_bottom - track_top;
let thumb_h = 16i16.min(track_len);
let range = (max - min).max(1) as i32;
let val_clamped = (value - min).max(0).min(max - min) as i32;
let travel = (track_len - thumb_h) as i32;
let thumb_top = track_top + ((val_clamped * travel) / range) as i16;
let thumb_bottom = thumb_top + thumb_h;
Self::fb_fill_rect(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
thumb_top,
left + 1,
thumb_bottom,
right - 1,
false,
);
self.draw_rect_border(bus, thumb_top, left, thumb_bottom, right);
Self::fb_fill_rect(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
thumb_top + 1,
right - 2,
thumb_bottom - 1,
right - 1,
true,
);
self.draw_thumb_grip_lines_v(bus, thumb_top, left, right);
}
} else {
let arrow_w = 16i16.min(right - left);
self.draw_rect_border(bus, top, left, bottom, left + arrow_w);
Self::fb_fill_rect(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
top + 1,
left + 1,
bottom - 1,
left + arrow_w - 1,
false,
);
Self::fb_fill_rect(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
top + 1,
left + arrow_w - 2,
bottom - 1,
left + arrow_w - 1,
true,
);
Self::fb_fill_rect(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
bottom - 2,
left + 1,
bottom - 1,
left + arrow_w - 1,
true,
);
self.draw_scroll_arrow(bus, top + 1, left + 1, 2);
let ra_left = right - arrow_w;
self.draw_rect_border(bus, top, ra_left, bottom, right);
Self::fb_fill_rect(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
top + 1,
ra_left + 1,
bottom - 1,
right - 1,
false,
);
Self::fb_fill_rect(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
top + 1,
right - 2,
bottom - 1,
right - 1,
true,
);
Self::fb_fill_rect(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
bottom - 2,
ra_left + 1,
bottom - 1,
right - 1,
true,
);
self.draw_scroll_arrow(bus, top + 1, ra_left + 1, 3);
let track_left = left + arrow_w;
let track_right = right - arrow_w;
if track_left < track_right {
self.fill_scroll_track(bus, top + 1, track_left, bottom - 1, track_right);
let track_len = track_right - track_left;
let thumb_w = 16i16.min(track_len);
let range = (max - min).max(1) as i32;
let val_clamped = (value - min).max(0).min(max - min) as i32;
let travel = (track_len - thumb_w) as i32;
let thumb_left = track_left + ((val_clamped * travel) / range) as i16;
let thumb_right = thumb_left + thumb_w;
Self::fb_fill_rect(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
top + 1,
thumb_left,
bottom - 1,
thumb_right,
false,
);
self.draw_rect_border(bus, top, thumb_left, bottom, thumb_right);
Self::fb_fill_rect(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
bottom - 2,
thumb_left + 1,
bottom - 1,
thumb_right - 1,
true,
);
self.draw_thumb_grip_lines_h(bus, thumb_left, top, bottom);
}
}
}
fn fill_scroll_track(
&self,
bus: &mut MacMemoryBus,
top: i16,
left: i16,
bottom: i16,
right: i16,
) {
const LT_GRAY: [u8; 8] = [0x88, 0x22, 0x88, 0x22, 0x88, 0x22, 0x88, 0x22];
let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
self.get_screen_params();
for y in top..bottom {
if y < 0 || y >= screen_height {
continue;
}
let pat_row = LT_GRAY[(y as u16 % 8) as usize];
for x in left..right {
if x < 0 || x >= screen_width {
continue;
}
let bit = 7 - (x as u16 % 8);
let on = (pat_row >> bit) & 1 != 0;
Self::fb_set_pixel(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
x,
y,
on,
);
}
}
}
fn draw_scroll_arrow(
&self,
bus: &mut MacMemoryBus,
content_top: i16,
content_left: i16,
direction: u8,
) {
const UP_ARROW: [(i16, &[i16]); 10] = [
(2, &[6]), (3, &[5, 7]), (4, &[4, 8]),
(5, &[3, 9]),
(6, &[2, 10]),
(7, &[1, 2, 3, 4, 8, 9, 10, 11]), (8, &[4, 8]), (9, &[4, 8]),
(10, &[4, 8]),
(11, &[4, 5, 6, 7, 8]), ];
let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
self.get_screen_params();
for &(row, cols) in &UP_ARROW {
for &col in cols {
let (py, px) = match direction {
0 => (row, col), 1 => (13 - row, col), 2 => (12 - col, row), 3 => (col, 13 - row), _ => continue,
};
let sy = content_top + py;
let sx = content_left + px;
Self::fb_set_pixel(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
sx,
sy,
true,
);
}
}
}
fn draw_thumb_grip_lines_v(
&self,
bus: &mut MacMemoryBus,
thumb_top: i16,
box_left: i16,
_box_right: i16,
) {
let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
self.get_screen_params();
let grip_left = box_left + 5;
let grip_right = box_left + 11; for &offset in &[5i16, 7, 9, 11] {
let y = thumb_top + offset;
for x in grip_left..grip_right {
Self::fb_set_pixel(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
x,
y,
true,
);
}
}
}
fn draw_thumb_grip_lines_h(
&self,
bus: &mut MacMemoryBus,
thumb_left: i16,
box_top: i16,
_box_bottom: i16,
) {
let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
self.get_screen_params();
let grip_top = box_top + 5;
let grip_bottom = box_top + 11; for &offset in &[5i16, 7, 9, 11] {
let x = thumb_left + offset;
for y in grip_top..grip_bottom {
Self::fb_set_pixel(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
x,
y,
true,
);
}
}
}
fn draw_hline(&self, bus: &mut MacMemoryBus, y: i16, left: i16, right: i16) {
let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
self.get_screen_params();
for x in left..right {
Self::fb_set_pixel(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
x,
y,
true,
);
}
}
fn draw_vline(&self, bus: &mut MacMemoryBus, x: i16, top: i16, bottom: i16) {
let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
self.get_screen_params();
for y in top..bottom {
Self::fb_set_pixel(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
x,
y,
true,
);
}
}
fn draw_scroll_arrow_inactive(
&self,
bus: &mut MacMemoryBus,
content_top: i16,
content_left: i16,
direction: u8,
) {
const UP_ARROW_14: [(i16, &[i16]); 10] = [
(2, &[6, 7]), (3, &[5, 8]),
(4, &[4, 9]),
(5, &[3, 10]),
(6, &[2, 11]),
(7, &[1, 2, 3, 4, 9, 10, 11, 12]), (8, &[4, 9]), (9, &[4, 9]),
(10, &[4, 9]),
(11, &[4, 5, 6, 7, 8, 9]), ];
const LEFT_ARROW_14: [(i16, &[i16]); 12] = [
(1, &[6]),
(2, &[5, 6]),
(3, &[4, 6]),
(4, &[3, 6, 7, 8, 9, 10]), (5, &[2, 10]),
(6, &[1, 10]), (7, &[1, 10]),
(8, &[2, 10]),
(9, &[3, 6, 7, 8, 9, 10]), (10, &[4, 6]),
(11, &[5, 6]),
(12, &[6]),
];
const RIGHT_ARROW_14: [(i16, &[i16]); 12] = [
(1, &[7]),
(2, &[7, 8]),
(3, &[7, 9]),
(4, &[3, 4, 5, 6, 7, 10]),
(5, &[3, 11]),
(6, &[3, 12]), (7, &[3, 12]),
(8, &[3, 11]),
(9, &[3, 4, 5, 6, 7, 10]),
(10, &[7, 9]),
(11, &[7, 8]),
(12, &[7]),
];
let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
self.get_screen_params();
match direction {
0 | 1 => {
for &(row, cols) in &UP_ARROW_14 {
for &col in cols {
let (py, px) = if direction == 0 {
(row, col)
} else {
(13 - row, col)
};
let sy = content_top + py;
let sx = content_left + px;
Self::fb_set_pixel(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
sx,
sy,
true,
);
}
}
}
2 => {
for &(row, cols) in &LEFT_ARROW_14 {
for &col in cols {
let sy = content_top + row;
let sx = content_left + col;
Self::fb_set_pixel(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
sx,
sy,
true,
);
}
}
}
3 => {
for &(row, cols) in &RIGHT_ARROW_14 {
for &col in cols {
let sy = content_top + row;
let sx = content_left + col;
Self::fb_set_pixel(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
sx,
sy,
true,
);
}
}
}
_ => {}
}
}
pub(crate) fn dispatch_control<C: CpuOps>(
&mut self,
is_tool: bool,
trap_num: u16,
cpu: &mut C,
bus: &mut MacMemoryBus,
) -> Option<Result<()>> {
Some(match (is_tool, trap_num) {
(true, 0x154) => {
let sp = cpu.read_reg(Register::A7);
let window_ptr = bus.read_long(sp + 22);
let bounds_ptr = bus.read_long(sp + 18);
let title_ptr = bus.read_long(sp + 14);
let visible = bus.read_byte(sp + 12) != 0;
let value = bus.read_word(sp + 10) as i16;
let min = bus.read_word(sp + 8) as i16;
let max = bus.read_word(sp + 6) as i16;
let proc_id = bus.read_word(sp + 4) as i16;
let ref_con = bus.read_long(sp);
let bounds = if bounds_ptr != 0 {
(
bus.read_word(bounds_ptr) as i16,
bus.read_word(bounds_ptr + 2) as i16,
bus.read_word(bounds_ptr + 4) as i16,
bus.read_word(bounds_ptr + 6) as i16,
)
} else {
(0, 0, 0, 0)
};
let title = Self::read_pascal_string(bus, title_ptr);
let ctrl_ptr = bus.alloc(296);
let handle = bus.alloc(4);
bus.write_long(handle, ctrl_ptr);
self.initialize_control_record(
bus, ctrl_ptr, window_ptr, bounds, &title, visible, value, min, max, proc_id,
ref_con,
);
self.ensure_control_aux_record(bus, handle);
if window_ptr != 0 {
let old_head = bus.read_long(window_ptr + 140);
bus.write_long(ctrl_ptr, old_head);
bus.write_long(window_ptr + 140, handle);
}
if visible {
self.draw_control(cpu, bus, ctrl_ptr);
}
bus.write_long(sp + 26, handle);
cpu.write_reg(Register::A7, sp + 26);
Ok(())
}
(true, 0x155) => {
let sp = cpu.read_reg(Register::A7);
let ctrl_handle = bus.read_long(sp);
cpu.write_reg(Register::A7, sp + 4);
if ctrl_handle != 0 {
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);
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);
}
Ok(())
}
(true, 0x156) => {
let sp = cpu.read_reg(Register::A7);
let window_ptr = bus.read_long(sp);
cpu.write_reg(Register::A7, sp + 4);
if window_ptr != 0 {
let mut ctrl_handle = bus.read_long(window_ptr + 140);
while ctrl_handle != 0 {
let ctrl_ptr = bus.read_long(ctrl_handle);
if ctrl_ptr == 0 {
bus.free(ctrl_handle);
break;
}
let next = bus.read_long(ctrl_ptr);
self.release_control_aux_record(bus, ctrl_handle);
self.control_proc_ids.remove(&ctrl_ptr);
bus.free(ctrl_ptr);
bus.free(ctrl_handle);
ctrl_handle = next;
}
bus.write_long(window_ptr + 140, 0);
}
Ok(())
}
(true, 0x157) => {
let sp = cpu.read_reg(Register::A7);
let ctrl_handle = bus.read_long(sp);
if ctrl_handle != 0 {
let ctrl_ptr = bus.read_long(ctrl_handle);
if ctrl_ptr != 0 {
bus.write_byte(ctrl_ptr + 16, 255); }
}
cpu.write_reg(Register::A7, sp + 4);
Ok(())
}
(true, 0x158) => {
let sp = cpu.read_reg(Register::A7);
let ctrl_handle = bus.read_long(sp);
if ctrl_handle != 0 {
let ctrl_ptr = bus.read_long(ctrl_handle);
if ctrl_ptr != 0 {
bus.write_byte(ctrl_ptr + 16, 0); }
}
cpu.write_reg(Register::A7, sp + 4);
Ok(())
}
(true, 0x159) => {
let sp = cpu.read_reg(Register::A7);
let v = bus.read_word(sp) as i16;
let h = bus.read_word(sp + 2) as i16;
let ctrl_handle = bus.read_long(sp + 4);
cpu.write_reg(Register::A7, sp + 8);
if ctrl_handle != 0 {
let ctrl_ptr = bus.read_long(ctrl_handle);
if ctrl_ptr != 0 {
let old_top = bus.read_word(ctrl_ptr + 8) as i16;
let old_left = bus.read_word(ctrl_ptr + 10) as i16;
let old_bottom = bus.read_word(ctrl_ptr + 12) as i16;
let old_right = bus.read_word(ctrl_ptr + 14) as i16;
let width = old_right - old_left;
let height = old_bottom - old_top;
let owner_window = bus.read_long(ctrl_ptr + 4);
if owner_window != 0 {
let (scr_top, scr_left, _, _) =
Self::dialog_screen_bounds(bus, owner_window);
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,
scr_top + old_top,
scr_left + old_left,
scr_top + old_bottom,
scr_left + old_right,
false, );
}
bus.write_word(ctrl_ptr + 8, v as u16);
bus.write_word(ctrl_ptr + 10, h as u16);
bus.write_word(ctrl_ptr + 12, (v + height) as u16);
bus.write_word(ctrl_ptr + 14, (h + width) as u16);
self.sync_dialog_item_rect_for_control(bus, ctrl_handle);
}
}
Ok(())
}
(true, 0x15A) => {
let sp = cpu.read_reg(Register::A7);
let ctrl_handle = bus.read_long(sp);
let ref_con = Self::control_record_ptr(bus, ctrl_handle)
.checked_add(36)
.map(|addr| bus.read_long(addr))
.unwrap_or(0);
bus.write_long(sp + 4, ref_con);
cpu.write_reg(Register::A7, sp + 4);
Ok(())
}
(true, 0x15B) => {
let sp = cpu.read_reg(Register::A7);
let ref_con = bus.read_long(sp);
let ctrl_handle = bus.read_long(sp + 4);
let ctrl_ptr = Self::control_record_ptr(bus, ctrl_handle);
if ctrl_ptr != 0 {
bus.write_long(ctrl_ptr + 36, ref_con);
}
cpu.write_reg(Register::A7, sp + 8);
Ok(())
}
(true, 0x15C) => {
let sp = cpu.read_reg(Register::A7);
let height = bus.read_word(sp) as i16;
let width = bus.read_word(sp + 2) as i16;
let ctrl_handle = bus.read_long(sp + 4);
let ctrl_ptr = Self::control_record_ptr(bus, ctrl_handle);
if ctrl_ptr != 0 {
let top = bus.read_word(ctrl_ptr + 8) as i16;
let left = bus.read_word(ctrl_ptr + 10) as i16;
bus.write_word(ctrl_ptr + 12, top.wrapping_add(height) as u16);
bus.write_word(ctrl_ptr + 14, left.wrapping_add(width) as u16);
self.sync_dialog_item_rect_for_control(bus, ctrl_handle);
}
cpu.write_reg(Register::A7, sp + 8);
Ok(())
}
(true, 0x15D) => {
let sp = cpu.read_reg(Register::A7);
let hilite_state = bus.read_word(sp) as u8;
let ctrl_handle = bus.read_long(sp + 2);
let ctrl_ptr = Self::control_record_ptr(bus, ctrl_handle);
if ctrl_ptr != 0 {
bus.write_byte(ctrl_ptr + 17, hilite_state);
self.draw_control(cpu, bus, ctrl_ptr);
}
cpu.write_reg(Register::A7, sp + 6);
Ok(())
}
(true, 0x15E) => {
let sp = cpu.read_reg(Register::A7);
let title_ptr = bus.read_long(sp);
let ctrl_handle = bus.read_long(sp + 4);
let ctrl_ptr = Self::control_record_ptr(bus, ctrl_handle);
let title = if ctrl_ptr != 0 {
Self::control_title_bytes(bus, ctrl_ptr)
} else {
Vec::new()
};
Self::write_pascal_string(bus, title_ptr, &title);
cpu.write_reg(Register::A7, sp + 8);
Ok(())
}
(true, 0x15F) => {
let sp = cpu.read_reg(Register::A7);
let title_ptr = bus.read_long(sp);
let ctrl_handle = bus.read_long(sp + 4);
cpu.write_reg(Register::A7, sp + 8);
let ctrl_ptr = Self::control_record_ptr(bus, ctrl_handle);
if ctrl_ptr != 0 {
let title = Self::read_pascal_string(bus, title_ptr);
Self::set_control_title_bytes(bus, ctrl_ptr, &title);
self.draw_control(cpu, bus, ctrl_ptr);
}
Ok(())
}
(true, 0x160) => {
let sp = cpu.read_reg(Register::A7);
let ctrl_handle = bus.read_long(sp);
let value = if let Some((dlg_ptr, item_no)) =
self.dialog_control_handles.get(&ctrl_handle).copied()
{
self.dialog_control_values
.get(&(dlg_ptr, item_no))
.copied()
.unwrap_or(0)
} else if ctrl_handle != 0 {
let ctrl_ptr = bus.read_long(ctrl_handle);
if ctrl_ptr != 0 {
bus.read_word(ctrl_ptr + 18) as i16
} else {
0
}
} else {
0
};
bus.write_word(sp + 4, value as u16);
cpu.write_reg(Register::A7, sp + 4);
Ok(())
}
(true, 0x163) => {
let sp = cpu.read_reg(Register::A7);
let requested = bus.read_word(sp) as i16;
let ctrl_handle = bus.read_long(sp + 2);
cpu.write_reg(Register::A7, sp + 6);
if ctrl_handle == 0 {
return Some(Ok(()));
}
let ctrl_ptr = bus.read_long(ctrl_handle);
if ctrl_ptr == 0 {
return Some(Ok(()));
}
let min = bus.read_word(ctrl_ptr + 20) as i16;
let max = bus.read_word(ctrl_ptr + 22) as i16;
let proc_id = self.control_proc_ids.get(&ctrl_ptr).copied().unwrap_or(0);
let value = if Self::is_popup_menu_proc_id(proc_id) {
requested
} else if min <= max {
requested.clamp(min, max)
} else {
requested
};
if let Some((dlg_ptr, item_no)) =
self.dialog_control_handles.get(&ctrl_handle).copied()
{
self.dialog_control_values.insert((dlg_ptr, item_no), value);
}
bus.write_word(ctrl_ptr + 18, value as u16);
self.draw_control(cpu, bus, ctrl_ptr);
Ok(())
}
(true, 0x161) => {
let sp = cpu.read_reg(Register::A7);
let ctrl_handle = bus.read_long(sp);
let min = Self::control_record_ptr(bus, ctrl_handle)
.checked_add(20)
.map(|addr| bus.read_word(addr))
.unwrap_or(0);
bus.write_word(sp + 4, min);
cpu.write_reg(Register::A7, sp + 4);
Ok(())
}
(true, 0x164) => {
let sp = cpu.read_reg(Register::A7);
let min = bus.read_word(sp) as i16;
let ctrl_handle = bus.read_long(sp + 2);
cpu.write_reg(Register::A7, sp + 6);
let ctrl_ptr = Self::control_record_ptr(bus, ctrl_handle);
if ctrl_ptr != 0 {
bus.write_word(ctrl_ptr + 20, min as u16);
let proc_id = self.control_proc_ids.get(&ctrl_ptr).copied().unwrap_or(0);
let value = bus.read_word(ctrl_ptr + 18) as i16;
if !Self::is_popup_menu_proc_id(proc_id) && value < min {
bus.write_word(ctrl_ptr + 18, min as u16);
if let Some((dlg_ptr, item_no)) =
self.dialog_control_handles.get(&ctrl_handle).copied()
{
self.dialog_control_values.insert((dlg_ptr, item_no), min);
}
self.draw_control(cpu, bus, ctrl_ptr);
}
}
Ok(())
}
(true, 0x162) => {
let sp = cpu.read_reg(Register::A7);
let ctrl_handle = bus.read_long(sp);
let max = Self::control_record_ptr(bus, ctrl_handle)
.checked_add(22)
.map(|addr| bus.read_word(addr))
.unwrap_or(0);
bus.write_word(sp + 4, max);
cpu.write_reg(Register::A7, sp + 4);
Ok(())
}
(true, 0x165) => {
let sp = cpu.read_reg(Register::A7);
let max = bus.read_word(sp) as i16;
let ctrl_handle = bus.read_long(sp + 2);
cpu.write_reg(Register::A7, sp + 6);
let ctrl_ptr = Self::control_record_ptr(bus, ctrl_handle);
if ctrl_ptr != 0 {
bus.write_word(ctrl_ptr + 22, max as u16);
let proc_id = self.control_proc_ids.get(&ctrl_ptr).copied().unwrap_or(0);
let value = bus.read_word(ctrl_ptr + 18) as i16;
if !Self::is_popup_menu_proc_id(proc_id) && value > max {
bus.write_word(ctrl_ptr + 18, max as u16);
if let Some((dlg_ptr, item_no)) =
self.dialog_control_handles.get(&ctrl_handle).copied()
{
self.dialog_control_values.insert((dlg_ptr, item_no), max);
}
self.draw_control(cpu, bus, ctrl_ptr);
}
}
Ok(())
}
(true, 0x166) => {
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 ctrl_handle = bus.read_long(sp + 4);
let ctrl_ptr = Self::control_record_ptr(bus, ctrl_handle);
let mut part: u16 = 0;
if ctrl_ptr != 0 {
let vis = bus.read_byte(ctrl_ptr + 16);
let hilite = bus.read_byte(ctrl_ptr + 17);
if vis == 255 && hilite != 255 {
let r_top = bus.read_word(ctrl_ptr + 8) as i16;
let r_left = bus.read_word(ctrl_ptr + 10) as i16;
let r_bottom = bus.read_word(ctrl_ptr + 12) as i16;
let r_right = bus.read_word(ctrl_ptr + 14) as i16;
if pt_v >= r_top && pt_v < r_bottom && pt_h >= r_left && pt_h < r_right {
part = match self.control_proc_ids.get(&ctrl_ptr).copied().unwrap_or(0)
{
16 => self.standard_scrollbar_testcontrol_part_code(
bus, ctrl_ptr, pt_v, pt_h,
),
_ => self.standard_testcontrol_part_code(ctrl_ptr),
};
}
}
}
bus.write_word(sp + 8, part);
cpu.write_reg(Register::A7, sp + 8);
Ok(())
}
(true, 0x168) => {
if self.control_tracking.is_some() {
if self.mouse_button {
let (mv, mh) = self.mouse_pos;
let new_item = self.control_tracking_item_at_point(mh, mv);
let old_item = self
.control_tracking
.as_ref()
.map(|tracking| tracking.highlighted_item)
.unwrap_or(0);
if new_item != old_item {
if old_item > 0 {
self.invert_control_tracking_item(bus, old_item);
}
if let Some(tracking) = self.control_tracking.as_mut() {
tracking.highlighted_item = new_item;
}
if new_item > 0 {
self.invert_control_tracking_item(bus, new_item);
}
}
} else {
let selected_item = self
.control_tracking
.as_ref()
.map(|tracking| tracking.highlighted_item)
.unwrap_or(0);
self.finish_popup_control_tracking(cpu, bus, selected_item);
}
return Some(Ok(()));
}
let sp = cpu.read_reg(Register::A7);
let ctrl_handle = bus.read_long(sp + 8);
let pt_v = bus.read_word(sp + 4) as i16;
let pt_h = bus.read_word(sp + 6) as i16;
let mut part: u16 = 0;
if ctrl_handle != 0 {
let ctrl_ptr = bus.read_long(ctrl_handle);
if ctrl_ptr != 0 {
let vis = bus.read_byte(ctrl_ptr + 16);
let hilite = bus.read_byte(ctrl_ptr + 17);
if vis == 255 && hilite != 255 {
let r_top = bus.read_word(ctrl_ptr + 8) as i16;
let r_left = bus.read_word(ctrl_ptr + 10) as i16;
let r_bottom = bus.read_word(ctrl_ptr + 12) as i16;
let r_right = bus.read_word(ctrl_ptr + 14) as i16;
if pt_v >= r_top && pt_v < r_bottom && pt_h >= r_left && pt_h < r_right
{
let proc_id =
self.control_proc_ids.get(&ctrl_ptr).copied().unwrap_or(0);
if Self::is_popup_menu_proc_id(proc_id) {
let menu_id = bus.read_word(ctrl_ptr + 20) as i16;
if let Some(menu_idx) =
self.menus.iter().position(|menu| menu.id == menu_id)
{
let dropdown_rect = self
.popup_control_dropdown_rect(bus, ctrl_ptr, menu_idx);
let saved = self.save_dropdown_pixels(bus, dropdown_rect);
self.draw_menu_dropdown(bus, menu_idx, dropdown_rect);
self.control_tracking = Some(ControlTrackingState {
ctrl_handle,
ctrl_ptr,
active_menu: menu_idx,
highlighted_item: 0,
saved_pixels: saved,
dropdown_rect,
stack_ptr: sp,
});
return Some(Ok(()));
}
}
part = 10;
}
}
}
}
bus.write_word(sp + 12, part);
cpu.write_reg(Register::A7, sp + 12);
Ok(())
}
(true, 0x169) => {
let sp = cpu.read_reg(Register::A7);
let window_ptr = bus.read_long(sp);
if window_ptr != 0 {
let mut ctrl_handle = bus.read_long(window_ptr + 140);
let mut ctrl_ptrs: Vec<u32> = Vec::new();
while ctrl_handle != 0 {
let ctrl_ptr = bus.read_long(ctrl_handle);
if ctrl_ptr == 0 {
break;
}
ctrl_ptrs.push(ctrl_ptr);
ctrl_handle = bus.read_long(ctrl_ptr);
}
for &ctrl_ptr in ctrl_ptrs.iter().rev() {
let proc_id = self.control_proc_ids.get(&ctrl_ptr).copied().unwrap_or(0);
if Self::is_popup_menu_proc_id(proc_id) {
let vis = bus.read_byte(ctrl_ptr + 16);
let hilite = bus.read_byte(ctrl_ptr + 17);
if vis == 255 && hilite != 255 {
let r_top = bus.read_word(ctrl_ptr + 8) as i16;
let r_left = bus.read_word(ctrl_ptr + 10) as i16;
let r_bottom = bus.read_word(ctrl_ptr + 12) as i16;
let r_right = bus.read_word(ctrl_ptr + 14) as i16;
let ctrl_value = bus.read_word(ctrl_ptr + 18) as i16;
let menu_id = bus.read_word(ctrl_ptr + 20) as i16;
let (scr_top, scr_left, _, _) =
Self::dialog_screen_bounds(bus, window_ptr);
let abs_top = scr_top + r_top;
let abs_left = scr_left + r_left;
let abs_bottom = scr_top + r_bottom;
let abs_right = scr_left + r_right;
let selected = ctrl_value.max(1) as usize;
let item_title = self.popup_menu_item_title(bus, menu_id, selected);
self.draw_popup_control(
bus,
abs_top,
abs_left,
abs_bottom,
abs_right,
&item_title.unwrap_or_default(),
);
}
} else {
self.draw_control(cpu, bus, ctrl_ptr);
}
}
}
cpu.write_reg(Register::A7, sp + 4);
Ok(())
}
(true, 0x153) => {
let sp = cpu.read_reg(Register::A7);
let update_rgn = bus.read_long(sp);
let window_ptr = bus.read_long(sp + 4);
cpu.write_reg(Register::A7, sp + 8);
if trace_controls_enabled() {
eprintln!(
"[CONTROL] UpdtControl window=${:08X} updateRgn=${:08X}",
window_ptr, update_rgn
);
}
let window_limit = bus.ram_size().saturating_sub(140);
if window_ptr == 0 || window_ptr > window_limit {
return Some(Ok(()));
}
if update_rgn == 0 {
return Some(Ok(()));
}
let update_rect = match Self::region_handle_rect(bus, update_rgn) {
Some(rect) => rect,
None => return Some(Ok(())),
};
let mut ctrl_handle = bus.read_long(window_ptr + 140);
let mut ctrl_ptrs: Vec<u32> = Vec::new();
while ctrl_handle != 0 {
let ctrl_ptr = bus.read_long(ctrl_handle);
if ctrl_ptr == 0 {
break;
}
ctrl_ptrs.push(ctrl_ptr);
ctrl_handle = bus.read_long(ctrl_ptr);
}
let (ut, ul, ub, ur) = update_rect;
for ctrl_ptr in ctrl_ptrs.iter().rev() {
let ctrl_ptr = *ctrl_ptr;
let ct = bus.read_word(ctrl_ptr + 8) as i16;
let cl = bus.read_word(ctrl_ptr + 10) as i16;
let cb = bus.read_word(ctrl_ptr + 12) as i16;
let cr = bus.read_word(ctrl_ptr + 14) as i16;
if cb <= ut || cr <= ul || ct >= ub || cl >= ur {
continue;
}
self.draw_control(cpu, bus, ctrl_ptr);
}
Ok(())
}
(true, 0x16C) => {
let sp = cpu.read_reg(Register::A7);
let which_ctrl_ptr = bus.read_long(sp);
let window_ptr = bus.read_long(sp + 4);
let pt_v = bus.read_word(sp + 8) as i16;
let pt_h = bus.read_word(sp + 10) as i16;
let mut found_handle: u32 = 0;
let mut found_part: u16 = 0;
if window_ptr != 0 {
let mut ctrl_handle = bus.read_long(window_ptr + 140);
while ctrl_handle != 0 {
let ctrl_ptr = bus.read_long(ctrl_handle);
if ctrl_ptr == 0 {
break;
}
let vis = bus.read_byte(ctrl_ptr + 16);
let hilite = bus.read_byte(ctrl_ptr + 17);
if vis == 255 && hilite != 255 {
let r_top = bus.read_word(ctrl_ptr + 8) as i16;
let r_left = bus.read_word(ctrl_ptr + 10) as i16;
let r_bottom = bus.read_word(ctrl_ptr + 12) as i16;
let r_right = bus.read_word(ctrl_ptr + 14) as i16;
if pt_v >= r_top && pt_v < r_bottom && pt_h >= r_left && pt_h < r_right
{
found_handle = ctrl_handle;
found_part = 10;
break;
}
}
ctrl_handle = bus.read_long(ctrl_ptr);
}
}
if which_ctrl_ptr != 0 {
bus.write_long(which_ctrl_ptr, found_handle);
}
bus.write_word(sp + 12, found_part);
cpu.write_reg(Register::A7, sp + 12);
Ok(())
}
(true, 0x1BE) => {
let sp = cpu.read_reg(Register::A7);
let window_ptr = bus.read_long(sp);
let ctrl_id = bus.read_word(sp + 4) as i16;
let cntl_data = self
.find_resource_any(*b"CNTL", ctrl_id)
.map(|(_, ptr)| ptr);
let Some(rsrc_addr) = cntl_data else {
bus.write_long(sp + 6, 0);
cpu.write_reg(Register::A7, sp + 6);
return Some(Ok(()));
};
let ctrl_ptr = bus.alloc(296);
let handle = bus.alloc(4);
bus.write_long(handle, ctrl_ptr);
let r_top = bus.read_word(rsrc_addr);
let r_left = bus.read_word(rsrc_addr + 2);
let r_bottom = bus.read_word(rsrc_addr + 4);
let r_right = bus.read_word(rsrc_addr + 6);
let value = bus.read_word(rsrc_addr + 8);
let vis_word = bus.read_word(rsrc_addr + 10);
let visibility: u8 = if vis_word != 0 { 255 } else { 0 };
let max = bus.read_word(rsrc_addr + 12);
let min = bus.read_word(rsrc_addr + 14);
let proc_id = bus.read_word(rsrc_addr + 16) as i16;
let ref_con = bus.read_long(rsrc_addr + 18);
let title = Self::read_pascal_string(bus, rsrc_addr + 22);
self.initialize_control_record(
bus,
ctrl_ptr,
window_ptr,
(r_top as i16, r_left as i16, r_bottom as i16, r_right as i16),
&title,
visibility == 255,
value as i16,
min as i16,
max as i16,
proc_id,
ref_con,
);
self.ensure_control_aux_record(bus, handle);
if trace_controls_enabled() {
eprintln!(
"[CONTROL] GetNewControl id={} proc_id={} cdef_id={} title=\"{}\" rect=({},{}..{},{} ) window=${:08X}",
ctrl_id,
proc_id,
proc_id >> 4,
String::from_utf8_lossy(&title),
r_top as i16,
r_left as i16,
r_bottom as i16,
r_right as i16,
window_ptr
);
}
if window_ptr != 0 {
let old_head = bus.read_long(window_ptr + 140);
bus.write_long(ctrl_ptr, old_head); bus.write_long(window_ptr + 140, handle); }
bus.write_long(sp + 6, handle);
cpu.write_reg(Register::A7, sp + 6);
Ok(())
}
(true, 0x16B) => {
let sp = cpu.read_reg(Register::A7);
let action_proc = bus.read_long(sp);
let ctrl_handle = bus.read_long(sp + 4);
let ctrl_ptr = Self::control_record_ptr(bus, ctrl_handle);
if ctrl_ptr != 0 {
bus.write_long(ctrl_ptr + 32, action_proc);
}
cpu.write_reg(Register::A7, sp + 8);
Ok(())
}
(true, 0x16A) => {
let sp = cpu.read_reg(Register::A7);
let ctrl_handle = bus.read_long(sp);
let action_proc = Self::control_record_ptr(bus, ctrl_handle)
.checked_add(32)
.map(|addr| bus.read_long(addr))
.unwrap_or(0);
bus.write_long(sp + 4, action_proc);
cpu.write_reg(Register::A7, sp + 4);
Ok(())
}
(true, 0x167) => {
let sp = cpu.read_reg(Register::A7);
cpu.write_reg(Register::A7, sp + 18);
Ok(())
}
(true, 0x243) => {
let sp = cpu.read_reg(Register::A7);
let color_table = bus.read_long(sp);
let ctrl_handle = bus.read_long(sp + 4);
if ctrl_handle != 0 {
let default_ctab = self.default_control_color_table_handle(bus);
let effective_ctab = if color_table != 0 {
color_table
} else {
default_ctab
};
let aux_handle = self.ensure_control_aux_record(bus, ctrl_handle);
let aux_ptr = bus.read_long(aux_handle);
if aux_ptr != 0 {
bus.write_long(aux_ptr + Self::AUX_CTL_OWNER_OFFSET, ctrl_handle);
bus.write_long(aux_ptr + Self::AUX_CTL_CTABLE_OFFSET, effective_ctab);
bus.write_word(aux_ptr + Self::AUX_CTL_FLAGS_OFFSET, 0);
bus.write_long(
aux_ptr + Self::AUX_CTL_RESERVED_OFFSET,
self.control_aux_reserved_value(bus, ctrl_handle),
);
}
let ctrl_ptr = Self::control_record_ptr(bus, ctrl_handle);
if ctrl_ptr != 0 && bus.read_byte(ctrl_ptr + 16) == 255 {
self.draw_control(cpu, bus, ctrl_ptr);
}
}
cpu.write_reg(Register::A7, sp + 8);
Ok(())
}
(true, 0x16D) => {
let sp = cpu.read_reg(Register::A7);
let ctrl_handle = bus.read_long(sp);
cpu.write_reg(Register::A7, sp + 4);
let saved_gdevice = self.current_gdevice;
let ram_limit = bus.ram_size().saturating_sub(4);
if ctrl_handle != 0 && ctrl_handle <= ram_limit {
let ctrl_ptr = bus.read_long(ctrl_handle);
let title_off = ctrl_ptr.saturating_add(40);
if ctrl_ptr != 0 && title_off < bus.ram_size() && title_off <= ram_limit {
let title_len = bus.read_byte(title_off) as u32;
let title_end = title_off.saturating_add(1).saturating_add(title_len);
if title_end <= bus.ram_size() {
self.draw_control(cpu, bus, ctrl_ptr);
debug_assert_eq!(self.current_gdevice, saved_gdevice);
}
}
}
Ok(())
}
(true, 0x009) => {
let sp = cpu.read_reg(Register::A7);
let ctrl_handle = bus.read_long(sp);
let variant: i16 = if ctrl_handle != 0 {
let ctrl_ptr = bus.read_long(ctrl_handle);
let proc_id = self.control_proc_ids.get(&ctrl_ptr).copied().unwrap_or(0);
proc_id & 0xF
} else {
0
};
cpu.write_reg(Register::A7, sp + 4);
bus.write_word(sp + 4, variant as u16);
Ok(())
}
_ => return None,
})
}
}
#[cfg(test)]
mod tests {
use super::super::test_helpers::{setup, setup_with_port};
use crate::cpu::{CpuOps, Register};
use crate::memory::{MacMemoryBus, MemoryBus};
use crate::trap::menu::{Menu, MenuItem};
use crate::trap::TrapDispatcher;
fn build_cntl_resource(
rect: (i16, i16, i16, i16),
value: i16,
visible: bool,
min: i16,
max: i16,
proc_id: i16,
ref_con: u32,
title: &[u8],
) -> Vec<u8> {
let mut data = Vec::with_capacity(23 + title.len());
data.extend_from_slice(&(rect.0 as u16).to_be_bytes());
data.extend_from_slice(&(rect.1 as u16).to_be_bytes());
data.extend_from_slice(&(rect.2 as u16).to_be_bytes());
data.extend_from_slice(&(rect.3 as u16).to_be_bytes());
data.extend_from_slice(&(value as u16).to_be_bytes());
data.extend_from_slice(&(if visible { 0xFFFFu16 } else { 0 }).to_be_bytes());
data.extend_from_slice(&(max as u16).to_be_bytes());
data.extend_from_slice(&(min as u16).to_be_bytes());
data.extend_from_slice(&(proc_id as u16).to_be_bytes());
data.extend_from_slice(&ref_con.to_be_bytes());
let len = title.len().min(255);
data.push(len as u8);
data.extend_from_slice(&title[..len]);
data
}
fn alloc_control_handle(
bus: &mut MacMemoryBus,
rect: (i16, i16, i16, i16),
vis: u8,
hilite: u8,
) -> (u32, u32) {
let ctrl_ptr = bus.alloc(40);
let ctrl_handle = bus.alloc(4);
bus.write_long(ctrl_handle, ctrl_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, vis);
bus.write_byte(ctrl_ptr + 17, hilite);
(ctrl_handle, ctrl_ptr)
}
fn alloc_scrollbar_control(
disp: &mut super::super::TrapDispatcher,
bus: &mut MacMemoryBus,
rect: (i16, i16, i16, i16),
vis: u8,
hilite: u8,
value: i16,
min: i16,
max: i16,
) -> (u32, u32) {
let (ctrl_handle, ctrl_ptr) = alloc_control_handle(bus, rect, vis, hilite);
bus.write_word(ctrl_ptr + 18, value as u16);
bus.write_word(ctrl_ptr + 20, min as u16);
bus.write_word(ctrl_ptr + 22, max as u16);
disp.control_proc_ids.insert(ctrl_ptr, 16);
(ctrl_handle, ctrl_ptr)
}
fn new_control_handle<C: CpuOps>(
disp: &mut TrapDispatcher,
cpu: &mut C,
bus: &mut MacMemoryBus,
window_ptr: u32,
visible: bool,
proc_id: i16,
) -> u32 {
let sp = 0x300000;
let bounds_ptr = bus.alloc(8);
let title_ptr = bus.alloc(16);
bus.write_word(bounds_ptr, 10);
bus.write_word(bounds_ptr + 2, 20);
bus.write_word(bounds_ptr + 4, 30);
bus.write_word(bounds_ptr + 6, 80);
bus.write_byte(title_ptr, 4);
bus.write_bytes(title_ptr + 1, b"Aux!");
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, 0x1234_5678);
bus.write_word(sp + 4, proc_id as u16);
bus.write_word(sp + 6, 99);
bus.write_word(sp + 8, 1);
bus.write_word(sp + 10, 7);
bus.write_byte(sp + 12, if visible { 1 } else { 0 });
bus.write_long(sp + 14, title_ptr);
bus.write_long(sp + 18, bounds_ptr);
bus.write_long(sp + 22, window_ptr);
let result = disp.dispatch_control(true, 0x154, cpu, bus);
assert!(result.unwrap().is_ok());
bus.read_long(cpu.read_reg(Register::A7))
}
fn make_control_ctab_handle(bus: &mut MacMemoryBus) -> u32 {
let ctab_ptr = bus.alloc(8 + 4 * 8);
let ctab_handle = bus.alloc(4);
bus.write_long(ctab_handle, ctab_ptr);
bus.write_long(ctab_ptr, 0); bus.write_word(ctab_ptr + 4, 0); bus.write_word(ctab_ptr + 6, 3); for (index, part_id) in [0u16, 1, 2, 3].iter().enumerate() {
let entry = ctab_ptr + 8 + index as u32 * 8;
bus.write_word(entry, *part_id);
bus.write_word(entry + 2, 0x1111u16.wrapping_mul(index as u16 + 1));
bus.write_word(entry + 4, 0x0101u16.wrapping_mul(index as u16 + 2));
bus.write_word(entry + 6, 0x0202u16.wrapping_mul(index as u16 + 3));
}
ctab_handle
}
#[test]
fn new_control_initializes_record_and_links_window() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = 0x300000;
let window_ptr = bus.alloc(200);
let bounds_ptr = bus.alloc(8);
let title_ptr = bus.alloc(16);
bus.write_word(bounds_ptr, 10);
bus.write_word(bounds_ptr + 2, 20);
bus.write_word(bounds_ptr + 4, 30);
bus.write_word(bounds_ptr + 6, 80);
bus.write_byte(title_ptr, 4);
bus.write_bytes(title_ptr + 1, b"Test");
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, 0x1234_5678);
bus.write_word(sp + 4, 0);
bus.write_word(sp + 6, 99);
bus.write_word(sp + 8, 1);
bus.write_word(sp + 10, 7);
bus.write_byte(sp + 12, 1);
bus.write_long(sp + 14, title_ptr);
bus.write_long(sp + 18, bounds_ptr);
bus.write_long(sp + 22, window_ptr);
let result = disp.dispatch_control(true, 0x154, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
let handle = bus.read_long(sp + 26);
let ctrl_ptr = bus.read_long(handle);
assert_ne!(handle, 0);
assert_ne!(ctrl_ptr, 0);
assert_eq!(bus.read_long(window_ptr + 140), handle);
assert_eq!(bus.read_long(ctrl_ptr + 4), window_ptr);
assert_eq!(bus.read_word(ctrl_ptr + 8), 10);
assert_eq!(bus.read_word(ctrl_ptr + 10), 20);
assert_eq!(bus.read_word(ctrl_ptr + 12), 30);
assert_eq!(bus.read_word(ctrl_ptr + 14), 80);
assert_eq!(bus.read_byte(ctrl_ptr + 16), 255);
assert_eq!(bus.read_word(ctrl_ptr + 18), 7);
assert_eq!(bus.read_word(ctrl_ptr + 20), 1);
assert_eq!(bus.read_word(ctrl_ptr + 22), 99);
assert_eq!(bus.read_long(ctrl_ptr + 36), 0x1234_5678);
assert_eq!(bus.read_byte(ctrl_ptr + 40), 4);
assert_eq!(bus.read_bytes(ctrl_ptr + 41, 4), b"Test");
assert_eq!(cpu.read_reg(Register::A7), sp + 26);
}
#[test]
fn newcontrol_creates_aux_record_and_getauxctl_returns_true() {
let (mut disp, mut cpu, mut bus) = setup();
let window_ptr = bus.alloc(200);
let ctrl_handle = new_control_handle(&mut disp, &mut cpu, &mut bus, window_ptr, false, 0);
let aux_state = disp
.control_aux_state(ctrl_handle)
.expect("fresh control should have an AuxCtlRec on BasiliskII System 7.5.3");
let aux_ptr = bus.read_long(aux_state.handle);
assert_ne!(aux_ptr, 0, "AuxCtlHandle must master a live AuxCtlRec");
assert_eq!(
bus.read_long(aux_ptr + 4),
ctrl_handle,
"acOwner must point back to the control handle"
);
assert_ne!(
bus.read_long(aux_ptr + 8),
0,
"fresh controls should already carry a non-NIL acCTable"
);
assert_eq!(
bus.read_long(0x0CD4),
aux_state.handle,
"AuxCtlHead low-memory global should point at the fresh control's aux record"
);
let aux_out = bus.alloc(4);
let sp = 0x300100;
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, aux_out);
bus.write_long(sp + 4, ctrl_handle);
bus.write_word(sp + 8, 0xFFFF);
let result = disp.dispatch_quickdraw(true, 0x244, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), sp + 8);
assert_eq!(
bus.read_long(aux_out),
aux_state.handle,
"GetAuxCtl should write the tracked AuxCtlHandle for fresh controls"
);
assert_eq!(
bus.read_word(sp + 8),
0x0100,
"GetAuxCtl should return TRUE for fresh tracked controls"
);
}
#[test]
fn setctlcolor_reuses_aux_record_and_getauxctl_reports_custom_colors() {
let (mut disp, mut cpu, mut bus) = setup();
let window_ptr = bus.alloc(200);
let ctrl_handle = new_control_handle(&mut disp, &mut cpu, &mut bus, window_ptr, false, 0);
let aux_before = disp
.control_aux_state(ctrl_handle)
.expect("fresh control should already have an AuxCtlRec");
let custom_ctab = make_control_ctab_handle(&mut bus);
let sp = 0x300140;
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, custom_ctab);
bus.write_long(sp + 4, ctrl_handle);
let result = disp.dispatch_control(true, 0x243, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), sp + 8);
let aux_after = disp
.control_aux_state(ctrl_handle)
.expect("SetCtlColor should keep the AuxCtlRec alive");
assert_eq!(
aux_after.handle, aux_before.handle,
"SetCtlColor should rewrite the existing AuxCtlRec instead of allocating a new one"
);
let aux_ptr = bus.read_long(aux_after.handle);
assert_eq!(
bus.read_long(aux_ptr + 8),
custom_ctab,
"SetCtlColor should rewrite acCTable to the caller-supplied CCTabHandle"
);
let aux_out = bus.alloc(4);
let get_sp = 0x300180;
cpu.write_reg(Register::A7, get_sp);
bus.write_long(get_sp, aux_out);
bus.write_long(get_sp + 4, ctrl_handle);
bus.write_word(get_sp + 8, 0);
let get_result = disp.dispatch_quickdraw(true, 0x244, &mut cpu, &mut bus);
assert!(get_result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), get_sp + 8);
assert_eq!(bus.read_long(aux_out), aux_after.handle);
assert_eq!(
bus.read_word(get_sp + 8),
0x0100,
"GetAuxCtl should return TRUE after SetCtlColor installs custom colors"
);
}
#[test]
fn disposecontrol_releases_aux_record_and_repairs_auxctl_chain() {
let (mut disp, mut cpu, mut bus) = setup();
let window_ptr = bus.alloc(200);
let first = new_control_handle(&mut disp, &mut cpu, &mut bus, window_ptr, false, 0);
let second = new_control_handle(&mut disp, &mut cpu, &mut bus, window_ptr, false, 1);
let first_ctab = make_control_ctab_handle(&mut bus);
let second_ctab = make_control_ctab_handle(&mut bus);
for (ctrl_handle, ctab_handle) in [(first, first_ctab), (second, second_ctab)] {
let sp = if ctrl_handle == first {
0x300180
} else {
0x3001A0
};
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, ctab_handle);
bus.write_long(sp + 4, ctrl_handle);
let result = disp.dispatch_control(true, 0x243, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
}
let first_state = disp
.control_aux_state(first)
.expect("first control aux record");
let second_state = disp
.control_aux_state(second)
.expect("second control aux record");
let first_aux_ptr = bus.read_long(first_state.handle);
let second_aux_ptr = bus.read_long(second_state.handle);
assert_eq!(
bus.read_long(second_aux_ptr),
first_state.handle,
"second AuxCtlRec should link to the first via acNext"
);
let sp = 0x3001C0;
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, first);
let result = disp.dispatch_control(true, 0x155, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), sp + 4);
assert!(
disp.control_aux_state(first).is_none(),
"DisposeControl should drop the released control from the aux-state map"
);
assert_eq!(
bus.read_long(0x0CD4),
second_state.handle,
"AuxCtlHead should remain pointed at the surviving control's aux record"
);
assert_eq!(
bus.read_long(second_aux_ptr),
0,
"DisposeControl should repair the acNext chain when unlinking a non-head aux record"
);
assert_eq!(
bus.get_alloc_size(first_state.handle),
None,
"DisposeControl should free the released AuxCtlHandle"
);
assert_eq!(
bus.get_alloc_size(first_aux_ptr),
None,
"DisposeControl should free the released AuxCtlRec"
);
}
#[test]
fn newcontrol_consumes_arguments_and_writes_result_slot() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = 0x300000u32;
let window_ptr = bus.alloc(200);
let bounds_ptr = bus.alloc(8);
let title_ptr = bus.alloc(16);
bus.write_word(bounds_ptr, 12);
bus.write_word(bounds_ptr + 2, 18);
bus.write_word(bounds_ptr + 4, 40);
bus.write_word(bounds_ptr + 6, 90);
bus.write_byte(title_ptr, 4);
bus.write_bytes(title_ptr + 1, b"Play");
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, 0x1020_3040);
bus.write_word(sp + 4, 0);
bus.write_word(sp + 6, 99);
bus.write_word(sp + 8, 0);
bus.write_word(sp + 10, 1);
bus.write_byte(sp + 12, 1);
bus.write_long(sp + 14, title_ptr);
bus.write_long(sp + 18, bounds_ptr);
bus.write_long(sp + 22, window_ptr);
disp.dispatch_control(true, 0x154, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(cpu.read_reg(Register::A7), sp + 26);
assert_ne!(bus.read_long(sp + 26), 0);
}
#[test]
fn newcontrol_prepends_new_handle_into_window_control_list() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = 0x300000u32;
let window_ptr = bus.alloc(200);
let bounds_ptr = bus.alloc(8);
let title_ptr = bus.alloc(16);
let (old_head, old_ptr) = alloc_control_handle(&mut bus, (5, 5, 15, 50), 255, 0);
bus.write_long(old_ptr + 4, window_ptr);
bus.write_long(old_ptr, 0);
bus.write_long(window_ptr + 140, old_head);
bus.write_word(bounds_ptr, 20);
bus.write_word(bounds_ptr + 2, 24);
bus.write_word(bounds_ptr + 4, 60);
bus.write_word(bounds_ptr + 6, 120);
bus.write_byte(title_ptr, 3);
bus.write_bytes(title_ptr + 1, b"New");
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, 0xA0B0_C0D0);
bus.write_word(sp + 4, 0);
bus.write_word(sp + 6, 100);
bus.write_word(sp + 8, 0);
bus.write_word(sp + 10, 10);
bus.write_byte(sp + 12, 1);
bus.write_long(sp + 14, title_ptr);
bus.write_long(sp + 18, bounds_ptr);
bus.write_long(sp + 22, window_ptr);
disp.dispatch_control(true, 0x154, &mut cpu, &mut bus)
.unwrap()
.unwrap();
let new_head = bus.read_long(sp + 26);
let new_ptr = bus.read_long(new_head);
assert_eq!(bus.read_long(window_ptr + 140), new_head);
assert_eq!(bus.read_long(new_ptr), old_head);
assert_eq!(bus.read_long(new_ptr + 4), window_ptr);
}
#[test]
fn disposecontrol_consumes_control_handle_argument() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = 0x300000u32;
let (ctrl_handle, _) = alloc_control_handle(&mut bus, (10, 20, 30, 60), 255, 0);
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, ctrl_handle);
disp.dispatch_control(true, 0x155, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(cpu.read_reg(Register::A7), sp + 4);
}
#[test]
fn disposecontrol_unlinks_control_from_owner_window_list() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = 0x300000u32;
let window_ptr = bus.alloc(200);
let (tail_handle, tail_ptr) = alloc_control_handle(&mut bus, (10, 20, 30, 60), 255, 0);
let (head_handle, head_ptr) = alloc_control_handle(&mut bus, (12, 24, 36, 72), 255, 0);
bus.write_long(tail_ptr + 4, window_ptr);
bus.write_long(head_ptr + 4, window_ptr);
bus.write_long(tail_ptr, 0);
bus.write_long(head_ptr, tail_handle);
bus.write_long(window_ptr + 140, head_handle);
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, head_handle);
disp.dispatch_control(true, 0x155, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(bus.read_long(window_ptr + 140), tail_handle);
assert_eq!(bus.read_long(tail_ptr), 0);
}
#[test]
fn killcontrols_consumes_window_argument() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = 0x300000u32;
let window_ptr = bus.alloc(200);
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, window_ptr);
disp.dispatch_control(true, 0x156, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(cpu.read_reg(Register::A7), sp + 4);
}
#[test]
fn killcontrols_clears_window_control_list_head() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = 0x300000u32;
let window_ptr = bus.alloc(200);
let (tail_handle, tail_ptr) = alloc_control_handle(&mut bus, (8, 12, 24, 80), 255, 0);
let (head_handle, head_ptr) = alloc_control_handle(&mut bus, (16, 20, 36, 100), 255, 0);
bus.write_long(tail_ptr + 4, window_ptr);
bus.write_long(head_ptr + 4, window_ptr);
bus.write_long(tail_ptr, 0);
bus.write_long(head_ptr, tail_handle);
bus.write_long(window_ptr + 140, head_handle);
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, window_ptr);
disp.dispatch_control(true, 0x156, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(bus.read_long(window_ptr + 140), 0);
}
#[test]
fn setctlaction_consumes_control_and_action_arguments() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = 0x300000u32;
let (ctrl_handle, _) = alloc_control_handle(&mut bus, (10, 20, 30, 60), 255, 0);
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, 0x00AA_BBCC);
bus.write_long(sp + 4, ctrl_handle);
disp.dispatch_control(true, 0x16B, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(cpu.read_reg(Register::A7), sp + 8);
}
#[test]
fn setctlaction_updates_control_action_procptr() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = 0x300000u32;
let (ctrl_handle, ctrl_ptr) = alloc_control_handle(&mut bus, (10, 20, 30, 60), 255, 0);
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, 0x0012_3456);
bus.write_long(sp + 4, ctrl_handle);
disp.dispatch_control(true, 0x16B, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(bus.read_long(ctrl_ptr + 32), 0x0012_3456);
}
#[test]
fn getctlaction_consumes_control_argument_and_writes_result_slot() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = 0x300000u32;
let (ctrl_handle, ctrl_ptr) = alloc_control_handle(&mut bus, (10, 20, 30, 60), 255, 0);
bus.write_long(ctrl_ptr + 32, 0x00AB_CDEF);
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, ctrl_handle);
disp.dispatch_control(true, 0x16A, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(cpu.read_reg(Register::A7), sp + 4);
assert_eq!(bus.read_long(sp + 4), 0x00AB_CDEF);
}
#[test]
fn getctlaction_returns_control_action_procptr() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = 0x300000u32;
let (ctrl_handle, ctrl_ptr) = alloc_control_handle(&mut bus, (10, 20, 30, 60), 255, 0);
bus.write_long(ctrl_ptr + 32, 0x00FE_DCBA);
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, ctrl_handle);
disp.dispatch_control(true, 0x16A, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(bus.read_long(sp + 4), 0x00FE_DCBA);
}
#[test]
fn getnewcontrol_parses_cntl_resource_and_links_at_window_head() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = 0x300000u32;
let window_ptr = bus.alloc(200);
let old_ctrl_ptr = bus.alloc(40);
let old_ctrl_handle = bus.alloc(4);
bus.write_long(old_ctrl_handle, old_ctrl_ptr);
bus.write_long(window_ptr + 140, old_ctrl_handle);
let cntl = build_cntl_resource((10, 20, 40, 100), 7, true, 2, 99, 0, 0x1234_5678, b"Play");
disp.install_test_resource(&mut bus, *b"CNTL", 128, &cntl);
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, window_ptr);
bus.write_word(sp + 4, 128);
bus.write_long(sp + 6, 0xDEAD_BEEF);
disp.dispatch_control(true, 0x1BE, &mut cpu, &mut bus)
.unwrap()
.unwrap();
let handle = bus.read_long(sp + 6);
let ctrl_ptr = bus.read_long(handle);
assert_ne!(handle, 0);
assert_ne!(ctrl_ptr, 0);
assert_eq!(cpu.read_reg(Register::A7), sp + 6);
assert_eq!(bus.read_long(window_ptr + 140), handle);
assert_eq!(bus.read_long(ctrl_ptr), old_ctrl_handle);
assert_eq!(bus.read_long(ctrl_ptr + 4), window_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, 40);
assert_eq!(bus.read_word(ctrl_ptr + 14) as i16, 100);
assert_eq!(bus.read_byte(ctrl_ptr + 16), 255);
assert_eq!(bus.read_word(ctrl_ptr + 18) as i16, 7);
assert_eq!(bus.read_word(ctrl_ptr + 20) as i16, 2);
assert_eq!(bus.read_word(ctrl_ptr + 22) as i16, 99);
assert_eq!(bus.read_long(ctrl_ptr + 36), 0x1234_5678);
assert_eq!(bus.read_byte(ctrl_ptr + 40), 4);
assert_eq!(bus.read_bytes(ctrl_ptr + 41, 4), b"Play");
}
#[test]
fn getnewcontrol_missing_cntl_returns_nil_and_preserves_window_list() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = 0x300000u32;
let window_ptr = bus.alloc(200);
let old_ctrl_ptr = bus.alloc(40);
let old_ctrl_handle = bus.alloc(4);
bus.write_long(old_ctrl_handle, old_ctrl_ptr);
bus.write_long(window_ptr + 140, old_ctrl_handle);
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, window_ptr);
bus.write_word(sp + 4, 999);
bus.write_long(sp + 6, 0xDEAD_BEEF);
disp.dispatch_control(true, 0x1BE, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(cpu.read_reg(Register::A7), sp + 6);
assert_eq!(bus.read_long(sp + 6), 0);
assert_eq!(bus.read_long(window_ptr + 140), old_ctrl_handle);
}
#[test]
fn control_accessors_round_trip_action_refcon_title_and_bounds() {
let (mut disp, mut cpu, mut bus) = setup();
let ctrl_ptr = bus.alloc(296);
let ctrl_handle = bus.alloc(4);
let title_ptr = bus.alloc(16);
let out_title_ptr = bus.alloc(16);
let sp = 0x300000;
bus.write_long(ctrl_handle, ctrl_ptr);
bus.write_long(ctrl_ptr + 36, 0xCAFE_BABE);
bus.write_long(ctrl_ptr + 32, 0x0012_3456);
bus.write_word(ctrl_ptr + 8, 10);
bus.write_word(ctrl_ptr + 10, 20);
bus.write_word(ctrl_ptr + 12, 30);
bus.write_word(ctrl_ptr + 14, 60);
bus.write_byte(ctrl_ptr + 16, 255);
bus.write_word(ctrl_ptr + 20, 3);
bus.write_word(ctrl_ptr + 22, 11);
bus.write_byte(title_ptr, 3);
bus.write_bytes(title_ptr + 1, b"EV!");
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, title_ptr);
bus.write_long(sp + 4, ctrl_handle);
let result = disp.dispatch_control(true, 0x15F, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_byte(ctrl_ptr + 40), 3);
assert_eq!(bus.read_bytes(ctrl_ptr + 41, 3), b"EV!");
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, out_title_ptr);
bus.write_long(sp + 4, ctrl_handle);
let result = disp.dispatch_control(true, 0x15E, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_byte(out_title_ptr), 3);
assert_eq!(bus.read_bytes(out_title_ptr + 1, 3), b"EV!");
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, ctrl_handle);
let result = disp.dispatch_control(true, 0x15A, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_long(sp + 4), 0xCAFE_BABE);
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, ctrl_handle);
let result = disp.dispatch_control(true, 0x16A, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_long(sp + 4), 0x0012_3456);
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, 10);
bus.write_long(sp + 2, ctrl_handle);
let result = disp.dispatch_control(true, 0x164, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, 21);
bus.write_long(sp + 2, ctrl_handle);
let result = disp.dispatch_control(true, 0x165, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, ctrl_handle);
let result = disp.dispatch_control(true, 0x161, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 4), 10);
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, ctrl_handle);
let result = disp.dispatch_control(true, 0x162, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 4), 21);
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, 15);
bus.write_word(sp + 2, 25);
bus.write_long(sp + 4, ctrl_handle);
let result = disp.dispatch_control(true, 0x166, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 8), 10);
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, 0);
bus.write_word(sp + 2, 0);
bus.write_long(sp + 4, ctrl_handle);
let result = disp.dispatch_control(true, 0x166, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 8), 0);
}
#[test]
fn sizecontrol_sets_bottom_right_from_requested_width_height_and_pops_args() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = 0x300000u32;
let (ctrl_handle, ctrl_ptr) = alloc_control_handle(&mut bus, (10, 20, 40, 70), 255, 0);
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, 18); bus.write_word(sp + 2, 44); bus.write_long(sp + 4, ctrl_handle);
disp.dispatch_control(true, 0x15C, &mut cpu, &mut bus)
.unwrap()
.unwrap();
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, 28);
assert_eq!(bus.read_word(ctrl_ptr + 14) as i16, 64);
assert_eq!(cpu.read_reg(Register::A7), sp + 8);
}
#[test]
fn getctitle_copies_control_title_to_output_str255_and_pops_args() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = 0x300000u32;
let ctrl_ptr = bus.alloc(296);
let ctrl_handle = bus.alloc(4);
bus.write_long(ctrl_handle, ctrl_ptr);
let out_title = bus.alloc(16);
bus.write_byte(ctrl_ptr + 40, 5);
bus.write_bytes(ctrl_ptr + 41, b"Hello");
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, out_title);
bus.write_long(sp + 4, ctrl_handle);
disp.dispatch_control(true, 0x15E, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(bus.read_byte(out_title), 5);
assert_eq!(bus.read_bytes(out_title + 1, 5), b"Hello");
assert_eq!(cpu.read_reg(Register::A7), sp + 8);
}
#[test]
fn setctitle_updates_control_title_from_input_str255_and_pops_args() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = 0x300000u32;
let ctrl_ptr = bus.alloc(296);
let ctrl_handle = bus.alloc(4);
bus.write_long(ctrl_handle, ctrl_ptr);
let in_title = bus.alloc(16);
bus.write_word(ctrl_ptr + 8, 10);
bus.write_word(ctrl_ptr + 10, 20);
bus.write_word(ctrl_ptr + 12, 40);
bus.write_word(ctrl_ptr + 14, 70);
bus.write_byte(ctrl_ptr + 16, 255);
bus.write_byte(ctrl_ptr + 40, 3);
bus.write_bytes(ctrl_ptr + 41, b"Old");
bus.write_byte(in_title, 7);
bus.write_bytes(in_title + 1, b"Options");
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, in_title);
bus.write_long(sp + 4, ctrl_handle);
disp.dispatch_control(true, 0x15F, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(bus.read_byte(ctrl_ptr + 40), 7);
assert_eq!(bus.read_bytes(ctrl_ptr + 41, 7), b"Options");
assert_eq!(cpu.read_reg(Register::A7), sp + 8);
}
#[test]
fn setctlvalue_clamps_above_max() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = 0x300000u32;
cpu.write_reg(Register::A7, sp);
let ctrl_ptr = bus.alloc(40);
bus.write_word(ctrl_ptr + 20, 0);
bus.write_word(ctrl_ptr + 22, 100);
let handle = bus.alloc(4);
bus.write_long(handle, ctrl_ptr);
bus.write_word(sp, 500);
bus.write_long(sp + 2, handle);
disp.dispatch_control(true, 0x163, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(bus.read_word(ctrl_ptr + 18) as i16, 100);
}
#[test]
fn setctlvalue_clamps_below_min() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = 0x300000u32;
cpu.write_reg(Register::A7, sp);
let ctrl_ptr = bus.alloc(40);
bus.write_word(ctrl_ptr + 20, 50);
bus.write_word(ctrl_ptr + 22, 100);
let handle = bus.alloc(4);
bus.write_long(handle, ctrl_ptr);
bus.write_word(sp, 10);
bus.write_long(sp + 2, handle);
disp.dispatch_control(true, 0x163, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(bus.read_word(ctrl_ptr + 18) as i16, 50);
}
#[test]
fn setctlvalue_passes_through_inrange() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = 0x300000u32;
cpu.write_reg(Register::A7, sp);
let ctrl_ptr = bus.alloc(40);
bus.write_word(ctrl_ptr + 20, 0);
bus.write_word(ctrl_ptr + 22, 100);
let handle = bus.alloc(4);
bus.write_long(handle, ctrl_ptr);
bus.write_word(sp, 42);
bus.write_long(sp + 2, handle);
disp.dispatch_control(true, 0x163, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(bus.read_word(ctrl_ptr + 18) as i16, 42);
}
#[test]
fn setctlvalue_popupmenuproc_variant_does_not_clamp_to_menu_id_min() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = 0x300000u32;
cpu.write_reg(Register::A7, sp);
let ctrl_ptr = bus.alloc(40);
bus.write_word(ctrl_ptr + 20, 1300); bus.write_word(ctrl_ptr + 22, 0);
let handle = bus.alloc(4);
bus.write_long(handle, ctrl_ptr);
disp.control_proc_ids.insert(ctrl_ptr, 1009);
bus.write_word(sp, 3);
bus.write_long(sp + 2, handle);
disp.dispatch_control(true, 0x163, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(bus.read_word(ctrl_ptr + 18) as i16, 3);
}
#[test]
fn setctlmin_bumps_below_value_up() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = 0x300000u32;
cpu.write_reg(Register::A7, sp);
let ctrl_ptr = bus.alloc(40);
bus.write_word(ctrl_ptr + 18, 5); bus.write_word(ctrl_ptr + 20, 0); bus.write_word(ctrl_ptr + 22, 100); let handle = bus.alloc(4);
bus.write_long(handle, ctrl_ptr);
bus.write_word(sp, 20); bus.write_long(sp + 2, handle);
disp.dispatch_control(true, 0x164, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(bus.read_word(ctrl_ptr + 20) as i16, 20);
assert_eq!(
bus.read_word(ctrl_ptr + 18) as i16,
20,
"current value must be bumped up to the new minimum"
);
}
#[test]
fn setctlmax_pulls_above_value_down() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = 0x300000u32;
cpu.write_reg(Register::A7, sp);
let ctrl_ptr = bus.alloc(40);
bus.write_word(ctrl_ptr + 18, 80); bus.write_word(ctrl_ptr + 20, 0); bus.write_word(ctrl_ptr + 22, 100); let handle = bus.alloc(4);
bus.write_long(handle, ctrl_ptr);
bus.write_word(sp, 50); bus.write_long(sp + 2, handle);
disp.dispatch_control(true, 0x165, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(bus.read_word(ctrl_ptr + 22) as i16, 50);
assert_eq!(
bus.read_word(ctrl_ptr + 18) as i16,
50,
"current value must be pulled down to the new maximum"
);
}
#[test]
fn setctlmin_inrange_value_untouched() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = 0x300000u32;
cpu.write_reg(Register::A7, sp);
let ctrl_ptr = bus.alloc(40);
bus.write_word(ctrl_ptr + 18, 50); bus.write_word(ctrl_ptr + 20, 0);
bus.write_word(ctrl_ptr + 22, 100);
let handle = bus.alloc(4);
bus.write_long(handle, ctrl_ptr);
bus.write_word(sp, 10); bus.write_long(sp + 2, handle);
disp.dispatch_control(true, 0x164, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(bus.read_word(ctrl_ptr + 20) as i16, 10);
assert_eq!(
bus.read_word(ctrl_ptr + 18) as i16,
50,
"current value in range must stay unchanged"
);
}
#[test]
fn showcontrol_sets_contrlvis_visible_and_pops_handle_arg() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = 0x300000u32;
let (ctrl_handle, ctrl_ptr) = alloc_control_handle(&mut bus, (10, 20, 30, 60), 0, 0);
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, ctrl_handle);
disp.dispatch_control(true, 0x157, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(bus.read_byte(ctrl_ptr + 16), 255);
assert_eq!(cpu.read_reg(Register::A7), sp + 4);
}
#[test]
fn hidecontrol_clears_contrlvis_and_pops_handle_arg() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = 0x300000u32;
let (ctrl_handle, ctrl_ptr) = alloc_control_handle(&mut bus, (10, 20, 30, 60), 255, 0);
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, ctrl_handle);
disp.dispatch_control(true, 0x158, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(bus.read_byte(ctrl_ptr + 16), 0);
assert_eq!(cpu.read_reg(Register::A7), sp + 4);
}
#[test]
fn movecontrol_repositions_rect_preserves_size_and_pops_args() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = 0x300000u32;
let (ctrl_handle, ctrl_ptr) = alloc_control_handle(&mut bus, (10, 20, 40, 70), 255, 0);
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, 35); bus.write_word(sp + 2, 90); bus.write_long(sp + 4, ctrl_handle);
disp.dispatch_control(true, 0x159, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(bus.read_word(ctrl_ptr + 8) as i16, 35);
assert_eq!(bus.read_word(ctrl_ptr + 10) as i16, 90);
assert_eq!(bus.read_word(ctrl_ptr + 12) as i16, 65);
assert_eq!(bus.read_word(ctrl_ptr + 14) as i16, 140);
assert_eq!(cpu.read_reg(Register::A7), sp + 8);
}
#[test]
fn hilite_control_writes_contrlhilite_and_pops_stack() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = 0x300000u32;
let (ctrl_handle, ctrl_ptr) = alloc_control_handle(&mut bus, (10, 20, 30, 60), 255, 0);
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, 1);
bus.write_long(sp + 2, ctrl_handle);
disp.dispatch_control(true, 0x15D, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(bus.read_byte(ctrl_ptr + 17), 1);
assert_eq!(cpu.read_reg(Register::A7), sp + 6);
}
#[test]
fn hilite_control_255_marks_control_inactive() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = 0x300000u32;
let (ctrl_handle, ctrl_ptr) = alloc_control_handle(&mut bus, (10, 20, 30, 60), 255, 0);
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, 255);
bus.write_long(sp + 2, ctrl_handle);
disp.dispatch_control(true, 0x15D, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(bus.read_byte(ctrl_ptr + 17), 255);
assert_eq!(cpu.read_reg(Register::A7), sp + 6);
}
#[test]
fn track_control_visible_active_button_hit_returns_inbutton() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = 0x300000u32;
let (ctrl_handle, _) = alloc_control_handle(&mut bus, (10, 20, 30, 60), 255, 0);
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, 0);
bus.write_word(sp + 4, 15);
bus.write_word(sp + 6, 25);
bus.write_long(sp + 8, ctrl_handle);
disp.dispatch_control(true, 0x168, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(bus.read_word(sp + 12), 10);
assert_eq!(cpu.read_reg(Register::A7), sp + 12);
}
#[test]
fn track_control_popup_menu_tracks_and_updates_value_on_release() {
let (mut disp, mut cpu, mut bus) = setup_with_port();
let sp = 0x300000u32;
let owner = bus.read_long(bus.read_long(cpu.read_reg(Register::A5)));
let (ctrl_handle, ctrl_ptr) = alloc_control_handle(&mut bus, (10, 20, 30, 130), 255, 0);
bus.write_long(ctrl_ptr + 4, owner);
bus.write_word(ctrl_ptr + 18, 1);
bus.write_word(ctrl_ptr + 20, 900); bus.write_word(ctrl_ptr + 22, 0);
disp.control_proc_ids.insert(ctrl_ptr, 1009);
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,
});
disp.mouse_button = true;
disp.mouse_pos = (15, 25);
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, 0);
bus.write_word(sp + 4, 15);
bus.write_word(sp + 6, 25);
bus.write_long(sp + 8, ctrl_handle);
bus.write_word(sp + 12, 0xBEEF);
disp.dispatch_control(true, 0x168, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(
cpu.read_reg(Register::A7),
sp,
"popup tracking should defer the TrackControl stack pop"
);
assert!(disp.control_tracking.is_some());
assert_eq!(bus.read_word(sp + 12), 0xBEEF);
disp.mouse_pos = (48, 25);
disp.dispatch_control(true, 0x168, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(
disp.control_tracking
.as_ref()
.map(|tracking| tracking.highlighted_item),
Some(2)
);
disp.mouse_button = false;
disp.dispatch_control(true, 0x168, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(cpu.read_reg(Register::A7), sp + 12);
assert_eq!(bus.read_word(sp + 12), 10);
assert_eq!(bus.read_word(ctrl_ptr + 18) as i16, 2);
assert!(disp.control_tracking.is_none());
}
#[test]
fn track_control_point_outside_returns_zero() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = 0x300000u32;
let (ctrl_handle, _) = alloc_control_handle(&mut bus, (10, 20, 30, 60), 255, 0);
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, 0);
bus.write_word(sp + 4, 5);
bus.write_word(sp + 6, 5);
bus.write_long(sp + 8, ctrl_handle);
disp.dispatch_control(true, 0x168, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(bus.read_word(sp + 12), 0);
assert_eq!(cpu.read_reg(Register::A7), sp + 12);
}
#[test]
fn track_control_inactive_or_invisible_control_returns_zero() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = 0x300000u32;
let (inactive_handle, _) = alloc_control_handle(&mut bus, (10, 20, 30, 60), 255, 255);
let (invisible_handle, _) = alloc_control_handle(&mut bus, (10, 20, 30, 60), 0, 0);
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, 0);
bus.write_word(sp + 4, 15);
bus.write_word(sp + 6, 25);
bus.write_long(sp + 8, inactive_handle);
disp.dispatch_control(true, 0x168, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(bus.read_word(sp + 12), 0);
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, 0);
bus.write_word(sp + 4, 15);
bus.write_word(sp + 6, 25);
bus.write_long(sp + 8, invisible_handle);
disp.dispatch_control(true, 0x168, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(bus.read_word(sp + 12), 0);
assert_eq!(cpu.read_reg(Register::A7), sp + 12);
}
#[test]
fn drawcontrols_pops_window_arg_and_preserves_control_list_links() {
let (mut disp, mut cpu, mut bus) = setup_with_port();
let sp = 0x300000u32;
let a5 = cpu.read_reg(Register::A5);
let qd_globals = bus.read_long(a5);
let window_ptr = bus.read_long(qd_globals);
let (first_handle, first_ptr) = alloc_control_handle(&mut bus, (10, 20, 30, 60), 255, 0);
let (second_handle, second_ptr) = alloc_control_handle(&mut bus, (18, 28, 38, 68), 255, 0);
bus.write_long(first_ptr + 4, window_ptr);
bus.write_long(second_ptr + 4, window_ptr);
bus.write_long(first_ptr, 0);
bus.write_long(second_ptr, first_handle);
bus.write_long(window_ptr + 140, second_handle);
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, window_ptr);
disp.dispatch_control(true, 0x169, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(cpu.read_reg(Register::A7), sp + 4);
assert_eq!(bus.read_long(window_ptr + 140), second_handle);
assert_eq!(bus.read_long(second_ptr), first_handle);
assert_eq!(bus.read_long(first_ptr), 0);
}
#[test]
fn findcontrol_visible_active_hit_returns_inbutton_and_control_handle() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = 0x300000u32;
let window_ptr = bus.alloc(200);
let which_ctrl_out = bus.alloc(4);
let (ctrl_handle, ctrl_ptr) = alloc_control_handle(&mut bus, (10, 20, 30, 60), 255, 0);
bus.write_long(ctrl_ptr, 0);
bus.write_long(window_ptr + 140, ctrl_handle);
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, which_ctrl_out);
bus.write_long(sp + 4, window_ptr);
bus.write_word(sp + 8, 15);
bus.write_word(sp + 10, 25);
disp.dispatch_control(true, 0x16C, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(bus.read_long(which_ctrl_out), ctrl_handle);
assert_eq!(bus.read_word(sp + 12), 10);
assert_eq!(cpu.read_reg(Register::A7), sp + 12);
}
#[test]
fn findcontrol_inactive_invisible_or_miss_returns_zero_and_nil() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = 0x300000u32;
let window_ptr = bus.alloc(200);
let which_ctrl_out = bus.alloc(4);
let (inactive_handle, inactive_ptr) =
alloc_control_handle(&mut bus, (10, 20, 30, 60), 255, 255);
bus.write_long(inactive_ptr, 0);
bus.write_long(window_ptr + 140, inactive_handle);
cpu.write_reg(Register::A7, sp);
bus.write_long(which_ctrl_out, 0xDEAD_BEEF);
bus.write_long(sp, which_ctrl_out);
bus.write_long(sp + 4, window_ptr);
bus.write_word(sp + 8, 15);
bus.write_word(sp + 10, 25);
disp.dispatch_control(true, 0x16C, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(bus.read_long(which_ctrl_out), 0);
assert_eq!(bus.read_word(sp + 12), 0);
assert_eq!(cpu.read_reg(Register::A7), sp + 12);
let (invisible_handle, invisible_ptr) =
alloc_control_handle(&mut bus, (10, 20, 30, 60), 0, 0);
bus.write_long(invisible_ptr, 0);
bus.write_long(window_ptr + 140, invisible_handle);
cpu.write_reg(Register::A7, sp);
bus.write_long(which_ctrl_out, 0xDEAD_BEEF);
bus.write_long(sp, which_ctrl_out);
bus.write_long(sp + 4, window_ptr);
bus.write_word(sp + 8, 15);
bus.write_word(sp + 10, 25);
disp.dispatch_control(true, 0x16C, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(bus.read_long(which_ctrl_out), 0);
assert_eq!(bus.read_word(sp + 12), 0);
assert_eq!(cpu.read_reg(Register::A7), sp + 12);
let (visible_handle, visible_ptr) =
alloc_control_handle(&mut bus, (10, 20, 30, 60), 255, 0);
bus.write_long(visible_ptr, 0);
bus.write_long(window_ptr + 140, visible_handle);
cpu.write_reg(Register::A7, sp);
bus.write_long(which_ctrl_out, 0xDEAD_BEEF);
bus.write_long(sp, which_ctrl_out);
bus.write_long(sp + 4, window_ptr);
bus.write_word(sp + 8, 5);
bus.write_word(sp + 10, 5);
disp.dispatch_control(true, 0x16C, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(bus.read_long(which_ctrl_out), 0);
assert_eq!(bus.read_word(sp + 12), 0);
assert_eq!(cpu.read_reg(Register::A7), sp + 12);
}
#[test]
fn testcontrol_returns_inbutton_for_push_buttons_and_incheckbox_for_check_family() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = 0x300000u32;
let (button_handle, button_ptr) = alloc_control_handle(&mut bus, (10, 20, 30, 60), 255, 0);
disp.control_proc_ids.insert(button_ptr, 0);
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, 15);
bus.write_word(sp + 2, 25);
bus.write_long(sp + 4, button_handle);
bus.write_word(sp + 8, 0xDEAD);
disp.dispatch_control(true, 0x166, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(bus.read_word(sp + 8), 10);
assert_eq!(cpu.read_reg(Register::A7), sp + 8);
let (checkbox_handle, checkbox_ptr) =
alloc_control_handle(&mut bus, (40, 50, 80, 120), 255, 0);
disp.control_proc_ids.insert(checkbox_ptr, 1);
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, 60);
bus.write_word(sp + 2, 70);
bus.write_long(sp + 4, checkbox_handle);
bus.write_word(sp + 8, 0xBEEF);
disp.dispatch_control(true, 0x166, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(bus.read_word(sp + 8), 11);
assert_eq!(cpu.read_reg(Register::A7), sp + 8);
let (radio_handle, radio_ptr) = alloc_control_handle(&mut bus, (90, 30, 120, 100), 255, 0);
disp.control_proc_ids.insert(radio_ptr, 2);
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, 100);
bus.write_word(sp + 2, 40);
bus.write_long(sp + 4, radio_handle);
bus.write_word(sp + 8, 0xCAFE);
disp.dispatch_control(true, 0x166, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(bus.read_word(sp + 8), 11);
assert_eq!(cpu.read_reg(Register::A7), sp + 8);
}
#[test]
fn testcontrol_returns_zero_for_inactive_invisible_or_miss() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = 0x300000u32;
let (ctrl_handle, ctrl_ptr) = alloc_control_handle(&mut bus, (10, 20, 30, 60), 255, 0);
disp.control_proc_ids.insert(ctrl_ptr, 0);
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, 5);
bus.write_word(sp + 2, 5);
bus.write_long(sp + 4, ctrl_handle);
bus.write_word(sp + 8, 0xDEAD);
disp.dispatch_control(true, 0x166, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(bus.read_word(sp + 8), 0);
bus.write_byte(ctrl_ptr + 17, 255);
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, 15);
bus.write_word(sp + 2, 25);
bus.write_long(sp + 4, ctrl_handle);
bus.write_word(sp + 8, 0xBEEF);
disp.dispatch_control(true, 0x166, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(bus.read_word(sp + 8), 0);
bus.write_byte(ctrl_ptr + 17, 0);
bus.write_byte(ctrl_ptr + 16, 0);
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, 15);
bus.write_word(sp + 2, 25);
bus.write_long(sp + 4, ctrl_handle);
bus.write_word(sp + 8, 0xCAFE);
disp.dispatch_control(true, 0x166, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(bus.read_word(sp + 8), 0);
assert_eq!(cpu.read_reg(Register::A7), sp + 8);
}
#[test]
fn testcontrol_function_protocol_consumes_controlhandle_and_point_and_writes_integer_result() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = 0x300000u32;
let (ctrl_handle, ctrl_ptr) = alloc_control_handle(&mut bus, (10, 20, 30, 60), 255, 0);
disp.control_proc_ids.insert(ctrl_ptr, 0);
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, 15);
bus.write_word(sp + 2, 25);
bus.write_long(sp + 4, ctrl_handle);
bus.write_word(sp + 8, 0xCAFE);
bus.write_word(sp + 10, 0xBEEF);
disp.dispatch_control(true, 0x166, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(cpu.read_reg(Register::A7), sp + 8);
assert_eq!(bus.read_word(sp + 8), 10);
assert_eq!(
bus.read_word(sp + 10),
0xBEEF,
"TestControl must only overwrite the INTEGER result slot"
);
}
#[test]
fn testcontrol_returns_standard_scrollbar_part_codes_for_vertical_scrollbar() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = 0x300000u32;
let (scroll_handle, _) =
alloc_scrollbar_control(&mut disp, &mut bus, (60, 240, 220, 256), 255, 0, 40, 0, 100);
for (pt_v, pt_h, expected) in [
(70i16, 248i16, 20u16),
(95, 248, 22),
(126, 248, 129),
(150, 248, 23),
(210, 248, 21),
] {
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, pt_v as u16);
bus.write_word(sp + 2, pt_h as u16);
bus.write_long(sp + 4, scroll_handle);
bus.write_word(sp + 8, 0xBEEF);
bus.write_word(sp + 10, 0xCAFE);
disp.dispatch_control(true, 0x166, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(cpu.read_reg(Register::A7), sp + 8);
assert_eq!(bus.read_word(sp + 8), expected);
assert_eq!(bus.read_word(sp + 10), 0xCAFE);
}
}
#[test]
fn draw1control_nil_handle_is_noop_and_pops_arg() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = 0x300000u32;
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, 0);
disp.dispatch_control(true, 0x16D, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(cpu.read_reg(Register::A7), sp + 4);
}
#[test]
fn draw1control_popup_control_preserves_current_gdevice_and_pops_arg() {
let (mut disp, mut cpu, mut bus) = setup_with_port();
let sp = 0x300000u32;
let window_ptr = disp.current_port;
let gdevice_before = disp.current_gdevice;
let (ctrl_handle, ctrl_ptr) =
alloc_button_control(&mut disp, &mut bus, window_ptr, (20, 20, 260, 60));
disp.control_proc_ids.insert(ctrl_ptr, 1008);
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, ctrl_handle);
disp.dispatch_control(true, 0x16D, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(cpu.read_reg(Register::A7), sp + 4);
assert_eq!(disp.current_gdevice, gdevice_before);
}
#[test]
fn draw1control_out_of_range_handle_is_noop_and_pops_arg() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = 0x300000u32;
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, bus.ram_size() + 4);
disp.dispatch_control(true, 0x16D, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(cpu.read_reg(Register::A7), sp + 4);
}
#[test]
fn draw1control_handle_pointing_at_out_of_range_control_ptr_is_noop_and_pops_arg() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = 0x300000u32;
let handle = bus.alloc(4);
assert_ne!(handle, 0);
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, handle);
bus.write_long(handle, bus.ram_size() + 0x1000);
disp.dispatch_control(true, 0x16D, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(cpu.read_reg(Register::A7), sp + 4);
}
#[test]
fn draw1control_handle_pointing_to_truncated_control_record_is_noop_and_pops_arg() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = 0x300000u32;
let handle = bus.alloc(4);
let ctrl_ptr = bus.ram_size().saturating_sub(41);
assert_ne!(handle, 0);
assert!(ctrl_ptr > 0);
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, handle);
bus.write_long(handle, ctrl_ptr);
bus.write_byte(ctrl_ptr + 40, 16);
disp.dispatch_control(true, 0x16D, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(cpu.read_reg(Register::A7), sp + 4);
}
#[test]
fn dragcontrol_pops_18_bytes_and_leaves_contrlrect_unchanged_on_no_drag_path() {
let (mut disp, mut cpu, mut bus) = setup_with_port();
let sp = 0x300000u32;
let window_ptr = disp.current_port;
let (ctrl_handle, ctrl_ptr) = alloc_control_handle(&mut bus, (88, 96, 132, 220), 0, 0);
let limit_rect_ptr = bus.alloc(8);
let slop_rect_ptr = bus.alloc(8);
let top_before = bus.read_word(ctrl_ptr + 8);
let left_before = bus.read_word(ctrl_ptr + 10);
let bottom_before = bus.read_word(ctrl_ptr + 12);
let right_before = bus.read_word(ctrl_ptr + 14);
bus.write_long(ctrl_ptr + 4, window_ptr);
bus.write_long(window_ptr + 140, ctrl_handle);
bus.write_word(limit_rect_ptr, 0);
bus.write_word(limit_rect_ptr + 2, 0);
bus.write_word(limit_rect_ptr + 4, 400);
bus.write_word(limit_rect_ptr + 6, 640);
bus.write_word(slop_rect_ptr, (-40i16) as u16);
bus.write_word(slop_rect_ptr + 2, (-40i16) as u16);
bus.write_word(slop_rect_ptr + 4, (-20i16) as u16);
bus.write_word(slop_rect_ptr + 6, (-20i16) as u16);
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, 0); bus.write_long(sp + 2, slop_rect_ptr);
bus.write_long(sp + 6, limit_rect_ptr);
bus.write_word(sp + 10, 12); bus.write_word(sp + 12, 18); bus.write_long(sp + 14, ctrl_handle);
disp.dispatch_control(true, 0x167, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(cpu.read_reg(Register::A7), sp + 18);
assert_eq!(bus.read_word(ctrl_ptr + 8), top_before);
assert_eq!(bus.read_word(ctrl_ptr + 10), left_before);
assert_eq!(bus.read_word(ctrl_ptr + 12), bottom_before);
assert_eq!(bus.read_word(ctrl_ptr + 14), right_before);
}
fn alloc_button_control(
disp: &mut super::super::TrapDispatcher,
bus: &mut MacMemoryBus,
window_ptr: u32,
bounds: (i16, i16, i16, i16),
) -> (u32, u32) {
let ctrl_ptr = bus.alloc(48);
let ctrl_handle = bus.alloc(4);
bus.write_long(ctrl_handle, ctrl_ptr);
bus.write_long(ctrl_ptr, 0);
bus.write_long(ctrl_ptr + 4, window_ptr);
bus.write_word(ctrl_ptr + 8, bounds.0 as u16);
bus.write_word(ctrl_ptr + 10, bounds.1 as u16);
bus.write_word(ctrl_ptr + 12, bounds.2 as u16);
bus.write_word(ctrl_ptr + 14, bounds.3 as u16);
bus.write_byte(ctrl_ptr + 16, 255);
bus.write_byte(ctrl_ptr + 17, 0);
bus.write_word(ctrl_ptr + 18, 0);
bus.write_word(ctrl_ptr + 20, 0);
bus.write_word(ctrl_ptr + 22, 1);
bus.write_long(ctrl_ptr + 24, 0);
bus.write_long(ctrl_ptr + 28, 0);
bus.write_long(ctrl_ptr + 32, 0);
bus.write_long(ctrl_ptr + 36, 0);
bus.write_byte(ctrl_ptr + 40, 0);
disp.control_proc_ids.insert(ctrl_ptr, 0);
(ctrl_handle, ctrl_ptr)
}
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
}
#[test]
fn updtcontrol_repaints_intersecting_controls_and_empty_regions() {
let (mut disp, mut cpu, mut bus) = setup_with_port();
let window_ptr = 0x181000u32;
let screen_base = bus.read_long(window_ptr + 2);
let row_bytes = (bus.read_word(window_ptr + 6) & 0x3FFF) as u32;
let (left_handle, left_ptr) =
alloc_button_control(&mut disp, &mut bus, window_ptr, (20, 20, 60, 120));
let (right_handle, right_ptr) =
alloc_button_control(&mut disp, &mut bus, window_ptr, (20, 150, 60, 250));
bus.write_long(window_ptr + 140, right_handle);
bus.write_long(right_ptr, left_handle);
bus.write_long(left_ptr, 0);
bus.write_long(left_ptr + 4, window_ptr);
bus.write_long(right_ptr + 4, window_ptr);
let sp = 0x300000u32;
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, window_ptr);
disp.dispatch_control(true, 0x169, &mut cpu, &mut bus)
.unwrap()
.unwrap();
let left_probe = (70, 40);
let right_probe = (200, 40);
assert!(!screen_pixel_is_set(
&bus,
screen_base,
row_bytes,
left_probe.0,
left_probe.1
));
assert!(!screen_pixel_is_set(
&bus,
screen_base,
row_bytes,
right_probe.0,
right_probe.1
));
bus.write_byte(left_ptr + 17, 1);
let narrow_rgn = bus.alloc(10);
bus.write_word(narrow_rgn, 10);
bus.write_word(narrow_rgn + 2, 20);
bus.write_word(narrow_rgn + 4, 20);
bus.write_word(narrow_rgn + 6, 60);
bus.write_word(narrow_rgn + 8, 120);
let narrow_handle = bus.alloc(4);
bus.write_long(narrow_handle, narrow_rgn);
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, narrow_handle);
bus.write_long(sp + 4, window_ptr);
let sp_before = cpu.read_reg(Register::A7);
disp.dispatch_control(true, 0x153, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(cpu.read_reg(Register::A7), sp_before + 8);
assert!(
screen_pixel_is_set(&bus, screen_base, row_bytes, left_probe.0, left_probe.1),
"left control should redraw when its rect intersects the update region"
);
assert!(
!screen_pixel_is_set(&bus, screen_base, row_bytes, right_probe.0, right_probe.1),
"right control should remain unchanged when it does not intersect the update region"
);
bus.write_byte(right_ptr + 17, 1);
let empty_rgn = bus.alloc(10);
bus.write_word(empty_rgn, 10);
bus.write_word(empty_rgn + 2, 0);
bus.write_word(empty_rgn + 4, 0);
bus.write_word(empty_rgn + 6, 0);
bus.write_word(empty_rgn + 8, 0);
let empty_handle = bus.alloc(4);
bus.write_long(empty_handle, empty_rgn);
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, empty_handle);
bus.write_long(sp + 4, window_ptr);
let sp_before = cpu.read_reg(Register::A7);
disp.dispatch_control(true, 0x153, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(cpu.read_reg(Register::A7), sp_before + 8);
assert!(
screen_pixel_is_set(&bus, screen_base, row_bytes, left_probe.0, left_probe.1),
"empty update regions should leave previously repainted controls unchanged"
);
assert!(
!screen_pixel_is_set(&bus, screen_base, row_bytes, right_probe.0, right_probe.1),
"empty update regions should not repaint controls outside the update region"
);
}
#[test]
fn updtcontrol_out_of_range_window_pointer_is_noop_and_pops_args() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = 0x300000u32;
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, 0);
bus.write_long(sp + 4, bus.ram_size() + 0x1000);
let sp_before = cpu.read_reg(Register::A7);
disp.dispatch_control(true, 0x153, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(
cpu.read_reg(Register::A7),
sp_before + 8,
"UpdtControl should still consume its Pascal frame"
);
}
fn install_control_for_variant_test(
disp: &mut crate::trap::TrapDispatcher,
bus: &mut MacMemoryBus,
proc_id: i16,
) -> u32 {
let ctrl_ptr = bus.alloc(296);
let ctrl_handle = bus.alloc(4);
bus.write_long(ctrl_handle, ctrl_ptr);
disp.control_proc_ids.insert(ctrl_ptr, proc_id);
ctrl_handle
}
#[test]
fn getcvariant_returns_variation_code_from_low_four_bits_of_proc_id() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = 0x300000u32;
let h_button = install_control_for_variant_test(&mut disp, &mut bus, 0);
let h_check = install_control_for_variant_test(&mut disp, &mut bus, 1);
let h_radio = install_control_for_variant_test(&mut disp, &mut bus, 2);
let h_scroll = install_control_for_variant_test(&mut disp, &mut bus, 16);
let h_popup_fixed = install_control_for_variant_test(&mut disp, &mut bus, 1009);
for (handle, expected) in [
(h_button, 0i16),
(h_check, 1),
(h_radio, 2),
(h_scroll, 0),
(h_popup_fixed, 1),
] {
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, handle);
bus.write_word(sp + 4, 0xDEAD);
disp.dispatch_control(true, 0x009, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(cpu.read_reg(Register::A7), sp + 4);
assert_eq!(
bus.read_word(sp + 4) as i16,
expected,
"GetCVariant must return low 4 bits of procID; got wrong value"
);
}
}
#[test]
fn getcvariant_returns_zero_for_nil_control_handle() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = 0x300000u32;
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, 0);
bus.write_word(sp + 4, 0xBEEF);
disp.dispatch_control(true, 0x009, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(cpu.read_reg(Register::A7), sp + 4);
assert_eq!(
bus.read_word(sp + 4) as i16,
0,
"NIL theControl must return 0 (no variant) without crashing"
);
}
#[test]
fn getcvariant_function_protocol_pops_handle_and_writes_integer_result() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = 0x300000u32;
let handle = install_control_for_variant_test(&mut disp, &mut bus, 2);
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, handle);
bus.write_word(sp + 4, 0xCAFE);
bus.write_word(sp + 6, 0xBABE);
disp.dispatch_control(true, 0x009, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(cpu.read_reg(Register::A7), sp + 4);
assert_eq!(bus.read_word(sp + 4) as i16, 2);
assert_eq!(
bus.read_word(sp + 6),
0xBABE,
"trap must not write past the 2-byte INTEGER result slot"
);
}
}