Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions src/trap/framebuffer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

use std::{collections::HashSet, sync::OnceLock};

use super::menu::menu_text_for_render;
use crate::memory::{MacMemoryBus, MemoryBus};
use crate::quickdraw::fonts::{heuristics::get_italic_slant, Glyph};
use crate::quickdraw::text::{
Expand Down Expand Up @@ -2287,8 +2288,8 @@ impl super::TrapDispatcher {
if !menu.visible_in_menu_bar {
continue;
}
let title = &menu.title;
let title_width = Self::menu_title_advance(title);
let title = menu_text_for_render(&menu.title);
let title_width = Self::menu_title_advance(&title);
// HIG 1992 p. 54 says unavailable menu titles remain visible
// but dimmed; p. 55 says pressing a menu title highlights it.
// Route title-state chrome through the provider while keeping
Expand Down Expand Up @@ -2316,7 +2317,7 @@ impl super::TrapDispatcher {
} else {
None
};
let system_mark = Self::is_system_menu_mark_title(title);
let system_mark = Self::is_system_menu_mark_title(&title);
// The retro mark is multi-coloured original artwork, so it dims
// through the pattern path on every screen depth rather than
// collapsing to a single grey silhouette.
Expand All @@ -2342,7 +2343,7 @@ impl super::TrapDispatcher {
screen_height,
x,
text_y,
title,
&title,
font_id,
font_size,
0,
Expand All @@ -2358,7 +2359,7 @@ impl super::TrapDispatcher {
screen_height,
x,
text_y,
title,
&title,
font_id,
font_size,
)
Expand Down Expand Up @@ -2431,7 +2432,8 @@ impl super::TrapDispatcher {
if Self::is_system_menu_mark_title(title) {
SYSTEM_MENU_MARK_TITLE_ADVANCE
} else {
Self::fb_measure_string(title, 0, 12)
let title = menu_text_for_render(title);
Self::fb_measure_string(&title, 0, 12)
}
}

Expand Down
83 changes: 73 additions & 10 deletions src/trap/menu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
use crate::cpu::{CpuOps, Register};
use crate::memory::{globals::addr, MacMemoryBus, MemoryBus};
use crate::menu_model::{GuestMenu, GuestMenuItem, GuestMenuSnapshot};
use crate::trap::types::{decode_mac_roman, encode_mac_roman_lossy};
use crate::trap::types::{decode_mac_roman, decode_mac_roman_for_render, encode_mac_roman_lossy};
use crate::ui_theme::UiThemeId;
use crate::Result;

Expand Down Expand Up @@ -199,6 +199,16 @@ fn internal_menu_string_to_unicode(value: &str) -> String {
decode_mac_roman(&internal_menu_string_bytes(value))
}

/// Convert the byte-preserving menu string representation into the text that
/// the framebuffer renderer can draw. MENU resource strings are MacRoman
/// bytes (Inside Macintosh Volume I, I-345); keeping their bytes in `String`
/// lets the Menu Manager round-trip them, but the Chicago replacement font
/// does not contain every MacRoman glyph. The render conversion expands the
/// classic ellipsis byte ($C9) and other punctuation into available glyphs.
pub(crate) fn menu_text_for_render(value: &str) -> String {
decode_mac_roman_for_render(&internal_menu_string_bytes(value))
}

/// Parse a MENU resource from guest memory into a Menu struct.
fn parse_menu_resource(bus: &MacMemoryBus, res_ptr: u32, handle: u32) -> Menu {
let menu_id = bus.read_word(res_ptr) as i16;
Expand Down Expand Up @@ -2795,7 +2805,8 @@ impl super::TrapDispatcher {

let mut max_width: i16 = 0;
for item in &menu.items {
let w = Self::fb_measure_string(&item.text, 0, 12);
let item_text = menu_text_for_render(&item.text);
let w = Self::fb_measure_string(&item_text, 0, 12);
let total = w + self.menu_item_width_extra(bus, item) + 24;
if total > max_width {
max_width = total;
Expand Down Expand Up @@ -4282,7 +4293,8 @@ impl super::TrapDispatcher {

let mut width: i16 = 0;
for item in &menu.items {
let w = Self::fb_measure_string(&item.text, 0, 12)
let item_text = menu_text_for_render(&item.text);
let w = Self::fb_measure_string(&item_text, 0, 12)
+ self.menu_item_width_extra(bus, item)
+ 26;
width = width.max(w);
Expand Down Expand Up @@ -4352,7 +4364,8 @@ impl super::TrapDispatcher {
};
let mut max_width = min_width;
for item in &menu.items {
let w = Self::fb_measure_string(&item.text, 0, 12);
let item_text = menu_text_for_render(&item.text);
let w = Self::fb_measure_string(&item_text, 0, 12);
let total =
w + self.menu_item_width_extra(bus, item) + Self::menu_item_pulldown_padding(item);
max_width = max_width.max(total);
Expand Down Expand Up @@ -4965,6 +4978,7 @@ impl super::TrapDispatcher {
let item_height = self.menu_item_height(bus, item);
let item_bottom = item_top + item_height;
let is_separator = item.text == "-";
let item_text = menu_text_for_render(&item.text);
let mark_pixel_index =
Self::menu_item_component_pixel_index(bus, menu.id, item_no, pixel_size, 4);
let name_pixel_index =
Expand Down Expand Up @@ -5172,7 +5186,7 @@ impl super::TrapDispatcher {
screen_height,
text_left,
text_y,
&item.text,
&item_text,
font_id,
font_size,
item.style,
Expand All @@ -5188,7 +5202,7 @@ impl super::TrapDispatcher {
screen_height,
text_left,
text_y,
&item.text,
&item_text,
font_id,
font_size,
item.style,
Expand Down Expand Up @@ -5603,6 +5617,7 @@ impl super::TrapDispatcher {
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;
let title = menu_text_for_render(&menu.title);
Self::fb_draw_string_styled_ink(
bus,
screen_base,
Expand All @@ -5612,7 +5627,7 @@ impl super::TrapDispatcher {
screen_height,
left + 7,
text_y,
&menu.title,
&title,
font_id,
font_size,
0,
Expand Down Expand Up @@ -5672,9 +5687,9 @@ impl super::TrapDispatcher {
mod tests {
use super::super::test_helpers::{setup, setup_with_port, MockCpu, TEST_SP};
use super::{
count_menu_items_from_memory, parse_appendmenu_items, parse_menu_resource, Menu, MenuItem,
MenuTrackingState, MC_ENTRY_SIZE, MENU_KEY_REDUCED_ICON, MENU_KEY_SMALL_ICON,
MENU_ROW_HEIGHT,
count_menu_items_from_memory, menu_text_for_render, parse_appendmenu_items,
parse_menu_resource, Menu, MenuItem, MenuTrackingState, MC_ENTRY_SIZE,
MENU_KEY_REDUCED_ICON, MENU_KEY_SMALL_ICON, MENU_ROW_HEIGHT,
};
use crate::cpu::{CpuOps, Register};
use crate::memory::{MacMemoryBus, MemoryBus};
Expand Down Expand Up @@ -9293,6 +9308,54 @@ mod tests {
}

// 0x137 — DrawMenuBar: no stack params, calls draw_menu_bar_to_fb.
#[test]
fn menu_render_text_keeps_macroman_ellipsis_geometry_and_pixels_consistent() {
const STORED: &str = "Open Game\u{C9}";
const RENDERED: &str = "Open Game...";

assert_eq!(menu_text_for_render(STORED), RENDERED);
assert_eq!(
super::super::TrapDispatcher::fb_measure_string(&menu_text_for_render(STORED), 0, 12,),
super::super::TrapDispatcher::fb_measure_string(RENDERED, 0, 12),
"MacRoman ellipsis measurement must use the same expanded text as drawing"
);
assert_eq!(
super::super::TrapDispatcher::menu_title_advance(STORED),
super::super::TrapDispatcher::menu_title_advance(RENDERED),
"menu-bar title geometry must use the render-facing ellipsis"
);

let capture = |title: &str, item_text: &str| {
let (mut disp, mut cpu, mut bus) = setup_with_port();
let (base, row_bytes) = setup_8bpp_menu_screen(&mut disp, &mut bus, 200, 100);
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, 650, 0x303900, "File");
append_menu_data(&mut disp, &mut cpu, &mut bus, menu, 0x303940, "Open Game");
disp.menus[0].title = title.to_string();
disp.menus[0].items[0].text = item_text.to_string();
insert_menu(&mut disp, &mut cpu, &mut bus, menu);

let title_width = super::super::TrapDispatcher::menu_title_advance(title);
let dropdown_width = disp.dropdown_width_for_menu(&bus, 0, 100);
disp.draw_menu_bar_to_fb(&mut bus);
disp.draw_menu_dropdown(&mut bus, 0, (20, 20, 60, dropdown_width + 20));

let pixels = (0..row_bytes * 100)
.map(|offset| bus.read_byte(base + offset))
.collect::<Vec<_>>();
(pixels, title_width, dropdown_width)
};

let (stored_pixels, stored_title_width, stored_dropdown_width) = capture(STORED, STORED);
let (rendered_pixels, rendered_title_width, rendered_dropdown_width) =
capture(RENDERED, RENDERED);

assert_eq!(stored_title_width, rendered_title_width);
assert_eq!(stored_dropdown_width, rendered_dropdown_width);
assert_eq!(stored_pixels, rendered_pixels);
}

#[test]
fn test_draw_menu_bar() {
let (mut disp, mut cpu, mut bus) = setup_with_port();
Expand Down
89 changes: 78 additions & 11 deletions src/trap/resource.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4240,10 +4240,13 @@ impl super::TrapDispatcher {
// FUNCTION PBHGetVInfo (paramBlock: HParmBlkPtr; async: Boolean): OSErr;
// Inside Macintosh Volume II, II-104; HFS variant Files 1992,
// 2-144 (subsection begins line 8018; trap macro `_HGetVolInfo`
// per line 8077).
// The OS-trap dispatcher masks `trap & 0x00FF`, so $A207
// PBHGetVInfo and $A007 PBGetVInfo land on the same low
// byte and share this arm.
// per line 8077). Inside Macintosh Volume IV (1986),
// IV-129–IV-130 shows that PBGetVInfo's classic block ends at
// ioVFrBlk (offset 62), while PBHGetVInfo's larger block adds
// ioVSigWord and the other HFS fields beginning at offset 64.
// The dispatcher masks `trap & 0x00FF`, so A007/A207/A607 share
// this prefix arm; retain the extended tail only for an original
// trap word carrying the HFS bit.
//
// PBGetVInfo ($A007): Returns mounted volume info and free-space figures
// PBHGetVInfo ($A207): HFS variant aliased onto $A007
Expand Down Expand Up @@ -4393,13 +4396,15 @@ impl super::TrapDispatcher {
bus.write_word(pb + 56, allocation_start); // ioAlBlSt
bus.write_long(pb + 58, next_catalog_id); // ioVNxtCNID
bus.write_word(pb + 62, free_blocks); // ioVFrBlk
bus.write_word(pb + 64, 0x4244); // ioVSigWord (HFS)

// The extracted volume is a File Manager abstraction, not a
// claim about the source image's physical device or driver.
bus.write_word(pb + 66, 0); // ioVDrvInfo
bus.write_word(pb + 68, 0); // ioVDRefNum
bus.write_word(pb + 70, 0); // ioVFSID (File Manager)
if self.current_trap_word & 0x0200 != 0 {
bus.write_word(pb + 64, 0x4244); // ioVSigWord (HFS)

// The extracted volume is a File Manager abstraction, not a
// claim about the source image's physical device or driver.
bus.write_word(pb + 66, 0); // ioVDrvInfo
bus.write_word(pb + 68, 0); // ioVDRefNum
bus.write_word(pb + 70, 0); // ioVFSID (File Manager)
}
bus.write_word(pb + 16, 0); // noErr
cpu.write_reg(Register::D0, 0);
Ok(())
Expand Down Expand Up @@ -15079,6 +15084,68 @@ mod tests {
assert_eq!(len, 11);
}

#[test]
fn pb_get_vinfo_preserves_bytes_after_classic_volume_parameter_block() {
let (mut disp, mut cpu, mut bus) = setup();
let pb = 0x300000u32;
cpu.write_reg(Register::A0, pb);
bus.write_word(pb + 28, 1);
bus.write_long(pb + 64, 0xCAFE_BABE);

call_trap_word(&mut disp, 0xA007, &mut cpu, &mut bus).unwrap();

assert_eq!(cpu.read_reg(Register::D0), 0);
assert_eq!(bus.read_long(pb + 64), 0xCAFE_BABE);
}

#[test]
fn pb_get_vinfo_async_trap_word_preserves_bytes_after_classic_volume_parameter_block() {
let (mut disp, mut cpu, mut bus) = setup();
let pb = 0x300000u32;
cpu.write_reg(Register::A0, pb);
bus.write_word(pb + 28, 1);
bus.write_long(pb + 64, 0xCAFE_BABE);

call_trap_word(&mut disp, 0xA407, &mut cpu, &mut bus).unwrap();

assert_eq!(cpu.read_reg(Register::D0), 0);
assert_eq!(bus.read_long(pb + 64), 0xCAFE_BABE);
}

#[test]
fn pb_hget_vinfo_populates_extended_volume_parameter_fields() {
let (mut disp, mut cpu, mut bus) = setup();
let pb = 0x300000u32;
cpu.write_reg(Register::A0, pb);
bus.write_word(pb + 28, 1);
bus.write_long(pb + 64, 0xCAFE_BABE);

call_trap_word(&mut disp, 0xA207, &mut cpu, &mut bus).unwrap();

assert_eq!(cpu.read_reg(Register::D0), 0);
assert_eq!(bus.read_word(pb + 64), 0x4244);
assert_eq!(bus.read_word(pb + 66), 0);
assert_eq!(bus.read_word(pb + 68), 0);
assert_eq!(bus.read_word(pb + 70), 0);
}

#[test]
fn pb_hget_vinfo_async_trap_word_also_populates_extended_fields() {
let (mut disp, mut cpu, mut bus) = setup();
let pb = 0x300000u32;
cpu.write_reg(Register::A0, pb);
bus.write_word(pb + 28, 1);
bus.write_long(pb + 64, 0xCAFE_BABE);

call_trap_word(&mut disp, 0xA607, &mut cpu, &mut bus).unwrap();

assert_eq!(cpu.read_reg(Register::D0), 0);
assert_eq!(bus.read_word(pb + 64), 0x4244);
assert_eq!(bus.read_word(pb + 66), 0);
assert_eq!(bus.read_word(pb + 68), 0);
assert_eq!(bus.read_word(pb + 70), 0);
}

#[test]
fn pb_hget_vinfo_positive_index_one_returns_boot_volume() {
let (mut disp, mut cpu, mut bus) = setup();
Expand Down
Loading