From c2865934153670553d7b902323e5d5ad3210d39e Mon Sep 17 00:00:00 2001 From: funman300 Date: Mon, 6 Jul 2026 14:28:09 -0700 Subject: [PATCH] fix(engine): resize pile-marker outline and watermark children on relayout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Found during Fold 7 on-device verification of v0.41.0: on_window_resized resized the marker fill sprite but never its children, so after any resize (fold/unfold, rotation) the outline frame and the A/K watermark kept their spawn-time size — rendering as oversized grey slabs over the empty foundation slots. The resize handler now re-derives both children from the new layout (outline = card_size + 2*PILE_MARKER_OUTLINE_WIDTH, watermark font = card_size.x * 0.28 — same formulas as spawn). Adds a regression test and an Android-gated relayout log line (width/height/insets) as the evidence channel for the remaining foldable layout bug (#130). Co-Authored-By: Claude Fable 5 --- solitaire_engine/src/table_plugin.rs | 88 +++++++++++++++++++++++++++- 1 file changed, 86 insertions(+), 2 deletions(-) diff --git a/solitaire_engine/src/table_plugin.rs b/solitaire_engine/src/table_plugin.rs index 82ca538..43a13a1 100644 --- a/solitaire_engine/src/table_plugin.rs +++ b/solitaire_engine/src/table_plugin.rs @@ -362,7 +362,15 @@ fn on_window_resized( (&mut Sprite, &mut Transform), (With, Without), >, - mut markers: Query<(&PileMarker, &mut Sprite, &mut Transform), Without>, + mut markers: Query< + (&PileMarker, &mut Sprite, &mut Transform, Option<&Children>), + Without, + >, + mut marker_outlines: Query< + &mut Sprite, + (Without, Without, Without), + >, + mut marker_labels: Query<&mut TextFont, With>, ) { let Some(ev) = events.read().last() else { return; @@ -373,6 +381,14 @@ fn on_window_resized( let safe_area_top = insets.top / scale; let safe_area_bottom = insets.bottom / scale; let hud_visible = hud_vis.as_deref().copied().unwrap_or_default() == HudVisibility::Visible; + // Android surface sizes after fold/unfold are unreliable (winit does not + // forward every content-rect change), so make each relayout observable + // in logcat — this is the primary evidence channel for foldable bugs. + #[cfg(target_os = "android")] + info!( + "layout: resize to {:.0}x{:.0} (insets top={:.0} bottom={:.0}, hud={hud_visible})", + window_size.x, window_size.y, insets.top, insets.bottom + ); let mut new_layout = compute_layout(window_size, safe_area_top, safe_area_bottom, hud_visible); // compute_layout sizes the fan for a worst-case column; refine it to the @@ -394,11 +410,27 @@ fn on_window_resized( transform.translation.y = 0.0; } - for (marker, mut sprite, mut transform) in &mut markers { + for (marker, mut sprite, mut transform, children) in &mut markers { if let Some(pos) = new_layout.pile_positions.get(&marker.0) { sprite.custom_size = Some(new_layout.card_size); transform.translation.x = pos.x; transform.translation.y = pos.y; + // Children carry the outline frame and the "A"/"K" watermark. + // They are sized at spawn from that layout's card size, so they + // must be re-derived here too or a resize (fold/unfold, rotation) + // leaves them at the stale size — visible as oversized grey + // frames on empty piles. + let outline_size = + new_layout.card_size + Vec2::splat(PILE_MARKER_OUTLINE_WIDTH * 2.0); + let font_size = new_layout.card_size.x * 0.28; + for child in children.into_iter().flatten() { + if let Ok(mut outline) = marker_outlines.get_mut(*child) { + outline.custom_size = Some(outline_size); + } + if let Ok(mut label) = marker_labels.get_mut(*child) { + label.font_size = font_size; + } + } } } @@ -623,6 +655,58 @@ mod tests { assert_eq!(types.len(), 12); } + #[test] + fn resize_updates_marker_outline_and_watermark_children() { + // Regression (found on Galaxy Fold 7, v0.41.0): `on_window_resized` + // resized the marker fill sprite but never its children, so after a + // fold/unfold the outline frame and "A"/"K" watermark stayed at + // spawn-time size — rendering as oversized grey slabs on empty piles. + let mut app = headless_app(); + // Shrink the window well below the 1280×800 spawn default. + app.world_mut().write_message(WindowResized { + window: Entity::PLACEHOLDER, + width: 640.0, + height: 480.0, + }); + app.update(); + + let card_size = app.world().resource::().0.card_size; + let expected_outline = card_size + Vec2::splat(PILE_MARKER_OUTLINE_WIDTH * 2.0); + let expected_font = card_size.x * 0.28; + + let marker_children: Vec = { + let mut q = app.world_mut().query::<(&PileMarker, &Children)>(); + q.iter(app.world()) + .flat_map(|(_, c)| c.iter().collect::>()) + .collect() + }; + assert!( + !marker_children.is_empty(), + "pile markers must have outline/watermark children" + ); + let mut outlines = 0; + let mut labels = 0; + for child in marker_children { + if let Some(sprite) = app.world().entity(child).get::() { + assert_eq!( + sprite.custom_size, + Some(expected_outline), + "outline child must track the resized card size", + ); + outlines += 1; + } + if let Some(font) = app.world().entity(child).get::() { + assert!( + (font.font_size - expected_font).abs() < 0.01, + "watermark font must track the resized card size", + ); + labels += 1; + } + } + assert_eq!(outlines, 12, "all 12 markers carry an outline child"); + assert!(labels >= 11, "tableau + foundation markers carry watermarks"); + } + #[test] fn pile_markers_hide_when_pile_is_occupied() { // After a fresh deal: the 7 tableau piles + the stock pile are -- 2.47.3