1use std::sync::OnceLock;
4
5use crate::memory::{MacMemoryBus, MemoryBus};
6use crate::quickdraw::text::{get_font_metrics, get_glyph};
7
8static NO_VISRGN_AUTO_EXPAND: OnceLock<bool> = OnceLock::new();
13fn no_visrgn_auto_expand_enabled() -> bool {
14 *NO_VISRGN_AUTO_EXPAND
15 .get_or_init(|| std::env::var_os("SYSTEMLESS_NO_VISRGN_AUTO_EXPAND").is_some())
16}
17
18impl super::TrapDispatcher {
19 pub(super) fn get_screen_params(&self) -> (u32, u32, i16, i16, u16) {
22 let (base, rb, w, h, ps) = self.screen_mode;
23 (base, rb, w as i16, h as i16, ps)
24 }
25
26 fn active_gdevice_ctab(bus: &MacMemoryBus) -> Option<u32> {
27 let gdevice_handle = {
28 let current = bus.read_long(0x0CC8); if current != 0 {
30 current
31 } else {
32 bus.read_long(0x08A4) }
34 };
35 if gdevice_handle == 0 {
36 return None;
37 }
38 let gdevice = bus.read_long(gdevice_handle);
39 if gdevice == 0 {
40 return None;
41 }
42 let pixmap_handle = bus.read_long(gdevice + 22);
43 if pixmap_handle == 0 {
44 return None;
45 }
46 let pixmap = bus.read_long(pixmap_handle);
47 if pixmap == 0 {
48 return None;
49 }
50 let ctab_handle = bus.read_long(pixmap + 42);
51 if ctab_handle == 0 {
52 return None;
53 }
54 let ctab = bus.read_long(ctab_handle);
55 if ctab == 0 {
56 return None;
57 }
58 Some(ctab)
59 }
60
61 fn ctab_value_luma(bus: &MacMemoryBus, ctab: u32, wanted_value: u8) -> Option<u32> {
62 let count = u32::from(bus.read_word(ctab + 6)).min(255) + 1;
63
64 let ordinal = u32::from(wanted_value);
65 if ordinal < count {
66 let entry = ctab + 8 + ordinal * 8;
67 if bus.read_word(entry) == u16::from(wanted_value) {
68 return Some(
69 u32::from(bus.read_word(entry + 2))
70 + u32::from(bus.read_word(entry + 4))
71 + u32::from(bus.read_word(entry + 6)),
72 );
73 }
74 }
75
76 for ordinal in 0..count {
77 let entry = ctab + 8 + ordinal * 8;
78 if bus.read_word(entry) != u16::from(wanted_value) {
79 continue;
80 }
81 return Some(
82 u32::from(bus.read_word(entry + 2))
83 + u32::from(bus.read_word(entry + 4))
84 + u32::from(bus.read_word(entry + 6)),
85 );
86 }
87 None
88 }
89
90 fn best_luma_pixel_index(bus: &MacMemoryBus, ctab: u32, brightest: bool) -> Option<u8> {
91 let count = u32::from(bus.read_word(ctab + 6)).min(255) + 1;
92 let mut best_index = 0u8;
93 let mut best_luma = 0u32;
94 let mut found = false;
95 for ordinal in 0..count {
96 let entry = ctab + 8 + ordinal * 8;
97 let value = bus.read_word(entry);
98 if value > 255 {
99 continue;
100 }
101 let luma = u32::from(bus.read_word(entry + 2))
102 + u32::from(bus.read_word(entry + 4))
103 + u32::from(bus.read_word(entry + 6));
104 let index = value as u8;
105 let better = if brightest {
106 luma > best_luma || (luma == best_luma && index < best_index)
107 } else {
108 luma < best_luma || (luma == best_luma && index < best_index)
109 };
110 if !found || better {
111 best_index = index;
112 best_luma = luma;
113 found = true;
114 }
115 }
116
117 found.then_some(best_index)
118 }
119
120 fn logical_white_pixel_index(bus: &MacMemoryBus) -> u8 {
121 let Some(ctab) = Self::active_gdevice_ctab(bus) else {
122 return 0;
123 };
124 if Self::ctab_value_luma(bus, ctab, 0) == Some(0xFFFF * 3) {
125 return 0;
126 }
127 Self::best_luma_pixel_index(bus, ctab, true).unwrap_or(0)
128 }
129
130 fn logical_black_pixel_index(bus: &MacMemoryBus) -> u8 {
131 let Some(ctab) = Self::active_gdevice_ctab(bus) else {
132 return 255;
133 };
134 if Self::ctab_value_luma(bus, ctab, 1) == Some(0) {
135 return 1;
136 }
137 if Self::ctab_value_luma(bus, ctab, 255) == Some(0) {
138 return 255;
139 }
140 Self::best_luma_pixel_index(bus, ctab, false).unwrap_or(255)
141 }
142
143 fn logical_mono_pixel_indexes(bus: &MacMemoryBus) -> (u8, u8) {
144 (
145 Self::logical_white_pixel_index(bus),
146 Self::logical_black_pixel_index(bus),
147 )
148 }
149
150 pub(crate) fn fb_set_pixel(
153 bus: &mut MacMemoryBus,
154 screen_base: u32,
155 row_bytes: u32,
156 pixel_size: u16,
157 screen_width: i16,
158 screen_height: i16,
159 x: i16,
160 y: i16,
161 black: bool,
162 ) {
163 if x < 0 || y < 0 || x >= screen_width || y >= screen_height {
164 return;
165 }
166 if pixel_size == 8 {
167 let addr = screen_base + (y as u32) * row_bytes + (x as u32);
168 bus.write_byte(
169 addr,
170 if black {
171 255
172 } else {
173 Self::logical_white_pixel_index(bus)
174 },
175 );
176 } else {
177 let byte_offset = (y as u32) * row_bytes + (x as u32 / 8);
178 let bit = 7 - (x as u32 % 8);
179 let addr = screen_base + byte_offset;
180 let b = bus.read_byte(addr);
181 if black {
182 bus.write_byte(addr, b | (1 << bit));
183 } else {
184 bus.write_byte(addr, b & !(1 << bit));
185 }
186 }
187 }
188
189 pub(crate) fn fb_fill_rect(
191 bus: &mut MacMemoryBus,
192 screen_base: u32,
193 row_bytes: u32,
194 pixel_size: u16,
195 screen_width: i16,
196 screen_height: i16,
197 top: i16,
198 left: i16,
199 bottom: i16,
200 right: i16,
201 black: bool,
202 ) {
203 if pixel_size == 8 {
204 let top = top.max(0).min(screen_height) as u32;
205 let left = left.max(0).min(screen_width) as u32;
206 let bottom = bottom.max(0).min(screen_height) as u32;
207 let right = right.max(0).min(screen_width) as u32;
208 if top >= bottom || left >= right {
209 return;
210 }
211 let fill = if black {
212 Self::logical_black_pixel_index(bus)
213 } else {
214 Self::logical_white_pixel_index(bus)
215 };
216 for y in top..bottom {
217 let row_addr = screen_base + y * row_bytes;
218 for x in left..right {
219 bus.write_byte(row_addr + x, fill);
220 }
221 }
222 return;
223 }
224 for y in top..bottom {
225 for x in left..right {
226 Self::fb_set_pixel(
227 bus,
228 screen_base,
229 row_bytes,
230 pixel_size,
231 screen_width,
232 screen_height,
233 x,
234 y,
235 black,
236 );
237 }
238 }
239 }
240
241 pub(crate) fn fb_hline(
243 bus: &mut MacMemoryBus,
244 screen_base: u32,
245 row_bytes: u32,
246 pixel_size: u16,
247 screen_width: i16,
248 screen_height: i16,
249 y: i16,
250 x1: i16,
251 x2: i16,
252 black: bool,
253 ) {
254 if pixel_size == 8 {
255 if y < 0 || y >= screen_height {
256 return;
257 }
258 let left = x1.max(0).min(screen_width) as u32;
259 let right = x2.max(0).min(screen_width) as u32;
260 if left >= right {
261 return;
262 }
263 let fill = if black {
264 Self::logical_black_pixel_index(bus)
265 } else {
266 Self::logical_white_pixel_index(bus)
267 };
268 let row_addr = screen_base + (y as u32) * row_bytes;
269 for x in left..right {
270 bus.write_byte(row_addr + x, fill);
271 }
272 return;
273 }
274 for x in x1..x2 {
275 Self::fb_set_pixel(
276 bus,
277 screen_base,
278 row_bytes,
279 pixel_size,
280 screen_width,
281 screen_height,
282 x,
283 y,
284 black,
285 );
286 }
287 }
288
289 pub(crate) fn fb_draw_char(
291 bus: &mut MacMemoryBus,
292 screen_base: u32,
293 row_bytes: u32,
294 pixel_size: u16,
295 screen_width: i16,
296 screen_height: i16,
297 x: i16,
298 y: i16,
299 ch: char,
300 font_id: i16,
301 font_size: i16,
302 ) -> i16 {
303 if let Some((glyph, data)) = get_glyph(font_id, font_size, ch) {
304 let gx = x + glyph.origin_x as i16;
305 let gy = y + glyph.origin_y as i16;
306 let gw = glyph.width as usize;
307 let gh = glyph.height as usize;
308 let black_index = if pixel_size == 8 {
309 Some(Self::logical_black_pixel_index(bus))
310 } else {
311 None
312 };
313
314 for row in 0..gh {
318 for col in 0..gw {
319 let byte_idx = glyph.data_offset + row * gw + col;
320 if byte_idx < data.len() && data[byte_idx] >= 128 {
321 let px = gx + col as i16;
322 let py = gy + row as i16;
323 if let Some(black_index) = black_index {
324 if px >= 0 && py >= 0 && px < screen_width && py < screen_height {
325 let addr = screen_base + (py as u32) * row_bytes + (px as u32);
326 bus.write_byte(addr, black_index);
327 }
328 } else {
329 Self::fb_set_pixel(
330 bus,
331 screen_base,
332 row_bytes,
333 pixel_size,
334 screen_width,
335 screen_height,
336 px,
337 py,
338 true,
339 );
340 }
341 }
342 }
343 }
344 glyph.advance as i16
345 } else {
346 6 }
348 }
349
350 pub(crate) fn fb_draw_string(
352 bus: &mut MacMemoryBus,
353 screen_base: u32,
354 row_bytes: u32,
355 pixel_size: u16,
356 screen_width: i16,
357 screen_height: i16,
358 x: i16,
359 y: i16,
360 s: &str,
361 font_id: i16,
362 font_size: i16,
363 ) -> i16 {
364 let mut cx = x;
365 for ch in s.chars() {
366 cx += Self::fb_draw_char(
367 bus,
368 screen_base,
369 row_bytes,
370 pixel_size,
371 screen_width,
372 screen_height,
373 cx,
374 y,
375 ch,
376 font_id,
377 font_size,
378 );
379 }
380 cx - x
381 }
382
383 pub(crate) fn draw_menu_bar_to_fb(&self, bus: &mut MacMemoryBus) {
387 if self.fullscreen_locked || self.menu_bar_hidden {
388 return;
389 }
390 let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
391 self.get_screen_params();
392 let menu_bar_height = bus.read_word(crate::memory::globals::addr::MBAR_HEIGHT) as i16;
393 if menu_bar_height <= 0 {
394 return;
395 }
396
397 Self::fb_fill_rect(
399 bus,
400 screen_base,
401 row_bytes,
402 pixel_size,
403 screen_width,
404 screen_height,
405 0,
406 0,
407 menu_bar_height,
408 screen_width,
409 false,
410 );
411
412 Self::fb_hline(
414 bus,
415 screen_base,
416 row_bytes,
417 pixel_size,
418 screen_width,
419 screen_height,
420 menu_bar_height - 1,
421 0,
422 screen_width,
423 true,
424 );
425
426 let font_id: i16 = 0;
428 let font_size: i16 = 12;
429 let metrics = get_font_metrics(font_id, font_size);
430 let text_height = metrics.ascent + metrics.descent;
432 let text_y = (menu_bar_height - text_height) / 2 + metrics.ascent;
433
434 let mut x: i16 = 18;
437 for menu in &self.menus {
438 if !menu.in_menu_bar {
439 continue;
440 }
441 let title = &menu.title;
442 let width = Self::fb_draw_string(
443 bus,
444 screen_base,
445 row_bytes,
446 pixel_size,
447 screen_width,
448 screen_height,
449 x,
450 text_y,
451 title,
452 font_id,
453 font_size,
454 );
455 x += width + 14; }
457 }
458
459 pub(crate) fn blit_window_to_screen(&self, bus: &mut MacMemoryBus) {
466 let (screen_base, screen_rb, screen_w, screen_h, pixel_size) = self.screen_mode;
467 let trace = std::env::var_os("SYSTEMLESS_TRACE_BLIT_WINDOW").is_some();
468 if self.front_window == 0 {
469 if trace {
470 eprintln!("[BLIT] skip: front_window=0");
471 }
472 return;
473 }
474 if pixel_size != 8 || screen_w == 0 || screen_h == 0 {
475 if trace {
476 eprintln!(
477 "[BLIT] skip: screen mode mismatch (pixel_size={}, w={}, h={})",
478 pixel_size, screen_w, screen_h
479 );
480 }
481 return;
482 }
483
484 let port = self.front_window;
488 let port_version = bus.read_word(port + 6);
489 let port_base = if (port_version & 0xC000) == 0xC000 {
490 let pm_handle = bus.read_long(port + 2);
492 if pm_handle == 0 {
493 if trace {
494 eprintln!("[BLIT] skip: CGrafPort pm_handle=0");
495 }
496 return;
497 }
498 let pm_ptr = bus.read_long(pm_handle);
499 if pm_ptr == 0 {
500 if trace {
501 eprintln!(
502 "[BLIT] skip: CGrafPort pm_ptr=0 (pm_handle=${:08X})",
503 pm_handle
504 );
505 }
506 return;
507 }
508 bus.read_long(pm_ptr) & 0x3FFFFFFF } else {
510 bus.read_long(port + 2)
512 };
513
514 if port_base == screen_base || port_base == 0 {
516 if trace {
517 eprintln!(
518 "[BLIT] skip: port_base=${:08X} screen_base=${:08X} \
519 (port draws directly to screen or port_base is NIL)",
520 port_base, screen_base
521 );
522 }
523 return;
524 }
525
526 let port_rb = if (port_version & 0xC000) == 0xC000 {
528 let pm_handle = bus.read_long(port + 2);
529 let pm_ptr = bus.read_long(pm_handle);
530 (bus.read_word(pm_ptr + 4) & 0x3FFF) as u32
531 } else {
532 (bus.read_word(port + 6) & 0x3FFF) as u32
533 };
534
535 let port_pixel_size: u32 = if (port_version & 0xC000) == 0xC000 {
539 let pm_handle = bus.read_long(port + 2);
540 let pm_ptr = bus.read_long(pm_handle);
541 bus.read_word(pm_ptr + 32) as u32
542 } else {
543 1
544 };
545
546 let wr_top = bus.read_word(port + 16) as i16;
548 let wr_left = bus.read_word(port + 18) as i16;
549 let wr_bottom = bus.read_word(port + 20) as i16;
550 let wr_right = bus.read_word(port + 22) as i16;
551
552 let w = (wr_right - wr_left) as u32;
553 let h = (wr_bottom - wr_top) as u32;
554 if w == 0 || h == 0 {
555 return;
556 }
557
558 let src_y_offset = wr_top.max(0) as u32;
559 let src_x_offset = wr_left.max(0) as u32;
560 let dst_y = src_y_offset;
561 let dst_x = src_x_offset;
562
563 if port_pixel_size == 1 && pixel_size == 8 {
568 let (white_index, black_index) = Self::logical_mono_pixel_indexes(bus);
569 let pb_top = bus.read_word(port + 8) as i16;
577 let pb_left = bus.read_word(port + 10) as i16;
578 let pb_bottom = bus.read_word(port + 12) as i16;
579 let pb_right = bus.read_word(port + 14) as i16;
580 let bitmap_w = (pb_right - pb_left).max(0) as u32;
581 let bitmap_h = (pb_bottom - pb_top).max(0) as u32;
582 let row_count = h.min((screen_h as u32).saturating_sub(dst_y)).min(bitmap_h);
583 let col_count = w.min((screen_w as u32).saturating_sub(dst_x)).min(bitmap_w);
584 for row in 0..row_count {
585 let src_row_addr = port_base + (src_y_offset + row) * port_rb;
586 let dst_row_addr = screen_base + (dst_y + row) * screen_rb + dst_x;
587 for col in 0..col_count {
588 let src_bit_x = src_x_offset + col;
589 let src_byte = bus.read_byte(src_row_addr + src_bit_x / 8);
590 let bit = (src_byte >> (7 - (src_bit_x & 7))) & 1;
591 let dst_idx = if bit == 0 { white_index } else { black_index };
592 bus.write_byte(dst_row_addr + col, dst_idx);
593 }
594 }
595 return;
596 }
597
598 if port_pixel_size != pixel_size as u32 {
601 return;
602 }
603
604 for row in 0..h.min(screen_h as u32 - dst_y) {
606 let src_addr = port_base + (src_y_offset + row) * port_rb + src_x_offset;
607 let dst_addr = screen_base + (dst_y + row) * screen_rb + dst_x;
608 let copy_w = w.min(screen_w as u32 - dst_x);
609 bus.block_move(src_addr, dst_addr, copy_w);
610 }
611 }
612
613 pub(crate) fn draw_window_chrome(&self, bus: &mut MacMemoryBus, active: bool) {
616 let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
617 self.get_screen_params();
618 let (wind_top, wind_left, wind_bottom, wind_right) = self.window_bounds;
619
620 let menu_bar_height = bus.read_word(crate::memory::globals::addr::MBAR_HEIGHT) as i16;
624 let tb_top = (wind_top - 19).max(menu_bar_height);
625 let tb_bottom = wind_top - 1;
626 let tb_left = wind_left - 1;
627 let tb_right = wind_right + 1;
628
629 Self::fb_fill_rect(
631 bus,
632 screen_base,
633 row_bytes,
634 pixel_size,
635 screen_width,
636 screen_height,
637 tb_top,
638 tb_left,
639 tb_bottom + 1,
640 tb_right,
641 false,
642 );
643
644 Self::fb_hline(
646 bus,
647 screen_base,
648 row_bytes,
649 pixel_size,
650 screen_width,
651 screen_height,
652 tb_top,
653 tb_left,
654 tb_right,
655 true,
656 );
657 Self::fb_hline(
658 bus,
659 screen_base,
660 row_bytes,
661 pixel_size,
662 screen_width,
663 screen_height,
664 tb_bottom,
665 tb_left,
666 tb_right,
667 true,
668 );
669 for y in tb_top..=tb_bottom {
671 Self::fb_set_pixel(
672 bus,
673 screen_base,
674 row_bytes,
675 pixel_size,
676 screen_width,
677 screen_height,
678 tb_left,
679 y,
680 true,
681 );
682 Self::fb_set_pixel(
683 bus,
684 screen_base,
685 row_bytes,
686 pixel_size,
687 screen_width,
688 screen_height,
689 tb_right - 1,
690 y,
691 true,
692 );
693 }
694
695 let font_id: i16 = 0; let font_size: i16 = 12;
698 let metrics = get_font_metrics(font_id, font_size);
699 let text_height = metrics.ascent + metrics.descent;
700 let tb_interior_height = tb_bottom - tb_top - 1;
701 let text_y = tb_top + 1 + (tb_interior_height - text_height) / 2 + metrics.ascent;
702
703 let (title_clear_left, title_clear_right) = if !self.window_title.is_empty() {
704 let mut title_width: i16 = 0;
705 for ch in self.window_title.chars() {
706 if let Some((glyph, _)) = get_glyph(font_id, font_size, ch) {
707 title_width += glyph.advance as i16;
708 } else {
709 title_width += 6;
710 }
711 }
712 let text_x = tb_left + (tb_right - tb_left - title_width) / 2;
713 (text_x - 8, text_x + title_width + 8)
714 } else {
715 (tb_right, tb_right) };
717
718 let is_movable_modal = self.window_proc_id == 5;
719 let has_go_away = active && matches!(self.window_proc_id, 0 | 4) && self.go_away_flag;
720 let _close_box_width = if has_go_away { 15i16 } else { 0 };
721
722 if is_movable_modal {
723 if !self.window_title.is_empty() {
726 let text_x = title_clear_left + 8;
727 Self::fb_draw_string(
728 bus,
729 screen_base,
730 row_bytes,
731 pixel_size,
732 screen_width,
733 screen_height,
734 text_x,
735 text_y,
736 &self.window_title,
737 font_id,
738 font_size,
739 );
740 }
741 } else {
742 if has_go_away {
760 let cb_size: i16 = 11;
761 let interior_top = tb_top + 1;
762 let interior_height = tb_bottom - interior_top;
763 let cb_top = interior_top + (interior_height - cb_size) / 2;
764 let cb_left = tb_left + 9; Self::fb_hline(
768 bus,
769 screen_base,
770 row_bytes,
771 pixel_size,
772 screen_width,
773 screen_height,
774 cb_top,
775 cb_left,
776 cb_left + cb_size,
777 true,
778 );
779 for y in cb_top..(cb_top + cb_size) {
780 Self::fb_set_pixel(
781 bus,
782 screen_base,
783 row_bytes,
784 pixel_size,
785 screen_width,
786 screen_height,
787 cb_left,
788 y,
789 true,
790 );
791 }
792
793 let inner_right = cb_left + cb_size - 2; let inner_bottom = cb_top + cb_size - 2; for y in (cb_top + 2)..(cb_top + cb_size - 1) {
798 Self::fb_set_pixel(
799 bus,
800 screen_base,
801 row_bytes,
802 pixel_size,
803 screen_width,
804 screen_height,
805 inner_right,
806 y,
807 true,
808 );
809 }
810 Self::fb_hline(
811 bus,
812 screen_base,
813 row_bytes,
814 pixel_size,
815 screen_width,
816 screen_height,
817 inner_bottom,
818 cb_left + 2,
819 cb_left + cb_size - 1,
820 true,
821 );
822 }
823
824 if active {
832 let stripe_left_edge = tb_left + 2;
833 let stripe_right_end = tb_right - 2;
834 let stripe_text_left = title_clear_left + 2;
835 let stripe_text_right = title_clear_right - 2;
836
837 let (cb_gap_left, cb_gap_right) = if has_go_away {
839 let cb_left = tb_left + 9;
840 let cb_right = cb_left + 10; (cb_left - 1, cb_right + 2) } else {
843 (stripe_right_end, stripe_right_end) };
845
846 for y in (tb_top + 4)..=(tb_bottom - 3) {
847 if (y - tb_top) % 2 == 0 {
848 let seg1_end = if has_go_away {
851 cb_gap_left
852 } else {
853 stripe_text_left
854 };
855 if stripe_left_edge < seg1_end {
856 Self::fb_hline(
857 bus,
858 screen_base,
859 row_bytes,
860 pixel_size,
861 screen_width,
862 screen_height,
863 y,
864 stripe_left_edge,
865 seg1_end,
866 true,
867 );
868 }
869 if has_go_away && cb_gap_right < stripe_text_left {
871 Self::fb_hline(
872 bus,
873 screen_base,
874 row_bytes,
875 pixel_size,
876 screen_width,
877 screen_height,
878 y,
879 cb_gap_right,
880 stripe_text_left,
881 true,
882 );
883 }
884 if stripe_text_right < stripe_right_end {
886 Self::fb_hline(
887 bus,
888 screen_base,
889 row_bytes,
890 pixel_size,
891 screen_width,
892 screen_height,
893 y,
894 stripe_text_right,
895 stripe_right_end,
896 true,
897 );
898 }
899 }
900 }
901 }
902
903 if active && !self.window_title.is_empty() {
905 let text_x = title_clear_left + 8;
906 Self::fb_draw_string(
907 bus,
908 screen_base,
909 row_bytes,
910 pixel_size,
911 screen_width,
912 screen_height,
913 text_x,
914 text_y,
915 &self.window_title,
916 font_id,
917 font_size,
918 );
919 }
920 }
921
922 for y in wind_top..wind_bottom {
924 Self::fb_set_pixel(
925 bus,
926 screen_base,
927 row_bytes,
928 pixel_size,
929 screen_width,
930 screen_height,
931 wind_left - 1,
932 y,
933 true,
934 );
935 Self::fb_set_pixel(
936 bus,
937 screen_base,
938 row_bytes,
939 pixel_size,
940 screen_width,
941 screen_height,
942 wind_right,
943 y,
944 true,
945 );
946 }
947 Self::fb_hline(
949 bus,
950 screen_base,
951 row_bytes,
952 pixel_size,
953 screen_width,
954 screen_height,
955 wind_bottom,
956 wind_left - 1,
957 wind_right + 1,
958 true,
959 );
960
961 for y in (tb_top + 1)..=(wind_bottom + 1) {
963 Self::fb_set_pixel(
964 bus,
965 screen_base,
966 row_bytes,
967 pixel_size,
968 screen_width,
969 screen_height,
970 wind_right + 1,
971 y,
972 true,
973 );
974 }
975 Self::fb_hline(
976 bus,
977 screen_base,
978 row_bytes,
979 pixel_size,
980 screen_width,
981 screen_height,
982 wind_bottom + 1,
983 tb_left + 1,
984 wind_right + 2,
985 true,
986 );
987 }
988
989 pub(crate) fn draw_grow_icon(&self, bus: &mut MacMemoryBus, window_ptr: u32) {
993 let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
994 self.get_screen_params();
995 let port_top = bus.read_word(window_ptr + 16) as i16;
997 let port_left = bus.read_word(window_ptr + 18) as i16;
998 let port_bottom = bus.read_word(window_ptr + 20) as i16;
999 let port_right = bus.read_word(window_ptr + 22) as i16;
1000 let port_version = bus.read_word(window_ptr + 6);
1002 let (scr_top, scr_left) = if (port_version & 0xC000) == 0xC000 {
1003 let pm_handle = bus.read_long(window_ptr + 2);
1004 if pm_handle != 0 {
1005 let pm_ptr = bus.read_long(pm_handle);
1006 let bt = bus.read_word(pm_ptr + 6) as i16;
1007 let bl = bus.read_word(pm_ptr + 8) as i16;
1008 (-bt, -bl)
1009 } else {
1010 (0, 0)
1011 }
1012 } else {
1013 let bt = bus.read_word(window_ptr + 8) as i16;
1014 let bl = bus.read_word(window_ptr + 10) as i16;
1015 (-bt, -bl)
1016 };
1017
1018 let content_top = scr_top + port_top;
1020 let content_left = scr_left + port_left;
1021 let content_bottom = scr_top + port_bottom;
1022 let content_right = scr_left + port_right;
1023
1024 let sep_x = content_right - 15;
1028 let sep_y = content_bottom - 15;
1029 let border_left = content_left - 1;
1030 let border_right = content_right + 1;
1031
1032 for y in content_top..content_bottom {
1034 Self::fb_set_pixel(
1035 bus,
1036 screen_base,
1037 row_bytes,
1038 pixel_size,
1039 screen_width,
1040 screen_height,
1041 sep_x,
1042 y,
1043 true,
1044 );
1045 }
1046 Self::fb_hline(
1048 bus,
1049 screen_base,
1050 row_bytes,
1051 pixel_size,
1052 screen_width,
1053 screen_height,
1054 sep_y,
1055 border_left,
1056 border_right + 1,
1057 true,
1058 );
1059 }
1060
1061 pub(crate) fn draw_thick_rect_border(
1066 &self,
1067 bus: &mut MacMemoryBus,
1068 top: i16,
1069 left: i16,
1070 bottom: i16,
1071 right: i16,
1072 ) {
1073 let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
1074 self.get_screen_params();
1075 for dy in 0..2i16 {
1077 Self::fb_hline(
1078 bus,
1079 screen_base,
1080 row_bytes,
1081 pixel_size,
1082 screen_width,
1083 screen_height,
1084 top + dy,
1085 left,
1086 right,
1087 true,
1088 );
1089 }
1090 Self::fb_hline(
1092 bus,
1093 screen_base,
1094 row_bytes,
1095 pixel_size,
1096 screen_width,
1097 screen_height,
1098 bottom - 1,
1099 left,
1100 right,
1101 true,
1102 );
1103 for dx in 0..2i16 {
1105 for y in top..bottom {
1106 Self::fb_set_pixel(
1107 bus,
1108 screen_base,
1109 row_bytes,
1110 pixel_size,
1111 screen_width,
1112 screen_height,
1113 left + dx,
1114 y,
1115 true,
1116 );
1117 }
1118 }
1119 for dx in 0..2i16 {
1121 for y in top..bottom {
1122 Self::fb_set_pixel(
1123 bus,
1124 screen_base,
1125 row_bytes,
1126 pixel_size,
1127 screen_width,
1128 screen_height,
1129 right - 2 + dx,
1130 y,
1131 true,
1132 );
1133 }
1134 }
1135 }
1136
1137 fn erase_structure_region(
1140 &self,
1141 bus: &mut MacMemoryBus,
1142 top: i16,
1143 left: i16,
1144 bottom: i16,
1145 right: i16,
1146 ) {
1147 let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
1148 self.get_screen_params();
1149 Self::fb_fill_rect(
1150 bus,
1151 screen_base,
1152 row_bytes,
1153 pixel_size,
1154 screen_width,
1155 screen_height,
1156 top,
1157 left,
1158 bottom,
1159 right,
1160 false,
1161 );
1162 }
1163
1164 fn erase_structure_frame_around_content(
1169 &self,
1170 bus: &mut MacMemoryBus,
1171 structure: (i16, i16, i16, i16),
1172 content: (i16, i16, i16, i16),
1173 ) {
1174 let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
1175 self.get_screen_params();
1176 let (st, sl, sb, sr) = structure;
1177 let (ct, cl, cb, cr) = content;
1178 let mut erase = |top: i16, left: i16, bottom: i16, right: i16| {
1179 if bottom <= top || right <= left {
1180 return;
1181 }
1182 Self::fb_fill_rect(
1183 bus,
1184 screen_base,
1185 row_bytes,
1186 pixel_size,
1187 screen_width,
1188 screen_height,
1189 top,
1190 left,
1191 bottom,
1192 right,
1193 false,
1194 );
1195 };
1196
1197 erase(st, sl, ct.min(sb), sr);
1198 erase(cb.max(st), sl, sb, sr);
1199 erase(ct.max(st), sl, cb.min(sb), cl.min(sr));
1200 erase(ct.max(st), cr.max(sl), cb.min(sb), sr);
1201 }
1202
1203 pub(crate) fn draw_single_window_chrome_inline(
1217 &mut self,
1218 bus: &mut MacMemoryBus,
1219 window_ptr: u32,
1220 hilited: bool,
1221 ) {
1222 if window_ptr == 0 {
1223 return;
1224 }
1225 if bus.read_byte(window_ptr + 110u32) == 0 {
1226 return; }
1228 let proc_id = self.window_proc_ids.get(&window_ptr).copied().unwrap_or(0);
1233 let port_version = bus.read_word(window_ptr + 6);
1234 let (pmap_top, pmap_left) = if (port_version & 0xC000) == 0xC000 {
1235 let pm_handle = bus.read_long(window_ptr + 2);
1236 let pm_ptr = bus.read_long(pm_handle);
1237 (
1238 bus.read_word(pm_ptr + 6) as i16,
1239 bus.read_word(pm_ptr + 8) as i16,
1240 )
1241 } else {
1242 (
1243 bus.read_word(window_ptr + 8) as i16,
1244 bus.read_word(window_ptr + 10) as i16,
1245 )
1246 };
1247 let wind_top = pmap_top.wrapping_neg();
1252 let wind_left = pmap_left.wrapping_neg();
1253 let port_bottom = bus.read_word(window_ptr + 20) as i16;
1254 let port_right = bus.read_word(window_ptr + 22) as i16;
1255 let wind_bottom = wind_top.wrapping_add(port_bottom);
1256 let wind_right = wind_left.wrapping_add(port_right);
1257 if wind_bottom <= wind_top || wind_right <= wind_left {
1258 return;
1259 }
1260 let saved_bounds = self.window_bounds;
1261 let saved_title = self.window_title.clone();
1262 let saved_go_away = self.go_away_flag;
1263 let saved_proc = self.window_proc_id;
1264 self.window_bounds = (wind_top, wind_left, wind_bottom, wind_right);
1265 let title_h = bus.read_long(window_ptr + 134u32);
1266 self.window_title = if title_h != 0 {
1267 let title_p = bus.read_long(title_h);
1268 if title_p != 0 {
1269 String::from_utf8_lossy(&bus.read_pstring(title_p)).into_owned()
1270 } else {
1271 String::new()
1272 }
1273 } else {
1274 String::new()
1275 };
1276 self.go_away_flag = bus.read_byte(window_ptr + 112u32) != 0;
1277 self.window_proc_id = proc_id;
1278 if matches!(proc_id, 1..=3) {
1279 self.draw_window_frame(bus);
1280 } else {
1281 self.draw_window_chrome(bus, hilited);
1282 }
1283 self.window_bounds = saved_bounds;
1284 self.window_title = saved_title;
1285 self.go_away_flag = saved_go_away;
1286 self.window_proc_id = saved_proc;
1287 }
1288
1289 pub(crate) fn draw_window_frame(&self, bus: &mut MacMemoryBus) {
1290 let (wind_top, wind_left, wind_bottom, wind_right) = self.window_bounds;
1291 match self.window_proc_id {
1292 2 => {
1293 self.draw_rect_border(
1299 bus,
1300 wind_top - 1,
1301 wind_left - 1,
1302 wind_bottom + 1,
1303 wind_right + 1,
1304 );
1305 }
1306 1 => {
1307 let struc_top = wind_top - 8;
1314 let struc_left = wind_left - 8;
1315 let struc_bottom = wind_bottom + 3;
1316 let struc_right = wind_right + 8;
1317 self.erase_structure_frame_around_content(
1318 bus,
1319 (struc_top, struc_left, struc_bottom, struc_right),
1320 (wind_top, wind_left, wind_bottom, wind_right),
1321 );
1322 self.draw_rect_border(bus, struc_top, struc_left, struc_bottom, struc_right);
1324 self.draw_thick_rect_border(
1326 bus,
1327 wind_top - 5,
1328 wind_left - 5,
1329 struc_bottom,
1330 wind_right + 5,
1331 );
1332 }
1333 3 => {
1334 self.erase_structure_frame_around_content(
1336 bus,
1337 (wind_top - 1, wind_left - 1, wind_bottom + 3, wind_right + 3),
1338 (wind_top, wind_left, wind_bottom, wind_right),
1339 );
1340 self.draw_rect_border(
1341 bus,
1342 wind_top - 1,
1343 wind_left - 1,
1344 wind_bottom + 1,
1345 wind_right + 1,
1346 );
1347 self.draw_shadow(
1349 bus,
1350 wind_top - 1,
1351 wind_left - 1,
1352 wind_bottom + 1,
1353 wind_right + 1,
1354 );
1355 }
1356 5 => {
1357 let struc_top = wind_top - 23;
1362 let struc_left = wind_left - 8;
1363 let struc_bottom = wind_bottom + 8;
1364 let struc_right = wind_right + 8;
1365 self.erase_structure_frame_around_content(
1366 bus,
1367 (struc_top, struc_left, struc_bottom, struc_right),
1368 (wind_top, wind_left, wind_bottom, wind_right),
1369 );
1370 self.draw_rect_border(bus, struc_top, struc_left, struc_bottom, struc_right);
1372 let thick_top = wind_top - 5;
1374 let thick_left = wind_left - 5;
1375 let thick_bottom = wind_bottom + 5;
1376 let thick_right = wind_right + 5;
1377 self.draw_thick_rect_border(bus, thick_top, thick_left, thick_bottom, thick_right);
1378 let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
1380 self.get_screen_params();
1381 Self::fb_hline(
1382 bus,
1383 screen_base,
1384 row_bytes,
1385 pixel_size,
1386 screen_width,
1387 screen_height,
1388 thick_bottom - 2,
1389 thick_left,
1390 thick_right,
1391 true,
1392 );
1393 }
1394 0 | 4 => {
1395 let tb_top = wind_top - 19;
1398 self.erase_structure_region(
1399 bus,
1400 tb_top,
1401 wind_left - 1,
1402 wind_bottom + 2,
1403 wind_right + 2,
1404 );
1405 self.draw_window_chrome(bus, true);
1406 }
1407 _ => {
1408 self.draw_rect_border(
1410 bus,
1411 wind_top - 1,
1412 wind_left - 1,
1413 wind_bottom + 1,
1414 wind_right + 1,
1415 );
1416 }
1417 }
1418 }
1419
1420 pub fn redraw_chrome(&mut self, bus: &mut MacMemoryBus) {
1427 let pending_dialog_snapshot = self
1438 .dialog_tracking
1439 .as_ref()
1440 .map(|tracking| !tracking.game_managed && !tracking.rendered_pixels_final)
1441 .unwrap_or(false);
1442 if !pending_dialog_snapshot {
1443 self.blit_window_to_screen(bus);
1444 }
1445
1446 let menu_bar_height = bus.read_word(crate::memory::globals::addr::MBAR_HEIGHT) as i16;
1447 let (_, _, screen_w, screen_h, _) = self.screen_mode;
1448
1449 let (wt, wl, wb, wr) = self.window_bounds;
1455 if self.fullscreen_locked && menu_bar_height > 0 {
1456 self.fullscreen_locked = false;
1457 }
1458
1459 if self.front_window != 0
1460 && wt <= 0
1461 && wl <= 0
1462 && wb >= screen_h as i16
1463 && wr >= screen_w as i16
1464 && menu_bar_height <= 0
1465 {
1466 self.fullscreen_locked = true;
1467 }
1468
1469 if !self.menus.is_empty() && !self.fullscreen_locked && !self.menu_bar_hidden {
1470 self.draw_menu_bar_to_fb(bus);
1471 }
1472 let effective_mbar = if self.fullscreen_locked || self.menu_bar_hidden {
1491 0
1492 } else {
1493 menu_bar_height
1494 };
1495 if self.front_window != 0 && !no_visrgn_auto_expand_enabled() && effective_mbar == 0 {
1501 let vis_top_expected = 0i16;
1502 let vis_rgn_handle = bus.read_long(self.front_window + 24);
1503 if vis_rgn_handle != 0 {
1504 let vis_rgn = bus.read_long(vis_rgn_handle);
1505 if vis_rgn != 0 {
1506 let current_vis_top = bus.read_word(vis_rgn + 2) as i16;
1507 if current_vis_top != vis_top_expected {
1508 bus.write_word(vis_rgn + 2, vis_top_expected as u16);
1509 let clip_rgn_handle = bus.read_long(self.front_window + 28);
1511 if clip_rgn_handle != 0 {
1512 let clip_rgn = bus.read_long(clip_rgn_handle);
1513 if clip_rgn != 0 {
1514 let clip_top = bus.read_word(clip_rgn + 2) as i16;
1515 if clip_top > vis_top_expected {
1516 bus.write_word(clip_rgn + 2, vis_top_expected as u16);
1517 }
1518 }
1519 }
1520 let port_top = bus.read_word(self.front_window + 16) as i16;
1521 if port_top > vis_top_expected {
1522 bus.write_word(self.front_window + 16, vis_top_expected as u16);
1523 }
1524 self.window_bounds.0 = vis_top_expected;
1525 }
1526 }
1527 }
1528 }
1529
1530 let skip_chrome = matches!(self.window_proc_id, 1 | 2 | 3 | 5) || effective_mbar <= 0;
1531
1532 if !skip_chrome && effective_mbar > 0 {
1538 let list_snapshot = self.window_list.clone();
1539 let saved_bounds = self.window_bounds;
1540 let saved_title = self.window_title.clone();
1541 let saved_proc = self.window_proc_id;
1542 let saved_go_away = self.go_away_flag;
1543 for &w in list_snapshot.iter().rev() {
1546 if w == self.front_window {
1547 continue;
1548 }
1549 if bus.read_byte(w + 110u32) == 0 {
1550 continue;
1552 }
1553 let port_version = bus.read_word(w + 6);
1560 let (pmap_top, pmap_left) = if (port_version & 0xC000) == 0xC000 {
1561 let pm_handle = bus.read_long(w + 2);
1562 let pm_ptr = bus.read_long(pm_handle);
1563 (
1564 bus.read_word(pm_ptr + 6) as i16,
1565 bus.read_word(pm_ptr + 8) as i16,
1566 )
1567 } else {
1568 (bus.read_word(w + 8) as i16, bus.read_word(w + 10) as i16)
1569 };
1570 let wind_top = -pmap_top;
1571 let wind_left = -pmap_left;
1572 let port_bottom = bus.read_word(w + 20) as i16;
1573 let port_right = bus.read_word(w + 22) as i16;
1574 let wind_bottom = wind_top + port_bottom;
1575 let wind_right = wind_left + port_right;
1576 if wind_bottom <= wind_top || wind_right <= wind_left {
1578 continue;
1579 }
1580 self.window_bounds = (wind_top, wind_left, wind_bottom, wind_right);
1581 let title_h = bus.read_long(w + 134u32);
1583 self.window_title = if title_h != 0 {
1584 let title_p = bus.read_long(title_h);
1585 if title_p != 0 {
1586 String::from_utf8_lossy(&bus.read_pstring(title_p)).into_owned()
1587 } else {
1588 String::new()
1589 }
1590 } else {
1591 String::new()
1592 };
1593 self.go_away_flag = bus.read_byte(w + 112u32) != 0;
1594 let w_proc = self.window_proc_ids.get(&w).copied().unwrap_or(0);
1597 self.window_proc_id = w_proc;
1598 let hilited = bus.read_byte(w + 111u32) != 0;
1599 if matches!(w_proc, 1..=3) {
1600 self.draw_window_frame(bus);
1601 } else {
1602 self.draw_window_chrome(bus, hilited);
1603 }
1604 }
1605 self.window_bounds = saved_bounds;
1607 self.window_title = saved_title;
1608 self.window_proc_id = saved_proc;
1609 self.go_away_flag = saved_go_away;
1610 }
1611
1612 if self.front_window != 0 && !skip_chrome {
1613 let front_hilited = bus.read_byte(self.front_window + 111u32) != 0;
1616 self.draw_window_chrome(bus, front_hilited);
1617 }
1618 if let Some(ref tracking) = self.dialog_tracking {
1627 if !tracking.game_managed && tracking.rendered_pixels_final {
1628 self.restore_dialog_pixels(bus, tracking.bounds, &tracking.rendered_pixels);
1630
1631 if tracking.edit_item > 0 {
1633 let idx = (tracking.edit_item - 1) as usize;
1634 if idx < tracking.items.len() {
1635 let item = &tracking.items[idx];
1636 let abs_top = tracking.bounds.0 + item.rect.0;
1637 let abs_left = tracking.bounds.1 + item.rect.1;
1638 let abs_bottom = tracking.bounds.0 + item.rect.2;
1639 let abs_right = tracking.bounds.1 + item.rect.3;
1640 self.draw_edit_text(
1641 bus,
1642 abs_top,
1643 abs_left,
1644 abs_bottom,
1645 abs_right,
1646 &tracking.edit_text,
1647 !tracking.edit_text_modified,
1648 );
1649 }
1650 }
1651
1652 if tracking.flash_remaining > 0 && tracking.flash_item > 0 {
1654 let fi = tracking.flash_item;
1655 if (fi as usize) <= tracking.items.len() {
1656 let item = &tracking.items[(fi - 1) as usize];
1657 let (it, il, ib, ir) = item.rect;
1658 let abs_top = tracking.bounds.0 + it;
1659 let abs_left = tracking.bounds.1 + il;
1660 let abs_bottom = tracking.bounds.0 + ib;
1661 let abs_right = tracking.bounds.1 + ir;
1662 if tracking.flash_remaining % 2 == 0 {
1663 self.invert_button_rect(bus, abs_top, abs_left, abs_bottom, abs_right);
1664 }
1665 }
1666 }
1667
1668 if let Some(ref popup) = tracking.active_popup {
1669 self.draw_menu_dropdown(bus, popup.active_menu, popup.dropdown_rect);
1670 if popup.highlighted_item > 0 {
1671 self.invert_dropdown_item_rect(
1672 bus,
1673 popup.dropdown_rect,
1674 popup.highlighted_item,
1675 );
1676 }
1677 }
1678 }
1679 }
1680
1681 if let Some(ref tracking) = self.menu_tracking {
1684 self.highlight_menu_title(bus, tracking.active_menu);
1685 self.draw_menu_dropdown(bus, tracking.active_menu, tracking.dropdown_rect);
1686 let show_highlight = if tracking.flash_remaining > 0 {
1689 tracking.flash_remaining % 2 == 0
1690 } else {
1691 true
1692 };
1693 if tracking.highlighted_item > 0 && show_highlight {
1694 self.invert_menu_item(bus, tracking.highlighted_item);
1695 }
1696 }
1697 }
1698}
1699
1700#[cfg(test)]
1701mod redraw_chrome_tests {
1702 use super::super::test_helpers::setup_with_port;
1703 use super::super::TrapDispatcher;
1704 use crate::memory::MemoryBus;
1705
1706 const PORT_PTR: u32 = 0x181000;
1714 const VIS_RGN: u32 = 0x182000;
1715 const CLIP_RGN: u32 = 0x182200;
1716
1717 fn install_twilight_style_black_index(
1718 disp: &mut TrapDispatcher,
1719 bus: &mut crate::memory::MacMemoryBus,
1720 ) {
1721 let gdevice_handle = disp.ensure_main_gdevice(bus);
1722 bus.write_long(0x08A4, gdevice_handle);
1723 bus.write_long(0x0CC8, gdevice_handle);
1724
1725 let gdevice = bus.read_long(gdevice_handle);
1726 let pixmap_handle = bus.read_long(gdevice + 22);
1727 let pixmap = bus.read_long(pixmap_handle);
1728 let ctab_handle = bus.read_long(pixmap + 42);
1729 let ctab = bus.read_long(ctab_handle);
1730
1731 let black_entry = ctab + 8 + 8;
1732 bus.write_word(black_entry, 1);
1733 bus.write_word(black_entry + 2, 0);
1734 bus.write_word(black_entry + 4, 0);
1735 bus.write_word(black_entry + 6, 0);
1736
1737 let repurposed_entry = ctab + 8 + 255 * 8;
1738 bus.write_word(repurposed_entry, 255);
1739 bus.write_word(repurposed_entry + 2, 0xFFFF);
1740 bus.write_word(repurposed_entry + 4, 0xFFFF);
1741 bus.write_word(repurposed_entry + 6, 0xCCCC);
1742 }
1743
1744 #[test]
1748 fn redraw_chrome_preserves_window_bounds_when_menu_bar_visible() {
1749 let (mut disp, _cpu, mut bus) = setup_with_port();
1750
1751 bus.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 20);
1754 bus.write_word(VIS_RGN + 2, 40); bus.write_word(CLIP_RGN + 2, 40); bus.write_word(PORT_PTR + 16, 40); disp.front_window = PORT_PTR;
1758 disp.window_bounds = (40, 2, 339, 508);
1759 disp.window_proc_id = 0; disp.fullscreen_locked = false;
1761 disp.menu_bar_hidden = false;
1764
1765 disp.redraw_chrome(&mut bus);
1766
1767 assert_eq!(
1768 disp.window_bounds.0, 40,
1769 "window_bounds.0 must not be clobbered when MBarHeight>0"
1770 );
1771 assert_eq!(
1772 bus.read_word(VIS_RGN + 2) as i16,
1773 40,
1774 "visRgn.top must not be rewritten when MBarHeight>0"
1775 );
1776 assert_eq!(
1777 bus.read_word(PORT_PTR + 16) as i16,
1778 40,
1779 "port_top must not be rewritten when MBarHeight>0"
1780 );
1781 }
1782
1783 #[test]
1788 fn redraw_chrome_expands_visrgn_when_menu_bar_hidden() {
1789 let (mut disp, _cpu, mut bus) = setup_with_port();
1790
1791 bus.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 0);
1794 bus.write_word(VIS_RGN + 2, 20); bus.write_word(CLIP_RGN + 2, 20); bus.write_word(PORT_PTR + 16, 20); disp.front_window = PORT_PTR;
1798 disp.window_bounds = (20, 0, 342, 512);
1799 disp.window_proc_id = 0; disp.fullscreen_locked = false;
1801
1802 disp.redraw_chrome(&mut bus);
1803
1804 assert_eq!(
1805 bus.read_word(VIS_RGN + 2) as i16,
1806 0,
1807 "visRgn.top must be expanded to 0 when MBarHeight=0"
1808 );
1809 assert_eq!(
1810 disp.window_bounds.0, 0,
1811 "window_bounds.0 must be updated to match expanded visRgn"
1812 );
1813 assert_eq!(
1814 bus.read_word(PORT_PTR + 16) as i16,
1815 0,
1816 "port_top must be updated to match expanded visRgn"
1817 );
1818 }
1819
1820 #[test]
1827 fn redraw_chrome_expands_visrgn_when_host_hides_menu_bar() {
1828 let (mut disp, _cpu, mut bus) = setup_with_port();
1829
1830 bus.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 20);
1834 bus.write_word(VIS_RGN + 2, 20); bus.write_word(CLIP_RGN + 2, 20);
1836 bus.write_word(PORT_PTR + 16, 20);
1837 disp.front_window = PORT_PTR;
1838 disp.window_bounds = (20, 0, 342, 512);
1839 disp.window_proc_id = 0; disp.fullscreen_locked = false;
1841 disp.menu_bar_hidden = true;
1842
1843 disp.redraw_chrome(&mut bus);
1844
1845 assert_eq!(
1846 bus.read_word(VIS_RGN + 2) as i16,
1847 0,
1848 "visRgn.top must be expanded to 0 when host hides the menu bar"
1849 );
1850 assert_eq!(
1851 disp.window_bounds.0, 0,
1852 "window_bounds.0 must follow visRgn.top up to 0"
1853 );
1854 assert_eq!(
1855 bus.read_word(PORT_PTR + 16) as i16,
1856 0,
1857 "port_top must follow visRgn.top up to 0"
1858 );
1859 }
1860
1861 #[test]
1868 fn redraw_chrome_does_not_paint_menu_bar_when_menu_bar_hidden() {
1869 let (mut disp, _cpu, mut bus) = setup_with_port();
1870
1871 let screen_base = bus.alloc(800 * 600);
1875 disp.screen_mode = (screen_base, 800, 800, 600, 8);
1876 bus.write_long(0x0824, screen_base);
1877
1878 for i in 0u32..(800 * 20) {
1881 bus.write_byte(screen_base + i, 0xAA);
1882 }
1883
1884 bus.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 20);
1887 disp.menus.push(super::super::menu::Menu {
1888 id: 1,
1889 title: String::from("Apple"),
1890 items: Vec::new(),
1891 enabled: true,
1892 handle: 0,
1893 in_menu_bar: true,
1894 });
1895 disp.front_window = 0;
1899 disp.fullscreen_locked = false;
1900 disp.menu_bar_hidden = true;
1901
1902 disp.redraw_chrome(&mut bus);
1903
1904 for &x in &[0u32, 100, 400, 799] {
1908 for &y in &[0u32, 5, 10, 19] {
1909 let off = y * 800 + x;
1910 assert_eq!(
1911 bus.read_byte(screen_base + off),
1912 0xAA,
1913 "menu bar pixel at (x={}, y={}) should not be repainted \
1914 when menu_bar_hidden=true",
1915 x,
1916 y
1917 );
1918 }
1919 }
1920 }
1921
1922 #[test]
1927 fn redraw_chrome_paints_menu_bar_when_not_hidden() {
1928 let (mut disp, _cpu, mut bus) = setup_with_port();
1929
1930 let screen_base = bus.alloc(800 * 600);
1931 disp.screen_mode = (screen_base, 800, 800, 600, 8);
1932 bus.write_long(0x0824, screen_base);
1933
1934 for i in 0u32..(800 * 20) {
1936 bus.write_byte(screen_base + i, 0xAA);
1937 }
1938
1939 bus.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 20);
1940 disp.menus.push(super::super::menu::Menu {
1941 id: 1,
1942 title: String::from("Apple"),
1943 items: Vec::new(),
1944 enabled: true,
1945 handle: 0,
1946 in_menu_bar: true,
1947 });
1948 disp.front_window = 0;
1949 disp.fullscreen_locked = false;
1950 disp.menu_bar_hidden = false;
1951
1952 disp.redraw_chrome(&mut bus);
1953
1954 for &x in &[100u32, 400, 700] {
1960 for &y in &[5u32, 10] {
1961 let off = y * 800 + x;
1962 let pix = bus.read_byte(screen_base + off);
1963 assert_ne!(
1964 pix, 0xAA,
1965 "menu bar pixel at (x={}, y={}) should be painted \
1966 when menu_bar_hidden=false (read 0x{:02X}, sentinel 0xAA)",
1967 x, y, pix
1968 );
1969 }
1970 }
1971 }
1972
1973 #[test]
1983 fn redraw_chrome_does_not_paint_on_mouse_at_top_when_hidden() {
1984 let (mut disp, _cpu, mut bus) = setup_with_port();
1985
1986 let screen_base = bus.alloc(800 * 600);
1987 disp.screen_mode = (screen_base, 800, 800, 600, 8);
1988 bus.write_long(0x0824, screen_base);
1989
1990 for i in 0u32..(800 * 20) {
1992 bus.write_byte(screen_base + i, 0xAA);
1993 }
1994
1995 bus.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 20);
1996 disp.menus.push(super::super::menu::Menu {
1997 id: 1,
1998 title: String::from("Apple"),
1999 items: Vec::new(),
2000 enabled: true,
2001 handle: 0,
2002 in_menu_bar: true,
2003 });
2004 disp.front_window = 0;
2005 disp.fullscreen_locked = false;
2006 disp.menu_bar_hidden = true;
2007
2008 disp.redraw_chrome(&mut bus);
2010 for x in [0u32, 100, 400, 799] {
2011 for y in [0u32, 5, 10, 19] {
2012 assert_eq!(
2013 bus.read_byte(screen_base + y * 800 + x),
2014 0xAA,
2015 "initial frame: sentinel must hold at (x={}, y={})",
2016 x,
2017 y
2018 );
2019 }
2020 }
2021
2022 disp.mouse_pos = (2, 400);
2026 bus.write_word(0x0828, 2u16); bus.write_word(0x082A, 400u16); bus.write_word(0x082C, 2u16); bus.write_word(0x082E, 400u16); bus.write_word(0x0830, 2u16); bus.write_word(0x0832, 400u16); disp.redraw_chrome(&mut bus);
2037 for x in [0u32, 100, 400, 799] {
2038 for y in [0u32, 5, 10, 19] {
2039 assert_eq!(
2040 bus.read_byte(screen_base + y * 800 + x),
2041 0xAA,
2042 "after mouse-at-top: sentinel must still hold at (x={}, y={})",
2043 x,
2044 y
2045 );
2046 }
2047 }
2048 }
2049
2050 #[test]
2051 fn redraw_chrome_skips_window_blit_while_dialog_snapshot_pending() {
2052 let (mut disp, _cpu, mut bus) = setup_with_port();
2053
2054 let screen_base = bus.alloc(800 * 600);
2055 disp.screen_mode = (screen_base, 800, 800, 600, 8);
2056 bus.write_long(0x0824, screen_base);
2057
2058 let offscreen_base = bus.alloc(64 * 200);
2059 bus.write_long(PORT_PTR + 2, offscreen_base);
2060 bus.write_word(PORT_PTR + 6, 64);
2061 bus.write_word(PORT_PTR + 8, 0);
2062 bus.write_word(PORT_PTR + 10, 0);
2063 bus.write_word(PORT_PTR + 12, 200);
2064 bus.write_word(PORT_PTR + 14, 512);
2065
2066 let probe = screen_base + 10 * 800 + 10;
2067 bus.write_byte(probe, 0x42);
2068 bus.write_byte(offscreen_base + 10 * 64 + 1, 0x00);
2069
2070 bus.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 20);
2071 disp.front_window = PORT_PTR;
2072 disp.window_bounds = (0, 0, 200, 512);
2073 disp.window_proc_id = 1;
2074 disp.dialog_tracking = Some(super::super::dispatch::DialogTrackingState {
2075 game_managed: false,
2076 rendered_pixels_final: false,
2077 ..Default::default()
2078 });
2079
2080 disp.redraw_chrome(&mut bus);
2081
2082 assert_eq!(
2083 bus.read_byte(probe),
2084 0x42,
2085 "pending ModalDialog snapshots must not be erased by blank offscreen port blits"
2086 );
2087
2088 if let Some(tracking) = disp.dialog_tracking.as_mut() {
2089 tracking.rendered_pixels_final = true;
2090 }
2091 disp.redraw_chrome(&mut bus);
2092
2093 assert_eq!(
2094 bus.read_byte(probe),
2095 0x00,
2096 "once the dialog snapshot is final, normal port blitting should resume"
2097 );
2098 }
2099
2100 #[test]
2101 fn fb_fill_rect_uses_active_ctab_brightest_entry_for_white() {
2102 let (mut disp, _cpu, mut bus) = setup_with_port();
2103
2104 let screen_base = bus.alloc(100 * 100);
2105 disp.screen_mode = (screen_base, 100, 100, 100, 8);
2106 bus.write_long(0x0824, screen_base);
2107
2108 let gdevice_handle = disp.ensure_main_gdevice(&mut bus);
2109 bus.write_long(0x08A4, gdevice_handle);
2110 bus.write_long(0x0CC8, gdevice_handle);
2111 let gdevice = bus.read_long(gdevice_handle);
2112 let pixmap_handle = bus.read_long(gdevice + 22);
2113 let pixmap = bus.read_long(pixmap_handle);
2114 let ctab_handle = bus.read_long(pixmap + 42);
2115 let ctab = bus.read_long(ctab_handle);
2116 for index in 0u32..256 {
2117 let entry = ctab + 8 + index * 8;
2118 bus.write_word(entry, index as u16);
2119 bus.write_word(entry + 2, 0);
2120 bus.write_word(entry + 4, 0);
2121 bus.write_word(entry + 6, 0);
2122 }
2123 let white_entry = ctab + 8 + 8;
2124 bus.write_word(white_entry, 1);
2125 bus.write_word(white_entry + 2, 0xFFFF);
2126 bus.write_word(white_entry + 4, 0xFFFF);
2127 bus.write_word(white_entry + 6, 0xFFFF);
2128
2129 TrapDispatcher::fb_fill_rect(
2130 &mut bus,
2131 screen_base,
2132 100,
2133 8,
2134 100,
2135 100,
2136 10,
2137 10,
2138 20,
2139 20,
2140 false,
2141 );
2142
2143 assert_eq!(
2144 bus.read_byte(screen_base + 10 * 100 + 10),
2145 1,
2146 "logical white must follow the active ColorTable, not hard-code CLUT index 0"
2147 );
2148 }
2149
2150 #[test]
2151 fn fb_fill_rect_uses_active_ctab_darkest_entry_for_black() {
2152 let (mut disp, _cpu, mut bus) = setup_with_port();
2153
2154 let screen_base = bus.alloc(100 * 100);
2155 disp.screen_mode = (screen_base, 100, 100, 100, 8);
2156 bus.write_long(0x0824, screen_base);
2157 install_twilight_style_black_index(&mut disp, &mut bus);
2158
2159 TrapDispatcher::fb_fill_rect(
2160 &mut bus,
2161 screen_base,
2162 100,
2163 8,
2164 100,
2165 100,
2166 10,
2167 10,
2168 20,
2169 20,
2170 true,
2171 );
2172
2173 assert_eq!(
2174 bus.read_byte(screen_base + 10 * 100 + 10),
2175 1,
2176 "logical black must follow the active ColorTable when index 255 is not black"
2177 );
2178 }
2179
2180 #[test]
2186 fn redraw_chrome_blit_1bpp_to_8bpp_is_bit_extracted() {
2187 let (mut disp, _cpu, mut bus) = setup_with_port();
2188
2189 let screen_base = bus.alloc(800 * 600);
2193 disp.screen_mode = (screen_base, 800, 800, 600, 8);
2194 bus.write_long(0x0824, screen_base);
2195
2196 let offscreen_base = bus.alloc(64 * 200);
2200 bus.write_long(PORT_PTR + 2, offscreen_base); bus.write_word(PORT_PTR + 6, 64); bus.write_word(PORT_PTR + 8, 0); bus.write_word(PORT_PTR + 10, 0); bus.write_word(PORT_PTR + 12, 200); bus.write_word(PORT_PTR + 14, 512); bus.write_byte(offscreen_base + 30 * 64 + 12, 0b10000000);
2216
2217 let row30_x96 = screen_base + 30 * 800 + 96;
2220 let row30_x97 = screen_base + 30 * 800 + 97;
2221 bus.write_byte(row30_x96, 0xAB);
2222 bus.write_byte(row30_x97, 0xAB);
2223
2224 bus.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 20);
2225 disp.front_window = PORT_PTR;
2226 disp.window_bounds = (0, 0, 200, 400);
2227 disp.window_proc_id = 1; disp.fullscreen_locked = false;
2229
2230 disp.redraw_chrome(&mut bus);
2231
2232 assert_eq!(
2233 bus.read_byte(row30_x96),
2234 0xFF,
2235 "1bpp src bit=1 must map to 8bpp screen idx 0xFF"
2236 );
2237 assert_eq!(
2238 bus.read_byte(row30_x97),
2239 0x00,
2240 "1bpp src bit=0 must map to 8bpp screen idx 0x00 (blit MUST fire)"
2241 );
2242 }
2243
2244 #[test]
2245 fn redraw_chrome_blit_1bpp_to_8bpp_uses_active_ctab_black_entry() {
2246 let (mut disp, _cpu, mut bus) = setup_with_port();
2247
2248 let screen_base = bus.alloc(800 * 600);
2249 disp.screen_mode = (screen_base, 800, 800, 600, 8);
2250 bus.write_long(0x0824, screen_base);
2251 install_twilight_style_black_index(&mut disp, &mut bus);
2252
2253 let offscreen_base = bus.alloc(64 * 200);
2254 bus.write_long(PORT_PTR + 2, offscreen_base);
2255 bus.write_word(PORT_PTR + 6, 64);
2256 bus.write_word(PORT_PTR + 8, 0);
2257 bus.write_word(PORT_PTR + 10, 0);
2258 bus.write_word(PORT_PTR + 12, 200);
2259 bus.write_word(PORT_PTR + 14, 512);
2260
2261 bus.write_byte(offscreen_base + 30 * 64 + 12, 0b10000000);
2262
2263 let row30_x96 = screen_base + 30 * 800 + 96;
2264 let row30_x97 = screen_base + 30 * 800 + 97;
2265 bus.write_byte(row30_x96, 0xAB);
2266 bus.write_byte(row30_x97, 0xAB);
2267
2268 bus.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 20);
2269 disp.front_window = PORT_PTR;
2270 disp.window_bounds = (0, 0, 200, 400);
2271 disp.window_proc_id = 1;
2272 disp.fullscreen_locked = false;
2273
2274 disp.redraw_chrome(&mut bus);
2275
2276 assert_eq!(
2277 bus.read_byte(row30_x96),
2278 1,
2279 "1bpp src bit=1 must use the active ColorTable's black index"
2280 );
2281 assert_eq!(
2282 bus.read_byte(row30_x97),
2283 0,
2284 "1bpp src bit=0 must still use the active ColorTable's white index"
2285 );
2286 }
2287}