use crate::cpu::{CpuOps, Register};
use crate::guest_call::GuestCallTarget;
use crate::guest_procedure::{resolve_guest_procedure, GuestIsa};
use crate::memory::{globals::addr, MacMemoryBus, MemoryBus};
use crate::menu_manager::{
compiled_menu_color_entries, filter_menu_color_entries,
for_each_standard_hierarchy_indicator_pixel, for_each_standard_scroll_down_indicator_pixel,
for_each_standard_scroll_up_indicator_pixel, hierarchical_menu_id, install_menu_list_copy,
laid_out_menu_item_count, menu_choice_value,
merge_menu_color_entries as shared_merge_menu_color_entries, new_standard_menu_record,
standard_menu_gray_pattern_is_ink, standard_menu_height as shared_standard_menu_height,
standard_menu_highlighted_value, standard_menu_icon_kind, standard_menu_icon_resource_id,
standard_menu_item_layout, standard_menu_text_advance as shared_standard_menu_text_advance,
standard_menu_width as shared_standard_menu_width, standard_popup_menu_layout,
standard_pull_down_menu_layout, standard_submenu_layout,
ColorIconLayout as SharedColorIconLayout, MenuBarBuild as SharedMenuBarBuild,
MenuBarBuildStep as SharedMenuBarBuildStep, MenuBarResource as SharedMenuBarResource,
MenuBarTitleRegion as SharedMenuBarTitleRegion, MenuColorTable,
MenuDefinitionInvocation as SharedMenuDefinitionInvocation,
MenuDefinitionPane as SharedMenuDefinitionPane,
MenuDefinitionTracking as SharedMenuDefinitionTracking, MenuItems as SharedMenuItems,
MenuKeyItem as SharedMenuKeyItem, MenuKeyMenu as SharedMenuKeyMenu, MenuList as SharedMenuList,
MenuListInstallRequest, MenuRow as SharedMenuRow, MenuRows as SharedMenuRows,
MenuSnapshotRecord as SharedMenuSnapshotRecord, MenuTrackingKind, MenuTrackingPane,
MonochromeMenuIconLayout as SharedMonochromeMenuIconLayout, ProcessMenuTrackingState,
ProcessTrackedMenuPane, StandardMenuChrome, StandardMenuIconKind, StandardMenuItemWidth,
StandardMenuPaneKind, SubmenuReconciliation, SubmenuRequest, TrackedMenuPaneView,
MAX_MENU_LIST_ENTRIES, MENU_COLOR_ENTRY_SIZE, STANDARD_MENU_BAR_FIRST_TITLE_LEFT,
STANDARD_MENU_BAR_TITLE_SPACING, STANDARD_MENU_DEFINITION_SHIM,
STANDARD_MENU_FLASH_PHASE_DELAY, STANDARD_MENU_SEPARATOR_HEIGHT,
};
#[cfg(test)]
use crate::menu_manager::{parse_menu_item_specs, standard_menu_row_height};
use crate::menu_model::GuestMenuSnapshot;
use crate::quickdraw::text::QuickDrawTextStyle;
use crate::trap::types::encode_mac_roman_lossy;
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(crate) type MenuTrackingState = ProcessMenuTrackingState;
pub(crate) type SubmenuTrackingState = ProcessTrackedMenuPane;
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct PendingMenuBarBuild {
pub(crate) stack_ptr: u32,
pub(crate) build: SharedMenuBarBuild<u32>,
}
pub(crate) fn tracked_menu_state(
kind: MenuTrackingKind,
menu_handle: u32,
rect: (i16, i16, i16, i16),
saved_pixels: Vec<u8>,
) -> MenuTrackingState {
tracked_menu_state_with_content_top(kind, menu_handle, rect, rect.0, saved_pixels)
}
fn tracked_menu_state_with_content_top(
kind: MenuTrackingKind,
menu_handle: u32,
rect: (i16, i16, i16, i16),
content_top: i16,
saved_pixels: Vec<u8>,
) -> MenuTrackingState {
let (popup_top, popup_left, popup_bottom, popup_right) = rect;
MenuTrackingState {
kind,
menu_handle,
popup_left,
popup_top,
content_top,
scroll_direction: None,
popup_width: popup_right.saturating_sub(popup_left),
popup_height: popup_bottom.saturating_sub(popup_top),
highlighted_item: 0,
definition: None,
flash_remaining: 0,
flash_delay: 0,
flash_result: 0,
saved_width: popup_right.saturating_sub(popup_left),
saved_height: popup_bottom.saturating_sub(popup_top),
front_buffer: None,
saved_pixels: saved_pixels.into_iter().map(u16::from).collect(),
item_appearances: Vec::new(),
submenus: Vec::new(),
}
}
pub(crate) fn tracked_submenu_state(
menu_handle: u32,
parent_item: i16,
rect: (i16, i16, i16, i16),
saved_pixels: Vec<u8>,
) -> SubmenuTrackingState {
let (popup_top, popup_left, popup_bottom, popup_right) = rect;
SubmenuTrackingState {
parent_item,
menu_handle,
popup_left,
popup_top,
content_top: popup_top,
scroll_direction: None,
popup_width: popup_right.saturating_sub(popup_left),
popup_height: popup_bottom.saturating_sub(popup_top),
highlighted_item: 0,
definition: None,
saved_width: popup_right.saturating_sub(popup_left),
saved_height: popup_bottom.saturating_sub(popup_top),
front_buffer: None,
saved_pixels: saved_pixels.into_iter().map(u16::from).collect(),
item_appearances: Vec::new(),
}
}
#[cfg(test)]
pub(crate) fn test_tracked_menu_state(
menu_handle: u32,
rect: (i16, i16, i16, i16),
highlighted_item: i16,
) -> MenuTrackingState {
let mut state = tracked_menu_state(MenuTrackingKind::MenuBar, menu_handle, rect, Vec::new());
state.highlighted_item = highlighted_item;
state
}
#[cfg(test)]
const MENU_KEY_REDUCED_ICON: u8 = 0x1D;
#[cfg(test)]
const MENU_KEY_SMALL_ICON: u8 = 0x1E;
const MENU_ROW_HEIGHT: i16 = 16;
const MDEF_TRAMPOLINE_SIZE: u32 = 60;
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 {
menu_items_from_memory(bus, menu_handle)
.map(|items| items.item_count())
.unwrap_or(0)
}
fn macroman_to_string(bytes: &[u8]) -> String {
bytes.iter().map(|&byte| byte as char).collect()
}
pub(super) fn internal_menu_string_bytes(value: &str) -> Vec<u8> {
let mut bytes = Vec::with_capacity(value.len());
for ch in value.chars() {
if (ch as u32) <= u8::MAX as u32 {
bytes.push(ch as u8);
} else {
bytes.extend(encode_mac_roman_lossy(&ch.to_string()));
}
}
bytes
}
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 = macroman_to_string(&title_bytes);
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 = macroman_to_string(&text_bytes);
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 menu_key_menu_from_memory(bus: &MacMemoryBus, menu_handle: u32) -> Option<SharedMenuKeyMenu> {
let menu_ptr = bus.read_long(menu_handle);
if menu_ptr == 0 {
return None;
}
let record_size = bus
.get_alloc_size(menu_ptr)
.unwrap_or_else(|| menu_resource_size(bus, menu_ptr) as u32);
let menu = SharedMenuItems::decode(&bus.read_bytes(menu_ptr, record_size as usize))?;
Some(SharedMenuKeyMenu {
id: bus.read_word(menu_ptr) as i16,
enabled: menu.enable_flags & 1 != 0,
items: menu
.items
.into_iter()
.map(|item| SharedMenuKeyItem {
command: item.command,
mark: item.mark,
enabled: item.enabled,
})
.collect(),
})
}
fn menu_items_from_memory(bus: &MacMemoryBus, menu_handle: u32) -> Option<SharedMenuItems> {
let menu_ptr = bus.read_long(menu_handle);
if menu_ptr == 0 {
return None;
}
let record_size = bus
.get_alloc_size(menu_ptr)
.unwrap_or_else(|| menu_resource_size(bus, menu_ptr) as u32);
SharedMenuItems::decode(&bus.read_bytes(menu_ptr, record_size as usize))
}
#[cfg(test)]
fn parse_appendmenu_items(bytes: &[u8]) -> Vec<MenuItem> {
parse_menu_item_specs(bytes)
.into_iter()
.map(|item| MenuItem {
text: macroman_to_string(&item.text),
icon: item.icon,
key_equiv: item.command,
mark: item.mark,
style: item.style,
enabled: item.enabled,
})
.collect()
}
fn refresh_menu_from_memory(bus: &MacMemoryBus, menu: &mut Menu) {
if menu.handle == 0 {
return;
}
let menu_ptr = bus.read_long(menu.handle);
if menu_ptr == 0 {
return;
}
let mut parsed = parse_menu_resource(bus, menu_ptr, menu.handle);
for (parsed_item, cached_item) in parsed
.items
.iter_mut()
.skip(31)
.zip(menu.items.iter().skip(31))
{
parsed_item.enabled = cached_item.enabled;
}
menu.id = parsed.id;
menu.title = parsed.title;
menu.items = parsed.items;
menu.enabled = parsed.enabled;
}
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))
}
fn menu_list_from_memory(bus: &MacMemoryBus, menu_list_handle: u32) -> Option<SharedMenuList> {
let list = bus.read_long(menu_list_handle);
if list == 0 {
return None;
}
let regular_bytes = usize::from(bus.read_word(list));
if regular_bytes % 6 != 0 || regular_bytes / 6 > MAX_MENU_LIST_ENTRIES {
return None;
}
let hierarchical_header = list.checked_add(6 + u32::try_from(regular_bytes).ok()?)?;
let hierarchical_bytes = usize::from(bus.read_word(hierarchical_header));
if hierarchical_bytes % 6 != 0 || hierarchical_bytes / 6 > MAX_MENU_LIST_ENTRIES {
return None;
}
let byte_count = 12usize
.checked_add(regular_bytes)?
.checked_add(hierarchical_bytes)?;
SharedMenuList::decode(&bus.read_bytes(list, byte_count))
}
const MC_ENTRY_SIZE: usize = MENU_COLOR_ENTRY_SIZE;
const MC_ALL_ITEMS: i16 = -98;
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 {
pub(crate) fn complete_deferred_menu_replacement(
&mut self,
bus: &MacMemoryBus,
menu_handle: u32,
old_ptr: u32,
) {
let menu_ptr = bus.read_long(menu_handle);
self.ptr_to_handle.remove(&old_ptr);
if menu_ptr != 0 {
self.ptr_to_handle.insert(menu_ptr, menu_handle);
}
if let Some(menu) = self.menus.iter_mut().find(|m| m.handle == menu_handle) {
refresh_menu_from_memory(bus, menu);
} else if menu_ptr != 0 {
self.menus
.push(parse_menu_resource(bus, menu_ptr, menu_handle));
}
}
fn menu_uses_standard_definition(&self, bus: &MacMemoryBus, menu_ptr: u32) -> bool {
let handle = bus.read_long(menu_ptr + 6);
self.loaded_handles
.get(&handle)
.is_some_and(|(_, resource_type, resource_id)| {
*resource_type == *b"MDEF" && *resource_id == 0
})
|| bus.read_bytes(bus.read_long(handle), STANDARD_MENU_DEFINITION_SHIM.len())
== STANDARD_MENU_DEFINITION_SHIM
}
fn arm_menu_definition<C: CpuOps>(
&mut self,
cpu: &mut C,
bus: &mut MacMemoryBus,
invocation: SharedMenuDefinitionInvocation,
) -> bool {
self.arm_menu_definition_to(cpu, bus, invocation, cpu.read_reg(Register::PC))
}
fn arm_menu_definition_to<C: CpuOps>(
&mut self,
cpu: &mut C,
bus: &mut MacMemoryBus,
invocation: SharedMenuDefinitionInvocation,
return_pc: u32,
) -> bool {
let menu_handle = invocation.menu_handle;
let menu_ptr = bus.read_long(menu_handle);
if menu_ptr == 0 || self.menu_uses_standard_definition(bus, menu_ptr) {
return false;
}
let proc_ptr = bus.read_long(bus.read_long(menu_ptr + 6));
let Some(procedure) =
resolve_guest_procedure(bus, proc_ptr, 0, None, GuestIsa::M68k, GuestIsa::M68k)
else {
return false;
};
if procedure.isa != GuestIsa::M68k || procedure.entry >= bus.ram_size() {
return false;
}
let proc_addr = procedure.entry;
let trampoline = if self.menu_def_trampoline == 0 {
self.menu_def_trampoline = bus.alloc(MDEF_TRAMPOLINE_SIZE);
self.menu_def_trampoline
} else {
self.menu_def_trampoline
};
let call = invocation.call(trampoline + 50);
let final_sp = cpu.read_reg(Register::A7);
let return_slot = final_sp.wrapping_sub(4);
bus.write_word(trampoline, 0x48E7); bus.write_word(trampoline + 2, 0xF0F0);
bus.write_word(trampoline + 4, 0x3F3C); bus.write_word(trampoline + 6, call.message as i16 as u16);
for (offset, value) in [
(8, call.menu_handle),
(14, call.menu_rect),
(20, call.hit_point),
(26, call.which_item),
] {
bus.write_word(trampoline + offset, 0x2F3C); bus.write_long(trampoline + offset + 2, value);
}
bus.write_word(trampoline + 32, 0x4EB9); bus.write_long(trampoline + 34, proc_addr);
bus.write_word(trampoline + 38, 0x2E7C); bus.write_long(trampoline + 40, return_slot.wrapping_sub(32));
bus.write_word(trampoline + 44, 0x4CDF); bus.write_word(trampoline + 46, 0x0F0F);
bus.write_word(trampoline + 48, 0x4E75); for (offset, byte) in invocation.scratch_bytes().into_iter().enumerate() {
bus.write_byte(trampoline + 50 + offset as u32, byte);
}
bus.write_long(return_slot, return_pc);
if return_pc != cpu.read_reg(Register::PC) {
self.guest_calls.begin_m68k(
GuestCallTarget {
isa: procedure.isa,
entry: procedure.entry,
rtoc: procedure.rtoc,
},
return_pc,
final_sp,
);
}
cpu.write_reg(Register::A7, return_slot);
cpu.write_reg(Register::PC, trampoline);
true
}
fn complete_pending_menu_definition<C: CpuOps>(
&mut self,
cpu: &C,
bus: &MacMemoryBus,
) -> Option<crate::menu_manager::MenuDefinitionMessage> {
let trampoline = self.menu_def_trampoline;
if self
.active_menu_definition()
.copied()
.and_then(SharedMenuDefinitionTracking::pending_invocation)
.is_none()
|| trampoline == 0
{
return None;
}
if !self
.guest_calls
.complete_m68k(cpu.read_reg(Register::PC), cpu.read_reg(Register::A7))
{
return None;
}
let bytes = bus.read_bytes(trampoline + 50, 10);
let Ok(bytes) = <[u8; 10]>::try_from(bytes) else {
return None;
};
self.active_menu_definition_mut()?
.complete_pending(SharedMenuDefinitionInvocation::decode_result(bytes))
}
fn arm_pending_menu_definition<C: CpuOps>(
&mut self,
cpu: &mut C,
bus: &mut MacMemoryBus,
return_pc: u32,
) -> bool {
let invocation = self
.active_menu_definition()
.copied()
.and_then(SharedMenuDefinitionTracking::pending_invocation);
invocation
.is_some_and(|invocation| self.arm_menu_definition_to(cpu, bus, invocation, return_pc))
}
fn active_menu_definition(&self) -> Option<&SharedMenuDefinitionTracking> {
self.menu_tracking
.as_ref()
.and_then(MenuTrackingState::active_definition)
.or(self.menu_definition_tracking.as_ref())
}
fn active_menu_definition_mut(&mut self) -> Option<&mut SharedMenuDefinitionTracking> {
if self
.menu_tracking
.as_ref()
.and_then(MenuTrackingState::active_definition)
.is_some()
{
return self
.menu_tracking
.as_mut()
.and_then(MenuTrackingState::active_definition_mut);
}
self.menu_definition_tracking.as_mut()
}
fn clear_active_menu_definition(&mut self) {
if let Some(tracking) = self.menu_tracking.as_mut() {
if tracking.take_active_definition().is_some() {
return;
}
}
self.menu_definition_tracking = None;
}
fn prepare_menu_definition_port<C: CpuOps>(&mut self, cpu: &mut C, bus: &mut MacMemoryBus) {
if self.menu_definition_port_state.is_none() {
self.menu_definition_port_state = Some(self.capture_current_port_state(bus));
}
let port = self.ensure_color_window_manager_port(bus);
self.set_current_port_state(bus, cpu, port, None);
}
fn restore_menu_definition_port<C: CpuOps>(&mut self, cpu: &mut C, bus: &mut MacMemoryBus) {
if let Some(snapshot) = self.menu_definition_port_state.take() {
self.restore_current_port_state(bus, cpu, &snapshot);
}
}
fn abort_custom_menu_tracking<C: CpuOps>(
&mut self,
cpu: &mut C,
bus: &mut MacMemoryBus,
result_offset: u32,
) {
self.clear_active_menu_definition();
let kind = self.menu_tracking.as_ref().map(|tracking| tracking.kind);
if let Some(saved) = self.menu_tracking.take() {
self.restore_menu_tracking_pixels(bus, saved);
}
if kind == Some(MenuTrackingKind::MenuBar) {
bus.write_word(addr::THE_MENU, 0);
self.draw_menu_bar_to_fb(bus);
} else {
self.restore_visible_dialog_snapshots(bus);
}
self.restore_menu_definition_port(cpu, bus);
self.finish_menu_no_hit(bus, cpu, self.menu_tracking_stack_ptr, result_offset);
}
fn finish_custom_menu_tracking<C: CpuOps>(
&mut self,
cpu: &mut C,
bus: &mut MacMemoryBus,
result_offset: u32,
result: u32,
) {
self.clear_active_menu_definition();
let kind = self.menu_tracking.as_ref().map(|tracking| tracking.kind);
if let Some(saved) = self.menu_tracking.take() {
self.restore_menu_tracking_pixels(bus, saved);
}
if kind == Some(MenuTrackingKind::MenuBar) {
bus.write_word(addr::THE_MENU, (result >> 16) as u16);
self.draw_menu_bar_to_fb(bus);
} else {
self.restore_visible_dialog_snapshots(bus);
}
self.restore_menu_definition_port(cpu, bus);
let sp = self.menu_tracking_stack_ptr;
bus.write_long(sp + result_offset, result);
cpu.write_reg(Register::A7, sp + result_offset);
}
pub(crate) fn guest_menu_snapshot(&mut self, bus: &MacMemoryBus) -> GuestMenuSnapshot {
self.current_menu_list(bus)
.unwrap_or_default()
.guest_snapshot(|menu_handle| {
let menu = bus.read_long(menu_handle);
if menu == 0 {
return None;
}
let title_len = usize::from(bus.read_byte(menu + 14));
Some(SharedMenuSnapshotRecord {
id: bus.read_word(menu) as i16,
title: bus.read_bytes(menu + 15, title_len),
items: menu_items_from_memory(bus, menu_handle)?,
})
})
}
pub(crate) fn queue_native_menu_selection(
&mut self,
bus: &MacMemoryBus,
menu_id: i16,
item_number: i16,
) -> Option<(i16, i16)> {
self.guest_menu_snapshot(bus)
.selectable_result(menu_id, item_number)?;
let region = self
.current_menu_list(bus)?
.regular_title_regions()
.into_iter()
.next()?;
let selection = (menu_id, item_number);
if self.pending_native_menu_selection.stage(selection) {
self.pending_native_menu_event = Some(super::dispatch::QueuedEvent {
what: 1,
message: 0,
where_v: 10,
where_h: region.left + (region.right - region.left) / 2,
modifiers: self.current_event_modifiers(),
});
self.pending_native_menu_event_tick = None;
}
Some((10, region.left + (region.right - region.left) / 2))
}
fn take_native_menu_selection(&mut self, bus: &MacMemoryBus) -> Option<u32> {
let (menu_id, item_number) = self.pending_native_menu_selection.take()?;
self.pending_native_menu_event = None;
self.pending_native_menu_event_tick = None;
self.guest_menu_snapshot(bus)
.selectable_result(menu_id, item_number)
}
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)
}
fn menu_def_proc_handle(&mut self, bus: &mut MacMemoryBus, mdef_id: i16) -> u32 {
if let Some((refnum, ptr)) = self.find_or_load_resource_any(bus, *b"MDEF", mdef_id) {
return self.get_or_create_resource_handle_in_file(bus, *b"MDEF", mdef_id, ptr, refnum);
}
self.synthesize_system_mdef(bus, mdef_id)
.map(|ptr| self.get_or_create_resource_handle_in_file(bus, *b"MDEF", mdef_id, ptr, 0))
.unwrap_or(0)
}
fn load_menu_resource(&mut self, bus: &mut MacMemoryBus, menu_id: i16) -> u32 {
let Some((refnum, res_ptr)) = self.find_or_load_resource_any(bus, *b"MENU", menu_id) else {
bus.write_word(0x0A60, (-192i16) as u16);
return 0;
};
let handle =
self.get_or_create_resource_handle_in_file(bus, *b"MENU", menu_id, res_ptr, refnum);
if handle == 0 {
bus.write_word(0x0A60, (-192i16) as u16);
return 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 menu_proc_placeholder = bus.read_long(menu_ptr + 6);
if !self.loaded_handles.contains_key(&menu_proc_placeholder) {
let mdef_id = (menu_proc_placeholder >> 16) as u16 as i16;
let menu_proc = self.menu_def_proc_handle(bus, mdef_id);
if menu_proc == 0 {
bus.write_word(0x0A60, (-192i16) as u16);
return 0;
}
bus.write_long(menu_ptr + 6, menu_proc);
}
let mut parsed = parse_menu_resource(bus, menu_ptr, handle);
if let Some(existing) = self.menus.iter_mut().find(|menu| menu.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);
handle
}
fn calculate_standard_menu_size(&self, bus: &mut MacMemoryBus, menu_handle: u32) {
let Some(menu) = self.menus.iter().find(|menu| menu.handle == menu_handle) else {
return;
};
let menu_ptr = bus.read_long(menu_handle);
if menu_ptr == 0 {
return;
}
let (_base, _row_bytes, _screen_width, screen_height, _depth) = self.get_screen_params();
let menu_height = shared_standard_menu_height(
&self.menu_rows(bus, &menu.items),
screen_height.saturating_sub(bus.read_word(addr::MBAR_HEIGHT) as i16),
);
let menu_width = self.standard_menu_width(bus, &menu.items);
bus.write_word(menu_ptr + 2, menu_width as u16);
bus.write_word(menu_ptr + 4, menu_height as u16);
}
fn continue_menu_bar_build<C: CpuOps>(&mut self, cpu: &mut C, bus: &mut MacMemoryBus) {
loop {
let Some(step) = self
.pending_menu_bar_build
.as_mut()
.and_then(|pending| pending.build.next_step())
else {
return;
};
let menu_handle = match step {
SharedMenuBarBuildStep::Size(menu_handle) => menu_handle,
SharedMenuBarBuildStep::Complete(result_handle) => {
let Some(pending) = self.pending_menu_bar_build.take() else {
return;
};
let stack_ptr = pending.stack_ptr;
bus.write_long(stack_ptr + 2, result_handle);
cpu.write_reg(Register::A7, stack_ptr + 2);
return;
}
};
if self.arm_menu_definition_to(
cpu,
bus,
SharedMenuDefinitionInvocation::size(menu_handle),
cpu.read_reg(Register::PC).wrapping_sub(2),
) {
return;
}
self.calculate_standard_menu_size(bus, menu_handle);
}
}
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_loaded_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(macroman_to_string(&bytes));
}
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)
}
}
fn live_menu_color_table_bytes(bus: &MacMemoryBus) -> Vec<u8> {
Self::menu_color_table_bytes(bus, bus.read_long(addr::MENU_C_INFO))
}
fn menu_color_rgb_pixel_index(bus: &MacMemoryBus, rgb: [u16; 3]) -> Option<u8> {
Self::fb_main_screen_pixel_index_for_rgb(bus, rgb)
}
pub(super) fn menu_standard_pixel_index(
bus: &MacMemoryBus,
pixel_size: u16,
black: bool,
) -> Option<u8> {
if !matches!(pixel_size, 2 | 4 | 8) {
return None;
}
Self::menu_color_rgb_pixel_index(bus, if black { [0; 3] } else { [0xFFFF; 3] })
}
fn menu_set_standard_pixel(
bus: &mut MacMemoryBus,
screen: (u32, u32, u16, i16, i16),
x: i16,
y: i16,
black: bool,
) {
let (screen_base, row_bytes, pixel_size, screen_width, screen_height) = screen;
if let Some(pixel_index) = Self::menu_standard_pixel_index(bus, pixel_size, black) {
Self::fb_set_pixel_index(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
x,
y,
pixel_index,
);
} else {
Self::fb_set_pixel(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
x,
y,
black,
);
}
}
pub(super) fn menu_bar_background_pixel_index(
&self,
bus: &MacMemoryBus,
pixel_size: u16,
) -> Option<u8> {
if !matches!(pixel_size, 2 | 4 | 8) {
return None;
}
let bytes = Self::live_menu_color_table_bytes(bus);
Self::menu_color_rgb_pixel_index(bus, MenuColorTable::new(&bytes).menu_bar_background())
}
pub(super) fn menu_title_pixel_index(
bus: &MacMemoryBus,
menu_id: i16,
pixel_size: u16,
) -> Option<u8> {
if !matches!(pixel_size, 2 | 4 | 8) {
return None;
}
let bytes = Self::live_menu_color_table_bytes(bus);
Self::menu_color_rgb_pixel_index(bus, MenuColorTable::new(&bytes).title_foreground(menu_id))
}
pub(super) fn menu_title_background_pixel_index(
&self,
bus: &MacMemoryBus,
menu_id: i16,
pixel_size: u16,
) -> Option<u8> {
if !matches!(pixel_size, 2 | 4 | 8) {
return None;
}
let bytes = Self::live_menu_color_table_bytes(bus);
Self::menu_color_rgb_pixel_index(bus, MenuColorTable::new(&bytes).title_background(menu_id))
}
fn menu_dropdown_background_pixel_index(
bus: &MacMemoryBus,
menu_id: i16,
pixel_size: u16,
) -> Option<u8> {
if !matches!(pixel_size, 2 | 4 | 8) {
return None;
}
let bytes = Self::live_menu_color_table_bytes(bus);
Self::menu_color_rgb_pixel_index(
bus,
MenuColorTable::new(&bytes).dropdown_background(menu_id),
)
}
fn menu_item_component_pixel_index(
bus: &MacMemoryBus,
menu_id: i16,
menu_item: i16,
pixel_size: u16,
item_rgb_offset: u32,
) -> Option<u8> {
if !matches!(pixel_size, 2 | 4 | 8) {
return None;
}
let bytes = Self::live_menu_color_table_bytes(bus);
let colors = MenuColorTable::new(&bytes).item_colors(menu_id, menu_item);
let rgb = match item_rgb_offset {
4 => colors.mark,
10 => colors.name,
16 => colors.command,
_ => return None,
};
Self::menu_color_rgb_pixel_index(bus, rgb)
}
fn menu_item_background_pixel_index(
bus: &MacMemoryBus,
menu_id: i16,
menu_item: i16,
pixel_size: u16,
) -> Option<u8> {
if !matches!(pixel_size, 2 | 4 | 8) {
return None;
}
let bytes = Self::live_menu_color_table_bytes(bus);
let rgb = MenuColorTable::new(&bytes)
.item_colors(menu_id, menu_item)
.background;
Self::menu_color_rgb_pixel_index(bus, rgb)
}
pub(super) fn menu_dim_pixel_index(
bus: &MacMemoryBus,
pixel_size: u16,
content_index: Option<u8>,
background_index: Option<u8>,
) -> Option<u8> {
if pixel_size == 1 {
return None;
}
let resolve = |index: Option<u8>, default: [u16; 3]| match index {
Some(index) => Self::fb_main_screen_rgb_for_pixel_index(bus, index).unwrap_or(default),
None => default,
};
let background = resolve(background_index, [0xFFFF; 3]);
let content = resolve(content_index, [0; 3]);
let midpoint = MenuColorTable::dimmed(content, background);
let gray = Self::fb_main_screen_pixel_index_for_rgb(bus, midpoint)?;
let content_pixel = Self::fb_main_screen_pixel_index_for_rgb(bus, content);
let background_pixel = Self::fb_main_screen_pixel_index_for_rgb(bus, background);
if Some(gray) == content_pixel || Some(gray) == background_pixel {
return None;
}
Some(gray)
}
fn menu_hilite_pixel_indexes(
bus: &MacMemoryBus,
background_index: Option<u8>,
foreground_index: Option<u8>,
pixel_size: u16,
) -> Option<(u8, u8)> {
if !matches!(pixel_size, 2 | 4 | 8) {
return None;
}
let background =
background_index.or_else(|| Self::menu_color_rgb_pixel_index(bus, [0xFFFF; 3]))?;
let foreground =
foreground_index.or_else(|| Self::menu_color_rgb_pixel_index(bus, [0; 3]))?;
Some((background, foreground))
}
fn menu_hilited_pixel_index(pixel: u8, background: u8, foreground: u8) -> u8 {
standard_menu_highlighted_value(pixel, background, foreground)
}
fn hilite_packed_menu_pixel(
bus: &mut MacMemoryBus,
screen: (u32, u32, i16, i16),
pixel_size: u16,
x: i16,
y: i16,
hilite_indexes: (u8, u8),
) {
let (screen_base, row_bytes, screen_width, screen_height) = screen;
if x < 0 || x >= screen_width || y < 0 || y >= screen_height {
return;
}
let Some(pixel) = Self::fb_get_pixel_index(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
x,
y,
) else {
return;
};
let highlighted = Self::menu_hilited_pixel_index(pixel, hilite_indexes.0, hilite_indexes.1);
Self::fb_set_pixel_index(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
x,
y,
highlighted,
);
}
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_or_load_resource_any(bus, *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 new_bytes = shared_merge_menu_color_entries(¤t_bytes, entries);
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_or_load_resource_any(bus, *b"mctb", resource_id)
else {
return;
};
let resource_size = bus.get_alloc_size(resource_ptr).unwrap_or(0) as usize;
let entries = compiled_menu_color_entries(&bus.read_bytes(resource_ptr, resource_size));
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 filtered = filter_menu_color_entries(¤t_bytes, |menu_id, menu_item| {
keep(menu_id, menu_item)
});
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 current_menu_list(&self, bus: &MacMemoryBus) -> Option<SharedMenuList> {
menu_list_from_memory(bus, bus.read_long(addr::MENU_LIST))
}
pub(super) fn menu_index_for_handle(&self, menu_handle: u32) -> Option<usize> {
self.menus
.iter()
.position(|menu| menu.handle == menu_handle)
}
fn menu_handle_for_index(&self, menu_index: usize) -> Option<u32> {
self.menus.get(menu_index).map(|menu| menu.handle)
}
pub(super) fn current_menu_bar_highlight_index(&self, bus: &MacMemoryBus) -> Option<usize> {
let selected_menu_id = bus.read_word(addr::THE_MENU) as i16;
if selected_menu_id == 0 {
return None;
}
let (owner_handle, _owner_id) = self
.current_menu_list(bus)?
.owning_regular_menu(selected_menu_id, |handle| {
menu_key_menu_from_memory(bus, handle)
})?;
self.menus
.iter()
.position(|menu| menu.handle == owner_handle)
}
fn replace_current_menu_list(
&mut self,
bus: &mut MacMemoryBus,
menu_list: &SharedMenuList,
) -> bool {
let current_handle = bus.read_long(addr::MENU_LIST);
let installation =
install_menu_list_copy(current_handle, menu_list, |request| match request {
MenuListInstallRequest::Allocate { bytes } => {
let handle = self.alloc_handle_with_bytes(bus, bytes);
(handle != 0).then_some(handle).ok_or(())
}
MenuListInstallRequest::Replace { handle, bytes } => self
.replace_handle_bytes(bus, handle, bytes)
.then_some(handle)
.ok_or(()),
});
let Ok(installation) = installation else {
return false;
};
if installation.allocated {
bus.write_long(addr::MENU_LIST, installation.handle);
}
true
}
fn relayout_menu_list(&self, bus: &MacMemoryBus, menu_list: &mut SharedMenuList) {
menu_list.relayout_regular_titles(
STANDARD_MENU_BAR_FIRST_TITLE_LEFT,
STANDARD_MENU_BAR_TITLE_SPACING,
|handle| {
let menu_ptr = bus.read_long(handle);
(menu_ptr != 0)
.then(|| {
let title_len = bus.read_byte(menu_ptr + 14) as usize;
macroman_to_string(&bus.read_bytes(menu_ptr + 15, title_len))
})
.map(|title| Self::menu_title_advance(&title))
.unwrap_or(0)
},
);
}
fn mutate_menu_items(
&mut self,
bus: &mut MacMemoryBus,
menu_handle: u32,
mutation: impl FnOnce(&mut crate::menu_manager::MenuItems) -> bool,
) -> bool {
let menu_ptr = bus.read_long(menu_handle);
if menu_ptr == 0 {
return false;
}
let record_size = bus
.get_alloc_size(menu_ptr)
.unwrap_or_else(|| menu_resource_size(bus, menu_ptr) as u32);
let original = bus.read_bytes(menu_ptr, record_size as usize);
let Some(mut items) = crate::menu_manager::MenuItems::decode(&original) else {
return false;
};
if !mutation(&mut items) {
return false;
}
let Some(bytes) = items.rebuild(&original) else {
return false;
};
if record_size as usize >= bytes.len() {
bus.write_bytes(menu_ptr, &bytes);
} else if bus.is_foreign_ordinary_sparse_address(menu_handle)
&& bus.is_foreign_ordinary_sparse_address(menu_ptr)
{
self.pending_memory_effects.push(
crate::process_context::PendingHandleByteReplacement {
handle: menu_handle,
expected_ptr: menu_ptr,
replacement: bytes,
},
);
return true;
} else {
let mut allocation = bytes;
allocation.resize(allocation.len().max(256), 0);
if !self.replace_handle_bytes(bus, menu_handle, &allocation) {
return false;
}
}
if let Some(menu) = self
.menus
.iter_mut()
.find(|menu| menu.handle == menu_handle)
{
refresh_menu_from_memory(bus, menu);
} else {
let menu_ptr = bus.read_long(menu_handle);
if menu_ptr != 0 {
self.menus
.push(parse_menu_resource(bus, menu_ptr, menu_handle));
}
}
true
}
fn resource_menu_names(
&mut self,
bus: &mut MacMemoryBus,
requested_type: [u8; 4],
) -> Vec<Vec<u8>> {
self.res_load = true;
let resource_types = if requested_type == *b"FONT" {
[Some(*b"FOND"), Some(*b"FONT")]
} else {
[Some(requested_type), None]
};
let mut names = Vec::new();
for resource_type in resource_types.into_iter().flatten() {
for (refnum, id, name, ptr) in self.named_resource_records_of_type(resource_type) {
let _ =
self.get_or_create_resource_handle_in_file(bus, resource_type, id, ptr, refnum);
names.push(encode_mac_roman_lossy(&name));
}
}
if requested_type == *b"FONT" {
for &(id, name) in crate::quickdraw::fonts::FONT_NAMES {
if id != crate::quickdraw::fonts::FONT_APPLICATION {
names.push(encode_mac_roman_lossy(name));
}
}
}
names
}
fn refresh_menus_from_memory(&mut self, bus: &MacMemoryBus) {
let menu_list = self.current_menu_list(bus).unwrap_or_default();
self.apply_menu_list_membership(bus, &menu_list);
for menu in &mut self.menus {
refresh_menu_from_memory(bus, menu);
}
}
pub(crate) fn service_invalid_menu_bar(&mut self, bus: &mut MacMemoryBus) -> bool {
if !self.event_queue.take_menu_bar_invalidation() {
return false;
}
self.release_initial_menu_bar_kiosk();
self.refresh_menus_from_memory(bus);
self.draw_menu_bar_to_fb(bus);
true
}
fn apply_menu_list_membership(&mut self, bus: &MacMemoryBus, menu_list: &SharedMenuList) {
let mut previous = std::mem::take(&mut self.menus);
let mut current = Vec::with_capacity(previous.len());
for (handle, hierarchical) in menu_list
.regular_handles()
.map(|handle| (handle, false))
.chain(
menu_list
.hierarchical_handles()
.map(|handle| (handle, true)),
)
{
let mut menu =
if let Some(index) = previous.iter().position(|menu| menu.handle == handle) {
previous.remove(index)
} else {
let menu_ptr = bus.read_long(handle);
if menu_ptr == 0 {
continue;
}
parse_menu_resource(bus, menu_ptr, handle)
};
menu.in_menu_bar = true;
menu.hierarchical = hierarchical;
menu.visible_in_menu_bar = !hierarchical;
current.push(menu);
}
for mut menu in previous {
menu.in_menu_bar = false;
menu.hierarchical = false;
menu.visible_in_menu_bar = false;
current.push(menu);
}
self.menus = current;
}
fn refresh_menu_cache_from_list(&mut self, bus: &MacMemoryBus, menu_list: &SharedMenuList) {
self.apply_menu_list_membership(bus, menu_list);
for menu in &mut self.menus {
refresh_menu_from_memory(bus, menu);
}
}
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);
let menu_list = SharedMenuList::default();
if self.replace_current_menu_list(bus, &menu_list) {
self.apply_menu_list_membership(bus, &menu_list);
}
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_proc = self.menu_def_proc_handle(bus, 0);
let title_bytes = if title_ptr == 0 {
Vec::new()
} else {
let title_len = usize::from(bus.read_byte(title_ptr));
bus.read_bytes(title_ptr + 1, title_len)
};
let menu_record = new_standard_menu_record(menu_id, menu_proc, &title_bytes);
let menu_ptr = bus.alloc(menu_record.len().max(256) as u32);
let handle = bus.alloc(4);
bus.write_long(handle, menu_ptr);
self.ptr_to_handle.insert(menu_ptr, handle);
bus.write_bytes(menu_ptr, &menu_record);
let title = macroman_to_string(&title_bytes);
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 = self.load_menu_resource(bus, menu_id);
cpu.write_reg(Register::D0, bus.read_word(0x0A60) as i16 as i32 as u32);
bus.write_long(sp + 2, handle);
cpu.write_reg(Register::A7, sp + 2);
if handle != 0 {
if self.arm_menu_definition(
cpu,
bus,
SharedMenuDefinitionInvocation::size(handle),
) {
return Some(Ok(()));
}
self.calculate_standard_menu_size(bus, handle);
}
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));
}
self.mutate_menu_items(bus, menu_handle, |items| items.append_specs(&bytes));
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);
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);
} else {
let menu_id = bus.read_word(menu_ptr) as i16;
if let Some((_, res_ptr)) =
self.find_or_load_resource_any(bus, *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 = false;
menu.hierarchical = false;
menu.visible_in_menu_bar = false;
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 = macroman_to_string(&title_bytes);
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: false,
hierarchical: false,
visible_in_menu_bar: false,
});
}
}
}
}
}
}
let mut menu_list = self.current_menu_list(bus).unwrap_or_default();
if self.menus.iter().any(|menu| menu.handle == menu_handle)
&& menu_list.insert(menu_handle, before_id, |candidate| {
let menu_ptr = bus.read_long(candidate);
(menu_ptr != 0).then(|| bus.read_word(menu_ptr) as i16)
})
{
self.relayout_menu_list(bus, &mut menu_list);
if self.replace_current_menu_list(bus, &menu_list) {
self.apply_menu_list_membership(bus, &menu_list);
}
}
cpu.write_reg(Register::A7, sp + 6);
Ok(())
}
(true, 0x137) => {
self.event_queue.take_menu_bar_invalidation();
self.release_initial_menu_bar_kiosk();
self.refresh_menus_from_memory(bus);
self.draw_menu_bar_to_fb(bus);
Ok(())
}
(true, 0x134) => {
let mut menu_list = self.current_menu_list(bus).unwrap_or_default();
menu_list.clear_entries();
if self.replace_current_menu_list(bus, &menu_list) {
self.apply_menu_list_membership(bus, &menu_list);
}
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 = menu_list_from_memory(bus, bus.read_long(addr::MENU_LIST))
.and_then(|menu_list| {
menu_list.find_handle_by_id(menu_id, |handle| {
let menu_ptr = bus.read_long(handle);
(menu_ptr != 0).then(|| bus.read_word(menu_ptr) as i16)
})
})
.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 source_handle = bus.read_long(sp);
cpu.write_reg(Register::A7, sp + 4);
let Some(menu_list) = menu_list_from_memory(bus, source_handle) else {
return Some(Ok(()));
};
if self.replace_current_menu_list(bus, &menu_list) {
self.refresh_menu_cache_from_list(bus, &menu_list);
}
Ok(())
}
(true, 0x1C0) => {
if self.pending_menu_bar_build.is_some() {
self.continue_menu_bar_build(cpu, bus);
return Some(Ok(()));
}
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_or_load_resource_any(bus, *b"MBAR", mbar_id)
{
let mbar_size = bus.get_alloc_size(mbar_ptr).unwrap_or(0) as usize;
let Some(mbar) =
SharedMenuBarResource::decode(&bus.read_bytes(mbar_ptr, mbar_size))
else {
bus.write_long(sp + 2, 0);
cpu.write_reg(Register::A7, sp + 2);
return Some(Ok(()));
};
self.clear_menu_color_table_entries(bus);
let menu_handles = mbar.load_regular_handles(|menu_id| {
let handle = self.load_menu_resource(bus, menu_id);
(handle != 0).then_some(handle)
});
let mut menu_list =
SharedMenuList::from_regular_handles(mbar_id, menu_handles.iter().copied());
self.relayout_menu_list(bus, &mut menu_list);
let list_bytes = menu_list.encode();
let list_block = bus.alloc(list_bytes.len() as u32);
bus.write_bytes(list_block, &list_bytes);
let list_handle = bus.alloc(4);
bus.write_long(list_handle, list_block);
self.pending_menu_bar_build = Some(PendingMenuBarBuild {
stack_ptr: sp,
build: SharedMenuBarBuild::new(list_handle, menu_handles),
});
list_handle
} else {
0
};
if handle != 0 && self.pending_menu_bar_build.is_some() {
self.continue_menu_bar_build(cpu, bus);
} else {
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 names = self.resource_menu_names(bus, res_type);
self.mutate_menu_items(bus, menu_handle, |items| {
items.insert_resource_names(names, i16::MAX)
});
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);
self.mutate_menu_items(bus, menu_handle, |items| items.set_enabled(item, false));
if let Some(menu) = self.menus.iter().find(|m| m.handle == menu_handle) {
if std::env::var_os("SYSTEMLESS_TRACE_MENUKEY").is_some() {
eprintln!(
"[MENUKEY] DisableItem menu={} title=\"{}\" item={} enabled={}",
menu.id, menu.title, item, menu.enabled
);
}
}
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);
self.mutate_menu_items(bus, menu_handle, |items| items.set_enabled(item, true));
if let Some(menu) = self.menus.iter().find(|m| m.handle == menu_handle) {
if std::env::var_os("SYSTEMLESS_TRACE_MENUKEY").is_some() {
eprintln!(
"[MENUKEY] EnableItem menu={} title=\"{}\" item={} enabled={}",
menu.id, menu.title, item, menu.enabled
);
}
}
Ok(())
}
(true, 0x13D) => {
if self
.menu_tracking
.as_ref()
.is_some_and(|tracking| tracking.front_buffer.is_some())
{
let sp = cpu.read_reg(Register::A7);
self.finish_menu_no_hit(bus, cpu, sp, 4);
return Some(Ok(()));
}
if self.menu_tracking.is_none() && self.pending_native_menu_selection.is_some() {
let sp = cpu.read_reg(Register::A7);
let result = self.take_native_menu_selection(bus).unwrap_or(0);
if let Some(index) = self.event_queue.iter().position(|event| event.what == 2) {
self.event_queue.remove(index);
}
bus.write_long(sp + 4, result);
cpu.write_reg(Register::A7, sp + 4);
self.record_menuselect_input_trace(
"native",
None,
None,
Some((result & 0xFFFF) as i16),
Some(result),
if result == 0 {
"native_selection_invalidated"
} else {
"native_selection"
},
);
return Some(Ok(()));
}
if self.menu_tracking.is_some() {
if self.active_menu_definition().is_some() {
let completed = self.complete_pending_menu_definition(cpu, bus);
if completed == Some(crate::menu_manager::MenuDefinitionMessage::Choose) {
let active_submenu = self.menu_tracking.as_ref().and_then(|tracking| {
match tracking.active_definition_pane() {
Some(SharedMenuDefinitionPane::Submenu(depth)) => tracking
.submenus
.get(depth)
.map(|submenu| submenu.dropdown_rect()),
_ => None,
}
});
if let Some((top, left, bottom, right)) = active_submenu {
let (mv, mh) = self.menu_tracking_mouse_pos(bus);
if mv < top || mv >= bottom || mh < left || mh >= right {
self.update_menu_tracking_for_point(bus, mh, mv);
if self.active_menu_definition().is_none() {
self.restore_menu_definition_port(cpu, bus);
return Some(Ok(()));
}
if self.active_menu_definition().is_some_and(|definition| {
definition.pending_invocation().is_some()
}) {
if !self.arm_pending_menu_definition(
cpu,
bus,
cpu.read_reg(Register::PC).wrapping_sub(2),
) {
self.abort_custom_menu_tracking(cpu, bus, 4);
}
return Some(Ok(()));
}
}
}
}
if self
.menu_tracking
.as_ref()
.is_some_and(|tracking| tracking.flash_remaining > 0)
{
let tracking = self.menu_tracking.as_mut().unwrap();
if tracking.flash_delay > 0 {
tracking.flash_delay -= 1;
return Some(Ok(()));
}
tracking.flash_remaining -= 1;
tracking.flash_delay = STANDARD_MENU_FLASH_PHASE_DELAY;
let remaining = tracking.flash_remaining;
let result = tracking.flash_result;
if remaining == 0 {
self.finish_custom_menu_tracking(cpu, bus, 4, result);
return Some(Ok(()));
}
self.active_menu_definition_mut()
.unwrap()
.flash(remaining & 1 == 0);
if !self.arm_pending_menu_definition(
cpu,
bus,
cpu.read_reg(Register::PC).wrapping_sub(2),
) {
self.abort_custom_menu_tracking(cpu, bus, 4);
}
return Some(Ok(()));
}
let (mv, mh) = self.menu_tracking_mouse_pos(bus);
if self.menu_tracking_button_down(bus) {
let new_menu = self.current_menu_title_hit_test(bus, mh);
let active_menu = self.menu_tracking.as_ref().unwrap().menu_handle;
let mbar_h = bus.read_word(addr::MBAR_HEIGHT) as i16;
if let Some(new_idx) = new_menu {
if self.menu_handle_for_index(new_idx) != Some(active_menu)
&& mv < mbar_h
{
let old_saved = self.menu_tracking.take().unwrap();
let sp = self.menu_tracking_stack_ptr;
self.clear_active_menu_definition();
self.restore_menu_tracking_pixels(bus, old_saved);
if self.open_menu_dropdown(bus, new_idx, sp) {
self.prepare_menu_definition_port(cpu, bus);
if !self.arm_pending_menu_definition(
cpu,
bus,
cpu.read_reg(Register::PC).wrapping_sub(2),
) {
self.abort_custom_menu_tracking(cpu, bus, 4);
}
} else {
self.restore_menu_definition_port(cpu, bus);
}
return Some(Ok(()));
}
}
}
let hit_point = (u32::from(mv as u16) << 16) | u32::from(mh as u16);
let choose_armed = self
.active_menu_definition_mut()
.and_then(|tracking| tracking.choose(hit_point))
.is_some();
if choose_armed {
if !self.arm_pending_menu_definition(
cpu,
bus,
cpu.read_reg(Register::PC).wrapping_sub(2),
) {
self.abort_custom_menu_tracking(cpu, bus, 4);
}
return Some(Ok(()));
}
if !self.menu_tracking_button_down(bus) {
let definition = *self.active_menu_definition().unwrap();
let item = definition.which_item();
let menu_handle = definition.menu_handle();
let menu_id = bus.read_long(menu_handle);
let result = if item > 0 && menu_id != 0 {
(u32::from(bus.read_word(menu_id)) << 16) | u32::from(item as u16)
} else {
0
};
if result == 0 {
self.finish_custom_menu_tracking(cpu, bus, 4, 0);
} else {
let tracking = self.menu_tracking.as_mut().unwrap();
let flash_enabled = tracking.begin_flash(
bus.read_word(crate::memory::globals::addr::MENU_FLASH),
result,
);
if !flash_enabled {
self.finish_custom_menu_tracking(cpu, bus, 4, result);
}
}
}
return Some(Ok(()));
}
if self.menu_tracking.as_ref().unwrap().flash_remaining > 0 {
let result = self.menu_tracking.as_ref().unwrap().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 = STANDARD_MENU_FLASH_PHASE_DELAY;
let new_remaining = t.flash_remaining;
if new_remaining == 0 {
let sp = self.menu_tracking_stack_ptr;
let saved = self.menu_tracking.take().unwrap();
let active_menu = saved
.submenus
.last()
.map(|submenu| submenu.menu_handle)
.unwrap_or(saved.menu_handle);
let highlighted_item = saved
.submenus
.last()
.map(|submenu| submenu.highlighted_item)
.unwrap_or(saved.highlighted_item);
self.restore_menu_tracking_pixels(bus, saved);
bus.write_word(addr::THE_MENU, (result >> 16) as u16);
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,
self.menu_index_for_handle(active_menu),
Some(highlighted_item),
Some(result),
"enabled_item_selected",
);
}
} else if !self.menu_tracking_button_down(bus) {
let (mv, mh) = self.menu_tracking_mouse_pos(bus);
self.update_menu_tracking_for_point(bus, mh, mv);
if self
.active_menu_definition()
.is_some_and(|definition| definition.pending_invocation().is_some())
{
self.prepare_menu_definition_port(cpu, bus);
if !self.arm_pending_menu_definition(
cpu,
bus,
cpu.read_reg(Register::PC).wrapping_sub(2),
) {
self.abort_custom_menu_tracking(cpu, bus, 4);
}
return Some(Ok(()));
}
let result = self.menu_tracking_selection_result(bus);
if result != 0 {
let (active_menu, item_idx) = self
.menu_tracking
.as_ref()
.and_then(|tracking| {
tracking
.submenus
.last()
.filter(|submenu| submenu.highlighted_item > 0)
.map(|submenu| {
(submenu.menu_handle, submenu.highlighted_item)
})
.or_else(|| {
(tracking.highlighted_item > 0).then_some((
tracking.menu_handle,
tracking.highlighted_item,
))
})
})
.unwrap_or((0, 0));
let tracking = self.menu_tracking.as_mut().unwrap();
let flash_enabled = tracking.begin_flash(
bus.read_word(crate::memory::globals::addr::MENU_FLASH),
result,
);
self.record_menuselect_input_trace(
"release",
None,
self.menu_index_for_handle(active_menu),
Some(item_idx),
Some(result),
if !flash_enabled {
"blink_disabled"
} else {
"start_flash"
},
);
if !flash_enabled {
self.finish_custom_menu_tracking(cpu, bus, 4, result);
}
} else {
let (sp, active_menu) = self
.menu_tracking
.as_ref()
.map(|tracking| {
(self.menu_tracking_stack_ptr, tracking.menu_handle)
})
.unwrap();
let saved = self.menu_tracking.take().unwrap();
self.restore_menu_tracking_pixels(bus, saved);
bus.write_word(addr::THE_MENU, 0);
self.draw_menu_bar_to_fb(bus);
self.finish_menu_no_hit(bus, cpu, sp, 4);
self.record_menuselect_input_trace(
"release",
None,
self.menu_index_for_handle(active_menu),
Some(0),
Some(0),
"no_selection",
);
}
} else {
let (mv, mh) = self.menu_tracking_mouse_pos(bus);
let new_menu = self.current_menu_title_hit_test(bus, 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 self.menu_handle_for_index(new_idx) != Some(tracking.menu_handle)
&& mv < mbar_h
{
let old_saved = self.menu_tracking.take().unwrap();
let sp = self.menu_tracking_stack_ptr;
self.restore_menu_tracking_pixels(bus, old_saved);
if self.open_menu_dropdown(bus, new_idx, sp) {
self.prepare_menu_definition_port(cpu, bus);
if !self.arm_pending_menu_definition(
cpu,
bus,
cpu.read_reg(Register::PC).wrapping_sub(2),
) {
self.abort_custom_menu_tracking(cpu, bus, 4);
}
} else {
self.restore_menu_definition_port(cpu, bus);
}
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
.submenus
.last()
.map(|submenu| (submenu.menu_handle, submenu.highlighted_item))
.unwrap_or((tracking.menu_handle, tracking.highlighted_item))
});
self.update_menu_tracking_for_point(bus, mh, mv);
if self
.active_menu_definition()
.is_some_and(|definition| definition.pending_invocation().is_some())
{
self.prepare_menu_definition_port(cpu, bus);
if !self.arm_pending_menu_definition(
cpu,
bus,
cpu.read_reg(Register::PC).wrapping_sub(2),
) {
self.abort_custom_menu_tracking(cpu, bus, 4);
}
return Some(Ok(()));
}
let new_trace = self.menu_tracking.as_ref().map(|tracking| {
tracking
.submenus
.last()
.map(|submenu| (submenu.menu_handle, submenu.highlighted_item))
.unwrap_or((tracking.menu_handle, 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,
self.menu_index_for_handle(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;
self.refresh_menus_from_memory(bus);
if let Some(menu_idx) = self.current_menu_title_hit_test(bus, pt_h) {
self.record_menuselect_input_trace(
"start",
Some((pt_v, pt_h)),
Some(menu_idx),
Some(0),
None,
"open_tracking",
);
if self.open_menu_dropdown(bus, menu_idx, sp) {
self.prepare_menu_definition_port(cpu, bus);
if !self.arm_pending_menu_definition(
cpu,
bus,
cpu.read_reg(Register::PC).wrapping_sub(2),
) {
self.abort_custom_menu_tracking(cpu, bus, 4);
}
}
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 self
.menu_tracking
.as_ref()
.is_some_and(|tracking| tracking.front_buffer.is_some())
{
let sp = cpu.read_reg(Register::A7);
self.finish_menu_no_hit(bus, cpu, sp, 10);
return Some(Ok(()));
}
if self.menu_tracking.is_some() {
if self.active_menu_definition().is_some() {
let completed = self.complete_pending_menu_definition(cpu, bus);
if completed == Some(crate::menu_manager::MenuDefinitionMessage::PopUp) {
let rect = self.active_menu_definition().unwrap().menu_rect();
if rect.2 <= rect.0 || rect.3 <= rect.1 {
self.abort_custom_menu_tracking(cpu, bus, 10);
return Some(Ok(()));
}
let menu_handle = self.menu_tracking.as_ref().unwrap().menu_handle;
let Some(menu_idx) = self.menu_index_for_handle(menu_handle) else {
self.abort_custom_menu_tracking(cpu, bus, 10);
return Some(Ok(()));
};
self.restore_visible_dialog_snapshots(bus);
let saved = self.save_dropdown_pixels(bus, rect);
let mut tracking = tracked_menu_state(
MenuTrackingKind::PopUp,
menu_handle,
rect,
saved,
);
tracking.definition = self.menu_definition_tracking.take();
self.menu_tracking = Some(tracking);
self.draw_menu_dropdown_chrome(bus, menu_idx, rect);
self.active_menu_definition_mut().unwrap().draw();
if !self.arm_pending_menu_definition(
cpu,
bus,
cpu.read_reg(Register::PC).wrapping_sub(2),
) {
self.abort_custom_menu_tracking(cpu, bus, 10);
}
return Some(Ok(()));
}
if self
.menu_tracking
.as_ref()
.is_some_and(|tracking| tracking.flash_remaining > 0)
{
let tracking = self.menu_tracking.as_mut().unwrap();
if tracking.flash_delay > 0 {
tracking.flash_delay -= 1;
return Some(Ok(()));
}
tracking.flash_remaining -= 1;
tracking.flash_delay = STANDARD_MENU_FLASH_PHASE_DELAY;
let remaining = tracking.flash_remaining;
let result = tracking.flash_result;
if remaining == 0 {
self.finish_custom_menu_tracking(cpu, bus, 10, result);
return Some(Ok(()));
}
self.active_menu_definition_mut()
.unwrap()
.flash(remaining & 1 == 0);
if !self.arm_pending_menu_definition(
cpu,
bus,
cpu.read_reg(Register::PC).wrapping_sub(2),
) {
self.abort_custom_menu_tracking(cpu, bus, 10);
}
return Some(Ok(()));
}
let (mv, mh) = self.menu_tracking_mouse_pos(bus);
let hit_point = (u32::from(mv as u16) << 16) | u32::from(mh as u16);
let choose_armed = self
.active_menu_definition_mut()
.and_then(|tracking| tracking.choose(hit_point))
.is_some();
if choose_armed {
if !self.arm_pending_menu_definition(
cpu,
bus,
cpu.read_reg(Register::PC).wrapping_sub(2),
) {
self.abort_custom_menu_tracking(cpu, bus, 10);
}
return Some(Ok(()));
}
if !self.menu_tracking_button_down(bus) {
let definition = *self.active_menu_definition().unwrap();
let item = definition.which_item();
let menu_ptr = bus.read_long(definition.menu_handle());
let result = if item > 0 && menu_ptr != 0 {
(u32::from(bus.read_word(menu_ptr)) << 16) | u32::from(item as u16)
} else {
0
};
if result == 0 {
self.finish_custom_menu_tracking(cpu, bus, 10, 0);
} else {
let tracking = self.menu_tracking.as_mut().unwrap();
let flash_enabled = tracking.begin_flash(
bus.read_word(crate::memory::globals::addr::MENU_FLASH),
result,
);
if !flash_enabled {
self.finish_custom_menu_tracking(cpu, bus, 10, result);
}
}
}
return Some(Ok(()));
}
if self.menu_tracking.as_ref().unwrap().flash_remaining > 0 {
let result = self.menu_tracking.as_ref().unwrap().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 = STANDARD_MENU_FLASH_PHASE_DELAY;
let new_remaining = t.flash_remaining;
if new_remaining == 0 {
let sp = self.menu_tracking_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 (mv, mh) = self.menu_tracking_mouse_pos(bus);
self.update_menu_tracking_for_point(bus, mh, mv);
let result = self.menu_tracking_selection_result(bus);
if result != 0 {
let tracking = self.menu_tracking.as_mut().unwrap();
let flash_enabled = tracking.begin_flash(
bus.read_word(crate::memory::globals::addr::MENU_FLASH),
result,
);
if !flash_enabled {
self.finish_custom_menu_tracking(cpu, bus, 10, result);
}
} else {
let sp = self.menu_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);
if !self.menu_tracking_button_down(bus) {
self.finish_menu_no_hit(bus, cpu, sp, 10);
return Some(Ok(()));
}
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.handle == menu_handle || m.id == menu_id)
{
if !self.menu_uses_standard_definition(bus, menu_ptr) {
self.menu_tracking = Some(tracked_menu_state(
MenuTrackingKind::PopUp,
menu_handle,
(0, 0, 0, 0),
Vec::new(),
));
self.menu_tracking_stack_ptr = sp;
let hit_point = (u32::from(top as u16) << 16) | u32::from(left as u16);
self.menu_definition_tracking =
Some(SharedMenuDefinitionTracking::begin_popup(
menu_handle,
hit_point,
popup_item,
));
self.prepare_menu_definition_port(cpu, bus);
if !self.arm_pending_menu_definition(
cpu,
bus,
cpu.read_reg(Register::PC).wrapping_sub(2),
) {
self.abort_custom_menu_tracking(cpu, bus, 10);
}
return Some(Ok(()));
}
self.preload_menu_item_icon_resources(bus, menu_idx);
let Some((dd_rect, highlighted_item, content_top)) =
self.popup_menu_dropdown_rect(bus, menu_idx, top, left, popup_item)
else {
self.finish_menu_no_hit(bus, cpu, sp, 10);
return Some(Ok(()));
};
self.restore_visible_dialog_snapshots(bus);
let saved = self.save_dropdown_pixels(bus, dd_rect);
self.menu_tracking = Some(tracked_menu_state_with_content_top(
MenuTrackingKind::PopUp,
menu_handle,
dd_rect,
content_top,
saved,
));
let rows = self.menu_rows(bus, &self.menus[menu_idx].items);
Self::write_menu_scrolling_globals(bus, &rows, content_top);
self.menu_tracking_stack_ptr = sp;
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 requested_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_menu_tracking_pixels(bus, tracking);
}
let target_menu_id = self
.current_menu_list(bus)
.and_then(|menu_list| {
menu_list.find_regular_handle_by_id(requested_menu_id, |handle| {
let menu = bus.read_long(handle);
(menu != 0).then(|| bus.read_word(menu) as i16)
})
})
.map_or(0, |_| requested_menu_id);
bus.write_word(addr::THE_MENU, target_menu_id as u16);
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 selection = self.current_menu_list(bus).and_then(|menu_list| {
menu_list.menu_key_selection(ch, |menu_handle| {
menu_key_menu_from_memory(bus, menu_handle)
})
});
let result = selection.map_or(0, |selection| selection.packed_result());
bus.write_word(addr::THE_MENU, (result >> 16) as u16);
if !self.menus.is_empty() {
self.draw_menu_bar_to_fb(bus);
}
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);
self.mutate_menu_items(bus, menu_handle, |items| {
items.set_mark(item, 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));
}
self.mutate_menu_items(bus, menu_handle, |items| {
items.set_text(item, &text_bytes)
});
}
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);
let mut menu_list = self.current_menu_list(bus).unwrap_or_default();
if menu_list
.remove_by_id(menu_id, |candidate| {
let menu_ptr = bus.read_long(candidate);
(menu_ptr != 0).then(|| bus.read_word(menu_ptr) as i16)
})
.is_some()
{
self.relayout_menu_list(bus, &mut menu_list);
if self.replace_current_menu_list(bus, &menu_list) {
self.apply_menu_list_membership(bus, &menu_list);
}
}
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);
self.mutate_menu_items(bus, menu_handle, |items| items.set_icon(item, 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);
self.mutate_menu_items(bus, menu_handle, |items| items.set_style(item, 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);
self.mutate_menu_items(bus, menu_handle, |items| items.set_mark(item, 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);
let menu_ptr = bus.read_long(menu_handle);
if menu_ptr != 0
&& self.arm_menu_definition(
cpu,
bus,
SharedMenuDefinitionInvocation::size(menu_handle),
)
{
return Some(Ok(()));
}
self.calculate_standard_menu_size(bus, menu_handle);
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 names = self.resource_menu_names(bus, res_type);
self.mutate_menu_items(bus, menu_handle, |items| {
items.insert_resource_names(names, after_item)
});
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 menu_id = bus.read_word(bus.read_long(menu_handle)) as i16;
if self.mutate_menu_items(bus, menu_handle, |items| items.delete(item)) {
self.filter_menu_color_table_entries(bus, |id, item_no| {
!(id == menu_id && item_no == 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));
}
self.mutate_menu_items(bus, menu_handle, |items| {
items.insert_specs(&bytes, after_item)
});
Ok(())
}
(true, 0x008) => {
let sp = cpu.read_reg(Register::A7);
cpu.write_reg(Register::A7, sp + 2);
Ok(())
}
(true, 0x01D) => {
self.event_queue.invalidate_menu_bar();
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(()));
}
self.mutate_menu_items(bus, menu_handle, |items| items.set_command(item, cmd_char));
Ok(())
}
(true, 0x13B) => {
let sp = cpu.read_reg(Register::A7);
let menu_list =
menu_list_from_memory(bus, bus.read_long(addr::MENU_LIST)).unwrap_or_default();
let bytes = menu_list.encode();
let block = bus.alloc(bytes.len() as u32);
bus.write_bytes(block, &bytes);
let handle = bus.alloc(4);
bus.write_long(handle, block);
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 {
hierarchical_menu_id(item.key_equiv, item.mark).is_some()
}
fn menu_item_has_command_key(item: &MenuItem) -> bool {
item.key_equiv > 0x20
}
fn menu_cicn_layout(bus: &MacMemoryBus, icon_ptr: u32) -> Option<SharedColorIconLayout> {
SharedColorIconLayout::decode_with(
|offset| {
let offset = u32::try_from(offset).ok()?;
Some(bus.read_byte(icon_ptr.checked_add(offset)?))
},
bus.get_alloc_size(icon_ptr).map(|size| size as usize),
)
}
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))
}
fn menu_item_icon_kind(&self, bus: &MacMemoryBus, item: &MenuItem) -> StandardMenuIconKind {
standard_menu_icon_kind(
item.icon,
item.key_equiv,
self.menu_item_cicn_size(bus, item),
)
}
pub(super) fn menu_item_height(&self, bus: &MacMemoryBus, item: &MenuItem) -> i16 {
if item.text == "-" {
return STANDARD_MENU_SEPARATOR_HEIGHT;
}
self.menu_item_icon_kind(bus, item)
.row_height(QuickDrawTextStyle::from_bits(item.style))
}
pub(super) fn menu_items_height(&self, bus: &MacMemoryBus, items: &[MenuItem]) -> i16 {
self.menu_rows(bus, items).total_height()
}
pub(super) fn menu_rows(&self, bus: &MacMemoryBus, items: &[MenuItem]) -> SharedMenuRows {
SharedMenuRows::new(
Self::laid_out_items(items)
.iter()
.map(|item| SharedMenuRow {
height: self.menu_item_height(bus, item),
selectable: item.enabled && item.text != "-",
}),
)
}
fn menu_tracking_rows(&self, bus: &MacMemoryBus, menu: &Menu) -> SharedMenuRows {
let geometry = self.menu_rows(bus, &menu.items);
let live = menu_items_from_memory(bus, menu.handle);
let menu_enabled = live
.as_ref()
.is_some_and(|items| items.enable_flags & 1 != 0);
SharedMenuRows::new((0..geometry.len()).map(|index| {
let item_number = i16::try_from(index + 1).unwrap_or(i16::MAX);
SharedMenuRow {
height: geometry.height(item_number, 0),
selectable: live
.as_ref()
.is_some_and(|items| items.item_is_selectable(item_number)),
}
}))
.with_menu_enabled(menu_enabled)
}
fn laid_out_items(items: &[MenuItem]) -> &[MenuItem] {
let end = laid_out_menu_item_count(items, |item| item.text == "-");
&items[..end]
}
fn menu_item_icon_width(&self, bus: &MacMemoryBus, item: &MenuItem) -> i16 {
self.menu_item_icon_kind(bus, item).width()
}
pub(super) fn standard_menu_width(&self, bus: &MacMemoryBus, items: &[MenuItem]) -> i16 {
shared_standard_menu_width(Self::laid_out_items(items).iter().map(|item| {
StandardMenuItemWidth {
text: shared_standard_menu_text_advance(&internal_menu_string_bytes(&item.text)),
icon: self.menu_item_icon_width(bus, item),
command: item.key_equiv,
}
}))
}
fn menu_item_icon_resource_id(item: &MenuItem) -> Option<i16> {
standard_menu_icon_resource_id(item.icon, item.key_equiv)
}
fn preload_menu_item_icon_resources(&mut self, bus: &mut MacMemoryBus, menu_idx: usize) {
let icon_ids: Vec<i16> = self
.menus
.get(menu_idx)
.map(|menu| {
menu.items
.iter()
.filter_map(Self::menu_item_icon_resource_id)
.collect()
})
.unwrap_or_default();
for icon_resource_id in icon_ids {
for res_type in [*b"cicn", *b"ICON", *b"SICN"] {
let _ = self.find_or_load_resource_any(bus, res_type, icon_resource_id);
}
}
}
fn cicn_menu_icon_resource_ptr(&self, item: &MenuItem) -> Option<u32> {
let icon_resource_id = Self::menu_item_icon_resource_id(item)?;
self.find_loaded_resource_any(*b"cicn", icon_resource_id)
.map(|(_, ptr)| ptr)
}
fn reduced_menu_icon_resource_ptr(&self, bus: &MacMemoryBus, item: &MenuItem) -> Option<u32> {
if self.menu_item_icon_kind(bus, item) != StandardMenuIconKind::Reduced {
return None;
}
let icon_resource_id = Self::menu_item_icon_resource_id(item)?;
self.find_loaded_resource_any(*b"ICON", icon_resource_id)
.map(|(_, ptr)| ptr)
}
fn small_menu_icon_resource_ptr(&self, bus: &MacMemoryBus, item: &MenuItem) -> Option<u32> {
if self.menu_item_icon_kind(bus, item) != StandardMenuIconKind::Small {
return None;
}
let icon_resource_id = Self::menu_item_icon_resource_id(item)?;
self.find_loaded_resource_any(*b"SICN", icon_resource_id)
.map(|(_, ptr)| ptr)
}
fn normal_menu_icon_resource_ptr(&self, bus: &MacMemoryBus, item: &MenuItem) -> Option<u32> {
if self.menu_item_icon_kind(bus, item) != StandardMenuIconKind::Normal {
return None;
}
let icon_resource_id = Self::menu_item_icon_resource_id(item)?;
self.find_loaded_resource_any(*b"ICON", icon_resource_id)
.map(|(_, ptr)| ptr)
}
fn draw_monochrome_menu_icon(
&self,
bus: &mut MacMemoryBus,
icon_ptr: u32,
top: i16,
left: i16,
kind: StandardMenuIconKind,
pixel_index_override: Option<u8>,
) {
let Some(layout) = SharedMonochromeMenuIconLayout::for_kind(
kind,
bus.get_alloc_size(icon_ptr).map(|size| size as usize),
) else {
return;
};
let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
self.get_screen_params();
for dy in 0..layout.height {
for dx in 0..layout.width {
let set = layout
.sample_with(
|offset| {
let offset = u32::try_from(offset).ok()?;
Some(bus.read_byte(icon_ptr.checked_add(offset)?))
},
dx,
dy,
)
.unwrap_or(false);
if 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 + i16::try_from(dx).unwrap_or(i16::MAX),
top + i16::try_from(dy).unwrap_or(i16::MAX),
pixel_index,
);
} else {
Self::fb_set_pixel(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
left + i16::try_from(dx).unwrap_or(i16::MAX),
top + i16::try_from(dy).unwrap_or(i16::MAX),
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();
let screen = (
screen_base,
row_bytes,
screen_pixel_size,
screen_width,
screen_height,
);
for dy in 0..layout.height {
for dx in 0..layout.width {
let (Ok(sx), Ok(sy)) = (usize::try_from(dx), usize::try_from(dy)) else {
continue;
};
let read = |offset: usize| {
let offset = u32::try_from(offset).ok()?;
Some(bus.read_byte(icon_ptr.checked_add(offset)?))
};
if !layout.mask_bit_with(read, sx, sy).unwrap_or(false) {
continue;
}
if matches!(screen_pixel_size, 2 | 4 | 8) {
let read = |offset: usize| {
let offset = u32::try_from(offset).ok()?;
Some(bus.read_byte(icon_ptr.checked_add(offset)?))
};
let Some(rgb) = layout.rgb_with(read, sx, sy) else {
continue;
};
let destination_index = Self::fb_main_screen_pixel_index_for_rgb(bus, rgb)
.unwrap_or_else(|| {
super::pict::closest_clut_index(
rgb[0],
rgb[1],
rgb[2],
&self.device_clut,
)
});
let dst_x = left + dx;
let dst_y = top + dy;
Self::fb_set_pixel_index(
bus,
screen_base,
row_bytes,
screen_pixel_size,
screen_width,
screen_height,
dst_x,
dst_y,
destination_index,
);
continue;
}
let read = |offset: usize| {
let offset = u32::try_from(offset).ok()?;
Some(bus.read_byte(icon_ptr.checked_add(offset)?))
};
let Some(set) = layout.monochrome_bit_with(read, sx, sy) else {
continue;
};
Self::menu_set_standard_pixel(bus, screen, left + dx, top + dy, set);
}
}
}
fn popup_menu_dropdown_rect(
&self,
bus: &MacMemoryBus,
menu_idx: usize,
top: i16,
left: i16,
popup_item: i16,
) -> Option<((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 width = self.standard_menu_width(bus, &menu.items);
let rows = self.menu_rows(bus, &menu.items);
let layout = standard_popup_menu_layout(
&rows,
width,
(screen_width, screen_height),
(top, left),
popup_item,
)?;
Some((layout.rect(), layout.highlighted_item, layout.content_top))
}
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;
};
min_width.max(self.standard_menu_width(bus, &menu.items))
}
fn open_menu_dropdown(
&mut self,
bus: &mut MacMemoryBus,
menu_idx: usize,
stack_ptr: u32,
) -> bool {
self.preload_menu_item_icon_resources(bus, menu_idx);
let (_screen_base, _row_bytes, screen_width, screen_height, _pixel_size) =
self.get_screen_params();
let region = self
.current_menu_title_regions_with_indices(bus)
.into_iter()
.find(|(idx, _region)| *idx == menu_idx);
if menu_idx >= self.menus.len() {
return false;
}
let Some((_idx, title_region)) = region else {
return false;
};
let menu = &self.menus[menu_idx];
let menu_id = menu.id;
let menu_ptr = bus.read_long(menu.handle);
let custom_definition = menu_ptr != 0 && !self.menu_uses_standard_definition(bus, menu_ptr);
let max_width = if custom_definition {
bus.read_word(menu_ptr + 2) as i16
} else {
self.dropdown_width_for_menu(bus, menu_idx, title_region.right - title_region.left + 20)
};
let content_height = if custom_definition {
bus.read_word(menu_ptr + 4) as i16
} else {
self.menu_items_height(bus, &menu.items)
};
let menu_bar_height = bus.read_word(addr::MBAR_HEIGHT) as i16;
let Some(layout) = standard_pull_down_menu_layout(
max_width,
content_height,
(screen_width, screen_height),
title_region.left,
menu_bar_height,
) else {
return false;
};
let dropdown_rect = layout.rect();
bus.write_word(addr::THE_MENU, menu_id as u16);
self.draw_menu_bar_to_fb(bus);
let saved = self.save_dropdown_pixels(bus, dropdown_rect);
if custom_definition {
self.draw_menu_dropdown_chrome(bus, menu_idx, dropdown_rect);
} else {
self.draw_menu_dropdown(bus, menu_idx, dropdown_rect);
}
let mut tracking =
tracked_menu_state(MenuTrackingKind::MenuBar, menu.handle, dropdown_rect, saved);
tracking.definition = custom_definition
.then(|| SharedMenuDefinitionTracking::begin_draw(menu.handle, dropdown_rect));
self.menu_tracking = Some(tracking);
if !custom_definition {
let rows = self.menu_rows(bus, &self.menus[menu_idx].items);
Self::write_menu_scrolling_globals(bus, &rows, dropdown_rect.0);
}
self.menu_tracking_stack_ptr = stack_ptr;
custom_definition
}
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) {
let root_rect = saved.dropdown_rect();
for submenu in saved.submenus.into_iter().rev() {
self.restore_dropdown_pixels(bus, submenu.dropdown_rect(), &submenu.saved_pixels);
}
self.restore_dropdown_pixels(bus, root_rect, &saved.saved_pixels);
}
fn menu_tracking_selection_result(&self, bus: &MacMemoryBus) -> u32 {
let Some(tracking) = self.menu_tracking.as_ref() else {
return 0;
};
tracking
.selection(|menu_handle, item_number| {
menu_items_from_memory(bus, menu_handle).is_some_and(|items| {
items.item_is_selectable(item_number)
&& !items.item_is_hierarchical(item_number)
})
})
.and_then(|(menu_handle, item_number)| {
let menu_ptr = bus.read_long(menu_handle);
(menu_ptr != 0).then(|| {
(u32::from(bus.read_word(menu_ptr)) << 16) | (u32::from(item_number as u16))
})
})
.unwrap_or(0)
}
fn submenu_menu_index_for_parent_item(
&self,
bus: &MacMemoryBus,
parent_menu_idx: usize,
parent_item: i16,
) -> Option<usize> {
if parent_item <= 0 {
return None;
}
let parent_menu_handle = self.menus.get(parent_menu_idx)?.handle;
let parent_items = menu_items_from_memory(bus, parent_menu_handle)?;
let submenu_handle = self.current_menu_list(bus)?.submenu_handle_for_item(
&parent_items,
parent_item,
|handle| {
let menu_ptr = bus.read_long(handle);
(menu_ptr != 0).then(|| bus.read_word(menu_ptr) as i16)
},
)?;
self.menus
.iter()
.position(|menu| menu.handle == submenu_handle)
}
fn submenu_rect_for_parent_item(
&self,
bus: &MacMemoryBus,
parent_menu_idx: usize,
parent_rect: (i16, i16, i16, i16),
submenu_idx: usize,
parent_item: i16,
) -> Option<(i16, i16, i16, i16)> {
let (_screen_base, _row_bytes, screen_width, screen_height, _pixel_size) =
self.get_screen_params();
let parent_menu = self.menus.get(parent_menu_idx)?;
let parent_offset = self.menu_rows(bus, &parent_menu.items).offset(parent_item);
let submenu = self.menus.get(submenu_idx)?;
let submenu_ptr = bus.read_long(submenu.handle);
let custom_definition =
submenu_ptr != 0 && !self.menu_uses_standard_definition(bus, submenu_ptr);
let (submenu_width, submenu_height) = if custom_definition {
(
bus.read_word(submenu_ptr + 2) as i16,
bus.read_word(submenu_ptr + 4) as i16,
)
} else {
(
self.dropdown_width_for_menu(bus, submenu_idx, 1),
self.menu_items_height(bus, &submenu.items),
)
};
standard_submenu_layout(
parent_rect,
parent_offset,
submenu_width,
submenu_height,
(screen_width, screen_height),
bus.read_word(addr::MBAR_HEIGHT) as i16,
)
.map(|layout| layout.rect())
}
fn ensure_submenu_for_request(&mut self, bus: &mut MacMemoryBus, request: SubmenuRequest<u32>) {
let resolved_child = self
.menu_index_for_handle(request.parent_handle)
.and_then(|parent_menu_idx| {
self.submenu_menu_index_for_parent_item(bus, parent_menu_idx, request.parent_item)
})
.and_then(|submenu_idx| self.menus.get(submenu_idx).map(|menu| menu.handle));
let reconciliation = self
.menu_tracking
.as_mut()
.map(|tracking| tracking.reconcile_submenu(request, resolved_child));
let (token, closed) = match reconciliation {
Some(SubmenuReconciliation::Stale | SubmenuReconciliation::Keep) | None => return,
Some(SubmenuReconciliation::Closed {
panes_deepest_first,
}) => {
for submenu in panes_deepest_first {
self.restore_dropdown_pixels(
bus,
submenu.dropdown_rect(),
&submenu.saved_pixels,
);
}
return;
}
Some(SubmenuReconciliation::Open {
token,
panes_deepest_first,
}) => (token, panes_deepest_first),
};
for submenu in closed {
self.restore_dropdown_pixels(bus, submenu.dropdown_rect(), &submenu.saved_pixels);
}
let request = token.request();
let submenu_handle = token.child_handle();
let Some(parent_rect) =
self.menu_tracking
.as_ref()
.and_then(|tracking| match request.parent {
MenuTrackingPane::Root => Some(tracking.dropdown_rect()),
MenuTrackingPane::Submenu(depth) => tracking
.submenus
.get(depth)
.map(|submenu| submenu.dropdown_rect()),
})
else {
return;
};
let Some(parent_menu_idx) = self.menu_index_for_handle(request.parent_handle) else {
return;
};
let Some(submenu_idx) = self.menu_index_for_handle(submenu_handle) else {
return;
};
let Some(dropdown_rect) = self.submenu_rect_for_parent_item(
bus,
parent_menu_idx,
parent_rect,
submenu_idx,
request.parent_item,
) else {
return;
};
let saved_pixels = self.save_dropdown_pixels(bus, dropdown_rect);
let submenu_ptr = bus.read_long(submenu_handle);
let custom_definition =
submenu_ptr != 0 && !self.menu_uses_standard_definition(bus, submenu_ptr);
let mut submenu = tracked_submenu_state(
submenu_handle,
request.parent_item,
dropdown_rect,
saved_pixels,
);
submenu.definition = custom_definition
.then(|| SharedMenuDefinitionTracking::begin_draw(submenu_handle, dropdown_rect));
let installed = self
.menu_tracking
.as_mut()
.is_some_and(|tracking| tracking.install_submenu(token, submenu).is_ok());
if !installed {
return;
}
if custom_definition {
self.draw_menu_dropdown_chrome(bus, submenu_idx, dropdown_rect);
} else {
self.draw_menu_dropdown(bus, submenu_idx, dropdown_rect);
}
}
fn write_standard_menu_choice(
&self,
bus: &mut MacMemoryBus,
menu_handle: u32,
item_number: i16,
) {
let menu_ptr = bus.read_long(menu_handle);
if menu_ptr == 0 {
return;
}
let menu_id = bus.read_word(menu_ptr) as i16;
bus.write_long(addr::MENU_DISABLE, menu_choice_value(menu_id, item_number));
}
fn update_menu_tracking_for_point(
&mut self,
bus: &mut MacMemoryBus,
mouse_x: i16,
mouse_y: i16,
) {
let point = (mouse_y, mouse_x);
let Some(menu_handle) = self
.menu_tracking
.as_ref()
.map(|tracking| tracking.menu_handle)
else {
return;
};
let Some(menu_idx) = self.menu_index_for_handle(menu_handle) else {
return;
};
let Some(menu) = self.menus.get(menu_idx) else {
return;
};
let root_rows = self.menu_tracking_rows(bus, menu);
let submenu_rows = self
.menu_tracking
.as_ref()
.map(|tracking| {
tracking
.submenus
.iter()
.map(|submenu| {
let menu_idx = self.menu_index_for_handle(submenu.menu_handle)?;
let menu = self.menus.get(menu_idx)?;
Some(self.menu_tracking_rows(bus, menu))
})
.collect::<Vec<_>>()
})
.unwrap_or_default();
let update = self
.menu_tracking
.as_mut()
.and_then(|tracking| tracking.track_standard_pointer(&root_rows, &submenu_rows, point));
let Some(update) = update else {
return;
};
let submenu_request = update.submenu_request();
self.write_standard_menu_choice(bus, update.menu_handle, update.pointer.menu_choice_item);
for submenu in update.closed_panes_deepest_first {
self.restore_dropdown_pixels(bus, submenu.dropdown_rect(), &submenu.saved_pixels);
}
let pane = match update.pane {
MenuTrackingPane::Root => self
.menu_tracking
.as_ref()
.map(|tracking| (menu_idx, tracking.dropdown_rect())),
MenuTrackingPane::Submenu(depth) => self
.menu_tracking
.as_ref()
.and_then(|tracking| tracking.submenus.get(depth))
.and_then(|submenu| {
self.menu_index_for_handle(submenu.menu_handle)
.map(|menu_idx| (menu_idx, submenu.dropdown_rect()))
}),
};
if update.previous_item != update.pointer.item {
if let Some((menu_idx, rect)) = pane {
self.draw_menu_dropdown(bus, menu_idx, rect);
}
}
if let Some(request) = submenu_request {
self.ensure_submenu_for_request(bus, request);
}
if update.pointer.scrolled {
if let Some((menu_idx, rect)) = pane {
self.draw_menu_dropdown(bus, menu_idx, rect);
}
}
Self::write_menu_scrolling_bounds(bus, update.content_top, update.content_bottom);
}
fn write_menu_scrolling_bounds(bus: &mut MacMemoryBus, content_top: i16, content_bottom: i16) {
bus.write_word(addr::TOP_MENU_ITEM, content_top as u16);
bus.write_word(addr::AT_MENU_BOTTOM, content_bottom as u16);
}
fn write_menu_scrolling_globals(
bus: &mut MacMemoryBus,
rows: &SharedMenuRows,
content_top: i16,
) {
Self::write_menu_scrolling_bounds(
bus,
content_top,
content_top.saturating_add(rows.total_height()),
);
}
pub(crate) fn current_menu_title_regions_with_indices(
&self,
bus: &MacMemoryBus,
) -> Vec<(usize, SharedMenuBarTitleRegion)> {
self.current_menu_list(bus)
.into_iter()
.flat_map(|menu_list| menu_list.regular_title_regions())
.filter_map(|region| {
self.menus
.iter()
.position(|menu| menu.handle == region.handle)
.map(|index| (index, region))
})
.collect()
}
fn current_menu_title_hit_test(&self, bus: &MacMemoryBus, mouse_x: i16) -> Option<usize> {
self.current_menu_title_regions_with_indices(bus)
.into_iter()
.find(|(_menu_idx, region)| region.contains_horizontal(mouse_x))
.map(|(menu_idx, _region)| menu_idx)
}
#[cfg(test)]
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()
}
#[cfg(test)]
fn menu_title_regions_with_indices(&self) -> Vec<(usize, i16, i16)> {
let mut regions = Vec::new();
let mut left = STANDARD_MENU_BAR_FIRST_TITLE_LEFT;
for (menu_idx, menu) in self.menus.iter().enumerate() {
if !menu.visible_in_menu_bar {
continue;
}
let width = Self::menu_title_advance(&menu.title);
let right = left
.saturating_add(width)
.saturating_add(STANDARD_MENU_BAR_TITLE_SPACING);
regions.push((menu_idx, left, right));
left = right;
}
regions
}
#[cfg(test)]
fn dropdown_item_at_point(&self, bus: &MacMemoryBus, mouse_x: i16, mouse_y: i16) -> i16 {
if let Some(ref tracking) = self.menu_tracking {
let Some(menu_idx) = self.menu_index_for_handle(tracking.menu_handle) else {
return 0;
};
let menu = &self.menus[menu_idx];
return self.menu_rows(bus, &menu.items).tracking_item_at_point(
tracking.dropdown_rect(),
tracking.content_top,
(mouse_y, mouse_x),
);
}
0
}
fn draw_menu_dropdown_chrome(
&self,
bus: &mut MacMemoryBus,
menu_idx: usize,
rect: (i16, i16, i16, i16),
) -> bool {
let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
self.get_screen_params();
let screen = (
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
);
let (top, left, bottom, right) = rect;
if menu_idx >= self.menus.len() {
return false;
}
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.menu_handle == menu.handle
&& 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,
);
}
let kind = if attached_pulldown {
StandardMenuPaneKind::PullDown
} else if detached_popup {
StandardMenuPaneKind::PopUp
} else {
StandardMenuPaneKind::Hierarchical
};
if let Some(chrome) = StandardMenuChrome::new(kind, rect) {
chrome.for_each_frame_pixel(|x, y| {
Self::menu_set_standard_pixel(bus, screen, x, y, true);
});
chrome.for_each_shadow_pixel(|x, y| {
Self::menu_set_standard_pixel(bus, screen, x, y, true);
});
}
}
attached_pulldown
}
pub(super) fn draw_menu_dropdown(
&self,
bus: &mut MacMemoryBus,
menu_idx: usize,
rect: (i16, i16, i16, i16),
) {
let attached_pulldown = self.draw_menu_dropdown_chrome(bus, menu_idx, rect);
let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
self.get_screen_params();
let screen = (
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
);
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 font_id: i16 = 0;
let font_size: i16 = 12;
let metrics = crate::quickdraw::text::get_font_metrics(font_id, font_size);
let (highlighted_item, content_top) = self
.menu_tracking
.as_ref()
.and_then(|tracking| {
if tracking.menu_handle == menu.handle && tracking.dropdown_rect() == rect {
Some((tracking.highlighted_item, tracking.content_top))
} else {
tracking
.submenus
.iter()
.find(|submenu| {
submenu.menu_handle == menu.handle && submenu.dropdown_rect() == rect
})
.map(|submenu| (submenu.highlighted_item, submenu.content_top))
}
})
.or_else(|| {
self.control_tracking
.as_ref()
.filter(|tracking| {
tracking.active_menu == menu_idx && tracking.dropdown_rect == rect
})
.map(|tracking| (tracking.highlighted_item, top))
})
.or_else(|| {
self.dialog_tracking
.as_ref()
.and_then(|tracking| tracking.active_popup.as_ref())
.filter(|popup| popup.active_menu == menu_idx && popup.dropdown_rect == rect)
.map(|popup| (popup.highlighted_item, top))
})
.unwrap_or((0, top));
let rows = self.menu_rows(bus, &menu.items);
let (scroll_up, scroll_down) = rows.scroll_indicators(rect, content_top);
let visible_item_top = top.saturating_add(if scroll_up { MENU_ROW_HEIGHT } else { 0 });
let visible_item_bottom =
bottom.saturating_sub(if scroll_down { MENU_ROW_HEIGHT } else { 0 });
let mut item_top = content_top;
for (i, item) in Self::laid_out_items(&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.saturating_add(item_height);
if item_bottom <= top {
item_top = item_bottom;
continue;
}
if item_top >= bottom {
break;
}
if item_top < visible_item_top || item_bottom > visible_item_bottom {
item_top = item_bottom;
continue;
}
let is_separator = item.text == "-";
let row_enabled = menu.enabled && item.enabled;
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 classic_selected = self.ui_theme_id() == UiThemeId::ClassicSystem7
&& highlighted_item == item_no
&& row_enabled
&& !is_separator;
let selected_mono = classic_selected && pixel_size == 1;
let selected_colors =
(classic_selected && matches!(pixel_size, 2 | 4 | 8)).then(|| {
let selected_background = name_pixel_index
.or_else(|| Self::menu_color_rgb_pixel_index(bus, [0; 3]))
.unwrap_or(255);
let selected_foreground =
Self::menu_item_background_pixel_index(bus, menu.id, item_no, pixel_size)
.or_else(|| Self::menu_color_rgb_pixel_index(bus, [0xFFFF; 3]))
.unwrap_or(0);
(selected_background, selected_foreground)
});
if let Some((selected_background, _)) = selected_colors {
Self::fb_fill_rect_index(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
item_top,
left + 1,
item_bottom,
right - 1,
selected_background,
);
}
let icon_kind = self.menu_item_icon_kind(bus, item);
let cicn_icon_ptr = matches!(icon_kind, StandardMenuIconKind::Color { .. })
.then(|| self.cicn_menu_icon_resource_ptr(item))
.flatten();
let reduced_icon_ptr = self.reduced_menu_icon_resource_ptr(bus, item);
let small_icon_ptr = self.small_menu_icon_resource_ptr(bus, item);
let normal_icon_ptr = self.normal_menu_icon_resource_ptr(bus, 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_row_chrome = self.draw_theme_menu_item_chrome(
bus,
item_top,
left + 1,
item_bottom,
right - 1,
row_enabled,
highlighted_item == i as i16 + 1,
is_separator,
item.icon != 0 && !has_app_icon_resource,
item.mark != 0,
has_command_key,
);
let layout = standard_menu_item_layout(
(left, right),
(item_top, item_height),
icon_kind,
item.mark != 0,
(metrics.ascent, metrics.descent),
attached_pulldown,
);
let text_y = layout.text_baseline;
let dim_row = !row_enabled || is_separator;
let dim_index = if dim_row {
Self::menu_dim_pixel_index(bus, pixel_size, name_pixel_index, dropdown_bg_index)
} else {
None
};
let dim_with_pattern = dim_row && dim_index.is_none();
let content_index = |component_index: Option<u8>| {
if let Some((_, selected_foreground)) = selected_colors {
Some(selected_foreground)
} else if dim_row {
dim_index.or(component_index)
} else {
component_index
}
};
if is_separator {
if provider_row_chrome {
item_top = item_bottom;
continue;
}
let sep_y = layout.separator_y;
for x in (left + 1)..(right - 1) {
match dim_index {
Some(pixel_index) => Self::fb_set_pixel_index(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
x,
sep_y,
pixel_index,
),
None => {
if standard_menu_gray_pattern_is_ink(x, sep_y) {
if let Some(pixel_index) = name_pixel_index {
Self::fb_set_pixel_index(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
x,
sep_y,
pixel_index,
);
} else {
Self::fb_set_pixel(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
x,
sep_y,
true,
);
}
}
}
}
}
item_top = item_bottom;
continue;
}
let is_hierarchical = Self::is_hierarchical_item(item);
let text_left = layout.text_left;
if item.mark != 0 && !is_hierarchical {
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) = content_index(mark_pixel_index) {
Self::fb_draw_string_styled_index(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
layout.mark_left,
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,
layout.mark_left,
text_y,
&mark_str,
font_id,
font_size,
);
}
}
if let Some(icon_ptr) = cicn_icon_ptr {
self.draw_cicn_menu_icon(bus, icon_ptr, item_top, layout.icon_left);
} else if let Some(icon_ptr) = normal_icon_ptr {
self.draw_monochrome_menu_icon(
bus,
icon_ptr,
item_top,
layout.icon_left,
StandardMenuIconKind::Normal,
content_index(name_pixel_index),
);
} else if let Some(icon_ptr) = reduced_icon_ptr {
self.draw_monochrome_menu_icon(
bus,
icon_ptr,
item_top,
layout.icon_left,
StandardMenuIconKind::Reduced,
content_index(name_pixel_index),
);
} else if let Some(icon_ptr) = small_icon_ptr {
self.draw_monochrome_menu_icon(
bus,
icon_ptr,
item_top,
layout.icon_left,
StandardMenuIconKind::Small,
content_index(name_pixel_index),
);
}
if let Some(pixel_index) = content_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 {
for_each_standard_hierarchy_indicator_pixel(
layout.indicator_left,
layout.indicator_mid_y,
|x, y| match content_index(command_pixel_index) {
Some(pixel_index) => Self::fb_set_pixel_index(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
x,
y,
pixel_index,
),
None => Self::fb_set_pixel(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
x,
y,
true,
),
},
);
}
if has_command_key {
let cmd_str = format!("\u{2318}{}", item.key_equiv as char);
let command_left = layout.command_left;
if let Some(pixel_index) = content_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 dim_with_pattern && !provider_row_chrome {
for y in item_top..item_bottom {
for x in (left + 1)..(right - 1) {
if !standard_menu_gray_pattern_is_ink(x, y) {
if let Some(bg_index) = dropdown_bg_index {
Self::fb_set_pixel_index(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
x,
y,
bg_index,
);
} else {
Self::fb_set_pixel(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
x,
y,
false,
);
}
}
}
}
}
if selected_mono {
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 {
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;
bus.write_byte(addr, bus.read_byte(addr) ^ (1 << bit));
}
}
}
}
item_top = item_bottom;
}
let center_x = left.saturating_add(right.saturating_sub(left) / 2);
if scroll_up {
for_each_standard_scroll_up_indicator_pixel(center_x, top, |x, y| {
Self::menu_set_standard_pixel(bus, screen, x, y, true);
});
}
if scroll_down {
for_each_standard_scroll_down_indicator_pixel(center_x, bottom, |x, y| {
Self::menu_set_standard_pixel(bus, screen, x, y, true);
});
}
}
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) = match pixel_size {
bits @ (1 | 2 | 4) => {
let pixels_per_byte = 8 / u32::from(bits);
(
(left.max(0) as u32) / pixels_per_byte,
(save_right.max(0) as u32).div_ceil(pixels_per_byte),
)
}
_ => {
(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<Pixel: Copy + Into<u16>>(
&self,
bus: &mut MacMemoryBus,
rect: (i16, i16, i16, i16),
saved: &[Pixel],
) {
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) = match pixel_size {
bits @ (1 | 2 | 4) => {
let pixels_per_byte = 8 / u32::from(bits);
(
(left.max(0) as u32) / pixels_per_byte,
(save_right.max(0) as u32).div_ceil(pixels_per_byte),
)
}
_ => (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].into() as u8);
idx += 1;
}
}
}
}
fn invert_menu_bar_rect(
&self,
bus: &mut MacMemoryBus,
top: i16,
left: i16,
bottom: i16,
right: i16,
hilite_indexes: Option<(u8, u8)>,
) {
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 matches!(pixel_size, 2 | 4) {
let indexes = hilite_indexes.unwrap_or((0, 15));
Self::hilite_packed_menu_pixel(
bus,
(screen_base, row_bytes, screen_width, screen_height),
pixel_size,
x,
y,
(
indexes.0 & ((1u16 << pixel_size) - 1) as u8,
indexes.1 & ((1u16 << pixel_size) - 1) as u8,
),
);
} else if pixel_size == 8 {
let addr = screen_base + (y as u32) * row_bytes + (x as u32);
let b = bus.read_byte(addr);
let (background, foreground) = hilite_indexes.unwrap_or((0, 255));
bus.write_byte(
addr,
Self::menu_hilited_pixel_index(b, background, foreground),
);
}
}
}
}
fn invert_menu_title_index(&self, bus: &mut MacMemoryBus, menu_idx: usize) -> bool {
let Some((_idx, region)) = self
.current_menu_title_regions_with_indices(bus)
.into_iter()
.find(|(idx, _region)| *idx == menu_idx)
else {
return false;
};
let Some(menu) = self.menus.get(menu_idx) else {
return false;
};
let (_screen_base, _row_bytes, _screen_width, _screen_height, pixel_size) =
self.get_screen_params();
let hilite_indexes = Self::menu_hilite_pixel_indexes(
bus,
self.menu_title_background_pixel_index(bus, menu.id, pixel_size),
Self::menu_title_pixel_index(bus, menu.id, pixel_size),
pixel_size,
);
let menu_bar_height = bus.read_word(addr::MBAR_HEIGHT) as i16;
let (top, left, bottom, right) = region.highlighted_rect(menu_bar_height);
self.invert_menu_bar_rect(bus, top, left, bottom, right, hilite_indexes);
self.redraw_color_system_menu_mark_title(bus, menu_idx, region.title_origin());
true
}
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 | 2 | 4 | 8) {
return;
}
if menu_id != 0 {
if let Some((idx, _region)) = self
.current_menu_title_regions_with_indices(bus)
.into_iter()
.find(|(idx, _region)| self.menus.get(*idx).is_some_and(|menu| menu.id == menu_id))
{
let current = self.current_menu_bar_highlight_index(bus);
if let Some(previous_idx) = current.filter(|previous_idx| *previous_idx != idx) {
self.invert_menu_title_index(bus, previous_idx);
}
self.invert_menu_title_index(bus, idx);
let target_menu_id = if current == Some(idx) { 0 } else { menu_id };
bus.write_word(addr::THE_MENU, target_menu_id as u16);
return;
}
}
let hilite_indexes = Self::menu_hilite_pixel_indexes(
bus,
self.menu_bar_background_pixel_index(bus, pixel_size),
Self::menu_title_pixel_index(bus, 0, pixel_size),
pixel_size,
);
self.invert_menu_bar_rect(bus, 0, 0, menu_bar_height, screen_width, hilite_indexes);
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;
}
let Some((active_menu_handle, dropdown_rect)) =
self.menu_tracking.as_mut().map(|tracking| {
tracking.highlighted_item = item;
(tracking.menu_handle, tracking.dropdown_rect())
})
else {
return;
};
let Some(active_menu) = self.menu_index_for_handle(active_menu_handle) else {
return;
};
self.draw_menu_dropdown(bus, active_menu, dropdown_rect);
}
pub(super) fn highlight_menu_title(&self, bus: &mut MacMemoryBus, menu_idx: usize) {
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 mut target_region = None;
for (idx, region) in self.current_menu_title_regions_with_indices(bus) {
if idx == menu_idx {
target_region = Some(region);
break;
}
}
let Some(region) = target_region else {
return;
};
let menu_bar_height = bus.read_word(addr::MBAR_HEIGHT) as i16;
if menu_bar_height <= 1 {
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;
};
if self.draw_theme_menu_title_chrome(
bus,
1,
region.left,
menu_bar_height - 1,
region.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,
region.title_origin(),
text_y,
&menu.title,
font_id,
font_size,
0,
false,
);
}
return;
}
let Some(menu) = self
.menus
.get(menu_idx)
.filter(|menu| menu.visible_in_menu_bar)
else {
return;
};
let hilite_indexes = Self::menu_hilite_pixel_indexes(
bus,
self.menu_title_background_pixel_index(bus, menu.id, pixel_size),
Self::menu_title_pixel_index(bus, menu.id, pixel_size),
pixel_size,
);
let (classic_top, classic_left, classic_bottom, classic_right) =
region.highlighted_rect(menu_bar_height);
for y in classic_top..classic_bottom {
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 matches!(pixel_size, 2 | 4) {
Self::hilite_packed_menu_pixel(
bus,
(screen_base, row_bytes, screen_width, screen_height),
pixel_size,
x,
y,
hilite_indexes.unwrap_or((0, ((1u16 << pixel_size) - 1) as u8)),
);
} else if let Some((background, foreground)) = 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, foreground),
);
} 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_hilited_pixel_index(b, 0, 255));
}
}
}
}
self.redraw_color_system_menu_mark_title(bus, menu_idx, region.title_origin());
}
fn redraw_color_system_menu_mark_title(
&self,
bus: &mut MacMemoryBus,
menu_idx: usize,
title_x: i16,
) {
let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
self.get_screen_params();
let Some(menu_enabled) = self
.menus
.get(menu_idx)
.filter(|menu| Self::is_system_menu_mark_title(&menu.title))
.map(|menu| menu.enabled)
else {
return;
};
if !matches!(pixel_size, 2 | 4 | 8) {
return;
}
self.fb_draw_retro_computer_menu_mark(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
title_x,
);
if !menu_enabled {
let menu_bar_height = bus.read_word(addr::MBAR_HEIGHT) as i16;
let background_x = title_x - 9;
let background_index = Self::fb_get_pixel_index(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
background_x,
1,
)
.unwrap_or(0);
self.fb_apply_menu_title_dim_pattern(
bus,
(
1,
title_x,
menu_bar_height - 1,
title_x.saturating_add(Self::menu_title_advance("\u{14}")),
),
Some(background_index),
true,
);
}
}
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::super::TrapDispatcher;
use super::{
count_menu_items_from_memory, menu_list_from_memory, parse_appendmenu_items,
parse_menu_resource, test_tracked_menu_state, Menu, MenuItem, MC_ENTRY_SIZE,
MENU_KEY_REDUCED_ICON, MENU_KEY_SMALL_ICON, MENU_ROW_HEIGHT,
};
use crate::cpu::{CpuOps, Register};
use crate::memory::{MacMemoryBus, MemoryBus};
use crate::menu_manager::{menu_choice_value, MenuDefinitionInvocation, TrackedMenuPaneView};
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 packed_4bpp_screen_pixel_index(
bus: &MacMemoryBus,
base: u32,
row_bytes: u32,
x: i16,
y: i16,
) -> u8 {
let packed = bus.read_byte(base + (y as u32 * row_bytes) + (x as u32 / 2));
if x & 1 == 0 {
packed >> 4
} else {
packed & 0x0F
}
}
fn set_packed_4bpp_screen_pixel_index(
bus: &mut MacMemoryBus,
screen: (u32, u32, i16, i16),
x: i16,
y: i16,
pixel_index: u8,
) {
let (base, row_bytes, width, height) = screen;
super::super::TrapDispatcher::fb_set_pixel_index(
bus,
base,
row_bytes,
4,
width,
height,
x,
y,
pixel_index,
);
}
fn setup_4bpp_menu_screen(
disp: &mut super::super::TrapDispatcher,
bus: &mut MacMemoryBus,
width: u16,
height: u16,
) -> (u32, u32) {
let row_bytes = u32::from(width).div_ceil(2);
let base = bus.alloc(row_bytes * u32::from(height));
disp.set_screen_mode_for_test(base, row_bytes, width, height, 4);
for offset in 0..(row_bytes * u32::from(height)) {
bus.write_byte(base + offset, 0);
}
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 make_8bpp_current_gdevice(bus: &mut MacMemoryBus) -> u32 {
let mut foreign = super::super::TrapDispatcher::new();
let base = bus.alloc(16 * 16);
foreign.set_screen_mode_for_test(base, 16, 16, 16, 8);
foreign.ensure_main_gdevice(bus)
}
fn remap_main_device_mono_indexes(bus: &mut MacMemoryBus, white: u8, black: u8) {
let gdevice_handle = bus.read_long(0x08A4); let gdevice = bus.read_long(gdevice_handle);
let pixmap_handle = bus.read_long(gdevice + 22);
let pixmap = bus.read_long(pixmap_handle);
let ctab_handle = bus.read_long(pixmap + 42);
let ctab = bus.read_long(ctab_handle);
let count = bus.read_word(ctab + 6) as u32 + 1;
assert!(u32::from(white.max(black)) < count);
for ordinal in 0..count {
let entry = ctab + 8 + ordinal * 8;
bus.write_word(entry, ordinal as u16);
for component in 0..3 {
bus.write_word(entry + 2 + component * 2, 0x8000);
}
}
for (index, component) in [(white, 0xFFFF), (black, 0)] {
let entry = ctab + 8 + u32::from(index) * 8;
for offset in [2, 4, 6] {
bus.write_word(entry + offset, component);
}
}
}
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 cicn_source_with_solid_4bpp_color(source_index: u8, rgb: [u16; 3]) -> Vec<u8> {
let width = 16u16;
let height = 16u16;
let pm_row_bytes = 8usize;
let mask_row_bytes = 2usize;
let mask_size = mask_row_bytes * usize::from(height);
let bmap_size = mask_size;
let ctab_offset = 82 + mask_size + bmap_size;
let pixel_offset = ctab_offset + 16;
let mut data = vec![0u8; pixel_offset + pm_row_bytes * usize::from(height)];
write_be_word(&mut data, 4, 0x8000 | pm_row_bytes as u16);
write_be_word(&mut data, 10, height);
write_be_word(&mut data, 12, width);
write_be_word(&mut data, 32, 4);
write_be_word(&mut data, 34, 1);
write_be_word(&mut data, 36, 4);
write_be_word(&mut data, 54, mask_row_bytes as u16);
write_be_word(&mut data, 60, height);
write_be_word(&mut data, 62, width);
write_be_word(&mut data, 68, mask_row_bytes as u16);
write_be_word(&mut data, 74, height);
write_be_word(&mut data, 76, width);
write_be_word(&mut data, ctab_offset + 6, 0);
write_be_word(&mut data, ctab_offset + 8, u16::from(source_index));
write_be_word(&mut data, ctab_offset + 10, rgb[0]);
write_be_word(&mut data, ctab_offset + 12, rgb[1]);
write_be_word(&mut data, ctab_offset + 14, rgb[2]);
let packed = (source_index << 4) | source_index;
for row in 0..usize::from(height) {
data[82 + row * mask_row_bytes] = 0xFF;
data[82 + row * mask_row_bytes + 1] = 0xFF;
data[pixel_offset + row * pm_row_bytes..pixel_offset + (row + 1) * pm_row_bytes]
.fill(packed);
}
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_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_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),
),
);
write_be_word(&mut data, base + 28, seed.wrapping_add(12));
}
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 packed_two_bit_menu_highlight_preserves_neighboring_pixels() {
let mut bus = MacMemoryBus::new(8 * 1024 * 1024);
let base = bus.alloc(1);
bus.write_byte(base, 0b00_01_10_11);
super::super::TrapDispatcher::hilite_packed_menu_pixel(
&mut bus,
(base, 1, 4, 1),
2,
0,
0,
(0, 3),
);
assert_eq!(bus.read_byte(base), 0b11_01_10_11);
super::super::TrapDispatcher::hilite_packed_menu_pixel(
&mut bus,
(base, 1, 4, 1),
2,
3,
0,
(0, 3),
);
assert_eq!(bus.read_byte(base), 0b11_01_10_00);
}
#[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 addresmenu_prioritizes_fond_names_loads_resources_and_exposes_builtin_fonts() {
let (mut disp, mut cpu, mut bus) = setup();
let handle = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 331, 0x306A00, "Font");
let fond_ptr = bus.alloc(1);
let font_ptr = bus.alloc(1);
bus.write_byte(fond_ptr, 1);
bus.write_byte(font_ptr, 2);
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"FONT", "Custom Face".to_string()), (7, font_ptr)),
((*b"FOND", "Custom Face".to_string()), (8, fond_ptr)),
]),
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,
});
disp.res_load = false;
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, u32::from_be_bytes(*b"FONT"));
bus.write_long(TEST_SP + 4, handle);
assert!(
disp.dispatch_menu(true, 0x14D, &mut cpu, &mut bus)
.unwrap()
.is_ok(),
"AddResMenu should succeed"
);
assert!(disp.res_load, "AppendResMenu must restore SetResLoad(TRUE)");
let menu = disp
.menus
.iter()
.find(|menu| menu.handle == handle)
.expect("font menu should remain registered");
for expected in ["Chicago", "Geneva", "Monaco", "New York", "Palatino"] {
assert!(
menu.items.iter().any(|item| item.text == expected),
"built-in family {expected} should appear in the Font menu"
);
}
assert!(
!menu.items.iter().any(|item| item.text == "Application"),
"the applFont selector is not a user-facing family"
);
assert_eq!(
menu.items
.iter()
.filter(|item| item.text == "Custom Face")
.count(),
1,
"the FOND name must suppress the later FONT duplicate"
);
for (resource_type, id, ptr) in [(*b"FOND", 8, fond_ptr), (*b"FONT", 7, font_ptr)] {
let resource_handle = disp.resource_handles_by_key[&(0, resource_type, id)];
assert_eq!(bus.read_long(resource_handle), ptr);
}
}
#[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_eq!(
width,
super::super::TrapDispatcher::fb_measure_string("Open", 0, 12) + 32,
"68k CalcMenuSize should use the shared Mac OS 8.1 width policy"
);
assert_eq!(height, 16);
}
#[test]
fn calcmenusize_arms_custom_mdef_size_message_with_pascal_arguments() {
let (mut disp, mut cpu, mut bus) = setup();
let handle = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 335, 0x306DD0, "Custom");
let menu_ptr = bus.read_long(handle);
let mdef_ptr = bus.alloc(2);
let mdef_handle = bus.alloc(4);
bus.write_word(mdef_ptr, 0x4E75);
bus.write_long(mdef_handle, mdef_ptr);
bus.write_long(menu_ptr + 6, mdef_handle);
disp.loaded_handles
.insert(mdef_handle, (mdef_ptr, *b"MDEF", 256));
let return_pc = 0x0012_3456;
cpu.write_reg(Register::PC, return_pc);
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());
let trampoline = disp.menu_def_trampoline;
assert_ne!(trampoline, 0);
assert_eq!(cpu.read_reg(Register::PC), trampoline);
assert_eq!(cpu.read_reg(Register::A7), TEST_SP);
assert_eq!(bus.read_long(TEST_SP), return_pc);
assert_eq!(bus.read_word(trampoline + 6), 2);
assert_eq!(bus.read_long(trampoline + 10), handle);
assert_eq!(bus.read_long(trampoline + 16), trampoline + 50);
assert_eq!(bus.read_long(trampoline + 22), 0);
assert_eq!(bus.read_long(trampoline + 28), trampoline + 58);
assert_eq!(bus.read_long(trampoline + 34), mdef_ptr);
assert!(disp.guest_calls.is_empty());
}
#[test]
fn calcmenusize_calls_the_m68k_record_from_a_fat_mdef_descriptor() {
let (mut disp, mut cpu, mut bus) = setup();
let handle = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 338, 0x306DE0, "Fat");
let menu_ptr = bus.read_long(handle);
let descriptor = bus.alloc(
crate::guest_procedure::ROUTINE_DESCRIPTOR_HEADER_SIZE
+ 2 * crate::guest_procedure::ROUTINE_RECORD_SIZE,
);
let powerpc_entry = bus.alloc(4);
let m68k_entry = bus.alloc(2);
let mdef_handle = bus.alloc(4);
bus.write_word(
descriptor,
crate::guest_procedure::ROUTINE_DESCRIPTOR_MIXED_MODE_TRAP,
);
bus.write_byte(
descriptor + 2,
crate::guest_procedure::ROUTINE_DESCRIPTOR_VERSION,
);
bus.write_word(descriptor + 10, 1);
let powerpc_record = descriptor + crate::guest_procedure::ROUTINE_DESCRIPTOR_HEADER_SIZE;
bus.write_byte(
powerpc_record + crate::guest_procedure::ROUTINE_RECORD_ISA_OFFSET,
crate::guest_procedure::ROUTINE_RECORD_POWERPC_ISA,
);
bus.write_long(
powerpc_record + crate::guest_procedure::ROUTINE_RECORD_PROC_DESCRIPTOR_OFFSET,
powerpc_entry,
);
let m68k_record = powerpc_record + crate::guest_procedure::ROUTINE_RECORD_SIZE;
bus.write_byte(
m68k_record + crate::guest_procedure::ROUTINE_RECORD_ISA_OFFSET,
crate::guest_procedure::ROUTINE_RECORD_M68K_ISA,
);
bus.write_long(
m68k_record + crate::guest_procedure::ROUTINE_RECORD_PROC_DESCRIPTOR_OFFSET,
m68k_entry,
);
bus.write_word(m68k_entry, 0x4E75);
bus.write_long(mdef_handle, descriptor);
bus.write_long(menu_ptr + 6, mdef_handle);
disp.loaded_handles
.insert(mdef_handle, (descriptor, *b"MDEF", 256));
let return_pc = 0x0012_3456;
cpu.write_reg(Register::PC, return_pc);
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());
let trampoline = disp.menu_def_trampoline;
assert_ne!(trampoline, 0);
assert_eq!(cpu.read_reg(Register::PC), trampoline);
assert_eq!(bus.read_long(trampoline + 34), m68k_entry);
assert_ne!(bus.read_long(trampoline + 34), powerpc_entry);
}
#[test]
fn calcmenusize_does_not_enter_a_powerpc_only_mdef_descriptor_as_m68k() {
let (mut disp, mut cpu, mut bus) = setup();
let handle = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 339, 0x306DE8, "PPC");
let menu_ptr = bus.read_long(handle);
let descriptor = bus.alloc(
crate::guest_procedure::ROUTINE_DESCRIPTOR_HEADER_SIZE
+ crate::guest_procedure::ROUTINE_RECORD_SIZE,
);
let powerpc_entry = bus.alloc(4);
let mdef_handle = bus.alloc(4);
bus.write_word(
descriptor,
crate::guest_procedure::ROUTINE_DESCRIPTOR_MIXED_MODE_TRAP,
);
bus.write_byte(
descriptor + 2,
crate::guest_procedure::ROUTINE_DESCRIPTOR_VERSION,
);
bus.write_word(descriptor + 10, 0);
let record = descriptor + crate::guest_procedure::ROUTINE_DESCRIPTOR_HEADER_SIZE;
bus.write_byte(
record + crate::guest_procedure::ROUTINE_RECORD_ISA_OFFSET,
crate::guest_procedure::ROUTINE_RECORD_POWERPC_ISA,
);
bus.write_long(
record + crate::guest_procedure::ROUTINE_RECORD_PROC_DESCRIPTOR_OFFSET,
powerpc_entry,
);
bus.write_long(mdef_handle, descriptor);
bus.write_long(menu_ptr + 6, mdef_handle);
disp.loaded_handles
.insert(mdef_handle, (descriptor, *b"MDEF", 256));
let return_pc = 0x0012_3456;
cpu.write_reg(Register::PC, return_pc);
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());
assert_eq!(disp.menu_def_trampoline, 0);
assert_eq!(cpu.read_reg(Register::PC), return_pc);
}
#[test]
fn custom_mdef_adapter_marshals_shared_choose_invocation_scratch() {
let (mut disp, mut cpu, mut bus) = setup();
let handle = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 336, 0x306DF0, "Custom");
let menu_ptr = bus.read_long(handle);
let mdef_ptr = bus.alloc(2);
let mdef_handle = bus.alloc(4);
bus.write_word(mdef_ptr, 0x4E75);
bus.write_long(mdef_handle, mdef_ptr);
bus.write_long(menu_ptr + 6, mdef_handle);
disp.loaded_handles
.insert(mdef_handle, (mdef_ptr, *b"MDEF", 256));
cpu.write_reg(Register::PC, 0x0012_3456);
cpu.write_reg(Register::A7, TEST_SP);
let invocation = MenuDefinitionInvocation {
message: crate::menu_manager::MenuDefinitionMessage::Choose,
menu_handle: handle,
menu_rect: (20, 30, 120, 180),
hit_point: 0x0050_0060,
which_item: 4,
};
assert!(disp.arm_menu_definition(&mut cpu, &mut bus, invocation));
let trampoline = disp.menu_def_trampoline;
assert_eq!(bus.read_word(trampoline + 6), 1);
assert_eq!(bus.read_long(trampoline + 10), handle);
assert_eq!(bus.read_long(trampoline + 16), trampoline + 50);
assert_eq!(bus.read_long(trampoline + 22), 0x0050_0060);
assert_eq!(bus.read_long(trampoline + 28), trampoline + 58);
assert_eq!(
bus.read_bytes(trampoline + 50, 10),
invocation.scratch_bytes()
);
}
#[test]
fn menuselect_retains_custom_mdef_draw_and_choose_until_release() {
let (mut disp, mut cpu, mut bus) = setup_with_port();
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 handle = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 337, 0x306E40, "Custom");
let menu_ptr = bus.read_long(handle);
bus.write_word(menu_ptr + 2, 80);
bus.write_word(menu_ptr + 4, 32);
let mdef_ptr = bus.alloc(2);
let mdef_handle = bus.alloc(4);
bus.write_word(mdef_ptr, 0x4E75);
bus.write_long(mdef_handle, mdef_ptr);
bus.write_long(menu_ptr + 6, mdef_handle);
disp.loaded_handles
.insert(mdef_handle, (mdef_ptr, *b"MDEF", 256));
insert_menu(&mut disp, &mut cpu, &mut bus, handle);
disp.draw_menu_bar_to_fb(&mut bus);
let original_port = disp.current_port;
let trap_pc = 0x0012_3400;
cpu.write_reg(Register::PC, trap_pc + 2);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 10);
bus.write_word(TEST_SP + 2, 12);
bus.write_byte(crate::memory::globals::addr::MB_STATE, 0);
bus.write_word(crate::memory::globals::addr::MOUSE_LOC2, 28);
bus.write_word(crate::memory::globals::addr::MOUSE_LOC2 + 2, 20);
assert!(disp
.dispatch_menu(true, 0x13D, &mut cpu, &mut bus)
.unwrap()
.is_ok());
let trampoline = disp.menu_def_trampoline;
assert_eq!(bus.read_word(trampoline + 6), 0);
assert_eq!(bus.read_long(TEST_SP - 4), trap_pc);
assert!(disp.is_menu_definition_callback_pending());
assert_eq!(disp.guest_calls.len(), 1);
assert_eq!(disp.current_port, disp.window_manager_cport);
cpu.write_reg(Register::PC, trap_pc + 2);
cpu.write_reg(Register::A7, TEST_SP);
assert!(disp
.dispatch_menu(true, 0x13D, &mut cpu, &mut bus)
.unwrap()
.is_ok());
assert_eq!(bus.read_word(trampoline + 6), 1);
assert_eq!(bus.read_long(trampoline + 22), 0x001C_0014);
assert_eq!(bus.read_word(trampoline + 58), 0);
bus.write_word(trampoline + 58, 2);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 1);
disp.dispatch_menu(true, 0x14A, &mut cpu, &mut bus)
.unwrap()
.unwrap();
bus.write_byte(crate::memory::globals::addr::MB_STATE, 0x80);
cpu.write_reg(Register::PC, trap_pc + 2);
cpu.write_reg(Register::A7, TEST_SP);
assert!(disp
.dispatch_menu(true, 0x13D, &mut cpu, &mut bus)
.unwrap()
.is_ok());
assert_eq!(disp.menu_tracking.as_ref().unwrap().flash_remaining, 2);
assert_eq!(cpu.read_reg(Register::A7), TEST_SP);
disp.menu_tracking.as_mut().unwrap().flash_delay = 0;
cpu.write_reg(Register::PC, trap_pc + 2);
cpu.write_reg(Register::A7, TEST_SP);
disp.dispatch_menu(true, 0x13D, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(bus.read_word(trampoline + 6), 1);
let (top, left, _, _) = disp.active_menu_definition().unwrap().menu_rect();
assert_eq!(
bus.read_long(trampoline + 22),
(u32::from((top - 1) as u16) << 16) | u32::from(left as u16)
);
assert_eq!(bus.read_word(trampoline + 58), 2);
bus.write_word(trampoline + 58, 0);
let tracking = disp.menu_tracking.as_mut().unwrap();
tracking.flash_remaining = 1;
tracking.flash_delay = 0;
cpu.write_reg(Register::PC, trap_pc + 2);
cpu.write_reg(Register::A7, TEST_SP);
disp.dispatch_menu(true, 0x13D, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(bus.read_long(TEST_SP + 4), (337u32 << 16) | 2);
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
assert_eq!(disp.menu_tracking, None);
assert_eq!(disp.menu_definition_tracking, None);
assert!(disp.guest_calls.is_empty());
assert_eq!(disp.current_port, original_port);
}
#[test]
fn popup_menu_select_runs_custom_popup_draw_and_choose_sequence() {
let (mut disp, mut cpu, mut bus) = setup_with_port();
setup_8bpp_menu_screen(&mut disp, &mut bus, 160, 96);
let handle = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 338, 0x306E80, "Custom");
let menu_ptr = bus.read_long(handle);
let mdef_ptr = bus.alloc(2);
let mdef_handle = bus.alloc(4);
bus.write_word(mdef_ptr, 0x4E75);
bus.write_long(mdef_handle, mdef_ptr);
bus.write_long(menu_ptr + 6, mdef_handle);
disp.loaded_handles
.insert(mdef_handle, (mdef_ptr, *b"MDEF", 256));
let trap_pc = 0x0012_3500;
cpu.write_reg(Register::PC, trap_pc + 2);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 4);
bus.write_word(TEST_SP + 2, 30);
bus.write_word(TEST_SP + 4, 40);
bus.write_long(TEST_SP + 6, handle);
bus.write_byte(crate::memory::globals::addr::MB_STATE, 0);
bus.write_word(crate::memory::globals::addr::MOUSE_LOC2, 52);
bus.write_word(crate::memory::globals::addr::MOUSE_LOC2 + 2, 45);
disp.dispatch_menu(true, 0x00B, &mut cpu, &mut bus)
.unwrap()
.unwrap();
let trampoline = disp.menu_def_trampoline;
assert_eq!(bus.read_word(trampoline + 6), 3);
assert_eq!(bus.read_long(trampoline + 22), 0x0028_001E);
assert_eq!(bus.read_word(trampoline + 58), 4);
for (offset, value) in [(0, 40i16), (2, 30), (4, 72), (6, 110), (8, 0)] {
bus.write_word(trampoline + 50 + offset, value as u16);
}
cpu.write_reg(Register::PC, trap_pc + 2);
cpu.write_reg(Register::A7, TEST_SP);
disp.dispatch_menu(true, 0x00B, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(bus.read_word(trampoline + 6), 0);
assert_eq!(
bus.read_bytes(trampoline + 50, 8),
[0, 40, 0, 30, 0, 72, 0, 110]
);
cpu.write_reg(Register::PC, trap_pc + 2);
cpu.write_reg(Register::A7, TEST_SP);
disp.dispatch_menu(true, 0x00B, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(bus.read_word(trampoline + 6), 1);
assert_eq!(bus.read_long(trampoline + 22), 0x0034_002D);
bus.write_word(trampoline + 58, 2);
bus.write_byte(crate::memory::globals::addr::MB_STATE, 0x80);
cpu.write_reg(Register::PC, trap_pc + 2);
cpu.write_reg(Register::A7, TEST_SP);
disp.dispatch_menu(true, 0x00B, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(disp.menu_tracking.as_ref().unwrap().flash_remaining, 6);
assert_eq!(cpu.read_reg(Register::A7), TEST_SP);
disp.menu_tracking.as_mut().unwrap().flash_delay = 0;
cpu.write_reg(Register::PC, trap_pc + 2);
cpu.write_reg(Register::A7, TEST_SP);
disp.dispatch_menu(true, 0x00B, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(bus.read_word(trampoline + 6), 1);
assert_eq!(bus.read_long(trampoline + 22), 0x0027_001E);
assert_eq!(bus.read_word(trampoline + 58), 2);
bus.write_word(trampoline + 58, 0);
let tracking = disp.menu_tracking.as_mut().unwrap();
tracking.flash_remaining = 1;
tracking.flash_delay = 0;
cpu.write_reg(Register::PC, trap_pc + 2);
cpu.write_reg(Register::A7, TEST_SP);
disp.dispatch_menu(true, 0x00B, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(bus.read_long(TEST_SP + 10), (338u32 << 16) | 2);
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 10);
assert_eq!(disp.menu_tracking, None);
assert_eq!(disp.menu_definition_tracking, None);
}
#[test]
fn calcmenusize_caps_oversized_menu_height_from_profile_geometry() {
let (mut disp, mut cpu, mut bus) = setup();
disp.set_screen_mode_for_test(0x0040_0000, 100, 800, 600, 1);
bus.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 20);
let handle = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 334, 0x306DE0, "File");
let description = (0..40).map(|_| "A").collect::<Vec<_>>().join(";");
append_menu_data(
&mut disp,
&mut cpu,
&mut bus,
handle,
0x306E00,
&description,
);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, handle);
disp.dispatch_menu(true, 0x148, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(bus.read_word(bus.read_long(handle) + 4), 560);
}
#[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, 108,
"classic CalcMenuSize should sum standard, normal ICON, cicn, and SICN row heights"
);
assert_eq!(
classic,
(classic.0, 108, 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"BoldItalic<B<I;Marked!\x12\rIconItem^5";
let items = parse_appendmenu_items(raw);
assert_eq!(items.len(), 3);
assert_eq!(items[0].text, "BoldItalic");
assert_eq!(items[0].style, 0x03);
assert_eq!(items[1].text, "Marked");
assert_eq!(items[1].mark, 0x12);
assert_eq!(items[2].text, "IconItem");
assert_eq!(items[2].icon, 5);
}
#[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),
0x110C,
"compiled mctb entries should preserve their 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_allocates_the_complete_maximum_length_title_record() {
let (mut disp, mut cpu, mut bus) = setup();
let title = "A".repeat(255);
let handle = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 231, 0x30B200, &title);
let menu_ptr = bus.read_long(handle);
assert_eq!(bus.get_alloc_size(menu_ptr), Some(271));
assert_eq!(bus.read_byte(menu_ptr + 14), 255);
assert_eq!(bus.read_byte(menu_ptr + 270), 0);
}
#[test]
fn newmenu_installs_callable_standard_mdef_handle() {
let (mut disp, mut cpu, mut bus) = setup();
let handle = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 232, 0x30B300, "Window");
let menu_ptr = bus.read_long(handle);
let mdef_handle = bus.read_long(menu_ptr + 6);
assert_ne!(
mdef_handle, 0,
"NewMenu must not leave the menuProc field NIL"
);
let mdef_ptr = bus.read_long(mdef_handle);
assert_ne!(mdef_ptr, 0, "standard MDEF handle must be loaded");
assert_eq!(
bus.read_word(mdef_ptr),
0x205F,
"standard MDEF shim should begin by recovering the JSR return address"
);
}
#[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 insert_and_delete_menu_sync_the_guest_dynamic_menu_list() {
let (mut disp, mut cpu, mut bus) = setup();
assert!(
disp.dispatch_menu(true, 0x130, &mut cpu, &mut bus)
.unwrap()
.is_ok(),
"InitMenus should succeed"
);
let menu_list = bus.read_long(crate::memory::globals::addr::MENU_LIST);
assert_ne!(menu_list, 0, "InitMenus should install a MenuList handle");
let empty_list = bus.read_long(menu_list);
assert_ne!(empty_list, 0, "MenuList handle should dereference");
assert_eq!(bus.read_word(empty_list), 0, "lastMenu should start empty");
assert_eq!(
bus.read_word(empty_list + 6),
0,
"lastHMenu should start empty"
);
let menu_id = 231i16;
let handle = new_menu_with_title(&mut disp, &mut cpu, &mut bus, menu_id, 0x30B200, "Home");
insert_menu(&mut disp, &mut cpu, &mut bus, handle);
let inserted_list = bus.read_long(menu_list);
assert_eq!(
bus.read_word(inserted_list),
6,
"one regular MenuRec should occupy six bytes"
);
assert_eq!(
bus.read_long(inserted_list + 6),
handle,
"MenuRec should expose the inserted menu handle"
);
assert_eq!(
bus.read_word(inserted_list + 12),
0,
"lastHMenu should follow the regular MenuRec"
);
delete_menu_by_id(&mut disp, &mut cpu, &mut bus, menu_id);
let deleted_list = bus.read_long(menu_list);
assert_eq!(
bus.read_word(deleted_list),
0,
"DeleteMenu should remove the regular MenuRec"
);
assert_eq!(
bus.read_word(deleted_list + 6),
0,
"lastHMenu should return to the empty-list offset"
);
}
#[test]
fn insert_and_delete_menu_use_shared_order_and_hierarchical_precedence() {
let (mut disp, mut cpu, mut bus) = setup();
let first = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 100, 0x30B300, "First");
let last = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 300, 0x30B340, "Last");
let middle = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 200, 0x30B380, "Middle");
let colliding_submenu =
new_menu_with_title(&mut disp, &mut cpu, &mut bus, 200, 0x30B3C0, "Submenu");
insert_menu(&mut disp, &mut cpu, &mut bus, first);
insert_menu(&mut disp, &mut cpu, &mut bus, last);
insert_menu_before_id(&mut disp, &mut cpu, &mut bus, middle, 300);
insert_menu_before_id(&mut disp, &mut cpu, &mut bus, colliding_submenu, -1);
let menu_list_handle = bus.read_long(crate::memory::globals::addr::MENU_LIST);
let inserted = super::menu_list_from_memory(&bus, menu_list_handle).unwrap();
assert_eq!(
inserted.regular_handles().collect::<Vec<_>>(),
vec![first, middle, last],
"InsertMenu must place a regular menu before the requested ID"
);
assert_eq!(
inserted.hierarchical_handles().collect::<Vec<_>>(),
vec![colliding_submenu]
);
assert_eq!(
get_mhandle_for_id(&mut disp, &mut cpu, &mut bus, 200),
colliding_submenu,
"GetMHandle must resolve a colliding hierarchical ID first"
);
delete_menu_by_id(&mut disp, &mut cpu, &mut bus, 200);
let after_submenu = super::menu_list_from_memory(&bus, menu_list_handle).unwrap();
assert!(after_submenu.hierarchical.is_empty());
assert_eq!(
after_submenu.regular_handles().collect::<Vec<_>>(),
vec![first, middle, last],
"DeleteMenu must remove a colliding hierarchical ID before a regular one"
);
assert_eq!(
get_mhandle_for_id(&mut disp, &mut cpu, &mut bus, 200),
middle
);
delete_menu_by_id(&mut disp, &mut cpu, &mut bus, 200);
let after_regular = super::menu_list_from_memory(&bus, menu_list_handle).unwrap();
assert_eq!(
after_regular.regular_handles().collect::<Vec<_>>(),
vec![first, last]
);
}
#[test]
fn submenu_resolution_uses_only_the_current_hierarchical_partition() {
let (mut disp, mut cpu, mut bus) = setup();
let root = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 129, 0x30B500, "Root");
append_menu_data(&mut disp, &mut cpu, &mut bus, root, 0x30B540, "Child");
set_item_cmd(&mut disp, &mut cpu, &mut bus, root, 1, 0x1B);
set_item_mark(&mut disp, &mut cpu, &mut bus, root, 1, 138);
let child = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 138, 0x30B580, "Child");
append_menu_data(&mut disp, &mut cpu, &mut bus, child, 0x30B5C0, "Choice");
insert_menu(&mut disp, &mut cpu, &mut bus, root);
let root_index = disp
.menus
.iter()
.position(|menu| menu.handle == root)
.unwrap();
assert_eq!(
disp.submenu_menu_index_for_parent_item(&bus, root_index, 1),
None,
"a detached menu record must not resolve as a submenu"
);
insert_menu(&mut disp, &mut cpu, &mut bus, child);
let root_index = disp
.menus
.iter()
.position(|menu| menu.handle == root)
.unwrap();
assert_eq!(
disp.submenu_menu_index_for_parent_item(&bus, root_index, 1),
None,
"a regular-partition ID match must not resolve as a submenu"
);
delete_menu_by_id(&mut disp, &mut cpu, &mut bus, 138);
insert_menu_before_id(&mut disp, &mut cpu, &mut bus, child, -1);
let root_index = disp
.menus
.iter()
.position(|menu| menu.handle == root)
.unwrap();
let child_index = disp
.menus
.iter()
.position(|menu| menu.handle == child)
.unwrap();
assert_eq!(
disp.submenu_menu_index_for_parent_item(&bus, root_index, 1),
Some(child_index),
"the matching hierarchical-partition handle must resolve"
);
let root_ptr = bus.read_long(root);
let first_item = root_ptr + 15 + u32::from(bus.read_byte(root_ptr + 14));
let command = first_item + 2 + u32::from(bus.read_byte(first_item));
bus.write_byte(command, b'C');
assert!(disp.menus[root_index].items[0].key_equiv == 0x1B);
assert_eq!(
disp.submenu_menu_index_for_parent_item(&bus, root_index, 1),
None,
"live guest item bytes must override the stale presentation cache"
);
bus.write_byte(command, 0x1B);
assert_eq!(
disp.submenu_menu_index_for_parent_item(&bus, root_index, 1),
Some(child_index)
);
}
#[test]
fn insert_menu_uses_live_guest_list_order_instead_of_the_presentation_cache() {
let (mut disp, mut cpu, mut bus) = setup();
let first = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 100, 0x30B400, "First");
let second = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 200, 0x30B440, "Second");
let inserted =
new_menu_with_title(&mut disp, &mut cpu, &mut bus, 150, 0x30B480, "Inserted");
insert_menu(&mut disp, &mut cpu, &mut bus, first);
insert_menu(&mut disp, &mut cpu, &mut bus, second);
let menu_list_handle = bus.read_long(crate::memory::globals::addr::MENU_LIST);
let menu_list_ptr = bus.read_long(menu_list_handle);
bus.write_long(menu_list_ptr + 6, second);
bus.write_long(menu_list_ptr + 12, first);
bus.write_word(menu_list_ptr + 2, 120);
bus.write_word(menu_list_ptr + 10, 40);
bus.write_word(menu_list_ptr + 16, 80);
assert_eq!(
disp.current_menu_title_regions_with_indices(&bus)
.into_iter()
.map(|(index, region)| (disp.menus[index].handle, region.left, region.right))
.collect::<Vec<_>>(),
vec![(second, 40, 80), (first, 80, 120)],
"68k title geometry must come from the live guest MenuList"
);
assert_eq!(disp.current_menu_title_hit_test(&bus, 39), None);
assert_eq!(
disp.current_menu_title_hit_test(&bus, 40)
.map(|index| disp.menus[index].handle),
Some(second)
);
assert_eq!(
disp.current_menu_title_hit_test(&bus, 80)
.map(|index| disp.menus[index].handle),
Some(first)
);
assert_eq!(disp.current_menu_title_hit_test(&bus, 120), None);
assert_eq!(
disp.guest_menu_snapshot(&bus)
.menus
.iter()
.map(|menu| menu.id)
.collect::<Vec<_>>(),
vec![200, 100],
"frontend snapshots must follow direct guest MenuList ordering changes"
);
insert_menu_before_id(&mut disp, &mut cpu, &mut bus, inserted, 100);
let live = super::menu_list_from_memory(&bus, menu_list_handle).unwrap();
assert_eq!(
live.regular_handles().collect::<Vec<_>>(),
vec![second, inserted, first],
"InsertMenu must preserve direct guest MenuList ordering changes"
);
assert_eq!(
disp.menus
.iter()
.filter(|menu| menu.in_menu_bar)
.map(|menu| menu.handle)
.collect::<Vec<_>>(),
vec![second, inserted, first],
"the 68k presentation cache must follow the guest list"
);
}
#[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_replaces_standard_mdef_id_placeholder_with_callable_handle() {
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);
disp.dispatch_menu(true, 0x1BF, &mut cpu, &mut bus)
.unwrap()
.unwrap();
let menu_handle = bus.read_long(TEST_SP + 2);
let live_menu_ptr = bus.read_long(menu_handle);
let mdef_handle = bus.read_long(live_menu_ptr + 6);
assert_ne!(
mdef_handle, 0,
"GetMenu must replace the standard MDEF ID placeholder"
);
let mdef_ptr = bus.read_long(mdef_handle);
assert_ne!(mdef_ptr, 0, "standard MDEF handle must be loaded");
assert_eq!(
bus.read_word(mdef_ptr),
0x205F,
"standard MDEF shim should be directly callable by 68k code"
);
}
#[test]
fn getmenu_resolves_custom_mdef_from_the_resource_chain() {
let (mut disp, mut cpu, mut bus) = setup();
let menu_ptr = seed_menu_resource(&mut bus, 129, "Custom");
let mdef_ptr = bus.alloc(2);
bus.write_word(menu_ptr + 6, 256);
bus.write_word(menu_ptr + 8, 0);
bus.write_word(mdef_ptr, 0x4E75); disp.resources = Some(crate::trap::dispatch::LoadedResources {
files: std::collections::HashMap::from([(
0,
crate::trap::dispatch::ResourceFileMap {
loaded: std::collections::HashMap::from([
((*b"MENU", 129), menu_ptr),
((*b"MDEF", 256), mdef_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, 129);
disp.dispatch_menu(true, 0x1BF, &mut cpu, &mut bus)
.unwrap()
.unwrap();
let menu_handle = bus.read_long(TEST_SP + 2);
let live_menu_ptr = bus.read_long(menu_handle);
let mdef_handle = bus.read_long(live_menu_ptr + 6);
assert_ne!(
mdef_handle,
u32::from(256u16) << 16,
"GetMenu must replace the raw ID-plus-zero placeholder"
);
assert_eq!(
bus.read_long(mdef_handle),
mdef_ptr,
"custom MDEF handle should dereference to the loaded resource"
);
let trampoline = disp.menu_def_trampoline;
assert_ne!(trampoline, 0);
assert_eq!(
bus.read_word(trampoline + 6),
2,
"GetMenu must size a newly created custom menu through mSizeMsg"
);
assert_eq!(bus.read_long(trampoline + 10), menu_handle);
}
#[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_rejects_a_truncated_declared_menu_sequence() {
let (mut disp, mut cpu, mut bus) = setup();
let mbar_ptr = bus.alloc(4);
bus.write_bytes(mbar_ptr, &[0, 2, 0, 128]);
disp.resources = Some(crate::trap::dispatch::LoadedResources {
files: std::collections::HashMap::from([(
0,
crate::trap::dispatch::ResourceFileMap {
loaded: std::collections::HashMap::from([((*b"MBAR", 902), mbar_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, 902);
bus.write_long(TEST_SP + 2, 0xDEAD_BEEF);
assert!(disp
.dispatch_menu(true, 0x1C0, &mut cpu, &mut bus)
.is_some_and(|result| result.is_ok()));
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 2);
assert_eq!(bus.read_long(TEST_SP + 2), 0);
}
#[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 menu_list = menu_list_from_memory(&bus, mbar_handle)
.expect("returned handle should contain a DynamicMenuList");
assert_eq!(menu_list.mb_res_id, 900);
assert_eq!(menu_list.regular.len(), 2);
assert!(menu_list.regular.iter().all(|entry| entry.handle != 0));
let file_handle = menu_list.regular[0].handle;
assert_eq!(
disp.loaded_handles.get(&file_handle).copied(),
Some((bus.read_long(file_handle), *b"MENU", 128)),
"GetNewMBar must obtain each menu through the Resource Manager",
);
assert_eq!(
disp.resource_handle_files.get(&file_handle).copied(),
Some(0),
"GetNewMBar menu handles should retain their resource-file owner",
);
assert_ne!(
bus.read_long(bus.read_long(file_handle) + 6),
0,
"GetNewMBar should resolve the standard MDEF through GetMenu",
);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 128);
disp.dispatch_menu(true, 0x1BF, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(
bus.read_long(cpu.read_reg(Register::A7)),
file_handle,
"GetMenu should reuse the MENU resource loaded by GetNewMBar",
);
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_sizes_each_custom_menu_before_returning_the_list() {
let (mut disp, mut cpu, mut bus) = setup();
let first_menu = seed_menu_resource(&mut bus, 601, "First");
let second_menu = seed_menu_resource(&mut bus, 602, "Second");
for menu in [first_menu, second_menu] {
bus.write_word(menu + 6, 256);
bus.write_word(menu + 8, 0);
}
let mdef = bus.alloc(2);
bus.write_word(mdef, 0x4E75);
let mbar = 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", 900), mbar),
((*b"MENU", 601), first_menu),
((*b"MENU", 602), second_menu),
((*b"MDEF", 256), mdef),
]),
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,
});
let trap_pc = 0x0012_3600;
cpu.write_reg(Register::PC, trap_pc + 2);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 900);
disp.dispatch_menu(true, 0x1C0, &mut cpu, &mut bus)
.unwrap()
.unwrap();
let trampoline = disp.menu_def_trampoline;
assert_eq!(bus.read_word(trampoline + 6), 2);
let first_handle = bus.read_long(trampoline + 10);
assert_eq!(bus.read_word(bus.read_long(first_handle)), 601);
assert!(disp.is_tracking_refire(0xA9C0));
bus.write_word(bus.read_long(first_handle) + 2, 101);
bus.write_word(bus.read_long(first_handle) + 4, 41);
cpu.write_reg(Register::PC, trap_pc + 2);
cpu.write_reg(Register::A7, TEST_SP);
disp.dispatch_menu(true, 0x1C0, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(bus.read_word(trampoline + 6), 2);
let second_handle = bus.read_long(trampoline + 10);
assert_eq!(bus.read_word(bus.read_long(second_handle)), 602);
bus.write_word(bus.read_long(second_handle) + 2, 102);
bus.write_word(bus.read_long(second_handle) + 4, 42);
cpu.write_reg(Register::PC, trap_pc + 2);
cpu.write_reg(Register::A7, TEST_SP);
disp.dispatch_menu(true, 0x1C0, &mut cpu, &mut bus)
.unwrap()
.unwrap();
let list_handle = bus.read_long(TEST_SP + 2);
assert_ne!(list_handle, 0);
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 2);
assert_eq!(disp.pending_menu_bar_build, None);
assert_eq!(bus.read_word(bus.read_long(first_handle) + 2), 101);
assert_eq!(bus.read_word(bus.read_long(second_handle) + 4), 42);
assert_eq!(
super::menu_list_from_memory(&bus, list_handle)
.unwrap()
.regular_handles()
.collect::<Vec<_>>(),
vec![first_handle, second_handle]
);
}
#[test]
fn getnewmbar_preserves_complete_resource_backed_menu_records() {
let (mut disp, mut cpu, mut bus) = setup();
let seed_ptr = seed_menu_resource(&mut bus, 640, "Long");
let menu_ptr = bus.alloc(320);
let seed_bytes = bus.read_bytes(seed_ptr, 256);
bus.write_bytes(menu_ptr, &seed_bytes);
bus.write_byte(menu_ptr + 300, 0xA5);
let mbar_ptr = seed_mbar_resource(&mut bus, &[640]);
disp.resources = Some(crate::trap::dispatch::LoadedResources {
files: std::collections::HashMap::from([(
0,
crate::trap::dispatch::ResourceFileMap {
loaded: std::collections::HashMap::from([
((*b"MBAR", 940), mbar_ptr),
((*b"MENU", 640), 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, 940);
disp.dispatch_menu(true, 0x1C0, &mut cpu, &mut bus)
.unwrap()
.unwrap();
let list_handle = bus.read_long(cpu.read_reg(Register::A7));
let list = menu_list_from_memory(&bus, list_handle).expect("GetNewMBar menu list");
let menu_handle = list.regular[0].handle;
assert_eq!(bus.read_long(menu_handle), menu_ptr);
assert_eq!(bus.get_alloc_size(menu_ptr), Some(320));
assert_eq!(bus.read_byte(menu_ptr + 300), 0xA5);
}
#[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 menukey_searches_installed_command_only_menu_without_drawing_title() {
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 visible = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 230, 0x306800, "File");
append_menu_data(&mut disp, &mut cpu, &mut bus, visible, 0x306840, "Open/O");
insert_menu(&mut disp, &mut cpu, &mut bus, visible);
let commands =
new_menu_with_title(&mut disp, &mut cpu, &mut bus, 231, 0x306880, "Commands");
append_menu_data(&mut disp, &mut cpu, &mut bus, commands, 0x3068C0, "Pause/P");
insert_menu_before(&mut disp, &mut cpu, &mut bus, commands, -1);
cpu.write_reg(Register::A7, TEST_SP);
disp.dispatch_menu(true, 0x137, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(disp.menu_title_regions().len(), 1);
let menu_bar_before = bus.read_bytes(base, row_bytes as usize * 20);
assert_eq!(
menu_key_result(&mut disp, &mut cpu, &mut bus, b'P'),
(231u32 << 16) | 1
);
assert_eq!(
bus.read_bytes(base, row_bytes as usize * 20),
menu_bar_before,
"a command-only match must not paint a hidden menu title"
);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 1);
bus.write_long(TEST_SP + 2, commands);
disp.dispatch_menu(true, 0x13A, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(menu_key_result(&mut disp, &mut cpu, &mut bus, b'P'), 0);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 1);
bus.write_long(TEST_SP + 2, commands);
disp.dispatch_menu(true, 0x139, &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, commands);
disp.dispatch_menu(true, 0x13A, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(menu_key_result(&mut disp, &mut cpu, &mut bus, b'P'), 0);
}
#[test]
fn menukey_prefers_regular_partition_and_highlights_hierarchical_owner() {
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 root = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 230, 0x306900, "File");
append_menu_data(
&mut disp,
&mut cpu,
&mut bus,
root,
0x306940,
"Regular/X;Recent",
);
set_item_cmd(&mut disp, &mut cpu, &mut bus, root, 2, 0x1b);
set_item_mark(&mut disp, &mut cpu, &mut bus, root, 2, 231);
insert_menu(&mut disp, &mut cpu, &mut bus, root);
let child = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 231, 0x306980, "Recent");
append_menu_data(
&mut disp,
&mut cpu,
&mut bus,
child,
0x3069C0,
"Nested/H;Shadow/X",
);
insert_menu_before(&mut disp, &mut cpu, &mut bus, child, -1);
cpu.write_reg(Register::A7, TEST_SP);
disp.dispatch_menu(true, 0x137, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(
menu_key_result(&mut disp, &mut cpu, &mut bus, b'X'),
(230u32 << 16) | 1,
"a hierarchical shortcut must not beat a regular-menu match"
);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 0);
disp.dispatch_menu(true, 0x138, &mut cpu, &mut bus)
.unwrap()
.unwrap();
let (left, right) = disp.menu_title_regions()[0];
let before = title_region_pixels(&bus, base, row_bytes, left, right);
assert_eq!(
menu_key_result(&mut disp, &mut cpu, &mut bus, b'H'),
(231u32 << 16) | 1
);
assert_eq!(
bus.read_word(crate::memory::globals::addr::THE_MENU),
231,
"TheMenu must retain the selected submenu ID rather than its root title ID"
);
let after = title_region_pixels(&bus, base, row_bytes, left, right);
assert!(
changed_pixel_count(&before, &after) > 0,
"a hierarchical shortcut must highlight its owning regular title"
);
}
#[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 checkitem_persists_mark_across_later_menu_mutations() {
let (mut disp, mut cpu, mut bus) = setup();
let menu = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 223, 0x306770, "Speed");
append_menu_data(&mut disp, &mut cpu, &mut bus, menu, 0x306780, "Moderate");
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 0x0100);
bus.write_word(TEST_SP + 2, 1);
bus.write_long(TEST_SP + 4, menu);
disp.dispatch_menu(true, 0x145, &mut cpu, &mut bus)
.unwrap()
.unwrap();
set_item_cmd(&mut disp, &mut cpu, &mut bus, menu, 1, b'M');
let item = disp
.menus
.iter()
.find(|candidate| candidate.handle == menu)
.and_then(|candidate| candidate.items.first())
.unwrap();
assert_eq!(item.mark, 0x12);
}
#[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 count_menu_items_rejects_an_unterminated_live_record() {
let (_disp, _cpu, mut bus) = setup();
let menu = bus.alloc(24);
let handle = bus.alloc(4);
bus.write_long(handle, menu);
bus.write_long(menu + 10, u32::MAX);
bus.write_byte(menu + 14, 0);
bus.write_byte(menu + 15, 4);
bus.write_bytes(menu + 16, b"Open");
bus.write_bytes(menu + 20, &[0, b'O', 0, 0]);
assert_eq!(count_menu_items_from_memory(&bus, handle), 0);
}
#[test]
fn long_menu_mutations_preserve_trailing_item_and_refresh_guest_edits() {
let (mut disp, mut cpu, mut bus) = setup();
let menu_id = 447;
let handle = new_menu_with_title(&mut disp, &mut cpu, &mut bus, menu_id, 0x303800, "Long");
let mut data = vec!["A/A"; 39];
data.push("Tail/Z");
append_menu_data(
&mut disp,
&mut cpu,
&mut bus,
handle,
0x303900,
&data.join(";"),
);
insert_menu(&mut disp, &mut cpu, &mut bus, handle);
let first_long_ptr = bus.read_long(handle);
assert!(
bus.get_alloc_size(first_long_ptr).unwrap() > 256,
"AppendMenu must grow MENU records beyond the legacy 256-byte buffer"
);
assert_eq!(count_menu_items_from_memory(&bus, handle), 40);
assert_eq!(
get_item_cmd(&mut disp, &mut cpu, &mut bus, handle, 40),
b'Z'
);
let replacement = "Expanded first item that forces another complete record resize";
write_pstring(&mut bus, 0x303A00, replacement);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, 0x303A00);
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());
set_item_cmd(&mut disp, &mut cpu, &mut bus, handle, 1, b'X');
for trap in [0x13A, 0x139] {
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 1);
bus.write_long(TEST_SP + 2, handle);
assert!(disp
.dispatch_menu(true, trap, &mut cpu, &mut bus)
.unwrap()
.is_ok());
}
let (height, width) = calc_menu_size_for_test(&mut disp, &mut cpu, &mut bus, handle);
assert!(height > 0 && width > 0);
let resized_ptr = bus.read_long(handle);
assert!(bus.get_alloc_size(resized_ptr).unwrap() > 256);
let parsed = parse_menu_resource(&bus, resized_ptr, handle);
assert_eq!(parsed.items.len(), 40);
assert_eq!(parsed.items[39].text, "Tail");
assert_eq!(parsed.items[39].key_equiv, b'Z');
let mut tail_ptr = resized_ptr + 15 + bus.read_byte(resized_ptr + 14) as u32;
for _ in 0..39 {
tail_ptr += 5 + bus.read_byte(tail_ptr) as u32;
}
assert_eq!(bus.read_byte(tail_ptr), 4);
for (index, byte) in b"Last".iter().enumerate() {
bus.write_byte(tail_ptr + 1 + index as u32, *byte);
}
bus.write_byte(tail_ptr + 6, b'Q');
cpu.write_reg(Register::A7, TEST_SP);
assert!(disp
.dispatch_menu(true, 0x137, &mut cpu, &mut bus)
.unwrap()
.is_ok());
assert_eq!(disp.menus[0].items[39].text, "Last");
assert_eq!(disp.menus[0].items[39].key_equiv, b'Q');
let snapshot = disp.guest_menu_snapshot(&bus);
assert_eq!(snapshot.menus[0].items[39].text, "Last");
assert_eq!(snapshot.menus[0].items[39].key_equivalent, Some('q'));
assert_eq!(
menu_key_result(&mut disp, &mut cpu, &mut bus, b'Q'),
((menu_id as u32) << 16) | 40
);
}
#[test]
fn resource_backed_long_menu_keeps_its_complete_record_when_mutated() {
let (mut disp, mut cpu, mut bus) = setup();
let menu_id = 448;
let resource_ptr = bus.alloc(263);
bus.write_word(resource_ptr, menu_id as u16);
bus.write_long(resource_ptr + 10, 0xFFFF_FFFF);
write_pstring(&mut bus, resource_ptr + 14, "Long");
let mut offset = resource_ptr + 19;
for _ in 0..39 {
bus.write_byte(offset, 1);
bus.write_byte(offset + 1, b'A');
bus.write_byte(offset + 3, b'A');
offset += 6;
}
bus.write_byte(offset, 4);
bus.write_bytes(offset + 1, b"Tail");
bus.write_byte(offset + 6, b'Z');
bus.write_byte(offset + 9, 0);
assert_eq!(offset + 10, resource_ptr + 263);
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), resource_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, menu_id as u16);
assert!(disp
.dispatch_menu(true, 0x1BF, &mut cpu, &mut bus)
.unwrap()
.is_ok());
let handle = bus.read_long(cpu.read_reg(Register::A7));
assert_eq!(bus.get_alloc_size(bus.read_long(handle)), Some(263));
set_item_cmd(&mut disp, &mut cpu, &mut bus, handle, 1, b'X');
let parsed = parse_menu_resource(&bus, bus.read_long(handle), handle);
assert_eq!(parsed.items.len(), 40);
assert_eq!(parsed.items[39].text, "Tail");
assert_eq!(parsed.items[39].key_equiv, b'Z');
}
#[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_preserves_guest_enableflags_changes_before_mutating_item() {
let (mut disp, mut cpu, mut bus) = setup();
let menu = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 145, 0x305C00, "File");
append_menu_data(
&mut disp,
&mut cpu,
&mut bus,
menu,
0x305D00,
"Open/O;Save/S",
);
insert_menu(&mut disp, &mut cpu, &mut bus, menu);
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_ptr = bus.read_long(menu);
bus.write_long(menu_ptr + 10, bus.read_long(menu_ptr + 10) | 1);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 2);
bus.write_long(TEST_SP + 2, menu);
assert!(disp
.dispatch_menu(true, 0x139, &mut cpu, &mut bus)
.unwrap()
.is_ok());
let state = disp.menus.iter().find(|m| m.handle == menu).unwrap();
assert!(
state.enabled,
"guest-restored menu bit must survive EnableItem"
);
assert!(
state.items[1].enabled,
"the requested item should also be enabled"
);
assert_eq!(
menu_key_result(&mut disp, &mut cpu, &mut bus, b'S'),
(145u32 << 16) | 2,
"MenuKey must observe the guest-restored menu enable bit"
);
}
#[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,
"MenuInfo has no individual disable bit after item 31"
);
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 remain a no-op"
);
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 invalmenubar_defers_and_coalesces_one_draw_until_a_toolbox_event_scan() {
let (mut disp, mut cpu, mut bus) = setup_with_port();
disp.sent_open_app_event = true;
disp.menu_bar_hidden = false;
bus.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 20);
let (base, row_bytes, width, height, depth) = disp.get_screen_params();
TrapDispatcher::fb_set_pixel(
&mut bus, base, row_bytes, depth, width, height, 100, 5, true,
);
let dirty =
TrapDispatcher::fb_get_pixel_index(&bus, base, row_bytes, depth, width, height, 100, 5);
assert!(disp
.dispatch_menu(true, 0x01D, &mut cpu, &mut bus)
.unwrap()
.is_ok());
assert!(disp
.dispatch_menu(true, 0x01D, &mut cpu, &mut bus)
.unwrap()
.is_ok());
assert!(disp.event_queue.menu_bar_is_invalid());
assert_eq!(
TrapDispatcher::fb_get_pixel_index(&bus, base, row_bytes, depth, width, height, 100, 5,),
dirty,
"InvalMenuBar must not draw synchronously"
);
cpu.write_reg(Register::D0, 0);
cpu.write_reg(Register::A0, 0x0020_0000);
assert!(disp
.dispatch_event(false, 0x030, &mut cpu, &mut bus)
.unwrap()
.is_ok());
assert!(
disp.event_queue.menu_bar_is_invalid(),
"low-level OS event scans must not consume Toolbox redraw work"
);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, 0x0020_0000);
bus.write_word(TEST_SP + 4, 0);
assert!(disp
.dispatch_toolbox(true, 0x171, &mut cpu, &mut bus)
.unwrap()
.is_ok());
assert!(!disp.event_queue.menu_bar_is_invalid());
assert_ne!(
TrapDispatcher::fb_get_pixel_index(&bus, base, row_bytes, depth, width, height, 100, 5,),
dirty,
"the next Toolbox event scan must perform DrawMenuBar"
);
TrapDispatcher::fb_set_pixel(
&mut bus, base, row_bytes, depth, width, height, 100, 5, true,
);
cpu.write_reg(Register::A7, TEST_SP);
assert!(disp
.dispatch_toolbox(true, 0x171, &mut cpu, &mut bus)
.unwrap()
.is_ok());
assert_eq!(
TrapDispatcher::fb_get_pixel_index(&bus, base, row_bytes, depth, width, height, 100, 5,),
dirty,
"the deferred redraw must be consumed exactly once"
);
assert!(disp
.dispatch_menu(true, 0x01D, &mut cpu, &mut bus)
.unwrap()
.is_ok());
assert!(disp
.dispatch_menu(true, 0x137, &mut cpu, &mut bus)
.unwrap()
.is_ok());
assert!(
!disp.event_queue.menu_bar_is_invalid(),
"an explicit DrawMenuBar must satisfy the deferred request"
);
}
#[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 draw_menu_bar_releases_initial_kiosk_policy() {
let (mut disp, mut cpu, mut bus) = setup_with_port();
disp.set_menu_bar_policy(crate::runner::MenuBarPolicy::InitialKiosk);
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");
assert_eq!(
disp.menu_bar_policy,
crate::runner::MenuBarPolicy::GuestControlled
);
assert!(!disp.menu_bar_hidden);
}
#[test]
fn draw_menu_bar_preserves_force_hidden_policy() {
let (mut disp, mut cpu, mut bus) = setup_with_port();
disp.set_menu_bar_policy(crate::runner::MenuBarPolicy::ForceHidden);
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");
assert_eq!(
disp.menu_bar_policy,
crate::runner::MenuBarPolicy::ForceHidden
);
assert!(disp.menu_bar_hidden);
}
#[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_uses_retro_computer_art_for_the_system_mark_without_layout_drift() {
let (mut disp, mut cpu, mut bus) = setup_with_port();
let (screen_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 system = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 128, 0x302300, "\u{14}");
let file = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 129, 0x302400, "File");
insert_menu(&mut disp, &mut cpu, &mut bus, system);
insert_menu(&mut disp, &mut cpu, &mut bus, file);
let system_title_width = super::super::TrapDispatcher::menu_title_advance("\u{14}");
assert_eq!(system_title_width, 11, "pinned system mark cell width");
let regions = disp.menu_title_regions();
assert_eq!(regions[0], (11, 18 + system_title_width + 6));
assert_eq!(regions[1].0, 18 + system_title_width + 6);
disp.draw_menu_bar_to_fb(&mut bus);
const PIXELS: [[u8; 10]; 12] = [
[0, 1, 1, 1, 1, 1, 1, 1, 1, 0],
[1, 2, 2, 2, 2, 2, 2, 2, 2, 1],
[1, 2, 3, 3, 3, 3, 3, 3, 2, 1],
[1, 2, 3, 4, 3, 3, 3, 3, 2, 1],
[1, 2, 3, 1, 3, 3, 1, 3, 2, 1],
[1, 2, 3, 3, 3, 3, 3, 3, 2, 1],
[1, 2, 3, 1, 3, 3, 1, 3, 2, 1],
[1, 2, 3, 3, 1, 1, 3, 3, 2, 1],
[1, 2, 2, 2, 2, 2, 2, 2, 2, 1],
[0, 1, 1, 1, 1, 1, 1, 1, 1, 0],
[0, 0, 0, 1, 2, 2, 1, 0, 0, 0],
[0, 0, 1, 1, 1, 1, 1, 1, 0, 0],
];
const PALETTE: [[u16; 3]; 4] = [
[0x2222, 0x2222, 0x2222],
[0xCCCC, 0xBBBB, 0x8888],
[0x2222, 0x9999, 0xCCCC],
[0xDDDD, 0xFFFF, 0xFFFF],
];
let palette_indices = PALETTE.map(|rgb| {
super::super::TrapDispatcher::fb_pixel_index_for_rgb(&bus, rgb)
.expect("8bpp test screen should expose a device color table")
});
let background =
super::super::TrapDispatcher::fb_pixel_index_for_rgb(&bus, [0xFFFF, 0xFFFF, 0xFFFF])
.unwrap();
for (row, pixels) in PIXELS.into_iter().enumerate() {
for (col, palette_index) in pixels.into_iter().enumerate() {
let expected = if palette_index == 0 {
background
} else {
palette_indices[usize::from(palette_index - 1)]
};
assert_eq!(
screen_pixel_index(
&bus,
screen_base,
row_bytes,
18 + col as i16,
3 + row as i16,
),
expected,
"system menu mark pixel ({col}, {row})"
);
}
}
assert_eq!(disp.menu_title_regions(), regions);
let file_midpoint = (regions[1].0 + regions[1].1) / 2;
assert_eq!(disp.menu_title_hit_test(file_midpoint), Some(1));
}
#[test]
fn drawmenubar_4bpp_keeps_the_color_system_mark_through_title_reversals() {
let (mut disp, mut cpu, mut bus) = setup_with_port();
let (base, row_bytes) = setup_4bpp_menu_screen(&mut disp, &mut bus, 128, 64);
let offscreen_gdevice = make_8bpp_current_gdevice(&mut bus);
bus.write_long(0x0CC8, offscreen_gdevice); disp.menu_bar_hidden = false;
bus.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 20);
let system = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 128, 0x302380, "\u{14}");
insert_menu(&mut disp, &mut cpu, &mut bus, system);
disp.draw_menu_bar_to_fb(&mut bus);
let palette_indices = crate::ui_art::RETRO_COMPUTER_MENU_MARK_PALETTE.map(|rgb| {
super::super::TrapDispatcher::fb_main_screen_pixel_index_for_rgb(&bus, rgb)
.expect("4bpp MainDevice should expose its color table")
});
let white =
super::super::TrapDispatcher::fb_main_screen_pixel_index_for_rgb(&bus, [0xFFFF; 3])
.unwrap();
let black =
super::super::TrapDispatcher::fb_main_screen_pixel_index_for_rgb(&bus, [0; 3]).unwrap();
let assert_mark_art = |bus: &MacMemoryBus, transparent_index: u8| {
for (row, pixels) in crate::ui_art::RETRO_COMPUTER_MENU_MARK_PIXELS
.into_iter()
.enumerate()
{
for (col, palette_index) in pixels.into_iter().enumerate() {
let expected = if palette_index == 0 {
transparent_index
} else {
palette_indices[usize::from(palette_index - 1)]
};
assert_eq!(
packed_4bpp_screen_pixel_index(
bus,
base,
row_bytes,
18 + col as i16,
3 + row as i16,
),
expected,
"packed system-menu mark pixel ({col}, {row})"
);
}
}
};
assert_mark_art(&bus, white);
let title_cell_pixel = (disp.menu_title_regions()[0].0 - 2, 1);
assert_eq!(
packed_4bpp_screen_pixel_index(
&bus,
base,
row_bytes,
title_cell_pixel.0,
title_cell_pixel.1,
),
white
);
let unrelated_pixel = (title_cell_pixel.0 + 1, title_cell_pixel.1 + 1);
set_packed_4bpp_screen_pixel_index(
&mut bus,
(base, row_bytes, 128, 64),
unrelated_pixel.0,
unrelated_pixel.1,
8,
);
let before = bus.read_bytes(base, (row_bytes * 64) as usize);
disp.highlight_menu_title(&mut bus, 0);
assert_mark_art(&bus, black);
assert_eq!(
packed_4bpp_screen_pixel_index(
&bus,
base,
row_bytes,
title_cell_pixel.0,
title_cell_pixel.1,
),
black,
"title selection should reverse only the transparent mark-cell background"
);
assert_eq!(
packed_4bpp_screen_pixel_index(
&bus,
base,
row_bytes,
unrelated_pixel.0,
unrelated_pixel.1,
),
8,
"title selection should preserve an unrelated packed color index"
);
disp.highlight_menu_title(&mut bus, 0);
assert_eq!(
bus.read_bytes(base, (row_bytes * 64) as usize),
before,
"two packed title reversals should restore the full menu bar exactly"
);
disp.flash_menu_bar(&mut bus, 128);
assert_mark_art(&bus, black);
assert_eq!(
packed_4bpp_screen_pixel_index(
&bus,
base,
row_bytes,
unrelated_pixel.0,
unrelated_pixel.1,
),
8,
"FlashMenuBar(menuID) should preserve an unrelated packed color index"
);
disp.flash_menu_bar(&mut bus, 128);
assert_eq!(
bus.read_bytes(base, (row_bytes * 64) as usize),
before,
"two packed FlashMenuBar(menuID) reversals should restore the full menu bar"
);
let mut kept_art_pixel = None;
let mut knocked_art_pixel = None;
for (row, pixels) in crate::ui_art::RETRO_COMPUTER_MENU_MARK_PIXELS
.into_iter()
.enumerate()
{
for (col, palette_index) in pixels.into_iter().enumerate() {
if palette_index == 0 {
continue;
}
let expected = palette_indices[usize::from(palette_index - 1)];
if expected == black || expected == white {
continue;
}
let point = (18 + col as i16, 3 + row as i16, expected);
if (point.0 + point.1) % 2 == 0 {
kept_art_pixel.get_or_insert(point);
} else {
knocked_art_pixel.get_or_insert(point);
}
}
}
let kept_art_pixel = kept_art_pixel.expect("colored art pixel on an enabled pattern bit");
let knocked_art_pixel =
knocked_art_pixel.expect("colored art pixel on a disabled pattern bit");
disp.menus[0].enabled = false;
disp.draw_menu_bar_to_fb(&mut bus);
assert_eq!(
packed_4bpp_screen_pixel_index(
&bus,
base,
row_bytes,
knocked_art_pixel.0,
knocked_art_pixel.1,
),
white,
"a disabled color mark should knock alternate art pixels into its normal background"
);
let disabled_before = bus.read_bytes(base, (row_bytes * 64) as usize);
disp.highlight_menu_title(&mut bus, 0);
assert_eq!(
packed_4bpp_screen_pixel_index(
&bus,
base,
row_bytes,
kept_art_pixel.0,
kept_art_pixel.1,
),
kept_art_pixel.2,
"selected disabled mark should retain art on enabled pattern bits"
);
assert_eq!(
packed_4bpp_screen_pixel_index(
&bus,
base,
row_bytes,
knocked_art_pixel.0,
knocked_art_pixel.1,
),
black,
"selected disabled mark should knock alternate art pixels into the reversed background"
);
disp.highlight_menu_title(&mut bus, 0);
assert_eq!(
bus.read_bytes(base, (row_bytes * 64) as usize),
disabled_before,
"two disabled-title reversals should restore the dimmed mark exactly"
);
disp.flash_menu_bar(&mut bus, 128);
assert_eq!(
packed_4bpp_screen_pixel_index(
&bus,
base,
row_bytes,
knocked_art_pixel.0,
knocked_art_pixel.1,
),
black,
"FlashMenuBar(menuID) should keep a disabled mark dimmed on the reversed background"
);
disp.flash_menu_bar(&mut bus, 128);
assert_eq!(
bus.read_bytes(base, (row_bytes * 64) as usize),
disabled_before,
"two disabled FlashMenuBar(menuID) reversals should restore the dimmed mark"
);
}
#[test]
fn drawmenubar_keeps_the_retro_computer_mark_legible_in_monochrome() {
let (mut disp, mut cpu, mut bus) = setup_with_port();
let row_bytes = 16;
let screen_base = bus.alloc(row_bytes * 64);
disp.set_screen_mode_for_test(screen_base, row_bytes, 128, 64, 1);
clear_1bpp_screen(&mut bus, screen_base, row_bytes, 64);
bus.write_long(crate::memory::globals::addr::SCRN_BASE, screen_base);
disp.menu_bar_hidden = false;
bus.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 20);
let system = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 128, 0x302300, "\u{14}");
insert_menu(&mut disp, &mut cpu, &mut bus, system);
disp.draw_menu_bar_to_fb(&mut bus);
for &(x, y) in &[
(1, 0),
(0, 1),
(9, 1),
(3, 4),
(6, 4),
(3, 6),
(6, 6),
(4, 7),
(5, 7),
(2, 11),
(7, 11),
] {
assert!(
screen_pixel_is_set(&bus, screen_base, row_bytes, 18 + x, 3 + y),
"dark mark pixel ({x}, {y})"
);
}
for &(x, y) in &[(0, 0), (1, 1), (3, 3), (5, 10)] {
assert!(
!screen_pixel_is_set(&bus, screen_base, row_bytes, 18 + x, 3 + y),
"light mark pixel ({x}, {y})"
);
}
}
#[test]
fn drawmenubar_8bpp_dims_a_disabled_title_to_solid_gray() {
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, 643, 0x303300, "File");
let city = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 644, 0x303340, "City");
insert_menu(&mut disp, &mut cpu, &mut bus, file);
insert_menu(&mut disp, &mut cpu, &mut bus, city);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 0);
bus.write_long(TEST_SP + 2, city);
assert!(
disp.dispatch_menu(true, 0x13A, &mut cpu, &mut bus)
.unwrap()
.is_ok(),
"DisableItem(menu, 0) should disable the whole City title"
);
disp.draw_menu_bar_to_fb(&mut bus);
let black = super::super::TrapDispatcher::fb_pixel_index_for_rgb(&bus, [0; 3]).unwrap();
let gray =
super::super::TrapDispatcher::fb_gray_pixel_index_between(&bus, [0xFFFF; 3], [0, 0, 0])
.expect("8bpp test screen should express an intermediate shade");
let regions = disp.menu_title_regions();
let cell = |(left, right): (i16, i16)| -> Vec<u8> {
(1..19)
.flat_map(|y| (left..right).map(move |x| (x, y)))
.map(|(x, y)| screen_pixel_index(&bus, base, row_bytes, x, y))
.collect()
};
let enabled = cell(regions[0]);
assert!(
enabled.iter().any(|&pixel| pixel == black),
"the enabled File title should draw in full black"
);
let dimmed = cell(regions[1]);
assert!(
dimmed.iter().any(|&pixel| pixel == gray),
"the disabled City title should draw in the intermediate grey shade"
);
assert!(
!dimmed.iter().any(|&pixel| pixel == black),
"no part of a dimmed title should stay full black"
);
}
#[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, 100, 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(test_tracked_menu_state(classic_menu, rect, 1));
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(test_tracked_menu_state(themed_menu, rect, 1));
themed.draw_menu_dropdown(&mut themed_bus, 0, rect);
assert!(
screen_pixel_is_set(&classic_bus, classic_base, classic_row_bytes, 23, 23),
"classic selected rows should reverse their full background"
);
assert!(
screen_pixel_is_set(&themed_bus, themed_base, themed_row_bytes, 23, 23),
"systemless-default provider should own highlighted menu-item row chrome"
);
assert!(
screen_pixel_is_set(&classic_bus, classic_base, classic_row_bytes, 100, 26),
"classic selected rows should remain filled away from the provider rail"
);
assert!(
!screen_pixel_is_set(&themed_bus, themed_base, themed_row_bytes, 100, 26),
"systemless-default highlight chrome should remain a provider-owned rail and frame"
);
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_preserves_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/1",
);
classic.menus[0].items[0].mark = 0x12;
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/1",
);
themed.menus[0].items[0].mark = 0x12;
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",
);
let command_left = rect.3 - 25;
let command_symbol_pixel = (row_top..row_bottom)
.flat_map(|y| (command_left..(command_left + 7)).map(move |x| (x, y)))
.find(|(x, y)| {
screen_pixel_is_set(&classic_bus, classic_base, classic_row_bytes, *x, *y)
})
.expect("classic command-key equivalent should draw the Command symbol itself");
let label_pixel = (row_top..row_bottom)
.flat_map(|y| ((rect.1 + 15)..(rect.1 + 60)).map(move |x| (x, y)))
.find(|(x, y)| {
screen_pixel_is_set(&classic_bus, classic_base, classic_row_bytes, *x, *y)
})
.expect("classic item label should draw");
assert!(
screen_pixel_is_set(
&themed_bus,
themed_base,
themed_row_bytes,
mark_pixel.0,
mark_pixel.1
),
"systemless-default row chrome should preserve the menu item's mark glyph"
);
assert!(
screen_pixel_is_set(
&themed_bus,
themed_base,
themed_row_bytes,
command_pixel.0,
command_pixel.1
),
"systemless-default row chrome should preserve the full command-key equivalent"
);
assert!(
screen_pixel_is_set(
&themed_bus,
themed_base,
themed_row_bytes,
command_symbol_pixel.0,
command_symbol_pixel.1
),
"systemless-default row chrome should preserve the Command symbol itself"
);
clear_1bpp_screen(&mut themed_bus, themed_base, themed_row_bytes, 342);
themed.menu_tracking = Some(test_tracked_menu_state(themed_menu, rect, 1));
themed.draw_menu_dropdown(&mut themed_bus, 0, rect);
assert!(
screen_pixel_is_set(
&themed_bus,
themed_base,
themed_row_bytes,
mark_pixel.0,
mark_pixel.1
),
"highlighted systemless-default row chrome should preserve the menu item's mark glyph"
);
assert!(
screen_pixel_is_set(
&themed_bus,
themed_base,
themed_row_bytes,
command_pixel.0,
command_pixel.1
),
"highlighted systemless-default row chrome should preserve the full command-key equivalent"
);
assert!(
screen_pixel_is_set(
&themed_bus,
themed_base,
themed_row_bytes,
command_symbol_pixel.0,
command_symbol_pixel.1
),
"highlighted systemless-default row chrome should preserve the Command symbol itself"
);
themed.redraw_chrome(&mut themed_bus);
for (pixel, label) in [
(mark_pixel, "checkmark"),
(label_pixel, "item label"),
(command_symbol_pixel, "Command symbol"),
(command_pixel, "command key"),
] {
assert!(
screen_pixel_is_set(&themed_bus, themed_base, themed_row_bytes, pixel.0, pixel.1),
"final themed chrome composition should preserve the highlighted {label}"
);
}
clear_1bpp_screen(&mut themed_bus, themed_base, themed_row_bytes, 342);
themed.menu_tracking = None;
themed.dialog_tracking = Some(super::super::dispatch::DialogTrackingState {
active_popup: Some(super::super::dispatch::DialogPopupTrackingState {
item_no: 1,
ctrl_handle: 0,
ctrl_ptr: 0,
active_menu: 0,
highlighted_item: 1,
saved_pixels: Vec::new(),
dropdown_rect: rect,
}),
..Default::default()
});
themed.draw_menu_dropdown(&mut themed_bus, 0, rect);
assert!(
screen_pixel_is_set(
&themed_bus,
themed_base,
themed_row_bytes,
rect.1 + 2,
row_top + 1
),
"dialog popup tracking should route the highlighted row through themed chrome"
);
themed.redraw_chrome(&mut themed_bus);
for (pixel, label) in [
(mark_pixel, "popup checkmark"),
(label_pixel, "popup item label"),
(command_symbol_pixel, "popup Command symbol"),
(command_pixel, "popup command key"),
] {
assert!(
screen_pixel_is_set(&themed_bus, themed_base, themed_row_bytes, pixel.0, pixel.1),
"final themed popup composition should preserve the highlighted {label}"
);
}
}
#[test]
fn draw_menu_dropdown_applies_setitemstyle_pixels_with_classic_style_metrics() {
let rect = (20, 20, 160, 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::standard_menu_row_height(
None,
false,
super::QuickDrawTextStyle::from_bits(super::QuickDrawTextStyle::SHADOW_BIT),
) - 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 super::QuickDrawTextStyle::from_bits(*style).shadow() {
plain_height.max(super::standard_menu_row_height(
None,
false,
super::QuickDrawTextStyle::from_bits(super::QuickDrawTextStyle::SHADOW_BIT),
))
} else {
plain_height
};
assert_eq!(
styled_height, expected_height,
"{label} style should use the classic MDEF row height"
);
assert_eq!(
styled.menu_item_icon_width(&styled_bus, &styled.menus[0].items[idx]),
plain.menu_item_icon_width(&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_dims_disabled_items_and_dividers_to_solid_gray() {
let rect = (20, 20, 70, 140);
let (mut disp, mut cpu, mut bus) = setup_with_port();
let (base, row_bytes) = setup_8bpp_menu_screen(&mut disp, &mut bus, 160, 96);
let menu = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 640, 0x303000, "Edit");
append_menu_data(
&mut disp,
&mut cpu,
&mut bus,
menu,
0x303040,
"Undo/Z;-;Copy",
);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 1);
bus.write_long(TEST_SP + 2, menu);
assert!(
disp.dispatch_menu(true, 0x13A, &mut cpu, &mut bus)
.unwrap()
.is_ok(),
"DisableItem should disable the first item"
);
disp.draw_menu_dropdown(&mut bus, 0, rect);
let background =
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();
let gray =
super::super::TrapDispatcher::fb_gray_pixel_index_between(&bus, [0xFFFF; 3], [0, 0, 0])
.expect("8bpp test screen should express an intermediate shade");
let row_pixels = |top: i16, height: i16| -> Vec<u8> {
(top..top + height)
.flat_map(|y| ((rect.1 + 1)..(rect.3 - 1)).map(move |x| (x, y)))
.map(|(x, y)| screen_pixel_index(&bus, base, row_bytes, x, y))
.collect()
};
let disabled = row_pixels(rect.0 + 1, MENU_ROW_HEIGHT - 1);
assert!(
disabled.iter().any(|&pixel| pixel == gray),
"the disabled item's text should draw in the intermediate grey shade"
);
assert!(
!disabled.iter().any(|&pixel| pixel == black),
"no part of a dimmed item should stay full black"
);
let divider_top = rect.0 + MENU_ROW_HEIGHT;
let divider_y = divider_top + super::STANDARD_MENU_SEPARATOR_HEIGHT / 2 - 1;
for x in (rect.1 + 1)..(rect.3 - 1) {
assert_eq!(
screen_pixel_index(&bus, base, row_bytes, x, divider_y),
gray,
"divider pixel at x={x}"
);
}
assert_eq!(
screen_pixel_index(&bus, base, row_bytes, rect.1 + 4, divider_y - 2),
background,
"the divider should be a single line, not a filled row"
);
let enabled = row_pixels(
divider_top + super::STANDARD_MENU_SEPARATOR_HEIGHT,
MENU_ROW_HEIGHT,
);
assert!(
enabled.iter().any(|&pixel| pixel == black),
"an enabled item's text should still draw in full black"
);
}
#[test]
fn draw_menu_dropdown_8bpp_dims_every_item_in_a_disabled_menu() {
let rect = (20, 20, 52, 140);
let (mut disp, mut cpu, mut bus) = setup_with_port();
let (base, row_bytes) = setup_8bpp_menu_screen(&mut disp, &mut bus, 160, 96);
let menu = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 645, 0x303680, "View");
append_menu_data(
&mut disp,
&mut cpu,
&mut bus,
menu,
0x3036C0,
"Zoom;Actual Size",
);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 0);
bus.write_long(TEST_SP + 2, menu);
disp.dispatch_menu(true, 0x13A, &mut cpu, &mut bus)
.unwrap()
.unwrap();
disp.draw_menu_dropdown(&mut bus, 0, rect);
let black = super::super::TrapDispatcher::fb_pixel_index_for_rgb(&bus, [0; 3]).unwrap();
let gray =
super::super::TrapDispatcher::fb_gray_pixel_index_between(&bus, [0xFFFF; 3], [0, 0, 0])
.expect("8bpp test screen should express an intermediate shade");
for row in 0..2 {
let pixels = ((rect.0 + row * MENU_ROW_HEIGHT + 1)
..(rect.0 + (row + 1) * MENU_ROW_HEIGHT - 1))
.flat_map(|y| ((rect.1 + 1)..(rect.3 - 1)).map(move |x| (x, y)))
.map(|(x, y)| screen_pixel_index(&bus, base, row_bytes, x, y))
.collect::<Vec<_>>();
assert!(
pixels.iter().any(|pixel| *pixel == gray),
"disabled menu row {} did not retain dimmed ink",
row + 1,
);
assert!(
!pixels.iter().any(|pixel| *pixel == black),
"disabled menu row {} retained enabled black ink",
row + 1,
);
}
}
#[test]
fn draw_menu_dropdown_1bpp_dims_disabled_items_with_the_gray_pattern() {
let rect = (20, 16, 60, 120);
let (mut disp, mut cpu, mut bus) = setup_with_port();
let row_bytes = 16u32;
let base = bus.alloc(row_bytes * 96);
disp.set_screen_mode_for_test(base, row_bytes, 128, 96, 1);
clear_1bpp_screen(&mut bus, base, row_bytes, 96);
bus.write_long(crate::memory::globals::addr::SCRN_BASE, base);
let menu = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 641, 0x303100, "Edit");
append_menu_data(&mut disp, &mut cpu, &mut bus, menu, 0x303140, "(Undo;-");
disp.draw_menu_dropdown(&mut bus, 0, rect);
let dimmed_row: Vec<(i16, i16)> = (rect.0..(rect.0 + MENU_ROW_HEIGHT))
.flat_map(|y| ((rect.1 + 1)..(rect.3 - 1)).map(move |x| (x, y)))
.collect();
assert!(
dimmed_row
.iter()
.any(|&(x, y)| screen_pixel_is_set(&bus, base, row_bytes, x, y)),
"the dimmed item should still leave legible ink on a 1-bit screen"
);
assert!(
dimmed_row
.iter()
.filter(|&&(x, y)| screen_pixel_is_set(&bus, base, row_bytes, x, y))
.all(|&(x, y)| (x + y) % 2 == 0),
"dimmed ink should survive only where the 50% grey pattern is on"
);
assert_eq!(
disp.menu_items_height(&bus, &disp.menus[0].items),
MENU_ROW_HEIGHT,
"a trailing divider should not claim a row"
);
}
#[test]
fn menu_layout_gives_a_trailing_divider_no_row() {
let (mut disp, mut cpu, mut bus) = setup_with_port();
setup_8bpp_menu_screen(&mut disp, &mut bus, 160, 96);
let menu = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 642, 0x303200, "\u{14}");
append_menu_data(
&mut disp,
&mut cpu,
&mut bus,
menu,
0x303240,
"About Systemless;-",
);
assert_eq!(disp.menus[0].items.len(), 2, "both items stay in the menu");
assert_eq!(
disp.menu_items_height(&bus, &disp.menus[0].items),
MENU_ROW_HEIGHT,
"only the About command claims a row"
);
append_menu_data(&mut disp, &mut cpu, &mut bus, menu, 0x303280, "Note Pad");
assert_eq!(
disp.menu_items_height(&bus, &disp.menus[0].items),
MENU_ROW_HEIGHT * 2 + super::STANDARD_MENU_SEPARATOR_HEIGHT,
"the divider claims its row again once an item follows it"
);
}
#[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_dropdown_remaps_cicn_color_table_to_main_screen_device() {
let rect = (20, 20, 38, 110);
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, 622, 0x302F00, "Color");
append_menu_data(&mut disp, &mut cpu, &mut bus, menu, 0x302F40, "Crop");
disp.menus[0].items[0].icon = 7;
let icon = cicn_source_with_solid_4bpp_color(3, [0, 0xFFFF, 0]);
disp.install_test_resource(&mut bus, *b"cicn", 263, &icon);
disp.draw_menu_dropdown(&mut bus, 0, rect);
let mapped_green =
super::super::TrapDispatcher::fb_main_screen_pixel_index_for_rgb(&bus, [0, 0xFFFF, 0])
.expect("main screen device should represent green");
assert_ne!(
mapped_green, 3,
"precondition: source icon index must differ from the device index"
);
assert_eq!(
screen_pixel_index(&bus, base, row_bytes, rect.1 + 4, rect.0 + 2),
mapped_green,
"cicn pixels should be mapped through their own ColorTable instead of copied as raw indexes"
);
}
#[test]
fn draw_menu_dropdown_4bpp_preserves_remapped_cicn_color_when_selected() {
let rect = (20, 20, 38, 110);
let (mut disp, mut cpu, mut bus) = setup_with_port();
let (base, row_bytes) = setup_4bpp_menu_screen(&mut disp, &mut bus, 128, 96);
let offscreen_gdevice = make_8bpp_current_gdevice(&mut bus);
bus.write_long(0x0CC8, offscreen_gdevice); let menu = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 622, 0x302F00, "Color");
append_menu_data(&mut disp, &mut cpu, &mut bus, menu, 0x302F40, "Crop");
disp.menus[0].items[0].icon = 7;
let icon = cicn_source_with_solid_4bpp_color(3, [0, 0xFFFF, 0]);
disp.install_test_resource(&mut bus, *b"cicn", 263, &icon);
disp.draw_menu_dropdown(&mut bus, 0, rect);
let icon_pixel = (rect.1 + 4, rect.0 + 2);
assert_eq!(
packed_4bpp_screen_pixel_index(&bus, base, row_bytes, icon_pixel.0, icon_pixel.1,),
8,
"4bpp cicn color should map through MainDevice instead of the foreign 8bpp TheGDevice"
);
disp.menu_tracking = Some(test_tracked_menu_state(menu, rect, 1));
disp.draw_menu_dropdown(&mut bus, 0, rect);
assert_eq!(
packed_4bpp_screen_pixel_index(&bus, base, row_bytes, icon_pixel.0, icon_pixel.1,),
8,
"selected color cicn artwork should keep its mapped 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 menu_chrome_4bpp_uses_main_device_standard_colors() {
let (mut disp, mut cpu, mut bus) = setup_with_port();
let (base, row_bytes) = setup_4bpp_menu_screen(&mut disp, &mut bus, 128, 96);
remap_main_device_mono_indexes(&mut bus, 5, 10);
let offscreen_gdevice = make_8bpp_current_gdevice(&mut bus);
bus.write_long(0x0CC8, offscreen_gdevice); 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, 620, 0x302E00, "File");
append_menu_data(&mut disp, &mut cpu, &mut bus, menu, 0x302E40, "Open/O");
insert_menu(&mut disp, &mut cpu, &mut bus, menu);
disp.draw_menu_bar_to_fb(&mut bus);
assert_eq!(
packed_4bpp_screen_pixel_index(&bus, base, row_bytes, 90, 5),
5,
"plain menu-bar background should use MainDevice white"
);
assert_eq!(
packed_4bpp_screen_pixel_index(&bus, base, row_bytes, 90, 19),
10,
"menu-bar bottom rule should use MainDevice black"
);
let mut rounded_corner_pixels = Vec::new();
crate::menu_manager::for_each_standard_menu_bar_corner_pixel(128, |x, y| {
rounded_corner_pixels.push((x, y));
});
for y in 0..5 {
for x in (0..6).chain(122..128) {
assert_eq!(
packed_4bpp_screen_pixel_index(&bus, base, row_bytes, x, y),
if rounded_corner_pixels.contains(&(x, y)) {
10
} else {
5
},
"rounded corner mask pixel ({x}, {y})",
);
}
}
let regions = disp.menu_title_regions();
assert!((1..19).any(|y| {
(regions[0].0..regions[0].1)
.any(|x| packed_4bpp_screen_pixel_index(&bus, base, row_bytes, x, y) == 10)
}));
let rect = (20, 21, 38, 111);
set_packed_4bpp_screen_pixel_index(&mut bus, (base, row_bytes, 128, 96), 20, 25, 7);
disp.draw_menu_dropdown(&mut bus, 0, rect);
assert_eq!(
packed_4bpp_screen_pixel_index(&bus, base, row_bytes, 22, 25),
5,
"plain dropdown background should use MainDevice white"
);
assert_eq!(
packed_4bpp_screen_pixel_index(&bus, base, row_bytes, 21, 25),
10,
"dropdown frame should use MainDevice black"
);
assert_eq!(
packed_4bpp_screen_pixel_index(&bus, base, row_bytes, 111, 25),
10,
"dropdown shadow should use MainDevice black"
);
assert_eq!(
packed_4bpp_screen_pixel_index(&bus, base, row_bytes, 20, 25),
7,
"drawing a low-nibble frame edge must preserve its packed neighbour"
);
let text_left = rect.1 + 15;
let text_right = text_left + super::super::TrapDispatcher::fb_measure_string("Open", 0, 12);
assert!(((rect.0 + 1)..(rect.0 + 17)).any(|y| {
(text_left..text_right)
.any(|x| packed_4bpp_screen_pixel_index(&bus, base, row_bytes, x, y) == 10)
}));
}
#[test]
fn menu_chrome_8bpp_uses_main_device_standard_colors() {
let (mut disp, mut cpu, mut bus) = setup_with_port();
let (base, row_bytes) = setup_8bpp_menu_screen(&mut disp, &mut bus, 128, 96);
remap_main_device_mono_indexes(&mut bus, 17, 23);
let offscreen_gdevice = make_8bpp_current_gdevice(&mut bus);
bus.write_long(0x0CC8, offscreen_gdevice); 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, 621, 0x302E80, "File");
append_menu_data(&mut disp, &mut cpu, &mut bus, menu, 0x302EC0, "Open/O");
insert_menu(&mut disp, &mut cpu, &mut bus, menu);
disp.draw_menu_bar_to_fb(&mut bus);
assert_eq!(screen_pixel_index(&bus, base, row_bytes, 90, 5), 17);
assert_eq!(screen_pixel_index(&bus, base, row_bytes, 90, 19), 23);
assert_eq!(screen_pixel_index(&bus, base, row_bytes, 0, 0), 23);
let rect = (20, 21, 38, 111);
disp.draw_menu_dropdown(&mut bus, 0, rect);
assert_eq!(screen_pixel_index(&bus, base, row_bytes, 22, 25), 17);
assert_eq!(screen_pixel_index(&bus, base, row_bytes, 21, 25), 23);
assert_eq!(screen_pixel_index(&bus, base, row_bytes, 111, 25), 23);
let text_left = rect.1 + 15;
let text_right = text_left + super::super::TrapDispatcher::fb_measure_string("Open", 0, 12);
assert!(((rect.0 + 1)..(rect.0 + 17)).any(|y| {
(text_left..text_right).any(|x| screen_pixel_index(&bus, base, row_bytes, x, y) == 23)
}));
}
#[test]
fn menu_dim_4bpp_uses_main_device_midpoint_color() {
let rect = (20, 20, 38, 110);
let (mut disp, mut cpu, mut bus) = setup_with_port();
let (base, row_bytes) = setup_4bpp_menu_screen(&mut disp, &mut bus, 128, 96);
remap_main_device_mono_indexes(&mut bus, 5, 10);
let offscreen_gdevice = make_8bpp_current_gdevice(&mut bus);
bus.write_long(0x0CC8, offscreen_gdevice);
let menu = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 621, 0x302EE0, "Dim");
append_menu_data(&mut disp, &mut cpu, &mut bus, menu, 0x302F20, "Unavailable");
disp.menus[0].items[0].enabled = false;
disp.draw_menu_dropdown(&mut bus, 0, rect);
let text_left = rect.1 + 15;
let text_right =
text_left + super::super::TrapDispatcher::fb_measure_string("Unavailable", 0, 12);
assert!(
((rect.0 + 1)..(rect.0 + 17)).any(|y| {
(text_left..text_right).any(|x| {
packed_4bpp_screen_pixel_index(&bus, base, row_bytes, x, y) == 0
})
}),
"disabled item should use MainDevice midpoint index 0 rather than the foreign active table"
);
}
#[test]
fn draw_menu_dropdown_1bpp_highlight_preserves_classic_row_inversion() {
let rect = (20, 20, 38, 120);
let (mut disp, mut cpu, mut bus) = setup_with_port();
let row_bytes = 16;
let base = bus.alloc(row_bytes * 96);
disp.set_screen_mode_for_test(base, row_bytes, 128, 96, 1);
clear_1bpp_screen(&mut bus, base, row_bytes, 96);
let menu = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 622, 0x302E80, "Mono");
append_menu_data(&mut disp, &mut cpu, &mut bus, menu, 0x302EC0, "Open/O");
disp.menus[0].items[0].mark = 0x12;
disp.menus[0].items[0].icon = 7;
let cicn = cicn_source_with_left_stripe(16, 16);
disp.install_test_resource(&mut bus, *b"cicn", 263, &cicn);
disp.draw_menu_dropdown(&mut bus, 0, rect);
let item_top = rect.0 + 1;
let item_bottom = item_top + 16;
let mark_pixel = (item_top..item_bottom)
.flat_map(|y| ((rect.1 + 3)..(rect.1 + 15)).map(move |x| (x, y)))
.find(|(x, y)| screen_pixel_is_set(&bus, base, row_bytes, *x, *y))
.expect("precondition: monochrome mark should draw");
let text_left = rect.1 + 34;
let text_right = text_left + super::super::TrapDispatcher::fb_measure_string("Open", 0, 12);
let name_pixel = (item_top..item_bottom)
.flat_map(|y| (text_left..text_right).map(move |x| (x, y)))
.find(|(x, y)| screen_pixel_is_set(&bus, base, row_bytes, *x, *y))
.expect("precondition: monochrome item name should draw");
let command_pixel = (item_top..item_bottom)
.flat_map(|y| ((rect.3 - 25)..(rect.3 - 1)).map(move |x| (x, y)))
.find(|(x, y)| screen_pixel_is_set(&bus, base, row_bytes, *x, *y))
.expect("precondition: monochrome command key should draw");
let icon_black_pixel = (rect.1 + 20, item_top);
let icon_white_pixel = (rect.1 + 18, item_top);
assert!(screen_pixel_is_set(
&bus,
base,
row_bytes,
icon_black_pixel.0,
icon_black_pixel.1
));
assert!(!screen_pixel_is_set(
&bus,
base,
row_bytes,
icon_white_pixel.0,
icon_white_pixel.1
));
let outside_edge = (rect.1, item_top + 4);
let outside_before =
screen_pixel_is_set(&bus, base, row_bytes, outside_edge.0, outside_edge.1);
let before = bus.read_bytes(base, (row_bytes * 96) as usize);
disp.menu_tracking = Some(test_tracked_menu_state(menu, rect, 1));
disp.draw_menu_dropdown(&mut bus, 0, rect);
for (component, pixel) in [
("mark", mark_pixel),
("name", name_pixel),
("command key", command_pixel),
("black cicn bit", icon_black_pixel),
] {
assert!(
!screen_pixel_is_set(&bus, base, row_bytes, pixel.0, pixel.1),
"selected monochrome {component} should be bit-inverted"
);
}
assert!(
screen_pixel_is_set(
&bus,
base,
row_bytes,
icon_white_pixel.0,
icon_white_pixel.1
),
"selected monochrome white cicn bit should be inverted to black"
);
assert_eq!(
screen_pixel_is_set(&bus, base, row_bytes, outside_edge.0, outside_edge.1,),
outside_before,
"non-byte-aligned selected row must preserve the border bit outside its rectangle"
);
disp.menu_tracking.as_mut().unwrap().highlighted_item = 0;
disp.draw_menu_dropdown(&mut bus, 0, rect);
assert_eq!(
bus.read_bytes(base, (row_bytes * 96) as usize),
before,
"redrawing the unselected 1bpp row should restore every framebuffer byte"
);
}
#[test]
fn draw_menu_dropdown_4bpp_highlight_reverses_all_item_component_colors() {
let rect = (20, 20, 38, 110);
let menu_id = 623;
let (clut, _) = super::super::TrapDispatcher::standard_mac_indexed_clut(4).unwrap();
let rgb = |index: usize| (clut[index][0], clut[index][1], clut[index][2]);
let white = rgb(0);
let red = rgb(3);
let blue = rgb(6);
let green = rgb(8);
let black = rgb(15);
let (mut disp, mut cpu, mut bus) = setup_with_port();
let (base, row_bytes) = setup_4bpp_menu_screen(&mut disp, &mut bus, 128, 96);
let offscreen_gdevice = make_8bpp_current_gdevice(&mut bus);
bus.write_long(0x0CC8, offscreen_gdevice); let menu = new_menu_with_title(&mut disp, &mut cpu, &mut bus, menu_id, 0x302F00, "Color");
append_menu_data(&mut disp, &mut cpu, &mut bus, menu, 0x302F40, "Open/O");
disp.menus[0].items[0].mark = 0x12;
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,
black,
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 edge_x = rect.1 + 1;
let edge_y = rect.0 + 5;
assert_eq!(
packed_4bpp_screen_pixel_index(&bus, base, row_bytes, edge_x, edge_y),
3,
"precondition: dropdown background should resolve to 4bpp red"
);
let packed_neighbor =
packed_4bpp_screen_pixel_index(&bus, base, row_bytes, edge_x - 1, edge_y);
let mark_pixel = ((rect.0 + 1)..(rect.0 + 17))
.flat_map(|y| ((rect.1 + 3)..(rect.1 + 15)).map(move |x| (x, y)))
.find(|(x, y)| packed_4bpp_screen_pixel_index(&bus, base, row_bytes, *x, *y) == 8)
.expect("precondition: item RGB1 green mark should draw");
let text_left = rect.1 + 15;
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 name_pixel = (text_top..text_bottom)
.flat_map(|y| (text_left..text_right).map(move |x| (x, y)))
.find(|(x, y)| packed_4bpp_screen_pixel_index(&bus, base, row_bytes, *x, *y) == 6)
.expect("precondition: item RGB2 blue text should draw");
let command_pixel = (text_top..text_bottom)
.flat_map(|y| ((rect.3 - 25)..(rect.3 - 1)).map(move |x| (x, y)))
.find(|(x, y)| packed_4bpp_screen_pixel_index(&bus, base, row_bytes, *x, *y) == 15)
.expect("precondition: item RGB3 black command key should draw");
let before = bus.read_bytes(base, (row_bytes * 96) as usize);
disp.menu_tracking = Some(test_tracked_menu_state(menu, rect, 1));
disp.draw_menu_dropdown(&mut bus, 0, rect);
assert_eq!(
packed_4bpp_screen_pixel_index(&bus, base, row_bytes, edge_x, edge_y),
6,
"selected 4bpp row should use item RGB2 as its background"
);
for (component, pixel) in [
("mark", mark_pixel),
("name", name_pixel),
("command key", command_pixel),
] {
assert_eq!(
packed_4bpp_screen_pixel_index(&bus, base, row_bytes, pixel.0, pixel.1),
3,
"selected 4bpp {component} should use item RGB4/menu background"
);
}
assert_eq!(
packed_4bpp_screen_pixel_index(&bus, base, row_bytes, edge_x - 1, edge_y),
packed_neighbor,
"redrawing a selected odd pixel must preserve the neighbouring high nibble"
);
disp.menu_tracking.as_mut().unwrap().highlighted_item = 0;
disp.draw_menu_dropdown(&mut bus, 0, rect);
assert_eq!(
bus.read_bytes(base, (row_bytes * 96) as usize),
before,
"redrawing the unselected 4bpp row should restore every packed framebuffer byte"
);
}
#[test]
fn draw_menu_dropdown_8bpp_highlight_reverses_all_item_component_colors() {
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();
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/O");
disp.menus[0].items[0].mark = 0x12;
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,
black,
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 + 1;
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 mark_pixel = ((rect.0 + 1)..(rect.0 + 17))
.flat_map(|y| ((rect.1 + 3)..(rect.1 + 15)).map(move |x| (x, y)))
.find(|(x, y)| screen_pixel_index(&bus, base, row_bytes, *x, *y) == 185)
.expect("precondition: item RGB1 green mark should draw");
let text_left = rect.1 + 15;
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 name_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");
let command_pixel = (text_top..text_bottom)
.flat_map(|y| ((rect.3 - 25)..(rect.3 - 1)).map(move |x| (x, y)))
.find(|(x, y)| screen_pixel_index(&bus, base, row_bytes, *x, *y) == 255)
.expect("precondition: item RGB3 black command key should draw");
let before = bus.read_bytes(base, (row_bytes * 96) as usize);
disp.menu_tracking = Some(test_tracked_menu_state(menu, rect, 1));
disp.draw_menu_dropdown(&mut bus, 0, rect);
assert_eq!(
screen_pixel_index(&bus, base, row_bytes, bg_x, bg_y),
210,
"selected 8bpp row should use item RGB2 as its background"
);
for (component, pixel) in [
("mark", mark_pixel),
("name", name_pixel),
("command key", command_pixel),
] {
assert_eq!(
screen_pixel_index(&bus, base, row_bytes, pixel.0, pixel.1),
35,
"selected 8bpp {component} should use item RGB4/menu background"
);
}
disp.menu_tracking.as_mut().unwrap().highlighted_item = 0;
disp.draw_menu_dropdown(&mut bus, 0, rect);
assert_eq!(
bus.read_bytes(base, (row_bytes * 96) as usize),
before,
"redrawing the unselected 8bpp row should restore every framebuffer byte"
);
}
#[test]
fn draw_menu_bar_4bpp_highlight_reverses_title_and_background_colors() {
let file_id = 625;
let (clut, _) = super::super::TrapDispatcher::standard_mac_indexed_clut(4).unwrap();
let rgb = |index: usize| (clut[index][0], clut[index][1], clut[index][2]);
let white = rgb(0);
let red = rgb(3);
let blue = rgb(6);
let (mut disp, mut cpu, mut bus) = setup_with_port();
let (base, row_bytes) = setup_4bpp_menu_screen(&mut disp, &mut bus, 128, 64);
let offscreen_gdevice = make_8bpp_current_gdevice(&mut bus);
bus.write_long(0x0CC8, offscreen_gdevice); 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, 0x303080, "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 classic_left = regions[0].0 - 2;
assert_eq!(
classic_left & 1,
1,
"test edge should occupy the low nibble"
);
let edge_y = 5;
assert_eq!(
packed_4bpp_screen_pixel_index(&bus, base, row_bytes, classic_left, edge_y),
3,
"precondition: menu bar should use its 4bpp red background"
);
let packed_neighbor =
packed_4bpp_screen_pixel_index(&bus, base, row_bytes, classic_left - 1, edge_y);
let unrelated_x = regions[0].1 + 1;
set_packed_4bpp_screen_pixel_index(
&mut bus,
(base, row_bytes, 128, 64),
unrelated_x,
edge_y,
8,
);
let blue_title_pixel = (1..19)
.flat_map(|y| (regions[0].0..regions[0].1).map(move |x| (x, y)))
.find(|(x, y)| packed_4bpp_screen_pixel_index(&bus, base, row_bytes, *x, *y) == 6)
.expect("precondition: menu-bar RGB1 blue title should draw");
let before = bus.read_bytes(base, (row_bytes * 64) as usize);
disp.highlight_menu_title(&mut bus, 0);
assert_eq!(
packed_4bpp_screen_pixel_index(&bus, base, row_bytes, classic_left, edge_y),
6,
"4bpp menu-title highlighting should replace RGB2 background with RGB1 title color"
);
assert_eq!(
packed_4bpp_screen_pixel_index(&bus, base, row_bytes, unrelated_x, edge_y),
8,
"4bpp menu-title highlighting should preserve unrelated indexed colors"
);
assert_eq!(
packed_4bpp_screen_pixel_index(
&bus,
base,
row_bytes,
blue_title_pixel.0,
blue_title_pixel.1,
),
3,
"4bpp menu-title highlighting should replace RGB1 title pixels with RGB2 background"
);
assert_eq!(
packed_4bpp_screen_pixel_index(&bus, base, row_bytes, classic_left - 1, edge_y),
packed_neighbor,
"highlighting a low nibble must preserve its neighbour outside the title rect"
);
disp.highlight_menu_title(&mut bus, 0);
assert_eq!(
bus.read_bytes(base, (row_bytes * 64) as usize),
before,
"highlighting the same packed title twice should restore every framebuffer byte"
);
}
#[test]
fn draw_menu_bar_8bpp_highlight_reverses_title_and_background_colors() {
let file_id = 625;
let red = (0xFFFF, 0, 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, 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 unrelated_x = regions[0].1 - 2;
let unrelated_y = 5;
bus.write_byte(
base + (unrelated_y as u32) * row_bytes + unrelated_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");
let before = bus.read_bytes(base, (row_bytes * 64) as usize);
disp.highlight_menu_title(&mut bus, 0);
assert_eq!(
screen_pixel_index(&bus, base, row_bytes, bg_x, bg_y),
210,
"8bpp menu-title highlighting should replace RGB2 background with RGB1 title color"
);
assert_eq!(
screen_pixel_index(&bus, base, row_bytes, unrelated_x, unrelated_y),
185,
"8bpp menu-title highlighting should preserve unrelated indexed colors"
);
assert_eq!(
screen_pixel_index(
&bus,
base,
row_bytes,
blue_title_pixel.0,
blue_title_pixel.1
),
35,
"8bpp menu-title highlighting should replace RGB1 title pixels with RGB2 background"
);
disp.highlight_menu_title(&mut bus, 0);
assert_eq!(
bus.read_bytes(base, (row_bytes * 64) as usize),
before,
"highlighting the same 8bpp title twice should restore every framebuffer byte"
);
}
#[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;
let expected_width = disp
.standard_menu_width(&bus, &disp.menus[0].items)
.max(regions[0].1 - regions[0].0 + 20);
let expected_rect = (
20,
expected_left,
20 + disp.menu_items_height(&bus, &disp.menus[0].items),
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 shared standard MDEF 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;
let expected_width = disp
.standard_menu_width(&bus, &disp.menus[0].items)
.max(regions[0].1 - regions[0].0 + 20);
let wide_item_width =
super::super::TrapDispatcher::fb_measure_string("Wide Underline", 0, 12) + 32;
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 Mac OS 8.1 standard width"
);
assert_eq!(
disp.menu_tracking.as_ref().unwrap().dropdown_rect(),
(
20,
expected_left,
20 + disp.menu_items_height(&bus, &disp.menus[0].items),
expected_left + expected_width,
),
"live pull-down geometry should match the System 7 plain-row width reference"
);
}
#[test]
fn script_code_item_does_not_reserve_icon_geometry() {
let (mut disp, mut cpu, mut bus) = setup_with_port();
let menu = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 629, 0x303700, "Script");
append_menu_data(&mut disp, &mut cpu, &mut bus, menu, 0x303740, "Localized");
disp.menus[0].items[0].icon = 7;
disp.menus[0].items[0].key_equiv = 0x1C;
let cicn = cicn_source_with_left_stripe(24, 20);
disp.install_test_resource(&mut bus, *b"cicn", 263, &cicn);
assert_eq!(
disp.menu_item_icon_width(&bus, &disp.menus[0].items[0]),
0,
"$1C makes the icon byte a script code even when a matching cicn exists",
);
assert_eq!(disp.menu_item_height(&bus, &disp.menus[0].items[0]), 16);
assert_eq!(
disp.cicn_menu_icon_resource_ptr(&disp.menus[0].items[0]),
None,
);
}
#[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;
let expected_height = 34 + 34 + 34 + 34 + super::STANDARD_MENU_SEPARATOR_HEIGHT + 16;
let expected_width = disp
.standard_menu_width(&bus, &disp.menus[0].items)
.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,
"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!(
disp.menu_item_icon_width(&bus, &disp.menus[0].items[0]),
32,
"normal icon-number rows should reserve a 32-pixel icon slot"
);
assert_eq!(
disp.dropdown_item_at_point(&bus, rect.1 + 5, rect.0 + 33),
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 + 141),
0,
"hit testing should leave the full separator row unselectable"
);
assert_eq!(
disp.dropdown_item_at_point(&bus, rect.1 + 5, rect.0 + 142),
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,
"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,
"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 + 19),
1,
"hit testing should include the full cicn-height first row"
);
assert_eq!(
disp.dropdown_item_at_point(&bus, rect.1 + 5, rect.0 + 20),
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,
"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,
"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 + 33),
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 + 34),
2,
"hit testing below the normal ICON row should hit the following item"
);
}
#[test]
fn retained_selection_uses_guest_menu_handle_after_cache_reordering() {
let (mut disp, mut cpu, mut bus) = setup();
let first = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 701, 0x302680, "First");
append_menu_data(&mut disp, &mut cpu, &mut bus, first, 0x3026C0, "Wrong");
let retained =
new_menu_with_title(&mut disp, &mut cpu, &mut bus, 702, 0x302700, "Retained");
append_menu_data(&mut disp, &mut cpu, &mut bus, retained, 0x302740, "Right");
disp.menu_tracking = Some(test_tracked_menu_state(retained, (20, 10, 38, 100), 1));
disp.menus.swap(0, 1);
assert_eq!(
disp.menu_tracking_selection_result(&bus),
(702u32 << 16) | 1,
"retained MenuSelect identity must survive presentation-cache reordering"
);
}
#[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,
"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 popupmenuselect_with_mouse_button_up_returns_zero_immediately() {
let (mut disp, mut cpu, mut bus) = setup_with_port();
disp.menu_bar_hidden = false;
disp.mouse_button = false;
bus.write_byte(crate::memory::globals::addr::MB_STATE, 0x80); bus.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 20);
let menu = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 402, 0x302000, "Pop");
append_menu_data(
&mut disp,
&mut cpu,
&mut bus,
menu,
0x302040,
"First;Second;Third",
);
insert_menu_before_id(&mut disp, &mut cpu, &mut bus, menu, -1);
let sp = TEST_SP;
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, 1); bus.write_word(sp + 2, 100); bus.write_word(sp + 4, 50); 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 argument frame"
);
assert_eq!(
bus.read_long(sp + 10),
0,
"PopUpMenuSelect must return 0 when called with mouse button up"
);
assert!(
disp.menu_tracking.is_none(),
"PopUpMenuSelect must not start menu tracking when button is up"
);
}
#[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"
);
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,
"DeleteMenu should remove the handle before disposal"
);
assert!(
bus.get_alloc_size(menu_ptr).is_some(),
"DeleteMenu must leave the menu record allocated"
);
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"
);
}
#[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_retains_one_live_title_and_drawmenubar_replays_it() {
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, 243, 0x30B800, "File");
append_menu_data(&mut disp, &mut cpu, &mut bus, file, 0x30B840, "Open/O");
insert_menu(&mut disp, &mut cpu, &mut bus, file);
let child = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 244, 0x30B880, "Recent");
insert_menu_before(&mut disp, &mut cpu, &mut bus, child, -1);
disp.draw_menu_bar_to_fb(&mut bus);
let normal = bus.read_bytes(base, (row_bytes * 64) as usize);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 243);
disp.dispatch_menu(true, 0x138, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(bus.read_word(crate::memory::globals::addr::THE_MENU), 243);
let highlighted = bus.read_bytes(base, (row_bytes * 64) as usize);
assert_ne!(highlighted, normal, "HiliteMenu must highlight its title");
disp.draw_menu_bar_to_fb(&mut bus);
assert_eq!(
bus.read_bytes(base, (row_bytes * 64) as usize),
highlighted,
"DrawMenuBar must replay the title retained in TheMenu"
);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 244);
disp.dispatch_menu(true, 0x138, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(
bus.read_word(crate::memory::globals::addr::THE_MENU),
0,
"a submenu ID has no title for HiliteMenu to highlight"
);
assert_eq!(bus.read_bytes(base, (row_bytes * 64) as usize), normal);
}
#[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 flashmenubar_zero_4bpp_swaps_bar_and_default_title_colors() {
let (clut, _) = super::super::TrapDispatcher::standard_mac_indexed_clut(4).unwrap();
let rgb = |index: usize| (clut[index][0], clut[index][1], clut[index][2]);
let white = rgb(0);
let red = rgb(3);
let blue = rgb(6);
let green = rgb(8);
let (mut disp, mut cpu, mut bus) = setup_with_port();
let (base, row_bytes) = setup_4bpp_menu_screen(&mut disp, &mut bus, 128, 64);
let offscreen_gdevice = make_8bpp_current_gdevice(&mut bus);
bus.write_long(0x0CC8, offscreen_gdevice); 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, 702, 0x306080, "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, green, red);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, entries_ptr);
bus.write_word(TEST_SP + 4, 1);
disp.dispatch_menu(true, 0x265, &mut cpu, &mut bus)
.unwrap()
.unwrap();
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)| packed_4bpp_screen_pixel_index(&bus, base, row_bytes, *x, *y) == 6)
.expect("precondition: default RGB1 blue title should draw before flashing");
assert_eq!(
packed_4bpp_screen_pixel_index(&bus, base, row_bytes, 90, 5),
3,
"precondition: menu-bar RGB4 red should fill the packed bar"
);
assert_eq!(
packed_4bpp_screen_pixel_index(&bus, base, row_bytes, 0, 0),
15,
"precondition: DrawMenuBar should stamp the packed top corner mask"
);
set_packed_4bpp_screen_pixel_index(&mut bus, (base, row_bytes, 128, 64), 100, 5, 8);
let before = bus.read_bytes(base, (row_bytes * 64) as usize);
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_eq!(
packed_4bpp_screen_pixel_index(&bus, base, row_bytes, 90, 5),
6,
"FlashMenuBar(0) should replace RGB4 background with default RGB1 title color"
);
assert_eq!(
packed_4bpp_screen_pixel_index(&bus, base, row_bytes, title_pixel.0, title_pixel.1,),
3,
"FlashMenuBar(0) should replace default RGB1 title pixels with RGB4 background"
);
assert_eq!(
packed_4bpp_screen_pixel_index(&bus, base, row_bytes, 100, 5),
8,
"FlashMenuBar(0) should preserve unrelated packed color indices"
);
assert_eq!(
packed_4bpp_screen_pixel_index(&bus, base, row_bytes, 0, 0),
15,
"FlashMenuBar(0) should preserve the packed 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_eq!(
bus.read_bytes(base, (row_bytes * 64) as usize),
before,
"a second packed FlashMenuBar(0) call should restore every framebuffer byte"
);
}
#[test]
fn flashmenubar_zero_8bpp_swaps_bar_and_default_title_colors() {
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, 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, 703, 0x306100, "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, green, red);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, entries_ptr);
bus.write_word(TEST_SP + 4, 1);
disp.dispatch_menu(true, 0x265, &mut cpu, &mut bus)
.unwrap()
.unwrap();
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_index(&bus, base, row_bytes, *x, *y) == 210)
.expect("precondition: default RGB1 blue title should draw before flashing");
assert_eq!(
screen_pixel_index(&bus, base, row_bytes, 90, 5),
35,
"precondition: menu-bar RGB4 red should fill the 8bpp bar"
);
bus.write_byte(base + 5 * row_bytes + 100, 185);
let before = bus.read_bytes(base, (row_bytes * 64) as usize);
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_eq!(
screen_pixel_index(&bus, base, row_bytes, 90, 5),
210,
"FlashMenuBar(0) should replace RGB4 background with default RGB1 title color"
);
assert_eq!(
screen_pixel_index(&bus, base, row_bytes, title_pixel.0, title_pixel.1),
35,
"FlashMenuBar(0) should replace default RGB1 title pixels with RGB4 background"
);
assert_eq!(
screen_pixel_index(&bus, base, row_bytes, 100, 5),
185,
"FlashMenuBar(0) should preserve unrelated indexed colors"
);
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_eq!(
bus.read_bytes(base, (row_bytes * 64) as usize),
before,
"a second 8bpp FlashMenuBar(0) call should restore every framebuffer byte"
);
}
#[test]
fn flashmenubar_zero_ignores_title_only_background_colors() {
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, 128, 64);
disp.menu_bar_hidden = false;
bus.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 20);
let menu_id = 704;
let file = new_menu_with_title(&mut disp, &mut cpu, &mut bus, menu_id, 0x306180, "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, menu_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, 1);
disp.dispatch_menu(true, 0x265, &mut cpu, &mut bus)
.unwrap()
.unwrap();
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_index(&bus, base, row_bytes, *x, *y) == 210)
.expect("precondition: title-only RGB1 blue should draw");
let title_bg_pixel = (regions[0].0 - 2, 5);
assert_eq!(
screen_pixel_index(&bus, base, row_bytes, 90, 5),
0,
"without MCEntry(0,0), StandardMBDF should keep the full bar standard white"
);
assert_eq!(
screen_pixel_index(&bus, base, row_bytes, title_bg_pixel.0, title_bg_pixel.1,),
35,
"StandardMBDF should erase the individual title cell to its RGB2 color"
);
let before = bus.read_bytes(base, (row_bytes * 64) as usize);
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_eq!(
screen_pixel_index(&bus, base, row_bytes, 90, 5),
255,
"FlashMenuBar(0) should use standard black when MCEntry(0,0) is absent"
);
assert_eq!(
screen_pixel_index(&bus, base, row_bytes, title_pixel.0, title_pixel.1),
210,
"whole-bar flash should preserve a per-title RGB1 color"
);
assert_eq!(
screen_pixel_index(&bus, base, row_bytes, title_bg_pixel.0, title_bg_pixel.1,),
35,
"whole-bar flash should preserve the per-title RGB2 background"
);
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_eq!(bus.read_bytes(base, (row_bytes * 64) as usize), before);
}
#[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_dynamic_menu_list_copy_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 menu_list = menu_list_from_memory(&bus, list_handle)
.expect("returned handle should contain a DynamicMenuList");
assert_eq!(
menu_list.regular_handles().collect::<Vec<_>>(),
vec![file, edit],
"snapshot should preserve current regular-menu 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);
let current_menu_list = bus.read_long(crate::memory::globals::addr::MENU_LIST);
assert_ne!(current_menu_list, 0);
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_eq!(
bus.read_long(crate::memory::globals::addr::MENU_LIST),
current_menu_list,
"SetMenuBar should preserve the manager-owned current-list Handle"
);
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"
);
bus.write_long(saved_menu_list, 0);
assert_eq!(
menu_list_from_memory(&bus, current_menu_list)
.expect("current list must outlive the caller-owned source")
.regular_handles()
.collect::<Vec<_>>(),
vec![file, edit],
"SetMenuBar must copy the list before the caller disposes its Handle"
);
}
#[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"));
}
#[test]
fn menuselect_disabled_item_updates_live_menuchoice_result() {
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 handle = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 520, 0x30B980, "File");
append_menu_data(
&mut disp,
&mut cpu,
&mut bus,
handle,
0x30B9C0,
"Open/O;Close/W",
);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 2);
bus.write_long(TEST_SP + 2, handle);
disp.dispatch_menu(true, 0x13A, &mut cpu, &mut bus)
.unwrap()
.unwrap();
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();
let title = disp.menu_title_regions()[0];
let title_mid_h = (title.0 + title.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();
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();
let expected = menu_choice_value(520, 2);
assert_eq!(
bus.read_long(crate::memory::globals::addr::MENU_DISABLE),
expected,
);
assert_eq!(
disp.menu_tracking
.as_ref()
.map(|tracking| tracking.highlighted_item),
Some(0),
"a disabled row must not become the MenuSelect highlight",
);
disp.mouse_button = false;
disp.dispatch_menu(true, 0x13D, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(bus.read_long(TEST_SP + 4), 0);
assert!(disp.menu_tracking.is_none());
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, 0xDEAD_BEEF);
disp.dispatch_menu(true, 0x266, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(bus.read_long(TEST_SP), expected);
assert_eq!(cpu.read_reg(Register::A7), TEST_SP);
}
#[test]
fn menuselect_observes_live_item_disable_during_tracking() {
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 handle = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 523, 0x30BD00, "File");
append_menu_data(&mut disp, &mut cpu, &mut bus, handle, 0x30BD40, "Open/O");
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();
let title = disp.menu_title_regions()[0];
let title_mid_h = (title.0 + title.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();
let menu_ptr = bus.read_long(handle);
let enable_flags = bus.read_long(menu_ptr + 10);
assert_ne!(enable_flags & (1 << 1), 0, "item 1 should start enabled");
bus.write_long(menu_ptr + 10, enable_flags & !(1 << 1));
let (top, left, _, _) = disp.menu_tracking.as_ref().unwrap().dropdown_rect();
disp.mouse_pos = (top + 8, left + 8);
disp.dispatch_menu(true, 0x13D, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(
bus.read_long(crate::memory::globals::addr::MENU_DISABLE),
menu_choice_value(523, 1),
);
assert_eq!(
disp.menu_tracking
.as_ref()
.map(|tracking| tracking.highlighted_item),
Some(0),
"the live-disabled item became highlighted",
);
disp.mouse_button = false;
disp.dispatch_menu(true, 0x13D, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(bus.read_long(TEST_SP + 4), 0);
assert!(disp.menu_tracking.is_none());
}
#[test]
fn menuselect_disabled_menu_opens_without_selecting_an_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 handle = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 522, 0x30BC00, "View");
append_menu_data(&mut disp, &mut cpu, &mut bus, handle, 0x30BC40, "Zoom/Z");
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 0);
bus.write_long(TEST_SP + 2, handle);
disp.dispatch_menu(true, 0x13A, &mut cpu, &mut bus)
.unwrap()
.unwrap();
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();
let (screen_base, row_bytes, _, screen_height, _) = disp.get_screen_params();
let screen_len = usize::try_from(
row_bytes.saturating_mul(u32::try_from(screen_height.max(0)).unwrap_or(0)),
)
.unwrap();
let screen_before = bus.read_bytes(screen_base, screen_len);
let title = disp.menu_title_regions()[0];
let title_mid_h = (title.0 + title.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();
let (top, left, _, _) = disp
.menu_tracking
.as_ref()
.expect("disabled title should still open")
.dropdown_rect();
disp.mouse_pos = (top + 8, left + 8);
disp.dispatch_menu(true, 0x13D, &mut cpu, &mut bus)
.unwrap()
.unwrap();
let expected = menu_choice_value(522, 1);
assert_eq!(
bus.read_long(crate::memory::globals::addr::MENU_DISABLE),
expected,
);
assert_eq!(
disp.menu_tracking
.as_ref()
.map(|tracking| tracking.highlighted_item),
Some(0),
);
disp.mouse_button = false;
disp.dispatch_menu(true, 0x13D, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(bus.read_long(TEST_SP + 4), 0);
assert!(disp.menu_tracking.is_none());
assert_eq!(
bus.read_long(crate::memory::globals::addr::MENU_DISABLE),
expected,
);
assert_eq!(
bus.read_bytes(screen_base, screen_len),
screen_before,
"disabled menu tracking did not restore the classic framebuffer",
);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, 0xDEAD_BEEF);
disp.dispatch_menu(true, 0x266, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(bus.read_long(TEST_SP), expected);
}
#[test]
fn setmenuflash_zero_returns_release_selection_without_blinking() {
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 handle = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 521, 0x30BA00, "File");
append_menu_data(
&mut disp,
&mut cpu,
&mut bus,
handle,
0x30BA40,
"Open/O;Save/S",
);
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();
let title = disp.menu_title_regions()[0];
let title_mid_h = (title.0 + title.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();
let (top, left, _, _) = disp.menu_tracking.as_ref().unwrap().dropdown_rect();
disp.mouse_pos = (top + 17, left + 8);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 0);
disp.dispatch_menu(true, 0x14A, &mut cpu, &mut bus)
.unwrap()
.unwrap();
disp.mouse_button = false;
cpu.write_reg(Register::A7, TEST_SP);
disp.dispatch_menu(true, 0x13D, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(disp.menu_tracking, None);
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
assert_eq!(bus.read_long(TEST_SP + 4), 0x0209_0002);
}
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 menukey_does_not_paint_titles_while_menu_bar_is_hidden() {
for (menu_bar_hidden, fullscreen_locked) in [(true, false), (false, true)] {
let (mut disp, mut cpu, mut bus) = setup_with_port();
let row_bytes = 512;
let height = 342;
let base = bus.alloc(row_bytes * height);
disp.set_screen_mode_for_test(base, row_bytes, 512, height as u16, 8);
clear_1bpp_screen(&mut bus, base, row_bytes, height);
bus.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 20);
let file = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 703, 0x30BA00, "File");
append_menu_data(&mut disp, &mut cpu, &mut bus, file, 0x30BA40, "New/N");
insert_menu(&mut disp, &mut cpu, &mut bus, file);
assert!(disp.menus[0].visible_in_menu_bar);
disp.menu_bar_hidden = menu_bar_hidden;
disp.fullscreen_locked = fullscreen_locked;
let before = bus.read_bytes(base, row_bytes as usize * 20);
let (result, stack_after) =
menu_key_result_and_stack(&mut disp, &mut cpu, &mut bus, b'N');
assert_eq!(result, 0x02BF_0001);
assert_eq!(stack_after, TEST_SP + 2);
assert_eq!(
bus.read_bytes(base, row_bytes as usize * 20),
before,
"MenuKey must not expose a title highlight while menu chrome is hidden"
);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 0);
disp.dispatch_menu(true, 0x138, &mut cpu, &mut bus)
.unwrap()
.unwrap();
disp.draw_menu_bar_to_fb(&mut bus);
assert_eq!(
bus.read_bytes(base, row_bytes as usize * 20),
before,
"clearing and compositing hidden menu chrome must leave the framebuffer unchanged"
);
}
}
#[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"
);
}
#[test]
fn popupmenuselect_faults_in_cicn_items_before_sizing_and_drawing() {
let (mut disp, mut cpu, mut bus) = setup_with_port();
let row_bytes = 64;
let base = bus.alloc(row_bytes * 160);
disp.set_screen_mode_for_test(base, row_bytes, 512, 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, 733, 0x30BE00, "Crops");
append_menu_data(&mut disp, &mut cpu, &mut bus, menu, 0x30BE40, "Corn;Wheat");
disp.menus[0].items[0].icon = 19;
let cicn = cicn_source_with_left_stripe(16, 16);
disp.install_test_resource(&mut bus, *b"cicn", 275, &cicn);
disp.resources
.as_mut()
.unwrap()
.files
.get_mut(&0)
.unwrap()
.loaded
.insert((*b"cicn", 275), 0);
assert!(
disp.find_loaded_resource_any(*b"cicn", 275).is_none(),
"precondition: the color icon should be nonresident"
);
insert_menu_before_id(&mut disp, &mut cpu, &mut bus, menu, -1);
dispatch_popupmenuselect_start(&mut disp, &mut cpu, &mut bus, menu, 50, 40, 0);
let rect = disp.menu_tracking.as_ref().unwrap().dropdown_rect();
assert_eq!(
rect.2 - rect.0,
16 + 16,
"popup geometry should use the loaded 16-pixel cicn rows"
);
assert!(
disp.find_loaded_resource_any(*b"cicn", 275).is_some(),
"PopUpMenuSelect should materialize a nonresident item icon"
);
assert!(
screen_pixel_is_set(&bus, base, row_bytes, rect.1 + 4, rect.0 + 2),
"the popup should draw the application color icon"
);
}
#[test]
fn popupmenuselect_tracks_oversized_content_from_shared_layout() {
let (mut disp, mut cpu, mut bus) = setup_with_port();
let row_bytes = 100;
let base = bus.alloc(row_bytes * 600);
disp.set_screen_mode_for_test(base, row_bytes, 800, 600, 1);
clear_1bpp_screen(&mut bus, base, row_bytes, 600);
bus.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 20);
disp.menu_bar_hidden = false;
disp.mouse_button = true;
let menu = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 734, 0x30BF00, "Long");
let description = (0..40).map(|_| "A").collect::<Vec<_>>().join(";");
append_menu_data(&mut disp, &mut cpu, &mut bus, menu, 0x30BF40, &description);
insert_menu_before_id(&mut disp, &mut cpu, &mut bus, menu, -1);
dispatch_popupmenuselect_start(&mut disp, &mut cpu, &mut bus, menu, 100, 120, 20);
let tracking = disp.menu_tracking.as_ref().expect("popup tracking active");
assert_eq!(tracking.popup_top, 4);
assert_eq!(tracking.popup_height, 576);
assert_eq!(tracking.content_top, -204);
assert_eq!(tracking.highlighted_item, 20);
assert_eq!(
bus.read_word(crate::memory::globals::addr::TOP_MENU_ITEM) as i16,
-204,
);
assert_eq!(
bus.read_word(crate::memory::globals::addr::AT_MENU_BOTTOM) as i16,
436,
);
assert_eq!(
disp.dropdown_item_at_point(&bus, tracking.popup_left + 4, 100),
20
);
assert!(
screen_pixel_is_set(
&bus,
base,
row_bytes,
tracking.popup_left + tracking.popup_width / 2,
tracking.popup_top + 4,
),
"the hidden upper content must replace the first visible row with an indicator"
);
let popup_left = tracking.popup_left;
disp.update_menu_tracking_for_point(&mut bus, popup_left + 4, 28);
assert_eq!(disp.menu_tracking.as_ref().unwrap().highlighted_item, 15);
disp.update_menu_tracking_for_point(&mut bus, popup_left + 4, 12);
assert_eq!(disp.menu_tracking.as_ref().unwrap().content_top, -204);
assert_eq!(disp.menu_tracking.as_ref().unwrap().highlighted_item, 0);
disp.update_menu_tracking_for_point(&mut bus, popup_left + 4, 12);
assert_eq!(disp.menu_tracking.as_ref().unwrap().content_top, -188);
assert_eq!(
bus.read_word(crate::memory::globals::addr::TOP_MENU_ITEM) as i16,
-188,
);
assert_eq!(
bus.read_word(crate::memory::globals::addr::AT_MENU_BOTTOM) as i16,
452,
);
disp.update_menu_tracking_for_point(&mut bus, popup_left + 4, 3);
assert_eq!(disp.menu_tracking.as_ref().unwrap().content_top, -172);
assert_eq!(
bus.read_word(crate::memory::globals::addr::AT_MENU_BOTTOM) as i16,
468,
);
}
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);
disp.mouse_pos = (58, 35);
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, (26, 30, 90, 98));
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, (95, 168, 159, 236));
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 + disp.menu_rows(&bus, &disp.menus[0].items).offset(4) + 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_routes_hierarchical_child_through_its_custom_mdef() {
let (mut disp, mut cpu, mut bus) = setup_with_port();
setup_8bpp_menu_screen(&mut disp, &mut bus, 240, 120);
disp.menu_bar_hidden = false;
bus.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 20);
let original_port = disp.current_port;
let root = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 140, 0x310600, "File");
append_menu_data(
&mut disp,
&mut cpu,
&mut bus,
root,
0x310640,
"Custom child",
);
let child = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 141, 0x310800, "Child");
append_menu_data(
&mut disp,
&mut cpu,
&mut bus,
child,
0x310840,
"Opaque definition data",
);
set_item_cmd(&mut disp, &mut cpu, &mut bus, root, 1, 0x1B);
set_item_mark(&mut disp, &mut cpu, &mut bus, root, 1, 141);
let child_ptr = bus.read_long(child);
bus.write_word(child_ptr + 2, 72);
bus.write_word(child_ptr + 4, 32);
let mdef_ptr = bus.alloc(2);
let mdef_handle = bus.alloc(4);
bus.write_word(mdef_ptr, 0x4E75);
bus.write_long(mdef_handle, mdef_ptr);
bus.write_long(child_ptr + 6, mdef_handle);
disp.loaded_handles
.insert(mdef_handle, (mdef_ptr, *b"MDEF", 256));
insert_menu(&mut disp, &mut cpu, &mut bus, root);
insert_menu_before(&mut disp, &mut cpu, &mut bus, child, -1);
cpu.write_reg(Register::A7, TEST_SP);
disp.dispatch_menu(true, 0x137, &mut cpu, &mut bus)
.unwrap()
.unwrap();
let title = disp.menu_title_regions()[0];
let title_mid_h = (title.0 + title.1) / 2;
let trap_pc = 0x0012_3600;
cpu.write_reg(Register::PC, trap_pc + 2);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 10);
bus.write_word(TEST_SP + 2, title_mid_h as u16);
disp.mouse_pos = (10, title_mid_h);
disp.mouse_button = true;
disp.dispatch_menu(true, 0x13D, &mut cpu, &mut bus)
.unwrap()
.unwrap();
let root_rect = disp.menu_tracking.as_ref().unwrap().dropdown_rect();
disp.mouse_pos = (root_rect.0 + 8, root_rect.1 + 16);
cpu.write_reg(Register::PC, trap_pc + 2);
cpu.write_reg(Register::A7, TEST_SP);
disp.dispatch_menu(true, 0x13D, &mut cpu, &mut bus)
.unwrap()
.unwrap();
let trampoline = disp.menu_def_trampoline;
let child_rect = disp.menu_tracking.as_ref().unwrap().submenus[0].dropdown_rect();
assert_eq!(child_rect.3 - child_rect.1, 72);
assert_eq!(child_rect.2 - child_rect.0, 32);
assert_eq!(bus.read_word(trampoline + 6), 0);
assert_eq!(bus.read_long(trampoline + 10), child);
assert_eq!(
disp.menu_tracking
.as_ref()
.unwrap()
.active_definition_pane(),
Some(super::SharedMenuDefinitionPane::Submenu(0))
);
disp.mouse_pos = (10, title_mid_h);
cpu.write_reg(Register::PC, trap_pc + 2);
cpu.write_reg(Register::A7, TEST_SP);
disp.dispatch_menu(true, 0x13D, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(bus.read_word(trampoline + 6), 1);
bus.write_word(trampoline + 58, 0);
cpu.write_reg(Register::PC, trap_pc + 2);
cpu.write_reg(Register::A7, TEST_SP);
disp.dispatch_menu(true, 0x13D, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert!(disp.menu_tracking.as_ref().unwrap().submenus.is_empty());
assert_eq!(disp.current_port, original_port);
disp.mouse_pos = (root_rect.0 + 8, root_rect.1 + 16);
cpu.write_reg(Register::PC, trap_pc + 2);
cpu.write_reg(Register::A7, TEST_SP);
disp.dispatch_menu(true, 0x13D, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(bus.read_word(trampoline + 6), 0);
disp.mouse_pos = (child_rect.0 + 8, child_rect.1 + 8);
cpu.write_reg(Register::PC, trap_pc + 2);
cpu.write_reg(Register::A7, TEST_SP);
disp.dispatch_menu(true, 0x13D, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(bus.read_word(trampoline + 6), 1);
assert_eq!(bus.read_long(trampoline + 10), child);
bus.write_word(trampoline + 58, 2);
disp.mouse_button = false;
cpu.write_reg(Register::PC, trap_pc + 2);
cpu.write_reg(Register::A7, TEST_SP);
disp.dispatch_menu(true, 0x13D, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(
disp.menu_tracking.as_ref().unwrap().flash_result,
(141u32 << 16) | 2
);
let tracking = disp.menu_tracking.as_mut().unwrap();
tracking.flash_remaining = 1;
tracking.flash_delay = 0;
cpu.write_reg(Register::PC, trap_pc + 2);
cpu.write_reg(Register::A7, TEST_SP);
disp.dispatch_menu(true, 0x13D, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(bus.read_long(TEST_SP + 4), (141u32 << 16) | 2);
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
assert_eq!(disp.menu_tracking, None);
}
#[test]
fn menuselect_tracks_nested_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 game = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 129, 0x310000, "Game");
append_menu_data(&mut disp, &mut cpu, &mut bus, game, 0x310040, "Options");
let options = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 138, 0x310200, "Options");
append_menu_data(&mut disp, &mut cpu, &mut bus, options, 0x310240, "Speed");
let speed = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 139, 0x310400, "Speed");
append_menu_data(
&mut disp,
&mut cpu,
&mut bus,
speed,
0x310440,
"Cycle;Fast;Moderate;Slow",
);
set_item_cmd(&mut disp, &mut cpu, &mut bus, game, 1, 0x1B);
set_item_mark(&mut disp, &mut cpu, &mut bus, game, 1, 138);
set_item_cmd(&mut disp, &mut cpu, &mut bus, options, 1, 0x1B);
set_item_mark(&mut disp, &mut cpu, &mut bus, options, 1, 139);
set_item_cmd(&mut disp, &mut cpu, &mut bus, speed, 1, 0x1B);
set_item_mark(&mut disp, &mut cpu, &mut bus, speed, 1, 138);
insert_menu(&mut disp, &mut cpu, &mut bus, game);
insert_menu_before(&mut disp, &mut cpu, &mut bus, options, -1);
insert_menu_before(&mut disp, &mut cpu, &mut bus, speed, -1);
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 game_mid_h = (regions[0].0 + regions[0].1) / 2;
disp.mouse_pos = (10, game_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, game_mid_h as u16);
bus.write_long(TEST_SP + 4, 0xFFFF_FFFF);
disp.dispatch_menu(true, 0x13D, &mut cpu, &mut bus)
.unwrap()
.unwrap();
let root_rect = disp.menu_tracking.as_ref().unwrap().dropdown_rect();
disp.mouse_pos = (root_rect.0 + 9, root_rect.1 + 24);
disp.dispatch_menu(true, 0x13D, &mut cpu, &mut bus)
.unwrap()
.unwrap();
let options_rect = disp.menu_tracking.as_ref().unwrap().submenus[0].dropdown_rect();
disp.mouse_pos = (options_rect.0 + 9, options_rect.1 + 24);
disp.dispatch_menu(true, 0x13D, &mut cpu, &mut bus)
.unwrap()
.unwrap();
let speed_rect = disp.menu_tracking.as_ref().unwrap().submenus[1].dropdown_rect();
disp.mouse_pos = (speed_rect.0 + 9, speed_rect.1 + 24);
disp.dispatch_menu(true, 0x13D, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(
disp.menu_tracking.as_ref().unwrap().submenus.len(),
2,
"a circular submenu must not grow the retained hierarchy"
);
disp.mouse_pos = (speed_rect.0 + 1 + 16 + 8, speed_rect.1 + 24);
disp.dispatch_menu(true, 0x13D, &mut cpu, &mut bus)
.unwrap()
.unwrap();
disp.mouse_button = false;
for _ in 0..40 {
disp.dispatch_menu(true, 0x13D, &mut cpu, &mut bus)
.unwrap()
.unwrap();
if disp.menu_tracking.is_none() {
break;
}
}
assert_eq!(bus.read_long(TEST_SP + 4), (139u32 << 16) | 2);
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
}
#[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"
);
assert_eq!(
disp.menu_tracking
.as_ref()
.expect("MenuSelect should be tracking")
.menu_handle,
edit,
"MenuSelect should open the regular menu after a hierarchical menu"
);
}
#[test]
fn save_and_restore_dropdown_pixels_4bpp_round_trip_odd_nibble_bounds() {
let (mut disp, _cpu, mut bus) = setup();
let (base, row_bytes) = setup_4bpp_menu_screen(&mut disp, &mut bus, 16, 4);
let byte_len = row_bytes * 4;
for offset in 0..byte_len {
bus.write_byte(
base + offset,
(offset as u8).wrapping_mul(7).wrapping_add(3),
);
}
let before = bus.read_bytes(base, byte_len as usize);
let rect = (0, 3, 1, 8);
let saved = disp.save_dropdown_pixels(&bus, rect);
assert_eq!(saved.len(), 2 * 4);
for y in 0..2u32 {
for byte_x in 1..5u32 {
bus.write_byte(base + y * row_bytes + byte_x, 0xAA);
}
}
disp.restore_dropdown_pixels(&mut bus, rect, &saved);
assert_eq!(
bus.read_bytes(base, byte_len as usize),
before,
"4bpp save/restore should preserve both boundary nibbles byte-for-byte"
);
}
#[test]
fn save_and_restore_dropdown_pixels_2bpp_round_trip_odd_field_bounds() {
let (mut disp, _cpu, mut bus) = setup();
let row_bytes = 4u32;
let height = 4u32;
let base = bus.alloc(row_bytes * height);
disp.screen_mode = (base, row_bytes, 12, height as u16, 2);
for offset in 0..row_bytes * height {
bus.write_byte(
base + offset,
(offset as u8).wrapping_mul(11).wrapping_add(5),
);
}
let before = bus.read_bytes(base, (row_bytes * height) as usize);
let rect = (0, 3, 1, 8);
let saved = disp.save_dropdown_pixels(&bus, rect);
assert_eq!(saved.len(), 2 * 3);
for y in 0..2u32 {
for byte_x in 0..3u32 {
bus.write_byte(base + y * row_bytes + byte_x, 0xAA);
}
}
disp.restore_dropdown_pixels(&mut bus, rect, &saved);
assert_eq!(
bus.read_bytes(base, (row_bytes * height) as usize),
before,
"2bpp save/restore should preserve boundary fields without touching row padding"
);
}
#[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"
);
}
#[test]
fn guest_menu_snapshot_exposes_only_the_inserted_menu_list() {
let (mut disp, mut cpu, mut bus) = setup();
let inserted = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 100, 0x306A80, "File");
let item_ptr = 0x306A90;
let item = b"New Level\xC9/N";
bus.write_byte(item_ptr, item.len() as u8);
bus.write_bytes(item_ptr + 1, item);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, item_ptr);
bus.write_long(TEST_SP + 4, inserted);
disp.dispatch_menu(true, 0x133, &mut cpu, &mut bus)
.unwrap()
.unwrap();
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 0x0100);
bus.write_word(TEST_SP + 2, 1);
bus.write_long(TEST_SP + 4, inserted);
disp.dispatch_menu(true, 0x145, &mut cpu, &mut bus)
.unwrap()
.unwrap();
insert_menu(&mut disp, &mut cpu, &mut bus, inserted);
let system_menu =
new_menu_with_title(&mut disp, &mut cpu, &mut bus, 102, 0x306AA0, "\u{14}");
insert_menu(&mut disp, &mut cpu, &mut bus, system_menu);
let _detached =
new_menu_with_title(&mut disp, &mut cpu, &mut bus, 101, 0x306AB0, "Detached");
let snapshot = disp.guest_menu_snapshot(&bus);
assert_eq!(snapshot.menus.len(), 2);
assert_eq!(snapshot.menus[0].id, 100);
assert_eq!(snapshot.menus[0].items[0].number, 1);
assert_eq!(snapshot.menus[0].items[0].text, "New Level…");
assert_eq!(snapshot.menus[0].items[0].key_equivalent, Some('n'));
assert!(snapshot.menus[0].items[0].checked);
assert_eq!(snapshot.menus[1].title, "Systemless");
}
#[test]
fn native_selection_returns_through_menuselect_pascal_frame() {
let (mut disp, mut cpu, mut bus) = setup();
let menu = new_menu_with_title(&mut disp, &mut cpu, &mut bus, -120, 0x306AC0, "Game");
append_menu_data(&mut disp, &mut cpu, &mut bus, menu, 0x306AD0, "Pause");
insert_menu(&mut disp, &mut cpu, &mut bus, menu);
assert!(disp.queue_native_menu_selection(&bus, -120, 1).is_some());
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP + 4, 0xDEAD_BEEF);
let result = disp
.dispatch_menu(true, 0x13D, &mut cpu, &mut bus)
.expect("MenuSelect handled");
assert!(result.is_ok());
assert_eq!(bus.read_long(TEST_SP + 4), 0xFF88_0001);
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
assert!(disp.pending_native_menu_selection.is_none());
assert!(disp.pending_native_menu_event.is_none());
assert!(disp.pending_native_menu_event_tick.is_none());
}
#[test]
fn native_selection_rejects_disabled_and_hierarchical_parent_items() {
let (mut disp, mut cpu, mut bus) = setup();
let menu = new_menu_with_title(&mut disp, &mut cpu, &mut bus, 100, 0x306AE0, "File");
append_menu_data(
&mut disp,
&mut cpu,
&mut bus,
menu,
0x306AF0,
"Disabled;Recent",
);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 1);
bus.write_long(TEST_SP + 2, menu);
disp.dispatch_menu(true, 0x13A, &mut cpu, &mut bus)
.unwrap()
.unwrap();
set_item_cmd(&mut disp, &mut cpu, &mut bus, menu, 2, 0x1B);
set_item_mark(&mut disp, &mut cpu, &mut bus, menu, 2, 7);
insert_menu(&mut disp, &mut cpu, &mut bus, menu);
assert_eq!(disp.queue_native_menu_selection(&bus, 100, 1), None);
assert_eq!(disp.queue_native_menu_selection(&bus, 100, 2), None);
assert_eq!(disp.queue_native_menu_selection(&bus, 999, 1), None);
}
}