use std::{collections::HashSet, sync::OnceLock};
use crate::memory::{MacMemoryBus, MemoryBus};
use crate::quickdraw::fonts::{heuristics::get_italic_slant, Glyph};
use crate::quickdraw::text::{
get_font_metrics, get_glyph, get_glyph_italic, get_underline_thickness,
};
use crate::ui_theme::{
CaretState, ControlKind, ControlState, DialogFrameKind, DialogFrameState, MenuBarState,
MenuDropdownState, MenuItemState, MenuTitleState, Rgb8, ScrollbarOrientation, ScrollbarPart,
ScrollbarState, TextFieldState, TextSelectionState, ThemeBitmap, ThemeDrawCtx, ThemeRect,
UiThemeId,
};
static NO_VISRGN_AUTO_EXPAND: OnceLock<bool> = OnceLock::new();
fn no_visrgn_auto_expand_enabled() -> bool {
*NO_VISRGN_AUTO_EXPAND
.get_or_init(|| std::env::var_os("SYSTEMLESS_NO_VISRGN_AUTO_EXPAND").is_some())
}
const TEXT_STYLE_BOLD: u8 = 0x01;
const TEXT_STYLE_ITALIC: u8 = 0x02;
const TEXT_STYLE_UNDERLINE: u8 = 0x04;
const TEXT_STYLE_OUTLINE: u8 = 0x08;
const TEXT_STYLE_SHADOW: u8 = 0x10;
const TEXT_STYLE_CONDENSE: u8 = 0x20;
const TEXT_STYLE_EXTEND: u8 = 0x40;
const STANDARD_GRAY_PATTERN: [u8; 8] = [0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55];
const SYSTEM_MENU_MARK_TITLE_ADVANCE: i16 = 11;
impl super::TrapDispatcher {
pub(super) fn get_screen_params(&self) -> (u32, u32, i16, i16, u16) {
let (base, rb, w, h, ps) = self.screen_mode;
(base, rb, w as i16, h as i16, ps)
}
pub(crate) fn draw_theme_push_button_chrome(
&self,
bus: &mut MacMemoryBus,
top: i16,
left: i16,
bottom: i16,
right: i16,
enabled: bool,
pressed: bool,
is_default: bool,
) -> bool {
self.draw_theme_control_chrome(
bus,
ControlKind::PushButton,
top,
left,
bottom,
right,
enabled,
pressed,
false,
is_default,
)
}
pub(crate) fn draw_theme_control_chrome(
&self,
bus: &mut MacMemoryBus,
kind: ControlKind,
top: i16,
left: i16,
bottom: i16,
right: i16,
enabled: bool,
pressed: bool,
selected: bool,
is_default: bool,
) -> bool {
if self.ui_theme_id() == UiThemeId::ClassicSystem7 {
return false;
}
let width = right.saturating_sub(left);
let height = bottom.saturating_sub(top);
if width <= 0 || height <= 0 {
return true;
}
let theme = self.ui_theme();
let pad = if kind == ControlKind::PushButton && is_default {
theme.dialog_metrics().default_button_outline.max(0)
} else {
0
};
let bitmap_width = width.saturating_add(pad.saturating_mul(2)) as u32;
let bitmap_height = height.saturating_add(pad.saturating_mul(2)) as u32;
let palette = theme.palette();
let mut bitmap = ThemeBitmap::new(bitmap_width, bitmap_height, palette.window_background);
let mut ctx = ThemeDrawCtx::new(&mut bitmap);
theme.draw_control(
&mut ctx,
ControlState {
kind,
rect: ThemeRect {
top: pad,
left: pad,
bottom: pad + height,
right: pad + width,
},
enabled,
pressed,
selected,
is_default,
},
);
self.blit_theme_bitmap_mono(
bus,
top.saturating_sub(pad),
left.saturating_sub(pad),
&bitmap,
);
true
}
pub(crate) fn draw_theme_scrollbar_chrome(
&self,
bus: &mut MacMemoryBus,
top: i16,
left: i16,
bottom: i16,
right: i16,
value: i16,
min: i16,
max: i16,
hilite: u8,
) -> bool {
if self.ui_theme_id() == UiThemeId::ClassicSystem7 {
return false;
}
let width = right.saturating_sub(left);
let height = bottom.saturating_sub(top);
if width <= 0 || height <= 0 {
return true;
}
let theme = self.ui_theme();
let palette = theme.palette();
let mut bitmap = ThemeBitmap::new(width as u32, height as u32, palette.window_background);
let mut ctx = ThemeDrawCtx::new(&mut bitmap);
theme.draw_scrollbar(
&mut ctx,
ScrollbarState {
rect: ThemeRect {
top: 0,
left: 0,
bottom: height,
right: width,
},
orientation: if height > width {
ScrollbarOrientation::Vertical
} else {
ScrollbarOrientation::Horizontal
},
enabled: hilite != 255 && min < max,
value,
min,
max,
highlighted_part: ScrollbarPart::from_control_part_code(hilite),
},
);
self.blit_theme_bitmap_mono(bus, top, left, &bitmap);
true
}
pub(crate) fn draw_theme_menu_bar_chrome(&self, bus: &mut MacMemoryBus, height: i16) -> bool {
if self.ui_theme_id() == UiThemeId::ClassicSystem7 {
return false;
}
let (_, _, screen_width, _, _) = self.get_screen_params();
if screen_width <= 0 || height <= 0 {
return true;
}
let theme = self.ui_theme();
let palette = theme.palette();
let mut bitmap = ThemeBitmap::new(
screen_width as u32,
height as u32,
palette.window_background,
);
let mut ctx = ThemeDrawCtx::new(&mut bitmap);
theme.draw_menu_bar(
&mut ctx,
MenuBarState {
rect: ThemeRect {
top: 0,
left: 0,
bottom: height,
right: screen_width,
},
},
);
self.blit_theme_bitmap_mono(bus, 0, 0, &bitmap);
true
}
pub(crate) fn draw_theme_menu_title_chrome(
&self,
bus: &mut MacMemoryBus,
top: i16,
left: i16,
bottom: i16,
right: i16,
enabled: bool,
highlighted: bool,
) -> bool {
if self.ui_theme_id() == UiThemeId::ClassicSystem7 {
return false;
}
let width = right.saturating_sub(left);
let height = bottom.saturating_sub(top);
if width <= 0 || height <= 0 {
return true;
}
let theme = self.ui_theme();
let palette = theme.palette();
let mut bitmap = ThemeBitmap::new(width as u32, height as u32, palette.window_background);
let mut ctx = ThemeDrawCtx::new(&mut bitmap);
theme.draw_menu_title(
&mut ctx,
MenuTitleState {
rect: ThemeRect {
top: 0,
left: 0,
bottom: height,
right: width,
},
enabled,
highlighted,
},
);
self.blit_theme_bitmap_mono(bus, top, left, &bitmap);
true
}
pub(crate) fn draw_theme_menu_dropdown_chrome(
&self,
bus: &mut MacMemoryBus,
top: i16,
left: i16,
bottom: i16,
right: i16,
) -> bool {
if self.ui_theme_id() == UiThemeId::ClassicSystem7 {
return false;
}
let width = right.saturating_sub(left);
let height = bottom.saturating_sub(top);
if width <= 0 || height <= 0 {
return true;
}
let theme = self.ui_theme();
let palette = theme.palette();
let mut bitmap = ThemeBitmap::new(width as u32, height as u32, palette.window_background);
let mut ctx = ThemeDrawCtx::new(&mut bitmap);
theme.draw_menu_dropdown(
&mut ctx,
MenuDropdownState {
rect: ThemeRect {
top: 0,
left: 0,
bottom: height,
right: width,
},
},
);
self.blit_theme_bitmap_mono(bus, top, left, &bitmap);
true
}
pub(crate) fn draw_theme_menu_item_chrome(
&self,
bus: &mut MacMemoryBus,
top: i16,
left: i16,
bottom: i16,
right: i16,
enabled: bool,
highlighted: bool,
separator: bool,
has_icon: bool,
checked: bool,
has_command_key: bool,
) -> bool {
if self.ui_theme_id() == UiThemeId::ClassicSystem7 {
return false;
}
let width = right.saturating_sub(left);
let height = bottom.saturating_sub(top);
if width <= 0 || height <= 0 {
return true;
}
let theme = self.ui_theme();
let palette = theme.palette();
let mut bitmap = ThemeBitmap::new(width as u32, height as u32, palette.frame_light);
let mut ctx = ThemeDrawCtx::new(&mut bitmap);
theme.draw_menu_item(
&mut ctx,
MenuItemState {
rect: ThemeRect {
top: 0,
left: 0,
bottom: height,
right: width,
},
enabled,
highlighted,
separator,
has_icon,
checked,
has_command_key,
},
);
self.blit_theme_bitmap_mono(bus, top, left, &bitmap);
true
}
pub(crate) fn draw_theme_text_selection(
&self,
bus: &mut MacMemoryBus,
top: i16,
left: i16,
bottom: i16,
right: i16,
active: bool,
) -> bool {
if self.ui_theme_id() == UiThemeId::ClassicSystem7 {
return false;
}
let width = right.saturating_sub(left);
let height = bottom.saturating_sub(top);
if width <= 0 || height <= 0 {
return true;
}
let theme = self.ui_theme();
let palette = theme.palette();
let mut bitmap = ThemeBitmap::new(width as u32, height as u32, palette.window_background);
let mut ctx = ThemeDrawCtx::new(&mut bitmap);
theme.draw_text_selection(
&mut ctx,
TextSelectionState {
rect: ThemeRect {
top: 0,
left: 0,
bottom: height,
right: width,
},
active,
},
);
self.blit_theme_bitmap_mono_masked(bus, top, left, &bitmap);
true
}
pub(crate) fn draw_theme_text_field(
&self,
bus: &mut MacMemoryBus,
top: i16,
left: i16,
bottom: i16,
right: i16,
enabled: bool,
focused: bool,
) -> bool {
if self.ui_theme_id() == UiThemeId::ClassicSystem7 {
return false;
}
let width = right.saturating_sub(left);
let height = bottom.saturating_sub(top);
if width <= 0 || height <= 0 {
return true;
}
let theme = self.ui_theme();
let palette = theme.palette();
let mut bitmap = ThemeBitmap::new(width as u32, height as u32, palette.window_background);
let mut ctx = ThemeDrawCtx::new(&mut bitmap);
theme.draw_text_field(
&mut ctx,
TextFieldState {
rect: ThemeRect {
top: 0,
left: 0,
bottom: height,
right: width,
},
enabled,
focused,
},
);
self.blit_theme_bitmap_mono(bus, top, left, &bitmap);
true
}
pub(crate) fn draw_theme_caret(
&self,
bus: &mut MacMemoryBus,
top: i16,
left: i16,
bottom: i16,
right: i16,
) -> bool {
if self.ui_theme_id() == UiThemeId::ClassicSystem7 {
return false;
}
let width = right.saturating_sub(left);
let height = bottom.saturating_sub(top);
if width <= 0 || height <= 0 {
return true;
}
let theme = self.ui_theme();
let pad = 1i16;
let palette = theme.palette();
let mut bitmap = ThemeBitmap::new(
width.saturating_add(pad.saturating_mul(2)) as u32,
height as u32,
palette.window_background,
);
let mut ctx = ThemeDrawCtx::new(&mut bitmap);
theme.draw_caret(
&mut ctx,
CaretState {
rect: ThemeRect {
top: 0,
left: pad,
bottom: height,
right: pad + width,
},
active: true,
},
);
self.blit_theme_bitmap_mono_masked(bus, top, left.saturating_sub(pad), &bitmap);
true
}
pub(crate) fn draw_theme_dialog_frame(
&self,
bus: &mut MacMemoryBus,
content: (i16, i16, i16, i16),
frame: (i16, i16, i16, i16),
proc_id: i16,
active: bool,
fill_content: bool,
) -> bool {
if self.ui_theme_id() == UiThemeId::ClassicSystem7 {
return false;
}
let (frame_top, frame_left, frame_bottom, frame_right) = frame;
let width = frame_right.saturating_sub(frame_left);
let height = frame_bottom.saturating_sub(frame_top);
if width <= 0 || height <= 0 {
return true;
}
if !fill_content {
self.erase_structure_frame_around_content(bus, frame, content);
}
let (content_top, content_left, content_bottom, content_right) = content;
let theme = self.ui_theme();
let palette = theme.palette();
let mut bitmap = ThemeBitmap::new(width as u32, height as u32, palette.window_background);
let mut ctx = ThemeDrawCtx::new(&mut bitmap);
theme.draw_dialog_frame(
&mut ctx,
DialogFrameState {
frame_rect: ThemeRect {
top: 0,
left: 0,
bottom: height,
right: width,
},
content_rect: ThemeRect {
top: content_top.saturating_sub(frame_top),
left: content_left.saturating_sub(frame_left),
bottom: content_bottom.saturating_sub(frame_top),
right: content_right.saturating_sub(frame_left),
},
kind: DialogFrameKind::from_window_proc_id(proc_id),
active,
fill_content,
},
);
if fill_content {
self.blit_theme_bitmap_mono(bus, frame_top, frame_left, &bitmap);
} else {
self.blit_theme_bitmap_mono_masked(bus, frame_top, frame_left, &bitmap);
}
true
}
fn blit_theme_bitmap_mono(
&self,
bus: &mut MacMemoryBus,
top: i16,
left: i16,
bitmap: &ThemeBitmap,
) {
let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
self.get_screen_params();
let rgba = bitmap.rgba();
for y in 0..bitmap.height() {
for x in 0..bitmap.width() {
let offset = ((y * bitmap.width() + x) * 4) as usize;
let color = Rgb8 {
r: rgba[offset],
g: rgba[offset + 1],
b: rgba[offset + 2],
};
Self::fb_set_pixel(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
left.saturating_add(x as i16),
top.saturating_add(y as i16),
Self::theme_color_is_mono_black(color),
);
}
}
}
fn blit_theme_bitmap_mono_masked(
&self,
bus: &mut MacMemoryBus,
top: i16,
left: i16,
bitmap: &ThemeBitmap,
) {
let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
self.get_screen_params();
let rgba = bitmap.rgba();
for y in 0..bitmap.height() {
for x in 0..bitmap.width() {
let offset = ((y * bitmap.width() + x) * 4) as usize;
let color = Rgb8 {
r: rgba[offset],
g: rgba[offset + 1],
b: rgba[offset + 2],
};
if !Self::theme_color_is_mono_black(color) {
continue;
}
Self::fb_set_pixel(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
left.saturating_add(x as i16),
top.saturating_add(y as i16),
true,
);
}
}
}
fn theme_color_is_mono_black(color: Rgb8) -> bool {
u16::from(color.r) + u16::from(color.g) + u16::from(color.b) < 128 * 3
}
fn active_gdevice_ctab(bus: &MacMemoryBus) -> Option<u32> {
let gdevice_handle = {
let current = bus.read_long(0x0CC8); if current != 0 {
current
} else {
bus.read_long(0x08A4) }
};
if gdevice_handle == 0 {
return None;
}
let gdevice = bus.read_long(gdevice_handle);
if gdevice == 0 {
return None;
}
let pixmap_handle = bus.read_long(gdevice + 22);
if pixmap_handle == 0 {
return None;
}
let pixmap = bus.read_long(pixmap_handle);
if pixmap == 0 {
return None;
}
let ctab_handle = bus.read_long(pixmap + 42);
if ctab_handle == 0 {
return None;
}
let ctab = bus.read_long(ctab_handle);
if ctab == 0 {
return None;
}
Some(ctab)
}
fn ctab_value_luma(bus: &MacMemoryBus, ctab: u32, wanted_value: u8) -> Option<u32> {
let count = u32::from(bus.read_word(ctab + 6)).min(255) + 1;
let ordinal = u32::from(wanted_value);
if ordinal < count {
let entry = ctab + 8 + ordinal * 8;
if bus.read_word(entry) == u16::from(wanted_value) {
return Some(
u32::from(bus.read_word(entry + 2))
+ u32::from(bus.read_word(entry + 4))
+ u32::from(bus.read_word(entry + 6)),
);
}
}
for ordinal in 0..count {
let entry = ctab + 8 + ordinal * 8;
if bus.read_word(entry) != u16::from(wanted_value) {
continue;
}
return Some(
u32::from(bus.read_word(entry + 2))
+ u32::from(bus.read_word(entry + 4))
+ u32::from(bus.read_word(entry + 6)),
);
}
None
}
fn best_luma_pixel_index(bus: &MacMemoryBus, ctab: u32, brightest: bool) -> Option<u8> {
let count = u32::from(bus.read_word(ctab + 6)).min(255) + 1;
let mut best_index = 0u8;
let mut best_luma = 0u32;
let mut found = false;
for ordinal in 0..count {
let entry = ctab + 8 + ordinal * 8;
let value = bus.read_word(entry);
if value > 255 {
continue;
}
let luma = u32::from(bus.read_word(entry + 2))
+ u32::from(bus.read_word(entry + 4))
+ u32::from(bus.read_word(entry + 6));
let index = value as u8;
let better = if brightest {
luma > best_luma || (luma == best_luma && index < best_index)
} else {
luma < best_luma || (luma == best_luma && index < best_index)
};
if !found || better {
best_index = index;
best_luma = luma;
found = true;
}
}
found.then_some(best_index)
}
pub(crate) fn fb_pixel_index_for_rgb(bus: &MacMemoryBus, rgb: [u16; 3]) -> Option<u8> {
let ctab = Self::active_gdevice_ctab(bus)?;
let count = u32::from(bus.read_word(ctab + 6)).min(255) + 1;
if rgb == [0, 0, 0] && Self::ctab_value_luma(bus, ctab, 255) == Some(0) {
return Some(255);
}
if rgb == [0xFFFF, 0xFFFF, 0xFFFF]
&& Self::ctab_value_luma(bus, ctab, 0) == Some(0xFFFF * 3)
{
return Some(0);
}
let mut best_index = None;
let mut best_distance = u64::MAX;
for ordinal in 0..count {
let entry = ctab + 8 + ordinal * 8;
let value = bus.read_word(entry);
if value > 255 {
continue;
}
let entry_rgb = [
bus.read_word(entry + 2),
bus.read_word(entry + 4),
bus.read_word(entry + 6),
];
let index = value as u8;
if entry_rgb == rgb {
return Some(index);
}
let dr = i64::from(entry_rgb[0]) - i64::from(rgb[0]);
let dg = i64::from(entry_rgb[1]) - i64::from(rgb[1]);
let db = i64::from(entry_rgb[2]) - i64::from(rgb[2]);
let distance = (dr * dr + dg * dg + db * db) as u64;
if distance < best_distance
|| (distance == best_distance && best_index.map_or(true, |best| index < best))
{
best_distance = distance;
best_index = Some(index);
}
}
best_index
}
pub(crate) fn fb_rgb_for_pixel_index(bus: &MacMemoryBus, index: u8) -> Option<[u16; 3]> {
let ctab = Self::active_gdevice_ctab(bus)?;
let count = u32::from(bus.read_word(ctab + 6)).min(255) + 1;
let ordinal = u32::from(index);
let read_entry = |entry: u32| {
[
bus.read_word(entry + 2),
bus.read_word(entry + 4),
bus.read_word(entry + 6),
]
};
if ordinal < count {
let entry = ctab + 8 + ordinal * 8;
if bus.read_word(entry) == u16::from(index) {
return Some(read_entry(entry));
}
}
for ordinal in 0..count {
let entry = ctab + 8 + ordinal * 8;
if bus.read_word(entry) == u16::from(index) {
return Some(read_entry(entry));
}
}
None
}
pub(crate) fn fb_gray_pixel_index_between(
bus: &MacMemoryBus,
background: [u16; 3],
foreground: [u16; 3],
) -> Option<u8> {
let midpoint = [
((u32::from(background[0]) + u32::from(foreground[0])) / 2) as u16,
((u32::from(background[1]) + u32::from(foreground[1])) / 2) as u16,
((u32::from(background[2]) + u32::from(foreground[2])) / 2) as u16,
];
let gray = Self::fb_pixel_index_for_rgb(bus, midpoint)?;
let background_index = Self::fb_pixel_index_for_rgb(bus, background);
let foreground_index = Self::fb_pixel_index_for_rgb(bus, foreground);
if Some(gray) == background_index || Some(gray) == foreground_index {
return None;
}
Some(gray)
}
fn logical_white_pixel_index(bus: &MacMemoryBus) -> u8 {
let Some(ctab) = Self::active_gdevice_ctab(bus) else {
return 0;
};
if Self::ctab_value_luma(bus, ctab, 0) == Some(0xFFFF * 3) {
return 0;
}
Self::best_luma_pixel_index(bus, ctab, true).unwrap_or(0)
}
fn logical_black_pixel_index(bus: &MacMemoryBus) -> u8 {
let Some(ctab) = Self::active_gdevice_ctab(bus) else {
return 255;
};
if Self::ctab_value_luma(bus, ctab, 1) == Some(0) {
return 1;
}
if Self::ctab_value_luma(bus, ctab, 255) == Some(0) {
return 255;
}
Self::best_luma_pixel_index(bus, ctab, false).unwrap_or(255)
}
fn logical_mono_pixel_indexes(bus: &MacMemoryBus) -> (u8, u8) {
(
Self::logical_white_pixel_index(bus),
Self::logical_black_pixel_index(bus),
)
}
pub(crate) fn fb_set_pixel(
bus: &mut MacMemoryBus,
screen_base: u32,
row_bytes: u32,
pixel_size: u16,
screen_width: i16,
screen_height: i16,
x: i16,
y: i16,
black: bool,
) {
if x < 0 || y < 0 || x >= screen_width || y >= screen_height {
return;
}
if pixel_size == 8 {
let addr = screen_base + (y as u32) * row_bytes + (x as u32);
bus.write_byte(
addr,
if black {
Self::logical_black_pixel_index(bus)
} else {
Self::logical_white_pixel_index(bus)
},
);
} else if pixel_size == 4 {
let addr = screen_base + (y as u32) * row_bytes + (x as u32 / 2);
let shift = if x & 1 == 0 { 4 } else { 0 };
let mask = 0x0F << shift;
let index = if black {
Self::logical_black_pixel_index(bus)
} else {
Self::logical_white_pixel_index(bus)
} & 0x0F;
let byte = bus.read_byte(addr);
bus.write_byte(addr, (byte & !mask) | (index << shift));
} else {
let byte_offset = (y as u32) * row_bytes + (x as u32 / 8);
let bit = 7 - (x as u32 % 8);
let addr = screen_base + byte_offset;
let b = bus.read_byte(addr);
if black {
bus.write_byte(addr, b | (1 << bit));
} else {
bus.write_byte(addr, b & !(1 << bit));
}
}
}
pub(crate) fn fb_set_pixel_index(
bus: &mut MacMemoryBus,
screen_base: u32,
row_bytes: u32,
pixel_size: u16,
screen_width: i16,
screen_height: i16,
x: i16,
y: i16,
pixel_index: u8,
) {
if pixel_size == 4 {
if x < 0 || y < 0 || x >= screen_width || y >= screen_height {
return;
}
let addr = screen_base + (y as u32) * row_bytes + (x as u32 / 2);
let shift = if x & 1 == 0 { 4 } else { 0 };
let mask = 0x0F << shift;
let byte = bus.read_byte(addr);
bus.write_byte(addr, (byte & !mask) | ((pixel_index & 0x0F) << shift));
return;
}
if pixel_size != 8 {
Self::fb_set_pixel(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
x,
y,
pixel_index != 0,
);
return;
}
if x < 0 || y < 0 || x >= screen_width || y >= screen_height {
return;
}
let addr = screen_base + (y as u32) * row_bytes + (x as u32);
bus.write_byte(addr, pixel_index);
}
pub(crate) fn fb_fill_rect_index(
bus: &mut MacMemoryBus,
screen_base: u32,
row_bytes: u32,
pixel_size: u16,
screen_width: i16,
screen_height: i16,
top: i16,
left: i16,
bottom: i16,
right: i16,
pixel_index: u8,
) {
if pixel_size == 4 {
for y in top..bottom {
for x in left..right {
Self::fb_set_pixel_index(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
x,
y,
pixel_index,
);
}
}
return;
}
if pixel_size != 8 {
Self::fb_fill_rect(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
top,
left,
bottom,
right,
pixel_index != 0,
);
return;
}
let top = top.max(0).min(screen_height) as u32;
let left = left.max(0).min(screen_width) as u32;
let bottom = bottom.max(0).min(screen_height) as u32;
let right = right.max(0).min(screen_width) as u32;
if top >= bottom || left >= right {
return;
}
let width = right - left;
for y in top..bottom {
let row_addr = screen_base + y * row_bytes;
bus.fill_bytes(row_addr + left, width, pixel_index);
}
}
pub(crate) fn fb_fill_rect(
bus: &mut MacMemoryBus,
screen_base: u32,
row_bytes: u32,
pixel_size: u16,
screen_width: i16,
screen_height: i16,
top: i16,
left: i16,
bottom: i16,
right: i16,
black: bool,
) {
if pixel_size == 8 {
let top = top.max(0).min(screen_height) as u32;
let left = left.max(0).min(screen_width) as u32;
let bottom = bottom.max(0).min(screen_height) as u32;
let right = right.max(0).min(screen_width) as u32;
if top >= bottom || left >= right {
return;
}
let fill = if black {
Self::logical_black_pixel_index(bus)
} else {
Self::logical_white_pixel_index(bus)
};
let width = right - left;
for y in top..bottom {
let row_addr = screen_base + y * row_bytes;
bus.fill_bytes(row_addr + left, width, fill);
}
return;
}
for y in top..bottom {
for x in left..right {
Self::fb_set_pixel(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
x,
y,
black,
);
}
}
}
pub(crate) fn fb_fill_pattern_rect(
bus: &mut MacMemoryBus,
screen_base: u32,
row_bytes: u32,
pixel_size: u16,
screen_width: i16,
screen_height: i16,
top: i16,
left: i16,
bottom: i16,
right: i16,
pattern: [u8; 8],
) {
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 {
let row = pattern[y.rem_euclid(8) as usize];
for x in left..right {
let bit = (row >> (7 - x.rem_euclid(8))) & 1;
Self::fb_set_pixel(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
x,
y,
bit != 0,
);
}
}
}
fn fb_pixel_is_logical_black(
bus: &MacMemoryBus,
screen_base: u32,
row_bytes: u32,
pixel_size: u16,
screen_width: i16,
screen_height: i16,
x: i16,
y: i16,
) -> bool {
if x < 0 || y < 0 || x >= screen_width || y >= screen_height {
return false;
}
if pixel_size == 8 {
let addr = screen_base + (y as u32) * row_bytes + (x as u32);
bus.read_byte(addr) == Self::logical_black_pixel_index(bus)
} else if pixel_size == 4 {
let addr = screen_base + (y as u32) * row_bytes + (x as u32 / 2);
let shift = if x & 1 == 0 { 4 } else { 0 };
((bus.read_byte(addr) >> shift) & 0x0F) == (Self::logical_black_pixel_index(bus) & 0x0F)
} else {
let byte_offset = (y as u32) * row_bytes + (x as u32 / 8);
let bit = 7 - (x as u32 % 8);
let addr = screen_base + byte_offset;
(bus.read_byte(addr) & (1 << bit)) != 0
}
}
fn visible_window_count(&self, bus: &MacMemoryBus) -> usize {
let mut count = 0usize;
let mut saw = HashSet::new();
for &window in &self.window_list {
if window != 0 && saw.insert(window) && bus.read_byte(window + 110u32) != 0 {
count += 1;
}
}
if self.front_window != 0
&& saw.insert(self.front_window)
&& bus.read_byte(self.front_window + 110u32) != 0
{
count += 1;
}
count
}
fn front_window_is_dialog_like(&self) -> bool {
self.front_window != 0
&& (self.dialog_items.contains_key(&self.front_window)
|| matches!(
self.window_proc_ids
.get(&self.front_window)
.copied()
.unwrap_or(self.window_proc_id),
1 | 2 | 3 | 5
))
}
fn exposed_background_samples_are_black(
&self,
bus: &MacMemoryBus,
excluded_rect: (i16, i16, i16, i16),
) -> bool {
let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
self.get_screen_params();
let top = excluded_rect.0.max(0).min(screen_height);
let left = excluded_rect.1.max(0).min(screen_width);
let bottom = excluded_rect.2.max(0).min(screen_height);
let right = excluded_rect.3.max(0).min(screen_width);
let rects = [
(0, 0, top, screen_width),
(bottom, 0, screen_height, screen_width),
(top, 0, bottom, left),
(top, right, bottom, screen_width),
];
let mut sampled = false;
for (rt, rl, rb, rr) in rects {
if rt >= rb || rl >= rr {
continue;
}
let mut y = rt;
while y < rb {
let mut x = rl;
while x < rr {
sampled = true;
let sample_points = [
(x, y),
(x.saturating_add(1).min(rr - 1), y),
(x, y.saturating_add(1).min(rb - 1)),
];
for (sample_x, sample_y) in sample_points {
if !Self::fb_pixel_is_logical_black(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
sample_x,
sample_y,
) {
return false;
}
}
x = x.saturating_add(16);
}
y = y.saturating_add(16);
}
}
sampled
}
fn fill_desktop_pattern_outside_rect(
&self,
bus: &mut MacMemoryBus,
excluded_rect: (i16, i16, i16, i16),
) {
let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
self.get_screen_params();
let top = excluded_rect.0.max(0).min(screen_height);
let left = excluded_rect.1.max(0).min(screen_width);
let bottom = excluded_rect.2.max(0).min(screen_height);
let right = excluded_rect.3.max(0).min(screen_width);
let rects = [
(0, 0, top, screen_width),
(bottom, 0, screen_height, screen_width),
(top, 0, bottom, left),
(top, right, bottom, screen_width),
];
for (rt, rl, rb, rr) in rects {
Self::fb_fill_pattern_rect(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
rt,
rl,
rb,
rr,
STANDARD_GRAY_PATTERN,
);
}
}
fn refresh_saved_under_with_desktop_pattern(&mut self, bus: &MacMemoryBus, window: u32) {
let (_, _, _, _, pixel_size) = self.get_screen_params();
let black = Self::logical_black_pixel_index(bus);
let white = Self::logical_white_pixel_index(bus);
let Some((top, left, width, height, pixels)) =
self.window_saved_under_pixels.get_mut(&window)
else {
return;
};
pixels.clear();
pixels.reserve(*width as usize * *height as usize);
for y in *top..top.saturating_add(*height) {
let row = STANDARD_GRAY_PATTERN[y.rem_euclid(8) as usize];
for x in *left..left.saturating_add(*width) {
let bit = (row >> (7 - x.rem_euclid(8))) & 1;
pixels.push(if pixel_size == 8 {
if bit != 0 {
black
} else {
white
}
} else {
bit
});
}
}
}
fn restore_kiosk_dialog_desktop_background(&mut self, bus: &mut MacMemoryBus) {
if !self.menu_bar_hidden
|| self.fullscreen_locked
|| !self.front_window_is_dialog_like()
|| self.visible_window_count(bus) != 1
{
return;
}
let bounds = self
.window_structure_rect(bus, self.front_window)
.unwrap_or(self.window_bounds);
if self.exposed_background_samples_are_black(bus, bounds) {
self.fill_desktop_pattern_outside_rect(bus, bounds);
self.refresh_saved_under_with_desktop_pattern(bus, self.front_window);
}
}
pub(crate) fn fb_hline(
bus: &mut MacMemoryBus,
screen_base: u32,
row_bytes: u32,
pixel_size: u16,
screen_width: i16,
screen_height: i16,
y: i16,
x1: i16,
x2: i16,
black: bool,
) {
if pixel_size == 8 {
if y < 0 || y >= screen_height {
return;
}
let left = x1.max(0).min(screen_width) as u32;
let right = x2.max(0).min(screen_width) as u32;
if left >= right {
return;
}
let fill = if black {
Self::logical_black_pixel_index(bus)
} else {
Self::logical_white_pixel_index(bus)
};
let row_addr = screen_base + (y as u32) * row_bytes;
for x in left..right {
bus.write_byte(row_addr + x, fill);
}
return;
}
for x in x1..x2 {
Self::fb_set_pixel(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
x,
y,
black,
);
}
}
pub(crate) fn fb_draw_char(
bus: &mut MacMemoryBus,
screen_base: u32,
row_bytes: u32,
pixel_size: u16,
screen_width: i16,
screen_height: i16,
x: i16,
y: i16,
ch: char,
font_id: i16,
font_size: i16,
) -> i16 {
if let Some((glyph, data)) = get_glyph(font_id, font_size, ch) {
Self::fb_draw_glyph_bitmap(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
x,
y,
glyph,
data,
);
glyph.advance as i16
} else {
6 }
}
fn fb_draw_glyph_bitmap(
bus: &mut MacMemoryBus,
screen_base: u32,
row_bytes: u32,
pixel_size: u16,
screen_width: i16,
screen_height: i16,
x: i16,
y: i16,
glyph: &Glyph,
data: &[u8],
) {
Self::fb_draw_glyph_bitmap_with_slant(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
x,
y,
glyph,
data,
None,
0,
None,
true,
);
}
fn fb_draw_glyph_bitmap_with_slant(
bus: &mut MacMemoryBus,
screen_base: u32,
row_bytes: u32,
pixel_size: u16,
screen_width: i16,
screen_height: i16,
x: i16,
y: i16,
glyph: &Glyph,
data: &[u8],
synthetic_italic: Option<(i16, i16)>,
style: u8,
pixel_index_override: Option<u8>,
black: bool,
) {
let gx = x + glyph.origin_x as i16;
let gy = y + glyph.origin_y as i16;
let gw = glyph.width as usize;
let gh = glyph.height as usize;
let metrics = synthetic_italic
.map(|(font_id, font_size)| (font_id, font_size, get_font_metrics(font_id, font_size)));
let text_index = if matches!(pixel_size, 4 | 8) {
Some(pixel_index_override.unwrap_or_else(|| {
if black {
Self::logical_black_pixel_index(bus)
} else {
Self::logical_white_pixel_index(bus)
}
}))
} else {
None
};
for row in 0..gh {
for col in 0..gw {
let byte_idx = glyph.data_offset + row * gw + col;
if byte_idx < data.len() && data[byte_idx] >= 128 {
let py = gy + row as i16;
let slant = metrics
.as_ref()
.map(|(font_id, font_size, metrics)| {
get_italic_slant(*font_id, *font_size, metrics, y, py)
})
.unwrap_or(0);
let (dst_start, dst_end) = if (style & TEXT_STYLE_EXTEND) != 0 {
let start = (col as i16 * 4) / 3;
let end = (((col as i16 + 1) * 4) / 3).max(start + 1);
(start, end)
} else if (style & TEXT_STYLE_CONDENSE) != 0 {
let start = (col as i16 * 3) / 4;
(start, start + 1)
} else {
let start = col as i16;
(start, start + 1)
};
for dst_col in dst_start..dst_end {
let px = gx + dst_col + slant;
if let Some(text_index) = text_index {
if px >= 0 && py >= 0 && px < screen_width && py < screen_height {
let addr = screen_base + (py as u32) * row_bytes + (px as u32);
bus.write_byte(addr, text_index);
}
} else {
Self::fb_set_pixel(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
px,
py,
black,
);
}
}
}
}
}
}
fn fb_set_styled_text_pixel(
bus: &mut MacMemoryBus,
screen_base: u32,
row_bytes: u32,
pixel_size: u16,
screen_width: i16,
screen_height: i16,
x: i16,
y: i16,
pixel_index_override: Option<u8>,
black: bool,
) {
if let Some(pixel_index) = pixel_index_override {
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,
);
}
}
fn fb_styled_glyph_base_pixels(
x: i16,
y: i16,
glyph: &Glyph,
data: &[u8],
synthetic_italic: Option<(i16, i16)>,
style: u8,
) -> HashSet<(i16, i16)> {
let gx = x + glyph.origin_x as i16;
let gy = y + glyph.origin_y as i16;
let gw = glyph.width as usize;
let gh = glyph.height as usize;
let metrics = synthetic_italic
.map(|(font_id, font_size)| (font_id, font_size, get_font_metrics(font_id, font_size)));
let mut pixels = HashSet::new();
for row in 0..gh {
for col in 0..gw {
let byte_idx = glyph.data_offset + row * gw + col;
if byte_idx >= data.len() || data[byte_idx] < 128 {
continue;
}
let py = gy + row as i16;
let slant = metrics
.as_ref()
.map(|(font_id, font_size, metrics)| {
get_italic_slant(*font_id, *font_size, metrics, y, py)
})
.unwrap_or(0);
let start = col as i16;
let (dst_start, dst_end) = (start, start + 1);
for dst_col in dst_start..dst_end {
let px = gx + dst_col + slant;
pixels.insert((px, py));
if (style & TEXT_STYLE_BOLD) != 0 {
pixels.insert((px + 1, py));
}
}
}
}
pixels
}
fn fb_styled_glyph_advance(glyph: &Glyph, style: u8) -> i16 {
let mut advance = glyph.advance as i16;
if (style & TEXT_STYLE_BOLD) != 0 {
advance += 1;
}
if (style & TEXT_STYLE_OUTLINE) != 0 {
advance += 1;
}
if (style & TEXT_STYLE_SHADOW) != 0 {
advance += 2;
}
if (style & TEXT_STYLE_CONDENSE) != 0 && advance >= 6 {
advance -= 1;
}
if (style & TEXT_STYLE_EXTEND) != 0 {
advance += 1;
}
advance.max(1)
}
fn fb_draw_char_styled(
bus: &mut MacMemoryBus,
screen_base: u32,
row_bytes: u32,
pixel_size: u16,
screen_width: i16,
screen_height: i16,
x: i16,
y: i16,
ch: char,
font_id: i16,
font_size: i16,
style: u8,
pixel_index_override: Option<u8>,
black: bool,
) -> i16 {
let italic = (style & TEXT_STYLE_ITALIC) != 0;
let (glyph_hit, synthetic_italic) = if italic {
if let Some(hit) = get_glyph_italic(font_id, font_size, ch) {
(Some(hit), None)
} else {
(
get_glyph(font_id, font_size, ch),
Some((font_id, font_size)),
)
}
} else {
(get_glyph(font_id, font_size, ch), None)
};
let Some((glyph, data)) = glyph_hit else {
return 6;
};
let glyph_y = if (style & TEXT_STYLE_SHADOW) != 0 {
y - 1
} else {
y
};
let base_pixels =
Self::fb_styled_glyph_base_pixels(x, glyph_y, glyph, data, synthetic_italic, style);
if (style & (TEXT_STYLE_OUTLINE | TEXT_STYLE_SHADOW)) == 0 {
for (px, py) in base_pixels.iter().copied() {
Self::fb_set_styled_text_pixel(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
px,
py,
pixel_index_override,
black,
);
}
return Self::fb_styled_glyph_advance(glyph, style);
}
let smear_max = if (style & TEXT_STYLE_SHADOW) != 0 && (style & TEXT_STYLE_OUTLINE) != 0 {
3
} else if (style & TEXT_STYLE_SHADOW) != 0 {
2
} else {
1
};
let min_x = base_pixels.iter().map(|(px, _)| *px).min().unwrap_or(x) - 1;
let max_x = base_pixels.iter().map(|(px, _)| *px).max().unwrap_or(x) + smear_max;
let min_y = base_pixels.iter().map(|(_, py)| *py).min().unwrap_or(y) - 1;
let max_y = base_pixels.iter().map(|(_, py)| *py).max().unwrap_or(y) + smear_max;
for py in min_y..=max_y {
for px in min_x..=max_x {
if base_pixels.contains(&(px, py)) {
continue;
}
let mut smeared = false;
'smear: for dy in -1..=smear_max {
for dx in -1..=smear_max {
if base_pixels.contains(&(px - dx, py - dy)) {
smeared = true;
break 'smear;
}
}
}
if smeared {
Self::fb_set_styled_text_pixel(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
px,
py,
pixel_index_override,
black,
);
}
}
}
Self::fb_styled_glyph_advance(glyph, style)
}
pub(crate) fn fb_draw_string(
bus: &mut MacMemoryBus,
screen_base: u32,
row_bytes: u32,
pixel_size: u16,
screen_width: i16,
screen_height: i16,
x: i16,
y: i16,
s: &str,
font_id: i16,
font_size: i16,
) -> i16 {
let mut cx = x;
for ch in s.chars() {
cx += Self::fb_draw_char(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
cx,
y,
ch,
font_id,
font_size,
);
}
cx - x
}
fn fb_draw_char_clipped(
bus: &mut MacMemoryBus,
screen_base: u32,
row_bytes: u32,
pixel_size: u16,
screen_width: i16,
screen_height: i16,
x: i16,
y: i16,
ch: char,
font_id: i16,
font_size: i16,
clip: (i16, i16, i16, i16),
) -> i16 {
let Some((glyph, data)) = get_glyph(font_id, font_size, ch) else {
return 6;
};
let gx = x + glyph.origin_x as i16;
let gy = y + glyph.origin_y as i16;
let gw = glyph.width as usize;
let gh = glyph.height as usize;
let (clip_top, clip_left, clip_bottom, clip_right) = clip;
for row in 0..gh {
let py = gy + row as i16;
if py < clip_top || py >= clip_bottom {
continue;
}
for col in 0..gw {
let px = gx + col as i16;
if px < clip_left || px >= clip_right {
continue;
}
let byte_idx = glyph.data_offset + row * gw + col;
if byte_idx < data.len() && data[byte_idx] >= 128 {
Self::fb_set_pixel(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
px,
py,
true,
);
}
}
}
glyph.advance as i16
}
fn fb_draw_string_clipped(
bus: &mut MacMemoryBus,
screen_base: u32,
row_bytes: u32,
pixel_size: u16,
screen_width: i16,
screen_height: i16,
x: i16,
y: i16,
s: &str,
font_id: i16,
font_size: i16,
clip: (i16, i16, i16, i16),
) -> i16 {
let mut cx = x;
for ch in s.chars() {
cx += Self::fb_draw_char_clipped(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
cx,
y,
ch,
font_id,
font_size,
clip,
);
}
cx - x
}
pub(crate) fn fb_draw_string_styled(
bus: &mut MacMemoryBus,
screen_base: u32,
row_bytes: u32,
pixel_size: u16,
screen_width: i16,
screen_height: i16,
x: i16,
y: i16,
s: &str,
font_id: i16,
font_size: i16,
style: u8,
) -> i16 {
Self::fb_draw_string_styled_with_index(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
x,
y,
s,
font_id,
font_size,
style,
None,
true,
)
}
pub(crate) fn fb_draw_string_styled_ink(
bus: &mut MacMemoryBus,
screen_base: u32,
row_bytes: u32,
pixel_size: u16,
screen_width: i16,
screen_height: i16,
x: i16,
y: i16,
s: &str,
font_id: i16,
font_size: i16,
style: u8,
black: bool,
) -> i16 {
Self::fb_draw_string_styled_with_index(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
x,
y,
s,
font_id,
font_size,
style,
None,
black,
)
}
pub(crate) fn fb_draw_string_styled_index(
bus: &mut MacMemoryBus,
screen_base: u32,
row_bytes: u32,
pixel_size: u16,
screen_width: i16,
screen_height: i16,
x: i16,
y: i16,
s: &str,
font_id: i16,
font_size: i16,
style: u8,
pixel_index: u8,
) -> i16 {
Self::fb_draw_string_styled_with_index(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
x,
y,
s,
font_id,
font_size,
style,
Some(pixel_index),
true,
)
}
fn fb_draw_string_styled_with_index(
bus: &mut MacMemoryBus,
screen_base: u32,
row_bytes: u32,
pixel_size: u16,
screen_width: i16,
screen_height: i16,
x: i16,
y: i16,
s: &str,
font_id: i16,
font_size: i16,
style: u8,
pixel_index_override: Option<u8>,
black: bool,
) -> i16 {
let mut cx = x;
for ch in s.chars() {
cx += Self::fb_draw_char_styled(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
cx,
y,
ch,
font_id,
font_size,
style,
pixel_index_override,
black,
);
}
if (style & TEXT_STYLE_UNDERLINE) != 0 && cx > x {
let thickness = get_underline_thickness(font_id, font_size).max(1);
for dy in 1..=thickness {
if let Some(pixel_index) = pixel_index_override {
Self::fb_set_pixel_index(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
x,
y + dy,
pixel_index,
);
for underline_x in (x + 1)..cx {
Self::fb_set_pixel_index(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
underline_x,
y + dy,
pixel_index,
);
}
} else {
Self::fb_hline(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
y + dy,
x,
cx,
black,
);
}
}
}
cx - x
}
pub(crate) fn draw_menu_bar_to_fb(&self, bus: &mut MacMemoryBus) {
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(crate::memory::globals::addr::MBAR_HEIGHT) as i16;
if menu_bar_height <= 0 {
return;
}
let menu_bar_bg_index = self.menu_bar_background_pixel_index(bus, pixel_size);
if !self.draw_theme_menu_bar_chrome(bus, menu_bar_height) {
if let Some(bg_index) = menu_bar_bg_index {
Self::fb_fill_rect_index(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
0,
0,
menu_bar_height,
screen_width,
bg_index,
);
} else {
Self::fb_fill_rect(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
0,
0,
menu_bar_height,
screen_width,
false,
);
}
Self::fb_hline(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
menu_bar_height - 1,
0,
screen_width,
true,
);
Self::fb_draw_menu_bar_rounded_corners(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
);
}
let font_id: i16 = 0;
let font_size: i16 = 12;
let metrics = 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 mut x: i16 = 18;
for menu in &self.menus {
if !menu.visible_in_menu_bar {
continue;
}
let title = &menu.title;
let title_width = Self::menu_title_advance(title);
self.draw_theme_menu_title_chrome(
bus,
1,
x - 7,
menu_bar_height - 1,
x + title_width + 6,
menu.enabled,
false,
);
let title_index = Self::menu_title_pixel_index(bus, menu.id, pixel_size);
let dim_title = self.ui_theme_id() == UiThemeId::ClassicSystem7 && !menu.enabled;
let dim_index = if dim_title {
Self::menu_dim_pixel_index(bus, pixel_size, title_index, menu_bar_bg_index)
} else {
None
};
let system_mark = Self::is_system_menu_mark_title(title);
let dim_with_pattern = dim_title && (dim_index.is_none() || system_mark);
let width = if system_mark {
self.fb_draw_retro_computer_menu_mark(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
x,
);
title_width
} else if let Some(pixel_index) = dim_index.or(title_index) {
Self::fb_draw_string_styled_index(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
x,
text_y,
title,
font_id,
font_size,
0,
pixel_index,
)
} else {
Self::fb_draw_string(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
x,
text_y,
title,
font_id,
font_size,
)
};
if dim_with_pattern {
let (dim_top, dim_bottom) = if system_mark {
(1, menu_bar_height - 1)
} else {
(
(text_y - metrics.ascent).max(0),
(text_y + metrics.descent).min(menu_bar_height - 1),
)
};
for py in dim_top..dim_bottom {
for px in x..x.saturating_add(width) {
if (px as i32 + py as i32) % 2 == 0 {
continue;
}
match menu_bar_bg_index {
Some(bg_index) => Self::fb_set_pixel_index(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
px,
py,
bg_index,
),
None => Self::fb_set_pixel(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
px,
py,
false,
),
}
}
}
}
x += width + 13;
}
}
fn is_system_menu_mark_title(title: &str) -> bool {
let mut chars = title.chars();
matches!(chars.next(), Some('\u{14}' | '\u{F8FF}')) && chars.next().is_none()
}
pub(crate) fn menu_title_advance(title: &str) -> i16 {
if Self::is_system_menu_mark_title(title) {
SYSTEM_MENU_MARK_TITLE_ADVANCE
} else {
Self::fb_measure_string(title, 0, 12)
}
}
fn fb_draw_retro_computer_menu_mark(
&self,
bus: &mut MacMemoryBus,
screen_base: u32,
row_bytes: u32,
pixel_size: u16,
screen_width: i16,
screen_height: i16,
x: i16,
) {
let palette_indices = crate::ui_art::RETRO_COMPUTER_MENU_MARK_PALETTE.map(|rgb| {
Self::fb_pixel_index_for_rgb(bus, rgb).unwrap_or_else(|| {
super::pict::closest_clut_index(rgb[0], rgb[1], rgb[2], &self.device_clut)
})
});
let left = x;
let top = 3;
for (dy, row) in crate::ui_art::RETRO_COMPUTER_MENU_MARK_PIXELS
.into_iter()
.enumerate()
{
for (dx, palette_index) in row.into_iter().enumerate() {
if palette_index == 0 {
continue;
}
let dst_x = left + dx as i16;
let dst_y = top + dy as i16;
if pixel_size == 8 {
Self::fb_set_pixel_index(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
dst_x,
dst_y,
palette_indices[usize::from(palette_index - 1)],
);
} else if palette_index == 1 {
Self::fb_set_pixel(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
dst_x,
dst_y,
true,
);
}
}
}
}
pub(crate) fn fb_draw_menu_bar_rounded_corners(
bus: &mut MacMemoryBus,
screen_base: u32,
row_bytes: u32,
pixel_size: u16,
screen_width: i16,
screen_height: i16,
) {
const LEFT: &[(i16, i16)] = &[
(0, 0),
(1, 0),
(2, 0),
(3, 0),
(4, 0),
(0, 1),
(1, 1),
(2, 1),
(0, 2),
(1, 2),
(0, 3),
(0, 4),
];
for &(x, y) in LEFT {
Self::fb_set_pixel(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
x,
y,
true,
);
Self::fb_set_pixel(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
screen_width - 1 - x,
y,
true,
);
}
}
pub(crate) fn blit_window_to_screen(&self, bus: &mut MacMemoryBus) {
let (screen_base, screen_rb, screen_w, screen_h, pixel_size) = self.screen_mode;
let trace = std::env::var_os("SYSTEMLESS_TRACE_BLIT_WINDOW").is_some();
if self.front_window == 0 {
if trace {
eprintln!("[BLIT] skip: front_window=0");
}
return;
}
if !matches!(pixel_size, 4 | 8) || screen_w == 0 || screen_h == 0 {
if trace {
eprintln!(
"[BLIT] skip: screen mode mismatch (pixel_size={}, w={}, h={})",
pixel_size, screen_w, screen_h
);
}
return;
}
let port = self.front_window;
let port_version = bus.read_word(port + 6);
let is_cgraf_port = (port_version & 0xC000) == 0xC000;
let (port_base, port_pixmap_ptr, port_pixmap_handle) = if is_cgraf_port {
let pm_handle = bus.read_long(port + 2);
if pm_handle == 0 {
if trace {
eprintln!("[BLIT] skip: CGrafPort pm_handle=0");
}
return;
}
let pm_ptr = bus.read_long(pm_handle);
if pm_ptr == 0 {
if trace {
eprintln!(
"[BLIT] skip: CGrafPort pm_ptr=0 (pm_handle=${:08X})",
pm_handle
);
}
return;
}
(
Self::offscreen_pixmap_base_ptr(bus, pm_ptr) & 0x3FFFFFFF,
pm_ptr,
pm_handle,
) } else {
(bus.read_long(port + 2), 0, 0)
};
if is_cgraf_port
&& self
.window_original_pixmaps
.get(&port)
.is_some_and(|&original| original != port_pixmap_handle)
{
if trace {
eprintln!(
"[BLIT] skip: CGrafPort portPixMap swapped original=${:08X} current=${:08X}",
self.window_original_pixmaps[&port], port_pixmap_handle
);
}
return;
}
if port_base == screen_base || port_base == 0 {
if trace {
eprintln!(
"[BLIT] skip: port_base=${:08X} screen_base=${:08X} \
(port draws directly to screen or port_base is NIL)",
port_base, screen_base
);
}
return;
}
let port_rb = if is_cgraf_port {
(bus.read_word(port_pixmap_ptr + 4) & 0x3FFF) as u32
} else {
(bus.read_word(port + 6) & 0x3FFF) as u32
};
let port_pixel_size: u32 = if is_cgraf_port {
bus.read_word(port_pixmap_ptr + 32) as u32
} else {
1
};
let port_ctab_handle = if is_cgraf_port {
bus.read_long(port_pixmap_ptr + 42)
} else {
0
};
let wr_top = bus.read_word(port + 16) as i16;
let wr_left = bus.read_word(port + 18) as i16;
let wr_bottom = bus.read_word(port + 20) as i16;
let wr_right = bus.read_word(port + 22) as i16;
let w = (wr_right - wr_left) as u32;
let h = (wr_bottom - wr_top) as u32;
if w == 0 || h == 0 {
return;
}
let src_y_offset = wr_top.max(0) as u32;
let src_x_offset = wr_left.max(0) as u32;
let dst_y = src_y_offset;
let dst_x = src_x_offset;
if port_pixel_size == 1 && pixel_size == 8 {
let (white_index, black_index) = Self::logical_mono_pixel_indexes(bus);
let pb_top = bus.read_word(port + 8) as i16;
let pb_left = bus.read_word(port + 10) as i16;
let pb_bottom = bus.read_word(port + 12) as i16;
let pb_right = bus.read_word(port + 14) as i16;
let bitmap_w = (pb_right - pb_left).max(0) as u32;
let bitmap_h = (pb_bottom - pb_top).max(0) as u32;
let row_count = h.min((screen_h as u32).saturating_sub(dst_y)).min(bitmap_h);
let col_count = w.min((screen_w as u32).saturating_sub(dst_x)).min(bitmap_w);
for row in 0..row_count {
let src_row_addr = port_base + (src_y_offset + row) * port_rb;
let dst_row_addr = screen_base + (dst_y + row) * screen_rb + dst_x;
for col in 0..col_count {
let src_bit_x = src_x_offset + col;
let src_byte = bus.read_byte(src_row_addr + src_bit_x / 8);
let bit = (src_byte >> (7 - (src_bit_x & 7))) & 1;
let dst_idx = if bit == 0 { white_index } else { black_index };
bus.write_byte(dst_row_addr + col, dst_idx);
}
}
return;
}
if port_pixel_size != pixel_size as u32 {
return;
}
let row_count = h.min((screen_h as u32).saturating_sub(dst_y));
let col_count = w.min((screen_w as u32).saturating_sub(dst_x));
if is_cgraf_port && port_pixel_size == 8 && pixel_size == 8 {
let screen_ctab_handle = Self::gdevice_ctab_handle(bus, self.main_gdevice_handle);
let src_ctab_seed = Self::ctab_seed(bus, port_ctab_handle);
let dst_ctab_seed = Self::ctab_seed(bus, screen_ctab_handle);
let src_clut = self.read_port_clut(bus, port_ctab_handle);
let dst_clut = self.read_port_clut(bus, screen_ctab_handle);
let hardware_palette_active = self.device_clut != self.color_manager_clut;
let skip_canonical_to_screen = Self::uses_canonical_system_8bpp_clut(&src_clut);
if port_ctab_handle != screen_ctab_handle
&& matches!(src_ctab_seed, Some(src_seed) if src_seed != 0)
&& src_ctab_seed != dst_ctab_seed
&& !skip_canonical_to_screen
&& !hardware_palette_active
{
let translation =
self.build_palette_translation(bus, &src_clut, &dst_clut, screen_ctab_handle);
for row in 0..row_count {
let src_addr = port_base + (src_y_offset + row) * port_rb + src_x_offset;
let dst_addr = screen_base + (dst_y + row) * screen_rb + dst_x;
for col in 0..col_count {
let src_idx = bus.read_byte(src_addr + col);
bus.write_byte(dst_addr + col, translation[src_idx as usize]);
}
}
return;
}
}
if port_pixel_size == 4 {
for row in 0..row_count {
let src_row = port_base + (src_y_offset + row) * port_rb;
let dst_row = screen_base + (dst_y + row) * screen_rb;
for col in 0..col_count {
let src_x = src_x_offset + col;
let src_byte = bus.read_byte(src_row + src_x / 2);
let src_index = if src_x & 1 == 0 {
src_byte >> 4
} else {
src_byte & 0x0F
};
let dst_x = dst_x + col;
let dst_addr = dst_row + dst_x / 2;
let dst_shift = if dst_x & 1 == 0 { 4 } else { 0 };
let dst_mask = 0x0F << dst_shift;
let dst_byte = bus.read_byte(dst_addr);
bus.write_byte(dst_addr, (dst_byte & !dst_mask) | (src_index << dst_shift));
}
}
return;
}
for row in 0..row_count {
let src_addr = port_base + (src_y_offset + row) * port_rb + src_x_offset;
let dst_addr = screen_base + (dst_y + row) * screen_rb + dst_x;
bus.block_move(src_addr, dst_addr, col_count);
}
}
pub fn framed_manual_cport_presentation_rect(
&self,
bus: &MacMemoryBus,
) -> Option<super::dispatch::ScreenCopyBitsRect> {
let frame = self.last_screen_frame_rect?;
let (_, _, screen_width, screen_height, screen_depth) = self.screen_mode;
let frame_width = frame.dst_right.saturating_sub(frame.dst_left);
let frame_height = frame.dst_bottom.saturating_sub(frame.dst_top);
if frame.dst_top < 0
|| frame.dst_left < 0
|| frame.dst_bottom > screen_height as i16
|| frame.dst_right > screen_width as i16
|| frame_width <= 0
|| frame_height <= 0
{
return None;
}
for &port in &self.cport_ports {
if self.window_list.contains(&port)
|| self.gworld_devices.contains_key(&port)
|| (bus.read_word(port + 6) & 0xC000) != 0xC000
{
continue;
}
let pixmap_handle = bus.read_long(port + 2);
let pixmap = (pixmap_handle != 0)
.then(|| bus.read_long(pixmap_handle))
.unwrap_or(0);
if pixmap == 0
|| (Self::offscreen_pixmap_base_ptr(bus, pixmap) & 0x3FFF_FFFF)
== self.screen_mode.0
|| bus.read_word(pixmap + 32) != screen_depth
{
continue;
}
let pixmap_height = (bus.read_word(pixmap + 10) as i16)
.saturating_sub(bus.read_word(pixmap + 6) as i16);
let pixmap_width = (bus.read_word(pixmap + 12) as i16)
.saturating_sub(bus.read_word(pixmap + 8) as i16);
let horizontal_frame = frame_width.saturating_sub(pixmap_width);
let vertical_frame = frame_height.saturating_sub(pixmap_height);
if pixmap_width > 0
&& pixmap_height > 0
&& horizontal_frame == vertical_frame
&& (2..=16).contains(&horizontal_frame)
&& horizontal_frame % 2 == 0
{
return Some(frame);
}
}
None
}
pub fn declared_centered_presentation_rect(
&self,
bus: &MacMemoryBus,
) -> Option<super::dispatch::ScreenCopyBitsRect> {
let (screen_base, _, screen_width, screen_height, screen_depth) = self.screen_mode;
let screen_width = u32::from(screen_width);
let screen_height = u32::from(screen_height);
if self.front_window == 0
|| screen_width == 0
|| screen_height == 0
|| !self.window_visible(bus, self.front_window)
{
return None;
}
let front = self.front_window;
if (bus.read_word(front + 6) & 0xC000) != 0xC000 {
return None;
}
let front_pixmap_handle = bus.read_long(front + 2);
let front_pixmap = (front_pixmap_handle != 0)
.then(|| bus.read_long(front_pixmap_handle))
.unwrap_or(0);
if front_pixmap == 0 || (bus.read_long(front_pixmap) & 0x3FFF_FFFF) != screen_base {
return None;
}
let port_covers_screen = (bus.read_word(front + 16) as i16) <= 0
&& (bus.read_word(front + 18) as i16) <= 0
&& (bus.read_word(front + 20) as i16) >= screen_height as i16
&& (bus.read_word(front + 22) as i16) >= screen_width as i16;
let (top, left, bottom, right) = self.window_bounds;
let window_covers_screen =
top <= 0 && left <= 0 && bottom >= screen_height as i16 && right >= screen_width as i16;
if !port_covers_screen && !window_covers_screen {
return None;
}
let min_area = u64::from(screen_width) * u64::from(screen_height) / 2;
let mut best: Option<(u64, i16, i16, i16, i16)> = None;
for &port in &self.cport_ports {
if port == front
|| self.window_list.contains(&port)
|| self.gworld_devices.contains_key(&port)
|| (bus.read_word(port + 6) & 0xC000) != 0xC000
{
continue;
}
let pixmap_handle = bus.read_long(port + 2);
if self
.cport_original_pixmaps
.get(&port)
.is_none_or(|&original| original != pixmap_handle)
{
continue;
}
let pixmap = (pixmap_handle != 0)
.then(|| bus.read_long(pixmap_handle))
.unwrap_or(0);
if pixmap == 0 || bus.read_word(pixmap + 32) != screen_depth {
continue;
}
let src_top = bus.read_word(pixmap + 6) as i16;
let src_left = bus.read_word(pixmap + 8) as i16;
let src_bottom = bus.read_word(pixmap + 10) as i16;
let src_right = bus.read_word(pixmap + 12) as i16;
if src_bottom <= src_top || src_right <= src_left {
continue;
}
let width = (src_right - src_left) as u32;
let height = (src_bottom - src_top) as u32;
let area = u64::from(width) * u64::from(height);
if width > screen_width
|| height > screen_height
|| (width == screen_width && height == screen_height)
|| area < min_area
{
continue;
}
if best.map(|current| area > current.0).unwrap_or(true) {
best = Some((area, src_top, src_left, src_bottom, src_right));
}
}
let (_, src_top, src_left, src_bottom, src_right) = best?;
let width = (src_right - src_left) as u32;
let height = (src_bottom - src_top) as u32;
let dst_left = ((screen_width - width) / 2) as i16;
let dst_top = ((screen_height - height) / 2) as i16;
Some(super::dispatch::ScreenCopyBitsRect {
src_top,
src_left,
src_bottom,
src_right,
dst_top,
dst_left,
dst_bottom: dst_top.saturating_add(height as i16),
dst_right: dst_left.saturating_add(width as i16),
})
}
pub(crate) fn blit_large_manual_cport_to_screen(&mut self, bus: &mut MacMemoryBus) {
let (screen_base, screen_rb, screen_w, screen_h, pixel_size) = self.screen_mode;
let screen_w_u32 = u32::from(screen_w);
let screen_h_u32 = u32::from(screen_h);
let trace = std::env::var_os("SYSTEMLESS_TRACE_BLIT_WINDOW").is_some();
let latched_port = self.manual_cport_presented_port;
if self.front_window == 0
|| pixel_size != 8
|| screen_w == 0
|| screen_h == 0
|| (self.copybits_screen_count != 0 && latched_port == 0)
{
if trace {
eprintln!(
"[BLIT-CPORT] skip: front=${:08X} pixel_size={} screen={}x{} copybits={} latched=${:08X}",
self.front_window,
pixel_size,
screen_w,
screen_h,
self.copybits_screen_count,
latched_port
);
}
return;
}
if !self.window_visible(bus, self.front_window) {
if trace {
eprintln!(
"[BLIT-CPORT] skip: front=${:08X} is not visible",
self.front_window
);
}
return;
}
let front_port = self.front_window;
let front_is_cgraf = (bus.read_word(front_port + 6) & 0xC000) == 0xC000;
let front_base = if front_is_cgraf {
let pm_handle = bus.read_long(front_port + 2);
let pm_ptr = (pm_handle != 0)
.then(|| bus.read_long(pm_handle))
.unwrap_or(0);
if pm_ptr == 0 {
return;
}
Self::offscreen_pixmap_base_ptr(bus, pm_ptr) & 0x3FFF_FFFF
} else {
bus.read_long(front_port + 2)
};
if front_base != screen_base {
if trace {
eprintln!(
"[BLIT-CPORT] skip: front base ${:08X} != screen ${:08X}",
front_base, screen_base
);
}
return;
}
let front_top = bus.read_word(front_port + 16) as i16;
let front_left = bus.read_word(front_port + 18) as i16;
let front_bottom = bus.read_word(front_port + 20) as i16;
let front_right = bus.read_word(front_port + 22) as i16;
let port_rect_covers_screen = front_top <= 0
&& front_left <= 0
&& front_bottom >= screen_h as i16
&& front_right >= screen_w as i16;
let (wt, wl, wb, wr) = self.window_bounds;
let window_bounds_cover_screen =
wt <= 0 && wl <= 0 && wb >= screen_h as i16 && wr >= screen_w as i16;
let front_covers_presentation = port_rect_covers_screen || window_bounds_cover_screen;
let screen_is_dark =
self.screen_is_dark_for_manual_cport(bus, screen_base, screen_rb, screen_w, screen_h);
let dark_screen_allows_presentation = !front_covers_presentation && screen_is_dark;
#[derive(Clone, Copy)]
struct Candidate {
port: u32,
base: u32,
row_bytes: u32,
width: u32,
height: u32,
ctab_handle: u32,
}
let screen_area = u64::from(screen_w_u32) * u64::from(screen_h_u32);
let min_area = (screen_area / 8).max(1);
let mut best: Option<Candidate> = None;
let mut considered_ports = 0u32;
let mut rejected_shape = 0u32;
let mut rejected_replaced_pixmap = 0u32;
let mut rejected_area = 0u32;
for &port in &self.cport_ports {
if port == front_port
|| self.window_list.contains(&port)
|| self.gworld_devices.contains_key(&port)
|| (self.copybits_screen_count != 0 && latched_port != 0 && port != latched_port)
{
continue;
}
considered_ports += 1;
if (bus.read_word(port + 6) & 0xC000) != 0xC000 {
rejected_shape += 1;
continue;
}
let pm_handle = bus.read_long(port + 2);
if pm_handle == 0 {
rejected_shape += 1;
continue;
}
if self
.cport_original_pixmaps
.get(&port)
.is_none_or(|&original| original != pm_handle)
{
rejected_replaced_pixmap += 1;
continue;
}
let pm_ptr = bus.read_long(pm_handle);
if pm_ptr == 0 {
rejected_shape += 1;
continue;
}
let base = Self::offscreen_pixmap_base_ptr(bus, pm_ptr) & 0x3FFF_FFFF;
let row_bytes = (bus.read_word(pm_ptr + 4) & 0x3FFF) as u32;
let top = bus.read_word(pm_ptr + 6) as i16;
let left = bus.read_word(pm_ptr + 8) as i16;
let bottom = bus.read_word(pm_ptr + 10) as i16;
let right = bus.read_word(pm_ptr + 12) as i16;
let width = (right - left).max(0) as u32;
let height = (bottom - top).max(0) as u32;
let port_pixel_size = bus.read_word(pm_ptr + 32) as u32;
if base == 0
|| base == screen_base
|| port_pixel_size != 8
|| width == 0
|| height == 0
|| width > screen_w_u32
|| height > screen_h_u32
|| row_bytes < width
{
rejected_shape += 1;
continue;
}
let area = u64::from(width) * u64::from(height);
if area < min_area {
rejected_area += 1;
continue;
}
let candidate = Candidate {
port,
base,
row_bytes,
width,
height,
ctab_handle: bus.read_long(pm_ptr + 42),
};
if best
.map(|current| area > u64::from(current.width) * u64::from(current.height))
.unwrap_or(true)
{
best = Some(candidate);
}
}
let Some(candidate) = best else {
if trace {
eprintln!(
"[BLIT-CPORT] skip: no candidate (tracked={}, considered={}, shape_rejects={}, replaced_pixmap_rejects={}, area_rejects={}, min_area={})",
self.cport_ports.len(),
considered_ports,
rejected_shape,
rejected_replaced_pixmap,
rejected_area,
min_area
);
}
return;
};
if latched_port != candidate.port {
let (visible_samples, distinct_indices) = self.manual_cport_sample_content(
bus,
candidate.base,
candidate.row_bytes,
candidate.width,
candidate.height,
);
if visible_samples < 8 || distinct_indices < 2 {
if trace {
eprintln!(
"[BLIT-CPORT] skip: candidate port=${:08X} base=${:08X} {}x{} has too little sampled content (visible={}, distinct={})",
candidate.port,
candidate.base,
candidate.width,
candidate.height,
visible_samples,
distinct_indices
);
}
return;
}
if !screen_is_dark {
if trace {
eprintln!(
"[BLIT-CPORT] skip: candidate port=${:08X} base=${:08X} {}x{} cannot latch over visible screen content (samples={})",
candidate.port, candidate.base, candidate.width, candidate.height, visible_samples
);
}
return;
}
}
if !front_covers_presentation && !dark_screen_allows_presentation {
if trace {
eprintln!(
"[BLIT-CPORT] skip: front portRect ({},{},{},{}) and window bounds ({},{},{},{}) do not cover {}x{}, screen is not dark enough for fallback presentation",
front_top,
front_left,
front_bottom,
front_right,
wt,
wl,
wb,
wr,
screen_w,
screen_h
);
}
return;
}
let dst_x = (screen_w_u32 - candidate.width) / 2;
let dst_y = (screen_h_u32 - candidate.height) / 2;
let row_count = candidate.height.min(screen_h_u32.saturating_sub(dst_y));
let col_count = candidate.width.min(screen_w_u32.saturating_sub(dst_x));
if row_count == 0 || col_count == 0 {
return;
}
let current_screen_witness = Self::manual_cport_screen_witness(
bus,
screen_base,
screen_rb,
dst_x,
dst_y,
col_count,
row_count,
);
if latched_port == candidate.port && !self.manual_cport_screen_witness.is_empty() {
let changed_samples = current_screen_witness
.iter()
.zip(&self.manual_cport_screen_witness)
.filter(|(current, previous)| current != previous)
.count();
if changed_samples >= 8 {
if trace {
eprintln!(
"[BLIT-CPORT] releasing port=${:08X}: screen changed at {} witness samples",
candidate.port, changed_samples
);
}
self.manual_cport_presented_port = 0;
self.manual_cport_screen_witness.clear();
return;
}
}
self.manual_cport_presented_port = candidate.port;
if trace {
eprintln!(
"[BLIT-CPORT] presenting port=${:08X} base=${:08X} {}x{} at {},{}",
candidate.port, candidate.base, col_count, row_count, dst_x, dst_y
);
}
let screen_ctab_handle = Self::gdevice_ctab_handle(bus, self.main_gdevice_handle);
let src_ctab_seed = Self::ctab_seed(bus, candidate.ctab_handle);
let dst_ctab_seed = Self::ctab_seed(bus, screen_ctab_handle);
let src_clut = self.read_port_clut(bus, candidate.ctab_handle);
let dst_clut = self.read_port_clut(bus, screen_ctab_handle);
let hardware_palette_active = self.device_clut != self.color_manager_clut;
let skip_canonical_to_screen = Self::uses_canonical_system_8bpp_clut(&src_clut);
if candidate.ctab_handle != screen_ctab_handle
&& matches!(src_ctab_seed, Some(src_seed) if src_seed != 0)
&& src_ctab_seed != dst_ctab_seed
&& !skip_canonical_to_screen
&& !hardware_palette_active
{
let translation =
self.build_palette_translation(bus, &src_clut, &dst_clut, screen_ctab_handle);
for row in 0..row_count {
let src_addr = candidate.base + row * candidate.row_bytes;
let dst_addr = screen_base + (dst_y + row) * screen_rb + dst_x;
for col in 0..col_count {
let src_idx = bus.read_byte(src_addr + col);
bus.write_byte(dst_addr + col, translation[src_idx as usize]);
}
}
self.manual_cport_screen_witness = Self::manual_cport_screen_witness(
bus,
screen_base,
screen_rb,
dst_x,
dst_y,
col_count,
row_count,
);
return;
}
for row in 0..row_count {
let src_addr = candidate.base + row * candidate.row_bytes;
let dst_addr = screen_base + (dst_y + row) * screen_rb + dst_x;
bus.block_move(src_addr, dst_addr, col_count);
}
self.manual_cport_screen_witness = Self::manual_cport_screen_witness(
bus,
screen_base,
screen_rb,
dst_x,
dst_y,
col_count,
row_count,
);
}
fn manual_cport_screen_witness(
bus: &MacMemoryBus,
screen_base: u32,
screen_row_bytes: u32,
left: u32,
top: u32,
width: u32,
height: u32,
) -> Vec<u8> {
let step_x = (width / 16).max(1);
let step_y = (height / 12).max(1);
let mut witness = Vec::with_capacity(16 * 12);
let mut y = step_y / 2;
while y < height {
let mut x = step_x / 2;
while x < width {
witness.push(bus.read_byte(screen_base + (top + y) * screen_row_bytes + left + x));
x += step_x;
}
y += step_y;
}
witness
}
pub fn manual_cport_presentation_rect(
&self,
bus: &MacMemoryBus,
) -> Option<super::dispatch::ScreenCopyBitsRect> {
let port = self.manual_cport_presented_port;
if port == 0 || (bus.read_word(port + 6) & 0xC000) != 0xC000 {
return None;
}
let pixmap_handle = bus.read_long(port + 2);
let pixmap = (pixmap_handle != 0)
.then(|| bus.read_long(pixmap_handle))
.unwrap_or(0);
if pixmap == 0 {
return None;
}
let (_, _, screen_width, screen_height, screen_depth) = self.screen_mode;
if bus.read_word(pixmap + 32) != screen_depth {
return None;
}
let src_top = bus.read_word(pixmap + 6) as i16;
let src_left = bus.read_word(pixmap + 8) as i16;
let src_bottom = bus.read_word(pixmap + 10) as i16;
let src_right = bus.read_word(pixmap + 12) as i16;
let width = u32::from(src_right.saturating_sub(src_left) as u16);
let height = u32::from(src_bottom.saturating_sub(src_top) as u16);
let screen_width = u32::from(screen_width);
let screen_height = u32::from(screen_height);
if width == 0 || height == 0 || width > screen_width || height > screen_height {
return None;
}
let dst_left = ((screen_width - width) / 2) as i16;
let dst_top = ((screen_height - height) / 2) as i16;
Some(super::dispatch::ScreenCopyBitsRect {
src_top,
src_left,
src_bottom,
src_right,
dst_top,
dst_left,
dst_bottom: dst_top.saturating_add(height as i16),
dst_right: dst_left.saturating_add(width as i16),
})
}
fn manual_cport_sample_content(
&self,
bus: &MacMemoryBus,
base: u32,
row_bytes: u32,
width: u32,
height: u32,
) -> (u32, u32) {
if base == 0 || width == 0 || height == 0 || row_bytes < width {
return (0, 0);
}
let visible = |idx: u8| {
let rgb = self.device_clut[idx as usize];
rgb[0] > 0x1111 || rgb[1] > 0x1111 || rgb[2] > 0x1111
};
let sample = |x: u32, y: u32| -> Option<u8> {
if x >= width || y >= height {
return None;
}
Some(bus.read_byte(base + y * row_bytes + x))
};
let mut visible_samples = 0u32;
let mut sampled_indices = [false; 256];
for (x, y) in [
(0, 0),
(width - 1, 0),
(0, height - 1),
(width - 1, height - 1),
(width / 2, height / 2),
] {
if let Some(index) = sample(x, y) {
sampled_indices[index as usize] = true;
if visible(index) {
visible_samples += 1;
}
}
}
let step_x = (width / 32).max(1);
let step_y = (height / 24).max(1);
let mut y = step_y / 2;
while y < height {
let mut x = step_x / 2;
while x < width {
if let Some(index) = sample(x, y) {
sampled_indices[index as usize] = true;
if visible(index) {
visible_samples += 1;
}
}
x += step_x;
}
y += step_y;
}
(
visible_samples,
sampled_indices.into_iter().filter(|present| *present).count() as u32,
)
}
fn screen_is_dark_for_manual_cport(
&self,
bus: &MacMemoryBus,
screen_base: u32,
screen_rb: u32,
screen_w: u16,
screen_h: u16,
) -> bool {
if screen_w == 0 || screen_h == 0 || self.copybits_screen_count != 0 {
return false;
}
let max_component = 0x1111;
let sample_step_x = (u32::from(screen_w) / 16).max(1);
let sample_step_y = (u32::from(screen_h) / 12).max(1);
let mut y = sample_step_y / 2;
while y < u32::from(screen_h) {
let row = screen_base + y * screen_rb;
let mut x = sample_step_x / 2;
while x < u32::from(screen_w) {
let idx = bus.read_byte(row + x) as usize;
let rgb = self.device_clut[idx];
if rgb[0] > max_component || rgb[1] > max_component || rgb[2] > max_component {
return false;
}
x += sample_step_x;
}
y += sample_step_y;
}
true
}
pub(crate) fn draw_window_chrome(&self, bus: &mut MacMemoryBus, active: bool) {
if self.windows_placed_offscreen.contains(&self.front_window) {
return;
}
let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
self.get_screen_params();
let (wind_top, wind_left, wind_bottom, wind_right) = self.window_bounds;
let menu_bar_height = bus.read_word(crate::memory::globals::addr::MBAR_HEIGHT) as i16;
let tb_top = (wind_top - 19).max(menu_bar_height);
let tb_bottom = wind_top - 1;
let tb_left = wind_left - 1;
let tb_right = wind_right + 1;
Self::fb_fill_rect(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
tb_top,
tb_left,
tb_bottom + 1,
tb_right,
false,
);
let is_movable_modal = self.window_proc_id == 5;
let has_go_away =
active && Self::window_is_document_proc(self.window_proc_id) && self.go_away_flag;
if is_movable_modal {
Self::fb_hline(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
tb_top,
tb_left,
tb_right,
true,
);
}
Self::fb_hline(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
tb_bottom,
tb_left,
tb_right,
true,
);
for y in tb_top..=tb_bottom {
Self::fb_set_pixel(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
tb_left,
y,
true,
);
Self::fb_set_pixel(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
tb_right - 1,
y,
true,
);
}
let font_id: i16 = 0; let font_size: i16 = 12;
let metrics = get_font_metrics(font_id, font_size);
let text_height = metrics.ascent + metrics.descent;
let tb_interior_height = tb_bottom - tb_top - 1;
let text_y = tb_top + 1 + (tb_interior_height - text_height) / 2 + metrics.ascent;
let (title_clear_left, title_clear_right) = if !self.window_title.is_empty() {
let mut title_width: i16 = 0;
for ch in self.window_title.chars() {
if let Some((glyph, _)) = get_glyph(font_id, font_size, ch) {
title_width += glyph.advance as i16;
} else {
title_width += 6;
}
}
let text_x = tb_left + (tb_right - tb_left - title_width) / 2;
(text_x - 8, text_x + title_width + 8)
} else {
(tb_right, tb_right) };
let _close_box_width = if has_go_away { 15i16 } else { 0 };
if is_movable_modal {
if !self.window_title.is_empty() {
let text_x = title_clear_left + 8;
Self::fb_draw_string(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
text_x,
text_y,
&self.window_title,
font_id,
font_size,
);
}
} else {
if has_go_away {
let cb_size: i16 = 11;
let interior_top = tb_top + 1;
let interior_height = tb_bottom - interior_top;
let cb_top = interior_top + (interior_height - cb_size) / 2;
let cb_left = tb_left + 9;
Self::fb_hline(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
cb_top,
cb_left,
cb_left + cb_size,
true,
);
for y in cb_top..(cb_top + cb_size) {
Self::fb_set_pixel(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
cb_left,
y,
true,
);
}
let inner_right = cb_left + cb_size - 2; let inner_bottom = cb_top + cb_size - 2; for y in (cb_top + 2)..(cb_top + cb_size - 1) {
Self::fb_set_pixel(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
inner_right,
y,
true,
);
}
Self::fb_hline(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
inner_bottom,
cb_left + 2,
cb_left + cb_size - 1,
true,
);
}
if active {
let stripe_left_edge = tb_left + 2;
let stripe_right_end = tb_right - 2;
let stripe_text_left = title_clear_left + 2;
let stripe_text_right = title_clear_right - 2;
let (cb_gap_left, cb_gap_right) = if has_go_away {
let cb_left = tb_left + 9;
let cb_right = cb_left + 10; (cb_left - 1, cb_right + 2) } else {
(stripe_right_end, stripe_right_end) };
for y in (tb_top + 1)..=(tb_bottom - 4) {
if (y - tb_top) % 2 == 1 {
let seg1_end = if has_go_away {
cb_gap_left
} else {
stripe_text_left
};
if stripe_left_edge < seg1_end {
Self::fb_hline(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
y,
stripe_left_edge,
seg1_end,
true,
);
}
if has_go_away && cb_gap_right < stripe_text_left {
Self::fb_hline(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
y,
cb_gap_right,
stripe_text_left,
true,
);
}
if stripe_text_right < stripe_right_end {
Self::fb_hline(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
y,
stripe_text_right,
stripe_right_end,
true,
);
}
}
}
}
if !self.window_title.is_empty() {
let text_x = title_clear_left + 8;
Self::fb_draw_string_clipped(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
text_x,
text_y - 5,
&self.window_title,
font_id,
font_size,
(tb_top, tb_left, tb_bottom - 2, tb_right),
);
}
}
for y in wind_top..wind_bottom {
Self::fb_set_pixel(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
wind_left - 1,
y,
true,
);
Self::fb_set_pixel(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
wind_right,
y,
true,
);
}
Self::fb_hline(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
wind_bottom,
wind_left - 1,
wind_right + 1,
true,
);
for y in tb_top..=(wind_bottom + 1) {
Self::fb_set_pixel(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
wind_right + 1,
y,
true,
);
}
Self::fb_hline(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
wind_bottom + 1,
tb_left + 1,
wind_right + 2,
true,
);
self.capture_gui_frame(bus, "draw_window_chrome");
}
pub(crate) fn draw_grow_icon(&self, bus: &mut MacMemoryBus, window_ptr: u32) {
let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
self.get_screen_params();
let port_top = bus.read_word(window_ptr + 16) as i16;
let port_left = bus.read_word(window_ptr + 18) as i16;
let port_bottom = bus.read_word(window_ptr + 20) as i16;
let port_right = bus.read_word(window_ptr + 22) as i16;
let port_version = bus.read_word(window_ptr + 6);
let (scr_top, scr_left) = if (port_version & 0xC000) == 0xC000 {
let pm_handle = bus.read_long(window_ptr + 2);
if pm_handle != 0 {
let pm_ptr = bus.read_long(pm_handle);
let bt = bus.read_word(pm_ptr + 6) as i16;
let bl = bus.read_word(pm_ptr + 8) as i16;
(-bt, -bl)
} else {
(0, 0)
}
} else {
let bt = bus.read_word(window_ptr + 8) as i16;
let bl = bus.read_word(window_ptr + 10) as i16;
(-bt, -bl)
};
let content_top = scr_top + port_top;
let content_left = scr_left + port_left;
let content_bottom = scr_top + port_bottom;
let content_right = scr_left + port_right;
let sep_x = content_right - 15;
let sep_y = content_bottom - 15;
let border_left = content_left - 1;
let border_right = content_right + 1;
for y in content_top..content_bottom {
Self::fb_set_pixel(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
sep_x,
y,
true,
);
}
Self::fb_hline(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
sep_y,
border_left,
border_right + 1,
true,
);
}
pub(crate) fn draw_thick_rect_border(
&self,
bus: &mut MacMemoryBus,
top: i16,
left: i16,
bottom: i16,
right: i16,
) {
let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
self.get_screen_params();
for dy in 0..2i16 {
Self::fb_hline(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
top + dy,
left,
right,
true,
);
}
Self::fb_hline(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
bottom - 1,
left,
right,
true,
);
for dx in 0..2i16 {
for y in top..bottom {
Self::fb_set_pixel(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
left + dx,
y,
true,
);
}
}
for dx in 0..2i16 {
for y in top..bottom {
Self::fb_set_pixel(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
right - 2 + dx,
y,
true,
);
}
}
}
fn erase_structure_region(
&self,
bus: &mut MacMemoryBus,
top: i16,
left: i16,
bottom: i16,
right: i16,
) {
let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
self.get_screen_params();
Self::fb_fill_rect(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
top,
left,
bottom,
right,
false,
);
}
fn erase_structure_frame_around_content(
&self,
bus: &mut MacMemoryBus,
structure: (i16, i16, i16, i16),
content: (i16, i16, i16, i16),
) {
let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
self.get_screen_params();
let (st, sl, sb, sr) = structure;
let (ct, cl, cb, cr) = content;
let mut erase = |top: i16, left: i16, bottom: i16, right: i16| {
if bottom <= top || right <= left {
return;
}
Self::fb_fill_rect(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
top,
left,
bottom,
right,
false,
);
};
erase(st, sl, ct.min(sb), sr);
erase(cb.max(st), sl, sb, sr);
erase(ct.max(st), sl, cb.min(sb), cl.min(sr));
erase(ct.max(st), cr.max(sl), cb.min(sb), sr);
}
pub(crate) fn draw_single_window_chrome_inline(
&mut self,
bus: &mut MacMemoryBus,
window_ptr: u32,
hilited: bool,
) {
if window_ptr == 0 {
return;
}
if bus.read_byte(window_ptr + 110u32) == 0 {
return; }
if self.windows_placed_offscreen.contains(&window_ptr) {
return;
}
if self.window_uses_custom_def_proc(bus, window_ptr) {
return;
}
let proc_id = self.window_proc_ids.get(&window_ptr).copied().unwrap_or(0);
let port_version = bus.read_word(window_ptr + 6);
let (pmap_top, pmap_left) = if (port_version & 0xC000) == 0xC000 {
let pm_handle = bus.read_long(window_ptr + 2);
let pm_ptr = bus.read_long(pm_handle);
(
bus.read_word(pm_ptr + 6) as i16,
bus.read_word(pm_ptr + 8) as i16,
)
} else {
(
bus.read_word(window_ptr + 8) as i16,
bus.read_word(window_ptr + 10) as i16,
)
};
let wind_top = pmap_top.wrapping_neg();
let wind_left = pmap_left.wrapping_neg();
let port_bottom = bus.read_word(window_ptr + 20) as i16;
let port_right = bus.read_word(window_ptr + 22) as i16;
let wind_bottom = wind_top.wrapping_add(port_bottom);
let wind_right = wind_left.wrapping_add(port_right);
if wind_bottom <= wind_top || wind_right <= wind_left {
return;
}
let saved_bounds = self.window_bounds;
let saved_title = self.window_title.clone();
let saved_go_away = self.go_away_flag;
let saved_proc = self.window_proc_id;
self.window_bounds = (wind_top, wind_left, wind_bottom, wind_right);
let title_h = bus.read_long(window_ptr + 134u32);
self.window_title = if title_h != 0 {
let title_p = bus.read_long(title_h);
if title_p != 0 {
String::from_utf8_lossy(&bus.read_pstring(title_p)).into_owned()
} else {
String::new()
}
} else {
String::new()
};
self.go_away_flag = bus.read_byte(window_ptr + 112u32) != 0;
self.window_proc_id = proc_id;
if matches!(proc_id, 1..=3) {
self.draw_window_frame(bus);
} else {
self.draw_window_chrome(bus, hilited);
}
self.window_bounds = saved_bounds;
self.window_title = saved_title;
self.go_away_flag = saved_go_away;
self.window_proc_id = saved_proc;
}
pub(crate) fn draw_window_frame(&self, bus: &mut MacMemoryBus) {
if self.windows_placed_offscreen.contains(&self.front_window) {
return;
}
let (wind_top, wind_left, wind_bottom, wind_right) = self.window_bounds;
if matches!(self.window_proc_id, 1 | 2 | 3 | 5) {
let frame = match self.window_proc_id {
1 => (wind_top - 8, wind_left - 8, wind_bottom + 3, wind_right + 8),
2 => (wind_top - 1, wind_left - 1, wind_bottom + 1, wind_right + 1),
3 => (wind_top - 1, wind_left - 1, wind_bottom + 3, wind_right + 3),
5 => (
wind_top - 23,
wind_left - 8,
wind_bottom + 8,
wind_right + 8,
),
_ => (wind_top, wind_left, wind_bottom, wind_right),
};
if self.draw_theme_dialog_frame(
bus,
(wind_top, wind_left, wind_bottom, wind_right),
frame,
self.window_proc_id,
true,
false,
) {
self.capture_gui_frame(bus, "draw_window_frame");
return;
}
}
match self.window_proc_id {
2 => {
self.draw_rect_border(
bus,
wind_top - 1,
wind_left - 1,
wind_bottom + 1,
wind_right + 1,
);
}
1 => {
let struc_top = wind_top - 8;
let struc_left = wind_left - 8;
let struc_bottom = wind_bottom + 3;
let struc_right = wind_right + 8;
self.erase_structure_frame_around_content(
bus,
(struc_top, struc_left, struc_bottom, struc_right),
(wind_top, wind_left, wind_bottom, wind_right),
);
self.draw_rect_border(bus, struc_top, struc_left, struc_bottom, struc_right);
self.draw_thick_rect_border(
bus,
wind_top - 5,
wind_left - 5,
struc_bottom,
wind_right + 5,
);
}
3 => {
self.erase_structure_frame_around_content(
bus,
(wind_top - 1, wind_left - 1, wind_bottom + 3, wind_right + 3),
(wind_top, wind_left, wind_bottom, wind_right),
);
self.draw_rect_border(
bus,
wind_top - 1,
wind_left - 1,
wind_bottom + 1,
wind_right + 1,
);
self.draw_shadow(
bus,
wind_top - 1,
wind_left - 1,
wind_bottom + 1,
wind_right + 1,
);
}
5 => {
let struc_top = wind_top - 23;
let struc_left = wind_left - 8;
let struc_bottom = wind_bottom + 8;
let struc_right = wind_right + 8;
self.erase_structure_frame_around_content(
bus,
(struc_top, struc_left, struc_bottom, struc_right),
(wind_top, wind_left, wind_bottom, wind_right),
);
self.draw_rect_border(bus, struc_top, struc_left, struc_bottom, struc_right);
let thick_top = wind_top - 5;
let thick_left = wind_left - 5;
let thick_bottom = wind_bottom + 5;
let thick_right = wind_right + 5;
self.draw_thick_rect_border(bus, thick_top, thick_left, thick_bottom, thick_right);
let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
self.get_screen_params();
Self::fb_hline(
bus,
screen_base,
row_bytes,
pixel_size,
screen_width,
screen_height,
thick_bottom - 2,
thick_left,
thick_right,
true,
);
}
proc_id if Self::window_is_document_proc(proc_id) => {
let tb_top = wind_top - 19;
self.erase_structure_region(
bus,
tb_top,
wind_left - 1,
wind_bottom + 2,
wind_right + 2,
);
self.draw_window_chrome(bus, true);
}
_ => {
self.draw_rect_border(
bus,
wind_top - 1,
wind_left - 1,
wind_bottom + 1,
wind_right + 1,
);
}
}
self.capture_gui_frame(bus, "draw_window_frame");
}
pub(crate) fn restore_visible_dialog_snapshots(&mut self, bus: &mut MacMemoryBus) {
if self.dialog_visible_snapshots.is_empty() {
return;
}
let front_window = self.front_window;
let front_is_dialog = front_window != 0 && self.dialog_items.contains_key(&front_window);
let snapshots: Vec<_> = self
.dialog_visible_snapshots
.iter()
.filter_map(|(&dialog_ptr, snapshot)| {
if self.dialogs_drawn_by_app.contains(&dialog_ptr) {
return None;
}
if front_is_dialog && dialog_ptr != front_window {
None
} else {
Some(snapshot.clone())
}
})
.collect();
for snapshot in snapshots {
let bounds = snapshot.bounds;
self.restore_dialog_pixels(bus, bounds, &snapshot.pixels);
}
}
pub fn redraw_chrome(&mut self, bus: &mut MacMemoryBus) {
let pending_dialog_snapshot = self
.dialog_tracking
.as_ref()
.map(|tracking| !tracking.game_managed && !tracking.rendered_pixels_final)
.unwrap_or(false);
if !pending_dialog_snapshot {
self.blit_window_to_screen(bus);
self.blit_large_manual_cport_to_screen(bus);
}
let menu_bar_height = bus.read_word(crate::memory::globals::addr::MBAR_HEIGHT) as i16;
let (_, _, screen_w, screen_h, _) = self.screen_mode;
let (wt, wl, wb, wr) = self.window_bounds;
if self.fullscreen_locked && menu_bar_height > 0 {
self.fullscreen_locked = false;
}
if self.front_window != 0
&& wt <= 0
&& wl <= 0
&& wb >= screen_h as i16
&& wr >= screen_w as i16
&& menu_bar_height <= 0
{
self.fullscreen_locked = true;
}
self.restore_kiosk_dialog_desktop_background(bus);
if !self.menus.is_empty() && !self.fullscreen_locked && !self.menu_bar_hidden {
self.draw_menu_bar_to_fb(bus);
}
let effective_mbar = if self.fullscreen_locked || self.menu_bar_hidden {
0
} else {
menu_bar_height
};
if self.front_window != 0 && !no_visrgn_auto_expand_enabled() && effective_mbar == 0 {
let vis_top_expected = 0i16;
let vis_rgn_handle = bus.read_long(self.front_window + 24);
if vis_rgn_handle != 0 {
let vis_rgn = bus.read_long(vis_rgn_handle);
if vis_rgn != 0 {
let current_vis_top = bus.read_word(vis_rgn + 2) as i16;
if current_vis_top != vis_top_expected {
bus.write_word(vis_rgn + 2, vis_top_expected as u16);
let clip_rgn_handle = bus.read_long(self.front_window + 28);
if clip_rgn_handle != 0 {
let clip_rgn = bus.read_long(clip_rgn_handle);
if clip_rgn != 0 {
let clip_top = bus.read_word(clip_rgn + 2) as i16;
if clip_top > vis_top_expected {
bus.write_word(clip_rgn + 2, vis_top_expected as u16);
}
}
}
let port_top = bus.read_word(self.front_window + 16) as i16;
if port_top > vis_top_expected {
bus.write_word(self.front_window + 16, vis_top_expected as u16);
}
self.window_bounds.0 = vis_top_expected;
}
}
}
}
let hidden_menu_fullscreen_top = if menu_bar_height > 0 {
menu_bar_height.saturating_add(2)
} else {
22
};
let hidden_menu_fullscreen_window = self.menu_bar_hidden
&& wt <= hidden_menu_fullscreen_top
&& wl <= 2
&& wb >= screen_h as i16 - 2
&& wr >= screen_w as i16 - 2;
let skip_chrome = matches!(self.window_proc_id, 1 | 2 | 3 | 5)
|| self.fullscreen_locked
|| menu_bar_height <= 0
|| hidden_menu_fullscreen_window;
if !skip_chrome && menu_bar_height > 0 {
let list_snapshot = self.window_list.clone();
let saved_bounds = self.window_bounds;
let saved_title = self.window_title.clone();
let saved_proc = self.window_proc_id;
let saved_go_away = self.go_away_flag;
for &w in list_snapshot.iter().rev() {
if w == self.front_window {
continue;
}
if bus.read_byte(w + 110u32) == 0 {
continue;
}
let preserved_front_pixels: Vec<_> = self
.window_structure_rect(bus, w)
.map(|back_structure| {
list_snapshot
.iter()
.take_while(|&&front_window| front_window != w)
.filter(|&&front_window| bus.read_byte(front_window + 110u32) != 0)
.filter_map(|&front_window| {
self.window_structure_rect(bus, front_window)
.and_then(|front_structure| {
Self::rect_intersection(back_structure, front_structure)
})
.and_then(|rect| self.save_screen_rect_pixels(bus, rect))
})
.collect()
})
.unwrap_or_default();
let port_version = bus.read_word(w + 6);
let (pmap_top, pmap_left) = if (port_version & 0xC000) == 0xC000 {
let pm_handle = bus.read_long(w + 2);
let pm_ptr = bus.read_long(pm_handle);
(
bus.read_word(pm_ptr + 6) as i16,
bus.read_word(pm_ptr + 8) as i16,
)
} else {
(bus.read_word(w + 8) as i16, bus.read_word(w + 10) as i16)
};
let wind_top = -pmap_top;
let wind_left = -pmap_left;
let port_bottom = bus.read_word(w + 20) as i16;
let port_right = bus.read_word(w + 22) as i16;
let wind_bottom = wind_top + port_bottom;
let wind_right = wind_left + port_right;
if wind_bottom <= wind_top || wind_right <= wind_left {
continue;
}
self.window_bounds = (wind_top, wind_left, wind_bottom, wind_right);
let title_h = bus.read_long(w + 134u32);
self.window_title = if title_h != 0 {
let title_p = bus.read_long(title_h);
if title_p != 0 {
String::from_utf8_lossy(&bus.read_pstring(title_p)).into_owned()
} else {
String::new()
}
} else {
String::new()
};
self.go_away_flag = bus.read_byte(w + 112u32) != 0;
let w_proc = self.window_proc_ids.get(&w).copied().unwrap_or(0);
if self.window_uses_custom_def_proc(bus, w) {
continue;
}
self.window_proc_id = w_proc;
let hilited = bus.read_byte(w + 111u32) != 0;
if matches!(w_proc, 1..=3) {
self.draw_window_frame(bus);
} else {
self.draw_window_chrome(bus, hilited);
}
for (top, left, width, height, pixels) in preserved_front_pixels {
self.restore_screen_rect_pixels(bus, top, left, width, height, &pixels);
}
}
self.window_bounds = saved_bounds;
self.window_title = saved_title;
self.window_proc_id = saved_proc;
self.go_away_flag = saved_go_away;
}
if self.front_window != 0 && !skip_chrome {
let front_hilited = bus.read_byte(self.front_window + 111u32) != 0;
self.draw_single_window_chrome_inline(bus, self.front_window, front_hilited);
}
if let Some(ref tracking) = self.dialog_tracking {
if !tracking.game_managed && tracking.rendered_pixels_final {
self.restore_dialog_pixels(bus, tracking.bounds, &tracking.rendered_pixels);
if tracking.edit_item > 0 {
let idx = (tracking.edit_item - 1) as usize;
if idx < tracking.items.len() {
let item = &tracking.items[idx];
let abs_top = tracking.bounds.0 + item.rect.0;
let abs_left = tracking.bounds.1 + item.rect.1;
let abs_bottom = tracking.bounds.0 + item.rect.2;
let abs_right = tracking.bounds.1 + item.rect.3;
self.draw_edit_text(
bus,
abs_top,
abs_left,
abs_bottom,
abs_right,
&tracking.edit_text,
!tracking.edit_text_modified,
);
}
}
if tracking.flash_remaining > 0 && tracking.flash_item > 0 {
let fi = tracking.flash_item;
if (fi as usize) <= tracking.items.len() {
let item = &tracking.items[(fi - 1) as usize];
let (it, il, ib, ir) = item.rect;
let abs_top = tracking.bounds.0 + it;
let abs_left = tracking.bounds.1 + il;
let abs_bottom = tracking.bounds.0 + ib;
let abs_right = tracking.bounds.1 + ir;
if tracking.flash_remaining % 2 == 0 {
self.invert_button_rect(bus, abs_top, abs_left, abs_bottom, abs_right);
}
}
}
if let Some(ref popup) = tracking.active_popup {
self.draw_menu_dropdown(bus, popup.active_menu, popup.dropdown_rect);
if popup.highlighted_item > 0 {
self.invert_dropdown_item_rect(
bus,
popup.active_menu,
popup.dropdown_rect,
popup.highlighted_item,
);
}
}
}
} else {
self.restore_visible_dialog_snapshots(bus);
self.redraw_retained_modal_dialog_click(bus);
}
if let Some(ref tracking) = self.menu_tracking {
self.highlight_menu_title(bus, tracking.active_menu);
self.draw_menu_dropdown(bus, tracking.active_menu, tracking.dropdown_rect);
let show_highlight = if tracking.flash_remaining > 0 {
tracking.flash_remaining % 2 == 0
} else {
true
};
if tracking.highlighted_item > 0 && show_highlight {
self.invert_menu_item(bus, tracking.highlighted_item);
}
}
self.capture_gui_frame(bus, "redraw_chrome");
}
}
#[cfg(test)]
mod redraw_chrome_tests {
use super::super::dispatch::DialogItem;
use super::super::test_helpers::setup_with_port;
use super::super::TrapDispatcher;
use crate::memory::MemoryBus;
const PORT_PTR: u32 = 0x181000;
const VIS_RGN: u32 = 0x182000;
const CLIP_RGN: u32 = 0x182200;
const WINDOW_VISIBLE_OFFSET: u32 = 110;
#[test]
fn indexed_framebuffer_helpers_preserve_adjacent_four_bit_pixels() {
let (mut disp, _cpu, mut bus) = setup_with_port();
let screen_base = bus.alloc(2);
bus.write_byte(screen_base, 0xAB);
bus.write_byte(screen_base + 1, 0xCD);
TrapDispatcher::fb_set_pixel_index(&mut bus, screen_base, 2, 4, 4, 1, 0, 0, 3);
TrapDispatcher::fb_set_pixel_index(&mut bus, screen_base, 2, 4, 4, 1, 1, 0, 4);
assert_eq!(bus.read_byte(screen_base), 0x34);
assert_eq!(bus.read_byte(screen_base + 1), 0xCD);
TrapDispatcher::fb_fill_rect_index(&mut bus, screen_base, 2, 4, 4, 1, 0, 1, 1, 3, 5);
assert_eq!(bus.read_byte(screen_base), 0x35);
assert_eq!(bus.read_byte(screen_base + 1), 0x5D);
disp.screen_mode = (screen_base, 2, 4, 1, 4);
let saved = disp
.save_screen_rect_pixels(&bus, (0, 1, 1, 3))
.expect("packed screen rectangle");
assert_eq!(saved.4, vec![5, 5]);
bus.write_byte(screen_base, 0xAA);
bus.write_byte(screen_base + 1, 0xAA);
disp.restore_screen_rect_pixels(&mut bus, saved.0, saved.1, saved.2, saved.3, &saved.4);
assert_eq!(bus.read_byte(screen_base), 0xA5);
assert_eq!(bus.read_byte(screen_base + 1), 0x5A);
}
fn track_manual_cport(disp: &mut TrapDispatcher, bus: &crate::memory::MacMemoryBus, port: u32) {
disp.cport_ports.insert(port);
disp.cport_original_pixmaps
.insert(port, bus.read_long(port + 2));
}
#[test]
fn fb_fill_pattern_rect_tiles_standard_gray_pattern() {
let (mut disp, _cpu, mut bus) = setup_with_port();
let screen_base = bus.alloc(8 * 8);
disp.screen_mode = (screen_base, 8, 8, 8, 8);
TrapDispatcher::fb_fill_pattern_rect(
&mut bus,
screen_base,
8,
8,
8,
8,
0,
0,
8,
8,
[0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55],
);
assert_eq!(bus.read_byte(screen_base), 255);
assert_eq!(bus.read_byte(screen_base + 1), 0);
assert_eq!(bus.read_byte(screen_base + 8), 0);
assert_eq!(bus.read_byte(screen_base + 9), 255);
}
fn install_twilight_style_black_index(
disp: &mut TrapDispatcher,
bus: &mut crate::memory::MacMemoryBus,
) {
let gdevice_handle = disp.ensure_main_gdevice(bus);
bus.write_long(0x08A4, gdevice_handle);
bus.write_long(0x0CC8, gdevice_handle);
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 black_entry = ctab + 8 + 8;
bus.write_word(black_entry, 1);
bus.write_word(black_entry + 2, 0);
bus.write_word(black_entry + 4, 0);
bus.write_word(black_entry + 6, 0);
let repurposed_entry = ctab + 8 + 255 * 8;
bus.write_word(repurposed_entry, 255);
bus.write_word(repurposed_entry + 2, 0xFFFF);
bus.write_word(repurposed_entry + 4, 0xFFFF);
bus.write_word(repurposed_entry + 6, 0xCCCC);
}
fn make_ctab_handle(
bus: &mut crate::memory::MacMemoryBus,
clut: &[[u16; 3]; 256],
seed: u32,
) -> u32 {
let ctab = bus.alloc(8 + 256 * 8);
bus.write_long(ctab, seed);
bus.write_word(ctab + 4, 0x8000);
bus.write_word(ctab + 6, 255);
for index in 0u32..256 {
let entry = ctab + 8 + index * 8;
let [r, g, b] = clut[index as usize];
bus.write_word(entry, index as u16);
bus.write_word(entry + 2, r);
bus.write_word(entry + 4, g);
bus.write_word(entry + 6, b);
}
let handle = bus.alloc(4);
bus.write_long(handle, ctab);
handle
}
fn install_8bpp_cgrafport(
bus: &mut crate::memory::MacMemoryBus,
base: u32,
row_bytes: u16,
width: u16,
height: u16,
ctab_handle: u32,
) -> u32 {
let pixmap = bus.alloc(50);
bus.write_long(pixmap, base);
bus.write_word(pixmap + 4, row_bytes | 0x8000);
bus.write_word(pixmap + 6, 0);
bus.write_word(pixmap + 8, 0);
bus.write_word(pixmap + 10, height);
bus.write_word(pixmap + 12, width);
bus.write_word(pixmap + 14, 0);
bus.write_word(pixmap + 16, 0);
bus.write_long(pixmap + 18, 0);
bus.write_long(pixmap + 22, 0x0048_0000);
bus.write_long(pixmap + 26, 0x0048_0000);
bus.write_word(pixmap + 30, 0);
bus.write_word(pixmap + 32, 8);
bus.write_word(pixmap + 34, 1);
bus.write_word(pixmap + 36, 8);
bus.write_long(pixmap + 38, 0);
bus.write_long(pixmap + 42, ctab_handle);
bus.write_long(pixmap + 46, 0);
let pixmap_handle = bus.alloc(4);
bus.write_long(pixmap_handle, pixmap);
bus.write_long(PORT_PTR + 2, pixmap_handle);
bus.write_word(PORT_PTR + 6, 0xC000);
bus.write_word(PORT_PTR + 16, 0);
bus.write_word(PORT_PTR + 18, 0);
bus.write_word(PORT_PTR + 20, height);
bus.write_word(PORT_PTR + 22, width);
pixmap_handle
}
fn install_8bpp_cgrafport_at(
bus: &mut crate::memory::MacMemoryBus,
port: u32,
base: u32,
row_bytes: u16,
width: u16,
height: u16,
ctab_handle: u32,
) -> u32 {
let pixmap = bus.alloc(50);
bus.write_long(pixmap, base);
bus.write_word(pixmap + 4, row_bytes | 0x8000);
bus.write_word(pixmap + 6, 0);
bus.write_word(pixmap + 8, 0);
bus.write_word(pixmap + 10, height);
bus.write_word(pixmap + 12, width);
bus.write_long(pixmap + 22, 0x0048_0000);
bus.write_long(pixmap + 26, 0x0048_0000);
bus.write_word(pixmap + 32, 8);
bus.write_word(pixmap + 34, 1);
bus.write_word(pixmap + 36, 8);
bus.write_long(pixmap + 42, ctab_handle);
let pixmap_handle = bus.alloc(4);
bus.write_long(pixmap_handle, pixmap);
bus.write_long(port + 2, pixmap_handle);
bus.write_word(port + 6, 0xC000);
bus.write_word(port + 16, 0);
bus.write_word(port + 18, 0);
bus.write_word(port + 20, height);
bus.write_word(port + 22, width);
pixmap_handle
}
#[test]
fn redraw_chrome_preserves_window_bounds_when_menu_bar_visible() {
let (mut disp, _cpu, mut bus) = setup_with_port();
bus.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 20);
bus.write_word(VIS_RGN + 2, 40); bus.write_word(CLIP_RGN + 2, 40); bus.write_word(PORT_PTR + 16, 40); disp.front_window = PORT_PTR;
disp.window_bounds = (40, 2, 339, 508);
disp.window_proc_id = 0; disp.fullscreen_locked = false;
disp.menu_bar_hidden = false;
disp.redraw_chrome(&mut bus);
assert_eq!(
disp.window_bounds.0, 40,
"window_bounds.0 must not be clobbered when MBarHeight>0"
);
assert_eq!(
bus.read_word(VIS_RGN + 2) as i16,
40,
"visRgn.top must not be rewritten when MBarHeight>0"
);
assert_eq!(
bus.read_word(PORT_PTR + 16) as i16,
40,
"port_top must not be rewritten when MBarHeight>0"
);
}
#[test]
fn redraw_chrome_expands_visrgn_when_menu_bar_hidden() {
let (mut disp, _cpu, mut bus) = setup_with_port();
bus.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 0);
bus.write_word(VIS_RGN + 2, 20); bus.write_word(CLIP_RGN + 2, 20); bus.write_word(PORT_PTR + 16, 20); disp.front_window = PORT_PTR;
disp.window_bounds = (20, 0, 342, 512);
disp.window_proc_id = 0; disp.fullscreen_locked = false;
disp.redraw_chrome(&mut bus);
assert_eq!(
bus.read_word(VIS_RGN + 2) as i16,
0,
"visRgn.top must be expanded to 0 when MBarHeight=0"
);
assert_eq!(
disp.window_bounds.0, 0,
"window_bounds.0 must be updated to match expanded visRgn"
);
assert_eq!(
bus.read_word(PORT_PTR + 16) as i16,
0,
"port_top must be updated to match expanded visRgn"
);
}
#[test]
fn redraw_chrome_expands_visrgn_when_host_hides_menu_bar() {
let (mut disp, _cpu, mut bus) = setup_with_port();
bus.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 20);
bus.write_word(VIS_RGN + 2, 20); bus.write_word(CLIP_RGN + 2, 20);
bus.write_word(PORT_PTR + 16, 20);
disp.front_window = PORT_PTR;
disp.window_bounds = (20, 0, 342, 512);
disp.window_proc_id = 0; disp.fullscreen_locked = false;
disp.menu_bar_hidden = true;
disp.redraw_chrome(&mut bus);
assert_eq!(
bus.read_word(VIS_RGN + 2) as i16,
0,
"visRgn.top must be expanded to 0 when host hides the menu bar"
);
assert_eq!(
disp.window_bounds.0, 0,
"window_bounds.0 must follow visRgn.top up to 0"
);
assert_eq!(
bus.read_word(PORT_PTR + 16) as i16,
0,
"port_top must follow visRgn.top up to 0"
);
}
#[test]
fn redraw_chrome_does_not_paint_menu_bar_when_menu_bar_hidden() {
let (mut disp, _cpu, mut bus) = setup_with_port();
let screen_base = bus.alloc(800 * 600);
disp.screen_mode = (screen_base, 800, 800, 600, 8);
bus.write_long(0x0824, screen_base);
for i in 0u32..(800 * 20) {
bus.write_byte(screen_base + i, 0xAA);
}
bus.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 20);
disp.menus.push(super::super::menu::Menu {
id: 1,
title: String::from("Apple"),
items: Vec::new(),
enabled: true,
handle: 0,
in_menu_bar: true,
hierarchical: false,
visible_in_menu_bar: true,
});
disp.front_window = 0;
disp.fullscreen_locked = false;
disp.menu_bar_hidden = true;
disp.redraw_chrome(&mut bus);
for &x in &[0u32, 100, 400, 799] {
for &y in &[0u32, 5, 10, 19] {
let off = y * 800 + x;
assert_eq!(
bus.read_byte(screen_base + off),
0xAA,
"menu bar pixel at (x={}, y={}) should not be repainted \
when menu_bar_hidden=true",
x,
y
);
}
}
}
#[test]
fn redraw_chrome_repaints_document_window_when_host_hides_menu_bar() {
let (mut disp, mut cpu, mut bus) = super::super::test_helpers::setup();
let screen_base = bus.alloc(800 * 600);
for offset in 0..800 * 600 {
bus.write_byte(screen_base + offset, 0xAA);
}
disp.screen_mode = (screen_base, 800, 800, 600, 8);
bus.write_long(crate::memory::globals::addr::SCREEN_BITS, screen_base);
bus.write_long(crate::memory::globals::addr::SCRN_BASE, screen_base);
bus.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 20);
disp.menu_bar_hidden = true;
let window_addr = bus.alloc(256);
disp.init_cgraf_window(
&mut bus,
&mut cpu,
window_addr,
screen_base,
60,
30,
220,
370,
"Zoom",
8,
true,
true,
true,
0,
);
for y in 0..20u32 {
for x in 0..800u32 {
bus.write_byte(screen_base + y * 800 + x, 0xAA);
}
}
for y in 41..59u32 {
for x in 29..371u32 {
bus.write_byte(screen_base + y * 800 + x, 0xAA);
}
}
disp.redraw_chrome(&mut bus);
assert_eq!(
bus.read_byte(screen_base + 10 * 800 + 100),
0xAA,
"hidden host menu bar should not be repainted"
);
assert_ne!(
bus.read_byte(screen_base + 45 * 800 + 50),
0xAA,
"non-fullscreen zoom document windows should redraw titlebar chrome"
);
}
#[test]
fn redraw_chrome_paints_menu_bar_when_not_hidden() {
let (mut disp, _cpu, mut bus) = setup_with_port();
let screen_base = bus.alloc(800 * 600);
disp.screen_mode = (screen_base, 800, 800, 600, 8);
bus.write_long(0x0824, screen_base);
for i in 0u32..(800 * 20) {
bus.write_byte(screen_base + i, 0xAA);
}
bus.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 20);
disp.menus.push(super::super::menu::Menu {
id: 1,
title: String::from("Apple"),
items: Vec::new(),
enabled: true,
handle: 0,
in_menu_bar: true,
hierarchical: false,
visible_in_menu_bar: true,
});
disp.front_window = 0;
disp.fullscreen_locked = false;
disp.menu_bar_hidden = false;
disp.redraw_chrome(&mut bus);
for &x in &[100u32, 400, 700] {
for &y in &[5u32, 10] {
let off = y * 800 + x;
let pix = bus.read_byte(screen_base + off);
assert_ne!(
pix, 0xAA,
"menu bar pixel at (x={}, y={}) should be painted \
when menu_bar_hidden=false (read 0x{:02X}, sentinel 0xAA)",
x, y, pix
);
}
}
}
#[test]
fn redraw_chrome_restores_desktop_pattern_behind_single_kiosk_dialog() {
let (mut disp, _cpu, mut bus) = setup_with_port();
let (screen_base, row_bytes, screen_w, screen_h, _) = disp.screen_mode;
bus.fill_bytes(screen_base, row_bytes * screen_h as u32, 0xFF);
bus.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 20);
disp.menu_bar_hidden = true;
disp.fullscreen_locked = false;
disp.front_window = PORT_PTR;
disp.window_list = vec![PORT_PTR];
disp.window_bounds = (100, 180, 208, 620);
disp.window_proc_id = 5;
disp.window_proc_ids.insert(PORT_PTR, 5);
disp.dialog_items
.insert(PORT_PTR, vec![DialogItem::default()]);
bus.write_byte(PORT_PTR + WINDOW_VISIBLE_OFFSET, 1);
disp.window_saved_under_pixels
.insert(PORT_PTR, (100, 180, 440, 108, vec![0xFF; 440 * 108]));
disp.redraw_chrome(&mut bus);
assert_eq!(
bus.read_byte(screen_base),
0xFF,
"standard gray pattern starts with a black pixel"
);
assert_eq!(
bus.read_byte(screen_base + 1),
0x00,
"standard gray pattern should add white desktop pixels"
);
assert_eq!(
bus.read_byte(screen_base + 120 * row_bytes + 200),
0xFF,
"dialog bounds must not be overwritten by the desktop fill"
);
let saved = &disp.window_saved_under_pixels[&PORT_PTR].4;
assert_eq!(
&saved[..2],
&[0xFF, 0x00],
"the save-under snapshot must track the synthesized desktop pattern"
);
assert_eq!(screen_w, 800, "test assumes the default 800-wide screen");
}
#[test]
fn redraw_chrome_keeps_existing_content_behind_kiosk_dialog() {
let (mut disp, _cpu, mut bus) = setup_with_port();
let (screen_base, row_bytes, _screen_w, screen_h, _) = disp.screen_mode;
bus.fill_bytes(screen_base, row_bytes * screen_h as u32, 0xFF);
bus.write_byte(screen_base, 0x00);
bus.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 20);
disp.menu_bar_hidden = true;
disp.fullscreen_locked = false;
disp.front_window = PORT_PTR;
disp.window_list = vec![PORT_PTR];
disp.window_bounds = (100, 180, 208, 620);
disp.window_proc_id = 5;
disp.window_proc_ids.insert(PORT_PTR, 5);
disp.dialog_items
.insert(PORT_PTR, vec![DialogItem::default()]);
bus.write_byte(PORT_PTR + WINDOW_VISIBLE_OFFSET, 1);
disp.redraw_chrome(&mut bus);
assert_eq!(
bus.read_byte(screen_base + 16),
0xFF,
"nonblack exposed content should suppress the desktop-pattern fill"
);
}
#[test]
fn redraw_chrome_does_not_paint_on_mouse_at_top_when_hidden() {
let (mut disp, _cpu, mut bus) = setup_with_port();
let screen_base = bus.alloc(800 * 600);
disp.screen_mode = (screen_base, 800, 800, 600, 8);
bus.write_long(0x0824, screen_base);
for i in 0u32..(800 * 20) {
bus.write_byte(screen_base + i, 0xAA);
}
bus.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 20);
disp.menus.push(super::super::menu::Menu {
id: 1,
title: String::from("Apple"),
items: Vec::new(),
enabled: true,
handle: 0,
in_menu_bar: true,
hierarchical: false,
visible_in_menu_bar: true,
});
disp.front_window = 0;
disp.fullscreen_locked = false;
disp.menu_bar_hidden = true;
disp.redraw_chrome(&mut bus);
for x in [0u32, 100, 400, 799] {
for y in [0u32, 5, 10, 19] {
assert_eq!(
bus.read_byte(screen_base + y * 800 + x),
0xAA,
"initial frame: sentinel must hold at (x={}, y={})",
x,
y
);
}
}
disp.mouse_pos = (2, 400);
bus.write_word(0x0828, 2u16); bus.write_word(0x082A, 400u16); bus.write_word(0x082C, 2u16); bus.write_word(0x082E, 400u16); bus.write_word(0x0830, 2u16); bus.write_word(0x0832, 400u16);
disp.redraw_chrome(&mut bus);
for x in [0u32, 100, 400, 799] {
for y in [0u32, 5, 10, 19] {
assert_eq!(
bus.read_byte(screen_base + y * 800 + x),
0xAA,
"after mouse-at-top: sentinel must still hold at (x={}, y={})",
x,
y
);
}
}
}
#[test]
fn redraw_chrome_skips_window_blit_while_dialog_snapshot_pending() {
let (mut disp, _cpu, mut bus) = setup_with_port();
let screen_base = bus.alloc(800 * 600);
disp.screen_mode = (screen_base, 800, 800, 600, 8);
bus.write_long(0x0824, screen_base);
let offscreen_base = bus.alloc(64 * 200);
bus.write_long(PORT_PTR + 2, offscreen_base);
bus.write_word(PORT_PTR + 6, 64);
bus.write_word(PORT_PTR + 8, 0);
bus.write_word(PORT_PTR + 10, 0);
bus.write_word(PORT_PTR + 12, 200);
bus.write_word(PORT_PTR + 14, 512);
let probe = screen_base + 10 * 800 + 10;
bus.write_byte(probe, 0x42);
bus.write_byte(offscreen_base + 10 * 64 + 1, 0x00);
bus.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 20);
disp.front_window = PORT_PTR;
disp.window_bounds = (0, 0, 200, 512);
disp.window_proc_id = 1;
disp.dialog_tracking = Some(super::super::dispatch::DialogTrackingState {
game_managed: false,
rendered_pixels_final: false,
..Default::default()
});
disp.redraw_chrome(&mut bus);
assert_eq!(
bus.read_byte(probe),
0x42,
"pending ModalDialog snapshots must not be erased by blank offscreen port blits"
);
if let Some(tracking) = disp.dialog_tracking.as_mut() {
tracking.rendered_pixels_final = true;
}
disp.redraw_chrome(&mut bus);
assert_eq!(
bus.read_byte(probe),
0x00,
"once the dialog snapshot is final, normal port blitting should resume"
);
}
#[test]
fn redraw_chrome_does_not_restore_stale_dialog_pixels_while_filter_snapshot_pending() {
let (mut disp, _cpu, mut bus) = setup_with_port();
let screen_base = bus.alloc(64 * 64);
for i in 0..64u32 * 64 {
bus.write_byte(screen_base + i, 0x00);
}
disp.screen_mode = (screen_base, 64, 64, 64, 8);
bus.write_long(0x0824, screen_base);
bus.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 0);
let bounds = (10, 10, 30, 30);
let stale_pixels = disp.save_dialog_pixels(&bus, bounds);
let probe = screen_base + 18 * 64 + 18;
bus.write_byte(probe, 0x44);
disp.dialog_tracking = Some(super::super::dispatch::DialogTrackingState {
bounds,
game_managed: false,
draw_procs_done: true,
rendered_pixels: stale_pixels,
rendered_pixels_final: false,
..Default::default()
});
disp.redraw_chrome(&mut bus);
assert_eq!(
bus.read_byte(probe),
0x44,
"non-final ModalDialog snapshots must not overwrite live filter/userItem drawing"
);
if let Some(tracking) = disp.dialog_tracking.as_mut() {
tracking.rendered_pixels_final = true;
}
disp.redraw_chrome(&mut bus);
assert_eq!(
bus.read_byte(probe),
0x00,
"final ModalDialog snapshots should still be restored normally"
);
}
#[test]
fn redraw_chrome_restores_visible_dialog_snapshot_after_modaldialog_returns() {
let (mut disp, _cpu, mut bus) = setup_with_port();
let screen_base = bus.alloc(800 * 600);
disp.screen_mode = (screen_base, 800, 800, 600, 8);
bus.write_long(0x0824, screen_base);
let offscreen_base = bus.alloc(64 * 200);
bus.write_long(PORT_PTR + 2, offscreen_base);
bus.write_word(PORT_PTR + 6, 64);
bus.write_word(PORT_PTR + 8, 0);
bus.write_word(PORT_PTR + 10, 0);
bus.write_word(PORT_PTR + 12, 200);
bus.write_word(PORT_PTR + 14, 512);
let probe = screen_base + 10 * 800 + 10;
bus.write_byte(probe, 0x11);
bus.write_byte(offscreen_base + 10 * 64 + 1, 0x00);
bus.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 20);
disp.front_window = PORT_PTR;
disp.window_bounds = (0, 0, 200, 512);
disp.window_proc_id = 1;
for y in 0..20u32 {
for x in 0..20u32 {
bus.write_byte(screen_base + y * 800 + x, 0x77);
}
}
let pixels = disp.save_dialog_pixels(&bus, (5, 5, 15, 15));
for y in 0..20u32 {
for x in 0..20u32 {
bus.write_byte(screen_base + y * 800 + x, 0x11);
}
}
let dialog_ptr = 0x00D1_A106;
disp.dialog_visible_snapshots.insert(
dialog_ptr,
super::super::dispatch::PersistentDialogSnapshot {
bounds: (5, 5, 15, 15),
pixels,
},
);
disp.redraw_chrome(&mut bus);
assert_eq!(
bus.read_byte(probe),
0x77,
"visible dialog snapshot should remain composited even when the front port changes"
);
}
#[test]
fn restore_visible_dialog_snapshots_preserves_application_drawn_pixels() {
let (mut disp, _cpu, mut bus) = setup_with_port();
let screen_base = bus.alloc(64 * 64);
disp.screen_mode = (screen_base, 64, 64, 64, 8);
bus.write_long(0x0824, screen_base);
let bounds = (10, 10, 30, 30);
for y in 10..30u32 {
for x in 10..30u32 {
bus.write_byte(screen_base + y * 64 + x, 0x22);
}
}
let stale_pixels = disp.save_dialog_pixels(&bus, bounds);
let dialog_ptr = 0x00D1_A106;
disp.dialog_visible_snapshots.insert(
dialog_ptr,
super::super::dispatch::PersistentDialogSnapshot {
bounds,
pixels: stale_pixels,
},
);
disp.dialogs_drawn_by_app.insert(dialog_ptr);
let probe = screen_base + 18 * 64 + 18;
bus.write_byte(probe, 0x77);
disp.restore_visible_dialog_snapshots(&mut bus);
assert_eq!(
bus.read_byte(probe),
0x77,
"a retained snapshot must not overwrite pixels painted after DrawDialog"
);
}
#[test]
fn restore_visible_dialog_snapshots_skips_parent_when_child_dialog_is_front() {
let (mut disp, _cpu, mut bus) = setup_with_port();
let screen_base = bus.alloc(800 * 600);
disp.screen_mode = (screen_base, 800, 800, 600, 8);
bus.write_long(0x0824, screen_base);
for y in 0..40u32 {
for x in 0..40u32 {
bus.write_byte(screen_base + y * 800 + x, 0x77);
}
}
let parent_pixels = disp.save_dialog_pixels(&bus, (5, 5, 15, 15));
let child_pixels = disp.save_dialog_pixels(&bus, (20, 20, 30, 30));
for y in 0..40u32 {
for x in 0..40u32 {
bus.write_byte(screen_base + y * 800 + x, 0x11);
}
}
let parent_dialog = 0x00D1_A106;
let child_dialog = 0x00D1_A107;
disp.dialog_visible_snapshots.insert(
parent_dialog,
super::super::dispatch::PersistentDialogSnapshot {
bounds: (5, 5, 15, 15),
pixels: parent_pixels,
},
);
disp.dialog_visible_snapshots.insert(
child_dialog,
super::super::dispatch::PersistentDialogSnapshot {
bounds: (20, 20, 30, 30),
pixels: child_pixels,
},
);
disp.front_window = child_dialog;
disp.dialog_items.insert(child_dialog, Vec::new());
disp.restore_visible_dialog_snapshots(&mut bus);
assert_eq!(
bus.read_byte(screen_base + 10 * 800 + 10),
0x11,
"retained parent dialog snapshot must not overpaint the front child dialog"
);
assert_eq!(
bus.read_byte(screen_base + 25 * 800 + 25),
0x77,
"front child dialog snapshot should still be restored"
);
}
#[test]
fn refresh_visible_dialog_snapshot_for_port_captures_later_screen_updates() {
let (mut disp, _cpu, mut bus) = setup_with_port();
let screen_base = bus.alloc(800 * 600);
disp.screen_mode = (screen_base, 800, 800, 600, 8);
bus.write_long(0x0824, screen_base);
for y in 0..40u32 {
for x in 0..40u32 {
bus.write_byte(screen_base + y * 800 + x, 0x11);
}
}
let dialog_ptr = 0x00D1_A106;
let bounds = (10, 10, 20, 20);
let pixels = disp.save_dialog_pixels(&bus, bounds);
disp.dialog_visible_snapshots.insert(
dialog_ptr,
super::super::dispatch::PersistentDialogSnapshot { bounds, pixels },
);
let probe = screen_base + 12 * 800 + 12;
bus.write_byte(probe, 0x77);
disp.refresh_visible_dialog_snapshot_for_port(&bus, dialog_ptr);
bus.write_byte(probe, 0x00);
disp.restore_visible_dialog_snapshots(&mut bus);
assert_eq!(
bus.read_byte(probe),
0x77,
"refresh should retain QuickDraw updates made after ModalDialog returned"
);
}
#[test]
fn fb_fill_rect_uses_active_ctab_brightest_entry_for_white() {
let (mut disp, _cpu, mut bus) = setup_with_port();
let screen_base = bus.alloc(100 * 100);
disp.screen_mode = (screen_base, 100, 100, 100, 8);
bus.write_long(0x0824, screen_base);
let gdevice_handle = disp.ensure_main_gdevice(&mut bus);
bus.write_long(0x08A4, gdevice_handle);
bus.write_long(0x0CC8, gdevice_handle);
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);
for index in 0u32..256 {
let entry = ctab + 8 + index * 8;
bus.write_word(entry, index as u16);
bus.write_word(entry + 2, 0);
bus.write_word(entry + 4, 0);
bus.write_word(entry + 6, 0);
}
let white_entry = ctab + 8 + 8;
bus.write_word(white_entry, 1);
bus.write_word(white_entry + 2, 0xFFFF);
bus.write_word(white_entry + 4, 0xFFFF);
bus.write_word(white_entry + 6, 0xFFFF);
TrapDispatcher::fb_fill_rect(
&mut bus,
screen_base,
100,
8,
100,
100,
10,
10,
20,
20,
false,
);
assert_eq!(
bus.read_byte(screen_base + 10 * 100 + 10),
1,
"logical white must follow the active ColorTable, not hard-code CLUT index 0"
);
}
#[test]
fn fb_fill_rect_uses_active_ctab_darkest_entry_for_black() {
let (mut disp, _cpu, mut bus) = setup_with_port();
let screen_base = bus.alloc(100 * 100);
disp.screen_mode = (screen_base, 100, 100, 100, 8);
bus.write_long(0x0824, screen_base);
install_twilight_style_black_index(&mut disp, &mut bus);
TrapDispatcher::fb_fill_rect(
&mut bus,
screen_base,
100,
8,
100,
100,
10,
10,
20,
20,
true,
);
assert_eq!(
bus.read_byte(screen_base + 10 * 100 + 10),
1,
"logical black must follow the active ColorTable when index 255 is not black"
);
}
#[test]
fn redraw_chrome_blit_1bpp_to_8bpp_is_bit_extracted() {
let (mut disp, _cpu, mut bus) = setup_with_port();
let screen_base = bus.alloc(800 * 600);
disp.screen_mode = (screen_base, 800, 800, 600, 8);
bus.write_long(0x0824, screen_base);
let offscreen_base = bus.alloc(64 * 200);
bus.write_long(PORT_PTR + 2, offscreen_base); bus.write_word(PORT_PTR + 6, 64);
bus.write_word(PORT_PTR + 8, 0); bus.write_word(PORT_PTR + 10, 0); bus.write_word(PORT_PTR + 12, 200); bus.write_word(PORT_PTR + 14, 512);
bus.write_byte(offscreen_base + 30 * 64 + 12, 0b10000000);
let row30_x96 = screen_base + 30 * 800 + 96;
let row30_x97 = screen_base + 30 * 800 + 97;
bus.write_byte(row30_x96, 0xAB);
bus.write_byte(row30_x97, 0xAB);
bus.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 20);
disp.front_window = PORT_PTR;
disp.window_bounds = (0, 0, 200, 400);
disp.window_proc_id = 1; disp.fullscreen_locked = false;
disp.redraw_chrome(&mut bus);
assert_eq!(
bus.read_byte(row30_x96),
0xFF,
"1bpp src bit=1 must map to 8bpp screen idx 0xFF"
);
assert_eq!(
bus.read_byte(row30_x97),
0x00,
"1bpp src bit=0 must map to 8bpp screen idx 0x00 (blit MUST fire)"
);
}
#[test]
fn redraw_chrome_blit_1bpp_to_8bpp_uses_active_ctab_black_entry() {
let (mut disp, _cpu, mut bus) = setup_with_port();
let screen_base = bus.alloc(800 * 600);
disp.screen_mode = (screen_base, 800, 800, 600, 8);
bus.write_long(0x0824, screen_base);
install_twilight_style_black_index(&mut disp, &mut bus);
let offscreen_base = bus.alloc(64 * 200);
bus.write_long(PORT_PTR + 2, offscreen_base);
bus.write_word(PORT_PTR + 6, 64);
bus.write_word(PORT_PTR + 8, 0);
bus.write_word(PORT_PTR + 10, 0);
bus.write_word(PORT_PTR + 12, 200);
bus.write_word(PORT_PTR + 14, 512);
bus.write_byte(offscreen_base + 30 * 64 + 12, 0b10000000);
let row30_x96 = screen_base + 30 * 800 + 96;
let row30_x97 = screen_base + 30 * 800 + 97;
bus.write_byte(row30_x96, 0xAB);
bus.write_byte(row30_x97, 0xAB);
bus.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 20);
disp.front_window = PORT_PTR;
disp.window_bounds = (0, 0, 200, 400);
disp.window_proc_id = 1;
disp.fullscreen_locked = false;
disp.redraw_chrome(&mut bus);
assert_eq!(
bus.read_byte(row30_x96),
1,
"1bpp src bit=1 must use the active ColorTable's black index"
);
assert_eq!(
bus.read_byte(row30_x97),
0,
"1bpp src bit=0 must still use the active ColorTable's white index"
);
}
#[test]
fn redraw_chrome_blit_8bpp_to_8bpp_translates_port_ctab_to_screen() {
let (mut disp, _cpu, mut bus) = setup_with_port();
let screen_base = bus.alloc(64 * 64);
disp.screen_mode = (screen_base, 64, 64, 64, 8);
bus.write_long(0x0824, screen_base);
let gdevice_handle = disp.ensure_main_gdevice(&mut bus);
bus.write_long(0x08A4, gdevice_handle);
bus.write_long(0x0CC8, gdevice_handle);
let mut src_clut = TrapDispatcher::standard_mac_8bpp_clut();
let dst_clut = TrapDispatcher::standard_mac_8bpp_clut();
src_clut[42] = dst_clut[7];
let src_ctab_handle = make_ctab_handle(&mut bus, &src_clut, 0x1234_5678);
let offscreen_base = bus.alloc(64 * 64);
install_8bpp_cgrafport(&mut bus, offscreen_base, 64, 64, 64, src_ctab_handle);
bus.write_byte(offscreen_base + 5 * 64 + 10, 42);
bus.write_byte(offscreen_base + 5 * 64 + 11, 8);
bus.write_byte(screen_base + 5 * 64 + 10, 0xAA);
bus.write_byte(screen_base + 5 * 64 + 11, 0xAA);
disp.front_window = PORT_PTR;
disp.window_bounds = (0, 0, 64, 64);
disp.window_proc_id = 1;
disp.blit_window_to_screen(&mut bus);
assert_eq!(
bus.read_byte(screen_base + 5 * 64 + 10),
7,
"8bpp window blit must translate source CTab index 42 to the screen index for its RGB"
);
assert_eq!(
bus.read_byte(screen_base + 5 * 64 + 11),
8,
"same-RGB entries should remain stable through the translation table"
);
}
#[test]
fn redraw_chrome_blit_4bpp_to_4bpp_copies_packed_pixels() {
let (mut disp, _cpu, mut bus) = setup_with_port();
let screen_base = bus.alloc(4 * 2);
disp.screen_mode = (screen_base, 4, 8, 2, 4);
bus.write_long(0x0824, screen_base);
let (clut, _) = TrapDispatcher::standard_mac_indexed_clut(4).unwrap();
let ctab_handle = make_ctab_handle(&mut bus, &clut, 4);
let offscreen_base = bus.alloc(4 * 2);
let pixmap_handle = install_8bpp_cgrafport(&mut bus, offscreen_base, 4, 8, 2, ctab_handle);
let pixmap = bus.read_long(pixmap_handle);
bus.write_word(pixmap + 32, 4);
bus.write_word(pixmap + 36, 4);
bus.write_bytes(offscreen_base, &[0x12, 0x34, 0x56, 0x78]);
bus.fill_bytes(screen_base, 4 * 2, 0xAA);
disp.front_window = PORT_PTR;
disp.window_bounds = (0, 0, 2, 8);
disp.window_proc_id = 1;
disp.blit_window_to_screen(&mut bus);
assert_eq!(bus.read_bytes(screen_base, 4), vec![0x12, 0x34, 0x56, 0x78]);
assert_eq!(
bus.read_bytes(screen_base + 4, 4),
vec![0x00, 0x00, 0x00, 0x00]
);
}
#[test]
fn redraw_chrome_blit_skips_tracked_window_swapped_to_scratch_pixmap() {
let (mut disp, _cpu, mut bus) = setup_with_port();
let screen_base = bus.alloc(64 * 64);
disp.screen_mode = (screen_base, 64, 64, 64, 8);
bus.write_long(0x0824, screen_base);
let gdevice_handle = disp.ensure_main_gdevice(&mut bus);
bus.write_long(0x08A4, gdevice_handle);
bus.write_long(0x0CC8, gdevice_handle);
let clut = TrapDispatcher::standard_mac_8bpp_clut();
let ctab_handle = make_ctab_handle(&mut bus, &clut, 8);
let original_base = bus.alloc(64 * 64);
let original_pixmap =
install_8bpp_cgrafport(&mut bus, original_base, 64, 64, 64, ctab_handle);
disp.window_original_pixmaps
.insert(PORT_PTR, original_pixmap);
let scratch_base = bus.alloc(64 * 64);
install_8bpp_cgrafport(&mut bus, scratch_base, 64, 64, 64, ctab_handle);
bus.write_byte(screen_base + 8 * 64 + 8, 0xAA);
bus.write_byte(scratch_base + 8 * 64 + 8, 0x22);
disp.front_window = PORT_PTR;
disp.blit_window_to_screen(&mut bus);
assert_eq!(
bus.read_byte(screen_base + 8 * 64 + 8),
0xAA,
"SetPortPix scratch buffers must not be auto-presented as window contents"
);
bus.write_long(PORT_PTR + 2, original_pixmap);
bus.write_byte(original_base + 8 * 64 + 8, 0x11);
disp.blit_window_to_screen(&mut bus);
assert_eq!(
bus.read_byte(screen_base + 8 * 64 + 8),
0x11,
"the original tracked window backing PixMap should still be presented"
);
}
#[test]
fn framed_manual_cport_uses_explicit_off_center_guest_geometry() {
let (mut disp, _cpu, mut bus) = setup_with_port();
let screen_base = bus.alloc(800 * 600);
disp.screen_mode = (screen_base, 800, 800, 600, 8);
let manual_port = bus.alloc(200);
let manual_base = bus.alloc(512 * 322);
install_8bpp_cgrafport_at(&mut bus, manual_port, manual_base, 512, 512, 322, 0);
track_manual_cport(&mut disp, &bus, manual_port);
disp.last_screen_frame_rect = Some(crate::trap::dispatch::ScreenCopyBitsRect {
src_top: 85,
src_left: 141,
src_bottom: 413,
src_right: 659,
dst_top: 85,
dst_left: 141,
dst_bottom: 413,
dst_right: 659,
});
assert_eq!(
disp.framed_manual_cport_presentation_rect(&bus),
disp.last_screen_frame_rect,
"a guest-drawn 518x328 frame should locate its 512x322 retained image without centering it"
);
}
#[test]
fn redraw_chrome_blits_large_manual_cport_centered_when_front_window_is_screen_backed() {
let (mut disp, _cpu, mut bus) = setup_with_port();
let screen_base = bus.alloc(800 * 600);
disp.screen_mode = (screen_base, 800, 800, 600, 8);
disp.device_clut[0] = [0, 0, 0];
disp.device_clut[0xAA] = [0, 0, 0];
bus.fill_bytes(screen_base, 800 * 600, 0);
bus.write_long(0x0824, screen_base);
install_8bpp_cgrafport(&mut bus, screen_base, 800, 800, 600, 0);
bus.write_byte(PORT_PTR + WINDOW_VISIBLE_OFFSET, 0xFF);
disp.front_window = PORT_PTR;
disp.window_list = vec![PORT_PTR];
let manual_port = bus.alloc(200);
let manual_base = bus.alloc(640 * 420);
install_8bpp_cgrafport_at(&mut bus, manual_port, manual_base, 640, 640, 420, 0);
track_manual_cport(&mut disp, &bus, manual_port);
bus.fill_bytes(manual_base, 640 * 420, 0x44);
bus.write_byte(manual_base + 419 * 640 + 639, 0x55);
bus.write_byte(screen_base + 90 * 800 + 79, 0xAA);
bus.write_byte(screen_base + 90 * 800 + 80, 0xAA);
bus.write_byte(screen_base + 509 * 800 + 719, 0xAA);
assert_eq!(
disp.declared_centered_presentation_rect(&bus),
Some(crate::trap::dispatch::ScreenCopyBitsRect {
src_top: 0,
src_left: 0,
src_bottom: 420,
src_right: 640,
dst_top: 90,
dst_left: 80,
dst_bottom: 510,
dst_right: 720,
}),
"the frontend should learn the declared viewport before the first scene is presented"
);
disp.blit_large_manual_cport_to_screen(&mut bus);
assert_eq!(
disp.manual_cport_presentation_rect(&bus),
Some(crate::trap::dispatch::ScreenCopyBitsRect {
src_top: 0,
src_left: 0,
src_bottom: 420,
src_right: 640,
dst_top: 90,
dst_left: 80,
dst_bottom: 510,
dst_right: 720,
}),
"only the manual CPort actually selected for presentation should become a frontend viewport"
);
assert_eq!(
bus.read_byte(screen_base + 90 * 800 + 80),
0x44,
"640x420 manual scene should be centered at x=80,y=90 on an 800x600 screen"
);
assert_eq!(
bus.read_byte(screen_base + 509 * 800 + 719),
0x55,
"bottom-right source pixel should land at the centered destination edge"
);
assert_eq!(
bus.read_byte(screen_base + 90 * 800 + 79),
0xAA,
"manual CPort presentation must not top-left blit or overrun the centered scene"
);
}
#[test]
fn redraw_chrome_blits_large_manual_cport_when_fullscreen_port_rect_origin_is_shifted() {
let (mut disp, _cpu, mut bus) = setup_with_port();
let screen_base = bus.alloc(800 * 600);
disp.screen_mode = (screen_base, 800, 800, 600, 8);
disp.device_clut[0] = [0, 0, 0];
disp.device_clut[0xAA] = [0, 0, 0];
bus.fill_bytes(screen_base, 800 * 600, 0);
bus.write_long(0x0824, screen_base);
install_8bpp_cgrafport(&mut bus, screen_base, 800, 800, 600, 0);
bus.write_byte(PORT_PTR + WINDOW_VISIBLE_OFFSET, 0xFF);
disp.front_window = PORT_PTR;
disp.window_list = vec![PORT_PTR];
disp.window_bounds = (0, 0, 600, 800);
bus.write_word(PORT_PTR + 16, (-85i16) as u16);
bus.write_word(PORT_PTR + 18, (-144i16) as u16);
bus.write_word(PORT_PTR + 20, 515);
bus.write_word(PORT_PTR + 22, 656);
let manual_port = bus.alloc(200);
let manual_base = bus.alloc(640 * 420);
install_8bpp_cgrafport_at(&mut bus, manual_port, manual_base, 640, 640, 420, 0);
track_manual_cport(&mut disp, &bus, manual_port);
bus.fill_bytes(manual_base, 640 * 420, 0x44);
bus.write_byte(manual_base + 640 * 420 - 1, 0x55);
bus.write_byte(screen_base + 90 * 800 + 80, 0xAA);
disp.blit_large_manual_cport_to_screen(&mut bus);
assert_eq!(
bus.read_byte(screen_base + 90 * 800 + 80),
0x44,
"shifted portRect must not hide a full-screen tracked window from the manual CPort presentation bridge"
);
}
#[test]
fn redraw_chrome_blits_large_manual_cport_when_small_front_window_leaves_dark_screen() {
let (mut disp, _cpu, mut bus) = setup_with_port();
let screen_base = bus.alloc(800 * 600);
disp.screen_mode = (screen_base, 800, 800, 600, 8);
disp.device_clut[255] = [0, 0, 0];
bus.fill_bytes(screen_base, 800 * 600, 0xFF);
bus.write_long(0x0824, screen_base);
install_8bpp_cgrafport(&mut bus, screen_base, 800, 800, 600, 0);
bus.write_byte(PORT_PTR + WINDOW_VISIBLE_OFFSET, 0xFF);
disp.front_window = PORT_PTR;
disp.window_list = vec![PORT_PTR];
disp.window_bounds = (185, 226, 415, 574);
bus.write_word(PORT_PTR + 16, 0);
bus.write_word(PORT_PTR + 18, 0);
bus.write_word(PORT_PTR + 20, 230);
bus.write_word(PORT_PTR + 22, 348);
let manual_port = bus.alloc(200);
let manual_base = bus.alloc(640 * 420);
install_8bpp_cgrafport_at(&mut bus, manual_port, manual_base, 640, 640, 420, 0);
track_manual_cport(&mut disp, &bus, manual_port);
let dst = screen_base + 90 * 800 + 80;
bus.fill_bytes(manual_base, 640 * 420, 0x44);
bus.write_byte(manual_base + 640 * 420 - 1, 0x55);
bus.write_byte(dst, 0xFF);
disp.blit_large_manual_cport_to_screen(&mut bus);
assert_eq!(
bus.read_byte(dst),
0x44,
"a blank screen-backed front window can still reveal a large app-managed scene buffer"
);
}
#[test]
fn redraw_chrome_does_not_blit_large_manual_cport_over_nonblank_small_front_window() {
let (mut disp, _cpu, mut bus) = setup_with_port();
let screen_base = bus.alloc(800 * 600);
disp.screen_mode = (screen_base, 800, 800, 600, 8);
disp.device_clut[1] = [0xFFFF, 0xFFFF, 0xFFFF];
disp.device_clut[255] = [0, 0, 0];
bus.fill_bytes(screen_base, 800 * 600, 0xFF);
bus.write_byte(screen_base + 25 * 800 + 25, 1);
bus.write_long(0x0824, screen_base);
install_8bpp_cgrafport(&mut bus, screen_base, 800, 800, 600, 0);
bus.write_byte(PORT_PTR + WINDOW_VISIBLE_OFFSET, 0xFF);
disp.front_window = PORT_PTR;
disp.window_list = vec![PORT_PTR];
disp.window_bounds = (185, 226, 415, 574);
bus.write_word(PORT_PTR + 16, 0);
bus.write_word(PORT_PTR + 18, 0);
bus.write_word(PORT_PTR + 20, 230);
bus.write_word(PORT_PTR + 22, 348);
let manual_port = bus.alloc(200);
let manual_base = bus.alloc(640 * 420);
install_8bpp_cgrafport_at(&mut bus, manual_port, manual_base, 640, 640, 420, 0);
track_manual_cport(&mut disp, &bus, manual_port);
let dst = screen_base + 90 * 800 + 80;
bus.write_byte(manual_base, 0x44);
bus.write_byte(dst, 0xAA);
disp.blit_large_manual_cport_to_screen(&mut bus);
assert_eq!(
bus.read_byte(dst),
0xAA,
"manual scene fallback must not overpaint an already visible small window"
);
}
#[test]
fn redraw_chrome_does_not_latch_blank_manual_cport_over_nonblank_fullscreen() {
let (mut disp, _cpu, mut bus) = setup_with_port();
let screen_base = bus.alloc(800 * 600);
disp.screen_mode = (screen_base, 800, 800, 600, 8);
disp.device_clut[1] = [0xFFFF, 0xFFFF, 0xFFFF];
disp.device_clut[255] = [0, 0, 0];
bus.fill_bytes(screen_base, 800 * 600, 0x01);
bus.write_long(0x0824, screen_base);
install_8bpp_cgrafport(&mut bus, screen_base, 800, 800, 600, 0);
bus.write_byte(PORT_PTR + WINDOW_VISIBLE_OFFSET, 0xFF);
disp.front_window = PORT_PTR;
disp.window_list = vec![PORT_PTR];
disp.window_bounds = (0, 0, 600, 800);
let manual_port = bus.alloc(200);
let manual_base = bus.alloc(656 * 600);
bus.fill_bytes(manual_base, 656 * 600, 0xFF);
install_8bpp_cgrafport_at(&mut bus, manual_port, manual_base, 656, 656, 600, 0);
track_manual_cport(&mut disp, &bus, manual_port);
let covered_probe = screen_base + 300 * 800 + 400;
disp.blit_large_manual_cport_to_screen(&mut bus);
assert_eq!(
bus.read_byte(covered_probe),
0x01,
"a blank manual CPort must not overpaint already-rendered fullscreen content"
);
assert_eq!(
disp.manual_cport_presented_port, 0,
"blank manual CPorts must not latch presentation privilege before real screen blits"
);
disp.copybits_screen_count = 1;
bus.write_byte(covered_probe, 0x02);
bus.write_byte(manual_base, 0x44);
disp.blit_large_manual_cport_to_screen(&mut bus);
assert_eq!(
bus.read_byte(covered_probe),
0x02,
"once screen CopyBits is active, an unlatched manual CPort must stay suppressed"
);
}
#[test]
fn redraw_chrome_does_not_latch_manual_cport_over_visible_fullscreen_content() {
let (mut disp, _cpu, mut bus) = setup_with_port();
let screen_base = bus.alloc(800 * 600);
disp.screen_mode = (screen_base, 800, 800, 600, 8);
disp.device_clut[1] = [0xFFFF, 0xFFFF, 0xFFFF];
bus.fill_bytes(screen_base, 800 * 600, 0x01);
bus.write_long(0x0824, screen_base);
install_8bpp_cgrafport(&mut bus, screen_base, 800, 800, 600, 0);
bus.write_byte(PORT_PTR + WINDOW_VISIBLE_OFFSET, 0xFF);
disp.front_window = PORT_PTR;
disp.window_list = vec![PORT_PTR];
disp.window_bounds = (0, 0, 600, 800);
let manual_port = bus.alloc(200);
let manual_base = bus.alloc(656 * 600);
bus.fill_bytes(manual_base, 656 * 600, 0x44);
install_8bpp_cgrafport_at(&mut bus, manual_port, manual_base, 656, 656, 600, 0);
track_manual_cport(&mut disp, &bus, manual_port);
let covered_probe = screen_base + 300 * 800 + 400;
disp.blit_large_manual_cport_to_screen(&mut bus);
assert_eq!(
bus.read_byte(covered_probe),
0x01,
"an unlatched manual CPort must not overpaint visible fullscreen content"
);
assert_eq!(
disp.manual_cport_presented_port, 0,
"visible fullscreen content should prevent acquiring the manual CPort fallback latch"
);
}
#[test]
fn redraw_chrome_does_not_blit_manual_cport_after_screen_copybits() {
let (mut disp, _cpu, mut bus) = setup_with_port();
let screen_base = bus.alloc(800 * 600);
disp.screen_mode = (screen_base, 800, 800, 600, 8);
disp.device_clut[0] = [0, 0, 0];
bus.fill_bytes(screen_base, 800 * 600, 0);
bus.write_long(0x0824, screen_base);
install_8bpp_cgrafport(&mut bus, screen_base, 800, 800, 600, 0);
bus.write_byte(PORT_PTR + WINDOW_VISIBLE_OFFSET, 0xFF);
disp.front_window = PORT_PTR;
disp.window_list = vec![PORT_PTR];
disp.copybits_screen_count = 1;
let manual_port = bus.alloc(200);
let manual_base = bus.alloc(640 * 420);
install_8bpp_cgrafport_at(&mut bus, manual_port, manual_base, 640, 640, 420, 0);
track_manual_cport(&mut disp, &bus, manual_port);
bus.write_byte(manual_base, 0x44);
bus.write_byte(screen_base + 90 * 800 + 80, 0xAA);
disp.blit_large_manual_cport_to_screen(&mut bus);
assert_eq!(
bus.read_byte(screen_base + 90 * 800 + 80),
0xAA,
"apps already presenting through CopyBits should not be overwritten by the fallback"
);
}
#[test]
fn redraw_chrome_does_not_present_a_setportpix_scratch_buffer() {
let (mut disp, _cpu, mut bus) = setup_with_port();
let screen_base = bus.alloc(800 * 600);
disp.screen_mode = (screen_base, 800, 800, 600, 8);
disp.device_clut[0] = [0, 0, 0];
bus.fill_bytes(screen_base, 800 * 600, 0);
bus.write_long(0x0824, screen_base);
install_8bpp_cgrafport(&mut bus, screen_base, 800, 800, 600, 0);
bus.write_byte(PORT_PTR + WINDOW_VISIBLE_OFFSET, 0xFF);
disp.front_window = PORT_PTR;
disp.window_list = vec![PORT_PTR];
let manual_port = bus.alloc(200);
let original_base = bus.alloc(800 * 600);
install_8bpp_cgrafport_at(&mut bus, manual_port, original_base, 800, 800, 600, 0);
track_manual_cport(&mut disp, &bus, manual_port);
let manual_base = bus.alloc(512 * 322);
bus.fill_bytes(manual_base, 512 * 322, 0x44);
install_8bpp_cgrafport_at(&mut bus, manual_port, manual_base, 512, 512, 322, 0);
disp.last_screen_frame_rect = Some(crate::trap::dispatch::ScreenCopyBitsRect {
src_top: 85,
src_left: 141,
src_bottom: 413,
src_right: 659,
dst_top: 85,
dst_left: 141,
dst_bottom: 413,
dst_right: 659,
});
let centered_probe = screen_base + 139 * 800 + 144;
disp.blit_large_manual_cport_to_screen(&mut bus);
assert_eq!(bus.read_byte(centered_probe), 0);
assert_eq!(disp.manual_cport_presented_port, 0);
assert_eq!(disp.declared_centered_presentation_rect(&bus), None);
assert_eq!(
disp.framed_manual_cport_presentation_rect(&bus),
disp.last_screen_frame_rect,
"the explicit screen frame may locate the viewport without presenting the attached scratch PixMap"
);
}
#[test]
fn redraw_chrome_continues_latched_manual_cport_after_later_screen_copybits() {
let (mut disp, _cpu, mut bus) = setup_with_port();
let screen_base = bus.alloc(800 * 600);
disp.screen_mode = (screen_base, 800, 800, 600, 8);
disp.device_clut[0] = [0, 0, 0];
bus.fill_bytes(screen_base, 800 * 600, 0);
bus.write_long(0x0824, screen_base);
install_8bpp_cgrafport(&mut bus, screen_base, 800, 800, 600, 0);
bus.write_byte(PORT_PTR + WINDOW_VISIBLE_OFFSET, 0xFF);
disp.front_window = PORT_PTR;
disp.window_list = vec![PORT_PTR];
let manual_port = bus.alloc(200);
let manual_base = bus.alloc(640 * 420);
install_8bpp_cgrafport_at(&mut bus, manual_port, manual_base, 640, 640, 420, 0);
track_manual_cport(&mut disp, &bus, manual_port);
let dst = screen_base + 90 * 800 + 80;
bus.fill_bytes(manual_base, 640 * 420, 0x44);
bus.write_byte(manual_base + 640 * 420 - 1, 0x55);
disp.blit_large_manual_cport_to_screen(&mut bus);
assert_eq!(bus.read_byte(dst), 0x44);
disp.copybits_screen_count = 1;
bus.write_byte(dst, 0xAA);
bus.write_byte(manual_base, 0x66);
disp.blit_large_manual_cport_to_screen(&mut bus);
assert_eq!(
bus.read_byte(dst),
0x66,
"a manual CPort selected before a later CopyBits should keep presenting"
);
}
#[test]
fn redraw_chrome_releases_latched_manual_cport_after_direct_framebuffer_draw() {
let (mut disp, _cpu, mut bus) = setup_with_port();
let screen_base = bus.alloc(800 * 600);
disp.screen_mode = (screen_base, 800, 800, 600, 8);
disp.device_clut[0] = [0, 0, 0];
bus.fill_bytes(screen_base, 800 * 600, 0);
bus.write_long(0x0824, screen_base);
install_8bpp_cgrafport(&mut bus, screen_base, 800, 800, 600, 0);
bus.write_byte(PORT_PTR + WINDOW_VISIBLE_OFFSET, 0xFF);
disp.front_window = PORT_PTR;
disp.window_list = vec![PORT_PTR];
let manual_port = bus.alloc(200);
let manual_base = bus.alloc(640 * 420);
install_8bpp_cgrafport_at(&mut bus, manual_port, manual_base, 640, 640, 420, 0);
track_manual_cport(&mut disp, &bus, manual_port);
let dst = screen_base + 90 * 800 + 80;
bus.fill_bytes(manual_base, 640 * 420, 0x44);
bus.write_byte(manual_base + 640 * 420 - 1, 0x55);
disp.blit_large_manual_cport_to_screen(&mut bus);
assert_eq!(bus.read_byte(dst), 0x44);
for row in 0..420 {
bus.fill_bytes(dst + row * 800, 640, 0xAA);
}
bus.fill_bytes(manual_base, 640 * 420, 0x66);
disp.blit_large_manual_cport_to_screen(&mut bus);
assert_eq!(
bus.read_byte(dst),
0xAA,
"direct framebuffer rendering must remain authoritative over the fallback CPort"
);
assert_eq!(
disp.manual_cport_presented_port, 0,
"substantial direct framebuffer rendering should release the compatibility latch"
);
}
}