Skip to main content

systemless/trap/
framebuffer.rs

1//! Framebuffer drawing methods for menu bar and window chrome rendering.
2
3use 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
17/// Opt-in gate for visRgn auto-expansion. Setting
18/// `SYSTEMLESS_NO_VISRGN_AUTO_EXPAND=1` skips expanding the front window's
19/// visRgn.top when MBarHeight=0. Cached via OnceLock to keep the per-frame
20/// redraw path syscall-free.
21static 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
27// MTE 1992 p. 3-60 defines the low-order `Style` byte bit assignments:
28// bold, italic, underline, outline, shadow, condensed, and extended.
29const 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    /// Read screen parameters from the dispatcher's screen_mode.
40    /// Returns (screen_base, row_bytes, width, height, pixel_size).
41    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); // TheGDevice
598            if current != 0 {
599                current
600            } else {
601                bus.read_long(0x08A4) // MainDevice
602            }
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        // Imaging With QuickDraw 1994 p. 4-82 describes inverse-table
694        // lookup as the Color Manager path from RGB colors to device pixel
695        // values. Keep canonical endpoints pinned when the active table has
696        // the standard white/black entries, then prefer exact matches before
697        // falling back to nearest Euclidean distance in 16-bit RGB space.
698        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    /// Set a single pixel in the framebuffer (screen coordinates).
770    /// Works for both 1bpp and 8bpp screen modes.
771    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    /// Fill a rectangle in the framebuffer
884    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            // SetDeskCPat defines the desktop as the Window Manager's
1131            // patterned background. In kiosk mode, preserve black full-screen
1132            // game surfaces, but restore the standard desktop pattern behind
1133            // single floating dialogs whose exposed background is still the
1134            // startup black stage.
1135            // Inside Macintosh Volume V, V-210
1136            self.fill_desktop_pattern_outside_rect(bus, bounds);
1137        }
1138    }
1139
1140    /// Draw a horizontal line in the framebuffer
1141    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    /// Draw a single character glyph to the framebuffer, return advance width
1189    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 // default advance for missing glyph
1218        }
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        // Glyph data is 8-bit coverage per pixel (row-major, one byte
1286        // per pixel). Threshold at >=128 (bitmap glyphs are exclusively
1287        // 0 or 255).
1288        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            // Menu item styles follow the classic Style bitset; the
1425            // System 7 MDEF renders bold item names with the synthetic
1426            // one-pixel strike and matching one-pixel pen advance while
1427            // CalcMenuSize keeps plain guest metrics. MTE 1992 pp. 3-133
1428            // to 3-134.
1429            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        // QuickDraw outlines/shadows text by smearing a 1-bit glyph mask,
1507        // then XORing the original glyph out of the result. That produces
1508        // hollow outline and shadow faces instead of drawing offset filled
1509        // glyph copies.
1510        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    /// Draw a string to the framebuffer, return total width
1557    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    /// Draw a string to the framebuffer with a classic Style bitset.
1675    ///
1676    /// MTE 1992 pp. 3-60 and 3-133 to 3-134 define menu item text
1677    /// styles as the `Style` bitset used by `SetItemStyle`; HIG 1992
1678    /// pp. 72 to 74 show Style menu item names displayed in their
1679    /// corresponding text styles. This helper changes drawn pixels only;
1680    /// callers keep measurements on the plain glyph advances so themed
1681    /// and classic-compatible guest metrics remain stable.
1682    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    /// Draw the menu bar at the top of the screen.
1865    /// Height is read from the MBarHeight low-memory global ($0BAA).
1866    /// If MBarHeight is 0, the menu bar is hidden (full-screen mode).
1867    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            // The System 7 menu bar is a white strip with a one-pixel
1881            // lower border. Macintosh Toolbox Essentials 1992, glossary
1882            // "menu bar".
1883            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        // Chicago 12 is the system font for menus (font_id=0, size=12)
1936        let font_id: i16 = 0;
1937        let font_size: i16 = 12;
1938        let metrics = get_font_metrics(font_id, font_size);
1939        // Vertically center text: baseline = top_margin + ascent
1940        let text_height = metrics.ascent + metrics.descent;
1941        let text_y = (menu_bar_height - text_height) / 2 + metrics.ascent;
1942
1943        // Draw visible menu titles from the current menu list. InsertMenu
1944        // with beforeID=-1 installs a submenu/popup in the current menu
1945        // list without adding a menu-bar title. MTE 1992, p. 3-121.
1946        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            // HIG 1992 p. 54 says unavailable menu titles remain visible
1954            // but dimmed; p. 55 says pressing a menu title highlights it.
1955            // Route title-state chrome through the provider while keeping
1956            // classic text metrics and title hit regions unchanged.
1957            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                // MTE 1992 p. 3-131: DisableItem(menu, 0) disables the
1972                // whole menu title. On a plain classic screen dump, the
1973                // standard MDEF's dimmed title treatment resolves to the
1974                // menu-bar background, matching the System 7.5.3 reference while
1975                // preserving title spacing and hit regions.
1976                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        // System 7's standard menu-bar appleMark title is MDEF-owned chrome,
2042        // not the raw Chicago $14 glyph. The mask below is the 11x14 title
2043        // bitmap captured from the BasiliskII/System 7.5.3 reference; it is drawn
2044        // one pixel left of the title origin and with the same baseline as
2045        // Chicago 12 menu titles.
2046        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        // The standard menu bar stamps the classic rounded screen-corner
2110        // mask when drawn at the top edge. IM:I I-354 defines DrawMenuBar
2111        // as the routine that redraws the current menu bar.
2112        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    /// Blit the front window's port pixels to the screen framebuffer.
2153    ///
2154    /// On a real Mac, the Window Manager composites window content to the
2155    /// screen. In Systemless HLE, games draw to the window's GrafPort which
2156    /// may use a different baseAddr than the screen framebuffer. This copies
2157    /// the window content so that screen captures reflect the actual game state.
2158    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        // Read the window's port baseAddr.
2178        // CGrafPort version flag is at offset +6 (not +0 which is `device`).
2179        // Inside Macintosh Volume V, V-47
2180        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            // CGrafPort: portPixMap handle at offset 2
2185            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) // mask off flags
2203        } else {
2204            // GrafPort: portBits.baseAddr at offset 2
2205            (bus.read_long(port + 2), 0, 0)
2206        };
2207
2208        // SetPortPix deliberately replaces a CGrafPort's portPixMap so
2209        // applications can draw or calculate in a buffer other than the
2210        // window/screen, then CopyBits it explicitly. If a tracked Window
2211        // Manager color window has been swapped to such a scratch PixMap,
2212        // do not synthesize an automatic whole-port presentation blit.
2213        // IM:V V-76; Imaging With QuickDraw 1994, 4-86..4-87.
2214        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 the window already draws directly to the screen, no blit needed
2230        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        // Read the port's rowBytes (from pixMap for CGrafPort, portBits for GrafPort)
2242        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        // Read source port pixel size. For CGrafPort, PixMap.pixelSize
2249        // lives at offset +32 of the PixMap struct. Basic GrafPort is
2250        // implicitly 1bpp.
2251        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        // Read window content bounds (portRect in GrafPort at offset +16)
2263        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        // 1bpp source → 8bpp screen via per-pixel bit extraction.
2280        // For each source bit, resolve logical white/black through the
2281        // active GDevice ColorTable. Applications can repurpose 0xFF away
2282        // from black while still expecting mono source bits to scan out black.
2283        if port_pixel_size == 1 && pixel_size == 8 {
2284            let (white_index, black_index) = Self::logical_mono_pixel_indexes(bus);
2285            // Games may set portRect MUCH larger than the actual BitMap
2286            // bounds (e.g. StuntCopter: portRect=(0,0..567,791) but
2287            // BitMap=(0,0..261,426)). Clamp source reads to the BitMap
2288            // bounds (portBits.bounds at port + 8..15) so we don't walk
2289            // past valid source data into adjacent rows. Without this,
2290            // the per-row stride bug produces horizontally-doubled or
2291            // tiled content.
2292            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        // Same-depth fast path. Anything that's neither matched-depth nor
2315        // 1bpp→8bpp falls through to a no-op.
2316        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        // Color QuickDraw treats PixMap pixels through their pmTable. When
2324        // HLE composites a front-window offscreen CGrafPort to the screen,
2325        // mirror the CopyBits indexed-color translation rule instead of raw
2326        // copying mismatched 8bpp indices. IM:V V-91, V-95, V-136.
2327        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        // block_move per row.
2356        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    /// Present a large app-managed CGrafPort when CopyBits did not present it.
2364    ///
2365    /// This is an HLE compatibility bridge, not a QuickDraw rule: real apps
2366    /// are responsible for copying offscreen ports to the screen. Some games
2367    /// keep a full-scene PixMap behind an OpenCPort/InitCPort and update it
2368    /// directly while their front window remains screen-backed. Without a
2369    /// Window Manager or video driver layer to observe that buffer, screenshots
2370    /// stay black even though the scene exists in guest memory.
2371    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            // CGrafPort/PixMap layout follows Imaging With QuickDraw
2488            // 1994, pp. 4-64..4-65: portPixMap is a PixMapHandle, with
2489            // baseAddr, rowBytes, bounds, pixelSize, and pmTable here.
2490            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    /// Draw window chrome (title bar, close box, border) into the framebuffer
2723    /// WIND bounds are the CONTENT RECT; title bar is drawn ABOVE it.
2724    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        // Title bar area: drawn ABOVE the content rect
2730        // Clamp to menu bar height — the Window Manager never draws
2731        // chrome into the menu bar area.
2732        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        // Fill title bar with white (exclusive bottom)
2739        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        // Draw title bar border. Classic document WDEFs leave the top edge
2758        // open; System 7.5.3 paints only side edges, bottom separator, and
2759        // active pinstripes. movableDBoxProc keeps the full title-frame top.
2760        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        // Left and right border of title bar
2787        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        // Calculate title text area if we have a title
2813        let font_id: i16 = 0; // Chicago
2814        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) // No clear area
2833        };
2834
2835        let _close_box_width = if has_go_away { 15i16 } else { 0 };
2836
2837        if is_movable_modal {
2838            // movableDBoxProc: plain title bar, no stripes
2839            // Just draw the title text centered
2840            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            // documentProc/noGrowDocProc: stripes + optional close box
2858
2859            // Draw close box if goAwayFlag is set.
2860            //
2861            // Classic Mac System 7.5.3 close-box graphic per BasiliskII reference
2862            // (window_goaway): NOT a clean FrameRect. The WDEF draws an 11×11
2863            // bounding region split into two shapes:
2864            //   * top-left  L-shape — top horizontal (11 wide) + left vertical
2865            //                         (11 tall), painting the 3D-highlight edge
2866            //   * bottom-right Γ-shape — right vertical (8 tall, inset 2 from
2867            //                            top + 1 from bottom) + bottom
2868            //                            horizontal (8 wide, inset 2 from left
2869            //                            + 1 from right), painting the inner
2870            //                            close-box outline
2871            // The 1-pixel gap between the two shapes gives the close box its
2872            // characteristic 3D-button appearance.
2873            // Inside Macintosh Volume V, V-188 figure 5-3.
2874            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; // 1px border + 8px padding
2880
2881                // Top-left L: full 11-wide top edge + full 11-tall left edge
2882                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                // Bottom-right Γ: 8-tall right edge + 8-wide bottom edge,
2909                // inset 2 from the top-left and 1 from the bottom-right.
2910                let inner_right = cb_left + cb_size - 2; // x=cb_left+9
2911                let inner_bottom = cb_top + cb_size - 2; // y=cb_top+9
2912                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            // Draw horizontal stripe pattern in title bar (classic Mac pinstripes)
2940            // Only active windows get stripes; inactive windows have plain white title bars
2941            //
2942            // System 7.5.3 reserves only 6 px of clear-area on each side of
2943            // the title text for stripes (the 16-px `title_clear_left/right`
2944            // margin is for text-glyph hit-testing, not for stripes). The
2945            // active document WDEF paints pinstripe rows at title-bar offsets
2946            // 1, 3, and 5; this row placement is calibrated against the
2947            // BasiliskII System 7.5.3 reference.
2948            // Inside Macintosh Volume V, V-188 figure 5-3.
2949            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                // Close box region to skip (if present)
2956                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; // QD exclusive right
2959                    (cb_left - 1, cb_right + 2) // 1px gap left, 2px gap right
2960                } else {
2961                    (stripe_right_end, stripe_right_end) // no gap
2962                };
2963
2964                for y in (tb_top + 1)..=(tb_bottom - 4) {
2965                    if (y - tb_top) % 2 == 1 {
2966                        // Draw stripe segments, skipping close box and title text gaps
2967                        // Segment 1: left edge to close box (or title text if no close box)
2968                        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                        // Segment 2: after close box to title text (only if close box exists)
2988                        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                        // Segment 3: after title text to right edge
3003                        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            // Draw title text centered in title bar. Active windows get
3022            // stripes and a close box; inactive windows keep the title text
3023            // over a plain title bar.
3024            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        // Draw window content area border
3044        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        // Bottom border line
3069        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        // Shadow effect
3083        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    /// Draw the grow icon (size box) in the bottom-right corner of a window.
3112    /// The grow icon is a 15x15 area at the intersection of scroll bars.
3113    /// Inside Macintosh Volume I, I-296
3114    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        // Read portRect (top, left, bottom, right) from the window record
3118        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        // Read PixMap bounds to get the origin offset
3123        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        // Content area in screen coordinates
3141        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        // DrawGrowIcon draws the scroll bar separator lines:
3147        // - Vertical line at content_right - 15 from content_top to content_bottom
3148        // - Horizontal line at content_bottom - 15 from border_left to border_right
3149        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        // Vertical scroll separator (full content height)
3155        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        // Horizontal scroll separator (border to border)
3169        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    /// Draw a 2-pixel thick rectangle border (FrameRect with PenSize 2,2).
3184    /// Coordinates are in QuickDraw convention: (top, left) inclusive, (bottom, right) exclusive.
3185    /// On the real Mac, the pen extends DOWN and RIGHT from each point,
3186    /// giving 2 rows at top but only 1 row at bottom (clipped to rect).
3187    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        // Top edge: 2 rows (pen at top extends to top+1)
3198        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        // Bottom edge: 1 row at bottom-1 (pen at bottom-1 would extend to bottom, clipped)
3213        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        // Left edge: 2 columns (pen at left extends to left+1)
3226        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        // Right edge: 2 columns (right-2 and right-1, both inside the rect)
3242        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    /// Erase (fill white) the structure region for a window, then draw the frame.
3260    /// On a real Mac the WDEF erases the structure region before drawing borders.
3261    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    /// Erase only the frame/shadow area around content, leaving the content
3287    /// pixels untouched. WDEFs erase their structure region before drawing
3288    /// borders, but in the HLE framebuffer the content area may already hold
3289    /// app-rendered pixels that should not be replaced with white.
3290    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    /// Draw the window frame/border for a given procID.
3326    /// Called when a visible window is created to render its frame to the screen.
3327    /// This implements the standard WDEF rendering for each window type:
3328    ///   - plainDBox (2): Single 1-pixel border
3329    ///   - dBoxProc (1): Double border (outer 1px at ±8, inner 2px at ±5)
3330    ///   - altDBoxProc (3): Single border + 2-pixel drop shadow
3331    ///   - document/zoom document procs (0, 4, 8, 12, 16): Title bar chrome
3332    ///   - movableDBoxProc (5): Double border + title bar chrome
3333    ///
3334    /// Draws a single window's chrome inline by deriving its screen-coord
3335    /// bounds, title, and goAway flag from the WindowRecord. The dispatcher's
3336    /// per-window state is swapped in temporarily so draw_window_chrome reads
3337    /// the right context, then restored.
3338    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; // not visible
3349        }
3350        if self.window_uses_custom_def_proc(bus, window_ptr) {
3351            return;
3352        }
3353        // plainDBox (2), dBoxProc (1), and altDBoxProc (3) windows have
3354        // no title bar; dispatch to draw_window_frame rather than
3355        // draw_window_chrome (which paints title-bar chrome).
3356        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        // wrapping_neg / wrapping_add match 68k Mac OS i16 wrap-
3372        // around — guards against debug-build panics on windows
3373        // whose pixmap.bounds.topLeft is i16::MIN or whose total
3374        // width/height exceeds i16 range.
3375        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                // plainDBox: single 1-pixel black border, no chrome.
3444                // Inside Macintosh Volume I, I-275: plainDBox windows
3445                // get their canonical 1px border from the system WDEF.
3446                // The border sits OUTSIDE the content rect so it doesn't
3447                // paint over content the application drew inside.
3448                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                // dBoxProc: double border
3458                // Structure region = content expanded by 8
3459                // WDEF erases structure, then draws:
3460                //   1. Outer 1px border at (content-8, content-8, content+3, content+8)
3461                //   2. Inner 2px border at (content-5, content-5, content+3, content+5)
3462                // Note: bottom offset is +3 (not +8), making the border asymmetric.
3463                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                // Outer 1px border
3473                self.draw_rect_border(bus, struc_top, struc_left, struc_bottom, struc_right);
3474                // Inner 2px border (content-5 top/left/right, content+3 bottom)
3475                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                // altDBoxProc: single border + 2px drop shadow
3485                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                // Shadow starts just below/right of the border (border bottom is at wind_bottom)
3498                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                // movableDBoxProc: double border wrapping title bar space + content
3508                // The outer border extends above the content to leave room for a
3509                // title bar area (18px), but no title bar chrome is drawn inside.
3510                // Outer border: symmetric ±8 except top adds 15 for title bar space
3511                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                // Outer 1px border
3521                self.draw_rect_border(bus, struc_top, struc_left, struc_bottom, struc_right);
3522                // Inner 2px border around content area (±5)
3523                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                // draw_thick_rect_border draws 1 row at bottom; movDBox needs 2 rows
3529                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                // Document-style windows with title bars
3546                // Erase structure region (content + title bar + 1px border + shadow)
3547                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                // Unknown procID: at least draw a single border
3559                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    /// Redraw the menu bar and window chrome into the framebuffer.
3595    ///
3596    /// On a real Mac, the Window Manager maintains these UI elements and redraws
3597    /// them after any update. Our emulator draws them as raw framebuffer pixels,
3598    /// so game drawing (explosions, etc.) can overwrite them. This method restores
3599    /// the chrome and should be called after each frame of emulation.
3600    pub fn redraw_chrome(&mut self, bus: &mut MacMemoryBus) {
3601        // Blit front window's port pixels to screen framebuffer if they differ.
3602        // On real Mac OS the Window Manager composites windows to the screen.
3603        // In HLE, games draw to the window's GrafPort which may have a different
3604        // baseAddr than the screen. Copy the window content so screenshots work.
3605        //
3606        // ModalDialog's HLE first paints standard dialog content directly into
3607        // the screen framebuffer, then injects any userItem draw procs and
3608        // re-snapshots the completed result. While that snapshot is pending,
3609        // the dialog's offscreen port may still be blank; blitting it here
3610        // erases the partially rendered dialog before the draw procs can finish.
3611        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        // Detect fullscreen: the front window covers the entire screen
3625        // (top <= 0, left <= 0, bottom >= screen_h, right >= screen_w)
3626        // and MBarHeight is 0.  Once detected, lock fullscreen mode so
3627        // that the game temporarily restoring MBarHeight (e.g. on
3628        // cursor-at-top) cannot flash the menu bar.
3629        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        // Skip chrome for borderless/dialog window types that have no title bar.
3650        // procID 1 = dBoxProc, 2 = plainDBox, 3 = altDBoxProc
3651        // All other standard types (documentProc, noGrowDocProc, etc.) get
3652        // chrome. Application custom WDEFs draw their own frames and are
3653        // skipped per-window below.
3654        // Also skip chrome when MBarHeight is 0 — the game has hidden the menu bar
3655        // for full-screen mode, so window chrome should not be drawn either.
3656        // Inside Macintosh Volume I, I-299; Inside Macintosh Volume V, V-245
3657
3658        // Games set MBarHeight to 0 by writing directly to the low-memory
3659        // global ($0BAA) for full-screen mode.  Since we can't intercept
3660        // memory writes, check here whether the front window's visRgn.top
3661        // is stale and needs expanding to cover the now-hidden menu bar area.
3662        // Inside Macintosh Volume V, V-245; Tricks of the Mac Game
3663        // Programming Gurus 1995, p. 30-265
3664        // `menu_bar_hidden` (default-on for game runtimes — see
3665        // `TrapDispatcher::menu_bar_hidden`) suppresses the menu bar even
3666        // when MBarHeight is non-zero. Treat it like fullscreen for
3667        // visRgn-expansion purposes so the band the menu bar would occupy
3668        // is owned by the front window's visRgn, not left unpainted.
3669        let effective_mbar = if self.fullscreen_locked || self.menu_bar_hidden {
3670            0
3671        } else {
3672            menu_bar_height
3673        };
3674        // Only expand visRgn when the menu bar is HIDDEN (effective_mbar == 0).
3675        // Games set MBarHeight=0 for fullscreen mode and expect their window's
3676        // visRgn to extend over the now-hidden menu bar area. Doing this
3677        // unconditionally would clobber wind_top for documentProc windows where
3678        // the menu bar is visible (degenerate title bar on every redraw).
3679        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                        // Also update clipRgn and portRect to match.
3689                        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        // Draw chrome for each visible non-front window FIRST
3725        // (back-to-front order per window_list which is front-to-back),
3726        // then the front window on top. Each back-window's chrome uses its
3727        // WindowRecord-stored state (bounds derived from portPixMap bounds,
3728        // title from titleHandle, goAway byte, hilited byte).
3729        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            // Iterate back-to-front so earlier windows get overdrawn
3736            // by later ones.
3737            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                    // Not visible.
3743                    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                // Derive per-window screen bounds from port geometry:
3763                // init_cgraf_window writes pixmap bounds as
3764                // (-wind_top, -wind_left, screen_h - wind_top,
3765                //  screen_w - wind_left) — so wind_top = -bounds_top,
3766                // wind_left = -bounds_left, and window size comes
3767                // from portRect at window_ptr+16.
3768                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                // Degenerate / invalid — skip.
3786                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                // Read title from titleHandle at +134.
3791                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                // Use the per-window procID. Windows with no title bar
3804                // (plainDBox/dBoxProc/altDBoxProc) draw only a border.
3805                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            // Restore front-window state before drawing front chrome.
3821            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            // Use the front window's hilited byte rather than hard-coding
3829            // active=true so HiliteWindow(front, false) renders no stripes.
3830            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 a modal dialog is active, restore the rendered snapshot and
3834        // redraw only dynamic elements (edit text, button flash) on top.
3835        // Game-managed dialogs (all userItems) handle their own rendering
3836        // via the filter proc — skip restoration to avoid overwriting their content.
3837        // While userItem draw procs or filter procs are pending
3838        // (rendered_pixels_final=false), skip restoration so their output
3839        // accumulates in the framebuffer. ModalDialog re-snapshots the final
3840        // state and sets rendered_pixels_final=true before we begin restoring.
3841        if let Some(ref tracking) = self.dialog_tracking {
3842            if !tracking.game_managed && tracking.rendered_pixels_final {
3843                // Blit the pre-rendered dialog snapshot (includes pictures)
3844                self.restore_dialog_pixels(bus, tracking.bounds, &tracking.rendered_pixels);
3845
3846                // Re-draw the edit text field on top (may have changed since snapshot)
3847                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                // During flash, alternate highlight on the flashing button
3868                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 a menu dropdown is open, redraw it on top of the menu bar
3901        // so that the menu bar redraw doesn't erase it.
3902        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            // During flash, alternate highlight: even remaining = highlighted,
3906            // odd remaining = not highlighted. Outside flash, always highlight.
3907            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    // Window/port layout from `setup_with_port`:
3928    //   port_ptr      = 0x181000
3929    //   port_top      = port_ptr + 16 (word)
3930    //   visRgn handle = port_ptr + 24 (long) → 0x182100
3931    //   visRgn        = 0x182000 (rgnSize @ +0, top @ +2, ...)
3932    //   clipRgn handle= port_ptr + 28 (long) → 0x182300
3933    //   clipRgn       = 0x182200
3934    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    /// When the menu bar is visible (MBarHeight > 0), the per-frame visRgn
4088    /// auto-expansion in `redraw_chrome` MUST NOT fire for a documentProc
4089    /// window whose visRgn.top is below the menu bar.
4090    #[test]
4091    fn redraw_chrome_preserves_window_bounds_when_menu_bar_visible() {
4092        let (mut disp, _cpu, mut bus) = setup_with_port();
4093
4094        // MissileCommand-shaped layout: WIND bounds (40, 2, 339, 508),
4095        // documentProc, menu bar visible at y=0..19.
4096        bus.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 20);
4097        bus.write_word(VIS_RGN + 2, 40); // visRgn.top
4098        bus.write_word(CLIP_RGN + 2, 40); // clipRgn.top
4099        bus.write_word(PORT_PTR + 16, 40); // port_top
4100        disp.front_window = PORT_PTR;
4101        disp.window_bounds = (40, 2, 339, 508);
4102        disp.window_proc_id = 0; // documentProc
4103        disp.fullscreen_locked = false;
4104        // Specifically pin "host menu bar visible" — the constructor
4105        // hides it by default for game runtimes.
4106        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    /// When the menu bar is hidden (MBarHeight == 0), the per-frame visRgn
4127    /// auto-expansion in `redraw_chrome` MUST fire and sweep the front
4128    /// window's visRgn.top down to 0 — fullscreen games rely on this so
4129    /// they can paint over the y=0..19 region.
4130    #[test]
4131    fn redraw_chrome_expands_visrgn_when_menu_bar_hidden() {
4132        let (mut disp, _cpu, mut bus) = setup_with_port();
4133
4134        // EV-shaped layout: front window's visRgn.top is still 20
4135        // (left over from before the game wrote MBarHeight=0).
4136        bus.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 0);
4137        bus.write_word(VIS_RGN + 2, 20); // stale visRgn.top
4138        bus.write_word(CLIP_RGN + 2, 20); // stale clipRgn.top
4139        bus.write_word(PORT_PTR + 16, 20); // stale port_top
4140        disp.front_window = PORT_PTR;
4141        disp.window_bounds = (20, 0, 342, 512);
4142        disp.window_proc_id = 0; // documentProc
4143        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    /// Host-side `menu_bar_hidden = true` with MBarHeight > 0 must still
4164    /// expand the front window's visRgn down to y=0. Otherwise the band
4165    /// the menu bar would have occupied is owned by nobody — the host
4166    /// suppresses its chrome paint, but the window also won't paint
4167    /// there, leaving stale or uninitialized pixels at the top of every
4168    /// fullscreen game capture.
4169    #[test]
4170    fn redraw_chrome_expands_visrgn_when_host_hides_menu_bar() {
4171        let (mut disp, _cpu, mut bus) = setup_with_port();
4172
4173        // Game has NOT zeroed MBarHeight (a polite app that just
4174        // installed menus and never went fullscreen) — but the host
4175        // suppresses chrome because we're running it as a game.
4176        bus.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 20);
4177        bus.write_word(VIS_RGN + 2, 20); // visRgn.top below where chrome would be
4178        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; // documentProc
4183        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    /// `redraw_chrome` MUST NOT paint the menu bar (the white strip at
4205    /// y=0..MBarHeight) when `menu_bar_hidden = true`, even if the
4206    /// guest has installed menus and left MBarHeight > 0. This pins
4207    /// the kiosk-mode contract: the host suppresses chrome regardless
4208    /// of game state, so fullscreen captures match the BasiliskII
4209    /// reference that has no menu bar to draw.
4210    #[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        // Allocate a real screen buffer so blit_window_to_screen has
4215        // somewhere to land (without a real screen, the test would
4216        // pass trivially).
4217        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        // Pre-fill the would-be menu bar band with a sentinel byte so
4222        // any chrome paint trampling it is detectable.
4223        for i in 0u32..(800 * 20) {
4224            bus.write_byte(screen_base + i, 0xAA);
4225        }
4226
4227        // Game has installed menus and left MBarHeight = 20, but the
4228        // host runtime is configured for kiosk mode.
4229        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        // Skip blit_window_to_screen by leaving front_window=0 — the
4241        // test specifically targets draw_menu_bar_to_fb, not the window
4242        // composite path.
4243        disp.front_window = 0;
4244        disp.fullscreen_locked = false;
4245        disp.menu_bar_hidden = true;
4246
4247        disp.redraw_chrome(&mut bus);
4248
4249        // Sample a few bytes inside the menu-bar band. None should
4250        // have been overwritten with white (0xFF) — they should still
4251        // be the 0xAA sentinel we pre-filled.
4252        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    /// Counterpart to the kiosk-mode test above: when `menu_bar_hidden
4324    /// = false` (app-style hosting), `redraw_chrome` MUST paint the
4325    /// menu bar so menus are reachable. This pins the env-var-driven
4326    /// opt-out (SYSTEMLESS_SHOW_MENU_BAR=1 → menu_bar_hidden=false).
4327    #[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        // Pre-fill with sentinel so we can detect any paint.
4336        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        // The menu bar fills with white. In Mac 8bpp CLUT convention
4358        // index 0 = white, index 255 = black (Imaging With QuickDraw
4359        // 1994, 4-7), so painted pixels read back as 0x00, not 0xFF.
4360        // Either way, they cannot still be the 0xAA sentinel we
4361        // pre-filled — that's the load-bearing assertion.
4362        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    /// Pin directive #3 from the task brief: "stays hidden even when
4443    /// the mouse hits the top of the screen". Hovering the mouse at
4444    /// y<MBarHeight while menu_bar_hidden=true must NOT trigger any
4445    /// chrome paint or menu-bar rendering. The previous tests cover
4446    /// the per-frame redraw_chrome guard; this test specifically
4447    /// re-runs redraw_chrome after a mouse_pos update to the top of
4448    /// the screen (the cursor-on-mbar scenario the user keeps
4449    /// flagging) and verifies the y=0..19 band still matches the
4450    /// pre-update sentinel.
4451    #[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        // Pre-fill the menu bar band with sentinel.
4460        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        // Initial redraw — sentinel preserved.
4480        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        // Move mouse to the top of the screen — the very scenario
4494        // the task brief calls out. mouse_pos updates to (v=2, h=400)
4495        // which is well inside the would-be menu-bar band.
4496        disp.mouse_pos = (2, 400);
4497        bus.write_word(0x0828, 2u16); // MTemp.v
4498        bus.write_word(0x082A, 400u16); // MTemp.h
4499        bus.write_word(0x082C, 2u16); // RawMouse.v
4500        bus.write_word(0x082E, 400u16); // RawMouse.h
4501        bus.write_word(0x0830, 2u16); // Mouse.v
4502        bus.write_word(0x0832, 400u16); // Mouse.h
4503
4504        // Re-run redraw_chrome. With menu_bar_hidden=true the band
4505        // must STILL be untouched even though the cursor is now
4506        // inside it.
4507        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    /// When a front window's port is 1bpp and the screen is 8bpp,
4841    /// `blit_window_to_screen` MUST do per-pixel bit extraction (each src
4842    /// bit → 0 or 0xFF). Falling through to the same-depth `block_move`
4843    /// path treats pixel-width as byte-width — a stride bug that produces
4844    /// a tiled-garbage band on screen.
4845    #[test]
4846    fn redraw_chrome_blit_1bpp_to_8bpp_is_bit_extracted() {
4847        let (mut disp, _cpu, mut bus) = setup_with_port();
4848
4849        // setup_with_port doesn't initialize disp.screen_mode. Allocate
4850        // a real 800×600 8bpp screen buffer in bus memory (the default
4851        // 4MB RAM doesn't reach the play-runner's $01F80000 base).
4852        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        // Allocate an offscreen 1bpp port buffer separate from the
4857        // screen. Basic GrafPort (port_version & 0xC000 != 0xC000) is
4858        // implicitly 1bpp.
4859        let offscreen_base = bus.alloc(64 * 200);
4860        bus.write_long(PORT_PTR + 2, offscreen_base); // portBits.baseAddr
4861        bus.write_word(PORT_PTR + 6, 64); // rowBytes (no flag bits = basic GrafPort)
4862
4863        // setup_with_port wrote portBits.bounds (0,0,342,512) and
4864        // portRect (0,0,342,512). Override portBits.bounds to match
4865        // our 64-byte rowBytes × 200-row allocation: (0, 0, 200, 512).
4866        bus.write_word(PORT_PTR + 8, 0); // bounds.top
4867        bus.write_word(PORT_PTR + 10, 0); // bounds.left
4868        bus.write_word(PORT_PTR + 12, 200); // bounds.bottom
4869        bus.write_word(PORT_PTR + 14, 512); // bounds.right
4870
4871        // Source pattern: row 30 byte 12 = 0b10000000 (bit 7 set) means
4872        // source pixel (row=30, col=96) = 1. Adjacent pixels (col=97..103)
4873        // are 0. After 1bpp→8bpp conversion, screen pixel (30, 96) should
4874        // become 0xFF and (30, 97) should become 0x00.
4875        bus.write_byte(offscreen_base + 30 * 64 + 12, 0b10000000);
4876
4877        // Pre-fill the test pixels with a sentinel so we can detect
4878        // both bit values being correctly written.
4879        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; // dBoxProc → skip chrome draw
4888        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        // SetOrigin and related port operations can shift the local
5099        // portRect while the Window Manager bounds still cover the screen.
5100        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}