1use std::{collections::HashSet, sync::OnceLock};
4
5use crate::memory::{MacMemoryBus, MemoryBus};
6use crate::quickdraw::fonts::{heuristics::get_italic_slant, Glyph};
7use crate::quickdraw::text::{
8 get_font_metrics, get_glyph, get_glyph_italic, get_underline_thickness,
9};
10use crate::ui_theme::{
11 CaretState, ControlKind, ControlState, DialogFrameKind, DialogFrameState, MenuBarState,
12 MenuDropdownState, MenuItemState, MenuTitleState, Rgb8, ScrollbarOrientation, ScrollbarPart,
13 ScrollbarState, TextFieldState, TextSelectionState, ThemeBitmap, ThemeDrawCtx, ThemeRect,
14 UiThemeId,
15};
16
17static NO_VISRGN_AUTO_EXPAND: OnceLock<bool> = OnceLock::new();
22fn no_visrgn_auto_expand_enabled() -> bool {
23 *NO_VISRGN_AUTO_EXPAND
24 .get_or_init(|| std::env::var_os("SYSTEMLESS_NO_VISRGN_AUTO_EXPAND").is_some())
25}
26
27const TEXT_STYLE_BOLD: u8 = 0x01;
30const TEXT_STYLE_ITALIC: u8 = 0x02;
31const TEXT_STYLE_UNDERLINE: u8 = 0x04;
32const TEXT_STYLE_OUTLINE: u8 = 0x08;
33const TEXT_STYLE_SHADOW: u8 = 0x10;
34const TEXT_STYLE_CONDENSE: u8 = 0x20;
35const TEXT_STYLE_EXTEND: u8 = 0x40;
36const STANDARD_GRAY_PATTERN: [u8; 8] = [0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55];
37
38impl super::TrapDispatcher {
39 pub(super) fn get_screen_params(&self) -> (u32, u32, i16, i16, u16) {
42 let (base, rb, w, h, ps) = self.screen_mode;
43 (base, rb, w as i16, h as i16, ps)
44 }
45
46 pub(crate) fn draw_theme_push_button_chrome(
47 &self,
48 bus: &mut MacMemoryBus,
49 top: i16,
50 left: i16,
51 bottom: i16,
52 right: i16,
53 enabled: bool,
54 pressed: bool,
55 is_default: bool,
56 ) -> bool {
57 self.draw_theme_control_chrome(
58 bus,
59 ControlKind::PushButton,
60 top,
61 left,
62 bottom,
63 right,
64 enabled,
65 pressed,
66 false,
67 is_default,
68 )
69 }
70
71 pub(crate) fn draw_theme_control_chrome(
72 &self,
73 bus: &mut MacMemoryBus,
74 kind: ControlKind,
75 top: i16,
76 left: i16,
77 bottom: i16,
78 right: i16,
79 enabled: bool,
80 pressed: bool,
81 selected: bool,
82 is_default: bool,
83 ) -> bool {
84 if self.ui_theme_id() == UiThemeId::ClassicSystem7 {
85 return false;
86 }
87
88 let width = right.saturating_sub(left);
89 let height = bottom.saturating_sub(top);
90 if width <= 0 || height <= 0 {
91 return true;
92 }
93
94 let theme = self.ui_theme();
95 let pad = if kind == ControlKind::PushButton && is_default {
96 theme.dialog_metrics().default_button_outline.max(0)
97 } else {
98 0
99 };
100 let bitmap_width = width.saturating_add(pad.saturating_mul(2)) as u32;
101 let bitmap_height = height.saturating_add(pad.saturating_mul(2)) as u32;
102 let palette = theme.palette();
103 let mut bitmap = ThemeBitmap::new(bitmap_width, bitmap_height, palette.window_background);
104 let mut ctx = ThemeDrawCtx::new(&mut bitmap);
105 theme.draw_control(
106 &mut ctx,
107 ControlState {
108 kind,
109 rect: ThemeRect {
110 top: pad,
111 left: pad,
112 bottom: pad + height,
113 right: pad + width,
114 },
115 enabled,
116 pressed,
117 selected,
118 is_default,
119 },
120 );
121
122 self.blit_theme_bitmap_mono(
123 bus,
124 top.saturating_sub(pad),
125 left.saturating_sub(pad),
126 &bitmap,
127 );
128 true
129 }
130
131 pub(crate) fn draw_theme_scrollbar_chrome(
132 &self,
133 bus: &mut MacMemoryBus,
134 top: i16,
135 left: i16,
136 bottom: i16,
137 right: i16,
138 value: i16,
139 min: i16,
140 max: i16,
141 hilite: u8,
142 ) -> bool {
143 if self.ui_theme_id() == UiThemeId::ClassicSystem7 {
144 return false;
145 }
146
147 let width = right.saturating_sub(left);
148 let height = bottom.saturating_sub(top);
149 if width <= 0 || height <= 0 {
150 return true;
151 }
152
153 let theme = self.ui_theme();
154 let palette = theme.palette();
155 let mut bitmap = ThemeBitmap::new(width as u32, height as u32, palette.window_background);
156 let mut ctx = ThemeDrawCtx::new(&mut bitmap);
157 theme.draw_scrollbar(
158 &mut ctx,
159 ScrollbarState {
160 rect: ThemeRect {
161 top: 0,
162 left: 0,
163 bottom: height,
164 right: width,
165 },
166 orientation: if height > width {
167 ScrollbarOrientation::Vertical
168 } else {
169 ScrollbarOrientation::Horizontal
170 },
171 enabled: hilite != 255 && min < max,
172 value,
173 min,
174 max,
175 highlighted_part: ScrollbarPart::from_control_part_code(hilite),
176 },
177 );
178 self.blit_theme_bitmap_mono(bus, top, left, &bitmap);
179 true
180 }
181
182 pub(crate) fn draw_theme_menu_bar_chrome(&self, bus: &mut MacMemoryBus, height: i16) -> bool {
183 if self.ui_theme_id() == UiThemeId::ClassicSystem7 {
184 return false;
185 }
186
187 let (_, _, screen_width, _, _) = self.get_screen_params();
188 if screen_width <= 0 || height <= 0 {
189 return true;
190 }
191
192 let theme = self.ui_theme();
193 let palette = theme.palette();
194 let mut bitmap = ThemeBitmap::new(
195 screen_width as u32,
196 height as u32,
197 palette.window_background,
198 );
199 let mut ctx = ThemeDrawCtx::new(&mut bitmap);
200 theme.draw_menu_bar(
201 &mut ctx,
202 MenuBarState {
203 rect: ThemeRect {
204 top: 0,
205 left: 0,
206 bottom: height,
207 right: screen_width,
208 },
209 },
210 );
211 self.blit_theme_bitmap_mono(bus, 0, 0, &bitmap);
212 true
213 }
214
215 pub(crate) fn draw_theme_menu_title_chrome(
216 &self,
217 bus: &mut MacMemoryBus,
218 top: i16,
219 left: i16,
220 bottom: i16,
221 right: i16,
222 enabled: bool,
223 highlighted: bool,
224 ) -> bool {
225 if self.ui_theme_id() == UiThemeId::ClassicSystem7 {
226 return false;
227 }
228
229 let width = right.saturating_sub(left);
230 let height = bottom.saturating_sub(top);
231 if width <= 0 || height <= 0 {
232 return true;
233 }
234
235 let theme = self.ui_theme();
236 let palette = theme.palette();
237 let mut bitmap = ThemeBitmap::new(width as u32, height as u32, palette.window_background);
238 let mut ctx = ThemeDrawCtx::new(&mut bitmap);
239 theme.draw_menu_title(
240 &mut ctx,
241 MenuTitleState {
242 rect: ThemeRect {
243 top: 0,
244 left: 0,
245 bottom: height,
246 right: width,
247 },
248 enabled,
249 highlighted,
250 },
251 );
252 self.blit_theme_bitmap_mono(bus, top, left, &bitmap);
253 true
254 }
255
256 pub(crate) fn draw_theme_menu_dropdown_chrome(
257 &self,
258 bus: &mut MacMemoryBus,
259 top: i16,
260 left: i16,
261 bottom: i16,
262 right: i16,
263 ) -> bool {
264 if self.ui_theme_id() == UiThemeId::ClassicSystem7 {
265 return false;
266 }
267
268 let width = right.saturating_sub(left);
269 let height = bottom.saturating_sub(top);
270 if width <= 0 || height <= 0 {
271 return true;
272 }
273
274 let theme = self.ui_theme();
275 let palette = theme.palette();
276 let mut bitmap = ThemeBitmap::new(width as u32, height as u32, palette.window_background);
277 let mut ctx = ThemeDrawCtx::new(&mut bitmap);
278 theme.draw_menu_dropdown(
279 &mut ctx,
280 MenuDropdownState {
281 rect: ThemeRect {
282 top: 0,
283 left: 0,
284 bottom: height,
285 right: width,
286 },
287 },
288 );
289 self.blit_theme_bitmap_mono(bus, top, left, &bitmap);
290 true
291 }
292
293 pub(crate) fn draw_theme_menu_item_chrome(
294 &self,
295 bus: &mut MacMemoryBus,
296 top: i16,
297 left: i16,
298 bottom: i16,
299 right: i16,
300 enabled: bool,
301 highlighted: bool,
302 separator: bool,
303 has_icon: bool,
304 checked: bool,
305 has_command_key: bool,
306 ) -> bool {
307 if self.ui_theme_id() == UiThemeId::ClassicSystem7 {
308 return false;
309 }
310
311 let width = right.saturating_sub(left);
312 let height = bottom.saturating_sub(top);
313 if width <= 0 || height <= 0 {
314 return true;
315 }
316
317 let theme = self.ui_theme();
318 let palette = theme.palette();
319 let mut bitmap = ThemeBitmap::new(width as u32, height as u32, palette.frame_light);
320 let mut ctx = ThemeDrawCtx::new(&mut bitmap);
321 theme.draw_menu_item(
322 &mut ctx,
323 MenuItemState {
324 rect: ThemeRect {
325 top: 0,
326 left: 0,
327 bottom: height,
328 right: width,
329 },
330 enabled,
331 highlighted,
332 separator,
333 has_icon,
334 checked,
335 has_command_key,
336 },
337 );
338 self.blit_theme_bitmap_mono(bus, top, left, &bitmap);
339 true
340 }
341
342 pub(crate) fn draw_theme_text_selection(
343 &self,
344 bus: &mut MacMemoryBus,
345 top: i16,
346 left: i16,
347 bottom: i16,
348 right: i16,
349 active: bool,
350 ) -> bool {
351 if self.ui_theme_id() == UiThemeId::ClassicSystem7 {
352 return false;
353 }
354
355 let width = right.saturating_sub(left);
356 let height = bottom.saturating_sub(top);
357 if width <= 0 || height <= 0 {
358 return true;
359 }
360
361 let theme = self.ui_theme();
362 let palette = theme.palette();
363 let mut bitmap = ThemeBitmap::new(width as u32, height as u32, palette.window_background);
364 let mut ctx = ThemeDrawCtx::new(&mut bitmap);
365 theme.draw_text_selection(
366 &mut ctx,
367 TextSelectionState {
368 rect: ThemeRect {
369 top: 0,
370 left: 0,
371 bottom: height,
372 right: width,
373 },
374 active,
375 },
376 );
377 self.blit_theme_bitmap_mono_masked(bus, top, left, &bitmap);
378 true
379 }
380
381 pub(crate) fn draw_theme_text_field(
382 &self,
383 bus: &mut MacMemoryBus,
384 top: i16,
385 left: i16,
386 bottom: i16,
387 right: i16,
388 enabled: bool,
389 focused: bool,
390 ) -> bool {
391 if self.ui_theme_id() == UiThemeId::ClassicSystem7 {
392 return false;
393 }
394
395 let width = right.saturating_sub(left);
396 let height = bottom.saturating_sub(top);
397 if width <= 0 || height <= 0 {
398 return true;
399 }
400
401 let theme = self.ui_theme();
402 let palette = theme.palette();
403 let mut bitmap = ThemeBitmap::new(width as u32, height as u32, palette.window_background);
404 let mut ctx = ThemeDrawCtx::new(&mut bitmap);
405 theme.draw_text_field(
406 &mut ctx,
407 TextFieldState {
408 rect: ThemeRect {
409 top: 0,
410 left: 0,
411 bottom: height,
412 right: width,
413 },
414 enabled,
415 focused,
416 },
417 );
418 self.blit_theme_bitmap_mono(bus, top, left, &bitmap);
419 true
420 }
421
422 pub(crate) fn draw_theme_caret(
423 &self,
424 bus: &mut MacMemoryBus,
425 top: i16,
426 left: i16,
427 bottom: i16,
428 right: i16,
429 ) -> bool {
430 if self.ui_theme_id() == UiThemeId::ClassicSystem7 {
431 return false;
432 }
433
434 let width = right.saturating_sub(left);
435 let height = bottom.saturating_sub(top);
436 if width <= 0 || height <= 0 {
437 return true;
438 }
439
440 let theme = self.ui_theme();
441 let pad = 1i16;
442 let palette = theme.palette();
443 let mut bitmap = ThemeBitmap::new(
444 width.saturating_add(pad.saturating_mul(2)) as u32,
445 height as u32,
446 palette.window_background,
447 );
448 let mut ctx = ThemeDrawCtx::new(&mut bitmap);
449 theme.draw_caret(
450 &mut ctx,
451 CaretState {
452 rect: ThemeRect {
453 top: 0,
454 left: pad,
455 bottom: height,
456 right: pad + width,
457 },
458 active: true,
459 },
460 );
461 self.blit_theme_bitmap_mono_masked(bus, top, left.saturating_sub(pad), &bitmap);
462 true
463 }
464
465 pub(crate) fn draw_theme_dialog_frame(
466 &self,
467 bus: &mut MacMemoryBus,
468 content: (i16, i16, i16, i16),
469 frame: (i16, i16, i16, i16),
470 proc_id: i16,
471 active: bool,
472 fill_content: bool,
473 ) -> bool {
474 if self.ui_theme_id() == UiThemeId::ClassicSystem7 {
475 return false;
476 }
477
478 let (frame_top, frame_left, frame_bottom, frame_right) = frame;
479 let width = frame_right.saturating_sub(frame_left);
480 let height = frame_bottom.saturating_sub(frame_top);
481 if width <= 0 || height <= 0 {
482 return true;
483 }
484
485 if !fill_content {
486 self.erase_structure_frame_around_content(bus, frame, content);
487 }
488
489 let (content_top, content_left, content_bottom, content_right) = content;
490 let theme = self.ui_theme();
491 let palette = theme.palette();
492 let mut bitmap = ThemeBitmap::new(width as u32, height as u32, palette.window_background);
493 let mut ctx = ThemeDrawCtx::new(&mut bitmap);
494 theme.draw_dialog_frame(
495 &mut ctx,
496 DialogFrameState {
497 frame_rect: ThemeRect {
498 top: 0,
499 left: 0,
500 bottom: height,
501 right: width,
502 },
503 content_rect: ThemeRect {
504 top: content_top.saturating_sub(frame_top),
505 left: content_left.saturating_sub(frame_left),
506 bottom: content_bottom.saturating_sub(frame_top),
507 right: content_right.saturating_sub(frame_left),
508 },
509 kind: DialogFrameKind::from_window_proc_id(proc_id),
510 active,
511 fill_content,
512 },
513 );
514 if fill_content {
515 self.blit_theme_bitmap_mono(bus, frame_top, frame_left, &bitmap);
516 } else {
517 self.blit_theme_bitmap_mono_masked(bus, frame_top, frame_left, &bitmap);
518 }
519 true
520 }
521
522 fn blit_theme_bitmap_mono(
523 &self,
524 bus: &mut MacMemoryBus,
525 top: i16,
526 left: i16,
527 bitmap: &ThemeBitmap,
528 ) {
529 let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
530 self.get_screen_params();
531 let rgba = bitmap.rgba();
532 for y in 0..bitmap.height() {
533 for x in 0..bitmap.width() {
534 let offset = ((y * bitmap.width() + x) * 4) as usize;
535 let color = Rgb8 {
536 r: rgba[offset],
537 g: rgba[offset + 1],
538 b: rgba[offset + 2],
539 };
540 Self::fb_set_pixel(
541 bus,
542 screen_base,
543 row_bytes,
544 pixel_size,
545 screen_width,
546 screen_height,
547 left.saturating_add(x as i16),
548 top.saturating_add(y as i16),
549 Self::theme_color_is_mono_black(color),
550 );
551 }
552 }
553 }
554
555 fn blit_theme_bitmap_mono_masked(
556 &self,
557 bus: &mut MacMemoryBus,
558 top: i16,
559 left: i16,
560 bitmap: &ThemeBitmap,
561 ) {
562 let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
563 self.get_screen_params();
564 let rgba = bitmap.rgba();
565 for y in 0..bitmap.height() {
566 for x in 0..bitmap.width() {
567 let offset = ((y * bitmap.width() + x) * 4) as usize;
568 let color = Rgb8 {
569 r: rgba[offset],
570 g: rgba[offset + 1],
571 b: rgba[offset + 2],
572 };
573 if !Self::theme_color_is_mono_black(color) {
574 continue;
575 }
576 Self::fb_set_pixel(
577 bus,
578 screen_base,
579 row_bytes,
580 pixel_size,
581 screen_width,
582 screen_height,
583 left.saturating_add(x as i16),
584 top.saturating_add(y as i16),
585 true,
586 );
587 }
588 }
589 }
590
591 fn theme_color_is_mono_black(color: Rgb8) -> bool {
592 u16::from(color.r) + u16::from(color.g) + u16::from(color.b) < 128 * 3
593 }
594
595 fn active_gdevice_ctab(bus: &MacMemoryBus) -> Option<u32> {
596 let gdevice_handle = {
597 let current = bus.read_long(0x0CC8); if current != 0 {
599 current
600 } else {
601 bus.read_long(0x08A4) }
603 };
604 if gdevice_handle == 0 {
605 return None;
606 }
607 let gdevice = bus.read_long(gdevice_handle);
608 if gdevice == 0 {
609 return None;
610 }
611 let pixmap_handle = bus.read_long(gdevice + 22);
612 if pixmap_handle == 0 {
613 return None;
614 }
615 let pixmap = bus.read_long(pixmap_handle);
616 if pixmap == 0 {
617 return None;
618 }
619 let ctab_handle = bus.read_long(pixmap + 42);
620 if ctab_handle == 0 {
621 return None;
622 }
623 let ctab = bus.read_long(ctab_handle);
624 if ctab == 0 {
625 return None;
626 }
627 Some(ctab)
628 }
629
630 fn ctab_value_luma(bus: &MacMemoryBus, ctab: u32, wanted_value: u8) -> Option<u32> {
631 let count = u32::from(bus.read_word(ctab + 6)).min(255) + 1;
632
633 let ordinal = u32::from(wanted_value);
634 if ordinal < count {
635 let entry = ctab + 8 + ordinal * 8;
636 if bus.read_word(entry) == u16::from(wanted_value) {
637 return Some(
638 u32::from(bus.read_word(entry + 2))
639 + u32::from(bus.read_word(entry + 4))
640 + u32::from(bus.read_word(entry + 6)),
641 );
642 }
643 }
644
645 for ordinal in 0..count {
646 let entry = ctab + 8 + ordinal * 8;
647 if bus.read_word(entry) != u16::from(wanted_value) {
648 continue;
649 }
650 return Some(
651 u32::from(bus.read_word(entry + 2))
652 + u32::from(bus.read_word(entry + 4))
653 + u32::from(bus.read_word(entry + 6)),
654 );
655 }
656 None
657 }
658
659 fn best_luma_pixel_index(bus: &MacMemoryBus, ctab: u32, brightest: bool) -> Option<u8> {
660 let count = u32::from(bus.read_word(ctab + 6)).min(255) + 1;
661 let mut best_index = 0u8;
662 let mut best_luma = 0u32;
663 let mut found = false;
664 for ordinal in 0..count {
665 let entry = ctab + 8 + ordinal * 8;
666 let value = bus.read_word(entry);
667 if value > 255 {
668 continue;
669 }
670 let luma = u32::from(bus.read_word(entry + 2))
671 + u32::from(bus.read_word(entry + 4))
672 + u32::from(bus.read_word(entry + 6));
673 let index = value as u8;
674 let better = if brightest {
675 luma > best_luma || (luma == best_luma && index < best_index)
676 } else {
677 luma < best_luma || (luma == best_luma && index < best_index)
678 };
679 if !found || better {
680 best_index = index;
681 best_luma = luma;
682 found = true;
683 }
684 }
685
686 found.then_some(best_index)
687 }
688
689 pub(crate) fn fb_pixel_index_for_rgb(bus: &MacMemoryBus, rgb: [u16; 3]) -> Option<u8> {
690 let ctab = Self::active_gdevice_ctab(bus)?;
691 let count = u32::from(bus.read_word(ctab + 6)).min(255) + 1;
692
693 if rgb == [0, 0, 0] && Self::ctab_value_luma(bus, ctab, 255) == Some(0) {
699 return Some(255);
700 }
701 if rgb == [0xFFFF, 0xFFFF, 0xFFFF]
702 && Self::ctab_value_luma(bus, ctab, 0) == Some(0xFFFF * 3)
703 {
704 return Some(0);
705 }
706
707 let mut best_index = None;
708 let mut best_distance = u64::MAX;
709 for ordinal in 0..count {
710 let entry = ctab + 8 + ordinal * 8;
711 let value = bus.read_word(entry);
712 if value > 255 {
713 continue;
714 }
715 let entry_rgb = [
716 bus.read_word(entry + 2),
717 bus.read_word(entry + 4),
718 bus.read_word(entry + 6),
719 ];
720 let index = value as u8;
721 if entry_rgb == rgb {
722 return Some(index);
723 }
724
725 let dr = i64::from(entry_rgb[0]) - i64::from(rgb[0]);
726 let dg = i64::from(entry_rgb[1]) - i64::from(rgb[1]);
727 let db = i64::from(entry_rgb[2]) - i64::from(rgb[2]);
728 let distance = (dr * dr + dg * dg + db * db) as u64;
729 if distance < best_distance
730 || (distance == best_distance && best_index.map_or(true, |best| index < best))
731 {
732 best_distance = distance;
733 best_index = Some(index);
734 }
735 }
736 best_index
737 }
738
739 fn logical_white_pixel_index(bus: &MacMemoryBus) -> u8 {
740 let Some(ctab) = Self::active_gdevice_ctab(bus) else {
741 return 0;
742 };
743 if Self::ctab_value_luma(bus, ctab, 0) == Some(0xFFFF * 3) {
744 return 0;
745 }
746 Self::best_luma_pixel_index(bus, ctab, true).unwrap_or(0)
747 }
748
749 fn logical_black_pixel_index(bus: &MacMemoryBus) -> u8 {
750 let Some(ctab) = Self::active_gdevice_ctab(bus) else {
751 return 255;
752 };
753 if Self::ctab_value_luma(bus, ctab, 1) == Some(0) {
754 return 1;
755 }
756 if Self::ctab_value_luma(bus, ctab, 255) == Some(0) {
757 return 255;
758 }
759 Self::best_luma_pixel_index(bus, ctab, false).unwrap_or(255)
760 }
761
762 fn logical_mono_pixel_indexes(bus: &MacMemoryBus) -> (u8, u8) {
763 (
764 Self::logical_white_pixel_index(bus),
765 Self::logical_black_pixel_index(bus),
766 )
767 }
768
769 pub(crate) fn fb_set_pixel(
772 bus: &mut MacMemoryBus,
773 screen_base: u32,
774 row_bytes: u32,
775 pixel_size: u16,
776 screen_width: i16,
777 screen_height: i16,
778 x: i16,
779 y: i16,
780 black: bool,
781 ) {
782 if x < 0 || y < 0 || x >= screen_width || y >= screen_height {
783 return;
784 }
785 if pixel_size == 8 {
786 let addr = screen_base + (y as u32) * row_bytes + (x as u32);
787 bus.write_byte(
788 addr,
789 if black {
790 Self::logical_black_pixel_index(bus)
791 } else {
792 Self::logical_white_pixel_index(bus)
793 },
794 );
795 } else {
796 let byte_offset = (y as u32) * row_bytes + (x as u32 / 8);
797 let bit = 7 - (x as u32 % 8);
798 let addr = screen_base + byte_offset;
799 let b = bus.read_byte(addr);
800 if black {
801 bus.write_byte(addr, b | (1 << bit));
802 } else {
803 bus.write_byte(addr, b & !(1 << bit));
804 }
805 }
806 }
807
808 pub(crate) fn fb_set_pixel_index(
809 bus: &mut MacMemoryBus,
810 screen_base: u32,
811 row_bytes: u32,
812 pixel_size: u16,
813 screen_width: i16,
814 screen_height: i16,
815 x: i16,
816 y: i16,
817 pixel_index: u8,
818 ) {
819 if pixel_size != 8 {
820 Self::fb_set_pixel(
821 bus,
822 screen_base,
823 row_bytes,
824 pixel_size,
825 screen_width,
826 screen_height,
827 x,
828 y,
829 pixel_index != 0,
830 );
831 return;
832 }
833 if x < 0 || y < 0 || x >= screen_width || y >= screen_height {
834 return;
835 }
836 let addr = screen_base + (y as u32) * row_bytes + (x as u32);
837 bus.write_byte(addr, pixel_index);
838 }
839
840 pub(crate) fn fb_fill_rect_index(
841 bus: &mut MacMemoryBus,
842 screen_base: u32,
843 row_bytes: u32,
844 pixel_size: u16,
845 screen_width: i16,
846 screen_height: i16,
847 top: i16,
848 left: i16,
849 bottom: i16,
850 right: i16,
851 pixel_index: u8,
852 ) {
853 if pixel_size != 8 {
854 Self::fb_fill_rect(
855 bus,
856 screen_base,
857 row_bytes,
858 pixel_size,
859 screen_width,
860 screen_height,
861 top,
862 left,
863 bottom,
864 right,
865 pixel_index != 0,
866 );
867 return;
868 }
869 let top = top.max(0).min(screen_height) as u32;
870 let left = left.max(0).min(screen_width) as u32;
871 let bottom = bottom.max(0).min(screen_height) as u32;
872 let right = right.max(0).min(screen_width) as u32;
873 if top >= bottom || left >= right {
874 return;
875 }
876 let width = right - left;
877 for y in top..bottom {
878 let row_addr = screen_base + y * row_bytes;
879 bus.fill_bytes(row_addr + left, width, pixel_index);
880 }
881 }
882
883 pub(crate) fn fb_fill_rect(
885 bus: &mut MacMemoryBus,
886 screen_base: u32,
887 row_bytes: u32,
888 pixel_size: u16,
889 screen_width: i16,
890 screen_height: i16,
891 top: i16,
892 left: i16,
893 bottom: i16,
894 right: i16,
895 black: bool,
896 ) {
897 if pixel_size == 8 {
898 let top = top.max(0).min(screen_height) as u32;
899 let left = left.max(0).min(screen_width) as u32;
900 let bottom = bottom.max(0).min(screen_height) as u32;
901 let right = right.max(0).min(screen_width) as u32;
902 if top >= bottom || left >= right {
903 return;
904 }
905 let fill = if black {
906 Self::logical_black_pixel_index(bus)
907 } else {
908 Self::logical_white_pixel_index(bus)
909 };
910 let width = right - left;
911 for y in top..bottom {
912 let row_addr = screen_base + y * row_bytes;
913 bus.fill_bytes(row_addr + left, width, fill);
914 }
915 return;
916 }
917 for y in top..bottom {
918 for x in left..right {
919 Self::fb_set_pixel(
920 bus,
921 screen_base,
922 row_bytes,
923 pixel_size,
924 screen_width,
925 screen_height,
926 x,
927 y,
928 black,
929 );
930 }
931 }
932 }
933
934 pub(crate) fn fb_fill_pattern_rect(
935 bus: &mut MacMemoryBus,
936 screen_base: u32,
937 row_bytes: u32,
938 pixel_size: u16,
939 screen_width: i16,
940 screen_height: i16,
941 top: i16,
942 left: i16,
943 bottom: i16,
944 right: i16,
945 pattern: [u8; 8],
946 ) {
947 let top = top.max(0).min(screen_height);
948 let left = left.max(0).min(screen_width);
949 let bottom = bottom.max(0).min(screen_height);
950 let right = right.max(0).min(screen_width);
951 if top >= bottom || left >= right {
952 return;
953 }
954
955 for y in top..bottom {
956 let row = pattern[y.rem_euclid(8) as usize];
957 for x in left..right {
958 let bit = (row >> (7 - x.rem_euclid(8))) & 1;
959 Self::fb_set_pixel(
960 bus,
961 screen_base,
962 row_bytes,
963 pixel_size,
964 screen_width,
965 screen_height,
966 x,
967 y,
968 bit != 0,
969 );
970 }
971 }
972 }
973
974 fn fb_pixel_is_logical_black(
975 bus: &MacMemoryBus,
976 screen_base: u32,
977 row_bytes: u32,
978 pixel_size: u16,
979 screen_width: i16,
980 screen_height: i16,
981 x: i16,
982 y: i16,
983 ) -> bool {
984 if x < 0 || y < 0 || x >= screen_width || y >= screen_height {
985 return false;
986 }
987 if pixel_size == 8 {
988 let addr = screen_base + (y as u32) * row_bytes + (x as u32);
989 bus.read_byte(addr) == Self::logical_black_pixel_index(bus)
990 } else {
991 let byte_offset = (y as u32) * row_bytes + (x as u32 / 8);
992 let bit = 7 - (x as u32 % 8);
993 let addr = screen_base + byte_offset;
994 (bus.read_byte(addr) & (1 << bit)) != 0
995 }
996 }
997
998 fn visible_window_count(&self, bus: &MacMemoryBus) -> usize {
999 let mut count = 0usize;
1000 let mut saw = HashSet::new();
1001 for &window in &self.window_list {
1002 if window != 0 && saw.insert(window) && bus.read_byte(window + 110u32) != 0 {
1003 count += 1;
1004 }
1005 }
1006 if self.front_window != 0
1007 && saw.insert(self.front_window)
1008 && bus.read_byte(self.front_window + 110u32) != 0
1009 {
1010 count += 1;
1011 }
1012 count
1013 }
1014
1015 fn front_window_is_dialog_like(&self) -> bool {
1016 self.front_window != 0
1017 && (self.dialog_items.contains_key(&self.front_window)
1018 || matches!(
1019 self.window_proc_ids
1020 .get(&self.front_window)
1021 .copied()
1022 .unwrap_or(self.window_proc_id),
1023 1 | 2 | 3 | 5
1024 ))
1025 }
1026
1027 fn exposed_background_samples_are_black(
1028 &self,
1029 bus: &MacMemoryBus,
1030 excluded_rect: (i16, i16, i16, i16),
1031 ) -> bool {
1032 let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
1033 self.get_screen_params();
1034 let top = excluded_rect.0.max(0).min(screen_height);
1035 let left = excluded_rect.1.max(0).min(screen_width);
1036 let bottom = excluded_rect.2.max(0).min(screen_height);
1037 let right = excluded_rect.3.max(0).min(screen_width);
1038 let rects = [
1039 (0, 0, top, screen_width),
1040 (bottom, 0, screen_height, screen_width),
1041 (top, 0, bottom, left),
1042 (top, right, bottom, screen_width),
1043 ];
1044
1045 let mut sampled = false;
1046 for (rt, rl, rb, rr) in rects {
1047 if rt >= rb || rl >= rr {
1048 continue;
1049 }
1050 let mut y = rt;
1051 while y < rb {
1052 let mut x = rl;
1053 while x < rr {
1054 sampled = true;
1055 let sample_points = [
1056 (x, y),
1057 (x.saturating_add(1).min(rr - 1), y),
1058 (x, y.saturating_add(1).min(rb - 1)),
1059 ];
1060 for (sample_x, sample_y) in sample_points {
1061 if !Self::fb_pixel_is_logical_black(
1062 bus,
1063 screen_base,
1064 row_bytes,
1065 pixel_size,
1066 screen_width,
1067 screen_height,
1068 sample_x,
1069 sample_y,
1070 ) {
1071 return false;
1072 }
1073 }
1074 x = x.saturating_add(16);
1075 }
1076 y = y.saturating_add(16);
1077 }
1078 }
1079 sampled
1080 }
1081
1082 fn fill_desktop_pattern_outside_rect(
1083 &self,
1084 bus: &mut MacMemoryBus,
1085 excluded_rect: (i16, i16, i16, i16),
1086 ) {
1087 let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
1088 self.get_screen_params();
1089 let top = excluded_rect.0.max(0).min(screen_height);
1090 let left = excluded_rect.1.max(0).min(screen_width);
1091 let bottom = excluded_rect.2.max(0).min(screen_height);
1092 let right = excluded_rect.3.max(0).min(screen_width);
1093 let rects = [
1094 (0, 0, top, screen_width),
1095 (bottom, 0, screen_height, screen_width),
1096 (top, 0, bottom, left),
1097 (top, right, bottom, screen_width),
1098 ];
1099
1100 for (rt, rl, rb, rr) in rects {
1101 Self::fb_fill_pattern_rect(
1102 bus,
1103 screen_base,
1104 row_bytes,
1105 pixel_size,
1106 screen_width,
1107 screen_height,
1108 rt,
1109 rl,
1110 rb,
1111 rr,
1112 STANDARD_GRAY_PATTERN,
1113 );
1114 }
1115 }
1116
1117 fn restore_kiosk_dialog_desktop_background(&self, bus: &mut MacMemoryBus) {
1118 if !self.menu_bar_hidden
1119 || self.fullscreen_locked
1120 || !self.front_window_is_dialog_like()
1121 || self.visible_window_count(bus) != 1
1122 {
1123 return;
1124 }
1125
1126 let bounds = self
1127 .window_structure_rect(bus, self.front_window)
1128 .unwrap_or(self.window_bounds);
1129 if self.exposed_background_samples_are_black(bus, bounds) {
1130 self.fill_desktop_pattern_outside_rect(bus, bounds);
1137 }
1138 }
1139
1140 pub(crate) fn fb_hline(
1142 bus: &mut MacMemoryBus,
1143 screen_base: u32,
1144 row_bytes: u32,
1145 pixel_size: u16,
1146 screen_width: i16,
1147 screen_height: i16,
1148 y: i16,
1149 x1: i16,
1150 x2: i16,
1151 black: bool,
1152 ) {
1153 if pixel_size == 8 {
1154 if y < 0 || y >= screen_height {
1155 return;
1156 }
1157 let left = x1.max(0).min(screen_width) as u32;
1158 let right = x2.max(0).min(screen_width) as u32;
1159 if left >= right {
1160 return;
1161 }
1162 let fill = if black {
1163 Self::logical_black_pixel_index(bus)
1164 } else {
1165 Self::logical_white_pixel_index(bus)
1166 };
1167 let row_addr = screen_base + (y as u32) * row_bytes;
1168 for x in left..right {
1169 bus.write_byte(row_addr + x, fill);
1170 }
1171 return;
1172 }
1173 for x in x1..x2 {
1174 Self::fb_set_pixel(
1175 bus,
1176 screen_base,
1177 row_bytes,
1178 pixel_size,
1179 screen_width,
1180 screen_height,
1181 x,
1182 y,
1183 black,
1184 );
1185 }
1186 }
1187
1188 pub(crate) fn fb_draw_char(
1190 bus: &mut MacMemoryBus,
1191 screen_base: u32,
1192 row_bytes: u32,
1193 pixel_size: u16,
1194 screen_width: i16,
1195 screen_height: i16,
1196 x: i16,
1197 y: i16,
1198 ch: char,
1199 font_id: i16,
1200 font_size: i16,
1201 ) -> i16 {
1202 if let Some((glyph, data)) = get_glyph(font_id, font_size, ch) {
1203 Self::fb_draw_glyph_bitmap(
1204 bus,
1205 screen_base,
1206 row_bytes,
1207 pixel_size,
1208 screen_width,
1209 screen_height,
1210 x,
1211 y,
1212 glyph,
1213 data,
1214 );
1215 glyph.advance as i16
1216 } else {
1217 6 }
1219 }
1220
1221 fn fb_draw_glyph_bitmap(
1222 bus: &mut MacMemoryBus,
1223 screen_base: u32,
1224 row_bytes: u32,
1225 pixel_size: u16,
1226 screen_width: i16,
1227 screen_height: i16,
1228 x: i16,
1229 y: i16,
1230 glyph: &Glyph,
1231 data: &[u8],
1232 ) {
1233 Self::fb_draw_glyph_bitmap_with_slant(
1234 bus,
1235 screen_base,
1236 row_bytes,
1237 pixel_size,
1238 screen_width,
1239 screen_height,
1240 x,
1241 y,
1242 glyph,
1243 data,
1244 None,
1245 0,
1246 None,
1247 true,
1248 );
1249 }
1250
1251 fn fb_draw_glyph_bitmap_with_slant(
1252 bus: &mut MacMemoryBus,
1253 screen_base: u32,
1254 row_bytes: u32,
1255 pixel_size: u16,
1256 screen_width: i16,
1257 screen_height: i16,
1258 x: i16,
1259 y: i16,
1260 glyph: &Glyph,
1261 data: &[u8],
1262 synthetic_italic: Option<(i16, i16)>,
1263 style: u8,
1264 pixel_index_override: Option<u8>,
1265 black: bool,
1266 ) {
1267 let gx = x + glyph.origin_x as i16;
1268 let gy = y + glyph.origin_y as i16;
1269 let gw = glyph.width as usize;
1270 let gh = glyph.height as usize;
1271 let metrics = synthetic_italic
1272 .map(|(font_id, font_size)| (font_id, font_size, get_font_metrics(font_id, font_size)));
1273 let text_index = if pixel_size == 8 {
1274 Some(pixel_index_override.unwrap_or_else(|| {
1275 if black {
1276 Self::logical_black_pixel_index(bus)
1277 } else {
1278 Self::logical_white_pixel_index(bus)
1279 }
1280 }))
1281 } else {
1282 None
1283 };
1284
1285 for row in 0..gh {
1289 for col in 0..gw {
1290 let byte_idx = glyph.data_offset + row * gw + col;
1291 if byte_idx < data.len() && data[byte_idx] >= 128 {
1292 let py = gy + row as i16;
1293 let slant = metrics
1294 .as_ref()
1295 .map(|(font_id, font_size, metrics)| {
1296 get_italic_slant(*font_id, *font_size, metrics, y, py)
1297 })
1298 .unwrap_or(0);
1299 let (dst_start, dst_end) = if (style & TEXT_STYLE_EXTEND) != 0 {
1300 let start = (col as i16 * 4) / 3;
1301 let end = (((col as i16 + 1) * 4) / 3).max(start + 1);
1302 (start, end)
1303 } else if (style & TEXT_STYLE_CONDENSE) != 0 {
1304 let start = (col as i16 * 3) / 4;
1305 (start, start + 1)
1306 } else {
1307 let start = col as i16;
1308 (start, start + 1)
1309 };
1310 for dst_col in dst_start..dst_end {
1311 let px = gx + dst_col + slant;
1312 if let Some(text_index) = text_index {
1313 if px >= 0 && py >= 0 && px < screen_width && py < screen_height {
1314 let addr = screen_base + (py as u32) * row_bytes + (px as u32);
1315 bus.write_byte(addr, text_index);
1316 }
1317 } else {
1318 Self::fb_set_pixel(
1319 bus,
1320 screen_base,
1321 row_bytes,
1322 pixel_size,
1323 screen_width,
1324 screen_height,
1325 px,
1326 py,
1327 black,
1328 );
1329 }
1330 }
1331 }
1332 }
1333 }
1334 }
1335
1336 fn fb_set_styled_text_pixel(
1337 bus: &mut MacMemoryBus,
1338 screen_base: u32,
1339 row_bytes: u32,
1340 pixel_size: u16,
1341 screen_width: i16,
1342 screen_height: i16,
1343 x: i16,
1344 y: i16,
1345 pixel_index_override: Option<u8>,
1346 black: bool,
1347 ) {
1348 if let Some(pixel_index) = pixel_index_override {
1349 Self::fb_set_pixel_index(
1350 bus,
1351 screen_base,
1352 row_bytes,
1353 pixel_size,
1354 screen_width,
1355 screen_height,
1356 x,
1357 y,
1358 pixel_index,
1359 );
1360 } else {
1361 Self::fb_set_pixel(
1362 bus,
1363 screen_base,
1364 row_bytes,
1365 pixel_size,
1366 screen_width,
1367 screen_height,
1368 x,
1369 y,
1370 black,
1371 );
1372 }
1373 }
1374
1375 fn fb_styled_glyph_base_pixels(
1376 x: i16,
1377 y: i16,
1378 glyph: &Glyph,
1379 data: &[u8],
1380 synthetic_italic: Option<(i16, i16)>,
1381 style: u8,
1382 ) -> HashSet<(i16, i16)> {
1383 let gx = x + glyph.origin_x as i16;
1384 let gy = y + glyph.origin_y as i16;
1385 let gw = glyph.width as usize;
1386 let gh = glyph.height as usize;
1387 let metrics = synthetic_italic
1388 .map(|(font_id, font_size)| (font_id, font_size, get_font_metrics(font_id, font_size)));
1389 let mut pixels = HashSet::new();
1390
1391 for row in 0..gh {
1392 for col in 0..gw {
1393 let byte_idx = glyph.data_offset + row * gw + col;
1394 if byte_idx >= data.len() || data[byte_idx] < 128 {
1395 continue;
1396 }
1397
1398 let py = gy + row as i16;
1399 let slant = metrics
1400 .as_ref()
1401 .map(|(font_id, font_size, metrics)| {
1402 get_italic_slant(*font_id, *font_size, metrics, y, py)
1403 })
1404 .unwrap_or(0);
1405 let start = col as i16;
1406 let (dst_start, dst_end) = (start, start + 1);
1407
1408 for dst_col in dst_start..dst_end {
1409 let px = gx + dst_col + slant;
1410 pixels.insert((px, py));
1411 if (style & TEXT_STYLE_BOLD) != 0 {
1412 pixels.insert((px + 1, py));
1413 }
1414 }
1415 }
1416 }
1417
1418 pixels
1419 }
1420
1421 fn fb_styled_glyph_advance(glyph: &Glyph, style: u8) -> i16 {
1422 let mut advance = glyph.advance as i16;
1423 if (style & TEXT_STYLE_BOLD) != 0 {
1424 advance += 1;
1430 }
1431 if (style & TEXT_STYLE_OUTLINE) != 0 {
1432 advance += 1;
1433 }
1434 if (style & TEXT_STYLE_SHADOW) != 0 {
1435 advance += 2;
1436 }
1437 if (style & TEXT_STYLE_CONDENSE) != 0 && advance >= 6 {
1438 advance -= 1;
1439 }
1440 if (style & TEXT_STYLE_EXTEND) != 0 {
1441 advance += 1;
1442 }
1443 advance.max(1)
1444 }
1445
1446 fn fb_draw_char_styled(
1447 bus: &mut MacMemoryBus,
1448 screen_base: u32,
1449 row_bytes: u32,
1450 pixel_size: u16,
1451 screen_width: i16,
1452 screen_height: i16,
1453 x: i16,
1454 y: i16,
1455 ch: char,
1456 font_id: i16,
1457 font_size: i16,
1458 style: u8,
1459 pixel_index_override: Option<u8>,
1460 black: bool,
1461 ) -> i16 {
1462 let italic = (style & TEXT_STYLE_ITALIC) != 0;
1463 let (glyph_hit, synthetic_italic) = if italic {
1464 if let Some(hit) = get_glyph_italic(font_id, font_size, ch) {
1465 (Some(hit), None)
1466 } else {
1467 (
1468 get_glyph(font_id, font_size, ch),
1469 Some((font_id, font_size)),
1470 )
1471 }
1472 } else {
1473 (get_glyph(font_id, font_size, ch), None)
1474 };
1475
1476 let Some((glyph, data)) = glyph_hit else {
1477 return 6;
1478 };
1479
1480 let glyph_y = if (style & TEXT_STYLE_SHADOW) != 0 {
1481 y - 1
1482 } else {
1483 y
1484 };
1485 let base_pixels =
1486 Self::fb_styled_glyph_base_pixels(x, glyph_y, glyph, data, synthetic_italic, style);
1487
1488 if (style & (TEXT_STYLE_OUTLINE | TEXT_STYLE_SHADOW)) == 0 {
1489 for (px, py) in base_pixels.iter().copied() {
1490 Self::fb_set_styled_text_pixel(
1491 bus,
1492 screen_base,
1493 row_bytes,
1494 pixel_size,
1495 screen_width,
1496 screen_height,
1497 px,
1498 py,
1499 pixel_index_override,
1500 black,
1501 );
1502 }
1503 return Self::fb_styled_glyph_advance(glyph, style);
1504 }
1505
1506 let smear_max = if (style & TEXT_STYLE_SHADOW) != 0 && (style & TEXT_STYLE_OUTLINE) != 0 {
1511 3
1512 } else if (style & TEXT_STYLE_SHADOW) != 0 {
1513 2
1514 } else {
1515 1
1516 };
1517 let min_x = base_pixels.iter().map(|(px, _)| *px).min().unwrap_or(x) - 1;
1518 let max_x = base_pixels.iter().map(|(px, _)| *px).max().unwrap_or(x) + smear_max;
1519 let min_y = base_pixels.iter().map(|(_, py)| *py).min().unwrap_or(y) - 1;
1520 let max_y = base_pixels.iter().map(|(_, py)| *py).max().unwrap_or(y) + smear_max;
1521
1522 for py in min_y..=max_y {
1523 for px in min_x..=max_x {
1524 if base_pixels.contains(&(px, py)) {
1525 continue;
1526 }
1527 let mut smeared = false;
1528 'smear: for dy in -1..=smear_max {
1529 for dx in -1..=smear_max {
1530 if base_pixels.contains(&(px - dx, py - dy)) {
1531 smeared = true;
1532 break 'smear;
1533 }
1534 }
1535 }
1536 if smeared {
1537 Self::fb_set_styled_text_pixel(
1538 bus,
1539 screen_base,
1540 row_bytes,
1541 pixel_size,
1542 screen_width,
1543 screen_height,
1544 px,
1545 py,
1546 pixel_index_override,
1547 black,
1548 );
1549 }
1550 }
1551 }
1552
1553 Self::fb_styled_glyph_advance(glyph, style)
1554 }
1555
1556 pub(crate) fn fb_draw_string(
1558 bus: &mut MacMemoryBus,
1559 screen_base: u32,
1560 row_bytes: u32,
1561 pixel_size: u16,
1562 screen_width: i16,
1563 screen_height: i16,
1564 x: i16,
1565 y: i16,
1566 s: &str,
1567 font_id: i16,
1568 font_size: i16,
1569 ) -> i16 {
1570 let mut cx = x;
1571 for ch in s.chars() {
1572 cx += Self::fb_draw_char(
1573 bus,
1574 screen_base,
1575 row_bytes,
1576 pixel_size,
1577 screen_width,
1578 screen_height,
1579 cx,
1580 y,
1581 ch,
1582 font_id,
1583 font_size,
1584 );
1585 }
1586 cx - x
1587 }
1588
1589 fn fb_draw_char_clipped(
1590 bus: &mut MacMemoryBus,
1591 screen_base: u32,
1592 row_bytes: u32,
1593 pixel_size: u16,
1594 screen_width: i16,
1595 screen_height: i16,
1596 x: i16,
1597 y: i16,
1598 ch: char,
1599 font_id: i16,
1600 font_size: i16,
1601 clip: (i16, i16, i16, i16),
1602 ) -> i16 {
1603 let Some((glyph, data)) = get_glyph(font_id, font_size, ch) else {
1604 return 6;
1605 };
1606 let gx = x + glyph.origin_x as i16;
1607 let gy = y + glyph.origin_y as i16;
1608 let gw = glyph.width as usize;
1609 let gh = glyph.height as usize;
1610 let (clip_top, clip_left, clip_bottom, clip_right) = clip;
1611 for row in 0..gh {
1612 let py = gy + row as i16;
1613 if py < clip_top || py >= clip_bottom {
1614 continue;
1615 }
1616 for col in 0..gw {
1617 let px = gx + col as i16;
1618 if px < clip_left || px >= clip_right {
1619 continue;
1620 }
1621 let byte_idx = glyph.data_offset + row * gw + col;
1622 if byte_idx < data.len() && data[byte_idx] >= 128 {
1623 Self::fb_set_pixel(
1624 bus,
1625 screen_base,
1626 row_bytes,
1627 pixel_size,
1628 screen_width,
1629 screen_height,
1630 px,
1631 py,
1632 true,
1633 );
1634 }
1635 }
1636 }
1637 glyph.advance as i16
1638 }
1639
1640 fn fb_draw_string_clipped(
1641 bus: &mut MacMemoryBus,
1642 screen_base: u32,
1643 row_bytes: u32,
1644 pixel_size: u16,
1645 screen_width: i16,
1646 screen_height: i16,
1647 x: i16,
1648 y: i16,
1649 s: &str,
1650 font_id: i16,
1651 font_size: i16,
1652 clip: (i16, i16, i16, i16),
1653 ) -> i16 {
1654 let mut cx = x;
1655 for ch in s.chars() {
1656 cx += Self::fb_draw_char_clipped(
1657 bus,
1658 screen_base,
1659 row_bytes,
1660 pixel_size,
1661 screen_width,
1662 screen_height,
1663 cx,
1664 y,
1665 ch,
1666 font_id,
1667 font_size,
1668 clip,
1669 );
1670 }
1671 cx - x
1672 }
1673
1674 pub(crate) fn fb_draw_string_styled(
1683 bus: &mut MacMemoryBus,
1684 screen_base: u32,
1685 row_bytes: u32,
1686 pixel_size: u16,
1687 screen_width: i16,
1688 screen_height: i16,
1689 x: i16,
1690 y: i16,
1691 s: &str,
1692 font_id: i16,
1693 font_size: i16,
1694 style: u8,
1695 ) -> i16 {
1696 Self::fb_draw_string_styled_with_index(
1697 bus,
1698 screen_base,
1699 row_bytes,
1700 pixel_size,
1701 screen_width,
1702 screen_height,
1703 x,
1704 y,
1705 s,
1706 font_id,
1707 font_size,
1708 style,
1709 None,
1710 true,
1711 )
1712 }
1713
1714 pub(crate) fn fb_draw_string_styled_ink(
1715 bus: &mut MacMemoryBus,
1716 screen_base: u32,
1717 row_bytes: u32,
1718 pixel_size: u16,
1719 screen_width: i16,
1720 screen_height: i16,
1721 x: i16,
1722 y: i16,
1723 s: &str,
1724 font_id: i16,
1725 font_size: i16,
1726 style: u8,
1727 black: bool,
1728 ) -> i16 {
1729 Self::fb_draw_string_styled_with_index(
1730 bus,
1731 screen_base,
1732 row_bytes,
1733 pixel_size,
1734 screen_width,
1735 screen_height,
1736 x,
1737 y,
1738 s,
1739 font_id,
1740 font_size,
1741 style,
1742 None,
1743 black,
1744 )
1745 }
1746
1747 pub(crate) fn fb_draw_string_styled_index(
1748 bus: &mut MacMemoryBus,
1749 screen_base: u32,
1750 row_bytes: u32,
1751 pixel_size: u16,
1752 screen_width: i16,
1753 screen_height: i16,
1754 x: i16,
1755 y: i16,
1756 s: &str,
1757 font_id: i16,
1758 font_size: i16,
1759 style: u8,
1760 pixel_index: u8,
1761 ) -> i16 {
1762 Self::fb_draw_string_styled_with_index(
1763 bus,
1764 screen_base,
1765 row_bytes,
1766 pixel_size,
1767 screen_width,
1768 screen_height,
1769 x,
1770 y,
1771 s,
1772 font_id,
1773 font_size,
1774 style,
1775 Some(pixel_index),
1776 true,
1777 )
1778 }
1779
1780 fn fb_draw_string_styled_with_index(
1781 bus: &mut MacMemoryBus,
1782 screen_base: u32,
1783 row_bytes: u32,
1784 pixel_size: u16,
1785 screen_width: i16,
1786 screen_height: i16,
1787 x: i16,
1788 y: i16,
1789 s: &str,
1790 font_id: i16,
1791 font_size: i16,
1792 style: u8,
1793 pixel_index_override: Option<u8>,
1794 black: bool,
1795 ) -> i16 {
1796 let mut cx = x;
1797 for ch in s.chars() {
1798 cx += Self::fb_draw_char_styled(
1799 bus,
1800 screen_base,
1801 row_bytes,
1802 pixel_size,
1803 screen_width,
1804 screen_height,
1805 cx,
1806 y,
1807 ch,
1808 font_id,
1809 font_size,
1810 style,
1811 pixel_index_override,
1812 black,
1813 );
1814 }
1815
1816 if (style & TEXT_STYLE_UNDERLINE) != 0 && cx > x {
1817 let thickness = get_underline_thickness(font_id, font_size).max(1);
1818 for dy in 1..=thickness {
1819 if let Some(pixel_index) = pixel_index_override {
1820 Self::fb_set_pixel_index(
1821 bus,
1822 screen_base,
1823 row_bytes,
1824 pixel_size,
1825 screen_width,
1826 screen_height,
1827 x,
1828 y + dy,
1829 pixel_index,
1830 );
1831 for underline_x in (x + 1)..cx {
1832 Self::fb_set_pixel_index(
1833 bus,
1834 screen_base,
1835 row_bytes,
1836 pixel_size,
1837 screen_width,
1838 screen_height,
1839 underline_x,
1840 y + dy,
1841 pixel_index,
1842 );
1843 }
1844 } else {
1845 Self::fb_hline(
1846 bus,
1847 screen_base,
1848 row_bytes,
1849 pixel_size,
1850 screen_width,
1851 screen_height,
1852 y + dy,
1853 x,
1854 cx,
1855 black,
1856 );
1857 }
1858 }
1859 }
1860
1861 cx - x
1862 }
1863
1864 pub(crate) fn draw_menu_bar_to_fb(&self, bus: &mut MacMemoryBus) {
1868 if self.fullscreen_locked || self.menu_bar_hidden {
1869 return;
1870 }
1871 let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
1872 self.get_screen_params();
1873 let menu_bar_height = bus.read_word(crate::memory::globals::addr::MBAR_HEIGHT) as i16;
1874 if menu_bar_height <= 0 {
1875 return;
1876 }
1877 let menu_bar_bg_index = self.menu_bar_background_pixel_index(bus, pixel_size);
1878
1879 if !self.draw_theme_menu_bar_chrome(bus, menu_bar_height) {
1880 if let Some(bg_index) = menu_bar_bg_index {
1884 Self::fb_fill_rect_index(
1885 bus,
1886 screen_base,
1887 row_bytes,
1888 pixel_size,
1889 screen_width,
1890 screen_height,
1891 0,
1892 0,
1893 menu_bar_height,
1894 screen_width,
1895 bg_index,
1896 );
1897 } else {
1898 Self::fb_fill_rect(
1899 bus,
1900 screen_base,
1901 row_bytes,
1902 pixel_size,
1903 screen_width,
1904 screen_height,
1905 0,
1906 0,
1907 menu_bar_height,
1908 screen_width,
1909 false,
1910 );
1911 }
1912
1913 Self::fb_hline(
1914 bus,
1915 screen_base,
1916 row_bytes,
1917 pixel_size,
1918 screen_width,
1919 screen_height,
1920 menu_bar_height - 1,
1921 0,
1922 screen_width,
1923 true,
1924 );
1925 Self::fb_draw_menu_bar_rounded_corners(
1926 bus,
1927 screen_base,
1928 row_bytes,
1929 pixel_size,
1930 screen_width,
1931 screen_height,
1932 );
1933 }
1934
1935 let font_id: i16 = 0;
1937 let font_size: i16 = 12;
1938 let metrics = get_font_metrics(font_id, font_size);
1939 let text_height = metrics.ascent + metrics.descent;
1941 let text_y = (menu_bar_height - text_height) / 2 + metrics.ascent;
1942
1943 let mut x: i16 = 18;
1947 for menu in &self.menus {
1948 if !menu.visible_in_menu_bar {
1949 continue;
1950 }
1951 let title = &menu.title;
1952 let title_width = Self::fb_measure_string(title, font_id, font_size);
1953 self.draw_theme_menu_title_chrome(
1958 bus,
1959 1,
1960 x - 7,
1961 menu_bar_height - 1,
1962 x + title_width + 6,
1963 menu.enabled,
1964 false,
1965 );
1966 let title_index = Self::menu_title_pixel_index(bus, menu.id, pixel_size);
1967 let classic_plain_dimmed_title = self.ui_theme_id() == UiThemeId::ClassicSystem7
1968 && title_index.is_none()
1969 && !menu.enabled;
1970 let width = if classic_plain_dimmed_title {
1971 title_width
1977 } else if Self::is_apple_mark_title(title) {
1978 Self::fb_draw_classic_apple_mark_title(
1979 bus,
1980 screen_base,
1981 row_bytes,
1982 pixel_size,
1983 screen_width,
1984 screen_height,
1985 x,
1986 text_y,
1987 title_index,
1988 );
1989 title_width
1990 } else if let Some(pixel_index) = title_index {
1991 Self::fb_draw_string_styled_index(
1992 bus,
1993 screen_base,
1994 row_bytes,
1995 pixel_size,
1996 screen_width,
1997 screen_height,
1998 x,
1999 text_y,
2000 title,
2001 font_id,
2002 font_size,
2003 0,
2004 pixel_index,
2005 )
2006 } else {
2007 Self::fb_draw_string(
2008 bus,
2009 screen_base,
2010 row_bytes,
2011 pixel_size,
2012 screen_width,
2013 screen_height,
2014 x,
2015 text_y,
2016 title,
2017 font_id,
2018 font_size,
2019 )
2020 };
2021 x += width + 13;
2022 }
2023 }
2024
2025 fn is_apple_mark_title(title: &str) -> bool {
2026 let mut chars = title.chars();
2027 matches!(chars.next(), Some('\u{14}' | '\u{F8FF}')) && chars.next().is_none()
2028 }
2029
2030 fn fb_draw_classic_apple_mark_title(
2031 bus: &mut MacMemoryBus,
2032 screen_base: u32,
2033 row_bytes: u32,
2034 pixel_size: u16,
2035 screen_width: i16,
2036 screen_height: i16,
2037 x: i16,
2038 baseline_y: i16,
2039 pixel_index_override: Option<u8>,
2040 ) {
2041 const MASK: [&str; 14] = [
2047 ".......##..",
2048 "......##...",
2049 "......#....",
2050 "..###..###.",
2051 ".##########",
2052 "...........",
2053 "...........",
2054 "...........",
2055 "...........",
2056 "###########",
2057 ".##########",
2058 ".##########",
2059 "..########.",
2060 "...##..##..",
2061 ];
2062
2063 let left = x - 1;
2064 let top = baseline_y - 12;
2065 for (dy, row) in MASK.iter().enumerate() {
2066 for (dx, byte) in row.as_bytes().iter().enumerate() {
2067 if *byte != b'#' {
2068 continue;
2069 }
2070 let dst_x = left + dx as i16;
2071 let dst_y = top + dy as i16;
2072 if let Some(pixel_index) = pixel_index_override {
2073 Self::fb_set_pixel_index(
2074 bus,
2075 screen_base,
2076 row_bytes,
2077 pixel_size,
2078 screen_width,
2079 screen_height,
2080 dst_x,
2081 dst_y,
2082 pixel_index,
2083 );
2084 } else {
2085 Self::fb_set_pixel(
2086 bus,
2087 screen_base,
2088 row_bytes,
2089 pixel_size,
2090 screen_width,
2091 screen_height,
2092 dst_x,
2093 dst_y,
2094 true,
2095 );
2096 }
2097 }
2098 }
2099 }
2100
2101 pub(crate) fn fb_draw_menu_bar_rounded_corners(
2102 bus: &mut MacMemoryBus,
2103 screen_base: u32,
2104 row_bytes: u32,
2105 pixel_size: u16,
2106 screen_width: i16,
2107 screen_height: i16,
2108 ) {
2109 const LEFT: &[(i16, i16)] = &[
2113 (0, 0),
2114 (1, 0),
2115 (2, 0),
2116 (3, 0),
2117 (4, 0),
2118 (0, 1),
2119 (1, 1),
2120 (2, 1),
2121 (0, 2),
2122 (1, 2),
2123 (0, 3),
2124 (0, 4),
2125 ];
2126 for &(x, y) in LEFT {
2127 Self::fb_set_pixel(
2128 bus,
2129 screen_base,
2130 row_bytes,
2131 pixel_size,
2132 screen_width,
2133 screen_height,
2134 x,
2135 y,
2136 true,
2137 );
2138 Self::fb_set_pixel(
2139 bus,
2140 screen_base,
2141 row_bytes,
2142 pixel_size,
2143 screen_width,
2144 screen_height,
2145 screen_width - 1 - x,
2146 y,
2147 true,
2148 );
2149 }
2150 }
2151
2152 pub(crate) fn blit_window_to_screen(&self, bus: &mut MacMemoryBus) {
2159 let (screen_base, screen_rb, screen_w, screen_h, pixel_size) = self.screen_mode;
2160 let trace = std::env::var_os("SYSTEMLESS_TRACE_BLIT_WINDOW").is_some();
2161 if self.front_window == 0 {
2162 if trace {
2163 eprintln!("[BLIT] skip: front_window=0");
2164 }
2165 return;
2166 }
2167 if pixel_size != 8 || screen_w == 0 || screen_h == 0 {
2168 if trace {
2169 eprintln!(
2170 "[BLIT] skip: screen mode mismatch (pixel_size={}, w={}, h={})",
2171 pixel_size, screen_w, screen_h
2172 );
2173 }
2174 return;
2175 }
2176
2177 let port = self.front_window;
2181 let port_version = bus.read_word(port + 6);
2182 let is_cgraf_port = (port_version & 0xC000) == 0xC000;
2183 let (port_base, port_pixmap_ptr, port_pixmap_handle) = if is_cgraf_port {
2184 let pm_handle = bus.read_long(port + 2);
2186 if pm_handle == 0 {
2187 if trace {
2188 eprintln!("[BLIT] skip: CGrafPort pm_handle=0");
2189 }
2190 return;
2191 }
2192 let pm_ptr = bus.read_long(pm_handle);
2193 if pm_ptr == 0 {
2194 if trace {
2195 eprintln!(
2196 "[BLIT] skip: CGrafPort pm_ptr=0 (pm_handle=${:08X})",
2197 pm_handle
2198 );
2199 }
2200 return;
2201 }
2202 (bus.read_long(pm_ptr) & 0x3FFFFFFF, pm_ptr, pm_handle) } else {
2204 (bus.read_long(port + 2), 0, 0)
2206 };
2207
2208 if is_cgraf_port
2215 && self
2216 .window_original_pixmaps
2217 .get(&port)
2218 .is_some_and(|&original| original != port_pixmap_handle)
2219 {
2220 if trace {
2221 eprintln!(
2222 "[BLIT] skip: CGrafPort portPixMap swapped original=${:08X} current=${:08X}",
2223 self.window_original_pixmaps[&port], port_pixmap_handle
2224 );
2225 }
2226 return;
2227 }
2228
2229 if port_base == screen_base || port_base == 0 {
2231 if trace {
2232 eprintln!(
2233 "[BLIT] skip: port_base=${:08X} screen_base=${:08X} \
2234 (port draws directly to screen or port_base is NIL)",
2235 port_base, screen_base
2236 );
2237 }
2238 return;
2239 }
2240
2241 let port_rb = if is_cgraf_port {
2243 (bus.read_word(port_pixmap_ptr + 4) & 0x3FFF) as u32
2244 } else {
2245 (bus.read_word(port + 6) & 0x3FFF) as u32
2246 };
2247
2248 let port_pixel_size: u32 = if is_cgraf_port {
2252 bus.read_word(port_pixmap_ptr + 32) as u32
2253 } else {
2254 1
2255 };
2256 let port_ctab_handle = if is_cgraf_port {
2257 bus.read_long(port_pixmap_ptr + 42)
2258 } else {
2259 0
2260 };
2261
2262 let wr_top = bus.read_word(port + 16) as i16;
2264 let wr_left = bus.read_word(port + 18) as i16;
2265 let wr_bottom = bus.read_word(port + 20) as i16;
2266 let wr_right = bus.read_word(port + 22) as i16;
2267
2268 let w = (wr_right - wr_left) as u32;
2269 let h = (wr_bottom - wr_top) as u32;
2270 if w == 0 || h == 0 {
2271 return;
2272 }
2273
2274 let src_y_offset = wr_top.max(0) as u32;
2275 let src_x_offset = wr_left.max(0) as u32;
2276 let dst_y = src_y_offset;
2277 let dst_x = src_x_offset;
2278
2279 if port_pixel_size == 1 && pixel_size == 8 {
2284 let (white_index, black_index) = Self::logical_mono_pixel_indexes(bus);
2285 let pb_top = bus.read_word(port + 8) as i16;
2293 let pb_left = bus.read_word(port + 10) as i16;
2294 let pb_bottom = bus.read_word(port + 12) as i16;
2295 let pb_right = bus.read_word(port + 14) as i16;
2296 let bitmap_w = (pb_right - pb_left).max(0) as u32;
2297 let bitmap_h = (pb_bottom - pb_top).max(0) as u32;
2298 let row_count = h.min((screen_h as u32).saturating_sub(dst_y)).min(bitmap_h);
2299 let col_count = w.min((screen_w as u32).saturating_sub(dst_x)).min(bitmap_w);
2300 for row in 0..row_count {
2301 let src_row_addr = port_base + (src_y_offset + row) * port_rb;
2302 let dst_row_addr = screen_base + (dst_y + row) * screen_rb + dst_x;
2303 for col in 0..col_count {
2304 let src_bit_x = src_x_offset + col;
2305 let src_byte = bus.read_byte(src_row_addr + src_bit_x / 8);
2306 let bit = (src_byte >> (7 - (src_bit_x & 7))) & 1;
2307 let dst_idx = if bit == 0 { white_index } else { black_index };
2308 bus.write_byte(dst_row_addr + col, dst_idx);
2309 }
2310 }
2311 return;
2312 }
2313
2314 if port_pixel_size != pixel_size as u32 {
2317 return;
2318 }
2319
2320 let row_count = h.min((screen_h as u32).saturating_sub(dst_y));
2321 let col_count = w.min((screen_w as u32).saturating_sub(dst_x));
2322
2323 if is_cgraf_port && port_pixel_size == 8 && pixel_size == 8 {
2328 let screen_ctab_handle = Self::gdevice_ctab_handle(bus, self.main_gdevice_handle);
2329 let src_ctab_seed = Self::ctab_seed(bus, port_ctab_handle);
2330 let dst_ctab_seed = Self::ctab_seed(bus, screen_ctab_handle);
2331 let src_clut = self.read_port_clut(bus, port_ctab_handle);
2332 let dst_clut = self.read_port_clut(bus, screen_ctab_handle);
2333 let hardware_palette_active = self.device_clut != self.color_manager_clut;
2334 let skip_canonical_to_screen = Self::uses_canonical_system_8bpp_clut(&src_clut);
2335 if port_ctab_handle != screen_ctab_handle
2336 && matches!(src_ctab_seed, Some(src_seed) if src_seed != 0)
2337 && src_ctab_seed != dst_ctab_seed
2338 && !skip_canonical_to_screen
2339 && !hardware_palette_active
2340 {
2341 let translation =
2342 self.build_palette_translation(bus, &src_clut, &dst_clut, screen_ctab_handle);
2343 for row in 0..row_count {
2344 let src_addr = port_base + (src_y_offset + row) * port_rb + src_x_offset;
2345 let dst_addr = screen_base + (dst_y + row) * screen_rb + dst_x;
2346 for col in 0..col_count {
2347 let src_idx = bus.read_byte(src_addr + col);
2348 bus.write_byte(dst_addr + col, translation[src_idx as usize]);
2349 }
2350 }
2351 return;
2352 }
2353 }
2354
2355 for row in 0..row_count {
2357 let src_addr = port_base + (src_y_offset + row) * port_rb + src_x_offset;
2358 let dst_addr = screen_base + (dst_y + row) * screen_rb + dst_x;
2359 bus.block_move(src_addr, dst_addr, col_count);
2360 }
2361 }
2362
2363 pub(crate) fn blit_large_manual_cport_to_screen(&mut self, bus: &mut MacMemoryBus) {
2372 let (screen_base, screen_rb, screen_w, screen_h, pixel_size) = self.screen_mode;
2373 let screen_w_u32 = u32::from(screen_w);
2374 let screen_h_u32 = u32::from(screen_h);
2375 let trace = std::env::var_os("SYSTEMLESS_TRACE_BLIT_WINDOW").is_some();
2376 let latched_port = self.manual_cport_presented_port;
2377
2378 if self.front_window == 0
2379 || pixel_size != 8
2380 || screen_w == 0
2381 || screen_h == 0
2382 || (self.copybits_screen_count != 0 && latched_port == 0)
2383 {
2384 if trace {
2385 eprintln!(
2386 "[BLIT-CPORT] skip: front=${:08X} pixel_size={} screen={}x{} copybits={} latched=${:08X}",
2387 self.front_window,
2388 pixel_size,
2389 screen_w,
2390 screen_h,
2391 self.copybits_screen_count,
2392 latched_port
2393 );
2394 }
2395 return;
2396 }
2397 if !self.window_visible(bus, self.front_window) {
2398 if trace {
2399 eprintln!(
2400 "[BLIT-CPORT] skip: front=${:08X} is not visible",
2401 self.front_window
2402 );
2403 }
2404 return;
2405 }
2406
2407 let front_port = self.front_window;
2408 let front_is_cgraf = (bus.read_word(front_port + 6) & 0xC000) == 0xC000;
2409 let front_base = if front_is_cgraf {
2410 let pm_handle = bus.read_long(front_port + 2);
2411 let pm_ptr = (pm_handle != 0)
2412 .then(|| bus.read_long(pm_handle))
2413 .unwrap_or(0);
2414 if pm_ptr == 0 {
2415 return;
2416 }
2417 bus.read_long(pm_ptr) & 0x3FFF_FFFF
2418 } else {
2419 bus.read_long(front_port + 2)
2420 };
2421 if front_base != screen_base {
2422 if trace {
2423 eprintln!(
2424 "[BLIT-CPORT] skip: front base ${:08X} != screen ${:08X}",
2425 front_base, screen_base
2426 );
2427 }
2428 return;
2429 }
2430
2431 let front_top = bus.read_word(front_port + 16) as i16;
2432 let front_left = bus.read_word(front_port + 18) as i16;
2433 let front_bottom = bus.read_word(front_port + 20) as i16;
2434 let front_right = bus.read_word(front_port + 22) as i16;
2435 let port_rect_covers_screen = front_top <= 0
2436 && front_left <= 0
2437 && front_bottom >= screen_h as i16
2438 && front_right >= screen_w as i16;
2439 let (wt, wl, wb, wr) = self.window_bounds;
2440 let window_bounds_cover_screen =
2441 wt <= 0 && wl <= 0 && wb >= screen_h as i16 && wr >= screen_w as i16;
2442 let front_covers_presentation = port_rect_covers_screen || window_bounds_cover_screen;
2443 let screen_is_dark =
2444 self.screen_is_dark_for_manual_cport(bus, screen_base, screen_rb, screen_w, screen_h);
2445 let dark_screen_allows_presentation = !front_covers_presentation && screen_is_dark;
2446
2447 #[derive(Clone, Copy)]
2448 struct Candidate {
2449 port: u32,
2450 base: u32,
2451 row_bytes: u32,
2452 width: u32,
2453 height: u32,
2454 ctab_handle: u32,
2455 }
2456
2457 let screen_area = u64::from(screen_w_u32) * u64::from(screen_h_u32);
2458 let min_area = (screen_area / 8).max(1);
2459 let mut best: Option<Candidate> = None;
2460 let mut considered_ports = 0u32;
2461 let mut rejected_shape = 0u32;
2462 let mut rejected_area = 0u32;
2463 for &port in &self.cport_ports {
2464 if port == front_port
2465 || self.window_list.contains(&port)
2466 || self.gworld_devices.contains_key(&port)
2467 || (self.copybits_screen_count != 0 && latched_port != 0 && port != latched_port)
2468 {
2469 continue;
2470 }
2471 considered_ports += 1;
2472 if (bus.read_word(port + 6) & 0xC000) != 0xC000 {
2473 rejected_shape += 1;
2474 continue;
2475 }
2476 let pm_handle = bus.read_long(port + 2);
2477 if pm_handle == 0 {
2478 rejected_shape += 1;
2479 continue;
2480 }
2481 let pm_ptr = bus.read_long(pm_handle);
2482 if pm_ptr == 0 {
2483 rejected_shape += 1;
2484 continue;
2485 }
2486
2487 let base = bus.read_long(pm_ptr) & 0x3FFF_FFFF;
2491 let row_bytes = (bus.read_word(pm_ptr + 4) & 0x3FFF) as u32;
2492 let top = bus.read_word(pm_ptr + 6) as i16;
2493 let left = bus.read_word(pm_ptr + 8) as i16;
2494 let bottom = bus.read_word(pm_ptr + 10) as i16;
2495 let right = bus.read_word(pm_ptr + 12) as i16;
2496 let width = (right - left).max(0) as u32;
2497 let height = (bottom - top).max(0) as u32;
2498 let port_pixel_size = bus.read_word(pm_ptr + 32) as u32;
2499 if base == 0
2500 || base == screen_base
2501 || port_pixel_size != 8
2502 || width == 0
2503 || height == 0
2504 || width > screen_w_u32
2505 || height > screen_h_u32
2506 || row_bytes < width
2507 {
2508 rejected_shape += 1;
2509 continue;
2510 }
2511 let area = u64::from(width) * u64::from(height);
2512 if area < min_area {
2513 rejected_area += 1;
2514 continue;
2515 }
2516
2517 let candidate = Candidate {
2518 port,
2519 base,
2520 row_bytes,
2521 width,
2522 height,
2523 ctab_handle: bus.read_long(pm_ptr + 42),
2524 };
2525 if best
2526 .map(|current| area > u64::from(current.width) * u64::from(current.height))
2527 .unwrap_or(true)
2528 {
2529 best = Some(candidate);
2530 }
2531 }
2532
2533 let Some(candidate) = best else {
2534 if trace {
2535 eprintln!(
2536 "[BLIT-CPORT] skip: no candidate (tracked={}, considered={}, shape_rejects={}, area_rejects={}, min_area={})",
2537 self.cport_ports.len(),
2538 considered_ports,
2539 rejected_shape,
2540 rejected_area,
2541 min_area
2542 );
2543 }
2544 return;
2545 };
2546 if latched_port != candidate.port {
2547 let visible_samples = self.manual_cport_visible_sample_count(
2548 bus,
2549 candidate.base,
2550 candidate.row_bytes,
2551 candidate.width,
2552 candidate.height,
2553 );
2554 if visible_samples < 8 {
2555 if trace {
2556 eprintln!(
2557 "[BLIT-CPORT] skip: candidate port=${:08X} base=${:08X} {}x{} has too little sampled content ({})",
2558 candidate.port, candidate.base, candidate.width, candidate.height, visible_samples
2559 );
2560 }
2561 return;
2562 }
2563 if !screen_is_dark {
2564 if trace {
2565 eprintln!(
2566 "[BLIT-CPORT] skip: candidate port=${:08X} base=${:08X} {}x{} cannot latch over visible screen content (samples={})",
2567 candidate.port, candidate.base, candidate.width, candidate.height, visible_samples
2568 );
2569 }
2570 return;
2571 }
2572 }
2573 if !front_covers_presentation && !dark_screen_allows_presentation {
2574 if trace {
2575 eprintln!(
2576 "[BLIT-CPORT] skip: front portRect ({},{},{},{}) and window bounds ({},{},{},{}) do not cover {}x{}, screen is not dark enough for fallback presentation",
2577 front_top,
2578 front_left,
2579 front_bottom,
2580 front_right,
2581 wt,
2582 wl,
2583 wb,
2584 wr,
2585 screen_w,
2586 screen_h
2587 );
2588 }
2589 return;
2590 }
2591 let dst_x = (screen_w_u32 - candidate.width) / 2;
2592 let dst_y = (screen_h_u32 - candidate.height) / 2;
2593 let row_count = candidate.height.min(screen_h_u32.saturating_sub(dst_y));
2594 let col_count = candidate.width.min(screen_w_u32.saturating_sub(dst_x));
2595 if row_count == 0 || col_count == 0 {
2596 return;
2597 }
2598 self.manual_cport_presented_port = candidate.port;
2599
2600 if trace {
2601 eprintln!(
2602 "[BLIT-CPORT] presenting port=${:08X} base=${:08X} {}x{} at {},{}",
2603 candidate.port, candidate.base, col_count, row_count, dst_x, dst_y
2604 );
2605 }
2606
2607 let screen_ctab_handle = Self::gdevice_ctab_handle(bus, self.main_gdevice_handle);
2608 let src_ctab_seed = Self::ctab_seed(bus, candidate.ctab_handle);
2609 let dst_ctab_seed = Self::ctab_seed(bus, screen_ctab_handle);
2610 let src_clut = self.read_port_clut(bus, candidate.ctab_handle);
2611 let dst_clut = self.read_port_clut(bus, screen_ctab_handle);
2612 let hardware_palette_active = self.device_clut != self.color_manager_clut;
2613 let skip_canonical_to_screen = Self::uses_canonical_system_8bpp_clut(&src_clut);
2614 if candidate.ctab_handle != screen_ctab_handle
2615 && matches!(src_ctab_seed, Some(src_seed) if src_seed != 0)
2616 && src_ctab_seed != dst_ctab_seed
2617 && !skip_canonical_to_screen
2618 && !hardware_palette_active
2619 {
2620 let translation =
2621 self.build_palette_translation(bus, &src_clut, &dst_clut, screen_ctab_handle);
2622 for row in 0..row_count {
2623 let src_addr = candidate.base + row * candidate.row_bytes;
2624 let dst_addr = screen_base + (dst_y + row) * screen_rb + dst_x;
2625 for col in 0..col_count {
2626 let src_idx = bus.read_byte(src_addr + col);
2627 bus.write_byte(dst_addr + col, translation[src_idx as usize]);
2628 }
2629 }
2630 return;
2631 }
2632
2633 for row in 0..row_count {
2634 let src_addr = candidate.base + row * candidate.row_bytes;
2635 let dst_addr = screen_base + (dst_y + row) * screen_rb + dst_x;
2636 bus.block_move(src_addr, dst_addr, col_count);
2637 }
2638 }
2639
2640 fn manual_cport_visible_sample_count(
2641 &self,
2642 bus: &MacMemoryBus,
2643 base: u32,
2644 row_bytes: u32,
2645 width: u32,
2646 height: u32,
2647 ) -> u32 {
2648 if base == 0 || width == 0 || height == 0 || row_bytes < width {
2649 return 0;
2650 }
2651 let visible = |idx: u8| {
2652 let rgb = self.device_clut[idx as usize];
2653 rgb[0] > 0x1111 || rgb[1] > 0x1111 || rgb[2] > 0x1111
2654 };
2655 let sample = |x: u32, y: u32| -> bool {
2656 if x >= width || y >= height {
2657 return false;
2658 }
2659 visible(bus.read_byte(base + y * row_bytes + x))
2660 };
2661
2662 let mut visible_samples = 0u32;
2663 for (x, y) in [
2664 (0, 0),
2665 (width - 1, 0),
2666 (0, height - 1),
2667 (width - 1, height - 1),
2668 (width / 2, height / 2),
2669 ] {
2670 if sample(x, y) {
2671 visible_samples += 1;
2672 }
2673 }
2674
2675 let step_x = (width / 32).max(1);
2676 let step_y = (height / 24).max(1);
2677 let mut y = step_y / 2;
2678 while y < height {
2679 let mut x = step_x / 2;
2680 while x < width {
2681 if sample(x, y) {
2682 visible_samples += 1;
2683 }
2684 x += step_x;
2685 }
2686 y += step_y;
2687 }
2688 visible_samples
2689 }
2690
2691 fn screen_is_dark_for_manual_cport(
2692 &self,
2693 bus: &MacMemoryBus,
2694 screen_base: u32,
2695 screen_rb: u32,
2696 screen_w: u16,
2697 screen_h: u16,
2698 ) -> bool {
2699 if screen_w == 0 || screen_h == 0 || self.copybits_screen_count != 0 {
2700 return false;
2701 }
2702 let max_component = 0x1111;
2703 let sample_step_x = (u32::from(screen_w) / 16).max(1);
2704 let sample_step_y = (u32::from(screen_h) / 12).max(1);
2705 let mut y = sample_step_y / 2;
2706 while y < u32::from(screen_h) {
2707 let row = screen_base + y * screen_rb;
2708 let mut x = sample_step_x / 2;
2709 while x < u32::from(screen_w) {
2710 let idx = bus.read_byte(row + x) as usize;
2711 let rgb = self.device_clut[idx];
2712 if rgb[0] > max_component || rgb[1] > max_component || rgb[2] > max_component {
2713 return false;
2714 }
2715 x += sample_step_x;
2716 }
2717 y += sample_step_y;
2718 }
2719 true
2720 }
2721
2722 pub(crate) fn draw_window_chrome(&self, bus: &mut MacMemoryBus, active: bool) {
2725 let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
2726 self.get_screen_params();
2727 let (wind_top, wind_left, wind_bottom, wind_right) = self.window_bounds;
2728
2729 let menu_bar_height = bus.read_word(crate::memory::globals::addr::MBAR_HEIGHT) as i16;
2733 let tb_top = (wind_top - 19).max(menu_bar_height);
2734 let tb_bottom = wind_top - 1;
2735 let tb_left = wind_left - 1;
2736 let tb_right = wind_right + 1;
2737
2738 Self::fb_fill_rect(
2740 bus,
2741 screen_base,
2742 row_bytes,
2743 pixel_size,
2744 screen_width,
2745 screen_height,
2746 tb_top,
2747 tb_left,
2748 tb_bottom + 1,
2749 tb_right,
2750 false,
2751 );
2752
2753 let is_movable_modal = self.window_proc_id == 5;
2754 let has_go_away =
2755 active && Self::window_is_document_proc(self.window_proc_id) && self.go_away_flag;
2756
2757 if is_movable_modal {
2761 Self::fb_hline(
2762 bus,
2763 screen_base,
2764 row_bytes,
2765 pixel_size,
2766 screen_width,
2767 screen_height,
2768 tb_top,
2769 tb_left,
2770 tb_right,
2771 true,
2772 );
2773 }
2774 Self::fb_hline(
2775 bus,
2776 screen_base,
2777 row_bytes,
2778 pixel_size,
2779 screen_width,
2780 screen_height,
2781 tb_bottom,
2782 tb_left,
2783 tb_right,
2784 true,
2785 );
2786 for y in tb_top..=tb_bottom {
2788 Self::fb_set_pixel(
2789 bus,
2790 screen_base,
2791 row_bytes,
2792 pixel_size,
2793 screen_width,
2794 screen_height,
2795 tb_left,
2796 y,
2797 true,
2798 );
2799 Self::fb_set_pixel(
2800 bus,
2801 screen_base,
2802 row_bytes,
2803 pixel_size,
2804 screen_width,
2805 screen_height,
2806 tb_right - 1,
2807 y,
2808 true,
2809 );
2810 }
2811
2812 let font_id: i16 = 0; let font_size: i16 = 12;
2815 let metrics = get_font_metrics(font_id, font_size);
2816 let text_height = metrics.ascent + metrics.descent;
2817 let tb_interior_height = tb_bottom - tb_top - 1;
2818 let text_y = tb_top + 1 + (tb_interior_height - text_height) / 2 + metrics.ascent;
2819
2820 let (title_clear_left, title_clear_right) = if !self.window_title.is_empty() {
2821 let mut title_width: i16 = 0;
2822 for ch in self.window_title.chars() {
2823 if let Some((glyph, _)) = get_glyph(font_id, font_size, ch) {
2824 title_width += glyph.advance as i16;
2825 } else {
2826 title_width += 6;
2827 }
2828 }
2829 let text_x = tb_left + (tb_right - tb_left - title_width) / 2;
2830 (text_x - 8, text_x + title_width + 8)
2831 } else {
2832 (tb_right, tb_right) };
2834
2835 let _close_box_width = if has_go_away { 15i16 } else { 0 };
2836
2837 if is_movable_modal {
2838 if !self.window_title.is_empty() {
2841 let text_x = title_clear_left + 8;
2842 Self::fb_draw_string(
2843 bus,
2844 screen_base,
2845 row_bytes,
2846 pixel_size,
2847 screen_width,
2848 screen_height,
2849 text_x,
2850 text_y,
2851 &self.window_title,
2852 font_id,
2853 font_size,
2854 );
2855 }
2856 } else {
2857 if has_go_away {
2875 let cb_size: i16 = 11;
2876 let interior_top = tb_top + 1;
2877 let interior_height = tb_bottom - interior_top;
2878 let cb_top = interior_top + (interior_height - cb_size) / 2;
2879 let cb_left = tb_left + 9; Self::fb_hline(
2883 bus,
2884 screen_base,
2885 row_bytes,
2886 pixel_size,
2887 screen_width,
2888 screen_height,
2889 cb_top,
2890 cb_left,
2891 cb_left + cb_size,
2892 true,
2893 );
2894 for y in cb_top..(cb_top + cb_size) {
2895 Self::fb_set_pixel(
2896 bus,
2897 screen_base,
2898 row_bytes,
2899 pixel_size,
2900 screen_width,
2901 screen_height,
2902 cb_left,
2903 y,
2904 true,
2905 );
2906 }
2907
2908 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) {
2913 Self::fb_set_pixel(
2914 bus,
2915 screen_base,
2916 row_bytes,
2917 pixel_size,
2918 screen_width,
2919 screen_height,
2920 inner_right,
2921 y,
2922 true,
2923 );
2924 }
2925 Self::fb_hline(
2926 bus,
2927 screen_base,
2928 row_bytes,
2929 pixel_size,
2930 screen_width,
2931 screen_height,
2932 inner_bottom,
2933 cb_left + 2,
2934 cb_left + cb_size - 1,
2935 true,
2936 );
2937 }
2938
2939 if active {
2950 let stripe_left_edge = tb_left + 2;
2951 let stripe_right_end = tb_right - 2;
2952 let stripe_text_left = title_clear_left + 2;
2953 let stripe_text_right = title_clear_right - 2;
2954
2955 let (cb_gap_left, cb_gap_right) = if has_go_away {
2957 let cb_left = tb_left + 9;
2958 let cb_right = cb_left + 10; (cb_left - 1, cb_right + 2) } else {
2961 (stripe_right_end, stripe_right_end) };
2963
2964 for y in (tb_top + 1)..=(tb_bottom - 4) {
2965 if (y - tb_top) % 2 == 1 {
2966 let seg1_end = if has_go_away {
2969 cb_gap_left
2970 } else {
2971 stripe_text_left
2972 };
2973 if stripe_left_edge < seg1_end {
2974 Self::fb_hline(
2975 bus,
2976 screen_base,
2977 row_bytes,
2978 pixel_size,
2979 screen_width,
2980 screen_height,
2981 y,
2982 stripe_left_edge,
2983 seg1_end,
2984 true,
2985 );
2986 }
2987 if has_go_away && cb_gap_right < stripe_text_left {
2989 Self::fb_hline(
2990 bus,
2991 screen_base,
2992 row_bytes,
2993 pixel_size,
2994 screen_width,
2995 screen_height,
2996 y,
2997 cb_gap_right,
2998 stripe_text_left,
2999 true,
3000 );
3001 }
3002 if stripe_text_right < stripe_right_end {
3004 Self::fb_hline(
3005 bus,
3006 screen_base,
3007 row_bytes,
3008 pixel_size,
3009 screen_width,
3010 screen_height,
3011 y,
3012 stripe_text_right,
3013 stripe_right_end,
3014 true,
3015 );
3016 }
3017 }
3018 }
3019 }
3020
3021 if !self.window_title.is_empty() {
3025 let text_x = title_clear_left + 8;
3026 Self::fb_draw_string_clipped(
3027 bus,
3028 screen_base,
3029 row_bytes,
3030 pixel_size,
3031 screen_width,
3032 screen_height,
3033 text_x,
3034 text_y - 5,
3035 &self.window_title,
3036 font_id,
3037 font_size,
3038 (tb_top, tb_left, tb_bottom - 2, tb_right),
3039 );
3040 }
3041 }
3042
3043 for y in wind_top..wind_bottom {
3045 Self::fb_set_pixel(
3046 bus,
3047 screen_base,
3048 row_bytes,
3049 pixel_size,
3050 screen_width,
3051 screen_height,
3052 wind_left - 1,
3053 y,
3054 true,
3055 );
3056 Self::fb_set_pixel(
3057 bus,
3058 screen_base,
3059 row_bytes,
3060 pixel_size,
3061 screen_width,
3062 screen_height,
3063 wind_right,
3064 y,
3065 true,
3066 );
3067 }
3068 Self::fb_hline(
3070 bus,
3071 screen_base,
3072 row_bytes,
3073 pixel_size,
3074 screen_width,
3075 screen_height,
3076 wind_bottom,
3077 wind_left - 1,
3078 wind_right + 1,
3079 true,
3080 );
3081
3082 for y in tb_top..=(wind_bottom + 1) {
3084 Self::fb_set_pixel(
3085 bus,
3086 screen_base,
3087 row_bytes,
3088 pixel_size,
3089 screen_width,
3090 screen_height,
3091 wind_right + 1,
3092 y,
3093 true,
3094 );
3095 }
3096 Self::fb_hline(
3097 bus,
3098 screen_base,
3099 row_bytes,
3100 pixel_size,
3101 screen_width,
3102 screen_height,
3103 wind_bottom + 1,
3104 tb_left + 1,
3105 wind_right + 2,
3106 true,
3107 );
3108 self.capture_gui_frame(bus, "draw_window_chrome");
3109 }
3110
3111 pub(crate) fn draw_grow_icon(&self, bus: &mut MacMemoryBus, window_ptr: u32) {
3115 let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
3116 self.get_screen_params();
3117 let port_top = bus.read_word(window_ptr + 16) as i16;
3119 let port_left = bus.read_word(window_ptr + 18) as i16;
3120 let port_bottom = bus.read_word(window_ptr + 20) as i16;
3121 let port_right = bus.read_word(window_ptr + 22) as i16;
3122 let port_version = bus.read_word(window_ptr + 6);
3124 let (scr_top, scr_left) = if (port_version & 0xC000) == 0xC000 {
3125 let pm_handle = bus.read_long(window_ptr + 2);
3126 if pm_handle != 0 {
3127 let pm_ptr = bus.read_long(pm_handle);
3128 let bt = bus.read_word(pm_ptr + 6) as i16;
3129 let bl = bus.read_word(pm_ptr + 8) as i16;
3130 (-bt, -bl)
3131 } else {
3132 (0, 0)
3133 }
3134 } else {
3135 let bt = bus.read_word(window_ptr + 8) as i16;
3136 let bl = bus.read_word(window_ptr + 10) as i16;
3137 (-bt, -bl)
3138 };
3139
3140 let content_top = scr_top + port_top;
3142 let content_left = scr_left + port_left;
3143 let content_bottom = scr_top + port_bottom;
3144 let content_right = scr_left + port_right;
3145
3146 let sep_x = content_right - 15;
3150 let sep_y = content_bottom - 15;
3151 let border_left = content_left - 1;
3152 let border_right = content_right + 1;
3153
3154 for y in content_top..content_bottom {
3156 Self::fb_set_pixel(
3157 bus,
3158 screen_base,
3159 row_bytes,
3160 pixel_size,
3161 screen_width,
3162 screen_height,
3163 sep_x,
3164 y,
3165 true,
3166 );
3167 }
3168 Self::fb_hline(
3170 bus,
3171 screen_base,
3172 row_bytes,
3173 pixel_size,
3174 screen_width,
3175 screen_height,
3176 sep_y,
3177 border_left,
3178 border_right + 1,
3179 true,
3180 );
3181 }
3182
3183 pub(crate) fn draw_thick_rect_border(
3188 &self,
3189 bus: &mut MacMemoryBus,
3190 top: i16,
3191 left: i16,
3192 bottom: i16,
3193 right: i16,
3194 ) {
3195 let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
3196 self.get_screen_params();
3197 for dy in 0..2i16 {
3199 Self::fb_hline(
3200 bus,
3201 screen_base,
3202 row_bytes,
3203 pixel_size,
3204 screen_width,
3205 screen_height,
3206 top + dy,
3207 left,
3208 right,
3209 true,
3210 );
3211 }
3212 Self::fb_hline(
3214 bus,
3215 screen_base,
3216 row_bytes,
3217 pixel_size,
3218 screen_width,
3219 screen_height,
3220 bottom - 1,
3221 left,
3222 right,
3223 true,
3224 );
3225 for dx in 0..2i16 {
3227 for y in top..bottom {
3228 Self::fb_set_pixel(
3229 bus,
3230 screen_base,
3231 row_bytes,
3232 pixel_size,
3233 screen_width,
3234 screen_height,
3235 left + dx,
3236 y,
3237 true,
3238 );
3239 }
3240 }
3241 for dx in 0..2i16 {
3243 for y in top..bottom {
3244 Self::fb_set_pixel(
3245 bus,
3246 screen_base,
3247 row_bytes,
3248 pixel_size,
3249 screen_width,
3250 screen_height,
3251 right - 2 + dx,
3252 y,
3253 true,
3254 );
3255 }
3256 }
3257 }
3258
3259 fn erase_structure_region(
3262 &self,
3263 bus: &mut MacMemoryBus,
3264 top: i16,
3265 left: i16,
3266 bottom: i16,
3267 right: i16,
3268 ) {
3269 let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
3270 self.get_screen_params();
3271 Self::fb_fill_rect(
3272 bus,
3273 screen_base,
3274 row_bytes,
3275 pixel_size,
3276 screen_width,
3277 screen_height,
3278 top,
3279 left,
3280 bottom,
3281 right,
3282 false,
3283 );
3284 }
3285
3286 fn erase_structure_frame_around_content(
3291 &self,
3292 bus: &mut MacMemoryBus,
3293 structure: (i16, i16, i16, i16),
3294 content: (i16, i16, i16, i16),
3295 ) {
3296 let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
3297 self.get_screen_params();
3298 let (st, sl, sb, sr) = structure;
3299 let (ct, cl, cb, cr) = content;
3300 let mut erase = |top: i16, left: i16, bottom: i16, right: i16| {
3301 if bottom <= top || right <= left {
3302 return;
3303 }
3304 Self::fb_fill_rect(
3305 bus,
3306 screen_base,
3307 row_bytes,
3308 pixel_size,
3309 screen_width,
3310 screen_height,
3311 top,
3312 left,
3313 bottom,
3314 right,
3315 false,
3316 );
3317 };
3318
3319 erase(st, sl, ct.min(sb), sr);
3320 erase(cb.max(st), sl, sb, sr);
3321 erase(ct.max(st), sl, cb.min(sb), cl.min(sr));
3322 erase(ct.max(st), cr.max(sl), cb.min(sb), sr);
3323 }
3324
3325 pub(crate) fn draw_single_window_chrome_inline(
3339 &mut self,
3340 bus: &mut MacMemoryBus,
3341 window_ptr: u32,
3342 hilited: bool,
3343 ) {
3344 if window_ptr == 0 {
3345 return;
3346 }
3347 if bus.read_byte(window_ptr + 110u32) == 0 {
3348 return; }
3350 if self.window_uses_custom_def_proc(bus, window_ptr) {
3351 return;
3352 }
3353 let proc_id = self.window_proc_ids.get(&window_ptr).copied().unwrap_or(0);
3357 let port_version = bus.read_word(window_ptr + 6);
3358 let (pmap_top, pmap_left) = if (port_version & 0xC000) == 0xC000 {
3359 let pm_handle = bus.read_long(window_ptr + 2);
3360 let pm_ptr = bus.read_long(pm_handle);
3361 (
3362 bus.read_word(pm_ptr + 6) as i16,
3363 bus.read_word(pm_ptr + 8) as i16,
3364 )
3365 } else {
3366 (
3367 bus.read_word(window_ptr + 8) as i16,
3368 bus.read_word(window_ptr + 10) as i16,
3369 )
3370 };
3371 let wind_top = pmap_top.wrapping_neg();
3376 let wind_left = pmap_left.wrapping_neg();
3377 let port_bottom = bus.read_word(window_ptr + 20) as i16;
3378 let port_right = bus.read_word(window_ptr + 22) as i16;
3379 let wind_bottom = wind_top.wrapping_add(port_bottom);
3380 let wind_right = wind_left.wrapping_add(port_right);
3381 if wind_bottom <= wind_top || wind_right <= wind_left {
3382 return;
3383 }
3384 let saved_bounds = self.window_bounds;
3385 let saved_title = self.window_title.clone();
3386 let saved_go_away = self.go_away_flag;
3387 let saved_proc = self.window_proc_id;
3388 self.window_bounds = (wind_top, wind_left, wind_bottom, wind_right);
3389 let title_h = bus.read_long(window_ptr + 134u32);
3390 self.window_title = if title_h != 0 {
3391 let title_p = bus.read_long(title_h);
3392 if title_p != 0 {
3393 String::from_utf8_lossy(&bus.read_pstring(title_p)).into_owned()
3394 } else {
3395 String::new()
3396 }
3397 } else {
3398 String::new()
3399 };
3400 self.go_away_flag = bus.read_byte(window_ptr + 112u32) != 0;
3401 self.window_proc_id = proc_id;
3402 if matches!(proc_id, 1..=3) {
3403 self.draw_window_frame(bus);
3404 } else {
3405 self.draw_window_chrome(bus, hilited);
3406 }
3407 self.window_bounds = saved_bounds;
3408 self.window_title = saved_title;
3409 self.go_away_flag = saved_go_away;
3410 self.window_proc_id = saved_proc;
3411 }
3412
3413 pub(crate) fn draw_window_frame(&self, bus: &mut MacMemoryBus) {
3414 let (wind_top, wind_left, wind_bottom, wind_right) = self.window_bounds;
3415 if matches!(self.window_proc_id, 1 | 2 | 3 | 5) {
3416 let frame = match self.window_proc_id {
3417 1 => (wind_top - 8, wind_left - 8, wind_bottom + 3, wind_right + 8),
3418 2 => (wind_top - 1, wind_left - 1, wind_bottom + 1, wind_right + 1),
3419 3 => (wind_top - 1, wind_left - 1, wind_bottom + 3, wind_right + 3),
3420 5 => (
3421 wind_top - 23,
3422 wind_left - 8,
3423 wind_bottom + 8,
3424 wind_right + 8,
3425 ),
3426 _ => (wind_top, wind_left, wind_bottom, wind_right),
3427 };
3428 if self.draw_theme_dialog_frame(
3429 bus,
3430 (wind_top, wind_left, wind_bottom, wind_right),
3431 frame,
3432 self.window_proc_id,
3433 true,
3434 false,
3435 ) {
3436 self.capture_gui_frame(bus, "draw_window_frame");
3437 return;
3438 }
3439 }
3440
3441 match self.window_proc_id {
3442 2 => {
3443 self.draw_rect_border(
3449 bus,
3450 wind_top - 1,
3451 wind_left - 1,
3452 wind_bottom + 1,
3453 wind_right + 1,
3454 );
3455 }
3456 1 => {
3457 let struc_top = wind_top - 8;
3464 let struc_left = wind_left - 8;
3465 let struc_bottom = wind_bottom + 3;
3466 let struc_right = wind_right + 8;
3467 self.erase_structure_frame_around_content(
3468 bus,
3469 (struc_top, struc_left, struc_bottom, struc_right),
3470 (wind_top, wind_left, wind_bottom, wind_right),
3471 );
3472 self.draw_rect_border(bus, struc_top, struc_left, struc_bottom, struc_right);
3474 self.draw_thick_rect_border(
3476 bus,
3477 wind_top - 5,
3478 wind_left - 5,
3479 struc_bottom,
3480 wind_right + 5,
3481 );
3482 }
3483 3 => {
3484 self.erase_structure_frame_around_content(
3486 bus,
3487 (wind_top - 1, wind_left - 1, wind_bottom + 3, wind_right + 3),
3488 (wind_top, wind_left, wind_bottom, wind_right),
3489 );
3490 self.draw_rect_border(
3491 bus,
3492 wind_top - 1,
3493 wind_left - 1,
3494 wind_bottom + 1,
3495 wind_right + 1,
3496 );
3497 self.draw_shadow(
3499 bus,
3500 wind_top - 1,
3501 wind_left - 1,
3502 wind_bottom + 1,
3503 wind_right + 1,
3504 );
3505 }
3506 5 => {
3507 let struc_top = wind_top - 23;
3512 let struc_left = wind_left - 8;
3513 let struc_bottom = wind_bottom + 8;
3514 let struc_right = wind_right + 8;
3515 self.erase_structure_frame_around_content(
3516 bus,
3517 (struc_top, struc_left, struc_bottom, struc_right),
3518 (wind_top, wind_left, wind_bottom, wind_right),
3519 );
3520 self.draw_rect_border(bus, struc_top, struc_left, struc_bottom, struc_right);
3522 let thick_top = wind_top - 5;
3524 let thick_left = wind_left - 5;
3525 let thick_bottom = wind_bottom + 5;
3526 let thick_right = wind_right + 5;
3527 self.draw_thick_rect_border(bus, thick_top, thick_left, thick_bottom, thick_right);
3528 let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
3530 self.get_screen_params();
3531 Self::fb_hline(
3532 bus,
3533 screen_base,
3534 row_bytes,
3535 pixel_size,
3536 screen_width,
3537 screen_height,
3538 thick_bottom - 2,
3539 thick_left,
3540 thick_right,
3541 true,
3542 );
3543 }
3544 proc_id if Self::window_is_document_proc(proc_id) => {
3545 let tb_top = wind_top - 19;
3548 self.erase_structure_region(
3549 bus,
3550 tb_top,
3551 wind_left - 1,
3552 wind_bottom + 2,
3553 wind_right + 2,
3554 );
3555 self.draw_window_chrome(bus, true);
3556 }
3557 _ => {
3558 self.draw_rect_border(
3560 bus,
3561 wind_top - 1,
3562 wind_left - 1,
3563 wind_bottom + 1,
3564 wind_right + 1,
3565 );
3566 }
3567 }
3568 self.capture_gui_frame(bus, "draw_window_frame");
3569 }
3570
3571 pub(crate) fn restore_visible_dialog_snapshots(&mut self, bus: &mut MacMemoryBus) {
3572 if self.dialog_visible_snapshots.is_empty() {
3573 return;
3574 }
3575 let front_window = self.front_window;
3576 let front_is_dialog = front_window != 0 && self.dialog_items.contains_key(&front_window);
3577 let snapshots: Vec<_> = self
3578 .dialog_visible_snapshots
3579 .iter()
3580 .filter_map(|(&dialog_ptr, snapshot)| {
3581 if front_is_dialog && dialog_ptr != front_window {
3582 None
3583 } else {
3584 Some(snapshot.clone())
3585 }
3586 })
3587 .collect();
3588 for snapshot in snapshots {
3589 let bounds = snapshot.bounds;
3590 self.restore_dialog_pixels(bus, bounds, &snapshot.pixels);
3591 }
3592 }
3593
3594 pub fn redraw_chrome(&mut self, bus: &mut MacMemoryBus) {
3601 let pending_dialog_snapshot = self
3612 .dialog_tracking
3613 .as_ref()
3614 .map(|tracking| !tracking.game_managed && !tracking.rendered_pixels_final)
3615 .unwrap_or(false);
3616 if !pending_dialog_snapshot {
3617 self.blit_window_to_screen(bus);
3618 self.blit_large_manual_cport_to_screen(bus);
3619 }
3620
3621 let menu_bar_height = bus.read_word(crate::memory::globals::addr::MBAR_HEIGHT) as i16;
3622 let (_, _, screen_w, screen_h, _) = self.screen_mode;
3623
3624 let (wt, wl, wb, wr) = self.window_bounds;
3630 if self.fullscreen_locked && menu_bar_height > 0 {
3631 self.fullscreen_locked = false;
3632 }
3633
3634 if self.front_window != 0
3635 && wt <= 0
3636 && wl <= 0
3637 && wb >= screen_h as i16
3638 && wr >= screen_w as i16
3639 && menu_bar_height <= 0
3640 {
3641 self.fullscreen_locked = true;
3642 }
3643
3644 self.restore_kiosk_dialog_desktop_background(bus);
3645
3646 if !self.menus.is_empty() && !self.fullscreen_locked && !self.menu_bar_hidden {
3647 self.draw_menu_bar_to_fb(bus);
3648 }
3649 let effective_mbar = if self.fullscreen_locked || self.menu_bar_hidden {
3670 0
3671 } else {
3672 menu_bar_height
3673 };
3674 if self.front_window != 0 && !no_visrgn_auto_expand_enabled() && effective_mbar == 0 {
3680 let vis_top_expected = 0i16;
3681 let vis_rgn_handle = bus.read_long(self.front_window + 24);
3682 if vis_rgn_handle != 0 {
3683 let vis_rgn = bus.read_long(vis_rgn_handle);
3684 if vis_rgn != 0 {
3685 let current_vis_top = bus.read_word(vis_rgn + 2) as i16;
3686 if current_vis_top != vis_top_expected {
3687 bus.write_word(vis_rgn + 2, vis_top_expected as u16);
3688 let clip_rgn_handle = bus.read_long(self.front_window + 28);
3690 if clip_rgn_handle != 0 {
3691 let clip_rgn = bus.read_long(clip_rgn_handle);
3692 if clip_rgn != 0 {
3693 let clip_top = bus.read_word(clip_rgn + 2) as i16;
3694 if clip_top > vis_top_expected {
3695 bus.write_word(clip_rgn + 2, vis_top_expected as u16);
3696 }
3697 }
3698 }
3699 let port_top = bus.read_word(self.front_window + 16) as i16;
3700 if port_top > vis_top_expected {
3701 bus.write_word(self.front_window + 16, vis_top_expected as u16);
3702 }
3703 self.window_bounds.0 = vis_top_expected;
3704 }
3705 }
3706 }
3707 }
3708
3709 let hidden_menu_fullscreen_top = if menu_bar_height > 0 {
3710 menu_bar_height.saturating_add(2)
3711 } else {
3712 22
3713 };
3714 let hidden_menu_fullscreen_window = self.menu_bar_hidden
3715 && wt <= hidden_menu_fullscreen_top
3716 && wl <= 2
3717 && wb >= screen_h as i16 - 2
3718 && wr >= screen_w as i16 - 2;
3719 let skip_chrome = matches!(self.window_proc_id, 1 | 2 | 3 | 5)
3720 || self.fullscreen_locked
3721 || menu_bar_height <= 0
3722 || hidden_menu_fullscreen_window;
3723
3724 if !skip_chrome && menu_bar_height > 0 {
3730 let list_snapshot = self.window_list.clone();
3731 let saved_bounds = self.window_bounds;
3732 let saved_title = self.window_title.clone();
3733 let saved_proc = self.window_proc_id;
3734 let saved_go_away = self.go_away_flag;
3735 for &w in list_snapshot.iter().rev() {
3738 if w == self.front_window {
3739 continue;
3740 }
3741 if bus.read_byte(w + 110u32) == 0 {
3742 continue;
3744 }
3745 let preserved_front_pixels: Vec<_> = self
3746 .window_structure_rect(bus, w)
3747 .map(|back_structure| {
3748 list_snapshot
3749 .iter()
3750 .take_while(|&&front_window| front_window != w)
3751 .filter(|&&front_window| bus.read_byte(front_window + 110u32) != 0)
3752 .filter_map(|&front_window| {
3753 self.window_structure_rect(bus, front_window)
3754 .and_then(|front_structure| {
3755 Self::rect_intersection(back_structure, front_structure)
3756 })
3757 .and_then(|rect| self.save_screen_rect_pixels(bus, rect))
3758 })
3759 .collect()
3760 })
3761 .unwrap_or_default();
3762 let port_version = bus.read_word(w + 6);
3769 let (pmap_top, pmap_left) = if (port_version & 0xC000) == 0xC000 {
3770 let pm_handle = bus.read_long(w + 2);
3771 let pm_ptr = bus.read_long(pm_handle);
3772 (
3773 bus.read_word(pm_ptr + 6) as i16,
3774 bus.read_word(pm_ptr + 8) as i16,
3775 )
3776 } else {
3777 (bus.read_word(w + 8) as i16, bus.read_word(w + 10) as i16)
3778 };
3779 let wind_top = -pmap_top;
3780 let wind_left = -pmap_left;
3781 let port_bottom = bus.read_word(w + 20) as i16;
3782 let port_right = bus.read_word(w + 22) as i16;
3783 let wind_bottom = wind_top + port_bottom;
3784 let wind_right = wind_left + port_right;
3785 if wind_bottom <= wind_top || wind_right <= wind_left {
3787 continue;
3788 }
3789 self.window_bounds = (wind_top, wind_left, wind_bottom, wind_right);
3790 let title_h = bus.read_long(w + 134u32);
3792 self.window_title = if title_h != 0 {
3793 let title_p = bus.read_long(title_h);
3794 if title_p != 0 {
3795 String::from_utf8_lossy(&bus.read_pstring(title_p)).into_owned()
3796 } else {
3797 String::new()
3798 }
3799 } else {
3800 String::new()
3801 };
3802 self.go_away_flag = bus.read_byte(w + 112u32) != 0;
3803 let w_proc = self.window_proc_ids.get(&w).copied().unwrap_or(0);
3806 if self.window_uses_custom_def_proc(bus, w) {
3807 continue;
3808 }
3809 self.window_proc_id = w_proc;
3810 let hilited = bus.read_byte(w + 111u32) != 0;
3811 if matches!(w_proc, 1..=3) {
3812 self.draw_window_frame(bus);
3813 } else {
3814 self.draw_window_chrome(bus, hilited);
3815 }
3816 for (top, left, width, height, pixels) in preserved_front_pixels {
3817 self.restore_screen_rect_pixels(bus, top, left, width, height, &pixels);
3818 }
3819 }
3820 self.window_bounds = saved_bounds;
3822 self.window_title = saved_title;
3823 self.window_proc_id = saved_proc;
3824 self.go_away_flag = saved_go_away;
3825 }
3826
3827 if self.front_window != 0 && !skip_chrome {
3828 let front_hilited = bus.read_byte(self.front_window + 111u32) != 0;
3831 self.draw_single_window_chrome_inline(bus, self.front_window, front_hilited);
3832 }
3833 if let Some(ref tracking) = self.dialog_tracking {
3842 if !tracking.game_managed && tracking.rendered_pixels_final {
3843 self.restore_dialog_pixels(bus, tracking.bounds, &tracking.rendered_pixels);
3845
3846 if tracking.edit_item > 0 {
3848 let idx = (tracking.edit_item - 1) as usize;
3849 if idx < tracking.items.len() {
3850 let item = &tracking.items[idx];
3851 let abs_top = tracking.bounds.0 + item.rect.0;
3852 let abs_left = tracking.bounds.1 + item.rect.1;
3853 let abs_bottom = tracking.bounds.0 + item.rect.2;
3854 let abs_right = tracking.bounds.1 + item.rect.3;
3855 self.draw_edit_text(
3856 bus,
3857 abs_top,
3858 abs_left,
3859 abs_bottom,
3860 abs_right,
3861 &tracking.edit_text,
3862 !tracking.edit_text_modified,
3863 );
3864 }
3865 }
3866
3867 if tracking.flash_remaining > 0 && tracking.flash_item > 0 {
3869 let fi = tracking.flash_item;
3870 if (fi as usize) <= tracking.items.len() {
3871 let item = &tracking.items[(fi - 1) as usize];
3872 let (it, il, ib, ir) = item.rect;
3873 let abs_top = tracking.bounds.0 + it;
3874 let abs_left = tracking.bounds.1 + il;
3875 let abs_bottom = tracking.bounds.0 + ib;
3876 let abs_right = tracking.bounds.1 + ir;
3877 if tracking.flash_remaining % 2 == 0 {
3878 self.invert_button_rect(bus, abs_top, abs_left, abs_bottom, abs_right);
3879 }
3880 }
3881 }
3882
3883 if let Some(ref popup) = tracking.active_popup {
3884 self.draw_menu_dropdown(bus, popup.active_menu, popup.dropdown_rect);
3885 if popup.highlighted_item > 0 {
3886 self.invert_dropdown_item_rect(
3887 bus,
3888 popup.active_menu,
3889 popup.dropdown_rect,
3890 popup.highlighted_item,
3891 );
3892 }
3893 }
3894 }
3895 } else {
3896 self.restore_visible_dialog_snapshots(bus);
3897 self.redraw_retained_modal_dialog_click(bus);
3898 }
3899
3900 if let Some(ref tracking) = self.menu_tracking {
3903 self.highlight_menu_title(bus, tracking.active_menu);
3904 self.draw_menu_dropdown(bus, tracking.active_menu, tracking.dropdown_rect);
3905 let show_highlight = if tracking.flash_remaining > 0 {
3908 tracking.flash_remaining % 2 == 0
3909 } else {
3910 true
3911 };
3912 if tracking.highlighted_item > 0 && show_highlight {
3913 self.invert_menu_item(bus, tracking.highlighted_item);
3914 }
3915 }
3916 self.capture_gui_frame(bus, "redraw_chrome");
3917 }
3918}
3919
3920#[cfg(test)]
3921mod redraw_chrome_tests {
3922 use super::super::dispatch::DialogItem;
3923 use super::super::test_helpers::setup_with_port;
3924 use super::super::TrapDispatcher;
3925 use crate::memory::MemoryBus;
3926
3927 const PORT_PTR: u32 = 0x181000;
3935 const VIS_RGN: u32 = 0x182000;
3936 const CLIP_RGN: u32 = 0x182200;
3937 const WINDOW_VISIBLE_OFFSET: u32 = 110;
3938
3939 #[test]
3940 fn fb_fill_pattern_rect_tiles_standard_gray_pattern() {
3941 let (mut disp, _cpu, mut bus) = setup_with_port();
3942 let screen_base = bus.alloc(8 * 8);
3943 disp.screen_mode = (screen_base, 8, 8, 8, 8);
3944
3945 TrapDispatcher::fb_fill_pattern_rect(
3946 &mut bus,
3947 screen_base,
3948 8,
3949 8,
3950 8,
3951 8,
3952 0,
3953 0,
3954 8,
3955 8,
3956 [0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55],
3957 );
3958
3959 assert_eq!(bus.read_byte(screen_base), 255);
3960 assert_eq!(bus.read_byte(screen_base + 1), 0);
3961 assert_eq!(bus.read_byte(screen_base + 8), 0);
3962 assert_eq!(bus.read_byte(screen_base + 9), 255);
3963 }
3964
3965 fn install_twilight_style_black_index(
3966 disp: &mut TrapDispatcher,
3967 bus: &mut crate::memory::MacMemoryBus,
3968 ) {
3969 let gdevice_handle = disp.ensure_main_gdevice(bus);
3970 bus.write_long(0x08A4, gdevice_handle);
3971 bus.write_long(0x0CC8, gdevice_handle);
3972
3973 let gdevice = bus.read_long(gdevice_handle);
3974 let pixmap_handle = bus.read_long(gdevice + 22);
3975 let pixmap = bus.read_long(pixmap_handle);
3976 let ctab_handle = bus.read_long(pixmap + 42);
3977 let ctab = bus.read_long(ctab_handle);
3978
3979 let black_entry = ctab + 8 + 8;
3980 bus.write_word(black_entry, 1);
3981 bus.write_word(black_entry + 2, 0);
3982 bus.write_word(black_entry + 4, 0);
3983 bus.write_word(black_entry + 6, 0);
3984
3985 let repurposed_entry = ctab + 8 + 255 * 8;
3986 bus.write_word(repurposed_entry, 255);
3987 bus.write_word(repurposed_entry + 2, 0xFFFF);
3988 bus.write_word(repurposed_entry + 4, 0xFFFF);
3989 bus.write_word(repurposed_entry + 6, 0xCCCC);
3990 }
3991
3992 fn make_ctab_handle(
3993 bus: &mut crate::memory::MacMemoryBus,
3994 clut: &[[u16; 3]; 256],
3995 seed: u32,
3996 ) -> u32 {
3997 let ctab = bus.alloc(8 + 256 * 8);
3998 bus.write_long(ctab, seed);
3999 bus.write_word(ctab + 4, 0x8000);
4000 bus.write_word(ctab + 6, 255);
4001 for index in 0u32..256 {
4002 let entry = ctab + 8 + index * 8;
4003 let [r, g, b] = clut[index as usize];
4004 bus.write_word(entry, index as u16);
4005 bus.write_word(entry + 2, r);
4006 bus.write_word(entry + 4, g);
4007 bus.write_word(entry + 6, b);
4008 }
4009 let handle = bus.alloc(4);
4010 bus.write_long(handle, ctab);
4011 handle
4012 }
4013
4014 fn install_8bpp_cgrafport(
4015 bus: &mut crate::memory::MacMemoryBus,
4016 base: u32,
4017 row_bytes: u16,
4018 width: u16,
4019 height: u16,
4020 ctab_handle: u32,
4021 ) -> u32 {
4022 let pixmap = bus.alloc(50);
4023 bus.write_long(pixmap, base);
4024 bus.write_word(pixmap + 4, row_bytes | 0x8000);
4025 bus.write_word(pixmap + 6, 0);
4026 bus.write_word(pixmap + 8, 0);
4027 bus.write_word(pixmap + 10, height);
4028 bus.write_word(pixmap + 12, width);
4029 bus.write_word(pixmap + 14, 0);
4030 bus.write_word(pixmap + 16, 0);
4031 bus.write_long(pixmap + 18, 0);
4032 bus.write_long(pixmap + 22, 0x0048_0000);
4033 bus.write_long(pixmap + 26, 0x0048_0000);
4034 bus.write_word(pixmap + 30, 0);
4035 bus.write_word(pixmap + 32, 8);
4036 bus.write_word(pixmap + 34, 1);
4037 bus.write_word(pixmap + 36, 8);
4038 bus.write_long(pixmap + 38, 0);
4039 bus.write_long(pixmap + 42, ctab_handle);
4040 bus.write_long(pixmap + 46, 0);
4041
4042 let pixmap_handle = bus.alloc(4);
4043 bus.write_long(pixmap_handle, pixmap);
4044 bus.write_long(PORT_PTR + 2, pixmap_handle);
4045 bus.write_word(PORT_PTR + 6, 0xC000);
4046 bus.write_word(PORT_PTR + 16, 0);
4047 bus.write_word(PORT_PTR + 18, 0);
4048 bus.write_word(PORT_PTR + 20, height);
4049 bus.write_word(PORT_PTR + 22, width);
4050 pixmap_handle
4051 }
4052
4053 fn install_8bpp_cgrafport_at(
4054 bus: &mut crate::memory::MacMemoryBus,
4055 port: u32,
4056 base: u32,
4057 row_bytes: u16,
4058 width: u16,
4059 height: u16,
4060 ctab_handle: u32,
4061 ) -> u32 {
4062 let pixmap = bus.alloc(50);
4063 bus.write_long(pixmap, base);
4064 bus.write_word(pixmap + 4, row_bytes | 0x8000);
4065 bus.write_word(pixmap + 6, 0);
4066 bus.write_word(pixmap + 8, 0);
4067 bus.write_word(pixmap + 10, height);
4068 bus.write_word(pixmap + 12, width);
4069 bus.write_long(pixmap + 22, 0x0048_0000);
4070 bus.write_long(pixmap + 26, 0x0048_0000);
4071 bus.write_word(pixmap + 32, 8);
4072 bus.write_word(pixmap + 34, 1);
4073 bus.write_word(pixmap + 36, 8);
4074 bus.write_long(pixmap + 42, ctab_handle);
4075
4076 let pixmap_handle = bus.alloc(4);
4077 bus.write_long(pixmap_handle, pixmap);
4078 bus.write_long(port + 2, pixmap_handle);
4079 bus.write_word(port + 6, 0xC000);
4080 bus.write_word(port + 16, 0);
4081 bus.write_word(port + 18, 0);
4082 bus.write_word(port + 20, height);
4083 bus.write_word(port + 22, width);
4084 pixmap_handle
4085 }
4086
4087 #[test]
4091 fn redraw_chrome_preserves_window_bounds_when_menu_bar_visible() {
4092 let (mut disp, _cpu, mut bus) = setup_with_port();
4093
4094 bus.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 20);
4097 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;
4101 disp.window_bounds = (40, 2, 339, 508);
4102 disp.window_proc_id = 0; disp.fullscreen_locked = false;
4104 disp.menu_bar_hidden = false;
4107
4108 disp.redraw_chrome(&mut bus);
4109
4110 assert_eq!(
4111 disp.window_bounds.0, 40,
4112 "window_bounds.0 must not be clobbered when MBarHeight>0"
4113 );
4114 assert_eq!(
4115 bus.read_word(VIS_RGN + 2) as i16,
4116 40,
4117 "visRgn.top must not be rewritten when MBarHeight>0"
4118 );
4119 assert_eq!(
4120 bus.read_word(PORT_PTR + 16) as i16,
4121 40,
4122 "port_top must not be rewritten when MBarHeight>0"
4123 );
4124 }
4125
4126 #[test]
4131 fn redraw_chrome_expands_visrgn_when_menu_bar_hidden() {
4132 let (mut disp, _cpu, mut bus) = setup_with_port();
4133
4134 bus.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 0);
4137 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;
4141 disp.window_bounds = (20, 0, 342, 512);
4142 disp.window_proc_id = 0; disp.fullscreen_locked = false;
4144
4145 disp.redraw_chrome(&mut bus);
4146
4147 assert_eq!(
4148 bus.read_word(VIS_RGN + 2) as i16,
4149 0,
4150 "visRgn.top must be expanded to 0 when MBarHeight=0"
4151 );
4152 assert_eq!(
4153 disp.window_bounds.0, 0,
4154 "window_bounds.0 must be updated to match expanded visRgn"
4155 );
4156 assert_eq!(
4157 bus.read_word(PORT_PTR + 16) as i16,
4158 0,
4159 "port_top must be updated to match expanded visRgn"
4160 );
4161 }
4162
4163 #[test]
4170 fn redraw_chrome_expands_visrgn_when_host_hides_menu_bar() {
4171 let (mut disp, _cpu, mut bus) = setup_with_port();
4172
4173 bus.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 20);
4177 bus.write_word(VIS_RGN + 2, 20); bus.write_word(CLIP_RGN + 2, 20);
4179 bus.write_word(PORT_PTR + 16, 20);
4180 disp.front_window = PORT_PTR;
4181 disp.window_bounds = (20, 0, 342, 512);
4182 disp.window_proc_id = 0; disp.fullscreen_locked = false;
4184 disp.menu_bar_hidden = true;
4185
4186 disp.redraw_chrome(&mut bus);
4187
4188 assert_eq!(
4189 bus.read_word(VIS_RGN + 2) as i16,
4190 0,
4191 "visRgn.top must be expanded to 0 when host hides the menu bar"
4192 );
4193 assert_eq!(
4194 disp.window_bounds.0, 0,
4195 "window_bounds.0 must follow visRgn.top up to 0"
4196 );
4197 assert_eq!(
4198 bus.read_word(PORT_PTR + 16) as i16,
4199 0,
4200 "port_top must follow visRgn.top up to 0"
4201 );
4202 }
4203
4204 #[test]
4211 fn redraw_chrome_does_not_paint_menu_bar_when_menu_bar_hidden() {
4212 let (mut disp, _cpu, mut bus) = setup_with_port();
4213
4214 let screen_base = bus.alloc(800 * 600);
4218 disp.screen_mode = (screen_base, 800, 800, 600, 8);
4219 bus.write_long(0x0824, screen_base);
4220
4221 for i in 0u32..(800 * 20) {
4224 bus.write_byte(screen_base + i, 0xAA);
4225 }
4226
4227 bus.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 20);
4230 disp.menus.push(super::super::menu::Menu {
4231 id: 1,
4232 title: String::from("Apple"),
4233 items: Vec::new(),
4234 enabled: true,
4235 handle: 0,
4236 in_menu_bar: true,
4237 hierarchical: false,
4238 visible_in_menu_bar: true,
4239 });
4240 disp.front_window = 0;
4244 disp.fullscreen_locked = false;
4245 disp.menu_bar_hidden = true;
4246
4247 disp.redraw_chrome(&mut bus);
4248
4249 for &x in &[0u32, 100, 400, 799] {
4253 for &y in &[0u32, 5, 10, 19] {
4254 let off = y * 800 + x;
4255 assert_eq!(
4256 bus.read_byte(screen_base + off),
4257 0xAA,
4258 "menu bar pixel at (x={}, y={}) should not be repainted \
4259 when menu_bar_hidden=true",
4260 x,
4261 y
4262 );
4263 }
4264 }
4265 }
4266
4267 #[test]
4268 fn redraw_chrome_repaints_document_window_when_host_hides_menu_bar() {
4269 let (mut disp, mut cpu, mut bus) = super::super::test_helpers::setup();
4270 let screen_base = bus.alloc(800 * 600);
4271 for offset in 0..800 * 600 {
4272 bus.write_byte(screen_base + offset, 0xAA);
4273 }
4274 disp.screen_mode = (screen_base, 800, 800, 600, 8);
4275 bus.write_long(crate::memory::globals::addr::SCREEN_BITS, screen_base);
4276 bus.write_long(crate::memory::globals::addr::SCRN_BASE, screen_base);
4277 bus.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 20);
4278 disp.menu_bar_hidden = true;
4279
4280 let window_addr = bus.alloc(256);
4281 disp.init_cgraf_window(
4282 &mut bus,
4283 &mut cpu,
4284 window_addr,
4285 screen_base,
4286 60,
4287 30,
4288 220,
4289 370,
4290 "Zoom",
4291 8,
4292 true,
4293 true,
4294 true,
4295 0,
4296 );
4297
4298 for y in 0..20u32 {
4299 for x in 0..800u32 {
4300 bus.write_byte(screen_base + y * 800 + x, 0xAA);
4301 }
4302 }
4303 for y in 41..59u32 {
4304 for x in 29..371u32 {
4305 bus.write_byte(screen_base + y * 800 + x, 0xAA);
4306 }
4307 }
4308
4309 disp.redraw_chrome(&mut bus);
4310
4311 assert_eq!(
4312 bus.read_byte(screen_base + 10 * 800 + 100),
4313 0xAA,
4314 "hidden host menu bar should not be repainted"
4315 );
4316 assert_ne!(
4317 bus.read_byte(screen_base + 45 * 800 + 50),
4318 0xAA,
4319 "non-fullscreen zoom document windows should redraw titlebar chrome"
4320 );
4321 }
4322
4323 #[test]
4328 fn redraw_chrome_paints_menu_bar_when_not_hidden() {
4329 let (mut disp, _cpu, mut bus) = setup_with_port();
4330
4331 let screen_base = bus.alloc(800 * 600);
4332 disp.screen_mode = (screen_base, 800, 800, 600, 8);
4333 bus.write_long(0x0824, screen_base);
4334
4335 for i in 0u32..(800 * 20) {
4337 bus.write_byte(screen_base + i, 0xAA);
4338 }
4339
4340 bus.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 20);
4341 disp.menus.push(super::super::menu::Menu {
4342 id: 1,
4343 title: String::from("Apple"),
4344 items: Vec::new(),
4345 enabled: true,
4346 handle: 0,
4347 in_menu_bar: true,
4348 hierarchical: false,
4349 visible_in_menu_bar: true,
4350 });
4351 disp.front_window = 0;
4352 disp.fullscreen_locked = false;
4353 disp.menu_bar_hidden = false;
4354
4355 disp.redraw_chrome(&mut bus);
4356
4357 for &x in &[100u32, 400, 700] {
4363 for &y in &[5u32, 10] {
4364 let off = y * 800 + x;
4365 let pix = bus.read_byte(screen_base + off);
4366 assert_ne!(
4367 pix, 0xAA,
4368 "menu bar pixel at (x={}, y={}) should be painted \
4369 when menu_bar_hidden=false (read 0x{:02X}, sentinel 0xAA)",
4370 x, y, pix
4371 );
4372 }
4373 }
4374 }
4375
4376 #[test]
4377 fn redraw_chrome_restores_desktop_pattern_behind_single_kiosk_dialog() {
4378 let (mut disp, _cpu, mut bus) = setup_with_port();
4379 let (screen_base, row_bytes, screen_w, screen_h, _) = disp.screen_mode;
4380 bus.fill_bytes(screen_base, row_bytes * screen_h as u32, 0xFF);
4381 bus.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 20);
4382
4383 disp.menu_bar_hidden = true;
4384 disp.fullscreen_locked = false;
4385 disp.front_window = PORT_PTR;
4386 disp.window_list = vec![PORT_PTR];
4387 disp.window_bounds = (100, 180, 208, 620);
4388 disp.window_proc_id = 5;
4389 disp.window_proc_ids.insert(PORT_PTR, 5);
4390 disp.dialog_items
4391 .insert(PORT_PTR, vec![DialogItem::default()]);
4392 bus.write_byte(PORT_PTR + WINDOW_VISIBLE_OFFSET, 1);
4393
4394 disp.redraw_chrome(&mut bus);
4395
4396 assert_eq!(
4397 bus.read_byte(screen_base),
4398 0xFF,
4399 "standard gray pattern starts with a black pixel"
4400 );
4401 assert_eq!(
4402 bus.read_byte(screen_base + 1),
4403 0x00,
4404 "standard gray pattern should add white desktop pixels"
4405 );
4406 assert_eq!(
4407 bus.read_byte(screen_base + 120 * row_bytes + 200),
4408 0xFF,
4409 "dialog bounds must not be overwritten by the desktop fill"
4410 );
4411 assert_eq!(screen_w, 800, "test assumes the default 800-wide screen");
4412 }
4413
4414 #[test]
4415 fn redraw_chrome_keeps_existing_content_behind_kiosk_dialog() {
4416 let (mut disp, _cpu, mut bus) = setup_with_port();
4417 let (screen_base, row_bytes, _screen_w, screen_h, _) = disp.screen_mode;
4418 bus.fill_bytes(screen_base, row_bytes * screen_h as u32, 0xFF);
4419 bus.write_byte(screen_base, 0x00);
4420 bus.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 20);
4421
4422 disp.menu_bar_hidden = true;
4423 disp.fullscreen_locked = false;
4424 disp.front_window = PORT_PTR;
4425 disp.window_list = vec![PORT_PTR];
4426 disp.window_bounds = (100, 180, 208, 620);
4427 disp.window_proc_id = 5;
4428 disp.window_proc_ids.insert(PORT_PTR, 5);
4429 disp.dialog_items
4430 .insert(PORT_PTR, vec![DialogItem::default()]);
4431 bus.write_byte(PORT_PTR + WINDOW_VISIBLE_OFFSET, 1);
4432
4433 disp.redraw_chrome(&mut bus);
4434
4435 assert_eq!(
4436 bus.read_byte(screen_base + 16),
4437 0xFF,
4438 "nonblack exposed content should suppress the desktop-pattern fill"
4439 );
4440 }
4441
4442 #[test]
4452 fn redraw_chrome_does_not_paint_on_mouse_at_top_when_hidden() {
4453 let (mut disp, _cpu, mut bus) = setup_with_port();
4454
4455 let screen_base = bus.alloc(800 * 600);
4456 disp.screen_mode = (screen_base, 800, 800, 600, 8);
4457 bus.write_long(0x0824, screen_base);
4458
4459 for i in 0u32..(800 * 20) {
4461 bus.write_byte(screen_base + i, 0xAA);
4462 }
4463
4464 bus.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 20);
4465 disp.menus.push(super::super::menu::Menu {
4466 id: 1,
4467 title: String::from("Apple"),
4468 items: Vec::new(),
4469 enabled: true,
4470 handle: 0,
4471 in_menu_bar: true,
4472 hierarchical: false,
4473 visible_in_menu_bar: true,
4474 });
4475 disp.front_window = 0;
4476 disp.fullscreen_locked = false;
4477 disp.menu_bar_hidden = true;
4478
4479 disp.redraw_chrome(&mut bus);
4481 for x in [0u32, 100, 400, 799] {
4482 for y in [0u32, 5, 10, 19] {
4483 assert_eq!(
4484 bus.read_byte(screen_base + y * 800 + x),
4485 0xAA,
4486 "initial frame: sentinel must hold at (x={}, y={})",
4487 x,
4488 y
4489 );
4490 }
4491 }
4492
4493 disp.mouse_pos = (2, 400);
4497 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);
4508 for x in [0u32, 100, 400, 799] {
4509 for y in [0u32, 5, 10, 19] {
4510 assert_eq!(
4511 bus.read_byte(screen_base + y * 800 + x),
4512 0xAA,
4513 "after mouse-at-top: sentinel must still hold at (x={}, y={})",
4514 x,
4515 y
4516 );
4517 }
4518 }
4519 }
4520
4521 #[test]
4522 fn redraw_chrome_skips_window_blit_while_dialog_snapshot_pending() {
4523 let (mut disp, _cpu, mut bus) = setup_with_port();
4524
4525 let screen_base = bus.alloc(800 * 600);
4526 disp.screen_mode = (screen_base, 800, 800, 600, 8);
4527 bus.write_long(0x0824, screen_base);
4528
4529 let offscreen_base = bus.alloc(64 * 200);
4530 bus.write_long(PORT_PTR + 2, offscreen_base);
4531 bus.write_word(PORT_PTR + 6, 64);
4532 bus.write_word(PORT_PTR + 8, 0);
4533 bus.write_word(PORT_PTR + 10, 0);
4534 bus.write_word(PORT_PTR + 12, 200);
4535 bus.write_word(PORT_PTR + 14, 512);
4536
4537 let probe = screen_base + 10 * 800 + 10;
4538 bus.write_byte(probe, 0x42);
4539 bus.write_byte(offscreen_base + 10 * 64 + 1, 0x00);
4540
4541 bus.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 20);
4542 disp.front_window = PORT_PTR;
4543 disp.window_bounds = (0, 0, 200, 512);
4544 disp.window_proc_id = 1;
4545 disp.dialog_tracking = Some(super::super::dispatch::DialogTrackingState {
4546 game_managed: false,
4547 rendered_pixels_final: false,
4548 ..Default::default()
4549 });
4550
4551 disp.redraw_chrome(&mut bus);
4552
4553 assert_eq!(
4554 bus.read_byte(probe),
4555 0x42,
4556 "pending ModalDialog snapshots must not be erased by blank offscreen port blits"
4557 );
4558
4559 if let Some(tracking) = disp.dialog_tracking.as_mut() {
4560 tracking.rendered_pixels_final = true;
4561 }
4562 disp.redraw_chrome(&mut bus);
4563
4564 assert_eq!(
4565 bus.read_byte(probe),
4566 0x00,
4567 "once the dialog snapshot is final, normal port blitting should resume"
4568 );
4569 }
4570
4571 #[test]
4572 fn redraw_chrome_does_not_restore_stale_dialog_pixels_while_filter_snapshot_pending() {
4573 let (mut disp, _cpu, mut bus) = setup_with_port();
4574
4575 let screen_base = bus.alloc(64 * 64);
4576 for i in 0..64u32 * 64 {
4577 bus.write_byte(screen_base + i, 0x00);
4578 }
4579 disp.screen_mode = (screen_base, 64, 64, 64, 8);
4580 bus.write_long(0x0824, screen_base);
4581 bus.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 0);
4582
4583 let bounds = (10, 10, 30, 30);
4584 let stale_pixels = disp.save_dialog_pixels(&bus, bounds);
4585 let probe = screen_base + 18 * 64 + 18;
4586 bus.write_byte(probe, 0x44);
4587
4588 disp.dialog_tracking = Some(super::super::dispatch::DialogTrackingState {
4589 bounds,
4590 game_managed: false,
4591 draw_procs_done: true,
4592 rendered_pixels: stale_pixels,
4593 rendered_pixels_final: false,
4594 ..Default::default()
4595 });
4596
4597 disp.redraw_chrome(&mut bus);
4598
4599 assert_eq!(
4600 bus.read_byte(probe),
4601 0x44,
4602 "non-final ModalDialog snapshots must not overwrite live filter/userItem drawing"
4603 );
4604
4605 if let Some(tracking) = disp.dialog_tracking.as_mut() {
4606 tracking.rendered_pixels_final = true;
4607 }
4608 disp.redraw_chrome(&mut bus);
4609
4610 assert_eq!(
4611 bus.read_byte(probe),
4612 0x00,
4613 "final ModalDialog snapshots should still be restored normally"
4614 );
4615 }
4616
4617 #[test]
4618 fn redraw_chrome_restores_visible_dialog_snapshot_after_modaldialog_returns() {
4619 let (mut disp, _cpu, mut bus) = setup_with_port();
4620
4621 let screen_base = bus.alloc(800 * 600);
4622 disp.screen_mode = (screen_base, 800, 800, 600, 8);
4623 bus.write_long(0x0824, screen_base);
4624
4625 let offscreen_base = bus.alloc(64 * 200);
4626 bus.write_long(PORT_PTR + 2, offscreen_base);
4627 bus.write_word(PORT_PTR + 6, 64);
4628 bus.write_word(PORT_PTR + 8, 0);
4629 bus.write_word(PORT_PTR + 10, 0);
4630 bus.write_word(PORT_PTR + 12, 200);
4631 bus.write_word(PORT_PTR + 14, 512);
4632
4633 let probe = screen_base + 10 * 800 + 10;
4634 bus.write_byte(probe, 0x11);
4635 bus.write_byte(offscreen_base + 10 * 64 + 1, 0x00);
4636
4637 bus.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 20);
4638 disp.front_window = PORT_PTR;
4639 disp.window_bounds = (0, 0, 200, 512);
4640 disp.window_proc_id = 1;
4641 for y in 0..20u32 {
4642 for x in 0..20u32 {
4643 bus.write_byte(screen_base + y * 800 + x, 0x77);
4644 }
4645 }
4646 let pixels = disp.save_dialog_pixels(&bus, (5, 5, 15, 15));
4647 for y in 0..20u32 {
4648 for x in 0..20u32 {
4649 bus.write_byte(screen_base + y * 800 + x, 0x11);
4650 }
4651 }
4652 let dialog_ptr = 0x00D1_A106;
4653 disp.dialog_visible_snapshots.insert(
4654 dialog_ptr,
4655 super::super::dispatch::PersistentDialogSnapshot {
4656 bounds: (5, 5, 15, 15),
4657 pixels,
4658 },
4659 );
4660
4661 disp.redraw_chrome(&mut bus);
4662
4663 assert_eq!(
4664 bus.read_byte(probe),
4665 0x77,
4666 "visible dialog snapshot should remain composited even when the front port changes"
4667 );
4668 }
4669
4670 #[test]
4671 fn restore_visible_dialog_snapshots_skips_parent_when_child_dialog_is_front() {
4672 let (mut disp, _cpu, mut bus) = setup_with_port();
4673
4674 let screen_base = bus.alloc(800 * 600);
4675 disp.screen_mode = (screen_base, 800, 800, 600, 8);
4676 bus.write_long(0x0824, screen_base);
4677
4678 for y in 0..40u32 {
4679 for x in 0..40u32 {
4680 bus.write_byte(screen_base + y * 800 + x, 0x77);
4681 }
4682 }
4683 let parent_pixels = disp.save_dialog_pixels(&bus, (5, 5, 15, 15));
4684 let child_pixels = disp.save_dialog_pixels(&bus, (20, 20, 30, 30));
4685 for y in 0..40u32 {
4686 for x in 0..40u32 {
4687 bus.write_byte(screen_base + y * 800 + x, 0x11);
4688 }
4689 }
4690
4691 let parent_dialog = 0x00D1_A106;
4692 let child_dialog = 0x00D1_A107;
4693 disp.dialog_visible_snapshots.insert(
4694 parent_dialog,
4695 super::super::dispatch::PersistentDialogSnapshot {
4696 bounds: (5, 5, 15, 15),
4697 pixels: parent_pixels,
4698 },
4699 );
4700 disp.dialog_visible_snapshots.insert(
4701 child_dialog,
4702 super::super::dispatch::PersistentDialogSnapshot {
4703 bounds: (20, 20, 30, 30),
4704 pixels: child_pixels,
4705 },
4706 );
4707 disp.front_window = child_dialog;
4708 disp.dialog_items.insert(child_dialog, Vec::new());
4709
4710 disp.restore_visible_dialog_snapshots(&mut bus);
4711
4712 assert_eq!(
4713 bus.read_byte(screen_base + 10 * 800 + 10),
4714 0x11,
4715 "retained parent dialog snapshot must not overpaint the front child dialog"
4716 );
4717 assert_eq!(
4718 bus.read_byte(screen_base + 25 * 800 + 25),
4719 0x77,
4720 "front child dialog snapshot should still be restored"
4721 );
4722 }
4723
4724 #[test]
4725 fn refresh_visible_dialog_snapshot_for_port_captures_later_screen_updates() {
4726 let (mut disp, _cpu, mut bus) = setup_with_port();
4727
4728 let screen_base = bus.alloc(800 * 600);
4729 disp.screen_mode = (screen_base, 800, 800, 600, 8);
4730 bus.write_long(0x0824, screen_base);
4731
4732 for y in 0..40u32 {
4733 for x in 0..40u32 {
4734 bus.write_byte(screen_base + y * 800 + x, 0x11);
4735 }
4736 }
4737
4738 let dialog_ptr = 0x00D1_A106;
4739 let bounds = (10, 10, 20, 20);
4740 let pixels = disp.save_dialog_pixels(&bus, bounds);
4741 disp.dialog_visible_snapshots.insert(
4742 dialog_ptr,
4743 super::super::dispatch::PersistentDialogSnapshot { bounds, pixels },
4744 );
4745
4746 let probe = screen_base + 12 * 800 + 12;
4747 bus.write_byte(probe, 0x77);
4748 disp.refresh_visible_dialog_snapshot_for_port(&bus, dialog_ptr);
4749
4750 bus.write_byte(probe, 0x00);
4751 disp.restore_visible_dialog_snapshots(&mut bus);
4752
4753 assert_eq!(
4754 bus.read_byte(probe),
4755 0x77,
4756 "refresh should retain QuickDraw updates made after ModalDialog returned"
4757 );
4758 }
4759
4760 #[test]
4761 fn fb_fill_rect_uses_active_ctab_brightest_entry_for_white() {
4762 let (mut disp, _cpu, mut bus) = setup_with_port();
4763
4764 let screen_base = bus.alloc(100 * 100);
4765 disp.screen_mode = (screen_base, 100, 100, 100, 8);
4766 bus.write_long(0x0824, screen_base);
4767
4768 let gdevice_handle = disp.ensure_main_gdevice(&mut bus);
4769 bus.write_long(0x08A4, gdevice_handle);
4770 bus.write_long(0x0CC8, gdevice_handle);
4771 let gdevice = bus.read_long(gdevice_handle);
4772 let pixmap_handle = bus.read_long(gdevice + 22);
4773 let pixmap = bus.read_long(pixmap_handle);
4774 let ctab_handle = bus.read_long(pixmap + 42);
4775 let ctab = bus.read_long(ctab_handle);
4776 for index in 0u32..256 {
4777 let entry = ctab + 8 + index * 8;
4778 bus.write_word(entry, index as u16);
4779 bus.write_word(entry + 2, 0);
4780 bus.write_word(entry + 4, 0);
4781 bus.write_word(entry + 6, 0);
4782 }
4783 let white_entry = ctab + 8 + 8;
4784 bus.write_word(white_entry, 1);
4785 bus.write_word(white_entry + 2, 0xFFFF);
4786 bus.write_word(white_entry + 4, 0xFFFF);
4787 bus.write_word(white_entry + 6, 0xFFFF);
4788
4789 TrapDispatcher::fb_fill_rect(
4790 &mut bus,
4791 screen_base,
4792 100,
4793 8,
4794 100,
4795 100,
4796 10,
4797 10,
4798 20,
4799 20,
4800 false,
4801 );
4802
4803 assert_eq!(
4804 bus.read_byte(screen_base + 10 * 100 + 10),
4805 1,
4806 "logical white must follow the active ColorTable, not hard-code CLUT index 0"
4807 );
4808 }
4809
4810 #[test]
4811 fn fb_fill_rect_uses_active_ctab_darkest_entry_for_black() {
4812 let (mut disp, _cpu, mut bus) = setup_with_port();
4813
4814 let screen_base = bus.alloc(100 * 100);
4815 disp.screen_mode = (screen_base, 100, 100, 100, 8);
4816 bus.write_long(0x0824, screen_base);
4817 install_twilight_style_black_index(&mut disp, &mut bus);
4818
4819 TrapDispatcher::fb_fill_rect(
4820 &mut bus,
4821 screen_base,
4822 100,
4823 8,
4824 100,
4825 100,
4826 10,
4827 10,
4828 20,
4829 20,
4830 true,
4831 );
4832
4833 assert_eq!(
4834 bus.read_byte(screen_base + 10 * 100 + 10),
4835 1,
4836 "logical black must follow the active ColorTable when index 255 is not black"
4837 );
4838 }
4839
4840 #[test]
4846 fn redraw_chrome_blit_1bpp_to_8bpp_is_bit_extracted() {
4847 let (mut disp, _cpu, mut bus) = setup_with_port();
4848
4849 let screen_base = bus.alloc(800 * 600);
4853 disp.screen_mode = (screen_base, 800, 800, 600, 8);
4854 bus.write_long(0x0824, screen_base);
4855
4856 let offscreen_base = bus.alloc(64 * 200);
4860 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);
4876
4877 let row30_x96 = screen_base + 30 * 800 + 96;
4880 let row30_x97 = screen_base + 30 * 800 + 97;
4881 bus.write_byte(row30_x96, 0xAB);
4882 bus.write_byte(row30_x97, 0xAB);
4883
4884 bus.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 20);
4885 disp.front_window = PORT_PTR;
4886 disp.window_bounds = (0, 0, 200, 400);
4887 disp.window_proc_id = 1; disp.fullscreen_locked = false;
4889
4890 disp.redraw_chrome(&mut bus);
4891
4892 assert_eq!(
4893 bus.read_byte(row30_x96),
4894 0xFF,
4895 "1bpp src bit=1 must map to 8bpp screen idx 0xFF"
4896 );
4897 assert_eq!(
4898 bus.read_byte(row30_x97),
4899 0x00,
4900 "1bpp src bit=0 must map to 8bpp screen idx 0x00 (blit MUST fire)"
4901 );
4902 }
4903
4904 #[test]
4905 fn redraw_chrome_blit_1bpp_to_8bpp_uses_active_ctab_black_entry() {
4906 let (mut disp, _cpu, mut bus) = setup_with_port();
4907
4908 let screen_base = bus.alloc(800 * 600);
4909 disp.screen_mode = (screen_base, 800, 800, 600, 8);
4910 bus.write_long(0x0824, screen_base);
4911 install_twilight_style_black_index(&mut disp, &mut bus);
4912
4913 let offscreen_base = bus.alloc(64 * 200);
4914 bus.write_long(PORT_PTR + 2, offscreen_base);
4915 bus.write_word(PORT_PTR + 6, 64);
4916 bus.write_word(PORT_PTR + 8, 0);
4917 bus.write_word(PORT_PTR + 10, 0);
4918 bus.write_word(PORT_PTR + 12, 200);
4919 bus.write_word(PORT_PTR + 14, 512);
4920
4921 bus.write_byte(offscreen_base + 30 * 64 + 12, 0b10000000);
4922
4923 let row30_x96 = screen_base + 30 * 800 + 96;
4924 let row30_x97 = screen_base + 30 * 800 + 97;
4925 bus.write_byte(row30_x96, 0xAB);
4926 bus.write_byte(row30_x97, 0xAB);
4927
4928 bus.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 20);
4929 disp.front_window = PORT_PTR;
4930 disp.window_bounds = (0, 0, 200, 400);
4931 disp.window_proc_id = 1;
4932 disp.fullscreen_locked = false;
4933
4934 disp.redraw_chrome(&mut bus);
4935
4936 assert_eq!(
4937 bus.read_byte(row30_x96),
4938 1,
4939 "1bpp src bit=1 must use the active ColorTable's black index"
4940 );
4941 assert_eq!(
4942 bus.read_byte(row30_x97),
4943 0,
4944 "1bpp src bit=0 must still use the active ColorTable's white index"
4945 );
4946 }
4947
4948 #[test]
4949 fn redraw_chrome_blit_8bpp_to_8bpp_translates_port_ctab_to_screen() {
4950 let (mut disp, _cpu, mut bus) = setup_with_port();
4951
4952 let screen_base = bus.alloc(64 * 64);
4953 disp.screen_mode = (screen_base, 64, 64, 64, 8);
4954 bus.write_long(0x0824, screen_base);
4955
4956 let gdevice_handle = disp.ensure_main_gdevice(&mut bus);
4957 bus.write_long(0x08A4, gdevice_handle);
4958 bus.write_long(0x0CC8, gdevice_handle);
4959
4960 let mut src_clut = TrapDispatcher::standard_mac_8bpp_clut();
4961 let dst_clut = TrapDispatcher::standard_mac_8bpp_clut();
4962 src_clut[42] = dst_clut[7];
4963 let src_ctab_handle = make_ctab_handle(&mut bus, &src_clut, 0x1234_5678);
4964
4965 let offscreen_base = bus.alloc(64 * 64);
4966 install_8bpp_cgrafport(&mut bus, offscreen_base, 64, 64, 64, src_ctab_handle);
4967
4968 bus.write_byte(offscreen_base + 5 * 64 + 10, 42);
4969 bus.write_byte(offscreen_base + 5 * 64 + 11, 8);
4970 bus.write_byte(screen_base + 5 * 64 + 10, 0xAA);
4971 bus.write_byte(screen_base + 5 * 64 + 11, 0xAA);
4972
4973 disp.front_window = PORT_PTR;
4974 disp.window_bounds = (0, 0, 64, 64);
4975 disp.window_proc_id = 1;
4976
4977 disp.blit_window_to_screen(&mut bus);
4978
4979 assert_eq!(
4980 bus.read_byte(screen_base + 5 * 64 + 10),
4981 7,
4982 "8bpp window blit must translate source CTab index 42 to the screen index for its RGB"
4983 );
4984 assert_eq!(
4985 bus.read_byte(screen_base + 5 * 64 + 11),
4986 8,
4987 "same-RGB entries should remain stable through the translation table"
4988 );
4989 }
4990
4991 #[test]
4992 fn redraw_chrome_blit_skips_tracked_window_swapped_to_scratch_pixmap() {
4993 let (mut disp, _cpu, mut bus) = setup_with_port();
4994
4995 let screen_base = bus.alloc(64 * 64);
4996 disp.screen_mode = (screen_base, 64, 64, 64, 8);
4997 bus.write_long(0x0824, screen_base);
4998 let gdevice_handle = disp.ensure_main_gdevice(&mut bus);
4999 bus.write_long(0x08A4, gdevice_handle);
5000 bus.write_long(0x0CC8, gdevice_handle);
5001
5002 let clut = TrapDispatcher::standard_mac_8bpp_clut();
5003 let ctab_handle = make_ctab_handle(&mut bus, &clut, 8);
5004 let original_base = bus.alloc(64 * 64);
5005 let original_pixmap =
5006 install_8bpp_cgrafport(&mut bus, original_base, 64, 64, 64, ctab_handle);
5007 disp.window_original_pixmaps
5008 .insert(PORT_PTR, original_pixmap);
5009
5010 let scratch_base = bus.alloc(64 * 64);
5011 install_8bpp_cgrafport(&mut bus, scratch_base, 64, 64, 64, ctab_handle);
5012
5013 bus.write_byte(screen_base + 8 * 64 + 8, 0xAA);
5014 bus.write_byte(scratch_base + 8 * 64 + 8, 0x22);
5015 disp.front_window = PORT_PTR;
5016
5017 disp.blit_window_to_screen(&mut bus);
5018
5019 assert_eq!(
5020 bus.read_byte(screen_base + 8 * 64 + 8),
5021 0xAA,
5022 "SetPortPix scratch buffers must not be auto-presented as window contents"
5023 );
5024
5025 bus.write_long(PORT_PTR + 2, original_pixmap);
5026 bus.write_byte(original_base + 8 * 64 + 8, 0x11);
5027
5028 disp.blit_window_to_screen(&mut bus);
5029
5030 assert_eq!(
5031 bus.read_byte(screen_base + 8 * 64 + 8),
5032 0x11,
5033 "the original tracked window backing PixMap should still be presented"
5034 );
5035 }
5036
5037 #[test]
5038 fn redraw_chrome_blits_large_manual_cport_centered_when_front_window_is_screen_backed() {
5039 let (mut disp, _cpu, mut bus) = setup_with_port();
5040
5041 let screen_base = bus.alloc(800 * 600);
5042 disp.screen_mode = (screen_base, 800, 800, 600, 8);
5043 disp.device_clut[0] = [0, 0, 0];
5044 disp.device_clut[0xAA] = [0, 0, 0];
5045 bus.fill_bytes(screen_base, 800 * 600, 0);
5046 bus.write_long(0x0824, screen_base);
5047 install_8bpp_cgrafport(&mut bus, screen_base, 800, 800, 600, 0);
5048 bus.write_byte(PORT_PTR + WINDOW_VISIBLE_OFFSET, 0xFF);
5049 disp.front_window = PORT_PTR;
5050 disp.window_list = vec![PORT_PTR];
5051
5052 let manual_port = bus.alloc(200);
5053 let manual_base = bus.alloc(640 * 420);
5054 install_8bpp_cgrafport_at(&mut bus, manual_port, manual_base, 640, 640, 420, 0);
5055 disp.cport_ports.insert(manual_port);
5056
5057 bus.fill_bytes(manual_base, 640 * 420, 0x44);
5058 bus.write_byte(manual_base + 419 * 640 + 639, 0x55);
5059 bus.write_byte(screen_base + 90 * 800 + 79, 0xAA);
5060 bus.write_byte(screen_base + 90 * 800 + 80, 0xAA);
5061 bus.write_byte(screen_base + 509 * 800 + 719, 0xAA);
5062
5063 disp.blit_large_manual_cport_to_screen(&mut bus);
5064
5065 assert_eq!(
5066 bus.read_byte(screen_base + 90 * 800 + 80),
5067 0x44,
5068 "640x420 manual scene should be centered at x=80,y=90 on an 800x600 screen"
5069 );
5070 assert_eq!(
5071 bus.read_byte(screen_base + 509 * 800 + 719),
5072 0x55,
5073 "bottom-right source pixel should land at the centered destination edge"
5074 );
5075 assert_eq!(
5076 bus.read_byte(screen_base + 90 * 800 + 79),
5077 0xAA,
5078 "manual CPort presentation must not top-left blit or overrun the centered scene"
5079 );
5080 }
5081
5082 #[test]
5083 fn redraw_chrome_blits_large_manual_cport_when_fullscreen_port_rect_origin_is_shifted() {
5084 let (mut disp, _cpu, mut bus) = setup_with_port();
5085
5086 let screen_base = bus.alloc(800 * 600);
5087 disp.screen_mode = (screen_base, 800, 800, 600, 8);
5088 disp.device_clut[0] = [0, 0, 0];
5089 disp.device_clut[0xAA] = [0, 0, 0];
5090 bus.fill_bytes(screen_base, 800 * 600, 0);
5091 bus.write_long(0x0824, screen_base);
5092 install_8bpp_cgrafport(&mut bus, screen_base, 800, 800, 600, 0);
5093 bus.write_byte(PORT_PTR + WINDOW_VISIBLE_OFFSET, 0xFF);
5094 disp.front_window = PORT_PTR;
5095 disp.window_list = vec![PORT_PTR];
5096 disp.window_bounds = (0, 0, 600, 800);
5097
5098 bus.write_word(PORT_PTR + 16, (-85i16) as u16);
5101 bus.write_word(PORT_PTR + 18, (-144i16) as u16);
5102 bus.write_word(PORT_PTR + 20, 515);
5103 bus.write_word(PORT_PTR + 22, 656);
5104
5105 let manual_port = bus.alloc(200);
5106 let manual_base = bus.alloc(640 * 420);
5107 install_8bpp_cgrafport_at(&mut bus, manual_port, manual_base, 640, 640, 420, 0);
5108 disp.cport_ports.insert(manual_port);
5109
5110 bus.fill_bytes(manual_base, 640 * 420, 0x44);
5111 bus.write_byte(screen_base + 90 * 800 + 80, 0xAA);
5112
5113 disp.blit_large_manual_cport_to_screen(&mut bus);
5114
5115 assert_eq!(
5116 bus.read_byte(screen_base + 90 * 800 + 80),
5117 0x44,
5118 "shifted portRect must not hide a full-screen tracked window from the manual CPort presentation bridge"
5119 );
5120 }
5121
5122 #[test]
5123 fn redraw_chrome_blits_large_manual_cport_when_small_front_window_leaves_dark_screen() {
5124 let (mut disp, _cpu, mut bus) = setup_with_port();
5125
5126 let screen_base = bus.alloc(800 * 600);
5127 disp.screen_mode = (screen_base, 800, 800, 600, 8);
5128 disp.device_clut[255] = [0, 0, 0];
5129 bus.fill_bytes(screen_base, 800 * 600, 0xFF);
5130 bus.write_long(0x0824, screen_base);
5131 install_8bpp_cgrafport(&mut bus, screen_base, 800, 800, 600, 0);
5132 bus.write_byte(PORT_PTR + WINDOW_VISIBLE_OFFSET, 0xFF);
5133 disp.front_window = PORT_PTR;
5134 disp.window_list = vec![PORT_PTR];
5135 disp.window_bounds = (185, 226, 415, 574);
5136
5137 bus.write_word(PORT_PTR + 16, 0);
5138 bus.write_word(PORT_PTR + 18, 0);
5139 bus.write_word(PORT_PTR + 20, 230);
5140 bus.write_word(PORT_PTR + 22, 348);
5141
5142 let manual_port = bus.alloc(200);
5143 let manual_base = bus.alloc(640 * 420);
5144 install_8bpp_cgrafport_at(&mut bus, manual_port, manual_base, 640, 640, 420, 0);
5145 disp.cport_ports.insert(manual_port);
5146
5147 let dst = screen_base + 90 * 800 + 80;
5148 bus.fill_bytes(manual_base, 640 * 420, 0x44);
5149 bus.write_byte(dst, 0xFF);
5150
5151 disp.blit_large_manual_cport_to_screen(&mut bus);
5152
5153 assert_eq!(
5154 bus.read_byte(dst),
5155 0x44,
5156 "a blank screen-backed front window can still reveal a large app-managed scene buffer"
5157 );
5158 }
5159
5160 #[test]
5161 fn redraw_chrome_does_not_blit_large_manual_cport_over_nonblank_small_front_window() {
5162 let (mut disp, _cpu, mut bus) = setup_with_port();
5163
5164 let screen_base = bus.alloc(800 * 600);
5165 disp.screen_mode = (screen_base, 800, 800, 600, 8);
5166 disp.device_clut[1] = [0xFFFF, 0xFFFF, 0xFFFF];
5167 disp.device_clut[255] = [0, 0, 0];
5168 bus.fill_bytes(screen_base, 800 * 600, 0xFF);
5169 bus.write_byte(screen_base + 25 * 800 + 25, 1);
5170 bus.write_long(0x0824, screen_base);
5171 install_8bpp_cgrafport(&mut bus, screen_base, 800, 800, 600, 0);
5172 bus.write_byte(PORT_PTR + WINDOW_VISIBLE_OFFSET, 0xFF);
5173 disp.front_window = PORT_PTR;
5174 disp.window_list = vec![PORT_PTR];
5175 disp.window_bounds = (185, 226, 415, 574);
5176
5177 bus.write_word(PORT_PTR + 16, 0);
5178 bus.write_word(PORT_PTR + 18, 0);
5179 bus.write_word(PORT_PTR + 20, 230);
5180 bus.write_word(PORT_PTR + 22, 348);
5181
5182 let manual_port = bus.alloc(200);
5183 let manual_base = bus.alloc(640 * 420);
5184 install_8bpp_cgrafport_at(&mut bus, manual_port, manual_base, 640, 640, 420, 0);
5185 disp.cport_ports.insert(manual_port);
5186
5187 let dst = screen_base + 90 * 800 + 80;
5188 bus.write_byte(manual_base, 0x44);
5189 bus.write_byte(dst, 0xAA);
5190
5191 disp.blit_large_manual_cport_to_screen(&mut bus);
5192
5193 assert_eq!(
5194 bus.read_byte(dst),
5195 0xAA,
5196 "manual scene fallback must not overpaint an already visible small window"
5197 );
5198 }
5199
5200 #[test]
5201 fn redraw_chrome_does_not_latch_blank_manual_cport_over_nonblank_fullscreen() {
5202 let (mut disp, _cpu, mut bus) = setup_with_port();
5203
5204 let screen_base = bus.alloc(800 * 600);
5205 disp.screen_mode = (screen_base, 800, 800, 600, 8);
5206 disp.device_clut[1] = [0xFFFF, 0xFFFF, 0xFFFF];
5207 disp.device_clut[255] = [0, 0, 0];
5208 bus.fill_bytes(screen_base, 800 * 600, 0x01);
5209 bus.write_long(0x0824, screen_base);
5210 install_8bpp_cgrafport(&mut bus, screen_base, 800, 800, 600, 0);
5211 bus.write_byte(PORT_PTR + WINDOW_VISIBLE_OFFSET, 0xFF);
5212 disp.front_window = PORT_PTR;
5213 disp.window_list = vec![PORT_PTR];
5214 disp.window_bounds = (0, 0, 600, 800);
5215
5216 let manual_port = bus.alloc(200);
5217 let manual_base = bus.alloc(656 * 600);
5218 bus.fill_bytes(manual_base, 656 * 600, 0xFF);
5219 install_8bpp_cgrafport_at(&mut bus, manual_port, manual_base, 656, 656, 600, 0);
5220 disp.cport_ports.insert(manual_port);
5221
5222 let covered_probe = screen_base + 300 * 800 + 400;
5223 disp.blit_large_manual_cport_to_screen(&mut bus);
5224
5225 assert_eq!(
5226 bus.read_byte(covered_probe),
5227 0x01,
5228 "a blank manual CPort must not overpaint already-rendered fullscreen content"
5229 );
5230 assert_eq!(
5231 disp.manual_cport_presented_port, 0,
5232 "blank manual CPorts must not latch presentation privilege before real screen blits"
5233 );
5234
5235 disp.copybits_screen_count = 1;
5236 bus.write_byte(covered_probe, 0x02);
5237 bus.write_byte(manual_base, 0x44);
5238 disp.blit_large_manual_cport_to_screen(&mut bus);
5239
5240 assert_eq!(
5241 bus.read_byte(covered_probe),
5242 0x02,
5243 "once screen CopyBits is active, an unlatched manual CPort must stay suppressed"
5244 );
5245 }
5246
5247 #[test]
5248 fn redraw_chrome_does_not_latch_manual_cport_over_visible_fullscreen_content() {
5249 let (mut disp, _cpu, mut bus) = setup_with_port();
5250
5251 let screen_base = bus.alloc(800 * 600);
5252 disp.screen_mode = (screen_base, 800, 800, 600, 8);
5253 disp.device_clut[1] = [0xFFFF, 0xFFFF, 0xFFFF];
5254 bus.fill_bytes(screen_base, 800 * 600, 0x01);
5255 bus.write_long(0x0824, screen_base);
5256 install_8bpp_cgrafport(&mut bus, screen_base, 800, 800, 600, 0);
5257 bus.write_byte(PORT_PTR + WINDOW_VISIBLE_OFFSET, 0xFF);
5258 disp.front_window = PORT_PTR;
5259 disp.window_list = vec![PORT_PTR];
5260 disp.window_bounds = (0, 0, 600, 800);
5261
5262 let manual_port = bus.alloc(200);
5263 let manual_base = bus.alloc(656 * 600);
5264 bus.fill_bytes(manual_base, 656 * 600, 0x44);
5265 install_8bpp_cgrafport_at(&mut bus, manual_port, manual_base, 656, 656, 600, 0);
5266 disp.cport_ports.insert(manual_port);
5267
5268 let covered_probe = screen_base + 300 * 800 + 400;
5269 disp.blit_large_manual_cport_to_screen(&mut bus);
5270
5271 assert_eq!(
5272 bus.read_byte(covered_probe),
5273 0x01,
5274 "an unlatched manual CPort must not overpaint visible fullscreen content"
5275 );
5276 assert_eq!(
5277 disp.manual_cport_presented_port, 0,
5278 "visible fullscreen content should prevent acquiring the manual CPort fallback latch"
5279 );
5280 }
5281
5282 #[test]
5283 fn redraw_chrome_does_not_blit_manual_cport_after_screen_copybits() {
5284 let (mut disp, _cpu, mut bus) = setup_with_port();
5285
5286 let screen_base = bus.alloc(800 * 600);
5287 disp.screen_mode = (screen_base, 800, 800, 600, 8);
5288 disp.device_clut[0] = [0, 0, 0];
5289 bus.fill_bytes(screen_base, 800 * 600, 0);
5290 bus.write_long(0x0824, screen_base);
5291 install_8bpp_cgrafport(&mut bus, screen_base, 800, 800, 600, 0);
5292 bus.write_byte(PORT_PTR + WINDOW_VISIBLE_OFFSET, 0xFF);
5293 disp.front_window = PORT_PTR;
5294 disp.window_list = vec![PORT_PTR];
5295 disp.copybits_screen_count = 1;
5296
5297 let manual_port = bus.alloc(200);
5298 let manual_base = bus.alloc(640 * 420);
5299 install_8bpp_cgrafport_at(&mut bus, manual_port, manual_base, 640, 640, 420, 0);
5300 disp.cport_ports.insert(manual_port);
5301
5302 bus.write_byte(manual_base, 0x44);
5303 bus.write_byte(screen_base + 90 * 800 + 80, 0xAA);
5304
5305 disp.blit_large_manual_cport_to_screen(&mut bus);
5306
5307 assert_eq!(
5308 bus.read_byte(screen_base + 90 * 800 + 80),
5309 0xAA,
5310 "apps already presenting through CopyBits should not be overwritten by the fallback"
5311 );
5312 }
5313
5314 #[test]
5315 fn redraw_chrome_continues_latched_manual_cport_after_later_screen_copybits() {
5316 let (mut disp, _cpu, mut bus) = setup_with_port();
5317
5318 let screen_base = bus.alloc(800 * 600);
5319 disp.screen_mode = (screen_base, 800, 800, 600, 8);
5320 disp.device_clut[0] = [0, 0, 0];
5321 bus.fill_bytes(screen_base, 800 * 600, 0);
5322 bus.write_long(0x0824, screen_base);
5323 install_8bpp_cgrafport(&mut bus, screen_base, 800, 800, 600, 0);
5324 bus.write_byte(PORT_PTR + WINDOW_VISIBLE_OFFSET, 0xFF);
5325 disp.front_window = PORT_PTR;
5326 disp.window_list = vec![PORT_PTR];
5327
5328 let manual_port = bus.alloc(200);
5329 let manual_base = bus.alloc(640 * 420);
5330 install_8bpp_cgrafport_at(&mut bus, manual_port, manual_base, 640, 640, 420, 0);
5331 disp.cport_ports.insert(manual_port);
5332
5333 let dst = screen_base + 90 * 800 + 80;
5334 bus.fill_bytes(manual_base, 640 * 420, 0x44);
5335 disp.blit_large_manual_cport_to_screen(&mut bus);
5336 assert_eq!(bus.read_byte(dst), 0x44);
5337
5338 disp.copybits_screen_count = 1;
5339 bus.write_byte(dst, 0xAA);
5340 bus.write_byte(manual_base, 0x66);
5341 disp.blit_large_manual_cport_to_screen(&mut bus);
5342
5343 assert_eq!(
5344 bus.read_byte(dst),
5345 0x66,
5346 "a manual CPort selected before a later CopyBits should keep presenting"
5347 );
5348 }
5349}