diff --git a/CLAUDE.md b/CLAUDE.md index 2482370..26b5b08 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -430,9 +430,11 @@ explicitly replacing the current one (despawn first, then spawn). ## 14.3 Safe area -Every `ModalScrim` automatically receives `padding.bottom` equal to the -logical gesture-bar height via `apply_safe_area_to_modal_scrims` in -`SafeAreaInsetsPlugin`. Do not manually add bottom padding to scrim nodes. +Every `ModalScrim` automatically receives `padding.top` equal to the logical +status-bar height and `padding.bottom` equal to the logical gesture-bar height +via `apply_safe_area_to_modal_scrims` in `SafeAreaInsetsPlugin`. This centres +the modal card within the usable area between both system bars. Do not manually +add top or bottom padding to scrim nodes. ## 14.4 Z-ordering diff --git a/docs/in-place-card-game-rewrite-plan.md b/docs/in-place-card-game-rewrite-plan.md index 4bc11df..089c949 100644 --- a/docs/in-place-card-game-rewrite-plan.md +++ b/docs/in-place-card-game-rewrite-plan.md @@ -2,7 +2,7 @@ **Date:** 2026-06-08 **Upstream rev:** `99b49e62` -**Status:** Phases 0–2 complete. Pre-Phase 3 undo audit complete (see §5 below). Awaiting approval for Phase 3. +**Status:** All phases complete (0–3). recycle_count drift and score compound error on undo fixed in `56e3b62`. --- diff --git a/solitaire_engine/src/safe_area.rs b/solitaire_engine/src/safe_area.rs index 1fc4e16..b499bb3 100644 --- a/solitaire_engine/src/safe_area.rs +++ b/solitaire_engine/src/safe_area.rs @@ -147,8 +147,13 @@ fn apply_safe_area_bottom_anchors( } } -/// Pads the bottom of every [`ModalScrim`] by the logical bottom inset so -/// modal cards don't extend into the Android gesture-navigation zone. +/// Pads both edges of every [`ModalScrim`] by the logical system-bar insets so +/// modal cards are centred within the usable area (between the status bar at +/// the top and the gesture-navigation bar at the bottom). +/// +/// `padding.top` = status-bar inset; `padding.bottom` = gesture-bar inset. +/// With `align_items: Center` / `justify_content: Center` on the scrim the +/// `ModalCard` lands at the visual midpoint of the visible content area. /// /// Fires when [`SafeAreaInsets`] changes (covers the common case of insets /// arriving a few frames after app start) AND when a new `ModalScrim` is @@ -165,8 +170,18 @@ fn apply_safe_area_to_modal_scrims( } let scale = windows.iter().next().map_or(1.0, |w| w.scale_factor()); let window_height = windows.iter().next().map_or(800.0, |w| w.height()); + // Clamp each inset to 25% of screen height so an unexpectedly large OS + // value can't push the modal card off the visible area entirely. + let top_logical = (insets.top / scale).min(window_height * 0.25); let bottom_logical = (insets.bottom / scale).min(window_height * 0.25); for mut node in &mut scrims { + // Set both edges so the scrim's content box equals the usable area + // between the status bar and the gesture/navigation bar. With + // `align_items: Center` / `justify_content: Center` on the scrim, + // the modal card is centred within that usable region rather than + // the full viewport, correcting the slight upward shift seen when + // only the bottom inset was applied. + node.padding.top = Val::Px(top_logical); node.padding.bottom = Val::Px(bottom_logical); } }