use crate::cpu::{CpuOps, Register};
use crate::memory::{globals::addr, MacMemoryBus, MemoryBus};
use crate::ui_theme::UiThemeId;
use crate::Result;
#[derive(Clone, Debug)]
pub struct MenuItem {
pub text: String,
pub icon: u8,
pub key_equiv: u8,
pub mark: u8,
pub style: u8,
pub enabled: bool,
}
#[derive(Clone, Debug)]
pub struct Menu {
pub id: i16,
pub title: String,
pub items: Vec<MenuItem>,
pub enabled: bool,
pub handle: u32,
pub in_menu_bar: bool,
pub hierarchical: bool,
pub visible_in_menu_bar: bool,
}
pub struct SubmenuTrackingState {
pub menu: usize,
pub parent_item: i16,
pub highlighted_item: i16,
pub saved_pixels: Vec<u8>,
pub dropdown_rect: (i16, i16, i16, i16),
}
pub struct MenuTrackingState {
pub active_menu: usize,
pub highlighted_item: i16,
pub saved_pixels: Vec<u8>,
pub dropdown_rect: (i16, i16, i16, i16),
pub submenu: Option<SubmenuTrackingState>,
pub stack_ptr: u32,
pub flash_remaining: u8,
pub flash_delay: u8,
pub flash_result: u32,
}
const MENU_KEY_REDUCED_ICON: u8 = 0x1D;
const MENU_KEY_SMALL_ICON: u8 = 0x1E;
const MENU_ROW_HEIGHT: i16 = 16;
const MENU_TEXT_STYLE_SHADOW: u8 = 0x10;
const MENU_SHADOW_STYLE_ROW_HEIGHT: i16 = MENU_ROW_HEIGHT + 5;
const MENU_NORMAL_ICON_SIZE: i16 = 32;
const MENU_NORMAL_ICON_ROW_HEIGHT: i16 = 34;
const MENU_NORMAL_ICON_TEXT_LEFT_OFFSET: i16 = 51;
#[derive(Clone, Copy, Debug)]
struct MenuCIconLayout {
width: i16,
height: i16,
pm_row_bytes: u32,
mask_row_bytes: u32,
bmap_row_bytes: u32,
pixel_size: u16,
mask_data_ptr: u32,
bmap_data_ptr: u32,
pixel_data_ptr: u32,
}
fn menu_resource_size(bus: &MacMemoryBus, ptr: u32) -> usize {
let mut offset = 14usize;
let title_len = bus.read_byte(ptr + offset as u32) as usize;
offset += 1 + title_len;
loop {
let item_len = bus.read_byte(ptr + offset as u32) as usize;
offset += 1;
if item_len == 0 {
break;
}
offset += item_len; offset += 4; }
offset
}
fn get_menu_item_field(bus: &MacMemoryBus, menu_ptr: u32, item: i16, field_offset: u32) -> u8 {
if item < 1 {
return 0;
}
let title_len = bus.read_byte(menu_ptr + 14) as u32;
let mut offset = 15 + title_len;
let mut idx: i16 = 0;
loop {
let item_len = bus.read_byte(menu_ptr + offset) as u32;
if item_len == 0 {
break;
}
idx += 1;
if idx == item {
return bus.read_byte(menu_ptr + offset + 1 + item_len + field_offset);
}
offset += 1 + item_len + 4; }
0
}
fn count_menu_items_from_memory(bus: &MacMemoryBus, menu_handle: u32) -> u16 {
if menu_handle == 0 {
return 0;
}
let menu_ptr = bus.read_long(menu_handle);
if menu_ptr == 0 {
return 0;
}
let title_len = bus.read_byte(menu_ptr + 14) as u32;
let mut offset = 15 + title_len;
let mut count: u16 = 0;
loop {
let item_len = bus.read_byte(menu_ptr + offset) as u32;
if item_len == 0 {
break;
}
count += 1;
offset += 1 + item_len + 4; }
count
}
fn parse_menu_resource(bus: &MacMemoryBus, res_ptr: u32, handle: u32) -> Menu {
let menu_id = bus.read_word(res_ptr) as i16;
let enable_flags = bus.read_long(res_ptr + 10);
let title_len = bus.read_byte(res_ptr + 14) as usize;
let mut title_bytes = Vec::with_capacity(title_len);
for i in 0..title_len {
title_bytes.push(bus.read_byte(res_ptr + 15 + i as u32));
}
let title = String::from_utf8_lossy(&title_bytes).into_owned();
let mut offset = res_ptr + 15 + title_len as u32;
let mut items = Vec::new();
loop {
let item_len = bus.read_byte(offset) as usize;
if item_len == 0 {
break;
}
let mut text_bytes = Vec::with_capacity(item_len);
for i in 0..item_len {
text_bytes.push(bus.read_byte(offset + 1 + i as u32));
}
let text = String::from_utf8_lossy(&text_bytes).into_owned();
offset += 1 + item_len as u32;
let icon = bus.read_byte(offset);
let key_equiv = bus.read_byte(offset + 1);
let mark = bus.read_byte(offset + 2);
let style = bus.read_byte(offset + 3);
offset += 4;
let item_index = items.len() + 1; let enabled = if item_index <= 31 {
(enable_flags & (1 << item_index)) != 0
} else {
true
};
items.push(MenuItem {
text,
icon,
key_equiv,
mark,
style,
enabled,
});
}
Menu {
id: menu_id,
title,
items,
enabled: (enable_flags & 1) != 0,
handle,
in_menu_bar: false,
hierarchical: false,
visible_in_menu_bar: false,
}
}
fn parse_appendmenu_items(bytes: &[u8]) -> Vec<MenuItem> {
let mut items: Vec<MenuItem> = Vec::new();
if bytes.is_empty() {
return items;
}
for raw_item in bytes.split(|&b| b == b';') {
let mut text = Vec::with_capacity(raw_item.len());
let mut item = MenuItem {
text: String::new(),
icon: 0,
key_equiv: 0,
mark: 0,
style: 0,
enabled: true,
};
let mut i = 0;
while i < raw_item.len() {
let c = raw_item[i];
match c {
b'(' => {
item.enabled = false;
i += 1;
}
b'/' if i + 1 < raw_item.len() => {
item.key_equiv = raw_item[i + 1];
i += 2;
}
b'!' if i + 1 < raw_item.len() => {
item.mark = raw_item[i + 1];
i += 2;
}
b'^' if i + 1 < raw_item.len() => {
item.icon = raw_item[i + 1].saturating_sub(1);
i += 2;
}
b'<' if i + 1 < raw_item.len() => {
item.style |= match raw_item[i + 1] {
b'B' => 0x01, b'I' => 0x02, b'U' => 0x04, b'O' => 0x08, b'S' => 0x10, _ => 0,
};
i += 2;
}
_ => {
text.push(c);
i += 1;
}
}
}
item.text = String::from_utf8_lossy(&text).into_owned();
items.push(item);
}
items
}
fn sync_enable_flags(bus: &mut MacMemoryBus, menu: &Menu) {
if menu.handle == 0 {
return;
}
let menu_ptr = bus.read_long(menu.handle);
if menu_ptr == 0 {
return;
}
let mut flags: u32 = 0xFFFFFFFF;
if !menu.enabled {
flags &= !1u32;
}
for (i, item) in menu.items.iter().enumerate() {
if i >= 31 {
break;
}
if !item.enabled {
flags &= !(1u32 << (i + 1));
}
}
bus.write_long(menu_ptr + 10, flags);
}
fn serialise_menu_items_to_memory(bus: &mut MacMemoryBus, menu: &Menu) {
const MENU_RECORD_SIZE: u32 = 256;
if menu.handle == 0 {
return;
}
let menu_ptr = bus.read_long(menu.handle);
if menu_ptr == 0 {
return;
}
let title_len = bus.read_byte(menu_ptr + 14) as u32;
let mut offset = 15 + title_len;
for item in &menu.items {
let bytes = item.text.as_bytes();
let text_len = bytes.len().min(255) as u32;
let item_size = 1 + text_len + 4;
if offset + item_size + 1 > MENU_RECORD_SIZE {
break;
}
bus.write_byte(menu_ptr + offset, text_len as u8);
for (i, b) in bytes.iter().take(text_len as usize).enumerate() {
bus.write_byte(menu_ptr + offset + 1 + i as u32, *b);
}
let attr_base = menu_ptr + offset + 1 + text_len;
bus.write_byte(attr_base, item.icon);
bus.write_byte(attr_base + 1, item.key_equiv);
bus.write_byte(attr_base + 2, item.mark);
bus.write_byte(attr_base + 3, item.style);
offset += item_size;
}
if offset < MENU_RECORD_SIZE {
bus.write_byte(menu_ptr + offset, 0);
}
}
fn looks_like_menu_ptr(bus: &MacMemoryBus, menu_ptr: u32) -> bool {
if menu_ptr == 0 || menu_ptr + 15 >= bus.ram_size() {
return false;
}
let title_len = bus.read_byte(menu_ptr + 14) as u32;
if title_len > 63 || menu_ptr + 15 + title_len >= bus.ram_size() {
return false;
}
true
}
fn looks_like_menu_handle(bus: &MacMemoryBus, handle: u32) -> bool {
if handle == 0 {
return false;
}
looks_like_menu_ptr(bus, bus.read_long(handle))
}
const MC_ENTRY_SIZE: usize = 30;
const MC_RESOURCE_ENTRY_SIZE: usize = 28;
const MC_ALL_ITEMS: i16 = -98;
const MC_LAST_ID_INDIC: i16 = -99;
fn mc_entry_key(bytes: &[u8]) -> Option<(i16, i16)> {
if bytes.len() < 4 {
return None;
}
Some((
i16::from_be_bytes([bytes[0], bytes[1]]),
i16::from_be_bytes([bytes[2], bytes[3]]),
))
}
fn mc_entry_matches(bytes: &[u8], menu_id: i16, menu_item: i16) -> bool {
mc_entry_key(bytes) == Some((menu_id, menu_item))
}
impl super::TrapDispatcher {
fn menu_tracking_button_down(&self, bus: &MacMemoryBus) -> bool {
self.mouse_button || bus.read_byte(addr::MB_STATE) == 0x00
}
fn menu_tracking_mouse_pos(&self, bus: &MacMemoryBus) -> (i16, i16) {
let v = bus.read_word(addr::MOUSE_LOC2) as i16;
let h = bus.read_word(addr::MOUSE_LOC2 + 2) as i16;
if v != 0 || h != 0 {
(v, h)
} else {
self.mouse_pos
}
}
fn menu_trace_menu_fields(&self, menu_idx: Option<usize>) -> String {
match menu_idx.and_then(|idx| self.menus.get(idx).map(|menu| (idx, menu))) {
Some((idx, menu)) => format!(
"menu_index={} menu_id={} menu_title={:?}",
idx, menu.id, menu.title
),
None => "menu_index=none menu_id=none menu_title=none".to_string(),
}
}
fn resolve_menu_handle_candidate(&self, bus: &MacMemoryBus, candidate: u32) -> Option<u32> {
if looks_like_menu_handle(bus, candidate) {
return Some(candidate);
}
if !looks_like_menu_ptr(bus, candidate) {
return None;
}
self.ptr_to_handle
.get(&candidate)
.copied()
.or_else(|| {
self.menus
.iter()
.find(|menu| bus.read_long(menu.handle) == candidate)
.map(|menu| menu.handle)
})
.filter(|handle| looks_like_menu_handle(bus, *handle))
}
fn record_menuselect_input_trace(
&mut self,
action: &str,
start_pt: Option<(i16, i16)>,
menu_idx: Option<usize>,
highlighted_item: Option<i16>,
result: Option<u32>,
outcome: &str,
) {
if !self.input_trace_enabled {
return;
}
let start = start_pt
.map(|(v, h)| format!("({v},{h})"))
.unwrap_or_else(|| "none".to_string());
let highlighted = highlighted_item
.map(|item| item.to_string())
.unwrap_or_else(|| "none".to_string());
let result = result
.map(|value| format!("${value:08X}"))
.unwrap_or_else(|| "pending".to_string());
self.record_input_trace_line(format!(
"A93D action={} start={} live_mouse=({},{}) {} {} highlighted_item={} result={} outcome={}",
action,
start,
self.mouse_pos.0,
self.mouse_pos.1,
self.input_trace_state_fields(),
self.menu_trace_menu_fields(menu_idx),
highlighted,
result,
outcome,
));
}
pub(crate) fn is_popup_menu_proc_id(proc_id: i16) -> bool {
(1008..=1023).contains(&proc_id)
}
pub(crate) fn popup_menu_item_title(
&self,
bus: &MacMemoryBus,
menu_id: i16,
selected: usize,
) -> Option<String> {
if selected == 0 {
return None;
}
self.menus
.iter()
.rev()
.find(|menu| menu.id == menu_id)
.and_then(|menu| menu.items.get(selected - 1))
.map(|item| item.text.clone())
.or_else(|| {
let (_, menu_ptr) = self.find_resource_any(*b"MENU", menu_id)?;
Self::popup_menu_item_title_from_resource(bus, menu_ptr, selected)
})
}
fn popup_menu_item_title_from_resource(
bus: &MacMemoryBus,
menu_ptr: u32,
selected: usize,
) -> Option<String> {
let title_len = bus.read_byte(menu_ptr + 14) as u32;
let mut offset = menu_ptr + 15 + title_len;
let mut nth = 0usize;
loop {
let item_len = bus.read_byte(offset) as usize;
if item_len == 0 {
break;
}
nth += 1;
if nth == selected {
let bytes = bus.read_bytes(offset + 1, item_len);
return Some(String::from_utf8_lossy(&bytes).into_owned());
}
offset += 1 + item_len as u32 + 4;
}
None
}
fn ensure_menu_color_table_handle(&mut self, bus: &mut MacMemoryBus) -> u32 {
let current = bus.read_long(addr::MENU_C_INFO);
if current != 0 {
return current;
}
let handle = self.alloc_handle_with_bytes(bus, &[]);
if handle != 0 {
bus.write_long(addr::MENU_C_INFO, handle);
}
handle
}
fn menu_color_table_bytes(bus: &MacMemoryBus, handle: u32) -> Vec<u8> {
if handle == 0 {
return Vec::new();
}
let data_ptr = bus.read_long(handle);
if data_ptr == 0 {
return Vec::new();
}
let size = bus.get_alloc_size(data_ptr).unwrap_or(0);
if size == 0 {
Vec::new()
} else {
bus.read_bytes(data_ptr, size as usize)
}
}
pub(super) fn menu_color_entry_ptr(
bus: &MacMemoryBus,
menu_id: i16,
menu_item: i16,
) -> Option<u32> {
let handle = bus.read_long(addr::MENU_C_INFO);
if handle == 0 {
return None;
}
let data_ptr = bus.read_long(handle);
if data_ptr == 0 {
return None;
}
let size = bus.get_alloc_size(data_ptr).unwrap_or(0) as usize;
let mut offset = 0usize;
while offset + MC_ENTRY_SIZE <= size {
let entry_ptr = data_ptr + offset as u32;
if bus.read_word(entry_ptr) as i16 == menu_id
&& bus.read_word(entry_ptr + 2) as i16 == menu_item
{
return Some(entry_ptr);
}
offset += MC_ENTRY_SIZE;
}
None
}
pub(super) fn menu_color_entry_rgb(
bus: &MacMemoryBus,
entry_ptr: u32,
rgb_offset: u32,
) -> [u16; 3] {
[
bus.read_word(entry_ptr + rgb_offset),
bus.read_word(entry_ptr + rgb_offset + 2),
bus.read_word(entry_ptr + rgb_offset + 4),
]
}
fn menu_color_rgb_pixel_index(bus: &MacMemoryBus, rgb: [u16; 3]) -> Option<u8> {
Self::fb_pixel_index_for_rgb(bus, rgb)
}
pub(super) fn menu_bar_background_pixel_index(
&self,
bus: &MacMemoryBus,
pixel_size: u16,
) -> Option<u8> {
if pixel_size != 8 {
return None;
}
let rgb = if let Some(menu_bar) = Self::menu_color_entry_ptr(bus, 0, 0) {
Self::menu_color_entry_rgb(bus, menu_bar, 22)
} else if let Some(title) = self
.menus
.iter()
.filter(|menu| menu.visible_in_menu_bar)
.find_map(|menu| Self::menu_color_entry_ptr(bus, menu.id, 0))
{
Self::menu_color_entry_rgb(bus, title, 10)
} else {
return None;
};
Self::menu_color_rgb_pixel_index(bus, rgb)
}
pub(super) fn menu_title_pixel_index(
bus: &MacMemoryBus,
menu_id: i16,
pixel_size: u16,
) -> Option<u8> {
if pixel_size != 8 {
return None;
}
let rgb = if let Some(title) = Self::menu_color_entry_ptr(bus, menu_id, 0) {
Self::menu_color_entry_rgb(bus, title, 4)
} else if let Some(menu_bar) = Self::menu_color_entry_ptr(bus, 0, 0) {
Self::menu_color_entry_rgb(bus, menu_bar, 4)
} else {
return None;
};
Self::menu_color_rgb_pixel_index(bus, rgb)
}
fn menu_dropdown_background_pixel_index(
bus: &MacMemoryBus,
menu_id: i16,
pixel_size: u16,
) -> Option<u8> {
if pixel_size != 8 {
return None;
}
let rgb = if let Some(title) = Self::menu_color_entry_ptr(bus, menu_id, 0) {
Self::menu_color_entry_rgb(bus, title, 22)
} else if let Some(menu_bar) = Self::menu_color_entry_ptr(bus, 0, 0) {
Self::menu_color_entry_rgb(bus, menu_bar, 10)
} else {
return None;
};
Self::menu_color_rgb_pixel_index(bus, rgb)
}
fn menu_item_component_pixel_index(
bus: &MacMemoryBus,
menu_id: i16,
menu_item: i16,
pixel_size: u16,
item_rgb_offset: u32,
) -> Option<u8> {
if pixel_size != 8 {
return None;
}
let rgb = if let Some(item) = Self::menu_color_entry_ptr(bus, menu_id, menu_item) {
Self::menu_color_entry_rgb(bus, item, item_rgb_offset)
} else if let Some(title) = Self::menu_color_entry_ptr(bus, menu_id, 0) {
Self::menu_color_entry_rgb(bus, title, 16)
} else if let Some(menu_bar) = Self::menu_color_entry_ptr(bus, 0, 0) {
Self::menu_color_entry_rgb(bus, menu_bar, 16)
} else {
return None;
};
Self::menu_color_rgb_pixel_index(bus, rgb)
}
fn menu_hilite_pixel_indexes(
&self,
bus: &MacMemoryBus,
background_index: Option<u8>,
pixel_size: u16,
) -> Option<(u8, u8)> {
if pixel_size != 8 {
return None;
}
let background =
background_index.or_else(|| Self::menu_color_rgb_pixel_index(bus, [0xFFFF; 3]))?;
let hilite = Self::menu_color_rgb_pixel_index(
bus,
[
self.hilite_color.0,
self.hilite_color.1,
self.hilite_color.2,
],
)?;
Some((background, hilite))
}
fn menu_hilited_pixel_index(pixel: u8, background: u8, hilite: u8) -> u8 {
if pixel == background {
hilite
} else if pixel == hilite {
background
} else {
pixel
}
}
fn menu_plain_hilited_pixel_index(bus: &MacMemoryBus, pixel: u8) -> u8 {
let white = Self::fb_pixel_index_for_rgb(bus, [0xFFFF; 3]).unwrap_or(0);
let black = Self::fb_pixel_index_for_rgb(bus, [0; 3]).unwrap_or(255);
if pixel == white {
black
} else if pixel == black {
white
} else {
pixel
}
}
fn alloc_handle_with_bytes(&mut self, bus: &mut MacMemoryBus, bytes: &[u8]) -> u32 {
let handle = bus.alloc(4);
if handle == 0 {
return 0;
}
if bytes.is_empty() {
bus.write_long(handle, 0);
return handle;
}
let data_ptr = bus.alloc(bytes.len() as u32);
if data_ptr == 0 {
bus.free(handle);
return 0;
}
bus.write_bytes(data_ptr, bytes);
bus.write_long(handle, data_ptr);
self.ptr_to_handle.insert(data_ptr, handle);
handle
}
pub(crate) fn create_popup_menu_handle(&mut self, bus: &mut MacMemoryBus, menu_id: i16) -> u32 {
if menu_id <= 0 {
return 0;
}
if let Some(menu) = self
.menus
.iter()
.rev()
.find(|menu| menu.id == menu_id && menu.handle != 0)
{
return menu.handle;
}
let menu_ptr = bus.alloc(256);
let handle = bus.alloc(4);
if menu_ptr == 0 || handle == 0 {
return 0;
}
bus.write_long(handle, menu_ptr);
self.ptr_to_handle.insert(menu_ptr, handle);
let menu = if let Some((_, res_ptr)) = self.find_resource_any(*b"MENU", menu_id) {
let res_size = menu_resource_size(bus, res_ptr);
for i in 0..res_size.min(256) {
bus.write_byte(menu_ptr + i as u32, bus.read_byte(res_ptr + i as u32));
}
parse_menu_resource(bus, menu_ptr, handle)
} else {
bus.write_word(menu_ptr, menu_id as u16);
bus.write_word(menu_ptr + 2, 0);
bus.write_word(menu_ptr + 4, 0);
bus.write_long(menu_ptr + 6, 0);
bus.write_long(menu_ptr + 10, 0xFFFF_FFFF);
bus.write_byte(menu_ptr + 14, 0);
bus.write_byte(menu_ptr + 15, 0);
Menu {
id: menu_id,
title: String::new(),
items: Vec::new(),
enabled: true,
handle,
in_menu_bar: false,
hierarchical: false,
visible_in_menu_bar: false,
}
};
self.menus.push(menu);
self.load_menu_color_resource(bus, menu_id);
handle
}
fn clone_menu_color_handle(&mut self, bus: &mut MacMemoryBus, handle: u32) -> u32 {
if handle == 0 {
return 0;
}
let bytes = Self::menu_color_table_bytes(bus, handle);
self.alloc_handle_with_bytes(bus, &bytes)
}
fn merge_menu_color_entries(&mut self, bus: &mut MacMemoryBus, entries: &[u8]) {
if entries.is_empty() {
return;
}
let current_handle = self.ensure_menu_color_table_handle(bus);
if current_handle == 0 {
return;
}
let current_bytes = Self::menu_color_table_bytes(bus, current_handle);
let mut new_bytes = current_bytes.clone();
for entry in entries.chunks_exact(MC_ENTRY_SIZE) {
let Some((menu_id, menu_item)) = mc_entry_key(entry) else {
continue;
};
let mut found_offset = None;
for (entry_index, existing) in new_bytes.chunks_exact(MC_ENTRY_SIZE).enumerate() {
if mc_entry_matches(existing, menu_id, menu_item) {
found_offset = Some(entry_index * MC_ENTRY_SIZE);
break;
}
}
if let Some(offset) = found_offset {
new_bytes[offset..offset + MC_ENTRY_SIZE].copy_from_slice(entry);
} else {
new_bytes.extend_from_slice(entry);
}
}
let _ = self.replace_handle_bytes(bus, current_handle, &new_bytes);
}
fn load_menu_color_resource(&mut self, bus: &mut MacMemoryBus, resource_id: i16) {
let Some((_, resource_ptr)) = self.find_resource_any(*b"mctb", resource_id) else {
return;
};
let resource_size = bus.get_alloc_size(resource_ptr).unwrap_or(0) as usize;
if resource_size < 2 {
return;
}
let declared_count = bus.read_word(resource_ptr) as i16;
if declared_count <= 0 {
return;
}
let available_entries = (resource_size - 2) / MC_RESOURCE_ENTRY_SIZE;
let entry_count = (declared_count as usize).min(available_entries);
let mut entries = Vec::with_capacity(entry_count * MC_ENTRY_SIZE);
for index in 0..entry_count {
let entry_ptr = resource_ptr + 2 + (index * MC_RESOURCE_ENTRY_SIZE) as u32;
let menu_id = bus.read_word(entry_ptr) as i16;
if menu_id == MC_LAST_ID_INDIC {
continue;
}
entries.extend_from_slice(&bus.read_bytes(entry_ptr, MC_RESOURCE_ENTRY_SIZE));
entries.extend_from_slice(&0u16.to_be_bytes());
}
self.merge_menu_color_entries(bus, &entries);
}
fn filter_menu_color_table_entries<F>(&mut self, bus: &mut MacMemoryBus, mut keep: F)
where
F: FnMut(i16, i16) -> bool,
{
let current_handle = bus.read_long(addr::MENU_C_INFO);
if current_handle == 0 {
return;
}
let current_bytes = Self::menu_color_table_bytes(bus, current_handle);
if current_bytes.is_empty() {
return;
}
let mut filtered = Vec::with_capacity(current_bytes.len());
for entry in current_bytes.chunks_exact(MC_ENTRY_SIZE) {
if let Some((menu_id, menu_item)) = mc_entry_key(entry) {
if keep(menu_id, menu_item) {
filtered.extend_from_slice(entry);
}
}
}
let _ = self.replace_handle_bytes(bus, current_handle, &filtered);
}
fn clear_menu_color_table_entries(&mut self, bus: &mut MacMemoryBus) {
let current_handle = bus.read_long(addr::MENU_C_INFO);
if current_handle != 0 {
let _ = self.replace_handle_bytes(bus, current_handle, &[]);
}
}
fn replace_handle_bytes(&mut self, bus: &mut MacMemoryBus, handle: u32, bytes: &[u8]) -> bool {
if handle == 0 {
return false;
}
let old_ptr = bus.read_long(handle);
let new_ptr = if bytes.is_empty() {
0
} else {
let new_ptr = bus.alloc(bytes.len() as u32);
if new_ptr == 0 {
return false;
}
bus.write_bytes(new_ptr, bytes);
new_ptr
};
if old_ptr != 0 && old_ptr != new_ptr {
self.ptr_to_handle.remove(&old_ptr);
bus.free(old_ptr);
}
bus.write_long(handle, new_ptr);
if new_ptr != 0 {
self.ptr_to_handle.insert(new_ptr, handle);
}
true
}
fn ensure_menu_record_capacity(
&mut self,
bus: &mut MacMemoryBus,
menu_handle: u32,
min_size: u32,
) -> u32 {
if menu_handle == 0 {
return 0;
}
let menu_ptr = bus.read_long(menu_handle);
if menu_ptr == 0 {
return 0;
}
let current_size = bus
.get_alloc_size(menu_ptr)
.unwrap_or_else(|| menu_resource_size(bus, menu_ptr) as u32);
if current_size >= min_size {
return menu_ptr;
}
self.resize_resource_allocation(bus, menu_handle, menu_ptr, min_size)
}
pub(crate) fn dispatch_menu<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, 0x130) => {
let _ = self.ensure_menu_color_table_handle(bus);
self.load_menu_color_resource(bus, 0);
Ok(())
}
(true, 0x131) => {
let sp = cpu.read_reg(Register::A7);
let title_ptr = bus.read_long(sp);
let menu_id = bus.read_word(sp + 4) as i16;
let menu_ptr = bus.alloc(256);
let handle = bus.alloc(4);
bus.write_long(handle, menu_ptr);
self.ptr_to_handle.insert(menu_ptr, handle);
bus.write_word(menu_ptr, menu_id as u16);
bus.write_word(menu_ptr + 2, 0);
bus.write_word(menu_ptr + 4, 0);
bus.write_long(menu_ptr + 6, 0);
bus.write_long(menu_ptr + 10, 0xFFFFFFFF); let mut title = String::new();
if title_ptr != 0 {
let title_len = bus.read_byte(title_ptr) as u32;
bus.write_byte(menu_ptr + 14, title_len as u8);
let mut title_bytes = Vec::with_capacity(title_len as usize);
for i in 0..title_len {
let b = bus.read_byte(title_ptr + 1 + i);
bus.write_byte(menu_ptr + 15 + i, b);
title_bytes.push(b);
}
bus.write_byte(menu_ptr + 15 + title_len, 0);
title = String::from_utf8_lossy(&title_bytes).into_owned();
}
if !self.menus.iter().any(|m| m.handle == handle) {
self.menus.push(Menu {
id: menu_id,
title,
items: Vec::new(),
enabled: true,
handle,
in_menu_bar: false,
hierarchical: false,
visible_in_menu_bar: false,
});
}
bus.write_long(sp + 6, handle);
cpu.write_reg(Register::A7, sp + 6);
Ok(())
}
(true, 0x1BF) => {
let sp = cpu.read_reg(Register::A7);
let menu_id = bus.read_word(sp) as i16;
let handle = match self.find_or_load_resource_any(bus, *b"MENU", menu_id) {
Some((refnum, res_ptr)) => {
let handle = self.get_or_create_resource_handle_in_file(
bus, *b"MENU", menu_id, res_ptr, refnum,
);
if handle != 0 {
let mut menu_ptr = bus.read_long(handle);
if menu_ptr == 0 {
menu_ptr = res_ptr;
bus.write_long(handle, menu_ptr);
self.ptr_to_handle.insert(menu_ptr, handle);
}
if menu_ptr != 0 {
if bus.get_alloc_size(menu_ptr).unwrap_or(0) < 256 {
let resized =
self.resize_resource_allocation(bus, handle, menu_ptr, 256);
if resized != 0 {
menu_ptr = resized;
}
}
let mut parsed = parse_menu_resource(bus, menu_ptr, handle);
if let Some(existing) =
self.menus.iter_mut().find(|m| m.handle == handle)
{
parsed.in_menu_bar = existing.in_menu_bar;
parsed.hierarchical = existing.hierarchical;
parsed.visible_in_menu_bar = existing.visible_in_menu_bar;
*existing = parsed;
} else {
self.menus.push(parsed);
}
}
}
self.load_menu_color_resource(bus, menu_id);
bus.write_word(0x0A60, 0);
cpu.write_reg(Register::D0, 0);
handle
}
None => {
bus.write_word(0x0A60, (-192i16) as u16);
cpu.write_reg(Register::D0, -192i32 as u32);
0
}
};
bus.write_long(sp + 2, handle);
cpu.write_reg(Register::A7, sp + 2);
Ok(())
}
(true, 0x133) => {
let sp = cpu.read_reg(Register::A7);
let text_ptr = bus.read_long(sp);
let mut menu_handle = bus.read_long(sp + 4);
cpu.write_reg(Register::A7, sp + 8);
if menu_handle == 0 {
let a0_handle = cpu.read_reg(Register::A0);
if let Some(resolved) = self.resolve_menu_handle_candidate(bus, a0_handle) {
menu_handle = resolved;
}
}
if menu_handle == 0 || text_ptr == 0 {
return Some(Ok(()));
}
let len = bus.read_byte(text_ptr) as usize;
let mut bytes = Vec::with_capacity(len);
for i in 0..len {
bytes.push(bus.read_byte(text_ptr + 1 + i as u32));
}
let parsed = parse_appendmenu_items(&bytes);
if !self.menus.iter().any(|m| m.handle == menu_handle) {
let menu_ptr = bus.read_long(menu_handle);
if menu_ptr != 0 {
self.menus
.push(parse_menu_resource(bus, menu_ptr, menu_handle));
}
}
self.ensure_menu_record_capacity(bus, menu_handle, 256);
if let Some(menu) = self.menus.iter_mut().find(|m| m.handle == menu_handle) {
for item in parsed {
menu.items.push(item);
}
sync_enable_flags(bus, menu);
let menu_copy = menu.clone();
serialise_menu_items_to_memory(bus, &menu_copy);
}
Ok(())
}
(true, 0x135) => {
let sp = cpu.read_reg(Register::A7);
let before_id = bus.read_word(sp) as i16;
let menu_handle = bus.read_long(sp + 2);
let visible_in_menu_bar = before_id != -1;
if menu_handle != 0 {
let menu_ptr = bus.read_long(menu_handle);
if menu_ptr != 0 {
if let Some(idx) = self.menus.iter().position(|m| m.handle == menu_handle) {
self.last_inserted_menu_id = Some(self.menus[idx].id);
self.menus[idx].in_menu_bar = true;
self.menus[idx].hierarchical = before_id == -1;
self.menus[idx].visible_in_menu_bar = visible_in_menu_bar;
} else {
let menu_id = bus.read_word(menu_ptr) as i16;
if let Some((_, res_ptr)) = self.find_resource_any(*b"MENU", menu_id) {
let mut menu = parse_menu_resource(bus, res_ptr, menu_handle);
eprintln!(
"[MENU] InsertMenu: ID={} title=\"{}\" items={}",
menu.id,
menu.title,
menu.items.len()
);
menu.in_menu_bar = true;
menu.hierarchical = before_id == -1;
menu.visible_in_menu_bar = visible_in_menu_bar;
self.last_inserted_menu_id = Some(menu.id);
self.menus.push(menu);
} else {
let title_len = bus.read_byte(menu_ptr + 14) as usize;
if title_len > 0 && title_len < 64 {
let mut title_bytes = Vec::with_capacity(title_len);
for i in 0..title_len {
title_bytes.push(bus.read_byte(menu_ptr + 15 + i as u32));
}
let title = String::from_utf8_lossy(&title_bytes).into_owned();
if !title.is_empty() {
eprintln!(
"[MENU] InsertMenu: title=\"{}\" (no resource)",
title
);
self.last_inserted_menu_id = Some(menu_id);
self.menus.push(Menu {
id: menu_id,
title,
items: Vec::new(),
enabled: true,
handle: menu_handle,
in_menu_bar: true,
hierarchical: before_id == -1,
visible_in_menu_bar,
});
}
}
}
}
}
}
cpu.write_reg(Register::A7, sp + 6);
Ok(())
}
(true, 0x137) => {
self.draw_menu_bar_to_fb(bus);
Ok(())
}
(true, 0x134) => {
self.menus.clear();
self.clear_menu_color_table_entries(bus);
Ok(())
}
(true, 0x149) => {
let sp = cpu.read_reg(Register::A7);
let menu_id = bus.read_word(sp) as i16;
let handle = self
.menus
.iter()
.find(|m| m.id == menu_id && m.in_menu_bar)
.map(|m| m.handle)
.unwrap_or(0);
bus.write_long(sp + 2, handle);
cpu.write_reg(Register::A7, sp + 2);
Ok(())
}
(true, 0x13C) => {
let sp = cpu.read_reg(Register::A7);
let menu_list = bus.read_long(sp);
cpu.write_reg(Register::A7, sp + 4);
if let Some(saved) = self.saved_menu_bars.get(&menu_list).cloned() {
self.menus = saved;
}
Ok(())
}
(true, 0x1C0) => {
let sp = cpu.read_reg(Register::A7);
let mbar_id = bus.read_word(sp) as i16;
let handle = if let Some((_, mbar_ptr)) = self.find_resource_any(*b"MBAR", mbar_id)
{
let menu_count = bus.read_word(mbar_ptr) as usize;
let mut snapshot = Vec::new();
self.clear_menu_color_table_entries(bus);
for i in 0..menu_count {
let menu_id = bus.read_word(mbar_ptr + 2 + (i as u32) * 2) as i16;
let Some((_, menu_res_ptr)) = self.find_resource_any(*b"MENU", menu_id)
else {
continue;
};
let menu_ptr = bus.alloc(256);
let menu_handle = bus.alloc(4);
bus.write_long(menu_handle, menu_ptr);
let res_size = menu_resource_size(bus, menu_res_ptr);
for j in 0..res_size.min(256) {
bus.write_byte(
menu_ptr + j as u32,
bus.read_byte(menu_res_ptr + j as u32),
);
}
let mut parsed = parse_menu_resource(bus, menu_ptr, menu_handle);
parsed.in_menu_bar = true;
parsed.hierarchical = false;
parsed.visible_in_menu_bar = true;
snapshot.push(parsed);
self.load_menu_color_resource(bus, menu_id);
}
let count = snapshot.len() as u32;
let list_block = bus.alloc(2 + 4 * count.max(1));
bus.write_word(list_block, count as u16);
for (idx, menu) in snapshot.iter().enumerate() {
bus.write_long(list_block + 2 + (idx as u32) * 4, menu.handle);
}
let list_handle = bus.alloc(4);
bus.write_long(list_handle, list_block);
self.saved_menu_bars.insert(list_handle, snapshot);
list_handle
} else {
0
};
bus.write_long(sp + 2, handle);
cpu.write_reg(Register::A7, sp + 2);
Ok(())
}
(true, 0x14D) => {
let sp = cpu.read_reg(Register::A7);
let res_type_word = bus.read_long(sp);
let menu_handle = bus.read_long(sp + 4);
cpu.write_reg(Register::A7, sp + 8);
let res_type = res_type_word.to_be_bytes();
let entries = self.named_resources_of_type(res_type);
let mut touched: Option<Menu> = None;
if let Some(menu) = self.menus.iter_mut().find(|m| m.handle == menu_handle) {
for (_id, name) in entries {
if name.is_empty() || name.starts_with('.') || name.starts_with('%') {
continue;
}
if menu.items.iter().any(|it| it.text == name) {
continue;
}
menu.items.push(MenuItem {
text: name,
icon: 0,
key_equiv: 0,
mark: 0,
style: 0,
enabled: true,
});
}
sync_enable_flags(bus, menu);
touched = Some(menu.clone());
}
if let Some(m) = touched {
serialise_menu_items_to_memory(bus, &m);
}
Ok(())
}
(true, 0x13A) => {
let sp = cpu.read_reg(Register::A7);
let item = bus.read_word(sp) as i16;
let menu_handle = bus.read_long(sp + 2);
cpu.write_reg(Register::A7, sp + 6);
if let Some(menu) = self.menus.iter_mut().find(|m| m.handle == menu_handle) {
if item == 0 {
menu.enabled = false;
} else if (1..=31).contains(&item) {
if let Some(mi) = menu.items.get_mut((item - 1) as usize) {
mi.enabled = false;
}
}
if std::env::var_os("SYSTEMLESS_TRACE_MENUKEY").is_some() {
eprintln!(
"[MENUKEY] DisableItem menu={} title=\"{}\" item={} enabled={}",
menu.id, menu.title, item, menu.enabled
);
}
sync_enable_flags(bus, menu);
}
Ok(())
}
(true, 0x139) => {
let sp = cpu.read_reg(Register::A7);
let item = bus.read_word(sp) as i16;
let menu_handle = bus.read_long(sp + 2);
cpu.write_reg(Register::A7, sp + 6);
if let Some(menu) = self.menus.iter_mut().find(|m| m.handle == menu_handle) {
if item == 0 {
menu.enabled = true;
} else if (1..=31).contains(&item) {
if let Some(mi) = menu.items.get_mut((item - 1) as usize) {
mi.enabled = true;
}
}
if std::env::var_os("SYSTEMLESS_TRACE_MENUKEY").is_some() {
eprintln!(
"[MENUKEY] EnableItem menu={} title=\"{}\" item={} enabled={}",
menu.id, menu.title, item, menu.enabled
);
}
sync_enable_flags(bus, menu);
}
Ok(())
}
(true, 0x13D) => {
if let Some(ref tracking) = self.menu_tracking {
if tracking.flash_remaining > 0 {
let result = tracking.flash_result;
let t = self.menu_tracking.as_mut().unwrap();
if t.flash_delay > 0 {
t.flash_delay -= 1;
return Some(Ok(()));
}
t.flash_remaining -= 1;
t.flash_delay = 3; let new_remaining = t.flash_remaining;
if new_remaining == 0 {
let sp = self.menu_tracking.as_ref().unwrap().stack_ptr;
let saved = self.menu_tracking.take().unwrap();
let active_menu = saved
.submenu
.as_ref()
.map(|submenu| submenu.menu)
.unwrap_or(saved.active_menu);
let highlighted_item = saved
.submenu
.as_ref()
.map(|submenu| submenu.highlighted_item)
.unwrap_or(saved.highlighted_item);
self.restore_menu_tracking_pixels(bus, saved);
self.draw_menu_bar_to_fb(bus);
bus.write_long(sp + 4, result);
cpu.write_reg(Register::A7, sp + 4);
self.record_menuselect_input_trace(
"finish",
None,
Some(active_menu),
Some(highlighted_item),
Some(result),
"enabled_item_selected",
);
}
} else if !self.menu_tracking_button_down(bus) {
let result = self.menu_tracking_selection_result();
if result != 0 {
let (active_menu, item_idx) = self
.menu_tracking
.as_ref()
.and_then(|tracking| {
tracking
.submenu
.as_ref()
.filter(|submenu| submenu.highlighted_item > 0)
.map(|submenu| (submenu.menu, submenu.highlighted_item))
.or_else(|| {
(tracking.highlighted_item > 0).then_some((
tracking.active_menu,
tracking.highlighted_item,
))
})
})
.unwrap_or((0, 0));
let tracking = self.menu_tracking.as_mut().unwrap();
tracking.flash_remaining = 6;
tracking.flash_delay = 3;
tracking.flash_result = result;
self.record_menuselect_input_trace(
"release",
None,
Some(active_menu),
Some(item_idx),
Some(result),
"start_flash",
);
} else {
let sp = tracking.stack_ptr;
let active_menu = tracking.active_menu;
let saved = self.menu_tracking.take().unwrap();
self.restore_menu_tracking_pixels(bus, saved);
self.draw_menu_bar_to_fb(bus);
self.finish_menu_no_hit(bus, cpu, sp, 4);
self.record_menuselect_input_trace(
"release",
None,
Some(active_menu),
Some(0),
Some(0),
"no_selection",
);
}
} else {
let (mv, mh) = self.menu_tracking_mouse_pos(bus);
let new_menu = self.menu_title_hit_test(mh);
let tracking = self.menu_tracking.as_ref().unwrap();
let mbar_h =
bus.read_word(crate::memory::globals::addr::MBAR_HEIGHT) as i16;
if let Some(new_idx) = new_menu {
if new_idx != tracking.active_menu && mv < mbar_h {
let old_saved = self.menu_tracking.take().unwrap();
let sp = old_saved.stack_ptr;
self.restore_menu_tracking_pixels(bus, old_saved);
self.open_menu_dropdown(bus, new_idx, sp);
self.record_menuselect_input_trace(
"tracking_switch",
None,
Some(new_idx),
Some(0),
None,
"menu_title_changed",
);
return Some(Ok(()));
}
}
let old_trace = self.menu_tracking.as_ref().map(|tracking| {
tracking
.submenu
.as_ref()
.map(|submenu| (submenu.menu, submenu.highlighted_item))
.unwrap_or((tracking.active_menu, tracking.highlighted_item))
});
self.update_menu_tracking_for_point(bus, mh, mv);
let new_trace = self.menu_tracking.as_ref().map(|tracking| {
tracking
.submenu
.as_ref()
.map(|submenu| (submenu.menu, submenu.highlighted_item))
.unwrap_or((tracking.active_menu, tracking.highlighted_item))
});
if new_trace != old_trace {
let (active_menu, highlighted_item) = new_trace.unwrap_or((0, 0));
self.record_menuselect_input_trace(
"tracking_update",
None,
Some(active_menu),
Some(highlighted_item),
None,
if highlighted_item > 0 {
"enabled_item_highlighted"
} else {
"no_enabled_item"
},
);
}
}
} else {
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;
if let Some(menu_idx) = self.menu_title_hit_test(pt_h) {
self.record_menuselect_input_trace(
"start",
Some((pt_v, pt_h)),
Some(menu_idx),
Some(0),
None,
"open_tracking",
);
self.open_menu_dropdown(bus, menu_idx, sp);
self.record_menuselect_input_trace(
"tracking_entered",
Some((pt_v, pt_h)),
Some(menu_idx),
Some(0),
None,
"menu_title_active",
);
} else {
self.record_menuselect_input_trace(
"start",
Some((pt_v, pt_h)),
None,
None,
Some(0),
"no_menu_title",
);
self.finish_menu_no_hit(bus, cpu, sp, 4);
}
}
Ok(())
}
(true, 0x00B) => {
if let Some(ref tracking) = self.menu_tracking {
if tracking.flash_remaining > 0 {
let result = tracking.flash_result;
let t = self.menu_tracking.as_mut().unwrap();
if t.flash_delay > 0 {
t.flash_delay -= 1;
return Some(Ok(()));
}
t.flash_remaining -= 1;
t.flash_delay = 3;
let new_remaining = t.flash_remaining;
if new_remaining == 0 {
let sp = self.menu_tracking.as_ref().unwrap().stack_ptr;
let saved = self.menu_tracking.take().unwrap();
self.restore_menu_tracking_pixels(bus, saved);
self.restore_visible_dialog_snapshots(bus);
bus.write_long(sp + 10, result);
cpu.write_reg(Register::A7, sp + 10);
}
} else if !self.menu_tracking_button_down(bus) {
let result = self.menu_tracking_selection_result();
if result != 0 {
let tracking = self.menu_tracking.as_mut().unwrap();
tracking.flash_remaining = 6;
tracking.flash_delay = 3;
tracking.flash_result = result;
} else {
let sp = tracking.stack_ptr;
let saved = self.menu_tracking.take().unwrap();
self.restore_menu_tracking_pixels(bus, saved);
self.restore_visible_dialog_snapshots(bus);
self.finish_menu_no_hit(bus, cpu, sp, 10);
}
} else {
let (mv, mh) = self.menu_tracking_mouse_pos(bus);
self.update_menu_tracking_for_point(bus, mh, mv);
}
} else {
let sp = cpu.read_reg(Register::A7);
let popup_item = bus.read_word(sp) as i16;
let left = bus.read_word(sp + 2) as i16;
let top = bus.read_word(sp + 4) as i16;
let menu_handle = bus.read_long(sp + 6);
let menu_ptr = bus.read_long(menu_handle);
if menu_ptr == 0 {
self.finish_menu_no_hit(bus, cpu, sp, 10);
return Some(Ok(()));
}
let menu_id = bus.read_word(menu_ptr) as i16;
if let Some(menu_idx) = self
.menus
.iter()
.position(|m| m.id == menu_id && m.in_menu_bar)
{
let (dd_rect, highlighted_item) =
self.popup_menu_dropdown_rect(bus, menu_idx, top, left, popup_item);
self.restore_visible_dialog_snapshots(bus);
let saved = self.save_dropdown_pixels(bus, dd_rect);
self.menu_tracking = Some(MenuTrackingState {
active_menu: menu_idx,
highlighted_item: 0,
saved_pixels: saved,
dropdown_rect: dd_rect,
submenu: None,
stack_ptr: sp,
flash_remaining: 0,
flash_delay: 0,
flash_result: 0,
});
self.draw_menu_dropdown(bus, menu_idx, dd_rect);
if highlighted_item > 0 {
self.set_menu_tracking_highlight(bus, highlighted_item);
}
} else {
self.finish_menu_no_hit(bus, cpu, sp, 10);
}
}
Ok(())
}
(true, 0x138) => {
let sp = cpu.read_reg(Register::A7);
let _menu_id = bus.read_word(sp) as i16;
cpu.write_reg(Register::A7, sp + 2);
if let Some(tracking) = self.menu_tracking.take() {
self.restore_dropdown_pixels(
bus,
tracking.dropdown_rect,
&tracking.saved_pixels,
);
}
if !self.menus.is_empty() {
self.draw_menu_bar_to_fb(bus);
}
Ok(())
}
(true, 0x13E) => {
let sp = cpu.read_reg(Register::A7);
let ch = (bus.read_word(sp) & 0xFF) as u8;
let mut result: u32 = 0;
let mut matched_menu_idx: Option<usize> = None;
let ch_upper = (ch as char).to_ascii_uppercase() as u8;
for (menu_idx, menu) in self.menus.iter().enumerate().rev() {
if !menu.visible_in_menu_bar || !menu.enabled {
continue;
}
for (i, item) in menu.items.iter().enumerate() {
if item.enabled
&& Self::menu_item_has_command_key(item)
&& (item.key_equiv as char).to_ascii_uppercase() as u8 == ch_upper
{
result = ((menu.id as u32) << 16) | ((i + 1) as u32);
matched_menu_idx = Some(menu_idx);
break;
}
}
if result != 0 {
break;
}
}
if let Some(menu_idx) = matched_menu_idx {
self.highlight_menu_title(bus, menu_idx);
}
if std::env::var_os("SYSTEMLESS_TRACE_MENUKEY").is_some() {
eprintln!(
"[MENUKEY] MenuKey ch=${:02X} '{}' -> ${:08X}",
ch,
if ch.is_ascii_graphic() {
ch as char
} else {
'.'
},
result
);
for menu in &self.menus {
eprintln!(
"[MENUKEY] menu {} \"{}\" in_bar={} visible={} enabled={} items={}",
menu.id,
menu.title,
menu.in_menu_bar,
menu.visible_in_menu_bar,
menu.enabled,
menu.items.len()
);
}
}
bus.write_long(sp + 2, result);
cpu.write_reg(Register::A7, sp + 2);
Ok(())
}
(true, 0x145) => {
let sp = cpu.read_reg(Register::A7);
let checked = bus.read_byte(sp) != 0;
let item = bus.read_word(sp + 2) as i16;
let menu_handle = bus.read_long(sp + 4);
cpu.write_reg(Register::A7, sp + 8);
if let Some(menu) = self.menus.iter_mut().find(|m| m.handle == menu_handle) {
if let Some(mi) = menu.items.get_mut((item - 1) as usize) {
mi.mark = if checked { 0x12 } else { 0 }; }
}
Ok(())
}
(true, 0x147) => {
let sp = cpu.read_reg(Register::A7);
let text_ptr = bus.read_long(sp);
let item = bus.read_word(sp + 4) as i16;
let mut menu_handle = bus.read_long(sp + 6);
cpu.write_reg(Register::A7, sp + 10);
if menu_handle == 0 {
let a0_handle = cpu.read_reg(Register::A0);
if let Some(resolved) = self.resolve_menu_handle_candidate(bus, a0_handle) {
menu_handle = resolved;
}
}
if text_ptr != 0 && item >= 1 {
let text_len = bus.read_byte(text_ptr) as usize;
let mut text_bytes = Vec::with_capacity(text_len);
for i in 0..text_len {
text_bytes.push(bus.read_byte(text_ptr + 1 + i as u32));
}
let text = String::from_utf8_lossy(&text_bytes).into_owned();
if !self.menus.iter().any(|m| m.handle == menu_handle) {
let menu_ptr = bus.read_long(menu_handle);
if menu_ptr != 0 {
self.menus
.push(parse_menu_resource(bus, menu_ptr, menu_handle));
}
}
let mut touched: Option<Menu> = None;
if let Some(menu) = self.menus.iter_mut().find(|m| m.handle == menu_handle) {
let idx = (item - 1) as usize;
if idx < menu.items.len() {
menu.items[idx].text = text;
touched = Some(menu.clone());
}
}
if let Some(m) = touched {
serialise_menu_items_to_memory(bus, &m);
}
}
Ok(())
}
(true, 0x132) => {
let sp = cpu.read_reg(Register::A7);
let menu_handle = bus.read_long(sp);
cpu.write_reg(Register::A7, sp + 4);
self.menus.retain(|m| m.handle != menu_handle);
if menu_handle != 0 {
let menu_ptr = bus.read_long(menu_handle);
if menu_ptr != 0 {
bus.free(menu_ptr);
self.ptr_to_handle.remove(&menu_ptr);
}
self.forget_resource_handle_index_for_handle(menu_handle);
self.loaded_handles.remove(&menu_handle);
self.detached_handles.remove(&menu_handle);
self.resource_handle_files.remove(&menu_handle);
self.detached_handle_files.remove(&menu_handle);
self.handle_state_bits.remove(&menu_handle);
bus.free(menu_handle);
}
Ok(())
}
(true, 0x136) => {
let sp = cpu.read_reg(Register::A7);
let menu_id = bus.read_word(sp) as i16;
cpu.write_reg(Register::A7, sp + 2);
self.menus.retain(|m| m.id != menu_id);
self.filter_menu_color_table_entries(bus, |id, _item| id != menu_id);
Ok(())
}
(true, 0x150) => {
let sp = cpu.read_reg(Register::A7);
let menu_handle = bus.read_long(sp);
let count = count_menu_items_from_memory(bus, menu_handle);
bus.write_word(sp + 4, count);
cpu.write_reg(Register::A7, sp + 4);
Ok(())
}
(true, 0x04E) => {
let sp = cpu.read_reg(Register::A7);
let cmd_ptr = bus.read_long(sp);
let item = bus.read_word(sp + 4) as i16;
let menu_handle = bus.read_long(sp + 6);
if cmd_ptr != 0 {
let cmd_char = if menu_handle != 0 {
let menu_ptr = bus.read_long(menu_handle);
if menu_ptr != 0 {
get_menu_item_field(bus, menu_ptr, item, 1)
} else {
0
}
} else {
0
};
bus.write_word(cmd_ptr, cmd_char as u16);
}
cpu.write_reg(Register::A7, sp + 10);
Ok(())
}
(true, 0x13F) => {
let sp = cpu.read_reg(Register::A7);
let icon_ptr = bus.read_long(sp);
let item = bus.read_word(sp + 4) as i16;
let menu_handle = bus.read_long(sp + 6);
if icon_ptr != 0 {
let icon = self
.menus
.iter()
.find(|m| m.handle == menu_handle)
.and_then(|m| m.items.get((item - 1) as usize))
.map(|mi| mi.icon)
.unwrap_or(0);
bus.write_word(icon_ptr, icon as u16);
}
cpu.write_reg(Register::A7, sp + 10);
Ok(())
}
(true, 0x140) => {
let sp = cpu.read_reg(Register::A7);
let icon = (bus.read_word(sp) & 0xFF) as u8;
let item = bus.read_word(sp + 2) as i16;
let menu_handle = bus.read_long(sp + 4);
if let Some(menu) = self.menus.iter_mut().find(|m| m.handle == menu_handle) {
if let Some(mi) = menu.items.get_mut((item - 1) as usize) {
mi.icon = icon;
}
}
cpu.write_reg(Register::A7, sp + 8);
Ok(())
}
(true, 0x141) => {
let sp = cpu.read_reg(Register::A7);
let style_ptr = bus.read_long(sp);
let item = bus.read_word(sp + 4) as i16;
let menu_handle = bus.read_long(sp + 6);
if style_ptr != 0 {
let style = self
.menus
.iter()
.find(|m| m.handle == menu_handle)
.and_then(|m| m.items.get((item - 1) as usize))
.map(|mi| mi.style)
.unwrap_or(0);
bus.write_word(style_ptr, style as u16);
}
cpu.write_reg(Register::A7, sp + 10);
Ok(())
}
(true, 0x142) => {
let sp = cpu.read_reg(Register::A7);
let style = (bus.read_word(sp) & 0xFF) as u8;
let item = bus.read_word(sp + 2) as i16;
let menu_handle = bus.read_long(sp + 4);
if let Some(menu) = self.menus.iter_mut().find(|m| m.handle == menu_handle) {
if let Some(mi) = menu.items.get_mut((item - 1) as usize) {
mi.style = style;
}
}
cpu.write_reg(Register::A7, sp + 8);
Ok(())
}
(true, 0x143) => {
let sp = cpu.read_reg(Register::A7);
let mark_ptr = bus.read_long(sp);
let item = bus.read_word(sp + 4) as i16;
let menu_handle = bus.read_long(sp + 6);
if mark_ptr != 0 {
let mark = self
.menus
.iter()
.find(|m| m.handle == menu_handle)
.and_then(|m| m.items.get((item - 1) as usize))
.map(|mi| mi.mark)
.unwrap_or(0);
bus.write_word(mark_ptr, mark as u16);
}
cpu.write_reg(Register::A7, sp + 10);
Ok(())
}
(true, 0x144) => {
let sp = cpu.read_reg(Register::A7);
let mark_char = (bus.read_word(sp) & 0xFF) as u8;
let item = bus.read_word(sp + 2) as i16;
let menu_handle = bus.read_long(sp + 4);
if let Some(menu) = self.menus.iter_mut().find(|m| m.handle == menu_handle) {
if let Some(mi) = menu.items.get_mut((item - 1) as usize) {
mi.mark = mark_char;
}
}
cpu.write_reg(Register::A7, sp + 8);
Ok(())
}
(true, 0x146) => {
let sp = cpu.read_reg(Register::A7);
let str_ptr = bus.read_long(sp);
let item = bus.read_word(sp + 4) as i16;
let menu_handle = bus.read_long(sp + 6);
if str_ptr != 0 {
if let Some(text) = self
.menus
.iter()
.find(|m| m.handle == menu_handle)
.and_then(|m| m.items.get((item - 1) as usize))
.map(|mi| mi.text.clone())
{
bus.write_pstring(str_ptr, text.as_bytes());
} else {
bus.write_byte(str_ptr, 0);
}
}
cpu.write_reg(Register::A7, sp + 10);
Ok(())
}
(true, 0x148) => {
let sp = cpu.read_reg(Register::A7);
let menu_handle = bus.read_long(sp);
cpu.write_reg(Register::A7, sp + 4);
if let Some(menu) = self.menus.iter().find(|m| m.handle == menu_handle) {
let menu_height = self.menu_items_height(bus, &menu.items) + 2;
let mut max_width: i16 = 0;
for item in &menu.items {
let w = Self::fb_measure_string(&item.text, 0, 12);
let total = w + self.menu_item_width_extra(bus, item) + 24;
if total > max_width {
max_width = total;
}
}
max_width = max_width.max(100);
if menu_handle != 0 {
let menu_ptr = bus.read_long(menu_handle);
if menu_ptr != 0 {
bus.write_word(menu_ptr + 2, max_width as u16);
bus.write_word(menu_ptr + 4, menu_height as u16);
}
}
}
Ok(())
}
(true, 0x14A) => {
let sp = cpu.read_reg(Register::A7);
let count = bus.read_word(sp);
bus.write_word(crate::memory::globals::addr::MENU_FLASH, count);
cpu.write_reg(Register::A7, sp + 2);
Ok(())
}
(true, 0x14B) => {
let sp = cpu.read_reg(Register::A7);
cpu.write_reg(Register::A7, sp + 8);
let port = self.current_port;
if port == 0 {
return Some(Ok(()));
}
let icon_handle = bus.read_long(sp);
let rect_ptr = bus.read_long(sp + 4);
if icon_handle == 0 || rect_ptr == 0 {
return Some(Ok(()));
}
let icon_ptr = bus.read_long(icon_handle);
if icon_ptr == 0 {
return Some(Ok(()));
}
let top = bus.read_word(rect_ptr) as i16;
let left = bus.read_word(rect_ptr + 2) as i16;
let bottom = bus.read_word(rect_ptr + 4) as i16;
let right = bus.read_word(rect_ptr + 6) as i16;
let dst_w = (right - left) as i32;
let dst_h = (bottom - top) as i32;
if dst_w <= 0 || dst_h <= 0 {
return Some(Ok(()));
}
let dst = self.resolve_copy_bitmap(bus, port.wrapping_add(2));
let magnify_x = dst_w >= 32;
let magnify_y = dst_h >= 32;
for dy in 0..dst_h {
let (sy_start, sy_end) = if magnify_y {
let sy = ((dy * 2 * 32 + 32) / (dst_h * 2)).min(31) as u32;
(sy, sy + 1)
} else {
let a = (dy * 32 / dst_h) as u32;
let b = (((dy + 1) * 32 / dst_h).min(32) as u32).max(a + 1);
(a, b)
};
let py = top as i32 + dy;
if py < dst.bounds_top as i32 || py >= dst.bounds_bottom as i32 {
continue;
}
let row_off = (py - dst.bounds_top as i32) as u32;
for dx in 0..dst_w {
let (sx_start, sx_end) = if magnify_x {
let sx = ((dx * 2 * 32 + 32) / (dst_w * 2)).min(31) as u32;
(sx, sx + 1)
} else {
let a = (dx * 32 / dst_w) as u32;
let b = (((dx + 1) * 32 / dst_w).min(32) as u32).max(a + 1);
(a, b)
};
let mut any_set = false;
'or_scan: for sy in sy_start..sy_end {
let row_data = bus.read_long(icon_ptr + sy * 4);
for sx in sx_start..sx_end {
if (row_data >> (31 - sx)) & 1 != 0 {
any_set = true;
break 'or_scan;
}
}
}
if !any_set {
continue;
}
let px = left as i32 + dx;
if px < dst.bounds_left as i32 || px >= dst.bounds_right as i32 {
continue;
}
let col = (px - dst.bounds_left as i32) as u32;
match dst.pixel_size {
1 => {
let addr = dst.base + row_off * dst.row_bytes + (col / 8);
let bit = 7 - (col % 8);
let byte = bus.read_byte(addr);
bus.write_byte(addr, byte | (1 << bit));
}
8 => {
let addr = dst.base + row_off * dst.row_bytes + col;
bus.write_byte(addr, 255);
}
_ => {}
}
}
}
Ok(())
}
(true, 0x14C) => {
let sp = cpu.read_reg(Register::A7);
let menu_id = bus.read_word(sp) as i16;
cpu.write_reg(Register::A7, sp + 2);
self.flash_menu_bar(bus, menu_id);
Ok(())
}
(true, 0x14E) => {
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 rect_ptr = bus.read_long(sp + 4);
let top = bus.read_word(rect_ptr) as i16;
let left = bus.read_word(rect_ptr + 2) as i16;
let bottom = bus.read_word(rect_ptr + 4) as i16;
let right = bus.read_word(rect_ptr + 6) as i16;
let pinned_v = pt_v.max(top).min(bottom - 1);
let pinned_h = pt_h.max(left).min(right - 1);
let result = ((pinned_v as u32) << 16) | (pinned_h as u16 as u32);
bus.write_long(sp + 8, result);
cpu.write_reg(Register::A7, sp + 8);
Ok(())
}
(true, 0x14F) => {
let sp = cpu.read_reg(Register::A7);
let pt_b_v = bus.read_word(sp) as i16;
let pt_b_h = bus.read_word(sp + 2) as i16;
let pt_a_v = bus.read_word(sp + 4) as i16;
let pt_a_h = bus.read_word(sp + 6) as i16;
let dv = pt_a_v.wrapping_sub(pt_b_v);
let dh = pt_a_h.wrapping_sub(pt_b_h);
let result = ((dv as u16 as u32) << 16) | (dh as u16 as u32);
bus.write_long(sp + 8, result);
cpu.write_reg(Register::A7, sp + 8);
Ok(())
}
(true, 0x151) => {
let sp = cpu.read_reg(Register::A7);
let after_item = bus.read_word(sp) as i16;
let res_type_word = bus.read_long(sp + 2);
let menu_handle = bus.read_long(sp + 6);
cpu.write_reg(Register::A7, sp + 10);
let res_type = res_type_word.to_be_bytes();
let entries = self.named_resources_of_type(res_type);
let mut touched: Option<Menu> = None;
if let Some(menu) = self.menus.iter_mut().find(|m| m.handle == menu_handle) {
let base = (after_item.max(0) as usize).min(menu.items.len());
let mut offset = 0usize;
for (_id, name) in entries {
if name.is_empty() || name.starts_with('.') || name.starts_with('%') {
continue;
}
if menu.items.iter().any(|it| it.text == name) {
continue;
}
menu.items.insert(
base + offset,
MenuItem {
text: name,
icon: 0,
key_equiv: 0,
mark: 0,
style: 0,
enabled: true,
},
);
offset += 1;
}
sync_enable_flags(bus, menu);
touched = Some(menu.clone());
}
if let Some(m) = touched {
serialise_menu_items_to_memory(bus, &m);
}
Ok(())
}
(true, 0x152) => {
let sp = cpu.read_reg(Register::A7);
let item = bus.read_word(sp) as i16;
let menu_handle = bus.read_long(sp + 2);
cpu.write_reg(Register::A7, sp + 6);
let mut touched: Option<Menu> = None;
let mut deleted_color_key: Option<(i16, i16)> = None;
if let Some(menu) = self.menus.iter_mut().find(|m| m.handle == menu_handle) {
let idx = (item - 1) as usize;
if idx < menu.items.len() {
menu.items.remove(idx);
deleted_color_key = Some((menu.id, item));
touched = Some(menu.clone());
}
}
if let Some(m) = touched {
serialise_menu_items_to_memory(bus, &m);
sync_enable_flags(bus, &m);
}
if let Some((menu_id, menu_item)) = deleted_color_key {
self.filter_menu_color_table_entries(bus, |id, item_no| {
!(id == menu_id && item_no == menu_item)
});
}
Ok(())
}
(true, 0x026) => {
let sp = cpu.read_reg(Register::A7);
let after_item = bus.read_word(sp) as i16;
let text_ptr = bus.read_long(sp + 2);
let menu_handle = bus.read_long(sp + 6);
cpu.write_reg(Register::A7, sp + 10);
if menu_handle == 0 || text_ptr == 0 {
return Some(Ok(()));
}
let len = bus.read_byte(text_ptr) as usize;
let mut bytes = Vec::with_capacity(len);
for i in 0..len {
bytes.push(bus.read_byte(text_ptr + 1 + i as u32));
}
let parsed = parse_appendmenu_items(&bytes);
let mut touched: Option<Menu> = None;
if let Some(menu) = self.menus.iter_mut().find(|m| m.handle == menu_handle) {
if !parsed.is_empty() {
let insert_idx = if after_item <= 0 {
0usize
} else {
(after_item as usize).min(menu.items.len())
};
for item in parsed {
menu.items.insert(insert_idx, item);
}
touched = Some(menu.clone());
}
}
if let Some(m) = touched {
sync_enable_flags(bus, &m);
serialise_menu_items_to_memory(bus, &m);
}
Ok(())
}
(true, 0x008) => {
let sp = cpu.read_reg(Register::A7);
cpu.write_reg(Register::A7, sp + 2);
Ok(())
}
(true, 0x01D) => Ok(()),
(true, 0x04F) => {
let sp = cpu.read_reg(Register::A7);
let cmd_char = (bus.read_word(sp) & 0xFF) as u8;
let item = bus.read_word(sp + 2) as i16;
let menu_handle = bus.read_long(sp + 4);
cpu.write_reg(Register::A7, sp + 8);
if menu_handle == 0 || item < 1 {
return Some(Ok(()));
}
let menu_clone =
if let Some(menu) = self.menus.iter_mut().find(|m| m.handle == menu_handle) {
if let Some(mi) = menu.items.get_mut((item - 1) as usize) {
mi.key_equiv = cmd_char;
} else {
return Some(Ok(()));
}
menu.clone()
} else {
return Some(Ok(()));
};
serialise_menu_items_to_memory(bus, &menu_clone);
Ok(())
}
(true, 0x13B) => {
let sp = cpu.read_reg(Register::A7);
let count = self.menus.len() as u32;
let block = bus.alloc(2 + 4 * count.max(1));
bus.write_word(block, count as u16);
for (i, menu) in self.menus.iter().enumerate() {
bus.write_long(block + 2 + (i as u32) * 4, menu.handle);
}
let handle = bus.alloc(4);
bus.write_long(handle, block);
self.saved_menu_bars.insert(handle, self.menus.clone());
bus.write_long(sp, handle);
Ok(())
}
(true, 0x260) => {
let sp = cpu.read_reg(Register::A7);
let menu_item = bus.read_word(sp) as i16;
let menu_id = bus.read_word(sp + 2) as i16;
let current_handle = bus.read_long(addr::MENU_C_INFO);
if current_handle != 0 {
let mut kept = Vec::new();
let current_ptr = bus.read_long(current_handle);
if current_ptr != 0 {
let current_size = bus.get_alloc_size(current_ptr).unwrap_or(0) as usize;
let current_bytes = if current_size == 0 {
Vec::new()
} else {
bus.read_bytes(current_ptr, current_size)
};
if !current_bytes.is_empty() {
kept.reserve(current_bytes.len());
for entry in current_bytes.chunks_exact(MC_ENTRY_SIZE) {
let remove = if menu_item == MC_ALL_ITEMS {
mc_entry_key(entry)
.is_some_and(|(id, item)| id == menu_id && item != 0)
} else {
mc_entry_matches(entry, menu_id, menu_item)
};
if !remove {
kept.extend_from_slice(entry);
}
}
let _ = self.replace_handle_bytes(bus, current_handle, &kept);
}
}
}
cpu.write_reg(Register::A7, sp + 4);
Ok(())
}
(true, 0x261) => {
let sp = cpu.read_reg(Register::A7);
let current_handle = bus.read_long(addr::MENU_C_INFO);
let copy_handle = self.clone_menu_color_handle(bus, current_handle);
bus.write_long(sp, copy_handle);
Ok(())
}
(true, 0x262) => {
let sp = cpu.read_reg(Register::A7);
let source_handle = bus.read_long(sp);
if source_handle != 0 {
let current_handle = self.ensure_menu_color_table_handle(bus);
if current_handle != 0 {
let copy_handle = self.clone_menu_color_handle(bus, source_handle);
if copy_handle != 0 {
let old_ptr = bus.read_long(current_handle);
if old_ptr != 0 {
bus.free(old_ptr);
}
bus.free(current_handle);
bus.write_long(addr::MENU_C_INFO, copy_handle);
}
}
}
cpu.write_reg(Register::A7, sp + 4);
Ok(())
}
(true, 0x263) => {
let sp = cpu.read_reg(Register::A7);
let handle = bus.read_long(sp);
if handle != 0 {
let data_ptr = bus.read_long(handle);
if data_ptr != 0 {
bus.free(data_ptr);
}
bus.free(handle);
}
cpu.write_reg(Register::A7, sp + 4);
Ok(())
}
(true, 0x264) => {
let sp = cpu.read_reg(Register::A7);
let menu_item = bus.read_word(sp) as i16;
let menu_id = bus.read_word(sp + 2) as i16;
let current_handle = bus.read_long(addr::MENU_C_INFO);
let mut result = 0;
if current_handle != 0 {
let current_ptr = bus.read_long(current_handle);
if current_ptr != 0 {
let current_size = bus.get_alloc_size(current_ptr).unwrap_or(0) as usize;
let mut offset = 0usize;
while offset + MC_ENTRY_SIZE <= current_size {
let entry_ptr = current_ptr + offset as u32;
if bus.read_word(entry_ptr) as i16 == menu_id
&& bus.read_word(entry_ptr + 2) as i16 == menu_item
{
result = entry_ptr;
break;
}
offset += MC_ENTRY_SIZE;
}
}
}
bus.write_long(sp + 4, result);
cpu.write_reg(Register::A7, sp + 4);
Ok(())
}
(true, 0x265) => {
let sp = cpu.read_reg(Register::A7);
let num_entries = bus.read_word(sp + 4) as i16;
let entries_ptr = bus.read_long(sp);
if num_entries > 0 && entries_ptr != 0 {
let mut entries = Vec::with_capacity(num_entries as usize * MC_ENTRY_SIZE);
for index in 0..num_entries as usize {
let entry_ptr = entries_ptr + (index as u32 * MC_ENTRY_SIZE as u32);
entries.extend_from_slice(&bus.read_bytes(entry_ptr, MC_ENTRY_SIZE));
}
self.merge_menu_color_entries(bus, &entries);
}
cpu.write_reg(Register::A7, sp + 6);
Ok(())
}
(true, 0x266) => {
let sp = cpu.read_reg(Register::A7);
let value = bus.read_long(addr::MENU_DISABLE);
bus.write_long(sp, value);
Ok(())
}
_ => return None,
})
}
fn is_hierarchical_item(item: &MenuItem) -> bool {
item.key_equiv == 0x1B && item.mark != 0
}
fn menu_item_has_command_key(item: &MenuItem) -> bool {
item.key_equiv > 0x20
}
fn menu_item_uses_reduced_icon(item: &MenuItem) -> bool {
item.icon != 0 && item.key_equiv == MENU_KEY_REDUCED_ICON
}
fn menu_item_uses_small_icon(item: &MenuItem) -> bool {
item.icon != 0 && item.key_equiv == MENU_KEY_SMALL_ICON
}
fn menu_item_uses_normal_icon(item: &MenuItem) -> bool {
item.icon != 0 && (item.key_equiv == 0 || item.key_equiv > 0x20)
}
fn menu_cicn_layout(bus: &MacMemoryBus, icon_ptr: u32) -> Option<MenuCIconLayout> {
if bus.get_alloc_size(icon_ptr).is_some_and(|size| size < 82) {
return None;
}
let pm_row_bytes = (bus.read_word(icon_ptr + 4) & 0x3FFF) as u32;
let pm_top = bus.read_word(icon_ptr + 6) as i16;
let pm_left = bus.read_word(icon_ptr + 8) as i16;
let pm_bottom = bus.read_word(icon_ptr + 10) as i16;
let pm_right = bus.read_word(icon_ptr + 12) as i16;
let pixel_size = bus.read_word(icon_ptr + 32);
let mask_row_bytes = (bus.read_word(icon_ptr + 54) & 0x3FFF) as u32;
let bmap_row_bytes = (bus.read_word(icon_ptr + 68) & 0x3FFF) as u32;
let width = pm_right - pm_left;
let height = pm_bottom - pm_top;
if width <= 0 || height <= 0 || pm_row_bytes == 0 || mask_row_bytes == 0 {
return None;
}
let height_u32 = height as u32;
let mask_data_size = mask_row_bytes.checked_mul(height_u32)?;
let bmap_data_size = bmap_row_bytes.checked_mul(height_u32)?;
let mask_data_ptr = icon_ptr + 82;
let bmap_data_ptr = mask_data_ptr.checked_add(mask_data_size)?;
let ctab_ptr = bmap_data_ptr.checked_add(bmap_data_size)?;
if let Some(resource_size) = bus.get_alloc_size(icon_ptr) {
let resource_end = icon_ptr.checked_add(resource_size)?;
if ctab_ptr.checked_add(8)? > resource_end {
return None;
}
}
let ct_size = bus.read_word(ctab_ptr + 6) as u32;
let ctab_total_bytes = 8u32.checked_add((ct_size + 1).checked_mul(8)?)?;
let pixel_data_ptr = ctab_ptr.checked_add(ctab_total_bytes)?;
if let Some(resource_size) = bus.get_alloc_size(icon_ptr) {
let resource_end = icon_ptr.checked_add(resource_size)?;
let pixel_data_size = pm_row_bytes.checked_mul(height_u32)?;
if pixel_data_ptr.checked_add(pixel_data_size)? > resource_end {
return None;
}
}
Some(MenuCIconLayout {
width,
height,
pm_row_bytes,
mask_row_bytes,
bmap_row_bytes,
pixel_size,
mask_data_ptr,
bmap_data_ptr,
pixel_data_ptr,
})
}
fn menu_cicn_pixel_index(
bus: &MacMemoryBus,
data_ptr: u32,
row_bytes: u32,
pixel_size: u16,
x: u32,
y: u32,
) -> Option<u8> {
let row_ptr = data_ptr.checked_add(y.checked_mul(row_bytes)?)?;
match pixel_size {
1 => {
let byte = bus.read_byte(row_ptr.checked_add(x / 8)?);
Some(((byte >> (7 - (x % 8))) & 1) as u8)
}
2 => {
let byte = bus.read_byte(row_ptr.checked_add(x / 4)?);
Some((byte >> (6 - 2 * (x % 4))) & 0x03)
}
4 => {
let byte = bus.read_byte(row_ptr.checked_add(x / 2)?);
Some(if x % 2 == 0 {
(byte >> 4) & 0x0F
} else {
byte & 0x0F
})
}
8 => Some(bus.read_byte(row_ptr.checked_add(x)?)),
size if size > 8 && size % 8 == 0 => {
let bytes_per_pixel = u32::from(size / 8);
Some(bus.read_byte(row_ptr.checked_add(x.checked_mul(bytes_per_pixel)?)?))
}
_ => None,
}
}
fn menu_item_cicn_size(&self, bus: &MacMemoryBus, item: &MenuItem) -> Option<(i16, i16)> {
let icon_ptr = self.cicn_menu_icon_resource_ptr(item)?;
let layout = Self::menu_cicn_layout(bus, icon_ptr)?;
Some((layout.width, layout.height))
}
pub(super) fn menu_item_height(&self, bus: &MacMemoryBus, item: &MenuItem) -> i16 {
let base_height = if let Some((_width, height)) = self.menu_item_cicn_size(bus, item) {
height.max(MENU_ROW_HEIGHT)
} else if Self::menu_item_uses_normal_icon(item) {
MENU_NORMAL_ICON_ROW_HEIGHT
} else {
MENU_ROW_HEIGHT
};
if (item.style & MENU_TEXT_STYLE_SHADOW) != 0 {
base_height.max(MENU_SHADOW_STYLE_ROW_HEIGHT)
} else {
base_height
}
}
pub(super) fn menu_items_height(&self, bus: &MacMemoryBus, items: &[MenuItem]) -> i16 {
items
.iter()
.map(|item| self.menu_item_height(bus, item))
.sum()
}
pub(super) fn menu_item_width_extra(&self, bus: &MacMemoryBus, item: &MenuItem) -> i16 {
let key_extra = if Self::menu_item_has_command_key(item) {
30
} else {
0
};
let is_hierarchical = Self::is_hierarchical_item(item);
let mark_extra = if item.mark != 0 && !is_hierarchical {
14
} else {
0
};
let hierarchy_extra = if is_hierarchical { 20 } else { 0 };
let icon_extra = if let Some((width, _height)) = self.menu_item_cicn_size(bus, item) {
width.max(MENU_ROW_HEIGHT)
} else if Self::menu_item_uses_reduced_icon(item) || Self::menu_item_uses_small_icon(item) {
MENU_ROW_HEIGHT
} else if Self::menu_item_uses_normal_icon(item) {
MENU_NORMAL_ICON_SIZE
} else {
0
};
key_extra + mark_extra + hierarchy_extra + icon_extra
}
fn menu_item_pulldown_padding(item: &MenuItem) -> i16 {
let has_key = Self::menu_item_has_command_key(item);
let has_mark_or_icon = item.mark != 0 || item.icon != 0;
if Self::menu_item_uses_normal_icon(item) {
6
} else if has_key && !has_mark_or_icon {
20
} else if !has_key && !has_mark_or_icon {
26
} else {
14
}
}
fn menu_item_icon_resource_id(item: &MenuItem) -> Option<i16> {
(item.icon != 0).then_some(i16::from(item.icon) + 256)
}
fn menu_item_has_cicn_resource(&self, item: &MenuItem) -> bool {
self.cicn_menu_icon_resource_ptr(item).is_some()
}
fn cicn_menu_icon_resource_ptr(&self, item: &MenuItem) -> Option<u32> {
let Some(icon_resource_id) = Self::menu_item_icon_resource_id(item) else {
return None;
};
self.find_resource_any(*b"cicn", icon_resource_id)
.map(|(_, ptr)| ptr)
}
fn reduced_menu_icon_resource_ptr(&self, item: &MenuItem) -> Option<u32> {
if !Self::menu_item_uses_reduced_icon(item) || self.menu_item_has_cicn_resource(item) {
return None;
}
let icon_resource_id = Self::menu_item_icon_resource_id(item)?;
self.find_resource_any(*b"ICON", icon_resource_id)
.map(|(_, ptr)| ptr)
}
fn small_menu_icon_resource_ptr(&self, item: &MenuItem) -> Option<u32> {
if !Self::menu_item_uses_small_icon(item) || self.menu_item_has_cicn_resource(item) {
return None;
}
let icon_resource_id = Self::menu_item_icon_resource_id(item)?;
self.find_resource_any(*b"SICN", icon_resource_id)
.map(|(_, ptr)| ptr)
}
fn normal_menu_icon_resource_ptr(&self, item: &MenuItem) -> Option<u32> {
if !Self::menu_item_uses_normal_icon(item) || self.menu_item_has_cicn_resource(item) {
return None;
}
let icon_resource_id = Self::menu_item_icon_resource_id(item)?;
self.find_resource_any(*b"ICON", icon_resource_id)
.map(|(_, ptr)| ptr)
}
fn draw_menu_icon_bitmap(
&self,
bus: &mut MacMemoryBus,
icon_ptr: u32,
top: i16,
left: i16,
dst_size: i16,
pixel_index_override: Option<u8>,
) {
let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
self.get_screen_params();
let dst_size = i32::from(dst_size);
for dy in 0..dst_size {
let sy_start = (dy * 32 / dst_size) as u32;
let sy_end = (((dy + 1) * 32 / dst_size).min(32) as u32).max(sy_start + 1);
for dx in 0..dst_size {
let sx_start = (dx * 32 / dst_size) as u32;
let sx_end = (((dx + 1) * 32 / dst_size).min(32) as u32).max(sx_start + 1);
let mut any_set = false;
'or_scan: for sy in sy_start..sy_end {
let row_data = bus.read_long(icon_ptr + sy * 4);
for sx in sx_start..sx_end {
if (row_data >> (31 - sx)) & 1 != 0 {
any_set = true;
break 'or_scan;
}
}
}
if any_set {
if let Some(pixel_index) = pixel_index_override {
Self::fb_set_pixel_index(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
left + dx as i16,
top + dy as i16,
pixel_index,
);
} else {
Self::fb_set_pixel(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
left + dx as i16,
top + dy as i16,
true,
);
}
}
}
}
}
fn draw_sicn_menu_icon(
&self,
bus: &mut MacMemoryBus,
icon_ptr: u32,
top: i16,
left: i16,
pixel_index_override: Option<u8>,
) {
if bus.get_alloc_size(icon_ptr).is_some_and(|size| size < 32) {
return;
}
let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
self.get_screen_params();
for dy in 0..MENU_ROW_HEIGHT {
let row_data = bus.read_word(icon_ptr + u32::from(dy as u16) * 2);
for dx in 0..MENU_ROW_HEIGHT {
if (row_data >> (15 - dx)) & 1 != 0 {
if let Some(pixel_index) = pixel_index_override {
Self::fb_set_pixel_index(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
left + dx,
top + dy,
pixel_index,
);
} else {
Self::fb_set_pixel(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
left + dx,
top + dy,
true,
);
}
}
}
}
}
fn draw_cicn_menu_icon(&self, bus: &mut MacMemoryBus, icon_ptr: u32, top: i16, left: i16) {
let Some(layout) = Self::menu_cicn_layout(bus, icon_ptr) else {
return;
};
let (screen_base, row_bytes, screen_width, screen_height, screen_pixel_size) =
self.get_screen_params();
for dy in 0..layout.height {
for dx in 0..layout.width {
let sx = dx as u32;
let sy = dy as u32;
let mask_byte =
bus.read_byte(layout.mask_data_ptr + sy * layout.mask_row_bytes + sx / 8);
if (mask_byte & (0x80 >> (sx % 8))) == 0 {
continue;
}
if screen_pixel_size == 8 && layout.pixel_size >= 2 {
let Some(pixel_index) = Self::menu_cicn_pixel_index(
bus,
layout.pixel_data_ptr,
layout.pm_row_bytes,
layout.pixel_size,
sx,
sy,
) else {
continue;
};
let dst_x = left + dx;
let dst_y = top + dy;
if dst_x >= 0 && dst_y >= 0 && dst_x < screen_width && dst_y < screen_height {
bus.write_byte(
screen_base + (dst_y as u32) * row_bytes + dst_x as u32,
pixel_index,
);
}
continue;
}
let source_data_ptr = if layout.bmap_row_bytes != 0 {
layout.bmap_data_ptr
} else {
layout.pixel_data_ptr
};
let source_row_bytes = if layout.bmap_row_bytes != 0 {
layout.bmap_row_bytes
} else {
layout.pm_row_bytes
};
let source_pixel_size = if layout.bmap_row_bytes != 0 {
1
} else {
layout.pixel_size
};
let Some(pixel_index) = Self::menu_cicn_pixel_index(
bus,
source_data_ptr,
source_row_bytes,
source_pixel_size,
sx,
sy,
) else {
continue;
};
Self::fb_set_pixel(
bus,
screen_base,
row_bytes,
screen_pixel_size,
screen_width,
screen_height,
left + dx,
top + dy,
pixel_index != 0,
);
}
}
}
fn popup_menu_dropdown_rect(
&self,
bus: &MacMemoryBus,
menu_idx: usize,
top: i16,
left: i16,
popup_item: i16,
) -> ((i16, i16, i16, i16), i16) {
let (_screen_base, _row_bytes, screen_width, screen_height, _pixel_size) =
self.get_screen_params();
let menu = &self.menus[menu_idx];
let mut width: i16 = 0;
for item in &menu.items {
let w = Self::fb_measure_string(&item.text, 0, 12)
+ self.menu_item_width_extra(bus, item)
+ 26;
width = width.max(w);
}
width = width.max(1);
let height = (self.menu_items_height(bus, &menu.items) + 2).max(1);
let highlighted_item = if popup_item >= 1 && (popup_item as usize) <= menu.items.len() {
popup_item
} else {
0
};
let item_offset = if highlighted_item > 0 {
menu.items
.iter()
.take((highlighted_item - 1) as usize)
.map(|item| self.menu_item_height(bus, item))
.sum::<i16>()
} else {
0
};
let desired_top = if highlighted_item > 0 {
top - 1 - item_offset
} else {
top
};
let desired_left = left - 1;
let clamped_left = if screen_width <= 0 {
desired_left
} else if width >= screen_width {
0
} else {
desired_left.clamp(0, screen_width - width)
};
let clamped_top = if screen_height <= 0 {
desired_top
} else if height >= screen_height {
0
} else {
desired_top.clamp(0, screen_height - height)
};
let right = if screen_width > 0 {
(clamped_left + width).min(screen_width)
} else {
clamped_left + width
};
let bottom = if screen_height > 0 {
(clamped_top + height).min(screen_height)
} else {
clamped_top + height
};
((clamped_top, clamped_left, bottom, right), highlighted_item)
}
fn dropdown_width_for_menu(&self, bus: &MacMemoryBus, menu_idx: usize, min_width: i16) -> i16 {
let Some(menu) = self.menus.get(menu_idx) else {
return min_width;
};
let mut max_width = min_width;
for item in &menu.items {
let w = Self::fb_measure_string(&item.text, 0, 12);
let total =
w + self.menu_item_width_extra(bus, item) + Self::menu_item_pulldown_padding(item);
max_width = max_width.max(total);
}
max_width
}
fn open_menu_dropdown(&mut self, bus: &mut MacMemoryBus, menu_idx: usize, stack_ptr: u32) {
let (_screen_base, _row_bytes, screen_width, screen_height, _pixel_size) =
self.get_screen_params();
let region = self
.menu_title_regions_with_indices()
.into_iter()
.find(|(idx, _, _)| *idx == menu_idx);
if menu_idx >= self.menus.len() {
return;
}
let Some((_idx, title_left, title_right)) = region else {
return;
};
let menu = &self.menus[menu_idx];
let dropdown_top: i16 = 20; let dropdown_left: i16 = title_left - 2;
let max_width = self.dropdown_width_for_menu(bus, menu_idx, title_right - title_left + 20);
let dropdown_bottom =
(dropdown_top + self.menu_items_height(bus, &menu.items) + 1).min(screen_height);
let dropdown_right = (dropdown_left + max_width).min(screen_width);
let dropdown_rect = (dropdown_top, dropdown_left, dropdown_bottom, dropdown_right);
let saved = self.save_dropdown_pixels(bus, dropdown_rect);
self.draw_menu_dropdown(bus, menu_idx, dropdown_rect);
self.highlight_menu_title(bus, menu_idx);
self.menu_tracking = Some(MenuTrackingState {
active_menu: menu_idx,
highlighted_item: 0,
saved_pixels: saved,
dropdown_rect,
submenu: None,
stack_ptr,
flash_remaining: 0,
flash_delay: 0,
flash_result: 0,
});
}
fn finish_menu_no_hit(
&self,
bus: &mut MacMemoryBus,
cpu: &mut dyn CpuOps,
stack_ptr: u32,
result_offset: u32,
) {
bus.write_long(stack_ptr + result_offset, 0);
cpu.write_reg(Register::A7, stack_ptr + result_offset);
}
fn restore_menu_tracking_pixels(&self, bus: &mut MacMemoryBus, saved: MenuTrackingState) {
if let Some(submenu) = saved.submenu {
self.restore_dropdown_pixels(bus, submenu.dropdown_rect, &submenu.saved_pixels);
}
self.restore_dropdown_pixels(bus, saved.dropdown_rect, &saved.saved_pixels);
}
fn menu_tracking_selection_result(&self) -> u32 {
let Some(tracking) = self.menu_tracking.as_ref() else {
return 0;
};
if let Some(submenu) = tracking.submenu.as_ref() {
if submenu.highlighted_item > 0 {
let menu = &self.menus[submenu.menu];
return ((menu.id as u32) << 16) | (submenu.highlighted_item as u32 & 0xFFFF);
}
}
if tracking.highlighted_item <= 0 {
return 0;
}
let menu = &self.menus[tracking.active_menu];
let item_idx = tracking.highlighted_item as usize - 1;
let Some(item) = menu.items.get(item_idx) else {
return 0;
};
if Self::is_hierarchical_item(item) {
return 0;
}
((menu.id as u32) << 16) | (tracking.highlighted_item as u32 & 0xFFFF)
}
fn submenu_menu_index_for_parent_item(&self, parent_item: i16) -> Option<usize> {
let tracking = self.menu_tracking.as_ref()?;
if parent_item <= 0 {
return None;
}
let parent_menu = self.menus.get(tracking.active_menu)?;
let item = parent_menu.items.get(parent_item as usize - 1)?;
if !Self::is_hierarchical_item(item) {
return None;
}
let submenu_id = item.mark as i16;
self.menus.iter().position(|m| m.id == submenu_id)
}
fn submenu_rect_for_parent_item(
&self,
bus: &MacMemoryBus,
submenu_idx: usize,
parent_item: i16,
) -> Option<(i16, i16, i16, i16)> {
let tracking = self.menu_tracking.as_ref()?;
let (_screen_base, _row_bytes, screen_width, screen_height, _pixel_size) =
self.get_screen_params();
let (parent_top, parent_left, _parent_bottom, parent_right) = tracking.dropdown_rect;
let parent_menu = self.menus.get(tracking.active_menu)?;
let parent_offset = parent_menu
.items
.iter()
.take((parent_item - 1).max(0) as usize)
.map(|item| self.menu_item_height(bus, item))
.sum::<i16>();
let submenu_top = parent_top + 1 + parent_offset;
let submenu_height = self.menu_items_height(bus, &self.menus.get(submenu_idx)?.items) + 2;
let submenu_width = self.dropdown_width_for_menu(bus, submenu_idx, 100);
let mut submenu_left = parent_right - 1;
let mut submenu_right = submenu_left + submenu_width;
if submenu_right > screen_width {
submenu_right = parent_left + 1;
submenu_left = (submenu_right - submenu_width).max(0);
}
let submenu_bottom = (submenu_top + submenu_height).min(screen_height);
Some((submenu_top, submenu_left, submenu_bottom, submenu_right))
}
fn close_submenu(&mut self, bus: &mut MacMemoryBus) {
let submenu = self
.menu_tracking
.as_mut()
.and_then(|tracking| tracking.submenu.take());
if let Some(submenu) = submenu {
self.restore_dropdown_pixels(bus, submenu.dropdown_rect, &submenu.saved_pixels);
}
}
fn ensure_submenu_for_parent_item(&mut self, bus: &mut MacMemoryBus, parent_item: i16) {
let Some(submenu_idx) = self.submenu_menu_index_for_parent_item(parent_item) else {
self.close_submenu(bus);
return;
};
let already_open = self
.menu_tracking
.as_ref()
.and_then(|tracking| tracking.submenu.as_ref())
.is_some_and(|submenu| {
submenu.menu == submenu_idx && submenu.parent_item == parent_item
});
if already_open {
return;
}
let Some(dropdown_rect) = self.submenu_rect_for_parent_item(bus, submenu_idx, parent_item)
else {
self.close_submenu(bus);
return;
};
self.close_submenu(bus);
let saved_pixels = self.save_dropdown_pixels(bus, dropdown_rect);
self.draw_menu_dropdown(bus, submenu_idx, dropdown_rect);
if let Some(tracking) = self.menu_tracking.as_mut() {
tracking.submenu = Some(SubmenuTrackingState {
menu: submenu_idx,
parent_item,
highlighted_item: 0,
saved_pixels,
dropdown_rect,
});
}
}
fn submenu_item_at_point(&self, mouse_x: i16, mouse_y: i16) -> Option<i16> {
let tracking = self.menu_tracking.as_ref()?;
let submenu = tracking.submenu.as_ref()?;
let (top, left, bottom, right) = submenu.dropdown_rect;
if mouse_x < left || mouse_x >= right || mouse_y < top || mouse_y >= bottom {
return None;
}
let item_height: i16 = 16;
let item_idx = (mouse_y - top - 1) / item_height;
let menu = self.menus.get(submenu.menu)?;
if item_idx < 0 || item_idx as usize >= menu.items.len() {
return Some(0);
}
let item = &menu.items[item_idx as usize];
if item.text == "-" || !item.enabled {
return Some(0);
}
Some(item_idx + 1)
}
fn update_submenu_highlight(&mut self, bus: &mut MacMemoryBus, new_item: i16) {
let Some((menu_idx, old_item, rect)) = self
.menu_tracking
.as_ref()
.and_then(|tracking| tracking.submenu.as_ref())
.map(|submenu| {
(
submenu.menu,
submenu.highlighted_item,
submenu.dropdown_rect,
)
})
else {
return;
};
if old_item == new_item {
return;
}
if old_item > 0 {
self.invert_dropdown_item_rect(bus, menu_idx, rect, old_item);
}
if let Some(submenu) = self
.menu_tracking
.as_mut()
.and_then(|tracking| tracking.submenu.as_mut())
{
submenu.highlighted_item = new_item;
}
if new_item > 0 {
self.invert_dropdown_item_rect(bus, menu_idx, rect, new_item);
}
}
fn update_parent_menu_highlight(&mut self, bus: &mut MacMemoryBus, new_item: i16) {
let Some(old_item) = self
.menu_tracking
.as_ref()
.map(|tracking| tracking.highlighted_item)
else {
return;
};
if old_item != new_item {
self.close_submenu(bus);
if old_item > 0 {
self.invert_menu_item(bus, old_item);
}
if let Some(tracking) = self.menu_tracking.as_mut() {
tracking.highlighted_item = new_item;
}
if new_item > 0 {
self.invert_menu_item(bus, new_item);
}
}
if new_item > 0 {
self.ensure_submenu_for_parent_item(bus, new_item);
} else {
self.close_submenu(bus);
}
}
fn update_menu_tracking_for_point(
&mut self,
bus: &mut MacMemoryBus,
mouse_x: i16,
mouse_y: i16,
) {
if let Some(submenu_item) = self.submenu_item_at_point(mouse_x, mouse_y) {
self.update_submenu_highlight(bus, submenu_item);
return;
}
let new_item = self.dropdown_item_at_point(bus, mouse_x, mouse_y);
self.update_parent_menu_highlight(bus, new_item);
}
pub(crate) fn menu_title_hit_test(&self, mouse_x: i16) -> Option<usize> {
for (menu_idx, left, right) in self.menu_title_regions_with_indices() {
if mouse_x >= left && mouse_x < right {
return Some(menu_idx);
}
}
None
}
#[cfg(test)]
fn menu_title_regions(&self) -> Vec<(i16, i16)> {
self.menu_title_regions_with_indices()
.into_iter()
.map(|(_, left, right)| (left, right))
.collect()
}
fn menu_title_regions_with_indices(&self) -> Vec<(usize, i16, i16)> {
let mut regions = Vec::new();
let mut x: i16 = 18;
for (menu_idx, menu) in self.menus.iter().enumerate() {
if !menu.visible_in_menu_bar {
continue;
}
let width = Self::fb_measure_string(&menu.title, 0, 12);
let left = x - 7; let right = x + width + 6; regions.push((menu_idx, left, right));
x += width + 13;
}
regions
}
fn dropdown_item_at_point(&self, bus: &MacMemoryBus, mouse_x: i16, mouse_y: i16) -> i16 {
if let Some(ref tracking) = self.menu_tracking {
let (top, left, bottom, right) = tracking.dropdown_rect;
if mouse_x >= left && mouse_x < right && mouse_y >= top && mouse_y < bottom {
let menu = &self.menus[tracking.active_menu];
let mut item_top = top + 1;
for (item_idx, item) in menu.items.iter().enumerate() {
let item_bottom = item_top + self.menu_item_height(bus, item);
if mouse_y >= item_top && mouse_y < item_bottom {
if item.text == "-" || !item.enabled {
return 0;
}
return item_idx as i16 + 1; }
item_top = item_bottom;
}
}
}
0
}
pub(super) fn draw_menu_dropdown(
&self,
bus: &mut MacMemoryBus,
menu_idx: usize,
rect: (i16, i16, i16, i16),
) {
let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
self.get_screen_params();
let (top, left, bottom, right) = rect;
if menu_idx >= self.menus.len() {
return;
}
let menu = &self.menus[menu_idx];
let dropdown_bg_index =
Self::menu_dropdown_background_pixel_index(bus, menu.id, pixel_size);
let detached_popup = self.menu_tracking.as_ref().is_some_and(|tracking| {
tracking.active_menu == menu_idx
&& tracking.dropdown_rect == rect
&& !menu.visible_in_menu_bar
});
let attached_pulldown =
!detached_popup && menu.in_menu_bar && top == bus.read_word(addr::MBAR_HEIGHT) as i16;
if !self.draw_theme_menu_dropdown_chrome(bus, top, left, bottom, right) {
if let Some(bg_index) = dropdown_bg_index {
Self::fb_fill_rect_index(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
top,
left,
bottom,
right,
bg_index,
);
} else {
Self::fb_fill_rect(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
top,
left,
bottom,
right,
false,
);
}
if !attached_pulldown {
Self::fb_hline(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
top,
left,
right,
true,
);
}
Self::fb_hline(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
bottom - 1,
left,
right,
true,
);
for y in top..bottom {
Self::fb_set_pixel(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
left,
y,
true,
);
Self::fb_set_pixel(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
right - 1,
y,
true,
);
}
let shadow_top = if detached_popup { top + 3 } else { top + 2 };
for y in shadow_top..=bottom {
Self::fb_set_pixel(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
right,
y,
true,
);
}
Self::fb_hline(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
bottom,
left + 3,
right + 1,
true,
);
}
let font_id: i16 = 0;
let font_size: i16 = 12;
let metrics = crate::quickdraw::text::get_font_metrics(font_id, font_size);
let highlighted_item = self
.menu_tracking
.as_ref()
.filter(|tracking| tracking.active_menu == menu_idx && tracking.dropdown_rect == rect)
.map(|tracking| tracking.highlighted_item)
.or_else(|| {
self.control_tracking
.as_ref()
.filter(|tracking| {
tracking.active_menu == menu_idx && tracking.dropdown_rect == rect
})
.map(|tracking| tracking.highlighted_item)
})
.unwrap_or(0);
let mut item_top = top + 1;
for (i, item) in menu.items.iter().enumerate() {
let item_no = i as i16 + 1;
let item_height = self.menu_item_height(bus, item);
let item_bottom = item_top + item_height;
let is_separator = item.text == "-";
let mark_pixel_index =
Self::menu_item_component_pixel_index(bus, menu.id, item_no, pixel_size, 4);
let name_pixel_index =
Self::menu_item_component_pixel_index(bus, menu.id, item_no, pixel_size, 10);
let command_pixel_index =
Self::menu_item_component_pixel_index(bus, menu.id, item_no, pixel_size, 16);
let cicn_icon_ptr = self.cicn_menu_icon_resource_ptr(item);
let reduced_icon_ptr = self.reduced_menu_icon_resource_ptr(item);
let small_icon_ptr = self.small_menu_icon_resource_ptr(item);
let normal_icon_ptr = self.normal_menu_icon_resource_ptr(item);
let has_app_icon_resource = cicn_icon_ptr.is_some()
|| reduced_icon_ptr.is_some()
|| small_icon_ptr.is_some()
|| normal_icon_ptr.is_some();
let has_command_key = Self::menu_item_has_command_key(item);
let provider_item_chrome = self.draw_theme_menu_item_chrome(
bus,
item_top,
left + 1,
item_bottom,
right - 1,
item.enabled,
highlighted_item == i as i16 + 1,
is_separator,
item.icon != 0 && !has_app_icon_resource,
item.mark != 0,
has_command_key,
);
let text_baseline_adjust = if attached_pulldown { -1 } else { 0 };
let text_y = item_top
+ (item_height - (metrics.ascent + metrics.descent)) / 2
+ metrics.ascent
+ text_baseline_adjust;
let classic_plain_dimmed_row =
dropdown_bg_index.is_none() && (!item.enabled || is_separator);
if is_separator {
if provider_item_chrome {
item_top = item_bottom;
continue;
}
if classic_plain_dimmed_row {
item_top = item_bottom;
continue;
}
let sep_y = item_top + item_height / 2;
for x in (left + 1)..(right - 1) {
if x % 2 == 0 {
Self::fb_set_pixel(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
x,
sep_y,
true,
);
}
}
item_top = item_bottom;
continue;
}
if classic_plain_dimmed_row && !provider_item_chrome {
item_top = item_bottom;
continue;
}
let is_hierarchical = Self::is_hierarchical_item(item);
let mut text_left = left + 15;
if item.mark != 0 && !is_hierarchical && !provider_item_chrome {
let mark_str: std::borrow::Cow<str> = if item.mark == 0x12 {
"\u{2713}".into() } else {
let s = String::from(item.mark as char);
s.into()
};
if let Some(pixel_index) = mark_pixel_index {
Self::fb_draw_string_styled_index(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
left + 3,
text_y,
&mark_str,
font_id,
font_size,
0,
pixel_index,
);
} else {
Self::fb_draw_string(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
left + 3,
text_y,
&mark_str,
font_id,
font_size,
);
}
}
if let Some(icon_ptr) = cicn_icon_ptr {
let icon_left = if item.mark != 0 { left + 18 } else { left + 2 };
self.draw_cicn_menu_icon(bus, icon_ptr, item_top, icon_left);
let icon_width = self
.menu_item_cicn_size(bus, item)
.map(|(width, _height)| width.max(MENU_ROW_HEIGHT))
.unwrap_or(MENU_ROW_HEIGHT);
text_left = icon_left + icon_width;
} else if let Some(icon_ptr) = normal_icon_ptr {
let icon_left = if item.mark != 0 { left + 18 } else { left + 2 };
self.draw_menu_icon_bitmap(
bus,
icon_ptr,
item_top,
icon_left,
MENU_NORMAL_ICON_SIZE,
name_pixel_index,
);
text_left = left + MENU_NORMAL_ICON_TEXT_LEFT_OFFSET;
} else if let Some(icon_ptr) = reduced_icon_ptr {
let icon_left = if item.mark != 0 { left + 18 } else { left + 2 };
self.draw_menu_icon_bitmap(
bus,
icon_ptr,
item_top,
icon_left,
MENU_ROW_HEIGHT,
name_pixel_index,
);
text_left = icon_left + MENU_ROW_HEIGHT;
} else if let Some(icon_ptr) = small_icon_ptr {
let icon_left = if item.mark != 0 { left + 18 } else { left + 2 };
self.draw_sicn_menu_icon(bus, icon_ptr, item_top, icon_left, name_pixel_index);
text_left = icon_left + MENU_ROW_HEIGHT;
} else if Self::menu_item_uses_normal_icon(item) {
text_left = left + MENU_NORMAL_ICON_TEXT_LEFT_OFFSET;
}
if let Some(pixel_index) = name_pixel_index {
Self::fb_draw_string_styled_index(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
text_left,
text_y,
&item.text,
font_id,
font_size,
item.style,
pixel_index,
);
} else {
Self::fb_draw_string_styled(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
text_left,
text_y,
&item.text,
font_id,
font_size,
item.style,
);
}
if is_hierarchical {
let tri_mid_y = item_top + item_height / 2;
for dx in 0..7 {
let x = right - 12 + dx;
let half_height = dx.min(6 - dx);
for dy in -half_height..=half_height {
Self::fb_set_pixel(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
x,
tri_mid_y + dy,
true,
);
}
}
}
if has_command_key && !provider_item_chrome {
let cmd_str = format!("\u{2318}{}", item.key_equiv as char);
let command_left = right - 25;
if let Some(pixel_index) = command_pixel_index {
Self::fb_draw_string_styled_index(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
command_left,
text_y,
&cmd_str,
font_id,
font_size,
0,
pixel_index,
);
} else {
Self::fb_draw_string_styled(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
command_left,
text_y,
&cmd_str,
font_id,
font_size,
0,
);
}
}
if !item.enabled && !provider_item_chrome {
for y in item_top..item_bottom {
for x in (left + 1)..(right - 1) {
if (x + y) % 2 == 0 {
Self::fb_set_pixel(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
x,
y,
false,
);
}
}
}
}
item_top = item_bottom;
}
}
pub(super) fn save_dropdown_pixels(
&self,
bus: &MacMemoryBus,
rect: (i16, i16, i16, i16),
) -> Vec<u8> {
let (screen_base, row_bytes, _, screen_h, pixel_size) = self.get_screen_params();
let (top, left, bottom, right) = rect;
let save_bottom = bottom + 1;
let save_right = right + 1;
let mut saved = Vec::new();
let screen_h_i16 = screen_h;
for y in top..save_bottom {
if y < 0 || y >= screen_h_i16 {
continue;
}
let row_start = screen_base + (y as u32) * row_bytes;
let (byte_left, byte_right) = if pixel_size == 1 {
(
(left.max(0) as u32) / 8,
(save_right.max(0) as u32).div_ceil(8),
)
} else {
(left.max(0) as u32, save_right.max(0) as u32)
};
let bx_end = byte_right.min(row_bytes);
for bx in byte_left..bx_end {
saved.push(bus.read_byte(row_start + bx));
}
}
saved
}
pub(super) fn restore_dropdown_pixels(
&self,
bus: &mut MacMemoryBus,
rect: (i16, i16, i16, i16),
saved: &[u8],
) {
let (screen_base, row_bytes, _, screen_h, pixel_size) = self.get_screen_params();
let (top, left, bottom, right) = rect;
let save_bottom = bottom + 1;
let save_right = right + 1;
let (byte_left, byte_right) = if pixel_size == 1 {
(
(left.max(0) as u32) / 8,
(save_right.max(0) as u32).div_ceil(8),
)
} else {
(left.max(0) as u32, save_right.max(0) as u32)
};
let bx_end = byte_right.min(row_bytes);
let bytes_per_row = bx_end.saturating_sub(byte_left);
let mut idx = 0;
let screen_h_i16 = screen_h;
for y in top..save_bottom {
if y < 0 || y >= screen_h_i16 {
continue;
}
let row_start = screen_base + (y as u32) * row_bytes;
for bx in byte_left..(byte_left + bytes_per_row) {
if idx < saved.len() {
bus.write_byte(row_start + bx, saved[idx]);
idx += 1;
}
}
}
}
pub(super) fn invert_dropdown_item_rect(
&self,
bus: &mut MacMemoryBus,
menu_idx: usize,
rect: (i16, i16, i16, i16),
item: i16,
) {
let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
self.get_screen_params();
let (top, left, _bottom, right) = rect;
if item < 1 || menu_idx >= self.menus.len() {
return;
}
let menu = &self.menus[menu_idx];
let mut item_top = top + 1;
for prior in menu.items.iter().take((item - 1) as usize) {
item_top += self.menu_item_height(bus, prior);
}
let Some(target_item) = menu.items.get((item - 1) as usize) else {
return;
};
let item_bottom = item_top + self.menu_item_height(bus, target_item);
let background_index = Self::menu_dropdown_background_pixel_index(bus, menu.id, pixel_size);
let hilite_indexes = background_index.and_then(|background| {
self.menu_hilite_pixel_indexes(bus, Some(background), pixel_size)
});
for y in item_top..item_bottom {
for x in (left + 1)..(right - 1) {
if x >= 0 && x < screen_width && y >= 0 && y < screen_height {
if pixel_size == 1 {
let byte_offset = (y as u32) * row_bytes + (x as u32 / 8);
let bit = 7 - (x as u32 % 8);
let addr = screen_base + byte_offset;
let b = bus.read_byte(addr);
bus.write_byte(addr, b ^ (1 << bit));
} else if let Some((background, hilite)) = hilite_indexes {
let addr = screen_base + (y as u32) * row_bytes + (x as u32);
let b = bus.read_byte(addr);
bus.write_byte(addr, Self::menu_hilited_pixel_index(b, background, hilite));
} else {
let addr = screen_base + (y as u32) * row_bytes + (x as u32);
let b = bus.read_byte(addr);
bus.write_byte(addr, Self::menu_plain_hilited_pixel_index(bus, b));
}
}
}
}
}
pub(super) fn invert_menu_item(&self, bus: &mut MacMemoryBus, item: i16) {
if let Some(ref tracking) = self.menu_tracking {
self.invert_dropdown_item_rect(bus, tracking.active_menu, tracking.dropdown_rect, item);
}
}
fn invert_menu_bar_rect(
&self,
bus: &mut MacMemoryBus,
top: i16,
left: i16,
bottom: i16,
right: i16,
) {
let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
self.get_screen_params();
let top = top.max(0).min(screen_height);
let left = left.max(0).min(screen_width);
let bottom = bottom.max(0).min(screen_height);
let right = right.max(0).min(screen_width);
if top >= bottom || left >= right {
return;
}
for y in top..bottom {
for x in left..right {
if pixel_size == 1 {
let byte_offset = (y as u32) * row_bytes + (x as u32 / 8);
let bit = 7 - (x as u32 % 8);
let addr = screen_base + byte_offset;
let b = bus.read_byte(addr);
bus.write_byte(addr, b ^ (1 << bit));
} else if pixel_size == 8 {
let addr = screen_base + (y as u32) * row_bytes + (x as u32);
let b = bus.read_byte(addr);
bus.write_byte(addr, Self::menu_plain_hilited_pixel_index(bus, b));
}
}
}
}
fn flash_menu_bar(&self, bus: &mut MacMemoryBus, menu_id: i16) {
if self.fullscreen_locked || self.menu_bar_hidden {
return;
}
let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
self.get_screen_params();
let menu_bar_height = bus.read_word(addr::MBAR_HEIGHT) as i16;
if menu_bar_height <= 0 {
return;
}
if !matches!(pixel_size, 1 | 8) {
return;
}
if menu_id != 0 {
if let Some((_, left, right)) = self
.menu_title_regions_with_indices()
.into_iter()
.find(|(idx, _, _)| self.menus.get(*idx).is_some_and(|menu| menu.id == menu_id))
{
self.invert_menu_bar_rect(bus, 1, left - 2, menu_bar_height - 1, right + 3);
return;
}
}
self.invert_menu_bar_rect(bus, 0, 0, menu_bar_height, screen_width);
Self::fb_draw_menu_bar_rounded_corners(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
);
}
fn set_menu_tracking_highlight(&mut self, bus: &mut MacMemoryBus, item: i16) {
let Some(old_item) = self
.menu_tracking
.as_ref()
.map(|tracking| tracking.highlighted_item)
else {
return;
};
if old_item == item {
return;
}
if self.ui_theme_id() == UiThemeId::ClassicSystem7 {
if old_item > 0 {
self.invert_menu_item(bus, old_item);
}
if let Some(tracking) = self.menu_tracking.as_mut() {
tracking.highlighted_item = item;
}
if item > 0 {
self.invert_menu_item(bus, item);
}
return;
}
let Some((active_menu, dropdown_rect)) = self.menu_tracking.as_mut().map(|tracking| {
tracking.highlighted_item = item;
(tracking.active_menu, tracking.dropdown_rect)
}) else {
return;
};
self.draw_menu_dropdown(bus, active_menu, dropdown_rect);
}
pub(super) fn highlight_menu_title(&self, bus: &mut MacMemoryBus, menu_idx: usize) {
let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
self.get_screen_params();
let mut target_region: Option<(i16, i16)> = None;
for (idx, left, right) in self.menu_title_regions_with_indices() {
if idx == menu_idx {
target_region = Some((left, right));
break;
}
}
let Some((left, right)) = target_region else {
return;
};
if self.ui_theme_id() != UiThemeId::ClassicSystem7 {
let Some(menu) = self
.menus
.get(menu_idx)
.filter(|menu| menu.visible_in_menu_bar)
else {
return;
};
let menu_bar_height = bus.read_word(addr::MBAR_HEIGHT) as i16;
if menu_bar_height <= 1 {
return;
}
if self.draw_theme_menu_title_chrome(
bus,
1,
left,
menu_bar_height - 1,
right,
menu.enabled,
true,
) {
let font_id: i16 = 0;
let font_size: i16 = 12;
let metrics = crate::quickdraw::text::get_font_metrics(font_id, font_size);
let text_height = metrics.ascent + metrics.descent;
let text_y = (menu_bar_height - text_height) / 2 + metrics.ascent;
Self::fb_draw_string_styled_ink(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
left + 7,
text_y,
&menu.title,
font_id,
font_size,
0,
false,
);
}
return;
}
let background_index = self.menu_bar_background_pixel_index(bus, pixel_size);
let hilite_indexes = background_index.and_then(|background| {
self.menu_hilite_pixel_indexes(bus, Some(background), pixel_size)
});
let classic_left = left - 2;
let classic_right = right + 3;
for y in 1i16..19 {
for x in classic_left..classic_right {
if x >= 0 && x < screen_width && y >= 0 && y < screen_height {
if pixel_size == 1 {
let byte_offset = (y as u32) * row_bytes + (x as u32 / 8);
let bit = 7 - (x as u32 % 8);
let addr = screen_base + byte_offset;
let b = bus.read_byte(addr);
bus.write_byte(addr, b ^ (1 << bit));
} else if let Some((background, hilite)) = hilite_indexes {
let addr = screen_base + (y as u32) * row_bytes + (x as u32);
let b = bus.read_byte(addr);
bus.write_byte(addr, Self::menu_hilited_pixel_index(b, background, hilite));
} else {
let addr = screen_base + (y as u32) * row_bytes + (x as u32);
let b = bus.read_byte(addr);
bus.write_byte(addr, Self::menu_plain_hilited_pixel_index(bus, b));
}
}
}
}
}
pub(crate) fn fb_measure_string(s: &str, font_id: i16, font_size: i16) -> i16 {
let mut width: i16 = 0;
for ch in s.chars() {
if let Some((glyph, _)) = crate::quickdraw::text::get_glyph(font_id, font_size, ch) {
width += glyph.advance as i16;
} else {
width += 6;
}
}
width
}
}
#[cfg(test)]
mod tests {
use super::super::test_helpers::{setup, setup_with_port, MockCpu, TEST_SP};
use super::{
parse_appendmenu_items, Menu, MenuItem, MenuTrackingState, MC_ENTRY_SIZE,
MC_RESOURCE_ENTRY_SIZE, MENU_KEY_REDUCED_ICON, MENU_KEY_SMALL_ICON,
};
use crate::cpu::{CpuOps, Register};
use crate::memory::{MacMemoryBus, MemoryBus};
use crate::ui_theme::UiThemeId;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use tracing::span::{Attributes, Record};
use tracing::subscriber::Interest;
use tracing::{Event, Id, Level, Metadata, Subscriber};
struct WarnCounter {
warnings: Arc<AtomicUsize>,
}
impl Subscriber for WarnCounter {
fn enabled(&self, _metadata: &Metadata<'_>) -> bool {
true
}
fn register_callsite(&self, _metadata: &'static Metadata<'static>) -> Interest {
Interest::always()
}
fn new_span(&self, _attrs: &Attributes<'_>) -> Id {
Id::from_u64(1)
}
fn record(&self, _span: &Id, _values: &Record<'_>) {}
fn record_follows_from(&self, _span: &Id, _follows: &Id) {}
fn event(&self, event: &Event<'_>) {
if *event.metadata().level() == Level::WARN {
self.warnings.fetch_add(1, Ordering::Relaxed);
}
}
fn enter(&self, _span: &Id) {}
fn exit(&self, _span: &Id) {}
}
fn write_pstring(bus: &mut crate::memory::MacMemoryBus, ptr: u32, s: &str) {
let bytes = s.as_bytes();
bus.write_byte(ptr, bytes.len().min(255) as u8);
for (i, b) in bytes.iter().take(255).enumerate() {
bus.write_byte(ptr + 1 + i as u32, *b);
}
}
fn screen_pixel_is_set(bus: &MacMemoryBus, base: u32, row_bytes: u32, x: i16, y: i16) -> bool {
let byte = bus.read_byte(base + (y as u32 * row_bytes) + ((x as u32) / 8));
byte & (0x80u8 >> ((x as u8) & 7)) != 0
}
fn title_region_pixels(
bus: &MacMemoryBus,
base: u32,
row_bytes: u32,
left: i16,
right: i16,
) -> Vec<bool> {
let mut pixels = Vec::new();
for y in 1i16..19 {
for x in left..right {
pixels.push(screen_pixel_is_set(bus, base, row_bytes, x, y));
}
}
pixels
}
fn changed_pixel_count(before: &[bool], after: &[bool]) -> usize {
before
.iter()
.zip(after.iter())
.filter(|(lhs, rhs)| lhs != rhs)
.count()
}
fn clear_1bpp_screen(bus: &mut MacMemoryBus, base: u32, row_bytes: u32, height: u32) {
for offset in 0..(row_bytes * height) {
bus.write_byte(base + offset, 0);
}
}
fn screen_pixel_index(bus: &MacMemoryBus, base: u32, row_bytes: u32, x: i16, y: i16) -> u8 {
bus.read_byte(base + (y as u32 * row_bytes) + x as u32)
}
fn clear_8bpp_screen(bus: &mut MacMemoryBus, base: u32, row_bytes: u32, height: u32, fill: u8) {
for offset in 0..(row_bytes * height) {
bus.write_byte(base + offset, fill);
}
}
fn setup_8bpp_menu_screen(
disp: &mut super::super::TrapDispatcher,
bus: &mut MacMemoryBus,
width: u16,
height: u16,
) -> (u32, u32) {
let row_bytes = u32::from(width);
let base = bus.alloc(row_bytes * u32::from(height));
disp.set_screen_mode_for_test(base, row_bytes, width, height, 8);
clear_8bpp_screen(bus, base, row_bytes, u32::from(height), 0xEE);
bus.write_long(crate::memory::globals::addr::SCRN_BASE, base);
let gdevice_handle = disp.ensure_main_gdevice(bus);
bus.write_long(0x08A4, gdevice_handle); bus.write_long(0x0CC8, gdevice_handle); (base, row_bytes)
}
fn menu_icon_source_with_left_stripe() -> [u8; 128] {
let mut icon = [0u8; 128];
for row in 0..32 {
icon[row * 4] = 0x30;
}
icon
}
fn sicn_source_with_left_stripe() -> [u8; 64] {
let mut sicn = [0u8; 64];
for row in 0..16 {
sicn[row * 2] = 0x30;
sicn[32 + row * 2] = 0xFF;
sicn[32 + row * 2 + 1] = 0xFF;
}
sicn
}
fn sicn_source_with_right_stripe() -> [u8; 64] {
let mut sicn = [0u8; 64];
for row in 0..16 {
sicn[row * 2 + 1] = 0x03;
sicn[32 + row * 2] = 0xFF;
sicn[32 + row * 2 + 1] = 0xFF;
}
sicn
}
fn write_be_word(data: &mut [u8], offset: usize, value: u16) {
data[offset] = (value >> 8) as u8;
data[offset + 1] = value as u8;
}
fn cicn_source_with_left_stripe(width: u16, height: u16) -> Vec<u8> {
let row_bytes = u32::from(width).div_ceil(8);
let mask_size = row_bytes * u32::from(height);
let bmap_size = row_bytes * u32::from(height);
let ctab_size = 16u32;
let pixel_size = row_bytes * u32::from(height);
let bmap_offset = 82 + mask_size as usize;
let ctab_offset = bmap_offset + bmap_size as usize;
let pixel_offset = ctab_offset + ctab_size as usize;
let mut data = vec![0u8; pixel_offset + pixel_size as usize];
write_be_word(&mut data, 4, row_bytes as u16);
write_be_word(&mut data, 10, height);
write_be_word(&mut data, 12, width);
write_be_word(&mut data, 32, 1);
write_be_word(&mut data, 54, row_bytes as u16);
write_be_word(&mut data, 60, height);
write_be_word(&mut data, 62, width);
write_be_word(&mut data, 68, row_bytes as u16);
write_be_word(&mut data, 74, height);
write_be_word(&mut data, 76, width);
write_be_word(&mut data, ctab_offset + 6, 0);
for row in 0..usize::from(height) {
let mask_row = 82 + row * row_bytes as usize;
let bmap_row = bmap_offset + row * row_bytes as usize;
let pixel_row = pixel_offset + row * row_bytes as usize;
for col in 0..row_bytes as usize {
data[mask_row + col] = 0xFF;
}
data[bmap_row] = 0x30;
data[pixel_row] = 0x30;
}
data
}
fn seed_menu_resource(bus: &mut crate::memory::MacMemoryBus, menu_id: i16, title: &str) -> u32 {
let menu_res_ptr = bus.alloc(256);
bus.write_word(menu_res_ptr, menu_id as u16);
bus.write_word(menu_res_ptr + 2, 0);
bus.write_word(menu_res_ptr + 4, 0);
bus.write_long(menu_res_ptr + 6, 0);
bus.write_long(menu_res_ptr + 10, 0xFFFF_FFFF);
write_pstring(bus, menu_res_ptr + 14, title);
bus.write_byte(menu_res_ptr + 15 + title.len() as u32, 0);
menu_res_ptr
}
fn seed_mbar_resource(bus: &mut crate::memory::MacMemoryBus, menu_ids: &[i16]) -> u32 {
let mbar_ptr = bus.alloc((2 + 2 * menu_ids.len()) as u32);
bus.write_word(mbar_ptr, menu_ids.len() as u16);
for (idx, menu_id) in menu_ids.iter().enumerate() {
bus.write_word(mbar_ptr + 2 + (idx as u32) * 2, *menu_id as u16);
}
mbar_ptr
}
fn new_menu_with_title(
disp: &mut super::super::TrapDispatcher,
cpu: &mut MockCpu,
bus: &mut crate::memory::MacMemoryBus,
menu_id: i16,
title_ptr: u32,
title: &str,
) -> u32 {
write_pstring(bus, title_ptr, title);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, title_ptr);
bus.write_word(TEST_SP + 4, menu_id as u16);
assert!(
disp.dispatch_menu(true, 0x131, cpu, bus).unwrap().is_ok(),
"NewMenu should succeed"
);
bus.read_long(cpu.read_reg(Register::A7))
}
fn append_menu_data(
disp: &mut super::super::TrapDispatcher,
cpu: &mut MockCpu,
bus: &mut crate::memory::MacMemoryBus,
menu_handle: u32,
data_ptr: u32,
data: &str,
) {
write_pstring(bus, data_ptr, data);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, data_ptr);
bus.write_long(TEST_SP + 4, menu_handle);
assert!(
disp.dispatch_menu(true, 0x133, cpu, bus).unwrap().is_ok(),
"AppendMenu should succeed"
);
}
fn set_menu_item_style(
disp: &mut super::super::TrapDispatcher,
cpu: &mut MockCpu,
bus: &mut crate::memory::MacMemoryBus,
menu_handle: u32,
item: i16,
style: u8,
) {
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, style as u16);
bus.write_word(TEST_SP + 2, item as u16);
bus.write_long(TEST_SP + 4, menu_handle);
assert!(
disp.dispatch_menu(true, 0x142, cpu, bus).unwrap().is_ok(),
"SetItemStyle should succeed"
);
}
fn calc_menu_size_for_test(
disp: &mut super::super::TrapDispatcher,
cpu: &mut MockCpu,
bus: &mut crate::memory::MacMemoryBus,
menu_handle: u32,
) -> (i16, i16) {
let menu_ptr = bus.read_long(menu_handle);
bus.write_word(menu_ptr + 4, 0);
bus.write_word(menu_ptr + 6, 0);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, menu_handle);
assert!(
disp.dispatch_menu(true, 0x148, cpu, bus).unwrap().is_ok(),
"CalcMenuSize should succeed"
);
(
bus.read_word(menu_ptr + 2) as i16,
bus.read_word(menu_ptr + 4) as i16,
)
}
fn write_rgb(bus: &mut crate::memory::MacMemoryBus, ptr: u32, rgb: (u16, u16, u16)) {
bus.write_word(ptr, rgb.0);
bus.write_word(ptr + 2, rgb.1);
bus.write_word(ptr + 4, rgb.2);
}
fn write_mc_entry(
bus: &mut crate::memory::MacMemoryBus,
ptr: u32,
menu_id: i16,
item: i16,
seed: u16,
) {
bus.write_word(ptr, menu_id as u16);
bus.write_word(ptr + 2, item as u16);
write_rgb(
bus,
ptr + 4,
(seed, seed.wrapping_add(1), seed.wrapping_add(2)),
);
write_rgb(
bus,
ptr + 10,
(
seed.wrapping_add(3),
seed.wrapping_add(4),
seed.wrapping_add(5),
),
);
write_rgb(
bus,
ptr + 16,
(
seed.wrapping_add(6),
seed.wrapping_add(7),
seed.wrapping_add(8),
),
);
write_rgb(
bus,
ptr + 22,
(
seed.wrapping_add(9),
seed.wrapping_add(10),
seed.wrapping_add(11),
),
);
bus.write_word(ptr + 28, 0);
}
fn write_mc_entry_colors(
bus: &mut crate::memory::MacMemoryBus,
ptr: u32,
menu_id: i16,
item: i16,
rgb1: (u16, u16, u16),
rgb2: (u16, u16, u16),
rgb3: (u16, u16, u16),
rgb4: (u16, u16, u16),
) {
bus.write_word(ptr, menu_id as u16);
bus.write_word(ptr + 2, item as u16);
write_rgb(bus, ptr + 4, rgb1);
write_rgb(bus, ptr + 10, rgb2);
write_rgb(bus, ptr + 16, rgb3);
write_rgb(bus, ptr + 22, rgb4);
bus.write_word(ptr + 28, 0);
}
fn write_rgb_bytes(data: &mut [u8], offset: usize, rgb: (u16, u16, u16)) {
write_be_word(data, offset, rgb.0);
write_be_word(data, offset + 2, rgb.1);
write_be_word(data, offset + 4, rgb.2);
}
fn compiled_mctb_resource(entries: &[(i16, i16, u16)]) -> Vec<u8> {
let mut data = vec![0u8; 2 + entries.len() * MC_RESOURCE_ENTRY_SIZE];
write_be_word(&mut data, 0, entries.len() as u16);
for (idx, &(menu_id, item, seed)) in entries.iter().enumerate() {
let base = 2 + idx * MC_RESOURCE_ENTRY_SIZE;
write_be_word(&mut data, base, menu_id as u16);
write_be_word(&mut data, base + 2, item as u16);
write_rgb_bytes(
&mut data,
base + 4,
(seed, seed.wrapping_add(1), seed.wrapping_add(2)),
);
write_rgb_bytes(
&mut data,
base + 10,
(
seed.wrapping_add(3),
seed.wrapping_add(4),
seed.wrapping_add(5),
),
);
write_rgb_bytes(
&mut data,
base + 16,
(
seed.wrapping_add(6),
seed.wrapping_add(7),
seed.wrapping_add(8),
),
);
write_rgb_bytes(
&mut data,
base + 22,
(
seed.wrapping_add(9),
seed.wrapping_add(10),
seed.wrapping_add(11),
),
);
}
data
}
fn install_mctb_resource(
disp: &mut super::super::TrapDispatcher,
bus: &mut crate::memory::MacMemoryBus,
resource_id: i16,
entries: &[(i16, i16, u16)],
) {
let data = compiled_mctb_resource(entries);
disp.install_test_resource(bus, *b"mctb", resource_id, &data);
}
fn set_mc_entries_for_test(
disp: &mut super::super::TrapDispatcher,
cpu: &mut MockCpu,
bus: &mut crate::memory::MacMemoryBus,
entries: &[(i16, i16, u16)],
) {
let ptr = bus.alloc((entries.len() * MC_ENTRY_SIZE) as u32);
for (idx, &(menu_id, item, seed)) in entries.iter().enumerate() {
write_mc_entry(bus, ptr + (idx * MC_ENTRY_SIZE) as u32, menu_id, item, seed);
}
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, ptr);
bus.write_word(TEST_SP + 4, entries.len() as u16);
assert!(
disp.dispatch_menu(true, 0x265, cpu, bus).unwrap().is_ok(),
"SetMCEntries should succeed"
);
}
fn get_mc_entry_ptr_for_test(
disp: &mut super::super::TrapDispatcher,
cpu: &mut MockCpu,
bus: &mut crate::memory::MacMemoryBus,
menu_id: i16,
item: i16,
) -> u32 {
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, item as u16);
bus.write_word(TEST_SP + 2, menu_id as u16);
bus.write_long(TEST_SP + 4, 0xDEADBEEF);
assert!(
disp.dispatch_menu(true, 0x264, cpu, bus).unwrap().is_ok(),
"GetMCEntry should succeed"
);
assert_eq!(
cpu.read_reg(Register::A7),
TEST_SP + 4,
"GetMCEntry should pop only the menuID/menuItem arguments"
);
bus.read_long(TEST_SP + 4)
}
fn insert_menu_item_data(
disp: &mut super::super::TrapDispatcher,
cpu: &mut MockCpu,
bus: &mut crate::memory::MacMemoryBus,
menu_handle: u32,
data_ptr: u32,
data: &str,
after_item: i16,
) {
write_pstring(bus, data_ptr, data);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, after_item as u16);
bus.write_long(TEST_SP + 2, data_ptr);
bus.write_long(TEST_SP + 6, menu_handle);
assert!(
disp.dispatch_menu(true, 0x026, cpu, bus).unwrap().is_ok(),
"InsertMenuItem should succeed"
);
}
fn insert_menu(
disp: &mut super::super::TrapDispatcher,
cpu: &mut MockCpu,
bus: &mut crate::memory::MacMemoryBus,
menu_handle: u32,
) {
insert_menu_before_id(disp, cpu, bus, menu_handle, 0);
}
fn insert_menu_before(
disp: &mut super::super::TrapDispatcher,
cpu: &mut MockCpu,
bus: &mut crate::memory::MacMemoryBus,
menu_handle: u32,
before_id: i16,
) {
insert_menu_before_id(disp, cpu, bus, menu_handle, before_id);
}
fn insert_menu_before_id(
disp: &mut super::super::TrapDispatcher,
cpu: &mut MockCpu,
bus: &mut crate::memory::MacMemoryBus,
menu_handle: u32,
before_id: i16,
) {
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, before_id as u16);
bus.write_long(TEST_SP + 2, menu_handle);
assert!(
disp.dispatch_menu(true, 0x135, cpu, bus).unwrap().is_ok(),
"InsertMenu should succeed"
);
}
fn get_mhandle_for_id(
disp: &mut super::super::TrapDispatcher,
cpu: &mut MockCpu,
bus: &mut crate::memory::MacMemoryBus,
menu_id: i16,
) -> u32 {
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, menu_id as u16);
assert!(
disp.dispatch_menu(true, 0x149, cpu, bus).unwrap().is_ok(),
"GetMHandle should succeed"
);
bus.read_long(cpu.read_reg(Register::A7))
}
fn delete_menu_by_id(
disp: &mut super::super::TrapDispatcher,
cpu: &mut MockCpu,
bus: &mut crate::memory::MacMemoryBus,
menu_id: i16,
) {
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, menu_id as u16);
assert!(
disp.dispatch_menu(true, 0x136, cpu, bus).unwrap().is_ok(),
"DeleteMenu should succeed"
);
}
fn dispose_menu_by_handle(
disp: &mut super::super::TrapDispatcher,
cpu: &mut MockCpu,
bus: &mut crate::memory::MacMemoryBus,
menu_handle: u32,
) {
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, menu_handle);
assert!(
disp.dispatch_menu(true, 0x132, cpu, bus).unwrap().is_ok(),
"DisposeMenu should succeed"
);
}
fn menu_key_result(
disp: &mut super::super::TrapDispatcher,
cpu: &mut MockCpu,
bus: &mut crate::memory::MacMemoryBus,
key: u8,
) -> u32 {
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, key as u16);
assert!(
disp.dispatch_menu(true, 0x13E, cpu, bus).unwrap().is_ok(),
"MenuKey should succeed"
);
bus.read_long(cpu.read_reg(Register::A7))
}
fn menu_key_result_and_stack(
disp: &mut super::super::TrapDispatcher,
cpu: &mut MockCpu,
bus: &mut crate::memory::MacMemoryBus,
key: u8,
) -> (u32, u32) {
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, key as u16);
bus.write_long(TEST_SP + 2, 0xDEAD_BEEF);
assert!(
disp.dispatch_menu(true, 0x13E, cpu, bus).unwrap().is_ok(),
"MenuKey should succeed"
);
(bus.read_long(TEST_SP + 2), cpu.read_reg(Register::A7))
}
fn set_item_cmd(
disp: &mut super::super::TrapDispatcher,
cpu: &mut MockCpu,
bus: &mut crate::memory::MacMemoryBus,
menu_handle: u32,
item: i16,
cmd: u8,
) {
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, cmd as u16);
bus.write_word(TEST_SP + 2, item as u16);
bus.write_long(TEST_SP + 4, menu_handle);
assert!(
disp.dispatch_menu(true, 0x04F, cpu, bus).unwrap().is_ok(),
"SetItemCmd should succeed"
);
}
fn set_item_mark(
disp: &mut super::super::TrapDispatcher,
cpu: &mut MockCpu,
bus: &mut crate::memory::MacMemoryBus,
menu_handle: u32,
item: i16,
mark: u8,
) {
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, mark as u16);
bus.write_word(TEST_SP + 2, item as u16);
bus.write_long(TEST_SP + 4, menu_handle);
assert!(
disp.dispatch_menu(true, 0x144, cpu, bus).unwrap().is_ok(),
"SetItemMark should succeed"
);
}
fn get_item_cmd(
disp: &mut super::super::TrapDispatcher,
cpu: &mut MockCpu,
bus: &mut crate::memory::MacMemoryBus,
menu_handle: u32,
item: i16,
) -> u8 {
let out_ptr = 0x306700u32;
bus.write_word(out_ptr, 0xFFFF);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, out_ptr);
bus.write_word(TEST_SP + 4, item as u16);
bus.write_long(TEST_SP + 6, menu_handle);
assert!(
disp.dispatch_menu(true, 0x04E, cpu, bus).unwrap().is_ok(),
"GetItemCmd should succeed"
);
(bus.read_word(out_ptr) & 0xFF) as u8
}
#[test]
fn addresmenu_appends_named_resources_as_enabled_plain_items_and_skips_hidden_names() {
let (mut disp, mut cpu, mut bus) = setup();
let handle = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 330, 0x306800, "Apple");
append_menu_data(&mut disp, &mut cpu, &mut bus, handle, 0x306900, "Existing");
disp.resources = Some(crate::trap::dispatch::LoadedResources {
files: std::collections::HashMap::from([(
0,
crate::trap::dispatch::ResourceFileMap {
loaded: std::collections::HashMap::new(),
named: std::collections::HashMap::from([
((*b"DRVR", ".HiddenDA".to_string()), (101, 0)),
((*b"DRVR", "%MetaDA".to_string()), (102, 0)),
((*b"DRVR", "Calculator".to_string()), (103, 0)),
((*b"DRVR", "Chooser".to_string()), (104, 0)),
((*b"MENU", "NotDriver".to_string()), (105, 0)),
]),
names_by_id: std::collections::HashMap::new(),
attrs: std::collections::HashMap::new(),
map_attrs: 0,
},
)]),
names: std::collections::HashMap::new(),
search_order: vec![0],
current_file: 0,
});
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, u32::from_be_bytes(*b"DRVR"));
bus.write_long(TEST_SP + 4, handle);
assert!(
disp.dispatch_menu(true, 0x14D, &mut cpu, &mut bus)
.unwrap()
.is_ok(),
"AddResMenu should succeed"
);
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
let menu = disp
.menus
.iter()
.find(|m| m.handle == handle)
.expect("menu should remain registered");
assert_eq!(menu.items[0].text, "Existing");
assert_eq!(menu.items.len(), 3, "two visible DRVR names should append");
assert!(
menu.items.iter().any(|it| it.text == "Calculator"),
"visible DRVR names should be appended"
);
assert!(
menu.items.iter().any(|it| it.text == "Chooser"),
"visible DRVR names should be appended"
);
assert!(
!menu.items.iter().any(|it| it.text == ".HiddenDA"),
"leading '.' names must be skipped"
);
assert!(
!menu.items.iter().any(|it| it.text == "%MetaDA"),
"leading '%' names must be skipped"
);
for item in menu
.items
.iter()
.filter(|it| it.text == "Calculator" || it.text == "Chooser")
{
assert!(item.enabled, "new AddResMenu items should be enabled");
assert_eq!(item.icon, 0, "new AddResMenu items should have no icon");
assert_eq!(item.mark, 0, "new AddResMenu items should have no mark");
assert_eq!(item.style, 0, "new AddResMenu items should be plain style");
}
}
#[test]
fn setitemstyle_consumes_menu_item_and_style_arguments() {
let (mut disp, mut cpu, mut bus) = setup();
let handle = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 340, 0x306A10, "File");
append_menu_data(&mut disp, &mut cpu, &mut bus, handle, 0x306A20, "Open");
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 0x0005);
bus.write_word(TEST_SP + 2, 1);
bus.write_long(TEST_SP + 4, handle);
assert!(
disp.dispatch_menu(true, 0x142, &mut cpu, &mut bus)
.unwrap()
.is_ok(),
"SetItemStyle should succeed"
);
assert_eq!(
cpu.read_reg(Register::A7),
TEST_SP + 8,
"SetItemStyle should consume style, item, and menu arguments"
);
}
#[test]
fn setitemstyle_updates_target_menu_item_style() {
let (mut disp, mut cpu, mut bus) = setup();
let handle = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 341, 0x306A30, "Edit");
append_menu_data(&mut disp, &mut cpu, &mut bus, handle, 0x306A40, "Paste");
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 0x0006);
bus.write_word(TEST_SP + 2, 1);
bus.write_long(TEST_SP + 4, handle);
assert!(
disp.dispatch_menu(true, 0x142, &mut cpu, &mut bus)
.unwrap()
.is_ok(),
"SetItemStyle should succeed"
);
let menu = disp
.menus
.iter()
.find(|m| m.handle == handle)
.expect("menu should exist");
assert_eq!(
menu.items[0].style, 0x06,
"SetItemStyle should store the requested style byte on the target item"
);
}
#[test]
fn getitemstyle_consumes_menu_item_and_stylevar_arguments() {
let (mut disp, mut cpu, mut bus) = setup();
let handle = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 342, 0x306A50, "View");
append_menu_data(&mut disp, &mut cpu, &mut bus, handle, 0x306A60, "Status");
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 0x0003);
bus.write_word(TEST_SP + 2, 1);
bus.write_long(TEST_SP + 4, handle);
assert!(
disp.dispatch_menu(true, 0x142, &mut cpu, &mut bus)
.unwrap()
.is_ok(),
"SetItemStyle should succeed"
);
let out_ptr = 0x306A70u32;
bus.write_word(out_ptr, 0xFFFF);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, out_ptr);
bus.write_word(TEST_SP + 4, 1);
bus.write_long(TEST_SP + 6, handle);
assert!(
disp.dispatch_menu(true, 0x141, &mut cpu, &mut bus)
.unwrap()
.is_ok(),
"GetItemStyle should succeed"
);
assert_eq!(
cpu.read_reg(Register::A7),
TEST_SP + 10,
"GetItemStyle should consume chStyle pointer, item, and menu arguments"
);
}
#[test]
fn getitemstyle_writes_current_item_style_to_output_pointer() {
let (mut disp, mut cpu, mut bus) = setup();
let handle = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 343, 0x306A80, "Window");
append_menu_data(&mut disp, &mut cpu, &mut bus, handle, 0x306A90, "Zoom");
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 0x0009);
bus.write_word(TEST_SP + 2, 1);
bus.write_long(TEST_SP + 4, handle);
assert!(
disp.dispatch_menu(true, 0x142, &mut cpu, &mut bus)
.unwrap()
.is_ok(),
"SetItemStyle should succeed"
);
let out_ptr = 0x306AA0u32;
bus.write_word(out_ptr, 0xFFFF);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, out_ptr);
bus.write_word(TEST_SP + 4, 1);
bus.write_long(TEST_SP + 6, handle);
assert!(
disp.dispatch_menu(true, 0x141, &mut cpu, &mut bus)
.unwrap()
.is_ok(),
"GetItemStyle should succeed"
);
assert_eq!(
bus.read_word(out_ptr),
0x0009,
"GetItemStyle should write the current style value to the chStyle output pointer"
);
}
#[test]
fn setmenuflash_consumes_count_argument() {
let (mut disp, mut cpu, mut bus) = setup();
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 7);
assert!(
disp.dispatch_menu(true, 0x14A, &mut cpu, &mut bus)
.unwrap()
.is_ok(),
"SetMenuFlash should succeed"
);
assert_eq!(
cpu.read_reg(Register::A7),
TEST_SP + 2,
"SetMenuFlash should consume one INTEGER argument"
);
}
#[test]
fn setmenuflash_writes_count_to_menuflash_global() {
let (mut disp, mut cpu, mut bus) = setup();
bus.write_word(crate::memory::globals::addr::MENU_FLASH, 3);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 1);
assert!(
disp.dispatch_menu(true, 0x14A, &mut cpu, &mut bus)
.unwrap()
.is_ok(),
"SetMenuFlash should succeed"
);
assert_eq!(
bus.read_word(crate::memory::globals::addr::MENU_FLASH),
1,
"SetMenuFlash should update the MenuFlash low-memory word"
);
}
#[test]
fn insertresmenu_consumes_menu_type_and_afteritem_arguments() {
let (mut disp, mut cpu, mut bus) = setup();
let handle = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 344, 0x306AB0, "Apple");
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 0);
bus.write_long(TEST_SP + 2, u32::from_be_bytes(*b"DRVR"));
bus.write_long(TEST_SP + 6, handle);
assert!(
disp.dispatch_menu(true, 0x151, &mut cpu, &mut bus)
.unwrap()
.is_ok(),
"InsertResMenu should succeed"
);
assert_eq!(
cpu.read_reg(Register::A7),
TEST_SP + 10,
"InsertResMenu should consume afterItem, type, and menu arguments"
);
}
#[test]
fn insertresmenu_inserts_visible_resource_names_at_requested_position() {
let (mut disp, mut cpu, mut bus) = setup();
let handle = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 345, 0x306AC0, "Apple");
append_menu_data(
&mut disp,
&mut cpu,
&mut bus,
handle,
0x306AD0,
"Existing;Tail",
);
disp.resources = Some(crate::trap::dispatch::LoadedResources {
files: std::collections::HashMap::from([(
0,
crate::trap::dispatch::ResourceFileMap {
loaded: std::collections::HashMap::new(),
named: std::collections::HashMap::from([
((*b"DRVR", ".HiddenDA".to_string()), (101, 0)),
((*b"DRVR", "%MetaDA".to_string()), (102, 0)),
((*b"DRVR", "Alpha".to_string()), (103, 0)),
((*b"DRVR", "Beta".to_string()), (104, 0)),
]),
names_by_id: std::collections::HashMap::new(),
attrs: std::collections::HashMap::new(),
map_attrs: 0,
},
)]),
names: std::collections::HashMap::new(),
search_order: vec![0],
current_file: 0,
});
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 1);
bus.write_long(TEST_SP + 2, u32::from_be_bytes(*b"DRVR"));
bus.write_long(TEST_SP + 6, handle);
assert!(
disp.dispatch_menu(true, 0x151, &mut cpu, &mut bus)
.unwrap()
.is_ok(),
"InsertResMenu should succeed"
);
let menu = disp
.menus
.iter()
.find(|m| m.handle == handle)
.expect("menu should remain registered");
let names: Vec<&str> = menu.items.iter().map(|it| it.text.as_str()).collect();
assert_eq!(
names,
vec!["Existing", "Alpha", "Beta", "Tail"],
"InsertResMenu should insert visible names after afterItem position"
);
assert!(
!menu.items.iter().any(|it| it.text == ".HiddenDA"),
"InsertResMenu should skip names starting with '.'"
);
assert!(
!menu.items.iter().any(|it| it.text == "%MetaDA"),
"InsertResMenu should skip names starting with '%'"
);
}
#[test]
fn setitemicon_sets_requested_icon_number_for_target_item() {
let (mut disp, mut cpu, mut bus) = setup();
let handle = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 331, 0x306A00, "File");
append_menu_data(&mut disp, &mut cpu, &mut bus, handle, 0x306B00, "Open");
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 7);
bus.write_word(TEST_SP + 2, 1);
bus.write_long(TEST_SP + 4, handle);
assert!(
disp.dispatch_menu(true, 0x140, &mut cpu, &mut bus)
.unwrap()
.is_ok(),
"SetItemIcon should succeed"
);
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
let menu = disp
.menus
.iter()
.find(|m| m.handle == handle)
.expect("menu should exist");
assert_eq!(menu.items[0].icon, 7, "SetItemIcon should store icon byte");
let icon_ptr = 0x306C00u32;
bus.write_word(icon_ptr, 0xFFFF);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, icon_ptr);
bus.write_word(TEST_SP + 4, 1);
bus.write_long(TEST_SP + 6, handle);
assert!(
disp.dispatch_menu(true, 0x13F, &mut cpu, &mut bus)
.unwrap()
.is_ok(),
"GetItemIcon should succeed"
);
assert_eq!(
cpu.read_reg(Register::A7),
TEST_SP + 10,
"GetItemIcon should pop 10 bytes from stack"
);
assert_eq!(bus.read_word(icon_ptr), 7);
}
#[test]
fn getitemicon_returns_zero_when_item_has_no_icon() {
let (mut disp, mut cpu, mut bus) = setup();
let handle = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 332, 0x306D80, "Edit");
append_menu_data(&mut disp, &mut cpu, &mut bus, handle, 0x306D90, "Paste");
let icon_ptr = 0x306DA0u32;
bus.write_word(icon_ptr, 0xFFFF);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, icon_ptr);
bus.write_word(TEST_SP + 4, 1);
bus.write_long(TEST_SP + 6, handle);
assert!(
disp.dispatch_menu(true, 0x13F, &mut cpu, &mut bus)
.unwrap()
.is_ok(),
"GetItemIcon should succeed"
);
assert_eq!(
cpu.read_reg(Register::A7),
TEST_SP + 10,
"GetItemIcon should pop one pointer, one item index, and one menu handle"
);
assert_eq!(
bus.read_word(icon_ptr),
0,
"GetItemIcon should write 0 when no icon is associated with the item"
);
}
#[test]
fn calcmenusize_writes_recalculated_menuwidth_and_menuheight_fields() {
let (mut disp, mut cpu, mut bus) = setup();
let handle = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 333, 0x306DB0, "File");
append_menu_data(&mut disp, &mut cpu, &mut bus, handle, 0x306DC0, "Open");
let menu_ptr = bus.read_long(handle);
bus.write_word(menu_ptr + 2, 0);
bus.write_word(menu_ptr + 4, 0);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, handle);
assert!(
disp.dispatch_menu(true, 0x148, &mut cpu, &mut bus)
.unwrap()
.is_ok(),
"CalcMenuSize should succeed"
);
assert_eq!(
cpu.read_reg(Register::A7),
TEST_SP + 4,
"CalcMenuSize should pop one MenuHandle argument"
);
let width = bus.read_word(menu_ptr + 2) as i16;
let height = bus.read_word(menu_ptr + 4) as i16;
assert!(
width > 0 && height > 0,
"CalcMenuSize should write nonzero width and height into the menu record"
);
}
#[test]
fn calcmenusize_recomputes_dimensions_after_menu_contents_change() {
let (mut disp, mut cpu, mut bus) = setup();
let handle = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 334, 0x306DD0, "File");
append_menu_data(&mut disp, &mut cpu, &mut bus, handle, 0x306DE0, "Open");
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, handle);
assert!(
disp.dispatch_menu(true, 0x148, &mut cpu, &mut bus)
.unwrap()
.is_ok(),
"initial CalcMenuSize should succeed"
);
let menu_ptr = bus.read_long(handle);
let width_before = bus.read_word(menu_ptr + 2) as i16;
let height_before = bus.read_word(menu_ptr + 4) as i16;
append_menu_data(
&mut disp,
&mut cpu,
&mut bus,
handle,
0x306DF0,
"This is a much longer menu item label",
);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, handle);
assert!(
disp.dispatch_menu(true, 0x148, &mut cpu, &mut bus)
.unwrap()
.is_ok(),
"recalc CalcMenuSize should succeed"
);
let width_after = bus.read_word(menu_ptr + 2) as i16;
let height_after = bus.read_word(menu_ptr + 4) as i16;
assert!(
width_after > width_before,
"adding a longer item should increase menuWidth after CalcMenuSize"
);
assert!(
height_after > height_before,
"adding an item should increase menuHeight after CalcMenuSize"
);
}
fn calcmenusize_results_for_theme(theme_id: UiThemeId) -> (i16, i16, u16, u32, u16) {
let (mut disp, mut cpu, mut bus) = setup();
disp.set_ui_theme_id(theme_id);
let handle = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 335, 0x306E00, "View");
append_menu_data(
&mut disp,
&mut cpu,
&mut bus,
handle,
0x306E40,
"Open/O;!XMarked;(-;Normal Icon;Color Icon;Small Icon",
);
disp.menus[0].items[3].icon = 7;
disp.install_test_resource(
&mut bus,
*b"ICON",
263,
&menu_icon_source_with_left_stripe(),
);
disp.menus[0].items[4].icon = 8;
disp.install_test_resource(
&mut bus,
*b"cicn",
264,
&cicn_source_with_left_stripe(24, 20),
);
disp.menus[0].items[5].icon = 9;
disp.menus[0].items[5].key_equiv = MENU_KEY_SMALL_ICON;
disp.install_test_resource(&mut bus, *b"SICN", 265, &sicn_source_with_left_stripe());
let menu_ptr = bus.read_long(handle);
bus.write_word(menu_ptr + 2, 0);
bus.write_word(menu_ptr + 4, 0);
bus.write_word(menu_ptr + 6, 0x1357);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, handle);
bus.write_word(TEST_SP + 4, 0xCAFE);
disp.dispatch_menu(true, 0x148, &mut cpu, &mut bus)
.unwrap()
.unwrap();
(
bus.read_word(menu_ptr + 2) as i16,
bus.read_word(menu_ptr + 4) as i16,
bus.read_word(menu_ptr + 6),
cpu.read_reg(Register::A7),
bus.read_word(TEST_SP + 4),
)
}
#[test]
fn systemless_theme_does_not_change_calcmenusize_menu_record_fields() {
let classic = calcmenusize_results_for_theme(UiThemeId::ClassicSystem7);
let themed = calcmenusize_results_for_theme(UiThemeId::SystemlessDefault);
assert!(
classic.0 >= 100,
"classic CalcMenuSize should retain the standard minimum menu width"
);
assert_eq!(
classic.1, 120,
"classic CalcMenuSize should sum standard, normal ICON, cicn, and SICN row heights"
);
assert_eq!(
classic,
(classic.0, 120, 0x1357, TEST_SP + 4, 0xCAFE),
"CalcMenuSize should write menuWidth/menuHeight, preserve adjacent fields, and pop one MenuHandle"
);
assert_eq!(
themed, classic,
"systemless-default must not change CalcMenuSize menu record geometry or stack protocol"
);
}
#[test]
fn pinrect_inside_point_returns_original_coordinates() {
let (mut disp, mut cpu, mut bus) = setup();
let rect_ptr = 0x306D00u32;
bus.write_word(rect_ptr, 10); bus.write_word(rect_ptr + 2, 20); bus.write_word(rect_ptr + 4, 40); bus.write_word(rect_ptr + 6, 80);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 15); bus.write_word(TEST_SP + 2, 25); bus.write_long(TEST_SP + 4, rect_ptr);
bus.write_long(TEST_SP + 8, 0xDEADBEEF);
assert!(
disp.dispatch_menu(true, 0x14E, &mut cpu, &mut bus)
.unwrap()
.is_ok(),
"PinRect should succeed"
);
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
assert_eq!(bus.read_long(TEST_SP + 8), ((15u32) << 16) | 25u32);
}
#[test]
fn pinrect_outside_point_clamps_to_nearest_interior_pixel() {
let (mut disp, mut cpu, mut bus) = setup();
let rect_ptr = 0x306E00u32;
bus.write_word(rect_ptr, 10); bus.write_word(rect_ptr + 2, 20); bus.write_word(rect_ptr + 4, 40); bus.write_word(rect_ptr + 6, 80);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 100); bus.write_word(TEST_SP + 2, (-5i16) as u16); bus.write_long(TEST_SP + 4, rect_ptr);
bus.write_long(TEST_SP + 8, 0xDEADBEEF);
assert!(
disp.dispatch_menu(true, 0x14E, &mut cpu, &mut bus)
.unwrap()
.is_ok(),
"PinRect should succeed"
);
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
assert_eq!(
bus.read_long(TEST_SP + 8),
((39u32) << 16) | 20u32,
"v clamps to bottom-1 and h clamps to left"
);
}
#[test]
fn deltapoint_negative_delta_packs_signed_words_without_sign_bleed() {
let (mut disp, mut cpu, mut bus) = setup();
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 5);
bus.write_word(TEST_SP + 2, 10);
bus.write_word(TEST_SP + 4, 0);
bus.write_word(TEST_SP + 6, 0);
bus.write_long(TEST_SP + 8, 0xDEAD_BEEF);
assert!(
disp.dispatch_menu(true, 0x14F, &mut cpu, &mut bus)
.unwrap()
.is_ok(),
"DeltaPoint should succeed"
);
assert_eq!(bus.read_long(TEST_SP + 8), 0xFFFB_FFF6u32);
}
#[test]
fn deltapoint_consumes_eight_arg_bytes_and_writes_function_result_slot() {
let (mut disp, mut cpu, mut bus) = setup();
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 30);
bus.write_word(TEST_SP + 2, 25);
bus.write_word(TEST_SP + 4, 50);
bus.write_word(TEST_SP + 6, 40);
bus.write_long(TEST_SP + 8, 0xDEAD_BEEF);
bus.write_long(TEST_SP + 12, 0xCAFE_BABE);
assert!(
disp.dispatch_menu(true, 0x14F, &mut cpu, &mut bus)
.unwrap()
.is_ok(),
"DeltaPoint should succeed"
);
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
assert_eq!(bus.read_long(TEST_SP + 8), ((20u32) << 16) | 15u32);
assert_eq!(bus.read_long(TEST_SP + 12), 0xCAFE_BABEu32);
}
#[test]
fn parse_appendmenu_items_splits_semicolons_and_parses_metachars() {
let raw = b"New/N;Open/O;(-;Quit/Q";
let items = parse_appendmenu_items(raw);
assert_eq!(items.len(), 4, "expected 4 items, got {:?}", items);
assert_eq!(items[0].text, "New");
assert_eq!(items[0].key_equiv, b'N');
assert!(items[0].enabled);
assert_eq!(items[1].text, "Open");
assert_eq!(items[1].key_equiv, b'O');
assert!(items[1].enabled);
assert_eq!(items[2].text, "-");
assert!(!items[2].enabled, "'(' must disable the item");
assert_eq!(items[3].text, "Quit");
assert_eq!(items[3].key_equiv, b'Q');
}
#[test]
fn parse_appendmenu_items_handles_marks_icons_and_styles() {
let raw = b"Bold<B;Italic<I;Marked!\x12;IconItem^\x05";
let items = parse_appendmenu_items(raw);
assert_eq!(items.len(), 4);
assert_eq!(items[0].text, "Bold");
assert_eq!(items[0].style, 0x01);
assert_eq!(items[1].text, "Italic");
assert_eq!(items[1].style, 0x02);
assert_eq!(items[2].text, "Marked");
assert_eq!(items[2].mark, 0x12);
assert_eq!(items[3].text, "IconItem");
assert_eq!(items[3].icon, 4); }
#[test]
fn parse_appendmenu_items_handles_single_item_no_separator() {
let raw = b"Solo";
let items = parse_appendmenu_items(raw);
assert_eq!(items.len(), 1);
assert_eq!(items[0].text, "Solo");
}
#[test]
fn popup_menu_item_title_prefers_live_menu_items() {
let (mut disp, _cpu, bus) = setup();
disp.menus.push(Menu {
id: 1008,
title: "Squadies1".to_string(),
items: vec![
MenuItem {
text: "Brix".to_string(),
icon: 0,
key_equiv: 0,
mark: 0,
style: 0,
enabled: true,
},
MenuItem {
text: "Ryan".to_string(),
icon: 0,
key_equiv: 0,
mark: 0,
style: 0,
enabled: true,
},
],
enabled: true,
handle: 0x1234,
in_menu_bar: false,
hierarchical: false,
visible_in_menu_bar: false,
});
assert_eq!(
disp.popup_menu_item_title(&bus, 1008, 2).as_deref(),
Some("Ryan")
);
}
#[test]
fn initmenus_procedure_call_preserves_stack_pointer() {
let (mut disp, mut cpu, mut bus) = setup();
let sp_before = cpu.read_reg(Register::A7);
let result = disp.dispatch_menu(true, 0x130, &mut cpu, &mut bus);
assert!(result.is_some(), "InitMenus should be handled");
assert!(result.unwrap().is_ok(), "InitMenus should succeed");
assert_ne!(
bus.read_long(crate::memory::globals::addr::MENU_C_INFO),
0,
"InitMenus should seed the live MenuCInfo handle"
);
assert_eq!(cpu.read_reg(Register::A7), sp_before);
}
#[test]
fn initmenus_autoloads_mctb_zero_resource_into_menucinfo() {
let (mut disp, mut cpu, mut bus) = setup();
install_mctb_resource(&mut disp, &mut bus, 0, &[(0, 0, 0x1100), (601, 0, 0x1200)]);
let result = disp.dispatch_menu(true, 0x130, &mut cpu, &mut bus);
assert!(result.is_some(), "InitMenus should be handled");
assert!(result.unwrap().is_ok(), "InitMenus should succeed");
let bar_entry = get_mc_entry_ptr_for_test(&mut disp, &mut cpu, &mut bus, 0, 0);
assert_ne!(
bar_entry, 0,
"InitMenus should autoload the mctb=0 menu-bar entry"
);
assert_eq!(
bus.read_word(bar_entry + 4),
0x1100,
"compiled mctb RGB1 should be copied into the live MCEntry"
);
assert_eq!(
bus.read_word(bar_entry + 28),
0,
"compiled 28-byte mctb entries should gain a zero reserved word"
);
assert_ne!(
get_mc_entry_ptr_for_test(&mut disp, &mut cpu, &mut bus, 601, 0),
0,
"InitMenus should autoload all declared mctb=0 entries"
);
}
#[test]
fn test_new_menu() {
let (mut disp, mut cpu, mut bus) = setup();
let result = disp.dispatch_menu(true, 0x131, &mut cpu, &mut bus);
assert!(result.is_some(), "NewMenu should be handled");
assert!(result.unwrap().is_ok(), "NewMenu should succeed");
let sp = cpu.read_reg(Register::A7);
assert_eq!(sp, TEST_SP + 6, "NewMenu should pop 6 bytes from stack");
let handle = bus.read_long(sp);
assert_ne!(handle, 0, "NewMenu should write a non-zero handle");
}
#[test]
fn newmenu_sets_menuid_and_copies_title_into_menu_data() {
let (mut disp, mut cpu, mut bus) = setup();
let menu_id = 230i16;
let title = "Tools";
let handle = new_menu_with_title(&mut disp, &mut cpu, &mut bus, menu_id, 0x30B100, title);
assert_ne!(handle, 0, "NewMenu should return a non-NIL handle");
let menu_ptr = bus.read_long(handle);
assert_ne!(menu_ptr, 0, "NewMenu handle should dereference to MenuInfo");
assert_eq!(
bus.read_word(menu_ptr) as i16,
menu_id,
"menuID field should match NewMenu(menuID)"
);
assert_eq!(
bus.read_byte(menu_ptr + 14),
title.len() as u8,
"menuData title length should match input Str255 length"
);
assert_eq!(
bus.read_bytes(menu_ptr + 15, title.len()),
title.as_bytes().to_vec(),
"menuData title bytes should match input Str255 bytes"
);
assert_eq!(
bus.read_byte(menu_ptr + 15 + title.len() as u32),
0,
"menuData should terminate items with an empty item string"
);
}
#[test]
fn newmenu_requires_insertmenu_before_getmhandle_finds_menu() {
let (mut disp, mut cpu, mut bus) = setup();
let menu_id = 231i16;
let handle = new_menu_with_title(&mut disp, &mut cpu, &mut bus, menu_id, 0x30B200, "Tools");
assert_eq!(
get_mhandle_for_id(&mut disp, &mut cpu, &mut bus, menu_id),
0,
"GetMHandle should return NIL before InsertMenu adds NewMenu to current list"
);
insert_menu(&mut disp, &mut cpu, &mut bus, handle);
assert_eq!(
get_mhandle_for_id(&mut disp, &mut cpu, &mut bus, menu_id),
handle,
"GetMHandle should return the NewMenu handle after InsertMenu"
);
}
#[test]
fn test_get_menu_returns_nil_when_resource_missing() {
let (mut disp, mut cpu, mut bus) = setup();
bus.write_word(0x0A60, 0);
bus.write_word(TEST_SP, 999); let result = disp.dispatch_menu(true, 0x1BF, &mut cpu, &mut bus);
assert!(result.is_some(), "GetMenu should be handled");
assert!(result.unwrap().is_ok(), "GetMenu should succeed");
let sp = cpu.read_reg(Register::A7);
assert_eq!(sp, TEST_SP + 2, "GetMenu should pop 2 bytes from stack");
assert_eq!(
bus.read_long(sp),
0,
"GetMenu must return NIL when MENU resource is unavailable"
);
assert_eq!(
bus.read_word(0x0A60) as i16,
-192,
"GetMenu miss must set ResErr to resNotFound"
);
}
#[test]
fn getmenu_hit_clears_stale_reserror() {
let (mut disp, mut cpu, mut bus) = setup();
let menu_ptr = seed_menu_resource(&mut bus, 128, "Game");
disp.resources = Some(crate::trap::dispatch::LoadedResources {
files: std::collections::HashMap::from([(
0,
crate::trap::dispatch::ResourceFileMap {
loaded: std::collections::HashMap::from([((*b"MENU", 128), menu_ptr)]),
named: std::collections::HashMap::new(),
names_by_id: std::collections::HashMap::new(),
attrs: std::collections::HashMap::new(),
map_attrs: 0,
},
)]),
names: std::collections::HashMap::new(),
search_order: vec![0],
current_file: 0,
});
bus.write_word(0x0A60, (-192i16) as u16);
bus.write_word(TEST_SP, 128);
let result = disp.dispatch_menu(true, 0x1BF, &mut cpu, &mut bus);
assert!(result.is_some(), "GetMenu should be handled");
assert!(result.unwrap().is_ok(), "GetMenu should succeed");
let sp = cpu.read_reg(Register::A7);
assert_ne!(bus.read_long(sp), 0, "GetMenu hit should return a handle");
assert_eq!(
bus.read_word(0x0A60),
0,
"GetMenu hit must clear stale resource errors"
);
}
#[test]
fn getmenu_returns_resource_backed_handle_reused_until_release() {
let (mut disp, mut cpu, mut bus) = setup();
let menu_ptr = seed_menu_resource(&mut bus, 128, "Game");
disp.resources = Some(crate::trap::dispatch::LoadedResources {
files: std::collections::HashMap::from([(
0,
crate::trap::dispatch::ResourceFileMap {
loaded: std::collections::HashMap::from([((*b"MENU", 128), menu_ptr)]),
named: std::collections::HashMap::new(),
names_by_id: std::collections::HashMap::new(),
attrs: std::collections::HashMap::new(),
map_attrs: 0,
},
)]),
names: std::collections::HashMap::new(),
search_order: vec![0],
current_file: 0,
});
bus.write_word(TEST_SP, 128);
let first = disp.dispatch_menu(true, 0x1BF, &mut cpu, &mut bus);
assert!(first.is_some(), "GetMenu should be handled");
assert!(first.unwrap().is_ok(), "GetMenu should succeed");
let first_handle = bus.read_long(cpu.read_reg(Register::A7));
assert_ne!(first_handle, 0, "GetMenu should return a handle on hit");
assert_eq!(
disp.loaded_handles.get(&first_handle).copied(),
Some((bus.read_long(first_handle), *b"MENU", 128)),
"GetMenu must return a Resource Manager-backed MENU handle"
);
assert_eq!(
disp.resource_handle_files.get(&first_handle).copied(),
Some(0),
"GetMenu-backed MENU handle should retain its resource-file owner"
);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 128);
let second = disp.dispatch_menu(true, 0x1BF, &mut cpu, &mut bus);
assert!(second.is_some(), "second GetMenu should be handled");
assert!(second.unwrap().is_ok(), "second GetMenu should succeed");
assert_eq!(
bus.read_long(cpu.read_reg(Register::A7)),
first_handle,
"repeated GetMenu should reuse the loaded MENU resource handle"
);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, first_handle);
let release = disp.dispatch_resource(true, 0x1A3, &mut cpu, &mut bus);
assert!(release.is_some(), "ReleaseResource should be handled");
assert!(release.unwrap().is_ok(), "ReleaseResource should succeed");
assert_eq!(
bus.read_long(first_handle),
0,
"ReleaseResource should nil the released menu handle"
);
assert!(
!disp.loaded_handles.contains_key(&first_handle),
"ReleaseResource should invalidate the old MENU handle identity"
);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 128);
let third = disp.dispatch_menu(true, 0x1BF, &mut cpu, &mut bus);
assert!(third.is_some(), "third GetMenu should be handled");
assert!(third.unwrap().is_ok(), "third GetMenu should succeed");
let third_handle = bus.read_long(cpu.read_reg(Register::A7));
assert_ne!(third_handle, 0, "GetMenu after release should reload");
assert_ne!(
third_handle, first_handle,
"GetMenu after ReleaseResource should allocate a fresh handle"
);
assert_eq!(
disp.resource_handle_files.get(&third_handle).copied(),
Some(0),
"reloaded MENU handle should be resource-backed"
);
}
#[test]
fn getmenu_autoloads_matching_mctb_resource_into_menucinfo() {
let (mut disp, mut cpu, mut bus) = setup();
let menu_id = 181i16;
let menu_ptr = seed_menu_resource(&mut bus, menu_id, "Color");
disp.resources = Some(crate::trap::dispatch::LoadedResources {
files: std::collections::HashMap::from([(
0,
crate::trap::dispatch::ResourceFileMap {
loaded: std::collections::HashMap::from([((*b"MENU", menu_id), menu_ptr)]),
named: std::collections::HashMap::new(),
names_by_id: std::collections::HashMap::new(),
attrs: std::collections::HashMap::new(),
map_attrs: 0,
},
)]),
names: std::collections::HashMap::new(),
search_order: vec![0],
current_file: 0,
});
install_mctb_resource(
&mut disp,
&mut bus,
menu_id,
&[(menu_id, 0, 0x2100), (menu_id, 2, 0x2200)],
);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, menu_id as u16);
let result = disp.dispatch_menu(true, 0x1BF, &mut cpu, &mut bus);
assert!(result.is_some(), "GetMenu should be handled");
assert!(result.unwrap().is_ok(), "GetMenu should succeed");
assert_ne!(
bus.read_long(TEST_SP + 2),
0,
"GetMenu should return the loaded MENU handle"
);
let title_entry = get_mc_entry_ptr_for_test(&mut disp, &mut cpu, &mut bus, menu_id, 0);
assert_ne!(
title_entry, 0,
"GetMenu should autoload the matching mctb title entry"
);
assert_eq!(
bus.read_word(title_entry + 10),
0x2103,
"compiled mctb RGB2 should be converted into the live MCEntry"
);
assert_ne!(
get_mc_entry_ptr_for_test(&mut disp, &mut cpu, &mut bus, menu_id, 2),
0,
"GetMenu should autoload matching mctb item entries"
);
}
#[test]
fn getnewmbar_missing_resource_returns_nil_and_pops_menuid_word() {
let (mut disp, mut cpu, mut bus) = setup();
bus.write_word(TEST_SP, 128);
bus.write_long(TEST_SP + 2, 0xDEAD_BEEF);
let result = disp.dispatch_menu(true, 0x1C0, &mut cpu, &mut bus);
assert!(result.is_some(), "GetNewMBar should be handled");
assert!(result.unwrap().is_ok(), "GetNewMBar should succeed");
assert_eq!(
cpu.read_reg(Register::A7),
TEST_SP + 2,
"GetNewMBar should consume one INTEGER argument"
);
assert_eq!(
bus.read_long(TEST_SP + 2),
0,
"GetNewMBar must return NIL when MBAR cannot be read"
);
}
#[test]
fn getnewmbar_present_resource_returns_handle_and_setmenubar_installs_it() {
let (mut disp, mut cpu, mut bus) = setup();
let baseline = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 300, 0x306000, "Old");
insert_menu(&mut disp, &mut cpu, &mut bus, baseline);
assert_ne!(get_mhandle_for_id(&mut disp, &mut cpu, &mut bus, 300), 0);
let file_menu_ptr = seed_menu_resource(&mut bus, 128, "File");
let edit_menu_ptr = seed_menu_resource(&mut bus, 129, "Edit");
let mbar_ptr = seed_mbar_resource(&mut bus, &[128, 129]);
disp.resources = Some(crate::trap::dispatch::LoadedResources {
files: std::collections::HashMap::from([(
0,
crate::trap::dispatch::ResourceFileMap {
loaded: std::collections::HashMap::from([
((*b"MBAR", 900), mbar_ptr),
((*b"MENU", 128), file_menu_ptr),
((*b"MENU", 129), edit_menu_ptr),
]),
named: std::collections::HashMap::new(),
names_by_id: std::collections::HashMap::new(),
attrs: std::collections::HashMap::new(),
map_attrs: 0,
},
)]),
names: std::collections::HashMap::new(),
search_order: vec![0],
current_file: 0,
});
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 900);
let result = disp.dispatch_menu(true, 0x1C0, &mut cpu, &mut bus);
assert!(result.is_some(), "GetNewMBar should be handled");
assert!(result.unwrap().is_ok(), "GetNewMBar should succeed");
assert_eq!(
cpu.read_reg(Register::A7),
TEST_SP + 2,
"GetNewMBar should pop menuBarID"
);
let mbar_handle = bus.read_long(TEST_SP + 2);
assert_ne!(mbar_handle, 0, "GetNewMBar should return a non-NIL handle");
let list_ptr = bus.read_long(mbar_handle);
assert_ne!(list_ptr, 0, "returned menu-list handle should dereference");
assert_eq!(
bus.read_word(list_ptr),
2,
"menu-list block should describe both MBAR menu IDs"
);
assert_ne!(
bus.read_long(list_ptr + 2),
0,
"first menu handle should be non-NIL"
);
assert_ne!(
bus.read_long(list_ptr + 6),
0,
"second menu handle should be non-NIL"
);
assert_ne!(
get_mhandle_for_id(&mut disp, &mut cpu, &mut bus, 300),
0,
"GetNewMBar alone should not replace the current menu list"
);
assert_eq!(
get_mhandle_for_id(&mut disp, &mut cpu, &mut bus, 128),
0,
"new MBAR menus should not be current until SetMenuBar"
);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, mbar_handle);
let set_result = disp.dispatch_menu(true, 0x13C, &mut cpu, &mut bus);
assert!(set_result.is_some(), "SetMenuBar should be handled");
assert!(set_result.unwrap().is_ok(), "SetMenuBar should succeed");
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
assert_eq!(
get_mhandle_for_id(&mut disp, &mut cpu, &mut bus, 300),
0,
"SetMenuBar should replace current list with the list from GetNewMBar"
);
assert_ne!(
get_mhandle_for_id(&mut disp, &mut cpu, &mut bus, 128),
0,
"SetMenuBar should install File menu from MBAR"
);
assert_ne!(
get_mhandle_for_id(&mut disp, &mut cpu, &mut bus, 129),
0,
"SetMenuBar should install Edit menu from MBAR"
);
}
#[test]
fn getnewmbar_rebuilds_menucinfo_from_menu_mctb_resources() {
let (mut disp, mut cpu, mut bus) = setup();
let baseline = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 300, 0x306080, "Old");
insert_menu(&mut disp, &mut cpu, &mut bus, baseline);
set_mc_entries_for_test(&mut disp, &mut cpu, &mut bus, &[(999, 1, 0x3100)]);
assert_ne!(
get_mc_entry_ptr_for_test(&mut disp, &mut cpu, &mut bus, 999, 1),
0,
"precondition: old MenuCInfo entry should exist before GetNewMBar"
);
let file_menu_ptr = seed_menu_resource(&mut bus, 601, "File");
let edit_menu_ptr = seed_menu_resource(&mut bus, 602, "Edit");
let mbar_ptr = seed_mbar_resource(&mut bus, &[601, 602]);
disp.resources = Some(crate::trap::dispatch::LoadedResources {
files: std::collections::HashMap::from([(
0,
crate::trap::dispatch::ResourceFileMap {
loaded: std::collections::HashMap::from([
((*b"MBAR", 901), mbar_ptr),
((*b"MENU", 601), file_menu_ptr),
((*b"MENU", 602), edit_menu_ptr),
]),
named: std::collections::HashMap::new(),
names_by_id: std::collections::HashMap::new(),
attrs: std::collections::HashMap::new(),
map_attrs: 0,
},
)]),
names: std::collections::HashMap::new(),
search_order: vec![0],
current_file: 0,
});
install_mctb_resource(&mut disp, &mut bus, 601, &[(601, 0, 0x3200)]);
install_mctb_resource(&mut disp, &mut bus, 602, &[(602, 2, 0x3300)]);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 901);
let result = disp.dispatch_menu(true, 0x1C0, &mut cpu, &mut bus);
assert!(result.is_some(), "GetNewMBar should be handled");
assert!(result.unwrap().is_ok(), "GetNewMBar should succeed");
assert_ne!(
bus.read_long(TEST_SP + 2),
0,
"GetNewMBar should return a new menu-list handle"
);
assert_ne!(
get_mhandle_for_id(&mut disp, &mut cpu, &mut bus, 300),
0,
"GetNewMBar should restore the previous current MenuList"
);
assert_eq!(
get_mhandle_for_id(&mut disp, &mut cpu, &mut bus, 601),
0,
"GetNewMBar should not install the new MenuList until SetMenuBar"
);
assert_eq!(
get_mc_entry_ptr_for_test(&mut disp, &mut cpu, &mut bus, 999, 1),
0,
"GetNewMBar should clear the previous MenuCInfo table"
);
assert_ne!(
get_mc_entry_ptr_for_test(&mut disp, &mut cpu, &mut bus, 601, 0),
0,
"GetNewMBar should load mctb entries for MBAR menu 601"
);
assert_ne!(
get_mc_entry_ptr_for_test(&mut disp, &mut cpu, &mut bus, 602, 2),
0,
"GetNewMBar should load mctb entries for MBAR menu 602"
);
}
#[test]
fn getmenu_appendmenu_then_insertmenu_enables_menukey_shortcut() {
let (mut disp, mut cpu, mut bus) = setup();
let menu_res_ptr = bus.alloc(64);
bus.write_word(menu_res_ptr, 180);
bus.write_word(menu_res_ptr + 2, 0);
bus.write_word(menu_res_ptr + 4, 0);
bus.write_long(menu_res_ptr + 6, 0);
bus.write_long(menu_res_ptr + 10, 0xFFFF_FFFF);
write_pstring(&mut bus, menu_res_ptr + 14, "File");
bus.write_byte(menu_res_ptr + 19, 0); disp.resources = Some(crate::trap::dispatch::LoadedResources {
files: std::collections::HashMap::from([(
0,
crate::trap::dispatch::ResourceFileMap {
loaded: std::collections::HashMap::from([((*b"MENU", 180), menu_res_ptr)]),
named: std::collections::HashMap::new(),
names_by_id: std::collections::HashMap::new(),
attrs: std::collections::HashMap::new(),
map_attrs: 0,
},
)]),
names: std::collections::HashMap::new(),
search_order: vec![0],
current_file: 0,
});
bus.write_word(TEST_SP, 180);
assert!(disp
.dispatch_menu(true, 0x1BF, &mut cpu, &mut bus)
.unwrap()
.is_ok());
let menu_handle = bus.read_long(cpu.read_reg(Register::A7));
assert_ne!(menu_handle, 0, "GetMenu should return a handle on hit");
assert_eq!(menu_key_result(&mut disp, &mut cpu, &mut bus, b'O'), 0);
append_menu_data(
&mut disp,
&mut cpu,
&mut bus,
menu_handle,
0x306000,
"Open/O",
);
insert_menu(&mut disp, &mut cpu, &mut bus, menu_handle);
assert_eq!(
menu_key_result(&mut disp, &mut cpu, &mut bus, b'O'),
(180u32 << 16) | 1,
"MenuKey should resolve appended shortcut after InsertMenu"
);
}
#[test]
fn menukey_duplicate_shortcuts_prefer_rightmost_menu() {
let (mut disp, mut cpu, mut bus) = setup();
let left = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 200, 0x306100, "Left");
append_menu_data(&mut disp, &mut cpu, &mut bus, left, 0x306200, "LeftCmd/X");
insert_menu(&mut disp, &mut cpu, &mut bus, left);
let right = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 201, 0x306300, "Right");
append_menu_data(&mut disp, &mut cpu, &mut bus, right, 0x306400, "RightCmd/X");
insert_menu(&mut disp, &mut cpu, &mut bus, right);
assert_eq!(
menu_key_result(&mut disp, &mut cpu, &mut bus, b'X'),
(201u32 << 16) | 1,
"MenuKey should choose the rightmost menu's matching item"
);
}
#[test]
fn menukey_ignores_uninserted_menu_items() {
let (mut disp, mut cpu, mut bus) = setup();
let menu = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 210, 0x306500, "Ghost");
append_menu_data(&mut disp, &mut cpu, &mut bus, menu, 0x306600, "Hidden/G");
assert_eq!(
menu_key_result(&mut disp, &mut cpu, &mut bus, b'G'),
0,
"MenuKey should ignore items from menus not yet inserted"
);
}
#[test]
fn getitemcmd_returns_zero_when_no_keyboard_equivalent() {
let (mut disp, mut cpu, mut bus) = setup();
let menu = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 220, 0x306710, "File");
append_menu_data(&mut disp, &mut cpu, &mut bus, menu, 0x306720, "Open");
assert_eq!(
get_item_cmd(&mut disp, &mut cpu, &mut bus, menu, 1),
0,
"GetItemCmd should return 0 when keyboard-equivalent field is clear"
);
}
#[test]
fn setitemcmd_sets_keyboard_equivalent_field() {
let (mut disp, mut cpu, mut bus) = setup();
let menu = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 221, 0x306730, "File");
append_menu_data(&mut disp, &mut cpu, &mut bus, menu, 0x306740, "Open");
set_item_cmd(&mut disp, &mut cpu, &mut bus, menu, 1, b'O');
assert_eq!(
get_item_cmd(&mut disp, &mut cpu, &mut bus, menu, 1),
b'O',
"GetItemCmd should read back cmdChar written by SetItemCmd"
);
insert_menu(&mut disp, &mut cpu, &mut bus, menu);
assert_eq!(
menu_key_result(&mut disp, &mut cpu, &mut bus, b'O'),
(221u32 << 16) | 1,
"MenuKey should honor command key installed via SetItemCmd"
);
}
#[test]
fn setitemcmd_sets_submenu_marker_for_hierarchical_items() {
let (mut disp, mut cpu, mut bus) = setup();
let menu = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 222, 0x306750, "File");
append_menu_data(&mut disp, &mut cpu, &mut bus, menu, 0x306760, "Open");
set_item_cmd(&mut disp, &mut cpu, &mut bus, menu, 1, 0x1B);
assert_eq!(
get_item_cmd(&mut disp, &mut cpu, &mut bus, menu, 1),
0x1B,
"GetItemCmd should return $1B after SetItemCmd installs submenu marker"
);
}
#[test]
fn test_append_menu() {
let (mut disp, mut cpu, mut bus) = setup();
let result = disp.dispatch_menu(true, 0x133, &mut cpu, &mut bus);
assert!(result.is_some(), "AppendMenu should be handled");
assert!(result.unwrap().is_ok(), "AppendMenu should succeed");
let sp = cpu.read_reg(Register::A7);
assert_eq!(sp, TEST_SP + 8, "AppendMenu should pop 8 bytes from stack");
}
#[test]
fn test_appendmenu_serialises_items_to_guest_memory() {
let (mut disp, mut cpu, mut bus) = setup();
let title_ptr = 0x302000u32;
bus.write_byte(title_ptr, 4);
bus.write_byte(title_ptr + 1, b'F');
bus.write_byte(title_ptr + 2, b'i');
bus.write_byte(title_ptr + 3, b'l');
bus.write_byte(title_ptr + 4, b'e');
bus.write_long(TEST_SP, title_ptr);
bus.write_word(TEST_SP + 4, 129);
let r = disp.dispatch_menu(true, 0x131, &mut cpu, &mut bus);
assert!(r.is_some() && r.unwrap().is_ok());
let sp_after_new = cpu.read_reg(Register::A7);
let handle = bus.read_long(sp_after_new);
assert_ne!(handle, 0);
let data_ptr = 0x302100u32;
let data = b"New/N;Open/O;Quit/Q";
bus.write_byte(data_ptr, data.len() as u8);
for (i, b) in data.iter().enumerate() {
bus.write_byte(data_ptr + 1 + i as u32, *b);
}
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, data_ptr); bus.write_long(TEST_SP + 4, handle); let r = disp.dispatch_menu(true, 0x133, &mut cpu, &mut bus);
assert!(r.is_some() && r.unwrap().is_ok());
assert_eq!(disp.menus.len(), 1);
assert_eq!(disp.menus[0].items.len(), 3);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, handle);
let r = disp.dispatch_menu(true, 0x150, &mut cpu, &mut bus);
assert!(r.is_some() && r.unwrap().is_ok());
let sp_count = cpu.read_reg(Register::A7);
let count = bus.read_word(sp_count);
assert_eq!(count, 3, "CountMItems must see AppendMenu'd items");
let menu_ptr = bus.read_long(handle);
assert_eq!(bus.read_byte(menu_ptr + 19), 3);
assert_eq!(bus.read_byte(menu_ptr + 20), b'N');
assert_eq!(bus.read_byte(menu_ptr + 21), b'e');
assert_eq!(bus.read_byte(menu_ptr + 22), b'w');
}
#[test]
fn appendmenu_tracks_raw_menu_handle_before_appending_items() {
let (mut disp, mut cpu, mut bus) = setup();
let handle = bus.alloc(4);
let menu_ptr = bus.alloc(32);
bus.write_long(handle, menu_ptr);
bus.write_word(menu_ptr, 4000);
bus.write_word(menu_ptr + 2, 0);
bus.write_word(menu_ptr + 4, 0);
bus.write_long(menu_ptr + 6, 0);
bus.write_long(menu_ptr + 10, 0xFFFF_FFFF);
write_pstring(&mut bus, menu_ptr + 14, "Sections");
bus.write_byte(menu_ptr + 15 + "Sections".len() as u32, 0);
append_menu_data(
&mut disp,
&mut cpu,
&mut bus,
handle,
0x302200,
"Graphics;Sound;Controls",
);
assert_eq!(disp.menus.len(), 1);
assert_eq!(disp.menus[0].id, 4000);
assert_eq!(disp.menus[0].items.len(), 3);
assert_eq!(
disp.popup_menu_item_title(&bus, 4000, 1).as_deref(),
Some("Graphics")
);
assert_eq!(
bus.get_alloc_size(bus.read_long(handle)),
Some(256),
"AppendMenu should grow small raw MENU handles before serialising items"
);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, handle);
let r = disp.dispatch_menu(true, 0x150, &mut cpu, &mut bus);
assert!(r.is_some() && r.unwrap().is_ok());
let sp_count = cpu.read_reg(Register::A7);
assert_eq!(bus.read_word(sp_count), 3);
}
#[test]
fn appendmenu_accepts_a0_menu_handle_when_stack_handle_is_nil() {
let (mut disp, mut cpu, mut bus) = setup();
let handle = bus.alloc(4);
let menu_ptr = bus.alloc(32);
let data_ptr = 0x302300;
bus.write_long(handle, menu_ptr);
bus.write_word(menu_ptr, 4000);
bus.write_word(menu_ptr + 2, 0);
bus.write_word(menu_ptr + 4, 0);
bus.write_long(menu_ptr + 6, 0);
bus.write_long(menu_ptr + 10, 0xFFFF_FFFF);
write_pstring(&mut bus, menu_ptr + 14, "Sections");
bus.write_byte(menu_ptr + 15 + "Sections".len() as u32, 0);
write_pstring(&mut bus, data_ptr, "Graphics");
cpu.write_reg(Register::A0, handle);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, data_ptr);
bus.write_long(TEST_SP + 4, 0);
let r = disp.dispatch_menu(true, 0x133, &mut cpu, &mut bus);
assert!(r.is_some() && r.unwrap().is_ok());
assert_eq!(
cpu.read_reg(Register::A7),
TEST_SP + 8,
"AppendMenu should still pop the stack data/handle slots"
);
assert_eq!(
disp.popup_menu_item_title(&bus, 4000, 1).as_deref(),
Some("Graphics")
);
}
#[test]
fn appendmenu_accepts_a0_menu_ptr_when_stack_handle_is_nil() {
let (mut disp, mut cpu, mut bus) = setup();
let handle = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 4000, 0x302500, "Sections");
let menu_ptr = bus.read_long(handle);
write_pstring(&mut bus, 0x302540, "Graphics");
cpu.write_reg(Register::A0, menu_ptr);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, 0x302540);
bus.write_long(TEST_SP + 4, 0);
let r = disp.dispatch_menu(true, 0x133, &mut cpu, &mut bus);
assert!(r.is_some() && r.unwrap().is_ok());
assert_eq!(
cpu.read_reg(Register::A7),
TEST_SP + 8,
"AppendMenu should still pop the stack data/handle slots"
);
assert_eq!(
disp.popup_menu_item_title(&bus, 4000, 1).as_deref(),
Some("Graphics")
);
}
#[test]
fn setitem_accepts_a0_menu_handle_when_stack_handle_is_nil() {
let (mut disp, mut cpu, mut bus) = setup();
let handle = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 4000, 0x302300, "Sections");
append_menu_data(&mut disp, &mut cpu, &mut bus, handle, 0x302340, " ");
write_pstring(&mut bus, 0x302380, "Graphics");
cpu.write_reg(Register::A0, handle);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, 0x302380);
bus.write_word(TEST_SP + 4, 1);
bus.write_long(TEST_SP + 6, 0);
let r = disp.dispatch_menu(true, 0x147, &mut cpu, &mut bus);
assert!(r.is_some() && r.unwrap().is_ok());
assert_eq!(
cpu.read_reg(Register::A7),
TEST_SP + 10,
"SetItem should still pop text, item, and menu argument slots"
);
assert_eq!(
disp.popup_menu_item_title(&bus, 4000, 1).as_deref(),
Some("Graphics")
);
}
#[test]
fn setitem_accepts_a0_menu_ptr_when_stack_handle_is_nil() {
let (mut disp, mut cpu, mut bus) = setup();
let handle = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 4000, 0x302600, "Sections");
append_menu_data(&mut disp, &mut cpu, &mut bus, handle, 0x302640, " ");
let menu_ptr = bus.read_long(handle);
write_pstring(&mut bus, 0x302680, "Graphics");
cpu.write_reg(Register::A0, menu_ptr);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, 0x302680);
bus.write_word(TEST_SP + 4, 1);
bus.write_long(TEST_SP + 6, 0);
let r = disp.dispatch_menu(true, 0x147, &mut cpu, &mut bus);
assert!(r.is_some() && r.unwrap().is_ok());
assert_eq!(
cpu.read_reg(Register::A7),
TEST_SP + 10,
"SetItem should still pop text, item, and menu argument slots"
);
assert_eq!(
disp.popup_menu_item_title(&bus, 4000, 1).as_deref(),
Some("Graphics")
);
}
#[test]
fn test_insertmenuitem_serialises_to_guest_memory() {
let (mut disp, mut cpu, mut bus) = setup();
let title_ptr = 0x303000u32;
bus.write_byte(title_ptr, 4);
for (i, b) in b"File".iter().enumerate() {
bus.write_byte(title_ptr + 1 + i as u32, *b);
}
bus.write_long(TEST_SP, title_ptr);
bus.write_word(TEST_SP + 4, 129);
assert!(disp
.dispatch_menu(true, 0x131, &mut cpu, &mut bus)
.unwrap()
.is_ok());
let handle = bus.read_long(cpu.read_reg(Register::A7));
assert_ne!(handle, 0);
let text_ptr = 0x303100u32;
bus.write_byte(text_ptr, 4);
for (i, b) in b"Copy".iter().enumerate() {
bus.write_byte(text_ptr + 1 + i as u32, *b);
}
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 0); bus.write_long(TEST_SP + 2, text_ptr);
bus.write_long(TEST_SP + 6, handle);
assert!(disp
.dispatch_menu(true, 0x026, &mut cpu, &mut bus)
.unwrap()
.is_ok());
assert_eq!(disp.menus[0].items.len(), 1);
assert_eq!(disp.menus[0].items[0].text, "Copy");
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, handle);
assert!(disp
.dispatch_menu(true, 0x150, &mut cpu, &mut bus)
.unwrap()
.is_ok());
assert_eq!(bus.read_word(cpu.read_reg(Register::A7)), 1);
}
#[test]
fn insertmenuitem_afteritem_zero_inserts_before_first_item() {
let (mut disp, mut cpu, mut bus) = setup();
let handle = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 150, 0x30A100, "Edit");
append_menu_data(&mut disp, &mut cpu, &mut bus, handle, 0x30A110, "B;C");
insert_menu_item_data(&mut disp, &mut cpu, &mut bus, handle, 0x30A120, "A", 0);
assert_eq!(disp.menus[0].items[0].text, "A");
assert_eq!(disp.menus[0].items[1].text, "B");
assert_eq!(disp.menus[0].items[2].text, "C");
}
#[test]
fn insertmenuitem_afteritem_index_and_past_end_place_items_per_spec() {
let (mut disp, mut cpu, mut bus) = setup();
let handle = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 151, 0x30A200, "Edit");
append_menu_data(&mut disp, &mut cpu, &mut bus, handle, 0x30A210, "A;B;C");
insert_menu_item_data(&mut disp, &mut cpu, &mut bus, handle, 0x30A220, "X", 2);
insert_menu_item_data(&mut disp, &mut cpu, &mut bus, handle, 0x30A230, "Tail", 99);
let texts: Vec<String> = disp.menus[0].items.iter().map(|i| i.text.clone()).collect();
assert_eq!(texts, vec!["A", "B", "X", "C", "Tail"]);
}
#[test]
fn insertmenuitem_multiple_items_reverse_order_and_metacharacters() {
let (mut disp, mut cpu, mut bus) = setup();
let menu_id = 152;
let handle = new_menu_with_title(&mut disp, &mut cpu, &mut bus, menu_id, 0x30A300, "Edit");
insert_menu_item_data(
&mut disp,
&mut cpu,
&mut bus,
handle,
0x30A310,
"Paste/V;Copy/C;Cut/X;(-;Undo/Z",
0,
);
let menu = &disp.menus[0];
assert_eq!(menu.items.len(), 5);
assert_eq!(menu.items[0].text, "Undo");
assert_eq!(menu.items[0].key_equiv, b'Z');
assert_eq!(menu.items[1].text, "-");
assert!(!menu.items[1].enabled, "'(' must disable inserted item");
assert_eq!(menu.items[2].text, "Cut");
assert_eq!(menu.items[2].key_equiv, b'X');
assert_eq!(menu.items[3].text, "Copy");
assert_eq!(menu.items[3].key_equiv, b'C');
assert_eq!(menu.items[4].text, "Paste");
assert_eq!(menu.items[4].key_equiv, b'V');
let menu_ptr = bus.read_long(handle);
let flags = bus.read_long(menu_ptr + 10);
assert_eq!(
flags & (1 << 2),
0,
"inserted disabled item must clear item-2 bit"
);
insert_menu(&mut disp, &mut cpu, &mut bus, handle);
assert_eq!(
menu_key_result(&mut disp, &mut cpu, &mut bus, b'C'),
((menu_id as u32) << 16) | 4,
"Copy command should map to item 4 in reverse-ordered insertion result"
);
}
#[test]
fn test_setitem_syncs_guest_memory_text() {
let (mut disp, mut cpu, mut bus) = setup();
let title_ptr = 0x304000u32;
bus.write_byte(title_ptr, 4);
for (i, b) in b"File".iter().enumerate() {
bus.write_byte(title_ptr + 1 + i as u32, *b);
}
bus.write_long(TEST_SP, title_ptr);
bus.write_word(TEST_SP + 4, 129);
assert!(disp
.dispatch_menu(true, 0x131, &mut cpu, &mut bus)
.unwrap()
.is_ok());
let handle = bus.read_long(cpu.read_reg(Register::A7));
let data_ptr = 0x304100u32;
bus.write_byte(data_ptr, 3);
for (i, b) in b"Old".iter().enumerate() {
bus.write_byte(data_ptr + 1 + i as u32, *b);
}
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, data_ptr);
bus.write_long(TEST_SP + 4, handle);
assert!(disp
.dispatch_menu(true, 0x133, &mut cpu, &mut bus)
.unwrap()
.is_ok());
let new_ptr = 0x304200u32;
bus.write_byte(new_ptr, 3);
for (i, b) in b"New".iter().enumerate() {
bus.write_byte(new_ptr + 1 + i as u32, *b);
}
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, new_ptr); bus.write_word(TEST_SP + 4, 1); bus.write_long(TEST_SP + 6, handle); assert!(disp
.dispatch_menu(true, 0x147, &mut cpu, &mut bus)
.unwrap()
.is_ok());
assert_eq!(disp.menus[0].items[0].text, "New");
let menu_ptr = bus.read_long(handle);
assert_eq!(bus.read_byte(menu_ptr + 19), 3);
assert_eq!(bus.read_byte(menu_ptr + 20), b'N');
assert_eq!(bus.read_byte(menu_ptr + 21), b'e');
assert_eq!(bus.read_byte(menu_ptr + 22), b'w');
}
#[test]
fn test_deletemenuitem_syncs_guest_memory() {
let (mut disp, mut cpu, mut bus) = setup();
let title_ptr = 0x305000u32;
bus.write_byte(title_ptr, 4);
for (i, b) in b"Edit".iter().enumerate() {
bus.write_byte(title_ptr + 1 + i as u32, *b);
}
bus.write_long(TEST_SP, title_ptr);
bus.write_word(TEST_SP + 4, 130);
assert!(disp
.dispatch_menu(true, 0x131, &mut cpu, &mut bus)
.unwrap()
.is_ok());
let handle = bus.read_long(cpu.read_reg(Register::A7));
let data_ptr = 0x305100u32;
let data = b"A;B;C";
bus.write_byte(data_ptr, data.len() as u8);
for (i, b) in data.iter().enumerate() {
bus.write_byte(data_ptr + 1 + i as u32, *b);
}
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, data_ptr);
bus.write_long(TEST_SP + 4, handle);
assert!(disp
.dispatch_menu(true, 0x133, &mut cpu, &mut bus)
.unwrap()
.is_ok());
assert_eq!(disp.menus[0].items.len(), 3);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 2);
bus.write_long(TEST_SP + 2, handle);
assert!(disp
.dispatch_menu(true, 0x152, &mut cpu, &mut bus)
.unwrap()
.is_ok());
assert_eq!(disp.menus[0].items.len(), 2);
assert_eq!(disp.menus[0].items[0].text, "A");
assert_eq!(disp.menus[0].items[1].text, "C");
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, handle);
assert!(disp
.dispatch_menu(true, 0x150, &mut cpu, &mut bus)
.unwrap()
.is_ok());
assert_eq!(bus.read_word(cpu.read_reg(Register::A7)), 2);
}
#[test]
fn deletemenuitem_removes_exact_menu_color_item_entry() {
let (mut disp, mut cpu, mut bus) = setup();
let menu_id = 521i16;
let handle = new_menu_with_title(&mut disp, &mut cpu, &mut bus, menu_id, 0x305180, "Edit");
append_menu_data(&mut disp, &mut cpu, &mut bus, handle, 0x3051C0, "A;B;C");
set_mc_entries_for_test(
&mut disp,
&mut cpu,
&mut bus,
&[
(menu_id, 0, 0x1100),
(menu_id, 1, 0x1200),
(menu_id, 2, 0x1300),
(menu_id, 3, 0x1400),
],
);
assert_ne!(
get_mc_entry_ptr_for_test(&mut disp, &mut cpu, &mut bus, menu_id, 2),
0,
"precondition: item-2 MenuCInfo entry should exist"
);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 2);
bus.write_long(TEST_SP + 2, handle);
assert!(
disp.dispatch_menu(true, 0x152, &mut cpu, &mut bus)
.unwrap()
.is_ok(),
"DeleteMenuItem should succeed"
);
assert_eq!(
get_mc_entry_ptr_for_test(&mut disp, &mut cpu, &mut bus, menu_id, 2),
0,
"DeleteMenuItem should remove the deleted item's MenuCInfo entry"
);
assert_ne!(
get_mc_entry_ptr_for_test(&mut disp, &mut cpu, &mut bus, menu_id, 0),
0,
"DeleteMenuItem should preserve the menu-title MenuCInfo entry"
);
assert_ne!(
get_mc_entry_ptr_for_test(&mut disp, &mut cpu, &mut bus, menu_id, 1),
0,
"DeleteMenuItem should preserve other item entries"
);
assert_ne!(
get_mc_entry_ptr_for_test(&mut disp, &mut cpu, &mut bus, menu_id, 3),
0,
"DeleteMenuItem should not renumber unrelated MenuCInfo entries"
);
}
#[test]
fn disableitem_item_gt_31_is_noop() {
let (mut disp, mut cpu, mut bus) = setup();
let title_ptr = 0x305200u32;
bus.write_byte(title_ptr, 4);
for (i, b) in b"Long".iter().enumerate() {
bus.write_byte(title_ptr + 1 + i as u32, *b);
}
bus.write_long(TEST_SP, title_ptr);
bus.write_word(TEST_SP + 4, 140);
assert!(disp
.dispatch_menu(true, 0x131, &mut cpu, &mut bus)
.unwrap()
.is_ok());
let handle = bus.read_long(cpu.read_reg(Register::A7));
let mut items = String::new();
for i in 1..=32 {
if i > 1 {
items.push(';');
}
items.push_str(&format!("I{i:02}"));
}
let data_ptr = 0x305300u32;
bus.write_byte(data_ptr, items.len() as u8);
for (i, b) in items.as_bytes().iter().enumerate() {
bus.write_byte(data_ptr + 1 + i as u32, *b);
}
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, data_ptr);
bus.write_long(TEST_SP + 4, handle);
assert!(disp
.dispatch_menu(true, 0x133, &mut cpu, &mut bus)
.unwrap()
.is_ok());
assert!(disp.menus[0].items[31].enabled, "item 32 starts enabled");
let menu_ptr = bus.read_long(handle);
let flags_before = bus.read_long(menu_ptr + 10);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 32);
bus.write_long(TEST_SP + 2, handle);
assert!(disp
.dispatch_menu(true, 0x13A, &mut cpu, &mut bus)
.unwrap()
.is_ok());
assert!(
disp.menus[0].items[31].enabled,
"DisableItem(item>31) must not change item state"
);
assert_eq!(
bus.read_long(menu_ptr + 10),
flags_before,
"DisableItem(item>31) must leave enableFlags unchanged"
);
}
#[test]
fn disableitem_zero_disables_menu_title_and_menukey_shortcuts() {
let (mut disp, mut cpu, mut bus) = setup();
let menu = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 144, 0x305A00, "File");
append_menu_data(
&mut disp,
&mut cpu,
&mut bus,
menu,
0x305B00,
"Open/O;Save/S",
);
insert_menu(&mut disp, &mut cpu, &mut bus, menu);
assert_eq!(
menu_key_result(&mut disp, &mut cpu, &mut bus, b'O'),
(144u32 << 16) | 1,
"MenuKey should resolve Open shortcut before whole-menu disable"
);
assert_eq!(
menu_key_result(&mut disp, &mut cpu, &mut bus, b'S'),
(144u32 << 16) | 2,
"MenuKey should resolve Save shortcut before whole-menu disable"
);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 0);
bus.write_long(TEST_SP + 2, menu);
assert!(disp
.dispatch_menu(true, 0x13A, &mut cpu, &mut bus)
.unwrap()
.is_ok());
let menu_state = disp
.menus
.iter()
.find(|m| m.handle == menu)
.expect("menu should remain tracked after disable");
assert!(
!menu_state.enabled,
"DisableItem(item=0) should disable the menu title"
);
assert_eq!(
menu_key_result(&mut disp, &mut cpu, &mut bus, b'O'),
0,
"MenuKey should return 0 for whole-menu-disabled Open shortcut"
);
assert_eq!(
menu_key_result(&mut disp, &mut cpu, &mut bus, b'S'),
0,
"MenuKey should return 0 for whole-menu-disabled Save shortcut"
);
}
#[test]
fn enableitem_item_gt_31_is_noop() {
let (mut disp, mut cpu, mut bus) = setup();
let title_ptr = 0x305400u32;
bus.write_byte(title_ptr, 4);
for (i, b) in b"Long".iter().enumerate() {
bus.write_byte(title_ptr + 1 + i as u32, *b);
}
bus.write_long(TEST_SP, title_ptr);
bus.write_word(TEST_SP + 4, 141);
assert!(disp
.dispatch_menu(true, 0x131, &mut cpu, &mut bus)
.unwrap()
.is_ok());
let handle = bus.read_long(cpu.read_reg(Register::A7));
let mut items = String::new();
for i in 1..=32 {
if i > 1 {
items.push(';');
}
if i == 32 {
items.push('(');
}
items.push_str(&format!("I{i:02}"));
}
let data_ptr = 0x305500u32;
bus.write_byte(data_ptr, items.len() as u8);
for (i, b) in items.as_bytes().iter().enumerate() {
bus.write_byte(data_ptr + 1 + i as u32, *b);
}
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, data_ptr);
bus.write_long(TEST_SP + 4, handle);
assert!(disp
.dispatch_menu(true, 0x133, &mut cpu, &mut bus)
.unwrap()
.is_ok());
assert!(
!disp.menus[0].items[31].enabled,
"item 32 starts disabled from metacharacter input"
);
let menu_ptr = bus.read_long(handle);
let flags_before = bus.read_long(menu_ptr + 10);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 32);
bus.write_long(TEST_SP + 2, handle);
assert!(disp
.dispatch_menu(true, 0x139, &mut cpu, &mut bus)
.unwrap()
.is_ok());
assert!(
!disp.menus[0].items[31].enabled,
"EnableItem(item>31) must not change item state"
);
assert_eq!(
bus.read_long(menu_ptr + 10),
flags_before,
"EnableItem(item>31) must leave enableFlags unchanged"
);
}
#[test]
fn enableitem_zero_reenables_menu_title_but_preserves_preexisting_disabled_items() {
let (mut disp, mut cpu, mut bus) = setup();
let title_ptr = 0x305600u32;
bus.write_byte(title_ptr, 4);
for (i, b) in b"Edit".iter().enumerate() {
bus.write_byte(title_ptr + 1 + i as u32, *b);
}
bus.write_long(TEST_SP, title_ptr);
bus.write_word(TEST_SP + 4, 142);
assert!(disp
.dispatch_menu(true, 0x131, &mut cpu, &mut bus)
.unwrap()
.is_ok());
let handle = bus.read_long(cpu.read_reg(Register::A7));
let data_ptr = 0x305700u32;
let data = b"Cut/X;Copy/C;Paste/V";
bus.write_byte(data_ptr, data.len() as u8);
for (i, b) in data.iter().enumerate() {
bus.write_byte(data_ptr + 1 + i as u32, *b);
}
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, data_ptr);
bus.write_long(TEST_SP + 4, handle);
assert!(disp
.dispatch_menu(true, 0x133, &mut cpu, &mut bus)
.unwrap()
.is_ok());
insert_menu(&mut disp, &mut cpu, &mut bus, handle);
assert_eq!(
menu_key_result(&mut disp, &mut cpu, &mut bus, b'X'),
(142u32 << 16) | 1,
"MenuKey should resolve item 1 before whole-menu disable"
);
assert_eq!(
menu_key_result(&mut disp, &mut cpu, &mut bus, b'C'),
(142u32 << 16) | 2,
"MenuKey should resolve item 2 before individual disable"
);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 2);
bus.write_long(TEST_SP + 2, handle);
assert!(disp
.dispatch_menu(true, 0x13A, &mut cpu, &mut bus)
.unwrap()
.is_ok());
assert!(!disp.menus[0].items[1].enabled);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 0);
bus.write_long(TEST_SP + 2, handle);
assert!(disp
.dispatch_menu(true, 0x13A, &mut cpu, &mut bus)
.unwrap()
.is_ok());
assert!(!disp.menus[0].enabled, "item=0 should disable menu title");
assert_eq!(
menu_key_result(&mut disp, &mut cpu, &mut bus, b'X'),
0,
"MenuKey should return 0 while whole menu is disabled"
);
assert_eq!(
menu_key_result(&mut disp, &mut cpu, &mut bus, b'C'),
0,
"MenuKey should return 0 for individually disabled item while whole menu is disabled"
);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 0);
bus.write_long(TEST_SP + 2, handle);
assert!(disp
.dispatch_menu(true, 0x139, &mut cpu, &mut bus)
.unwrap()
.is_ok());
assert!(disp.menus[0].enabled, "menu title should be re-enabled");
assert!(disp.menus[0].items[0].enabled, "item 1 stays enabled");
assert!(
!disp.menus[0].items[1].enabled,
"individually disabled item must remain disabled"
);
assert!(disp.menus[0].items[2].enabled, "item 3 stays enabled");
assert_eq!(
menu_key_result(&mut disp, &mut cpu, &mut bus, b'X'),
(142u32 << 16) | 1,
"MenuKey should resolve non-individually-disabled item after EnableItem(item=0)"
);
assert_eq!(
menu_key_result(&mut disp, &mut cpu, &mut bus, b'C'),
0,
"MenuKey should keep preexisting individually disabled item unavailable after EnableItem(item=0)"
);
let menu_ptr = bus.read_long(handle);
let flags = bus.read_long(menu_ptr + 10);
assert_eq!(flags & 1, 1, "menu-enabled bit should be set");
assert_eq!(flags & (1 << 2), 0, "item 2 bit should remain cleared");
}
#[test]
fn deletemenuitem_zero_or_oob_is_noop() {
let (mut disp, mut cpu, mut bus) = setup();
let title_ptr = 0x305800u32;
bus.write_byte(title_ptr, 4);
for (i, b) in b"Edit".iter().enumerate() {
bus.write_byte(title_ptr + 1 + i as u32, *b);
}
bus.write_long(TEST_SP, title_ptr);
bus.write_word(TEST_SP + 4, 143);
assert!(disp
.dispatch_menu(true, 0x131, &mut cpu, &mut bus)
.unwrap()
.is_ok());
let handle = bus.read_long(cpu.read_reg(Register::A7));
let data_ptr = 0x305900u32;
let data = b"A;B;C";
bus.write_byte(data_ptr, data.len() as u8);
for (i, b) in data.iter().enumerate() {
bus.write_byte(data_ptr + 1 + i as u32, *b);
}
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, data_ptr);
bus.write_long(TEST_SP + 4, handle);
assert!(disp
.dispatch_menu(true, 0x133, &mut cpu, &mut bus)
.unwrap()
.is_ok());
assert_eq!(disp.menus[0].items.len(), 3);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 0);
bus.write_long(TEST_SP + 2, handle);
assert!(disp
.dispatch_menu(true, 0x152, &mut cpu, &mut bus)
.unwrap()
.is_ok());
assert_eq!(disp.menus[0].items.len(), 3);
assert_eq!(disp.menus[0].items[0].text, "A");
assert_eq!(disp.menus[0].items[1].text, "B");
assert_eq!(disp.menus[0].items[2].text, "C");
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 4);
bus.write_long(TEST_SP + 2, handle);
assert!(disp
.dispatch_menu(true, 0x152, &mut cpu, &mut bus)
.unwrap()
.is_ok());
assert_eq!(disp.menus[0].items.len(), 3);
assert_eq!(disp.menus[0].items[0].text, "A");
assert_eq!(disp.menus[0].items[1].text, "B");
assert_eq!(disp.menus[0].items[2].text, "C");
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, handle);
assert!(disp
.dispatch_menu(true, 0x150, &mut cpu, &mut bus)
.unwrap()
.is_ok());
assert_eq!(bus.read_word(cpu.read_reg(Register::A7)), 3);
}
#[test]
fn test_insert_menu() {
let (mut disp, mut cpu, mut bus) = setup();
let menu_ptr = 0x300000u32;
bus.write_word(menu_ptr, 1); bus.write_byte(menu_ptr + 14, 4); bus.write_byte(menu_ptr + 15, b'F');
bus.write_byte(menu_ptr + 16, b'i');
bus.write_byte(menu_ptr + 17, b'l');
bus.write_byte(menu_ptr + 18, b'e');
let handle_addr = 0x300100u32;
bus.write_long(handle_addr, menu_ptr);
bus.write_word(TEST_SP, 0); bus.write_long(TEST_SP + 2, handle_addr);
let result = disp.dispatch_menu(true, 0x135, &mut cpu, &mut bus);
assert!(result.is_some(), "InsertMenu should be handled");
assert!(result.unwrap().is_ok(), "InsertMenu should succeed");
let sp = cpu.read_reg(Register::A7);
assert_eq!(sp, TEST_SP + 6, "InsertMenu should pop 6 bytes from stack");
assert_eq!(disp.menus.len(), 1, "InsertMenu should add one menu");
assert_eq!(
disp.menus[0].title, "File",
"InsertMenu should add 'File' to menus"
);
}
#[test]
fn test_newmenu_then_insertmenu_does_not_duplicate() {
let (mut disp, mut cpu, mut bus) = setup();
let title_ptr = 0x301000u32;
bus.write_byte(title_ptr, 4);
bus.write_byte(title_ptr + 1, b'F');
bus.write_byte(title_ptr + 2, b'i');
bus.write_byte(title_ptr + 3, b'l');
bus.write_byte(title_ptr + 4, b'e');
bus.write_long(TEST_SP, title_ptr); bus.write_word(TEST_SP + 4, 129); let result = disp.dispatch_menu(true, 0x131, &mut cpu, &mut bus);
assert!(result.is_some() && result.unwrap().is_ok());
let sp_after_new = cpu.read_reg(Register::A7);
let handle = bus.read_long(sp_after_new);
assert_ne!(handle, 0);
assert_eq!(
disp.menus.len(),
1,
"NewMenu should register the menu exactly once"
);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 0); bus.write_long(TEST_SP + 2, handle);
let result = disp.dispatch_menu(true, 0x135, &mut cpu, &mut bus);
assert!(result.is_some() && result.unwrap().is_ok());
assert_eq!(
disp.menus.len(),
1,
"InsertMenu must not duplicate the entry NewMenu already registered"
);
assert_eq!(disp.menus[0].title, "File");
assert_eq!(disp.menus[0].handle, handle);
assert_eq!(disp.last_inserted_menu_id, Some(129));
}
#[test]
fn invalmenubar_procedure_call_preserves_stack_pointer() {
let (mut disp, mut cpu, mut bus) = setup();
let sp_before = cpu.read_reg(Register::A7);
let result = disp.dispatch_menu(true, 0x01D, &mut cpu, &mut bus);
assert!(result.is_some(), "InvalMenuBar should be handled");
assert!(result.unwrap().is_ok(), "InvalMenuBar should succeed");
assert_eq!(
cpu.read_reg(Register::A7),
sp_before,
"InvalMenuBar is a parameterless procedure and must preserve A7"
);
let sp_before_five = cpu.read_reg(Register::A7);
for _ in 0..5 {
let result = disp.dispatch_menu(true, 0x01D, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
}
assert_eq!(
cpu.read_reg(Register::A7),
sp_before_five,
"InvalMenuBar must preserve A7 across a 5-call composition"
);
}
#[test]
fn initprocmenu_consumes_mbresid_and_preserves_stack_pointer() {
let (mut disp, mut cpu, mut bus) = setup();
let sp_start = cpu.read_reg(Register::A7);
let sentinel_addr = sp_start - 4;
bus.write_long(sentinel_addr, 0xCAFE_BEEF);
let sp_with_arg = sp_start - 6;
bus.write_word(sp_with_arg, 0x0000); cpu.write_reg(Register::A7, sp_with_arg);
let result = disp.dispatch_menu(true, 0x008, &mut cpu, &mut bus);
assert!(result.is_some(), "InitProcMenu should be handled");
assert!(result.unwrap().is_ok(), "InitProcMenu should succeed");
assert_eq!(
cpu.read_reg(Register::A7),
sp_with_arg + 2,
"InitProcMenu must pop exactly 2 bytes (one INTEGER mbResID)"
);
assert_eq!(
bus.read_long(sentinel_addr),
0xCAFE_BEEF,
"InitProcMenu must not write a FUNCTION result slot"
);
let sp_pre_five = cpu.read_reg(Register::A7);
let sp_after_pushes = sp_pre_five - 10;
for i in 0..5 {
bus.write_word(sp_pre_five - 2 * (i + 1) as u32, 0x0000);
}
cpu.write_reg(Register::A7, sp_after_pushes);
for _ in 0..5 {
let result = disp.dispatch_menu(true, 0x008, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
}
assert_eq!(
cpu.read_reg(Register::A7),
sp_pre_five,
"InitProcMenu must pop 2 bytes per call across a 5-call composition"
);
}
#[test]
fn test_draw_menu_bar() {
let (mut disp, mut cpu, mut bus) = setup_with_port();
let result = disp.dispatch_menu(true, 0x137, &mut cpu, &mut bus);
assert!(result.is_some(), "DrawMenuBar should be handled");
assert!(result.unwrap().is_ok(), "DrawMenuBar should succeed");
}
#[test]
fn drawmenubar_uninserted_newmenu_title_is_not_hit_testable() {
let (mut disp, mut cpu, mut bus) = setup_with_port();
disp.menu_bar_hidden = false;
bus.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 20);
let _ghost = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 300, 0x302000, "Ghost");
cpu.write_reg(Register::A7, TEST_SP);
let result = disp.dispatch_menu(true, 0x137, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert!(
disp.menu_title_regions().is_empty(),
"DrawMenuBar should have no title regions when no menus are inserted"
);
assert!(
disp.menu_title_hit_test(20).is_none(),
"Uninserted NewMenu title must not be menu-bar hit-testable"
);
}
#[test]
fn drawmenubar_skips_uninserted_menus_and_tracks_inserted_title_regions() {
let (mut disp, mut cpu, mut bus) = setup_with_port();
disp.menu_bar_hidden = false;
bus.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 20);
let _ghost = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 300, 0x302000, "Ghost");
let file = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 128, 0x302100, "File");
let edit = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 129, 0x302200, "Edit");
insert_menu(&mut disp, &mut cpu, &mut bus, file);
insert_menu(&mut disp, &mut cpu, &mut bus, edit);
cpu.write_reg(Register::A7, TEST_SP);
let result = disp.dispatch_menu(true, 0x137, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
let regions = disp.menu_title_regions();
assert_eq!(
regions.len(),
2,
"Only inserted menus should receive menu-bar title regions"
);
let file_mid = (regions[0].0 + regions[0].1) / 2;
let file_idx = disp.menu_title_hit_test(file_mid).expect("file hit");
assert_eq!(disp.menus[file_idx].id, 128);
let edit_mid = (regions[1].0 + regions[1].1) / 2;
let edit_idx = disp.menu_title_hit_test(edit_mid).expect("edit hit");
assert_eq!(disp.menus[edit_idx].id, 129);
}
#[test]
fn drawmenubar_tracks_full_top_menubar_title_order_and_hits() {
let (mut disp, mut cpu, mut bus) = setup_with_port();
disp.menu_bar_hidden = false;
bus.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 20);
for (id, title, ptr) in [
(128, "Apple", 0x302300),
(129, "File", 0x302360),
(130, "Edit", 0x3023C0),
(131, "Special", 0x302420),
] {
let handle = new_menu_with_title(&mut disp, &mut cpu, &mut bus, id, ptr, title);
insert_menu(&mut disp, &mut cpu, &mut bus, handle);
}
cpu.write_reg(Register::A7, TEST_SP);
let result = disp.dispatch_menu(true, 0x137, &mut cpu, &mut bus);
assert!(result.is_some(), "DrawMenuBar should be handled");
assert!(result.unwrap().is_ok(), "DrawMenuBar should succeed");
let regions = disp.menu_title_regions();
assert_eq!(
regions.len(),
4,
"Apple/File/Edit/Special should all receive title hit regions"
);
for pair in regions.windows(2) {
assert!(
pair[0].1 <= pair[1].0,
"top menu-bar title regions should be left-to-right and non-overlapping"
);
}
for ((left, right), expected_id) in regions.iter().copied().zip([128, 129, 130, 131]) {
let hit_h = (left + right) / 2;
let menu_idx = disp
.menu_title_hit_test(hit_h)
.expect("title midpoint should hit an inserted menu");
assert_eq!(
disp.menus[menu_idx].id, expected_id,
"menu title midpoint should hit the expected top-bar menu"
);
}
}
#[test]
fn drawmenubar_systemless_theme_routes_chrome_through_provider() {
let (mut classic, mut classic_cpu, mut classic_bus) = setup_with_port();
let classic_row_bytes = 64;
let classic_base = classic_bus.alloc(classic_row_bytes * 342);
classic.set_screen_mode_for_test(classic_base, classic_row_bytes, 512, 342, 1);
classic.menu_bar_hidden = false;
clear_1bpp_screen(&mut classic_bus, classic_base, classic_row_bytes, 342);
classic_bus.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 20);
classic_cpu.write_reg(Register::A7, TEST_SP);
classic
.dispatch_menu(true, 0x137, &mut classic_cpu, &mut classic_bus)
.unwrap()
.unwrap();
let (mut themed, mut themed_cpu, mut themed_bus) = setup_with_port();
let themed_row_bytes = 64;
let themed_base = themed_bus.alloc(themed_row_bytes * 342);
themed.set_ui_theme_id(UiThemeId::SystemlessDefault);
themed.set_screen_mode_for_test(themed_base, themed_row_bytes, 512, 342, 1);
themed.menu_bar_hidden = false;
clear_1bpp_screen(&mut themed_bus, themed_base, themed_row_bytes, 342);
themed_bus.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 20);
themed_cpu.write_reg(Register::A7, TEST_SP);
themed
.dispatch_menu(true, 0x137, &mut themed_cpu, &mut themed_bus)
.unwrap()
.unwrap();
assert!(
!screen_pixel_is_set(&classic_bus, classic_base, classic_row_bytes, 5, 0),
"classic System 7 menu bar chrome should leave the top edge white"
);
assert!(
screen_pixel_is_set(&themed_bus, themed_base, themed_row_bytes, 5, 0),
"systemless-default menu bar provider should own the top edge"
);
}
#[test]
fn drawmenubar_systemless_theme_routes_title_states_through_provider() {
let (mut classic, mut classic_cpu, mut classic_bus) = setup_with_port();
let classic_row_bytes = 64;
let classic_base = classic_bus.alloc(classic_row_bytes * 342);
classic.set_screen_mode_for_test(classic_base, classic_row_bytes, 512, 342, 1);
classic.menu_bar_hidden = false;
clear_1bpp_screen(&mut classic_bus, classic_base, classic_row_bytes, 342);
classic_bus.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 20);
let classic_file = new_menu_with_title(
&mut classic,
&mut classic_cpu,
&mut classic_bus,
621,
0x302E00,
"File",
);
let classic_edit = new_menu_with_title(
&mut classic,
&mut classic_cpu,
&mut classic_bus,
622,
0x302E40,
"Edit",
);
insert_menu(
&mut classic,
&mut classic_cpu,
&mut classic_bus,
classic_file,
);
insert_menu(
&mut classic,
&mut classic_cpu,
&mut classic_bus,
classic_edit,
);
classic_cpu.write_reg(Register::A7, TEST_SP);
classic_bus.write_word(TEST_SP, 0);
classic_bus.write_long(TEST_SP + 2, classic_edit);
classic
.dispatch_menu(true, 0x13A, &mut classic_cpu, &mut classic_bus)
.unwrap()
.unwrap();
classic_cpu.write_reg(Register::A7, TEST_SP);
classic
.dispatch_menu(true, 0x137, &mut classic_cpu, &mut classic_bus)
.unwrap()
.unwrap();
let (mut themed, mut themed_cpu, mut themed_bus) = setup_with_port();
let themed_row_bytes = 64;
let themed_base = themed_bus.alloc(themed_row_bytes * 342);
themed.set_ui_theme_id(UiThemeId::SystemlessDefault);
themed.set_screen_mode_for_test(themed_base, themed_row_bytes, 512, 342, 1);
themed.menu_bar_hidden = false;
clear_1bpp_screen(&mut themed_bus, themed_base, themed_row_bytes, 342);
themed_bus.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 20);
let themed_file = new_menu_with_title(
&mut themed,
&mut themed_cpu,
&mut themed_bus,
621,
0x302E00,
"File",
);
let themed_edit = new_menu_with_title(
&mut themed,
&mut themed_cpu,
&mut themed_bus,
622,
0x302E40,
"Edit",
);
insert_menu(&mut themed, &mut themed_cpu, &mut themed_bus, themed_file);
insert_menu(&mut themed, &mut themed_cpu, &mut themed_bus, themed_edit);
themed_cpu.write_reg(Register::A7, TEST_SP);
themed_bus.write_word(TEST_SP, 0);
themed_bus.write_long(TEST_SP + 2, themed_edit);
themed
.dispatch_menu(true, 0x13A, &mut themed_cpu, &mut themed_bus)
.unwrap()
.unwrap();
themed_cpu.write_reg(Register::A7, TEST_SP);
themed
.dispatch_menu(true, 0x137, &mut themed_cpu, &mut themed_bus)
.unwrap()
.unwrap();
let classic_regions = classic.menu_title_regions();
let themed_regions = themed.menu_title_regions();
assert_eq!(
themed_regions, classic_regions,
"systemless-default menu-title chrome must preserve title hit regions"
);
let (file_left, _file_right) = themed_regions[0];
let (edit_left, _edit_right) = themed_regions[1];
assert!(
screen_pixel_is_set(
&themed_bus,
themed_base,
themed_row_bytes,
file_left + 4,
17
),
"systemless-default should draw provider chrome for an enabled menu title"
);
assert!(
!screen_pixel_is_set(
&classic_bus,
classic_base,
classic_row_bytes,
file_left + 4,
17
),
"classic System 7 path should not draw the systemless enabled-title underline"
);
assert!(
screen_pixel_is_set(&themed_bus, themed_base, themed_row_bytes, edit_left + 2, 3),
"systemless-default should draw provider chrome for a disabled menu title"
);
assert!(
!screen_pixel_is_set(
&classic_bus,
classic_base,
classic_row_bytes,
edit_left + 2,
3
),
"classic System 7 path should not draw the systemless disabled-title frame"
);
}
#[test]
fn highlight_menu_title_systemless_theme_routes_highlight_state_through_provider() {
let (mut disp, mut cpu, mut bus) = setup_with_port();
let row_bytes = 64;
let base = bus.alloc(row_bytes * 342);
disp.set_ui_theme_id(UiThemeId::SystemlessDefault);
disp.set_screen_mode_for_test(base, row_bytes, 512, 342, 1);
disp.menu_bar_hidden = false;
clear_1bpp_screen(&mut bus, base, row_bytes, 342);
bus.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 20);
let file = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 623, 0x302E80, "File");
insert_menu(&mut disp, &mut cpu, &mut bus, file);
cpu.write_reg(Register::A7, TEST_SP);
disp.dispatch_menu(true, 0x137, &mut cpu, &mut bus)
.unwrap()
.unwrap();
let regions = disp.menu_title_regions();
let (left, right) = regions[0];
assert!(
screen_pixel_is_set(&bus, base, row_bytes, left + 4, 17),
"precondition: enabled systemless title chrome should draw before highlighting"
);
let title_pixel = (2..16)
.flat_map(|y| ((left + 7)..(right - 7)).map(move |x| (x, y)))
.find(|(x, y)| screen_pixel_is_set(&bus, base, row_bytes, *x, *y))
.expect("precondition: menu title text should draw before highlighting");
disp.highlight_menu_title(&mut bus, 0);
assert_eq!(
disp.menu_title_regions(),
regions,
"systemless highlighted title chrome must preserve title hit regions"
);
assert!(
screen_pixel_is_set(&bus, base, row_bytes, left + 1, 5),
"systemless-default should fill highlighted title chrome through the provider"
);
assert!(
screen_pixel_is_set(&bus, base, row_bytes, left + 4, 17),
"provider-highlighted title chrome should redraw, not invert, the enabled-title underline"
);
assert!(
!screen_pixel_is_set(&bus, base, row_bytes, title_pixel.0, title_pixel.1),
"highlighted systemless title should redraw the menu text in the highlighted foreground"
);
}
#[test]
fn draw_menu_dropdown_systemless_theme_routes_chrome_through_provider() {
let rect = (20, 20, 56, 120);
let (mut classic, mut classic_cpu, mut classic_bus) = setup_with_port();
let classic_row_bytes = 64;
let classic_base = classic_bus.alloc(classic_row_bytes * 342);
classic.set_screen_mode_for_test(classic_base, classic_row_bytes, 512, 342, 1);
clear_1bpp_screen(&mut classic_bus, classic_base, classic_row_bytes, 342);
let classic_menu = new_menu_with_title(
&mut classic,
&mut classic_cpu,
&mut classic_bus,
610,
0x302300,
"File",
);
append_menu_data(
&mut classic,
&mut classic_cpu,
&mut classic_bus,
classic_menu,
0x302340,
"Open/O;Close/W",
);
classic.draw_menu_dropdown(&mut classic_bus, 0, rect);
let (mut themed, mut themed_cpu, mut themed_bus) = setup_with_port();
let themed_row_bytes = 64;
let themed_base = themed_bus.alloc(themed_row_bytes * 342);
themed.set_ui_theme_id(UiThemeId::SystemlessDefault);
themed.set_screen_mode_for_test(themed_base, themed_row_bytes, 512, 342, 1);
clear_1bpp_screen(&mut themed_bus, themed_base, themed_row_bytes, 342);
let themed_menu = new_menu_with_title(
&mut themed,
&mut themed_cpu,
&mut themed_bus,
610,
0x302300,
"File",
);
append_menu_data(
&mut themed,
&mut themed_cpu,
&mut themed_bus,
themed_menu,
0x302340,
"Open/O;Close/W",
);
themed.draw_menu_dropdown(&mut themed_bus, 0, rect);
assert!(
screen_pixel_is_set(&classic_bus, classic_base, classic_row_bytes, 120, 22),
"classic dropdown chrome should draw its right-edge drop shadow"
);
assert!(
!screen_pixel_is_set(&themed_bus, themed_base, themed_row_bytes, 120, 22),
"systemless-default dropdown provider should not draw the classic shadow"
);
}
#[test]
fn draw_menu_dropdown_systemless_theme_routes_item_states_through_provider() {
let rect = (20, 20, 86, 140);
let (mut classic, mut classic_cpu, mut classic_bus) = setup_with_port();
let classic_row_bytes = 64;
let classic_base = classic_bus.alloc(classic_row_bytes * 342);
classic.set_screen_mode_for_test(classic_base, classic_row_bytes, 512, 342, 1);
clear_1bpp_screen(&mut classic_bus, classic_base, classic_row_bytes, 342);
let classic_menu = new_menu_with_title(
&mut classic,
&mut classic_cpu,
&mut classic_bus,
611,
0x302400,
"File",
);
append_menu_data(
&mut classic,
&mut classic_cpu,
&mut classic_bus,
classic_menu,
0x302440,
"Open/O;Pick;-;(Dim/D",
);
classic.menus[0].items[0].mark = 0x12;
classic.menus[0].items[1].icon = 7;
classic.menu_tracking = Some(MenuTrackingState {
active_menu: 0,
highlighted_item: 2,
saved_pixels: Vec::new(),
dropdown_rect: rect,
stack_ptr: TEST_SP,
flash_remaining: 0,
flash_delay: 0,
flash_result: 0,
submenu: None,
});
classic.draw_menu_dropdown(&mut classic_bus, 0, rect);
let (mut themed, mut themed_cpu, mut themed_bus) = setup_with_port();
let themed_row_bytes = 64;
let themed_base = themed_bus.alloc(themed_row_bytes * 342);
themed.set_ui_theme_id(UiThemeId::SystemlessDefault);
themed.set_screen_mode_for_test(themed_base, themed_row_bytes, 512, 342, 1);
clear_1bpp_screen(&mut themed_bus, themed_base, themed_row_bytes, 342);
let themed_menu = new_menu_with_title(
&mut themed,
&mut themed_cpu,
&mut themed_bus,
611,
0x302400,
"File",
);
append_menu_data(
&mut themed,
&mut themed_cpu,
&mut themed_bus,
themed_menu,
0x302440,
"Open/O;Pick;-;(Dim/D",
);
themed.menus[0].items[0].mark = 0x12;
themed.menus[0].items[1].icon = 7;
themed.menu_tracking = Some(MenuTrackingState {
active_menu: 0,
highlighted_item: 2,
saved_pixels: Vec::new(),
dropdown_rect: rect,
stack_ptr: TEST_SP,
flash_remaining: 0,
flash_delay: 0,
flash_result: 0,
submenu: None,
});
themed.draw_menu_dropdown(&mut themed_bus, 0, rect);
assert!(
!screen_pixel_is_set(&classic_bus, classic_base, classic_row_bytes, 23, 39),
"classic dropdown rows should not draw the systemless highlight rail"
);
assert!(
screen_pixel_is_set(&themed_bus, themed_base, themed_row_bytes, 23, 39),
"systemless-default provider should own highlighted menu-item row chrome"
);
assert!(
!screen_pixel_is_set(&classic_bus, classic_base, classic_row_bytes, 36, 43),
"classic dropdown path should not draw systemless menu-item icon chrome"
);
assert!(
screen_pixel_is_set(&themed_bus, themed_base, themed_row_bytes, 36, 43),
"systemless-default provider should receive and render menu-item icon state"
);
assert!(
!screen_pixel_is_set(&classic_bus, classic_base, classic_row_bytes, 29, 79),
"classic separator remains dotted at this odd x-coordinate"
);
assert!(
screen_pixel_is_set(&themed_bus, themed_base, themed_row_bytes, 29, 79),
"systemless-default provider should own separator menu-item row chrome"
);
}
#[test]
fn draw_menu_dropdown_systemless_theme_owns_mark_and_command_indicators() {
let rect = (20, 20, 56, 140);
let row_top = rect.0 + 1;
let row_bottom = row_top + 16;
let (mut classic, mut classic_cpu, mut classic_bus) = setup_with_port();
let classic_row_bytes = 64;
let classic_base = classic_bus.alloc(classic_row_bytes * 342);
classic.set_screen_mode_for_test(classic_base, classic_row_bytes, 512, 342, 1);
clear_1bpp_screen(&mut classic_bus, classic_base, classic_row_bytes, 342);
let classic_menu = new_menu_with_title(
&mut classic,
&mut classic_cpu,
&mut classic_bus,
618,
0x302B00,
"File",
);
append_menu_data(
&mut classic,
&mut classic_cpu,
&mut classic_bus,
classic_menu,
0x302B40,
"Open/O",
);
classic.menus[0].items[0].mark = b'W';
classic.draw_menu_dropdown(&mut classic_bus, 0, rect);
let (mut themed, mut themed_cpu, mut themed_bus) = setup_with_port();
let themed_row_bytes = 64;
let themed_base = themed_bus.alloc(themed_row_bytes * 342);
themed.set_ui_theme_id(UiThemeId::SystemlessDefault);
themed.set_screen_mode_for_test(themed_base, themed_row_bytes, 512, 342, 1);
clear_1bpp_screen(&mut themed_bus, themed_base, themed_row_bytes, 342);
let themed_menu = new_menu_with_title(
&mut themed,
&mut themed_cpu,
&mut themed_bus,
618,
0x302B00,
"File",
);
append_menu_data(
&mut themed,
&mut themed_cpu,
&mut themed_bus,
themed_menu,
0x302B40,
"Open/O",
);
themed.menus[0].items[0].mark = b'W';
themed.draw_menu_dropdown(&mut themed_bus, 0, rect);
let provider_mark_pixel = |x: i16, y: i16| {
let provider_left = rect.1 + 1;
let provider_top = row_top;
x >= provider_left + 5
&& x < provider_left + 13
&& y >= provider_top + 3
&& y < provider_top + 13
};
let mark_pixel = (row_top..row_bottom)
.flat_map(|y| ((rect.1 + 4)..(rect.1 + 16)).map(move |x| (x, y)))
.find(|(x, y)| {
screen_pixel_is_set(&classic_bus, classic_base, classic_row_bytes, *x, *y)
&& !provider_mark_pixel(*x, *y)
})
.expect("classic mark glyph should have a pixel outside the provider mark indicator");
let provider_command_pixel = |x: i16, y: i16| {
let provider_left = rect.1 + 1;
let provider_top = row_top;
let provider_width = (rect.3 - 1) - provider_left;
x >= provider_left + provider_width - 15
&& x < provider_left + provider_width - 13
&& y >= provider_top + 4
&& y < provider_top + 12
};
let command_pixel = (row_top..row_bottom)
.flat_map(|y| ((rect.3 - 28)..(rect.3 - 4)).map(move |x| (x, y)))
.find(|(x, y)| {
screen_pixel_is_set(&classic_bus, classic_base, classic_row_bytes, *x, *y)
&& !provider_command_pixel(*x, *y)
})
.expect(
"classic command-key glyph should have a pixel outside the provider command marker",
);
assert!(
!screen_pixel_is_set(
&themed_bus,
themed_base,
themed_row_bytes,
mark_pixel.0,
mark_pixel.1
),
"systemless-default should not draw the classic mark glyph once the provider owns marks"
);
assert!(
!screen_pixel_is_set(
&themed_bus,
themed_base,
themed_row_bytes,
command_pixel.0,
command_pixel.1
),
"systemless-default should not draw the classic command-key glyph once the provider owns command indicators"
);
assert!(
screen_pixel_is_set(
&themed_bus,
themed_base,
themed_row_bytes,
rect.1 + 8,
row_top + 3
),
"systemless-default provider should still draw its mark indicator"
);
assert!(
screen_pixel_is_set(
&themed_bus,
themed_base,
themed_row_bytes,
rect.3 - 16,
row_top + 5
),
"systemless-default provider should still draw its command-key indicator"
);
}
#[test]
fn draw_menu_dropdown_applies_setitemstyle_pixels_with_classic_style_metrics() {
let rect = (20, 20, 104, 180);
let menu_data = "Bold;Italic;Underline;Outline;Shadow;Condense;Extend";
let style_cases = [
("bold", "Bold", 0x01u8),
("italic", "Italic", 0x02u8),
("underline", "Underline", 0x04u8),
("outline", "Outline", 0x08u8),
("shadow", "Shadow", 0x10u8),
("condense", "Condense", 0x20u8),
("extend", "Extend", 0x40u8),
];
let (mut plain, mut plain_cpu, mut plain_bus) = setup_with_port();
let plain_row_bytes = 64;
let plain_base = plain_bus.alloc(plain_row_bytes * 342);
plain.set_screen_mode_for_test(plain_base, plain_row_bytes, 512, 342, 1);
clear_1bpp_screen(&mut plain_bus, plain_base, plain_row_bytes, 342);
let plain_menu = new_menu_with_title(
&mut plain,
&mut plain_cpu,
&mut plain_bus,
619,
0x302C00,
"Style",
);
append_menu_data(
&mut plain,
&mut plain_cpu,
&mut plain_bus,
plain_menu,
0x302C40,
menu_data,
);
let plain_size =
calc_menu_size_for_test(&mut plain, &mut plain_cpu, &mut plain_bus, plain_menu);
plain.draw_menu_dropdown(&mut plain_bus, 0, rect);
let (mut styled, mut styled_cpu, mut styled_bus) = setup_with_port();
let styled_row_bytes = 64;
let styled_base = styled_bus.alloc(styled_row_bytes * 342);
styled.set_screen_mode_for_test(styled_base, styled_row_bytes, 512, 342, 1);
clear_1bpp_screen(&mut styled_bus, styled_base, styled_row_bytes, 342);
let styled_menu = new_menu_with_title(
&mut styled,
&mut styled_cpu,
&mut styled_bus,
619,
0x302C00,
"Style",
);
append_menu_data(
&mut styled,
&mut styled_cpu,
&mut styled_bus,
styled_menu,
0x302C40,
menu_data,
);
for (idx, (_label, _text, style)) in style_cases.iter().enumerate() {
set_menu_item_style(
&mut styled,
&mut styled_cpu,
&mut styled_bus,
styled_menu,
idx as i16 + 1,
*style,
);
}
let styled_size =
calc_menu_size_for_test(&mut styled, &mut styled_cpu, &mut styled_bus, styled_menu);
styled.draw_menu_dropdown(&mut styled_bus, 0, rect);
assert_eq!(
styled_size.0, plain_size.0,
"SetItemStyle should not change CalcMenuSize width"
);
assert_eq!(
styled_size.1,
plain_size.1 + (super::MENU_SHADOW_STYLE_ROW_HEIGHT - super::MENU_ROW_HEIGHT),
"System 7.5.3's standard MDEF grows the shadow-styled item row"
);
let mut row_top = rect.0 + 1;
for (idx, (label, text, style)) in style_cases.iter().enumerate() {
let styled_height = styled.menu_item_height(&styled_bus, &styled.menus[0].items[idx]);
let plain_height = plain.menu_item_height(&plain_bus, &plain.menus[0].items[idx]);
let expected_height = if (*style & super::MENU_TEXT_STYLE_SHADOW) != 0 {
plain_height.max(super::MENU_SHADOW_STYLE_ROW_HEIGHT)
} else {
plain_height
};
assert_eq!(
styled_height, expected_height,
"{label} style should use the classic MDEF row height"
);
assert_eq!(
styled.menu_item_width_extra(&styled_bus, &styled.menus[0].items[idx]),
plain.menu_item_width_extra(&plain_bus, &plain.menus[0].items[idx]),
"{label} style should not change mark/icon/command geometry"
);
let row_bottom = row_top + styled_height;
let text_left = rect.1 + 18;
let text_right =
text_left + super::super::TrapDispatcher::fb_measure_string(text, 0, 12) + 8;
let styled_only_pixel = ((row_top - 1)..(row_bottom + 2))
.flat_map(|y| ((text_left - 2)..(text_right + 4)).map(move |x| (x, y)))
.find(|(x, y)| {
screen_pixel_is_set(&styled_bus, styled_base, styled_row_bytes, *x, *y)
&& !screen_pixel_is_set(&plain_bus, plain_base, plain_row_bytes, *x, *y)
});
let plain_only_pixel = ((row_top - 1)..(row_bottom + 2))
.flat_map(|y| ((text_left - 2)..(text_right + 4)).map(move |x| (x, y)))
.find(|(x, y)| {
screen_pixel_is_set(&plain_bus, plain_base, plain_row_bytes, *x, *y)
&& !screen_pixel_is_set(&styled_bus, styled_base, styled_row_bytes, *x, *y)
});
assert!(
styled_only_pixel.is_some() || plain_only_pixel.is_some(),
"{label} SetItemStyle bit should produce a visible menu-text pixel difference"
);
row_top = row_bottom;
}
}
#[test]
fn draw_menu_dropdown_8bpp_uses_menucinfo_background_and_item_name_color() {
let rect = (20, 20, 38, 110);
let menu_id = 620;
let red = (0xFFFF, 0, 0);
let green = (0, 0xFFFF, 0);
let blue = (0, 0, 0xFFFF);
let black = (0, 0, 0);
let white = (0xFFFF, 0xFFFF, 0xFFFF);
let (mut disp, mut cpu, mut bus) = setup_with_port();
let (base, row_bytes) = setup_8bpp_menu_screen(&mut disp, &mut bus, 128, 96);
let menu = new_menu_with_title(&mut disp, &mut cpu, &mut bus, menu_id, 0x302D00, "Color");
append_menu_data(&mut disp, &mut cpu, &mut bus, menu, 0x302D40, "Open");
let entries_ptr = bus.alloc((2 * MC_ENTRY_SIZE) as u32);
write_mc_entry_colors(&mut bus, entries_ptr, menu_id, 0, black, white, green, red);
write_mc_entry_colors(
&mut bus,
entries_ptr + MC_ENTRY_SIZE as u32,
menu_id,
1,
green,
blue,
green,
red,
);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, entries_ptr);
bus.write_word(TEST_SP + 4, 2);
assert!(
disp.dispatch_menu(true, 0x265, &mut cpu, &mut bus)
.unwrap()
.is_ok(),
"SetMCEntries should install title and item color entries"
);
disp.draw_menu_dropdown(&mut bus, 0, rect);
assert_eq!(
screen_pixel_index(&bus, base, row_bytes, 24, 22),
35,
"title RGB4 full red should fill the 8bpp dropdown background"
);
let text_left = rect.1 + 18;
let text_top = rect.0 + 1;
let text_bottom = text_top + 16;
let text_right = text_left + super::super::TrapDispatcher::fb_measure_string("Open", 0, 12);
let has_blue_text_pixel = (text_top..text_bottom)
.flat_map(|y| (text_left..text_right).map(move |x| (x, y)))
.any(|(x, y)| screen_pixel_index(&bus, base, row_bytes, x, y) == 210);
assert!(
has_blue_text_pixel,
"item RGB2 full blue should draw the menu item name"
);
let has_title_default_green_text_pixel = (text_top..text_bottom)
.flat_map(|y| (text_left..text_right).map(move |x| (x, y)))
.any(|(x, y)| screen_pixel_index(&bus, base, row_bytes, x, y) == 185);
assert!(
!has_title_default_green_text_pixel,
"item RGB2 should override the title RGB3 default item color"
);
}
#[test]
fn draw_menu_dropdown_8bpp_uses_item_name_color_for_black_and_white_icons() {
let rect = (20, 20, 70, 150);
let menu_id = 621;
let red = (0xFFFF, 0, 0);
let green = (0, 0xFFFF, 0);
let blue = (0, 0, 0xFFFF);
let black = (0, 0, 0);
let white = (0xFFFF, 0xFFFF, 0xFFFF);
let (mut disp, mut cpu, mut bus) = setup_with_port();
let (base, row_bytes) = setup_8bpp_menu_screen(&mut disp, &mut bus, 160, 100);
let menu = new_menu_with_title(&mut disp, &mut cpu, &mut bus, menu_id, 0x302E00, "Color");
append_menu_data(&mut disp, &mut cpu, &mut bus, menu, 0x302E40, "Big;Small");
disp.menus[0].items[0].icon = 7;
disp.menus[0].items[1].icon = 8;
disp.menus[0].items[1].key_equiv = MENU_KEY_SMALL_ICON;
let icon = menu_icon_source_with_left_stripe();
let sicn = sicn_source_with_left_stripe();
disp.install_test_resource(&mut bus, *b"ICON", 263, &icon);
disp.install_test_resource(&mut bus, *b"SICN", 264, &sicn);
let entries_ptr = bus.alloc((2 * MC_ENTRY_SIZE) as u32);
write_mc_entry_colors(&mut bus, entries_ptr, menu_id, 0, black, white, green, red);
write_mc_entry_colors(
&mut bus,
entries_ptr + MC_ENTRY_SIZE as u32,
menu_id,
1,
green,
blue,
green,
red,
);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, entries_ptr);
bus.write_word(TEST_SP + 4, 2);
assert!(
disp.dispatch_menu(true, 0x265, &mut cpu, &mut bus)
.unwrap()
.is_ok(),
"SetMCEntries should install title and item color entries"
);
disp.draw_menu_dropdown(&mut bus, 0, rect);
assert_eq!(
screen_pixel_index(&bus, base, row_bytes, rect.1 + 4, rect.0 + 1),
210,
"normal black-and-white ICON pixels should use the item RGB2 name color"
);
assert_eq!(
screen_pixel_index(&bus, base, row_bytes, rect.1 + 4, rect.0 + 35),
185,
"SICN pixels should fall back to the title RGB3 default item color"
);
}
#[test]
fn draw_menu_bar_8bpp_uses_menucinfo_bar_and_title_colors() {
let file_id = 622;
let edit_id = 623;
let red = (0xFFFF, 0, 0);
let green = (0, 0xFFFF, 0);
let blue = (0, 0, 0xFFFF);
let white = (0xFFFF, 0xFFFF, 0xFFFF);
let (mut disp, mut cpu, mut bus) = setup_with_port();
let (base, row_bytes) = setup_8bpp_menu_screen(&mut disp, &mut bus, 160, 64);
disp.menu_bar_hidden = false;
bus.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 20);
let file = new_menu_with_title(&mut disp, &mut cpu, &mut bus, file_id, 0x302F00, "File");
let edit = new_menu_with_title(&mut disp, &mut cpu, &mut bus, edit_id, 0x302F40, "Edit");
insert_menu(&mut disp, &mut cpu, &mut bus, file);
insert_menu(&mut disp, &mut cpu, &mut bus, edit);
let entries_ptr = bus.alloc((2 * MC_ENTRY_SIZE) as u32);
write_mc_entry_colors(&mut bus, entries_ptr, 0, 0, green, white, green, red);
write_mc_entry_colors(
&mut bus,
entries_ptr + MC_ENTRY_SIZE as u32,
file_id,
0,
blue,
red,
green,
white,
);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, entries_ptr);
bus.write_word(TEST_SP + 4, 2);
assert!(
disp.dispatch_menu(true, 0x265, &mut cpu, &mut bus)
.unwrap()
.is_ok(),
"SetMCEntries should install menu bar and title color entries"
);
disp.draw_menu_bar_to_fb(&mut bus);
assert_eq!(
screen_pixel_index(&bus, base, row_bytes, 5, 5),
35,
"menu bar entry RGB4 full red should fill the 8bpp menu bar"
);
let regions = disp.menu_title_regions();
let file_has_blue_title_pixel = (1..19).any(|y| {
(regions[0].0..regions[0].1)
.any(|x| screen_pixel_index(&bus, base, row_bytes, x, y) == 210)
});
assert!(
file_has_blue_title_pixel,
"menu title entry RGB1 full blue should draw that menu title"
);
let edit_has_green_title_pixel = (1..19).any(|y| {
(regions[1].0..regions[1].1)
.any(|x| screen_pixel_index(&bus, base, row_bytes, x, y) == 185)
});
assert!(
edit_has_green_title_pixel,
"menu bar entry RGB1 full green should draw titles without their own entry"
);
}
#[test]
fn draw_menu_dropdown_8bpp_highlight_swaps_background_with_hilite_color() {
let rect = (20, 20, 38, 110);
let menu_id = 624;
let red = (0xFFFF, 0, 0);
let green = (0, 0xFFFF, 0);
let blue = (0, 0, 0xFFFF);
let black = (0, 0, 0);
let white = (0xFFFF, 0xFFFF, 0xFFFF);
let (mut disp, mut cpu, mut bus) = setup_with_port();
disp.hilite_color = green;
let (base, row_bytes) = setup_8bpp_menu_screen(&mut disp, &mut bus, 128, 96);
let menu = new_menu_with_title(&mut disp, &mut cpu, &mut bus, menu_id, 0x303000, "Color");
append_menu_data(&mut disp, &mut cpu, &mut bus, menu, 0x303040, "Open");
let entries_ptr = bus.alloc((2 * MC_ENTRY_SIZE) as u32);
write_mc_entry_colors(&mut bus, entries_ptr, menu_id, 0, black, white, green, red);
write_mc_entry_colors(
&mut bus,
entries_ptr + MC_ENTRY_SIZE as u32,
menu_id,
1,
green,
blue,
green,
red,
);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, entries_ptr);
bus.write_word(TEST_SP + 4, 2);
assert!(
disp.dispatch_menu(true, 0x265, &mut cpu, &mut bus)
.unwrap()
.is_ok(),
"SetMCEntries should install dropdown color entries"
);
disp.draw_menu_dropdown(&mut bus, 0, rect);
let bg_x = rect.1 + 70;
let bg_y = rect.0 + 5;
assert_eq!(
screen_pixel_index(&bus, base, row_bytes, bg_x, bg_y),
35,
"precondition: dropdown background should be the MenuCInfo red"
);
let hilite_swap_x = rect.1 + 80;
let hilite_swap_y = rect.0 + 5;
bus.write_byte(
base + (hilite_swap_y as u32) * row_bytes + hilite_swap_x as u32,
185,
);
let text_left = rect.1 + 18;
let text_top = rect.0 + 1;
let text_bottom = text_top + 16;
let text_right = text_left + super::super::TrapDispatcher::fb_measure_string("Open", 0, 12);
let blue_text_pixel = (text_top..text_bottom)
.flat_map(|y| (text_left..text_right).map(move |x| (x, y)))
.find(|(x, y)| screen_pixel_index(&bus, base, row_bytes, *x, *y) == 210)
.expect("precondition: item RGB2 blue text should draw");
disp.invert_dropdown_item_rect(&mut bus, 0, rect, 1);
assert_eq!(
screen_pixel_index(&bus, base, row_bytes, bg_x, bg_y),
185,
"8bpp menu highlighting should swap the background to HiliteColor"
);
assert_ne!(
screen_pixel_index(&bus, base, row_bytes, bg_x, bg_y),
220,
"8bpp menu highlighting must not use the indexed complement of red"
);
assert_eq!(
screen_pixel_index(&bus, base, row_bytes, hilite_swap_x, hilite_swap_y),
35,
"8bpp menu highlighting should swap existing HiliteColor pixels back to the background"
);
assert_eq!(
screen_pixel_index(&bus, base, row_bytes, blue_text_pixel.0, blue_text_pixel.1),
210,
"8bpp menu highlighting should leave non-background MenuCInfo text colors unchanged"
);
disp.invert_dropdown_item_rect(&mut bus, 0, rect, 1);
assert_eq!(
screen_pixel_index(&bus, base, row_bytes, bg_x, bg_y),
35,
"highlighting the same row twice should restore the background"
);
assert_eq!(
screen_pixel_index(&bus, base, row_bytes, hilite_swap_x, hilite_swap_y),
185,
"highlighting the same row twice should restore existing HiliteColor pixels"
);
}
#[test]
fn draw_menu_bar_8bpp_highlight_swaps_background_with_hilite_color() {
let file_id = 625;
let red = (0xFFFF, 0, 0);
let green = (0, 0xFFFF, 0);
let blue = (0, 0, 0xFFFF);
let white = (0xFFFF, 0xFFFF, 0xFFFF);
let (mut disp, mut cpu, mut bus) = setup_with_port();
disp.hilite_color = green;
let (base, row_bytes) = setup_8bpp_menu_screen(&mut disp, &mut bus, 128, 64);
disp.menu_bar_hidden = false;
bus.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 20);
let file = new_menu_with_title(&mut disp, &mut cpu, &mut bus, file_id, 0x303100, "File");
insert_menu(&mut disp, &mut cpu, &mut bus, file);
let entries_ptr = bus.alloc(MC_ENTRY_SIZE as u32);
write_mc_entry_colors(&mut bus, entries_ptr, 0, 0, blue, white, blue, red);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, entries_ptr);
bus.write_word(TEST_SP + 4, 1);
assert!(
disp.dispatch_menu(true, 0x265, &mut cpu, &mut bus)
.unwrap()
.is_ok(),
"SetMCEntries should install menu bar color entry"
);
disp.draw_menu_bar_to_fb(&mut bus);
let regions = disp.menu_title_regions();
let bg_x = regions[0].0 + 1;
let bg_y = 5;
assert_eq!(
screen_pixel_index(&bus, base, row_bytes, bg_x, bg_y),
35,
"precondition: menu title background should be the MenuCInfo red bar"
);
let hilite_swap_x = regions[0].1 - 2;
let hilite_swap_y = 5;
bus.write_byte(
base + (hilite_swap_y as u32) * row_bytes + hilite_swap_x as u32,
185,
);
let blue_title_pixel = (1..19)
.flat_map(|y| (regions[0].0..regions[0].1).map(move |x| (x, y)))
.find(|(x, y)| screen_pixel_index(&bus, base, row_bytes, *x, *y) == 210)
.expect("precondition: menu-bar RGB1 blue title should draw");
disp.highlight_menu_title(&mut bus, 0);
assert_eq!(
screen_pixel_index(&bus, base, row_bytes, bg_x, bg_y),
185,
"8bpp menu-title highlighting should swap the bar background to HiliteColor"
);
assert_ne!(
screen_pixel_index(&bus, base, row_bytes, bg_x, bg_y),
220,
"8bpp menu-title highlighting must not use the indexed complement of red"
);
assert_eq!(
screen_pixel_index(&bus, base, row_bytes, hilite_swap_x, hilite_swap_y),
35,
"8bpp menu-title highlighting should swap existing HiliteColor pixels back to the bar background"
);
assert_eq!(
screen_pixel_index(
&bus,
base,
row_bytes,
blue_title_pixel.0,
blue_title_pixel.1
),
210,
"8bpp menu-title highlighting should leave non-background title colors unchanged"
);
disp.highlight_menu_title(&mut bus, 0);
assert_eq!(
screen_pixel_index(&bus, base, row_bytes, bg_x, bg_y),
35,
"highlighting the same title twice should restore the bar background"
);
assert_eq!(
screen_pixel_index(&bus, base, row_bytes, hilite_swap_x, hilite_swap_y),
185,
"highlighting the same title twice should restore existing HiliteColor pixels"
);
}
#[test]
fn draw_menu_bar_plain_8bpp_highlight_swaps_logical_black_white() {
let (mut disp, mut cpu, mut bus) = setup_with_port();
let (base, row_bytes) = setup_8bpp_menu_screen(&mut disp, &mut bus, 128, 64);
disp.menu_bar_hidden = false;
bus.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 20);
let file = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 626, 0x303200, "File");
insert_menu(&mut disp, &mut cpu, &mut bus, file);
disp.draw_menu_bar_to_fb(&mut bus);
let regions = disp.menu_title_regions();
let classic_left = regions[0].0 - 2;
let right = regions[0].1;
let white =
super::super::TrapDispatcher::fb_pixel_index_for_rgb(&bus, [0xFFFF; 3]).unwrap();
let black = super::super::TrapDispatcher::fb_pixel_index_for_rgb(&bus, [0; 3]).unwrap();
assert_eq!(
screen_pixel_index(&bus, base, row_bytes, classic_left, 1),
white,
"precondition: plain menu-title background should be logical white"
);
let title_pixel = (1..19)
.flat_map(|y| (classic_left..right).map(move |x| (x, y)))
.find(|(x, y)| screen_pixel_index(&bus, base, row_bytes, *x, *y) == black)
.expect("precondition: plain title should draw logical black text");
disp.highlight_menu_title(&mut bus, 0);
assert_eq!(
screen_pixel_index(&bus, base, row_bytes, classic_left, 1),
black,
"plain 8bpp menu-title highlight should invert white background to black"
);
assert_eq!(
screen_pixel_index(&bus, base, row_bytes, title_pixel.0, title_pixel.1),
white,
"plain 8bpp menu-title highlight should invert black title text to white"
);
}
#[test]
fn open_menu_dropdown_uses_system7_attached_pulldown_chrome() {
let (mut disp, mut cpu, mut bus) = setup_with_port();
let (base, row_bytes) = setup_8bpp_menu_screen(&mut disp, &mut bus, 160, 96);
disp.menu_bar_hidden = false;
bus.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 20);
let file = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 627, 0x303300, "File");
append_menu_data(
&mut disp,
&mut cpu,
&mut bus,
file,
0x303340,
"New/N;Open/O;Close/W",
);
disp.menus[0].items[1].mark = 0x12;
disp.menus[0].items[2].style = 0x01;
insert_menu(&mut disp, &mut cpu, &mut bus, file);
disp.draw_menu_bar_to_fb(&mut bus);
let regions = disp.menu_title_regions();
let expected_left = regions[0].0 - 2;
let expected_width = disp.menus[0]
.items
.iter()
.map(|item| {
super::super::TrapDispatcher::fb_measure_string(&item.text, 0, 12)
+ disp.menu_item_width_extra(&bus, item)
+ super::super::TrapDispatcher::menu_item_pulldown_padding(item)
})
.max()
.unwrap()
.max(regions[0].1 - regions[0].0 + 20);
let expected_rect = (
20,
expected_left,
20 + disp.menu_items_height(&bus, &disp.menus[0].items) + 1,
expected_left + expected_width,
);
let white =
super::super::TrapDispatcher::fb_pixel_index_for_rgb(&bus, [0xFFFF; 3]).unwrap();
let black = super::super::TrapDispatcher::fb_pixel_index_for_rgb(&bus, [0; 3]).unwrap();
disp.open_menu_dropdown(&mut bus, 0, TEST_SP);
assert_eq!(
disp.menu_tracking.as_ref().unwrap().dropdown_rect,
expected_rect,
"attached pull-down display rect should use the System 7 MDEF padding, not CalcMenuSize's stored width"
);
assert_eq!(
screen_pixel_index(&bus, base, row_bytes, expected_left + 1, expected_rect.0),
white,
"attached pull-down should not draw a separate top horizontal border"
);
assert_eq!(
screen_pixel_index(&bus, base, row_bytes, expected_left, expected_rect.0),
black,
"attached pull-down should still draw its left vertical border"
);
assert_eq!(
screen_pixel_index(&bus, base, row_bytes, expected_rect.3 - 1, expected_rect.0),
black,
"attached pull-down should still draw its right vertical border"
);
assert_eq!(
screen_pixel_index(&bus, base, row_bytes, expected_left, 1),
black,
"opening a pull-down should highlight the attached menu title"
);
}
#[test]
fn open_menu_dropdown_uses_system7_plain_item_padding() {
let (mut disp, mut cpu, mut bus) = setup_with_port();
let (_base, _row_bytes) = setup_8bpp_menu_screen(&mut disp, &mut bus, 180, 120);
disp.menu_bar_hidden = false;
bus.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 20);
let style = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 628, 0x303500, "Style");
append_menu_data(
&mut disp,
&mut cpu,
&mut bus,
style,
0x303540,
"Plain;Underline;-;Wide Underline",
);
insert_menu(&mut disp, &mut cpu, &mut bus, style);
disp.draw_menu_bar_to_fb(&mut bus);
let regions = disp.menu_title_regions();
let expected_left = regions[0].0 - 2;
let expected_width = disp.menus[0]
.items
.iter()
.map(|item| {
super::super::TrapDispatcher::fb_measure_string(&item.text, 0, 12)
+ disp.menu_item_width_extra(&bus, item)
+ super::super::TrapDispatcher::menu_item_pulldown_padding(item)
})
.max()
.unwrap()
.max(regions[0].1 - regions[0].0 + 20);
let wide_item_width =
super::super::TrapDispatcher::fb_measure_string("Wide Underline", 0, 12) + 26;
disp.open_menu_dropdown(&mut bus, 0, TEST_SP);
assert_eq!(
expected_width, wide_item_width,
"plain no-mark/no-command rows should use the System 7 live pull-down padding"
);
assert_eq!(
disp.menu_tracking.as_ref().unwrap().dropdown_rect,
(
20,
expected_left,
20 + disp.menu_items_height(&bus, &disp.menus[0].items) + 1,
expected_left + expected_width,
),
"live pull-down geometry should match the System 7 plain-row width reference"
);
}
#[test]
fn open_menu_dropdown_uses_system7_icon_column_geometry_without_icon_resources() {
let (mut disp, mut cpu, mut bus) = setup_with_port();
let (_base, _row_bytes) = setup_8bpp_menu_screen(&mut disp, &mut bus, 240, 220);
disp.menu_bar_hidden = false;
bus.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 20);
let menu = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 629, 0x303700, "Icons");
append_menu_data(
&mut disp,
&mut cpu,
&mut bus,
menu,
0x303740,
"Folder View/F;Document/D;Checked Icon/C;Disabled Icon/X;(-;No Icon/N",
);
for (idx, item) in disp.menus[0].items.iter_mut().take(4).enumerate() {
item.icon = (idx + 1) as u8;
}
disp.menus[0].items[2].mark = 0x12;
disp.menus[0].items[3].enabled = false;
insert_menu(&mut disp, &mut cpu, &mut bus, menu);
disp.draw_menu_bar_to_fb(&mut bus);
let regions = disp.menu_title_regions();
let expected_left = regions[0].0 - 2;
let expected_height = 34 + 34 + 34 + 34 + 16 + 16 + 1;
let expected_width = disp.menus[0]
.items
.iter()
.map(|item| {
super::super::TrapDispatcher::fb_measure_string(&item.text, 0, 12)
+ disp.menu_item_width_extra(&bus, item)
+ super::super::TrapDispatcher::menu_item_pulldown_padding(item)
})
.max()
.unwrap()
.max(regions[0].1 - regions[0].0 + 20);
disp.open_menu_dropdown(&mut bus, 0, TEST_SP);
let rect = disp.menu_tracking.as_ref().unwrap().dropdown_rect;
assert_eq!(
disp.menu_items_height(&bus, &disp.menus[0].items),
expected_height - 1,
"icon-column menus should use 34px normal ICON rows and a standard separator row"
);
assert_eq!(
rect,
(
20,
expected_left,
20 + expected_height,
expected_left + expected_width,
),
"normal icon-number rows without resources should still reserve System 7 icon-column pull-down geometry"
);
assert_eq!(
super::super::TrapDispatcher::menu_item_pulldown_padding(&disp.menus[0].items[0]),
6,
"normal icon-column command rows should use the System 7 live pull-down padding"
);
assert_eq!(
disp.dropdown_item_at_point(&bus, rect.1 + 5, rect.0 + 34),
1,
"hit testing should include the full first 34px normal ICON row"
);
assert_eq!(
disp.dropdown_item_at_point(&bus, rect.1 + 5, rect.0 + 128),
0,
"hit testing inside the fourth 34px normal ICON row should respect its disabled state"
);
assert_eq!(
disp.dropdown_item_at_point(&bus, rect.1 + 5, rect.0 + 137),
0,
"hit testing in the separator row should not select an item"
);
assert_eq!(
disp.dropdown_item_at_point(&bus, rect.1 + 5, rect.0 + 152),
0,
"hit testing should leave the full separator row unselectable"
);
assert_eq!(
disp.dropdown_item_at_point(&bus, rect.1 + 5, rect.0 + 153),
6,
"hit testing below the icon-column separator should reach the following item"
);
}
#[test]
fn draw_menu_dropdown_reduced_icon_resource_draws_app_icon_not_theme_placeholder() {
let rect = (20, 20, 38, 140);
let (mut disp, mut cpu, mut bus) = setup_with_port();
let row_bytes = 64;
let base = bus.alloc(row_bytes * 342);
disp.set_ui_theme_id(UiThemeId::SystemlessDefault);
disp.set_screen_mode_for_test(base, row_bytes, 512, 342, 1);
clear_1bpp_screen(&mut bus, base, row_bytes, 342);
let menu = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 612, 0x302500, "File");
append_menu_data(&mut disp, &mut cpu, &mut bus, menu, 0x302540, "Pick");
disp.menus[0].items[0].icon = 7;
disp.menus[0].items[0].key_equiv = MENU_KEY_REDUCED_ICON;
let icon = menu_icon_source_with_left_stripe();
disp.install_test_resource(&mut bus, *b"ICON", 263, &icon);
disp.draw_menu_dropdown(&mut bus, 0, rect);
assert!(
screen_pixel_is_set(&bus, base, row_bytes, 23, 22),
"menu item should draw the app's reduced ICON resource in the icon slot"
);
assert!(
!screen_pixel_is_set(&bus, base, row_bytes, 36, 28),
"an explicit ICON resource should suppress systemless icon placeholder chrome"
);
assert!(
!screen_pixel_is_set(&bus, base, row_bytes, 124, 28),
"$1D reduced-icon selector should not be rendered as command-key chrome"
);
}
#[test]
fn sicn_menu_icon_resource_draws_small_icon_without_row_growth_or_theme_placeholder() {
let (mut disp, mut cpu, mut bus) = setup_with_port();
let row_bytes = 64;
let base = bus.alloc(row_bytes * 342);
disp.set_ui_theme_id(UiThemeId::SystemlessDefault);
disp.set_screen_mode_for_test(base, row_bytes, 512, 342, 1);
clear_1bpp_screen(&mut bus, base, row_bytes, 342);
let menu = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 615, 0x302800, "File");
append_menu_data(&mut disp, &mut cpu, &mut bus, menu, 0x302840, "Pick;Next");
disp.menus[0].items[0].icon = 7;
disp.menus[0].items[0].key_equiv = MENU_KEY_SMALL_ICON;
let sicn = sicn_source_with_left_stripe();
disp.install_test_resource(&mut bus, *b"SICN", 263, &sicn);
insert_menu(&mut disp, &mut cpu, &mut bus, menu);
disp.open_menu_dropdown(&mut bus, 0, TEST_SP);
let rect = disp.menu_tracking.as_ref().unwrap().dropdown_rect;
assert_eq!(
rect.2 - rect.0,
16 + 16 + 1,
"SICN menu items should keep standard 16px attached pull-down geometry"
);
assert!(
screen_pixel_is_set(&bus, base, row_bytes, rect.1 + 4, rect.0 + 2),
"SICN resource should draw the app's small icon in the icon slot"
);
assert!(
!screen_pixel_is_set(&bus, base, row_bytes, rect.1 + 16, rect.0 + 8),
"an explicit SICN resource should suppress systemless icon placeholder chrome"
);
assert!(
!screen_pixel_is_set(&bus, base, row_bytes, rect.1 + 8, rect.2 - 8),
"$1E SICN selector should not be rendered as command-key chrome"
);
}
#[test]
fn cicn_menu_icon_resource_precedes_sicn_and_suppresses_theme_placeholder() {
let (mut disp, mut cpu, mut bus) = setup_with_port();
let row_bytes = 64;
let base = bus.alloc(row_bytes * 342);
disp.set_ui_theme_id(UiThemeId::SystemlessDefault);
disp.set_screen_mode_for_test(base, row_bytes, 512, 342, 1);
clear_1bpp_screen(&mut bus, base, row_bytes, 342);
let menu = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 616, 0x302900, "File");
append_menu_data(&mut disp, &mut cpu, &mut bus, menu, 0x302940, "Pick;Next");
disp.menus[0].items[0].icon = 7;
disp.menus[0].items[0].key_equiv = MENU_KEY_SMALL_ICON;
let cicn = cicn_source_with_left_stripe(24, 20);
let sicn = sicn_source_with_right_stripe();
disp.install_test_resource(&mut bus, *b"cicn", 263, &cicn);
disp.install_test_resource(&mut bus, *b"SICN", 263, &sicn);
insert_menu(&mut disp, &mut cpu, &mut bus, menu);
disp.open_menu_dropdown(&mut bus, 0, TEST_SP);
let rect = disp.menu_tracking.as_ref().unwrap().dropdown_rect;
assert_eq!(
rect.2 - rect.0,
20 + 16 + 1,
"cicn menu rows should grow to the cicn resource rectangle height in an attached pull-down"
);
assert!(
screen_pixel_is_set(&bus, base, row_bytes, rect.1 + 4, rect.0 + 2),
"cicn resource should draw before the SICN fallback"
);
assert!(
!screen_pixel_is_set(&bus, base, row_bytes, rect.1 + 17, rect.0 + 2),
"SICN fallback pixels must not draw when a cicn resource exists"
);
assert!(
!screen_pixel_is_set(&bus, base, row_bytes, rect.1 + 16, rect.0 + 8),
"an explicit cicn resource should suppress systemless icon placeholder chrome"
);
assert_eq!(
disp.dropdown_item_at_point(&bus, rect.1 + 5, rect.0 + 20),
1,
"hit testing should include the full cicn-height first row"
);
assert_eq!(
disp.dropdown_item_at_point(&bus, rect.1 + 5, rect.0 + 22),
2,
"hit testing below the cicn-height row should reach the next item"
);
}
#[test]
fn calcmenusize_uses_cicn_geometry_instead_of_icon_fallback_geometry() {
let (mut disp, mut cpu, mut bus) = setup();
let menu = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 617, 0x302A00, "File");
append_menu_data(&mut disp, &mut cpu, &mut bus, menu, 0x302A40, "Pick;Next");
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 7);
bus.write_word(TEST_SP + 2, 1);
bus.write_long(TEST_SP + 4, menu);
assert!(
disp.dispatch_menu(true, 0x140, &mut cpu, &mut bus)
.unwrap()
.is_ok(),
"SetItemIcon should install a normal icon menu item"
);
let cicn = cicn_source_with_left_stripe(24, 20);
let icon = menu_icon_source_with_left_stripe();
disp.install_test_resource(&mut bus, *b"cicn", 263, &cicn);
disp.install_test_resource(&mut bus, *b"ICON", 263, &icon);
let menu_ptr = bus.read_long(menu);
bus.write_word(menu_ptr + 4, 0);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, menu);
assert!(
disp.dispatch_menu(true, 0x148, &mut cpu, &mut bus)
.unwrap()
.is_ok(),
"CalcMenuSize should succeed"
);
assert_eq!(
bus.read_word(menu_ptr + 4) as i16,
20 + 16 + 2,
"CalcMenuSize should use cicn resource height instead of normal ICON's 32px row"
);
}
#[test]
fn normal_icon_resource_expands_dropdown_geometry_and_hit_testing() {
let (mut disp, mut cpu, mut bus) = setup_with_port();
let row_bytes = 64;
let base = bus.alloc(row_bytes * 342);
disp.set_screen_mode_for_test(base, row_bytes, 512, 342, 1);
clear_1bpp_screen(&mut bus, base, row_bytes, 342);
let menu = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 613, 0x302600, "File");
append_menu_data(&mut disp, &mut cpu, &mut bus, menu, 0x302640, "Pick;Next");
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 7);
bus.write_word(TEST_SP + 2, 1);
bus.write_long(TEST_SP + 4, menu);
assert!(
disp.dispatch_menu(true, 0x140, &mut cpu, &mut bus)
.unwrap()
.is_ok(),
"SetItemIcon should install a normal ICON menu item"
);
let icon = menu_icon_source_with_left_stripe();
disp.install_test_resource(&mut bus, *b"ICON", 263, &icon);
insert_menu(&mut disp, &mut cpu, &mut bus, menu);
disp.open_menu_dropdown(&mut bus, 0, TEST_SP);
let rect = disp.menu_tracking.as_ref().unwrap().dropdown_rect;
assert_eq!(
rect.2 - rect.0,
34 + 16 + 1,
"normal ICON row should enlarge the live attached pull-down height around its 32px icon slot"
);
assert!(
screen_pixel_is_set(&bus, base, row_bytes, rect.1 + 4, rect.0 + 1),
"normal ICON resource should draw at full 32x32 size in the first row"
);
assert_eq!(
disp.dropdown_item_at_point(&bus, rect.1 + 5, rect.0 + 34),
1,
"hit testing near the bottom of the 34px normal ICON row should still hit item 1"
);
assert_eq!(
disp.dropdown_item_at_point(&bus, rect.1 + 5, rect.0 + 36),
2,
"hit testing below the normal ICON row should hit the following item"
);
}
#[test]
fn calcmenusize_accounts_for_normal_icon_row_height() {
let (mut disp, mut cpu, mut bus) = setup();
let menu = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 614, 0x302700, "File");
append_menu_data(&mut disp, &mut cpu, &mut bus, menu, 0x302740, "Pick;Next");
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 7);
bus.write_word(TEST_SP + 2, 1);
bus.write_long(TEST_SP + 4, menu);
assert!(
disp.dispatch_menu(true, 0x140, &mut cpu, &mut bus)
.unwrap()
.is_ok(),
"SetItemIcon should install a normal ICON menu item"
);
let menu_ptr = bus.read_long(menu);
bus.write_word(menu_ptr + 4, 0);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, menu);
assert!(
disp.dispatch_menu(true, 0x148, &mut cpu, &mut bus)
.unwrap()
.is_ok(),
"CalcMenuSize should succeed"
);
assert_eq!(
bus.read_word(menu_ptr + 4) as i16,
34 + 16 + 2,
"CalcMenuSize should write summed row heights including normal ICON rows"
);
}
#[test]
fn popupmenuselect_nohit_path_preserves_stack_and_returns_zero() {
let (mut disp, mut cpu, mut bus) = setup_with_port();
disp.menu_bar_hidden = false;
disp.mouse_button = false;
bus.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 20);
let menu = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 128, 0x302000, "Pop");
let sp = TEST_SP;
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, 0); bus.write_word(sp + 2, 0xFC18); bus.write_word(sp + 4, 0xFC18); bus.write_long(sp + 6, menu);
bus.write_long(sp + 10, 0xDEAD_BEEF);
let result = disp.dispatch_menu(true, 0x00B, &mut cpu, &mut bus);
assert!(result.is_some(), "PopUpMenuSelect should be handled");
assert!(result.unwrap().is_ok(), "PopUpMenuSelect should return");
assert_eq!(
cpu.read_reg(Register::A7),
sp + 10,
"PopUpMenuSelect should consume the 10-byte Pascal argument frame"
);
assert_eq!(
bus.read_long(sp + 10),
0,
"PopUpMenuSelect no-hit path should return 0"
);
assert!(
disp.menu_tracking.is_none(),
"uninserted pop-up menus should not seed tracking"
);
}
#[test]
fn deletemenu_removes_only_target_menu_id_from_current_list() {
let (mut disp, mut cpu, mut bus) = setup();
let left_id = 240i16;
let right_id = 241i16;
let left = new_menu_with_title(&mut disp, &mut cpu, &mut bus, left_id, 0x30B300, "Left");
let right = new_menu_with_title(&mut disp, &mut cpu, &mut bus, right_id, 0x30B400, "Right");
insert_menu(&mut disp, &mut cpu, &mut bus, left);
insert_menu(&mut disp, &mut cpu, &mut bus, right);
assert_eq!(
get_mhandle_for_id(&mut disp, &mut cpu, &mut bus, left_id),
left
);
assert_eq!(
get_mhandle_for_id(&mut disp, &mut cpu, &mut bus, right_id),
right
);
delete_menu_by_id(&mut disp, &mut cpu, &mut bus, left_id);
assert_eq!(
get_mhandle_for_id(&mut disp, &mut cpu, &mut bus, left_id),
0,
"Deleted menu ID should no longer be in the current menu list"
);
assert_eq!(
get_mhandle_for_id(&mut disp, &mut cpu, &mut bus, right_id),
right,
"Deleting one menu ID should not remove other inserted menus"
);
}
#[test]
fn deletemenu_removes_all_menu_color_entries_for_menu_id() {
let (mut disp, mut cpu, mut bus) = setup();
let target_id = 511i16;
let other_id = 512i16;
let target =
new_menu_with_title(&mut disp, &mut cpu, &mut bus, target_id, 0x30B4C0, "Target");
let other = new_menu_with_title(&mut disp, &mut cpu, &mut bus, other_id, 0x30B500, "Other");
insert_menu(&mut disp, &mut cpu, &mut bus, target);
insert_menu(&mut disp, &mut cpu, &mut bus, other);
set_mc_entries_for_test(
&mut disp,
&mut cpu,
&mut bus,
&[
(target_id, 0, 0x2100),
(target_id, 1, 0x2200),
(target_id, 2, 0x2300),
(other_id, 1, 0x2400),
(0, 0, 0x2500),
],
);
delete_menu_by_id(&mut disp, &mut cpu, &mut bus, target_id);
assert_eq!(
get_mc_entry_ptr_for_test(&mut disp, &mut cpu, &mut bus, target_id, 0),
0,
"DeleteMenu should remove the deleted menu title entry"
);
assert_eq!(
get_mc_entry_ptr_for_test(&mut disp, &mut cpu, &mut bus, target_id, 1),
0,
"DeleteMenu should remove item entries for the deleted menu"
);
assert_eq!(
get_mc_entry_ptr_for_test(&mut disp, &mut cpu, &mut bus, target_id, 2),
0,
"DeleteMenu should remove every entry for the deleted menu ID"
);
assert_ne!(
get_mc_entry_ptr_for_test(&mut disp, &mut cpu, &mut bus, other_id, 1),
0,
"DeleteMenu should preserve other menus' MenuCInfo entries"
);
assert_ne!(
get_mc_entry_ptr_for_test(&mut disp, &mut cpu, &mut bus, 0, 0),
0,
"DeleteMenu should preserve the default MenuCInfo entry"
);
}
#[test]
fn deletemenu_does_not_dispose_menu_record_memory() {
let (mut disp, mut cpu, mut bus) = setup();
let menu_id = 242i16;
let title = "Temp";
let handle = new_menu_with_title(&mut disp, &mut cpu, &mut bus, menu_id, 0x30B500, title);
let menu_ptr = bus.read_long(handle);
insert_menu(&mut disp, &mut cpu, &mut bus, handle);
delete_menu_by_id(&mut disp, &mut cpu, &mut bus, menu_id);
assert_eq!(
get_mhandle_for_id(&mut disp, &mut cpu, &mut bus, menu_id),
0
);
assert_eq!(
bus.read_long(handle),
menu_ptr,
"DeleteMenu should not clear or retarget the caller-held MenuHandle"
);
assert_eq!(
bus.read_word(menu_ptr) as i16,
menu_id,
"MenuInfo memory should still be readable after DeleteMenu"
);
assert_eq!(bus.read_byte(menu_ptr + 14), title.len() as u8);
}
#[test]
fn disposemenu_releases_newmenu_menuhandle_and_record_allocations() {
let (mut disp, mut cpu, mut bus) = setup();
let menu_id = 244i16;
let handle = new_menu_with_title(&mut disp, &mut cpu, &mut bus, menu_id, 0x30B800, "Temp");
let menu_ptr = bus.read_long(handle);
insert_menu(&mut disp, &mut cpu, &mut bus, handle);
assert_eq!(
get_mhandle_for_id(&mut disp, &mut cpu, &mut bus, menu_id),
handle,
"precondition: inserted menu should be discoverable by GetMHandle"
);
assert!(
bus.get_alloc_size(handle).is_some(),
"precondition: NewMenu should allocate a MenuHandle master pointer"
);
assert!(
bus.get_alloc_size(menu_ptr).is_some(),
"precondition: NewMenu should allocate a menu record block"
);
dispose_menu_by_handle(&mut disp, &mut cpu, &mut bus, handle);
assert_eq!(
bus.get_alloc_size(handle),
None,
"DisposeMenu should free the MenuHandle master-pointer allocation"
);
assert_eq!(
bus.get_alloc_size(menu_ptr),
None,
"DisposeMenu should free the menu record allocation"
);
assert_eq!(
get_mhandle_for_id(&mut disp, &mut cpu, &mut bus, menu_id),
0,
"Disposed menu should no longer be returned by GetMHandle"
);
}
#[test]
fn disposemenu_consumes_menuhandle_argument() {
let (mut disp, mut cpu, mut bus) = setup();
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, 0);
let result = disp.dispatch_menu(true, 0x132, &mut cpu, &mut bus);
assert!(result.is_some(), "DisposeMenu should be handled");
assert!(result.unwrap().is_ok(), "DisposeMenu should succeed");
assert_eq!(
cpu.read_reg(Register::A7),
TEST_SP + 4,
"DisposeMenu should pop one MenuHandle argument (4 bytes)"
);
}
#[test]
fn hilitemenu_pops_menuid_word_and_clears_active_tracking_state() {
let (mut disp, mut cpu, mut bus) = setup_with_port();
disp.menu_bar_hidden = false;
bus.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 20);
let menu_id = 243i16;
let handle = new_menu_with_title(&mut disp, &mut cpu, &mut bus, menu_id, 0x30B600, "File");
append_menu_data(
&mut disp,
&mut cpu,
&mut bus,
handle,
0x30B700,
"Open/O;Close/W",
);
insert_menu(&mut disp, &mut cpu, &mut bus, handle);
disp.open_menu_dropdown(&mut bus, 0, TEST_SP);
assert!(
disp.menu_tracking.is_some(),
"precondition: dropdown tracking should be active"
);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 0);
let result = disp.dispatch_menu(true, 0x138, &mut cpu, &mut bus);
assert!(result.is_some(), "HiliteMenu should be handled");
assert!(result.unwrap().is_ok(), "HiliteMenu should succeed");
assert_eq!(
cpu.read_reg(Register::A7),
TEST_SP + 2,
"HiliteMenu should pop one menuID word"
);
assert!(
disp.menu_tracking.is_none(),
"HiliteMenu(0) should clear active menu tracking/highlight state"
);
}
#[test]
fn hilitemenu_five_call_composition_advances_stack_by_ten_bytes() {
let (mut disp, mut cpu, mut bus) = setup();
let sp_before = cpu.read_reg(Register::A7);
for i in 0..5u32 {
bus.write_word(sp_before.wrapping_sub((i + 1) * 2), 0);
}
cpu.write_reg(Register::A7, sp_before.wrapping_sub(10));
for _ in 0..5 {
let result = disp.dispatch_menu(true, 0x138, &mut cpu, &mut bus);
assert!(result.is_some(), "HiliteMenu should be handled");
assert!(result.unwrap().is_ok(), "HiliteMenu should succeed");
}
assert_eq!(
cpu.read_reg(Register::A7),
sp_before,
"5 HiliteMenu(0) calls should pop 5×2 = 10 bytes cumulatively"
);
}
#[test]
fn flashmenubar_five_call_composition_advances_stack_by_ten_bytes() {
let (mut disp, mut cpu, mut bus) = setup();
let sp_before = cpu.read_reg(Register::A7);
for i in 0..5u32 {
bus.write_word(sp_before.wrapping_sub((i + 1) * 2), 0);
}
cpu.write_reg(Register::A7, sp_before.wrapping_sub(10));
for _ in 0..5 {
let result = disp.dispatch_menu(true, 0x14C, &mut cpu, &mut bus);
assert!(result.is_some(), "FlashMenuBar should be handled");
assert!(result.unwrap().is_ok(), "FlashMenuBar should succeed");
}
assert_eq!(
cpu.read_reg(Register::A7),
sp_before,
"5 FlashMenuBar(0) calls should pop 5×2 = 10 bytes cumulatively"
);
}
#[test]
fn flashmenubar_zero_inverts_and_restores_top_menu_bar_strip() {
let (mut disp, mut cpu, mut bus) = setup_with_port();
let row_bytes = 64;
let base = bus.alloc(row_bytes * 342);
disp.set_screen_mode_for_test(base, row_bytes, 512, 342, 1);
disp.menu_bar_hidden = false;
clear_1bpp_screen(&mut bus, base, row_bytes, 342);
bus.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 20);
let file = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 701, 0x306000, "File");
insert_menu(&mut disp, &mut cpu, &mut bus, file);
cpu.write_reg(Register::A7, TEST_SP);
disp.dispatch_menu(true, 0x137, &mut cpu, &mut bus)
.unwrap()
.unwrap();
let regions = disp.menu_title_regions();
let title_pixel = (1..19)
.flat_map(|y| (regions[0].0..regions[0].1).map(move |x| (x, y)))
.find(|(x, y)| screen_pixel_is_set(&bus, base, row_bytes, *x, *y))
.expect("precondition: menu title should draw before flashing");
assert!(
!screen_pixel_is_set(&bus, base, row_bytes, 200, 5),
"precondition: menu-bar background should start white"
);
assert!(
screen_pixel_is_set(&bus, base, row_bytes, 0, 0),
"precondition: DrawMenuBar should stamp the top corner mask"
);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 0);
disp.dispatch_menu(true, 0x14C, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert!(
screen_pixel_is_set(&bus, base, row_bytes, 200, 5),
"FlashMenuBar(0) should invert the menu-bar background"
);
assert!(
!screen_pixel_is_set(&bus, base, row_bytes, title_pixel.0, title_pixel.1),
"FlashMenuBar(0) should invert title glyph pixels too"
);
assert!(
screen_pixel_is_set(&bus, base, row_bytes, 0, 0),
"FlashMenuBar(0) should preserve the top corner mask"
);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 0);
disp.dispatch_menu(true, 0x14C, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert!(
!screen_pixel_is_set(&bus, base, row_bytes, 200, 5),
"a second FlashMenuBar(0) call should restore the background"
);
assert!(
screen_pixel_is_set(&bus, base, row_bytes, title_pixel.0, title_pixel.1),
"a second FlashMenuBar(0) call should restore title glyph pixels"
);
assert!(
screen_pixel_is_set(&bus, base, row_bytes, 0, 0),
"a second FlashMenuBar(0) call should keep the top corner mask"
);
}
#[test]
fn menuchoice_pascal_function_preserves_stack_across_five_calls() {
let (mut disp, mut cpu, mut bus) = setup();
let sp_before = cpu.read_reg(Register::A7);
for _ in 0..5 {
let slot = cpu.read_reg(Register::A7).wrapping_sub(4);
bus.write_long(slot, 0xDEADBEEF);
cpu.write_reg(Register::A7, slot);
let result = disp.dispatch_menu(true, 0x266, &mut cpu, &mut bus);
assert!(result.is_some(), "MenuChoice should be handled");
assert!(result.unwrap().is_ok(), "MenuChoice should succeed");
assert_eq!(
cpu.read_reg(Register::A7),
slot,
"MenuChoice must leave A7 unchanged across the trap dispatch"
);
let _result_val = bus.read_long(slot);
cpu.write_reg(Register::A7, cpu.read_reg(Register::A7).wrapping_add(4));
}
assert_eq!(
cpu.read_reg(Register::A7),
sp_before,
"5 MenuChoice() FUNCTION call sequences must net-balance A7"
);
}
#[test]
fn menuchoice_reads_menu_disable_lowmem_value() {
let (mut disp, mut cpu, mut bus) = setup();
let sp_before = cpu.read_reg(Register::A7);
let expected = 0x11223344u32;
let slot = sp_before.wrapping_sub(4);
bus.write_long(crate::memory::globals::addr::MENU_DISABLE, expected);
bus.write_long(slot, 0xDEADBEEF);
cpu.write_reg(Register::A7, slot);
let result = disp.dispatch_menu(true, 0x266, &mut cpu, &mut bus);
assert!(result.is_some(), "MenuChoice should be handled");
assert!(result.unwrap().is_ok(), "MenuChoice should succeed");
assert_eq!(
cpu.read_reg(Register::A7),
slot,
"MenuChoice must leave A7 unchanged across the trap dispatch"
);
assert_eq!(
bus.read_long(slot),
expected,
"MenuChoice should return the seeded MenuDisable value"
);
cpu.write_reg(Register::A7, slot.wrapping_add(4));
assert_eq!(
cpu.read_reg(Register::A7),
sp_before,
"MenuChoice should net-balance the Pascal FUNCTION call sequence"
);
}
#[test]
fn menu_color_family_five_call_compositions_preserve_stack_pointer() {
let (mut disp, mut cpu, mut bus) = setup();
let sp0 = cpu.read_reg(Register::A7);
for i in 0..5u32 {
bus.write_long(sp0.wrapping_sub((i + 1) * 4), 0);
}
cpu.write_reg(Register::A7, sp0.wrapping_sub(20));
for _ in 0..5 {
disp.dispatch_menu(true, 0x260, &mut cpu, &mut bus)
.expect("DelMCEntries handled")
.expect("DelMCEntries Ok");
}
assert_eq!(
cpu.read_reg(Register::A7),
sp0,
"5 DelMCEntries calls should pop 5×4 = 20 bytes cumulatively"
);
let sp1 = cpu.read_reg(Register::A7);
for _ in 0..5 {
let slot = cpu.read_reg(Register::A7).wrapping_sub(4);
bus.write_long(slot, 0xDEADBEEF);
cpu.write_reg(Register::A7, slot);
disp.dispatch_menu(true, 0x261, &mut cpu, &mut bus)
.expect("GetMCInfo handled")
.expect("GetMCInfo Ok");
assert_eq!(
cpu.read_reg(Register::A7),
slot,
"GetMCInfo must leave A7 unchanged across the trap dispatch"
);
cpu.write_reg(Register::A7, cpu.read_reg(Register::A7).wrapping_add(4));
}
assert_eq!(
cpu.read_reg(Register::A7),
sp1,
"5 GetMCInfo() FUNCTION call sequences must net-balance A7"
);
let sp2 = cpu.read_reg(Register::A7);
for i in 0..5u32 {
bus.write_long(sp2.wrapping_sub((i + 1) * 4), 0);
}
cpu.write_reg(Register::A7, sp2.wrapping_sub(20));
for _ in 0..5 {
disp.dispatch_menu(true, 0x262, &mut cpu, &mut bus)
.expect("SetMCInfo handled")
.expect("SetMCInfo Ok");
}
assert_eq!(
cpu.read_reg(Register::A7),
sp2,
"5 SetMCInfo calls should pop 5×4 = 20 bytes cumulatively"
);
let sp3 = cpu.read_reg(Register::A7);
for i in 0..5u32 {
bus.write_long(sp3.wrapping_sub((i + 1) * 4), 0);
}
cpu.write_reg(Register::A7, sp3.wrapping_sub(20));
for _ in 0..5 {
disp.dispatch_menu(true, 0x263, &mut cpu, &mut bus)
.expect("DispMCInfo handled")
.expect("DispMCInfo Ok");
}
assert_eq!(
cpu.read_reg(Register::A7),
sp3,
"5 DispMCInfo calls should pop 5×4 = 20 bytes cumulatively"
);
let sp4 = cpu.read_reg(Register::A7);
for _ in 0..5 {
let cur = cpu.read_reg(Register::A7);
bus.write_long(cur.wrapping_sub(4), 0xDEADBEEF);
bus.write_long(cur.wrapping_sub(8), 0);
cpu.write_reg(Register::A7, cur.wrapping_sub(8));
disp.dispatch_menu(true, 0x264, &mut cpu, &mut bus)
.expect("GetMCEntry handled")
.expect("GetMCEntry Ok");
assert_eq!(
cpu.read_reg(Register::A7),
cur.wrapping_sub(4),
"GetMCEntry must pop 4 bytes of args, leaving slot at SP+0"
);
cpu.write_reg(Register::A7, cpu.read_reg(Register::A7).wrapping_add(4));
}
assert_eq!(
cpu.read_reg(Register::A7),
sp4,
"5 GetMCEntry() FUNCTION call sequences must net-balance A7"
);
let sp5 = cpu.read_reg(Register::A7);
for i in 0..5u32 {
bus.write_long(sp5.wrapping_sub((i + 1) * 6), 0);
bus.write_word(sp5.wrapping_sub((i + 1) * 6).wrapping_add(4), 0);
}
cpu.write_reg(Register::A7, sp5.wrapping_sub(30));
for _ in 0..5 {
disp.dispatch_menu(true, 0x265, &mut cpu, &mut bus)
.expect("SetMCEntries handled")
.expect("SetMCEntries Ok");
}
assert_eq!(
cpu.read_reg(Register::A7),
sp5,
"5 SetMCEntries calls should pop 5×6 = 30 bytes cumulatively"
);
}
#[test]
fn clearmenubar_empties_current_menu_list() {
let (mut disp, mut cpu, mut bus) = setup();
let file = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 501, 0x30B640, "File");
let edit = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 502, 0x30B680, "Edit");
insert_menu(&mut disp, &mut cpu, &mut bus, file);
insert_menu(&mut disp, &mut cpu, &mut bus, edit);
assert_ne!(get_mhandle_for_id(&mut disp, &mut cpu, &mut bus, 501), 0);
assert_ne!(get_mhandle_for_id(&mut disp, &mut cpu, &mut bus, 502), 0);
let result = disp.dispatch_menu(true, 0x134, &mut cpu, &mut bus);
assert!(result.is_some(), "ClearMenuBar should be handled");
assert!(result.unwrap().is_ok(), "ClearMenuBar should succeed");
assert_eq!(
get_mhandle_for_id(&mut disp, &mut cpu, &mut bus, 501),
0,
"ClearMenuBar should remove previously inserted menu ID 501"
);
assert_eq!(
get_mhandle_for_id(&mut disp, &mut cpu, &mut bus, 502),
0,
"ClearMenuBar should remove previously inserted menu ID 502"
);
}
#[test]
fn clearmenubar_clears_menu_color_information_table() {
let (mut disp, mut cpu, mut bus) = setup();
let file_id = 501i16;
let edit_id = 502i16;
let file = new_menu_with_title(&mut disp, &mut cpu, &mut bus, file_id, 0x30B6C0, "File");
let edit = new_menu_with_title(&mut disp, &mut cpu, &mut bus, edit_id, 0x30B700, "Edit");
insert_menu(&mut disp, &mut cpu, &mut bus, file);
insert_menu(&mut disp, &mut cpu, &mut bus, edit);
set_mc_entries_for_test(
&mut disp,
&mut cpu,
&mut bus,
&[
(0, 0, 0x3100),
(file_id, 0, 0x3200),
(file_id, 1, 0x3300),
(edit_id, 1, 0x3400),
],
);
assert_ne!(
get_mc_entry_ptr_for_test(&mut disp, &mut cpu, &mut bus, file_id, 1),
0,
"precondition: file item MenuCInfo entry should exist"
);
assert_ne!(
get_mc_entry_ptr_for_test(&mut disp, &mut cpu, &mut bus, 0, 0),
0,
"precondition: default MenuCInfo entry should exist"
);
let result = disp.dispatch_menu(true, 0x134, &mut cpu, &mut bus);
assert!(result.is_some(), "ClearMenuBar should be handled");
assert!(result.unwrap().is_ok(), "ClearMenuBar should succeed");
assert_eq!(
get_mhandle_for_id(&mut disp, &mut cpu, &mut bus, file_id),
0,
"ClearMenuBar should remove the File menu from the current list"
);
assert_eq!(
get_mhandle_for_id(&mut disp, &mut cpu, &mut bus, edit_id),
0,
"ClearMenuBar should remove the Edit menu from the current list"
);
assert_eq!(
get_mc_entry_ptr_for_test(&mut disp, &mut cpu, &mut bus, file_id, 0),
0,
"ClearMenuBar should clear menu title MenuCInfo entries"
);
assert_eq!(
get_mc_entry_ptr_for_test(&mut disp, &mut cpu, &mut bus, file_id, 1),
0,
"ClearMenuBar should clear menu item MenuCInfo entries"
);
assert_eq!(
get_mc_entry_ptr_for_test(&mut disp, &mut cpu, &mut bus, edit_id, 1),
0,
"ClearMenuBar should clear every menu's MenuCInfo entries"
);
assert_eq!(
get_mc_entry_ptr_for_test(&mut disp, &mut cpu, &mut bus, 0, 0),
0,
"ClearMenuBar should clear the default MenuCInfo entry too"
);
}
#[test]
fn clearmenubar_has_no_parameters_and_preserves_stack_pointer() {
let (mut disp, mut cpu, mut bus) = setup();
let sp_before = cpu.read_reg(Register::A7);
let result = disp.dispatch_menu(true, 0x134, &mut cpu, &mut bus);
assert!(result.is_some(), "ClearMenuBar should be handled");
assert!(result.unwrap().is_ok(), "ClearMenuBar should succeed");
assert_eq!(
cpu.read_reg(Register::A7),
sp_before,
"ClearMenuBar should not pop stack arguments"
);
}
#[test]
fn test_get_mhandle() {
let (mut disp, mut cpu, mut bus) = setup();
bus.write_word(TEST_SP, 1); let result = disp.dispatch_menu(true, 0x149, &mut cpu, &mut bus);
assert!(result.is_some(), "GetMHandle should be handled");
assert!(result.unwrap().is_ok(), "GetMHandle should succeed");
let sp = cpu.read_reg(Register::A7);
assert_eq!(sp, TEST_SP + 2, "GetMHandle should pop 2 bytes from stack");
let handle = bus.read_long(sp);
assert_eq!(handle, 0, "GetMHandle should return 0 handle when no menus");
}
#[test]
fn getmenubar_returns_non_nil_counted_menu_list_and_preserves_stack_pointer() {
let (mut disp, mut cpu, mut bus) = setup();
let file = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 510, 0x30B740, "File");
let edit = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 511, 0x30B780, "Edit");
insert_menu(&mut disp, &mut cpu, &mut bus, file);
insert_menu(&mut disp, &mut cpu, &mut bus, edit);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, 0xDEAD_BEEF);
let result = disp.dispatch_menu(true, 0x13B, &mut cpu, &mut bus);
assert!(result.is_some(), "GetMenuBar should be handled");
assert!(result.unwrap().is_ok(), "GetMenuBar should succeed");
assert_eq!(
cpu.read_reg(Register::A7),
TEST_SP,
"GetMenuBar should preserve A7 (no parameters)"
);
let list_handle = bus.read_long(TEST_SP);
assert_ne!(list_handle, 0, "GetMenuBar should return a non-NIL handle");
let list_ptr = bus.read_long(list_handle);
assert_ne!(list_ptr, 0, "returned menu-list handle should dereference");
assert_eq!(
bus.read_word(list_ptr),
2,
"menu-list block should start with the current menu count"
);
assert_eq!(
bus.read_long(list_ptr + 2),
file,
"first menu handle in snapshot should match current list order"
);
assert_eq!(
bus.read_long(list_ptr + 6),
edit,
"second menu handle in snapshot should match current list order"
);
}
#[test]
fn setmenubar_consumes_menulist_handle_argument() {
let (mut disp, mut cpu, mut bus) = setup();
bus.write_long(TEST_SP, 0); let result = disp.dispatch_menu(true, 0x13C, &mut cpu, &mut bus);
assert!(result.is_some(), "SetMenuBar should be handled");
assert!(result.unwrap().is_ok(), "SetMenuBar should succeed");
let sp = cpu.read_reg(Register::A7);
assert_eq!(sp, TEST_SP + 4, "SetMenuBar should pop 4 bytes from stack");
}
#[test]
fn setmenubar_restores_current_menu_list_from_getmenubar_snapshot() {
let (mut disp, mut cpu, mut bus) = setup();
let file = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 128, 0x30B700, "File");
let edit = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 129, 0x30B710, "Edit");
insert_menu(&mut disp, &mut cpu, &mut bus, file);
insert_menu(&mut disp, &mut cpu, &mut bus, edit);
cpu.write_reg(Register::A7, TEST_SP);
assert!(
disp.dispatch_menu(true, 0x13B, &mut cpu, &mut bus)
.unwrap()
.is_ok(),
"GetMenuBar should succeed"
);
let saved_menu_list = bus.read_long(TEST_SP);
assert_ne!(
saved_menu_list, 0,
"GetMenuBar should return non-NIL handle"
);
assert!(
disp.dispatch_menu(true, 0x134, &mut cpu, &mut bus)
.unwrap()
.is_ok(),
"ClearMenuBar should succeed"
);
let tools = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 130, 0x30B720, "Tools");
insert_menu(&mut disp, &mut cpu, &mut bus, tools);
assert_eq!(get_mhandle_for_id(&mut disp, &mut cpu, &mut bus, 128), 0);
assert_eq!(get_mhandle_for_id(&mut disp, &mut cpu, &mut bus, 129), 0);
assert_ne!(get_mhandle_for_id(&mut disp, &mut cpu, &mut bus, 130), 0);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, saved_menu_list);
assert!(
disp.dispatch_menu(true, 0x13C, &mut cpu, &mut bus)
.unwrap()
.is_ok(),
"SetMenuBar should succeed"
);
assert_ne!(
get_mhandle_for_id(&mut disp, &mut cpu, &mut bus, 128),
0,
"SetMenuBar should restore menu ID 128 from saved list"
);
assert_ne!(
get_mhandle_for_id(&mut disp, &mut cpu, &mut bus, 129),
0,
"SetMenuBar should restore menu ID 129 from saved list"
);
assert_eq!(
get_mhandle_for_id(&mut disp, &mut cpu, &mut bus, 130),
0,
"SetMenuBar should replace current list with the saved snapshot"
);
}
#[test]
fn menuselect_no_menu_hit_returns_zero_and_pops_startpt() {
let (mut disp, mut cpu, mut bus) = setup();
disp.enable_input_trace_capture();
disp.mouse_pos = (40, 120);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 40);
bus.write_word(TEST_SP + 2, 120);
bus.write_long(TEST_SP + 4, 0xDEAD_BEEF);
let result = disp.dispatch_menu(true, 0x13D, &mut cpu, &mut bus);
assert!(result.is_some(), "MenuSelect should be handled");
assert!(result.unwrap().is_ok(), "MenuSelect should succeed");
assert_eq!(
cpu.read_reg(Register::A7),
TEST_SP + 4,
"MenuSelect should pop the Point argument on no-hit return"
);
assert_eq!(
bus.read_long(TEST_SP + 4),
0,
"MenuSelect should return 0 when no menu item is selected"
);
assert!(
disp.menu_tracking.is_none(),
"MenuSelect should not leave tracking state active on no-hit return"
);
let trace = disp.input_trace_text();
assert!(trace.contains("A93D action=start start=(40,120)"));
assert!(trace.contains("result=$00000000 outcome=no_menu_title"));
}
#[test]
fn menuselect_title_hit_enters_tracking_without_immediate_stack_pop() {
let (mut disp, mut cpu, mut bus) = setup_with_port();
disp.enable_input_trace_capture();
disp.menu_bar_hidden = false;
bus.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 20);
let handle = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 520, 0x30B800, "File");
append_menu_data(&mut disp, &mut cpu, &mut bus, handle, 0x30B840, "Open/O");
insert_menu(&mut disp, &mut cpu, &mut bus, handle);
cpu.write_reg(Register::A7, TEST_SP);
assert!(
disp.dispatch_menu(true, 0x137, &mut cpu, &mut bus)
.unwrap()
.is_ok(),
"DrawMenuBar should succeed before MenuSelect tracking"
);
let regions = disp.menu_title_regions();
assert!(
!regions.is_empty(),
"menu title regions should be available"
);
let title_mid_h = (regions[0].0 + regions[0].1) / 2;
disp.mouse_pos = (10, title_mid_h);
disp.mouse_button = true;
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 10);
bus.write_word(TEST_SP + 2, title_mid_h as u16);
bus.write_long(TEST_SP + 4, 0xA5A5_A5A5);
let result = disp.dispatch_menu(true, 0x13D, &mut cpu, &mut bus);
assert!(result.is_some(), "MenuSelect should be handled");
assert!(result.unwrap().is_ok(), "MenuSelect should succeed");
assert_eq!(
cpu.read_reg(Register::A7),
TEST_SP,
"initial title-hit MenuSelect call should defer stack pop while tracking"
);
assert_eq!(
bus.read_long(TEST_SP + 4),
0xA5A5_A5A5,
"MenuSelect should defer writing result while tracking is active"
);
assert!(
disp.menu_tracking.is_some(),
"MenuSelect should enter tracking mode when menu title is hit"
);
let trace = disp.input_trace_text();
assert!(trace.contains("A93D action=start"));
assert!(trace.contains("outcome=open_tracking"));
assert!(trace.contains("A93D action=tracking_entered"));
assert!(trace.contains("tracking=menu:active"));
}
#[test]
fn menuselect_enabled_item_selection_traces_update_release_and_finish() {
let (mut disp, mut cpu, mut bus) = setup_with_port();
disp.enable_input_trace_capture();
disp.menu_bar_hidden = false;
bus.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 20);
let handle = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 520, 0x30B900, "File");
append_menu_data(
&mut disp,
&mut cpu,
&mut bus,
handle,
0x30B940,
"Open/O;Close/W",
);
insert_menu(&mut disp, &mut cpu, &mut bus, handle);
cpu.write_reg(Register::A7, TEST_SP);
assert!(
disp.dispatch_menu(true, 0x137, &mut cpu, &mut bus)
.unwrap()
.is_ok(),
"DrawMenuBar should succeed before MenuSelect tracking"
);
let regions = disp.menu_title_regions();
let title_mid_h = (regions[0].0 + regions[0].1) / 2;
disp.mouse_pos = (10, title_mid_h);
disp.mouse_button = true;
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 10);
bus.write_word(TEST_SP + 2, title_mid_h as u16);
bus.write_long(TEST_SP + 4, 0xA5A5_A5A5);
disp.dispatch_menu(true, 0x13D, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert!(disp.menu_tracking.is_some());
let (dropdown_top, dropdown_left, _, _) =
disp.menu_tracking.as_ref().unwrap().dropdown_rect;
disp.mouse_pos = (dropdown_top + 17, dropdown_left + 8);
disp.dispatch_menu(true, 0x13D, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(
disp.menu_tracking
.as_ref()
.map(|tracking| tracking.highlighted_item),
Some(2)
);
disp.mouse_button = false;
disp.dispatch_menu(true, 0x13D, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(
disp.menu_tracking
.as_ref()
.map(|tracking| tracking.flash_result),
Some(0x0208_0002)
);
for _ in 0..40 {
if disp.menu_tracking.is_none() {
break;
}
disp.dispatch_menu(true, 0x13D, &mut cpu, &mut bus)
.unwrap()
.unwrap();
}
assert!(
disp.menu_tracking.is_none(),
"MenuSelect should finish after flash phases"
);
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
assert_eq!(bus.read_long(TEST_SP + 4), 0x0208_0002);
let trace = disp.input_trace_text();
assert!(trace.contains("A93D action=tracking_update"));
assert!(
trace.contains("highlighted_item=2 result=pending outcome=enabled_item_highlighted")
);
assert!(trace.contains("A93D action=release"));
assert!(trace.contains("highlighted_item=2 result=$02080002 outcome=start_flash"));
assert!(trace.contains("A93D action=finish"));
assert!(trace.contains("tracking=menu:idle dialog:idle control:idle"));
assert!(trace.contains("highlighted_item=2 result=$02080002 outcome=enabled_item_selected"));
}
struct MenuKeyThemeSnapshot {
lower_result: u32,
lower_stack_after: u32,
title_regions: Vec<(i16, i16)>,
matched_title_changed_pixels: usize,
other_title_changed_pixels: usize,
disabled_rightmost_result: u32,
disabled_rightmost_stack_after: u32,
all_disabled_result: u32,
all_disabled_stack_after: u32,
uninserted_result: u32,
uninserted_stack_after: u32,
}
fn menu_key_results_for_theme(theme_id: UiThemeId) -> MenuKeyThemeSnapshot {
let (mut disp, mut cpu, mut bus) = setup_with_port();
disp.set_ui_theme_id(theme_id);
let row_bytes = 64;
let base = bus.alloc(row_bytes * 342);
disp.set_screen_mode_for_test(base, row_bytes, 512, 342, 1);
disp.menu_bar_hidden = false;
clear_1bpp_screen(&mut bus, base, row_bytes, 342);
bus.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 20);
let file = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 700, 0x30B700, "File");
append_menu_data(&mut disp, &mut cpu, &mut bus, file, 0x30B740, "Open/O");
insert_menu(&mut disp, &mut cpu, &mut bus, file);
let edit = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 701, 0x30B800, "Edit");
append_menu_data(&mut disp, &mut cpu, &mut bus, edit, 0x30B840, "Other/O");
insert_menu(&mut disp, &mut cpu, &mut bus, edit);
let ghost = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 702, 0x30B900, "Ghost");
append_menu_data(&mut disp, &mut cpu, &mut bus, ghost, 0x30B940, "Ghost/G");
cpu.write_reg(Register::A7, TEST_SP);
disp.dispatch_menu(true, 0x137, &mut cpu, &mut bus)
.unwrap()
.unwrap();
let title_regions = disp.menu_title_regions();
let file_before = title_region_pixels(
&bus,
base,
row_bytes,
title_regions[0].0,
title_regions[0].1,
);
let edit_before = title_region_pixels(
&bus,
base,
row_bytes,
title_regions[1].0,
title_regions[1].1,
);
let (lower_result, lower_stack_after) =
menu_key_result_and_stack(&mut disp, &mut cpu, &mut bus, b'o');
let file_after = title_region_pixels(
&bus,
base,
row_bytes,
title_regions[0].0,
title_regions[0].1,
);
let edit_after = title_region_pixels(
&bus,
base,
row_bytes,
title_regions[1].0,
title_regions[1].1,
);
let other_title_changed_pixels = changed_pixel_count(&file_before, &file_after);
let matched_title_changed_pixels = changed_pixel_count(&edit_before, &edit_after);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 0);
disp.dispatch_menu(true, 0x138, &mut cpu, &mut bus)
.unwrap()
.unwrap();
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 1);
bus.write_long(TEST_SP + 2, edit);
disp.dispatch_menu(true, 0x13A, &mut cpu, &mut bus)
.unwrap()
.unwrap();
let (disabled_rightmost_result, disabled_rightmost_stack_after) =
menu_key_result_and_stack(&mut disp, &mut cpu, &mut bus, b'O');
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 0);
disp.dispatch_menu(true, 0x138, &mut cpu, &mut bus)
.unwrap()
.unwrap();
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 0);
bus.write_long(TEST_SP + 2, file);
disp.dispatch_menu(true, 0x13A, &mut cpu, &mut bus)
.unwrap()
.unwrap();
let (all_disabled_result, all_disabled_stack_after) =
menu_key_result_and_stack(&mut disp, &mut cpu, &mut bus, b'O');
let (uninserted_result, uninserted_stack_after) =
menu_key_result_and_stack(&mut disp, &mut cpu, &mut bus, b'G');
MenuKeyThemeSnapshot {
lower_result,
lower_stack_after,
title_regions,
matched_title_changed_pixels,
other_title_changed_pixels,
disabled_rightmost_result,
disabled_rightmost_stack_after,
all_disabled_result,
all_disabled_stack_after,
uninserted_result,
uninserted_stack_after,
}
}
#[test]
fn systemless_theme_does_not_change_menukey_result_or_highlight_geometry() {
let classic = menu_key_results_for_theme(UiThemeId::ClassicSystem7);
let themed = menu_key_results_for_theme(UiThemeId::SystemlessDefault);
assert_eq!(classic.lower_result, 0x02BD_0001);
assert_eq!(classic.lower_stack_after, TEST_SP + 2);
assert_eq!(classic.disabled_rightmost_result, 0x02BC_0001);
assert_eq!(classic.disabled_rightmost_stack_after, TEST_SP + 2);
assert_eq!(classic.all_disabled_result, 0);
assert_eq!(classic.all_disabled_stack_after, TEST_SP + 2);
assert_eq!(classic.uninserted_result, 0);
assert_eq!(classic.uninserted_stack_after, TEST_SP + 2);
assert!(
classic.matched_title_changed_pixels > classic.other_title_changed_pixels,
"classic MenuKey should primarily change the matched rightmost title"
);
assert!(
classic.matched_title_changed_pixels > 0,
"classic MenuKey should highlight the matched rightmost title"
);
assert_eq!(themed.lower_result, classic.lower_result);
assert_eq!(themed.lower_stack_after, classic.lower_stack_after);
assert_eq!(
themed.disabled_rightmost_result,
classic.disabled_rightmost_result
);
assert_eq!(
themed.disabled_rightmost_stack_after,
classic.disabled_rightmost_stack_after
);
assert_eq!(themed.all_disabled_result, classic.all_disabled_result);
assert_eq!(
themed.all_disabled_stack_after,
classic.all_disabled_stack_after
);
assert_eq!(themed.uninserted_result, classic.uninserted_result);
assert_eq!(
themed.uninserted_stack_after,
classic.uninserted_stack_after
);
assert_eq!(
themed.title_regions, classic.title_regions,
"systemless-default must not change MenuKey title geometry"
);
assert_eq!(
themed.other_title_changed_pixels, 0,
"systemless-default should keep non-matched title chrome unchanged"
);
assert!(
themed.matched_title_changed_pixels > 0,
"systemless-default MenuKey should route the matched title highlight through the provider"
);
}
struct PopUpMenuSelectThemeSnapshot {
rect: (i16, i16, i16, i16),
highlighted_item: i16,
item_at_requested_point: i16,
first_stack_after: u32,
result: u32,
final_stack_after: u32,
tracking_finished: bool,
clamped_rect: (i16, i16, i16, i16),
clamped_highlighted_item: i16,
uninserted_result: u32,
uninserted_stack_after: u32,
uninserted_tracking_finished: bool,
}
fn dispatch_popupmenuselect_start(
disp: &mut super::super::TrapDispatcher,
cpu: &mut MockCpu,
bus: &mut crate::memory::MacMemoryBus,
menu_handle: u32,
top: i16,
left: i16,
popup_item: i16,
) {
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, popup_item as u16);
bus.write_word(TEST_SP + 2, left as u16);
bus.write_word(TEST_SP + 4, top as u16);
bus.write_long(TEST_SP + 6, menu_handle);
bus.write_long(TEST_SP + 10, 0xDEAD_BEEF);
assert!(
disp.dispatch_menu(true, 0x00B, cpu, bus).unwrap().is_ok(),
"PopUpMenuSelect should succeed"
);
}
fn finish_popupmenuselect(
disp: &mut super::super::TrapDispatcher,
cpu: &mut MockCpu,
bus: &mut crate::memory::MacMemoryBus,
) -> (u32, u32, bool) {
disp.mouse_button = false;
bus.write_byte(crate::memory::globals::addr::MB_STATE, 0x80);
for _ in 0..80 {
if disp.menu_tracking.is_none() {
break;
}
disp.dispatch_menu(true, 0x00B, cpu, bus).unwrap().unwrap();
}
(
bus.read_long(TEST_SP + 10),
cpu.read_reg(Register::A7),
disp.menu_tracking.is_none(),
)
}
fn popupmenuselect_theme_snapshot(theme_id: UiThemeId) -> PopUpMenuSelectThemeSnapshot {
let (mut disp, mut cpu, mut bus) = setup_with_port();
disp.set_ui_theme_id(theme_id);
let row_bytes = 30;
let base = bus.alloc(row_bytes * 160);
disp.set_screen_mode_for_test(base, row_bytes, 240, 160, 1);
clear_1bpp_screen(&mut bus, base, row_bytes, 160);
disp.menu_bar_hidden = false;
disp.mouse_button = true;
let menu = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 730, 0x30BB00, "Pop");
append_menu_data(
&mut disp,
&mut cpu,
&mut bus,
menu,
0x30BB40,
"One;Two;Three;Four",
);
insert_menu_before_id(&mut disp, &mut cpu, &mut bus, menu, -1);
dispatch_popupmenuselect_start(&mut disp, &mut cpu, &mut bus, menu, 58, 30, 3);
let tracking = disp.menu_tracking.as_ref().expect("popup tracking active");
let rect = tracking.dropdown_rect;
let highlighted_item = tracking.highlighted_item;
let first_stack_after = cpu.read_reg(Register::A7);
let item_at_requested_point = disp.dropdown_item_at_point(&bus, 35, 58);
let (result, final_stack_after, tracking_finished) =
finish_popupmenuselect(&mut disp, &mut cpu, &mut bus);
let (mut clamp_disp, mut clamp_cpu, mut clamp_bus) = setup_with_port();
clamp_disp.set_ui_theme_id(theme_id);
let clamp_base = clamp_bus.alloc(row_bytes * 160);
clamp_disp.set_screen_mode_for_test(clamp_base, row_bytes, 240, 160, 1);
clear_1bpp_screen(&mut clamp_bus, clamp_base, row_bytes, 160);
clamp_disp.menu_bar_hidden = false;
clamp_disp.mouse_button = true;
let clamp_menu = new_menu_with_title(
&mut clamp_disp,
&mut clamp_cpu,
&mut clamp_bus,
731,
0x30BC00,
"Pop",
);
append_menu_data(
&mut clamp_disp,
&mut clamp_cpu,
&mut clamp_bus,
clamp_menu,
0x30BC40,
"One;Two;Three;Four",
);
insert_menu_before_id(
&mut clamp_disp,
&mut clamp_cpu,
&mut clamp_bus,
clamp_menu,
-1,
);
dispatch_popupmenuselect_start(
&mut clamp_disp,
&mut clamp_cpu,
&mut clamp_bus,
clamp_menu,
150,
220,
4,
);
let clamp_tracking = clamp_disp
.menu_tracking
.as_ref()
.expect("clamped popup tracking active");
let clamped_rect = clamp_tracking.dropdown_rect;
let clamped_highlighted_item = clamp_tracking.highlighted_item;
let (mut miss_disp, mut miss_cpu, mut miss_bus) = setup_with_port();
miss_disp.set_ui_theme_id(theme_id);
miss_disp.menu_bar_hidden = false;
miss_disp.mouse_button = false;
let miss_menu = new_menu_with_title(
&mut miss_disp,
&mut miss_cpu,
&mut miss_bus,
732,
0x30BD00,
"Pop",
);
append_menu_data(
&mut miss_disp,
&mut miss_cpu,
&mut miss_bus,
miss_menu,
0x30BD40,
"One;Two",
);
dispatch_popupmenuselect_start(
&mut miss_disp,
&mut miss_cpu,
&mut miss_bus,
miss_menu,
20,
20,
1,
);
PopUpMenuSelectThemeSnapshot {
rect,
highlighted_item,
item_at_requested_point,
first_stack_after,
result,
final_stack_after,
tracking_finished,
clamped_rect,
clamped_highlighted_item,
uninserted_result: miss_bus.read_long(TEST_SP + 10),
uninserted_stack_after: miss_cpu.read_reg(Register::A7),
uninserted_tracking_finished: miss_disp.menu_tracking.is_none(),
}
}
#[test]
fn systemless_theme_does_not_change_popupmenuselect_geometry_and_result() {
let classic = popupmenuselect_theme_snapshot(UiThemeId::ClassicSystem7);
let themed = popupmenuselect_theme_snapshot(UiThemeId::SystemlessDefault);
assert_eq!(classic.rect, (25, 29, 91, 91));
assert_eq!(classic.highlighted_item, 3);
assert_eq!(classic.item_at_requested_point, 3);
assert_eq!(classic.first_stack_after, TEST_SP);
assert_eq!(classic.result, 0x02DA_0003);
assert_eq!(classic.final_stack_after, TEST_SP + 10);
assert!(classic.tracking_finished);
assert_eq!(classic.clamped_rect, (94, 178, 160, 240));
assert_eq!(classic.clamped_highlighted_item, 4);
assert_eq!(classic.uninserted_result, 0);
assert_eq!(classic.uninserted_stack_after, TEST_SP + 10);
assert!(classic.uninserted_tracking_finished);
assert_eq!(
themed.rect, classic.rect,
"systemless-default must preserve PopUpMenuSelect popup geometry"
);
assert_eq!(themed.highlighted_item, classic.highlighted_item);
assert_eq!(
themed.item_at_requested_point,
classic.item_at_requested_point
);
assert_eq!(themed.first_stack_after, classic.first_stack_after);
assert_eq!(themed.result, classic.result);
assert_eq!(themed.final_stack_after, classic.final_stack_after);
assert_eq!(themed.tracking_finished, classic.tracking_finished);
assert_eq!(themed.clamped_rect, classic.clamped_rect);
assert_eq!(
themed.clamped_highlighted_item,
classic.clamped_highlighted_item
);
assert_eq!(themed.uninserted_result, classic.uninserted_result);
assert_eq!(
themed.uninserted_stack_after,
classic.uninserted_stack_after
);
assert_eq!(
themed.uninserted_tracking_finished,
classic.uninserted_tracking_finished
);
}
fn menuselect_enabled_item_result_for_theme(
theme_id: UiThemeId,
) -> (u32, u32, (i16, i16, i16, i16)) {
let (mut disp, mut cpu, mut bus) = setup_with_port();
disp.set_ui_theme_id(theme_id);
disp.menu_bar_hidden = false;
bus.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 20);
let handle = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 520, 0x30BA00, "File");
append_menu_data(
&mut disp,
&mut cpu,
&mut bus,
handle,
0x30BA40,
"Open/O;Close/W",
);
insert_menu(&mut disp, &mut cpu, &mut bus, handle);
cpu.write_reg(Register::A7, TEST_SP);
disp.dispatch_menu(true, 0x137, &mut cpu, &mut bus)
.unwrap()
.unwrap();
disp.mouse_pos = (10, 15);
disp.mouse_button = true;
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 10);
bus.write_word(TEST_SP + 2, 15);
bus.write_long(TEST_SP + 4, 0xA5A5_A5A5);
disp.dispatch_menu(true, 0x13D, &mut cpu, &mut bus)
.unwrap()
.unwrap();
let dropdown_rect = disp.menu_tracking.as_ref().unwrap().dropdown_rect;
let (dropdown_top, dropdown_left, _, _) = dropdown_rect;
disp.mouse_pos = (dropdown_top + 17, dropdown_left + 8);
disp.dispatch_menu(true, 0x13D, &mut cpu, &mut bus)
.unwrap()
.unwrap();
disp.mouse_button = false;
disp.dispatch_menu(true, 0x13D, &mut cpu, &mut bus)
.unwrap()
.unwrap();
for _ in 0..40 {
if disp.menu_tracking.is_none() {
break;
}
disp.dispatch_menu(true, 0x13D, &mut cpu, &mut bus)
.unwrap()
.unwrap();
}
(
bus.read_long(TEST_SP + 4),
cpu.read_reg(Register::A7),
dropdown_rect,
)
}
#[test]
fn systemless_theme_does_not_change_menuselect_result_encoding() {
let classic = menuselect_enabled_item_result_for_theme(UiThemeId::ClassicSystem7);
let themed = menuselect_enabled_item_result_for_theme(UiThemeId::SystemlessDefault);
assert_eq!(classic.0, 0x0208_0002);
assert_eq!(classic.1, TEST_SP + 4);
assert_eq!(
themed, classic,
"systemless-default must not change MenuSelect result encoding or dropdown geometry"
);
}
#[test]
fn menuselect_tracks_hierarchical_submenu_item() {
let (mut disp, mut cpu, mut bus) = setup_with_port();
disp.menu_bar_hidden = false;
bus.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 20);
let file = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 129, 0x310000, "File");
append_menu_data(
&mut disp,
&mut cpu,
&mut bus,
file,
0x310040,
"New Game;Resume Game;-;Practice Battle;Pause Game;Resign Game;-;Quit",
);
let practice =
new_menu_with_title(&mut disp, &mut cpu, &mut bus, 138, 0x310200, "Practice");
append_menu_data(
&mut disp,
&mut cpu,
&mut bus,
practice,
0x310240,
"1: Cake Walk;(2: One-Gun;(3: Sucker Punch;(4: Airborne",
);
set_item_cmd(&mut disp, &mut cpu, &mut bus, file, 4, 0x1B);
set_item_mark(&mut disp, &mut cpu, &mut bus, file, 4, 138);
insert_menu(&mut disp, &mut cpu, &mut bus, file);
insert_menu_before(&mut disp, &mut cpu, &mut bus, practice, -1);
cpu.write_reg(Register::A7, TEST_SP);
assert!(
disp.dispatch_menu(true, 0x137, &mut cpu, &mut bus)
.unwrap()
.is_ok(),
"DrawMenuBar should succeed before MenuSelect tracking"
);
let regions = disp.menu_title_regions();
let file_mid_h = (regions[0].0 + regions[0].1) / 2;
disp.mouse_pos = (10, file_mid_h);
disp.mouse_button = true;
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 10);
bus.write_word(TEST_SP + 2, file_mid_h as u16);
bus.write_long(TEST_SP + 4, 0xFFFF_FFFF);
assert!(
disp.dispatch_menu(true, 0x13D, &mut cpu, &mut bus)
.unwrap()
.is_ok(),
"MenuSelect should enter tracking on the File title"
);
let parent_rect = disp
.menu_tracking
.as_ref()
.expect("MenuSelect should be tracking")
.dropdown_rect;
let parent_item_y = parent_rect.0 + 1 + 3 * 16 + 8;
disp.mouse_pos = (parent_item_y, parent_rect.1 + 24);
assert!(
disp.dispatch_menu(true, 0x13D, &mut cpu, &mut bus)
.unwrap()
.is_ok(),
"MenuSelect should track the hierarchical parent item"
);
disp.mouse_pos = (parent_item_y, parent_rect.3 + 20);
assert!(
disp.dispatch_menu(true, 0x13D, &mut cpu, &mut bus)
.unwrap()
.is_ok(),
"MenuSelect should track into the submenu"
);
disp.mouse_button = false;
for _ in 0..40 {
assert!(
disp.dispatch_menu(true, 0x13D, &mut cpu, &mut bus)
.unwrap()
.is_ok(),
"MenuSelect should finish submenu selection"
);
if disp.menu_tracking.is_none() {
break;
}
}
assert!(
disp.menu_tracking.is_none(),
"MenuSelect should finish after flashing the submenu item"
);
assert_eq!(
bus.read_long(TEST_SP + 4),
(138u32 << 16) | 1,
"MenuSelect should return the selected submenu menuID/item"
);
assert_eq!(
cpu.read_reg(Register::A7),
TEST_SP + 4,
"MenuSelect should pop the Point argument after submenu selection"
);
}
#[test]
fn menuselect_regular_menu_after_hierarchical_menu_still_opens() {
let (mut disp, mut cpu, mut bus) = setup_with_port();
disp.menu_bar_hidden = false;
bus.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 20);
let file = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 540, 0x311000, "File");
append_menu_data(&mut disp, &mut cpu, &mut bus, file, 0x311040, "Open");
let submenu = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 541, 0x311100, "Practice");
append_menu_data(
&mut disp,
&mut cpu,
&mut bus,
submenu,
0x311140,
"1: Cake Walk",
);
let edit = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 542, 0x311200, "Edit");
append_menu_data(&mut disp, &mut cpu, &mut bus, edit, 0x311240, "Copy");
insert_menu(&mut disp, &mut cpu, &mut bus, file);
insert_menu_before(&mut disp, &mut cpu, &mut bus, submenu, -1);
insert_menu(&mut disp, &mut cpu, &mut bus, edit);
cpu.write_reg(Register::A7, TEST_SP);
assert!(
disp.dispatch_menu(true, 0x137, &mut cpu, &mut bus)
.unwrap()
.is_ok(),
"DrawMenuBar should succeed before MenuSelect tracking"
);
let regions = disp.menu_title_regions();
assert_eq!(
regions.len(),
2,
"hierarchical menu must not create a menu-bar title"
);
let edit_mid_h = (regions[1].0 + regions[1].1) / 2;
disp.mouse_pos = (10, edit_mid_h);
disp.mouse_button = true;
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 10);
bus.write_word(TEST_SP + 2, edit_mid_h as u16);
bus.write_long(TEST_SP + 4, 0xA5A5_A5A5);
assert!(
disp.dispatch_menu(true, 0x13D, &mut cpu, &mut bus)
.unwrap()
.is_ok(),
"MenuSelect should enter tracking on the Edit title"
);
let edit_idx = disp
.menus
.iter()
.position(|menu| menu.handle == edit)
.expect("Edit menu should still be tracked");
assert_eq!(
disp.menu_tracking
.as_ref()
.expect("MenuSelect should be tracking")
.active_menu,
edit_idx,
"MenuSelect should open the regular menu after a hierarchical menu"
);
}
#[test]
fn save_dropdown_pixels_handles_negative_top_without_overflow() {
let (mut disp, _cpu, mut bus) = setup();
let screen_base = bus.alloc((800 * 600) as u32);
for i in 0..800u32 * 600 {
bus.write_byte(screen_base + i, 0x77);
}
bus.write_long(0x0824, screen_base);
disp.screen_mode = (screen_base, 800, 800, 600, 8);
let saved = disp.save_dropdown_pixels(&bus, (-2, 100, 4, 120));
assert_eq!(saved.len(), 5 * 21);
for &b in &saved {
assert_eq!(b, 0x77, "on-screen bytes must come from framebuffer");
}
}
#[test]
fn save_dropdown_pixels_handles_top_beyond_screen_height_without_overflow() {
let (mut disp, _cpu, mut bus) = setup();
let screen_base = bus.alloc((800 * 600) as u32);
bus.write_long(0x0824, screen_base);
disp.screen_mode = (screen_base, 800, 800, 600, 8);
let saved = disp.save_dropdown_pixels(&bus, (600, 0, 610, 50));
assert_eq!(
saved.len(),
0,
"dropdown entirely off-screen must produce an empty buffer"
);
}
#[test]
fn ploticon_pascal_procedure_preserves_stack_across_five_calls() {
let (mut disp, mut cpu, mut bus) = setup();
let sp_pre = cpu.read_reg(Register::A7);
let icons: [u32; 5] = std::array::from_fn(|_| {
let icon_ptr = bus.alloc(128);
let icon_handle = bus.alloc(4);
bus.write_long(icon_handle, icon_ptr);
icon_handle
});
let rects: [u32; 5] = std::array::from_fn(|i| {
let rect_ptr = bus.alloc(8);
bus.write_word(rect_ptr, 2);
bus.write_word(rect_ptr + 2, (i as u16) * 40 + 2);
bus.write_word(rect_ptr + 4, 34);
bus.write_word(rect_ptr + 6, (i as u16) * 40 + 34);
rect_ptr
});
for i in 0..5 {
let sp = cpu.read_reg(Register::A7);
cpu.write_reg(Register::A7, sp - 8);
bus.write_long(sp - 8, icons[i]);
bus.write_long(sp - 4, rects[i]);
let result = disp.dispatch_menu(true, 0x14B, &mut cpu, &mut bus);
assert!(
result.unwrap().is_ok(),
"PlotIcon dispatch should succeed (call {})",
i
);
assert_eq!(
cpu.read_reg(Register::A7),
sp,
"PlotIcon should pop 8 bytes per call (call {})",
i
);
}
assert_eq!(
cpu.read_reg(Register::A7),
sp_pre,
"5 successive PlotIcon calls should net-balance A7"
);
}
#[test]
fn ploticon_current_port_zero_short_circuits_before_touching_memory() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = cpu.read_reg(Register::A7);
let warnings = Arc::new(AtomicUsize::new(0));
let subscriber = WarnCounter {
warnings: warnings.clone(),
};
cpu.write_reg(Register::A7, sp - 8);
bus.write_long(sp - 8, 0x0050_0000);
bus.write_long(sp - 4, 0x0060_0000);
let _guard = tracing::subscriber::set_default(subscriber);
let result = disp.dispatch_menu(true, 0x14B, &mut cpu, &mut bus);
assert!(result.is_some(), "PlotIcon should be handled");
assert!(
result.unwrap().is_ok(),
"PlotIcon should return cleanly when no current port is set"
);
assert_eq!(
cpu.read_reg(Register::A7),
sp,
"PlotIcon should still pop the 8-byte argument frame"
);
assert_eq!(
warnings.load(Ordering::Relaxed),
0,
"PlotIcon should not touch icon/rect memory when current_port is NIL"
);
}
}