@@ -640,6 +640,46 @@ impl TerminalRenderer {
640640 }
641641 }
642642
643+ /// Whether a modal panel overlay overlaps the window-frame border, in which
644+ /// case the sim + frame should be wiped to a clean matte behind it.
645+ ///
646+ /// Returns false when there is no visible frame (fullscreen / no layout), so
647+ /// the simulation always shows through when there is no border to clutter.
648+ /// A panel "intersects the border" when its rectangle is not fully contained
649+ /// within the sim interior — i.e. it reaches into or past the frame ring.
650+ fn modal_overlaps_frame ( & self , panels : & [ Option < ( & RenderedOverlay , usize , usize ) > ] ) -> bool {
651+ use crate :: render:: window:: FallbackMode ;
652+ if !self . window_frame . is_visible ( ) {
653+ return false ;
654+ }
655+ let Some ( layout) = & self . window_layout else {
656+ return false ;
657+ } ;
658+ if matches ! ( layout. fallback, FallbackMode :: Fullscreen ) {
659+ return false ;
660+ }
661+ let ( sx, sy, sw, sh) = ( layout. sim_x , layout. sim_y , layout. sim_w , layout. sim_h ) ;
662+ panels. iter ( ) . flatten ( ) . any ( |( overlay, x, y) | {
663+ let w = overlay
664+ . lines
665+ . iter ( )
666+ . map ( |l| l. chars ( ) . count ( ) )
667+ . max ( )
668+ . unwrap_or ( 0 ) ;
669+ let h = overlay. lines . len ( ) ;
670+ if w == 0 || h == 0 {
671+ return false ;
672+ }
673+ // The floating title box sits one row above the body.
674+ let top = y. saturating_sub ( usize:: from ( overlay. title_box . is_some ( ) ) ) ;
675+ let bottom = y + h;
676+ let right = x + w;
677+ // Fully inside the interior → does not touch the border.
678+ let contained = * x >= sx && top >= sy && right <= sx + sw && bottom <= sy + sh;
679+ !contained
680+ } )
681+ }
682+
643683 /// Render a frame with text overlays.
644684 ///
645685 /// Draws the sim frame, then composites overlays on top in z-order:
@@ -760,39 +800,64 @@ impl TerminalRenderer {
760800 buffer. apply_pause_effect ( pause_style, fc, pause_pulse_draw_mode) ;
761801 }
762802
763- if let Some ( mut grid) = grid_renderer. cloned ( ) {
764- grid. initialize ( self . width , self . height ) ;
765-
766- // Calculate average brightness for adaptive opacity
767- let total_brightness: f32 = downsampled
768- . iter ( )
769- . map ( |cell| cell. top . max ( cell. bottom ) )
770- . sum ( ) ;
771- let avg_brightness = if !downsampled. is_empty ( ) && max_trail_value > 0.0 {
772- ( total_brightness / ( downsampled. len ( ) as f32 ) ) / max_trail_value
773- } else {
774- 0.0
775- } ;
803+ // When a modal panel overlaps the window-frame border, wipe the sim + frame
804+ // to a clean matte so the panel reads as the sole focus instead of fighting
805+ // the border and simulation behind it.
806+ let blank_backdrop = self . modal_overlaps_frame ( & [
807+ controls_lines,
808+ dashboard_lines,
809+ config_browser_lines,
810+ config_save_lines,
811+ dirty_guard_lines,
812+ keyboard_hints_lines,
813+ preset_comparison_lines,
814+ palette_editor_overlay,
815+ ] ) ;
816+
817+ if !blank_backdrop {
818+ if let Some ( mut grid) = grid_renderer. cloned ( ) {
819+ grid. initialize ( self . width , self . height ) ;
820+
821+ // Calculate average brightness for adaptive opacity
822+ let total_brightness: f32 = downsampled
823+ . iter ( )
824+ . map ( |cell| cell. top . max ( cell. bottom ) )
825+ . sum ( ) ;
826+ let avg_brightness = if !downsampled. is_empty ( ) && max_trail_value > 0.0 {
827+ ( total_brightness / ( downsampled. len ( ) as f32 ) ) / max_trail_value
828+ } else {
829+ 0.0
830+ } ;
776831
777- for y in 0 ..self . height {
778- for x in 0 ..self . width {
779- if grid. is_grid_position ( x, y, self . width , self . height ) {
780- let ( on_vertical, on_horizontal) = grid. get_grid_lines ( x, y) ;
781- let opacity =
782- grid. calculate_opacity ( x, y, self . width , self . height , avg_brightness) ;
783- buffer. render_grid_background (
784- x,
785- y,
786- grid. color ,
787- opacity,
788- on_vertical,
789- on_horizontal,
790- ) ;
832+ for y in 0 ..self . height {
833+ for x in 0 ..self . width {
834+ if grid. is_grid_position ( x, y, self . width , self . height ) {
835+ let ( on_vertical, on_horizontal) = grid. get_grid_lines ( x, y) ;
836+ let opacity = grid. calculate_opacity (
837+ x,
838+ y,
839+ self . width ,
840+ self . height ,
841+ avg_brightness,
842+ ) ;
843+ buffer. render_grid_background (
844+ x,
845+ y,
846+ grid. color ,
847+ opacity,
848+ on_vertical,
849+ on_horizontal,
850+ ) ;
851+ }
791852 }
792853 }
793854 }
794855 }
795856
857+ if blank_backdrop {
858+ buffer. fill_background ( ) ;
859+ }
860+
796861 // Palette accent color used for accented title badges and border.
797862 let accent = palette:: palette_accent_color (
798863 & self . palette ,
@@ -802,7 +867,7 @@ impl TerminalRenderer {
802867 self . intensity_mapping . as_ref ( ) ,
803868 ) ;
804869
805- if self . window_frame . is_visible ( ) {
870+ if !blank_backdrop && self . window_frame . is_visible ( ) {
806871 if let Some ( ref layout) = self . window_layout {
807872 use crate :: render:: window:: FallbackMode ;
808873 if !matches ! ( layout. fallback, FallbackMode :: Fullscreen ) {
@@ -1180,39 +1245,63 @@ impl TerminalRenderer {
11801245 buffer. apply_pause_effect ( pause_style, fc, pause_pulse_draw_mode) ;
11811246 }
11821247
1183- if let Some ( mut grid) = grid_renderer. cloned ( ) {
1184- grid. initialize ( self . width , self . height ) ;
1185-
1186- // Calculate average brightness from all species combined
1187- let total_brightness: f32 = all_downsampled_cells
1188- . iter ( )
1189- . map ( |cell| cell. top . max ( cell. bottom ) )
1190- . sum ( ) ;
1191- let avg_brightness = if !all_downsampled_cells. is_empty ( ) && max_trail_value > 0.0 {
1192- ( total_brightness / ( all_downsampled_cells. len ( ) as f32 ) ) / max_trail_value
1193- } else {
1194- 0.0
1195- } ;
1248+ // See render_with_overlay: blank the sim + frame when a modal panel overlaps
1249+ // the window-frame border, so the panel is the sole focus.
1250+ let blank_backdrop = self . modal_overlaps_frame ( & [
1251+ controls_lines,
1252+ dashboard_lines,
1253+ config_browser_lines,
1254+ config_save_lines,
1255+ dirty_guard_lines,
1256+ keyboard_hints_lines,
1257+ preset_comparison_lines,
1258+ palette_editor_overlay,
1259+ ] ) ;
1260+
1261+ if !blank_backdrop {
1262+ if let Some ( mut grid) = grid_renderer. cloned ( ) {
1263+ grid. initialize ( self . width , self . height ) ;
1264+
1265+ // Calculate average brightness from all species combined
1266+ let total_brightness: f32 = all_downsampled_cells
1267+ . iter ( )
1268+ . map ( |cell| cell. top . max ( cell. bottom ) )
1269+ . sum ( ) ;
1270+ let avg_brightness = if !all_downsampled_cells. is_empty ( ) && max_trail_value > 0.0 {
1271+ ( total_brightness / ( all_downsampled_cells. len ( ) as f32 ) ) / max_trail_value
1272+ } else {
1273+ 0.0
1274+ } ;
11961275
1197- for y in 0 ..self . height {
1198- for x in 0 ..self . width {
1199- if grid. is_grid_position ( x, y, self . width , self . height ) {
1200- let ( on_vertical, on_horizontal) = grid. get_grid_lines ( x, y) ;
1201- let opacity =
1202- grid. calculate_opacity ( x, y, self . width , self . height , avg_brightness) ;
1203- buffer. render_grid_background (
1204- x,
1205- y,
1206- grid. color ,
1207- opacity,
1208- on_vertical,
1209- on_horizontal,
1210- ) ;
1276+ for y in 0 ..self . height {
1277+ for x in 0 ..self . width {
1278+ if grid. is_grid_position ( x, y, self . width , self . height ) {
1279+ let ( on_vertical, on_horizontal) = grid. get_grid_lines ( x, y) ;
1280+ let opacity = grid. calculate_opacity (
1281+ x,
1282+ y,
1283+ self . width ,
1284+ self . height ,
1285+ avg_brightness,
1286+ ) ;
1287+ buffer. render_grid_background (
1288+ x,
1289+ y,
1290+ grid. color ,
1291+ opacity,
1292+ on_vertical,
1293+ on_horizontal,
1294+ ) ;
1295+ }
12111296 }
12121297 }
12131298 }
12141299 }
12151300
1301+ if blank_backdrop {
1302+ buffer. fill_background ( ) ;
1303+ }
1304+
12161305 // Palette accent color for accented title badges (same approach as single-species).
12171306 let accent = palette:: palette_accent_color (
12181307 & self . palette ,
@@ -1222,7 +1311,7 @@ impl TerminalRenderer {
12221311 self . intensity_mapping . as_ref ( ) ,
12231312 ) ;
12241313
1225- if self . window_frame . is_visible ( ) {
1314+ if !blank_backdrop && self . window_frame . is_visible ( ) {
12261315 if let Some ( ref layout) = self . window_layout {
12271316 use crate :: render:: window:: FallbackMode ;
12281317 if !matches ! ( layout. fallback, FallbackMode :: Fullscreen ) {
0 commit comments