From 4b484f899aefba45d86d70c51166e578000a04f3 Mon Sep 17 00:00:00 2001 From: funman300 Date: Wed, 15 Jul 2026 15:23:05 -0700 Subject: [PATCH] feat(engine): duck the glass tab bar during card drags While a drag is committed the floating bar slides below the screen edge (UiTransform translation - no layout dirtying, no fight with the safe-area anchor over Node::bottom) so it never occludes a drop target on the bottom tableau rows; it slides back on release. Same tween rate as the label reveal, snaps under reduce-motion. Desktop docked bar is untouched (no TabBarDuck component). Co-Authored-By: Claude Fable 5 --- solitaire_engine/src/hud_plugin/mod.rs | 1 + solitaire_engine/src/hud_plugin/tab_bar.rs | 61 +++++++++++++++++++++- solitaire_engine/src/hud_plugin/tests.rs | 48 +++++++++++++++++ 3 files changed, 109 insertions(+), 1 deletion(-) diff --git a/solitaire_engine/src/hud_plugin/mod.rs b/solitaire_engine/src/hud_plugin/mod.rs index a2ef864..7f918fe 100644 --- a/solitaire_engine/src/hud_plugin/mod.rs +++ b/solitaire_engine/src/hud_plugin/mod.rs @@ -603,6 +603,7 @@ impl Plugin for HudPlugin { // disjoint from the UiTextFx readout writers (tab labels // vs HUD readouts), which Bevy can't prove, so declare it. animate_tab_expansion.ambiguous_with(UiTextFx), + duck_tab_bar_during_drag, ) .chain() .in_set(HudButtons) diff --git a/solitaire_engine/src/hud_plugin/tab_bar.rs b/solitaire_engine/src/hud_plugin/tab_bar.rs index 5d3ef8c..5318090 100644 --- a/solitaire_engine/src/hud_plugin/tab_bar.rs +++ b/solitaire_engine/src/hud_plugin/tab_bar.rs @@ -24,7 +24,10 @@ use crate::assets::hud_icon_svg::HudIcon; use crate::ui_glass::{GLASS_BORDER_PX, glass_surface}; use crate::ui_theme::{ACCENT_PRIMARY_HOVER, SPACE_3}; use bevy::text::{LineBreak, TextLayout}; -use bevy::ui::Overflow; +use bevy::ui::{Overflow, UiTransform, Val2}; + +use crate::resources::DragState; +use crate::safe_area::SafeAreaInsets; /// Height of both the pill and the circular Menu button, in logical px. /// 64 keeps the inner 48-px buttons comfortably past the 44-px touch @@ -99,6 +102,20 @@ pub struct TabLabelWrap { #[derive(Component, Debug)] pub struct TabLabel; +/// Duck-away state on the bar container: while a card drag is committed +/// the whole bar slides below the screen edge so it never occludes a +/// drop target on the bottom tableau rows, then slides back on release. +/// `progress` ∈ [0, 1]; 1 = fully off-screen. +#[derive(Component, Debug)] +pub struct TabBarDuck { + /// Current slide amount, animated by [`duck_tab_bar_during_drag`]. + pub progress: f32, +} + +/// Extra slide distance past the bar's own height so its drop shadow +/// (16 px blur + 6 px offset) also clears the screen edge. +const DUCK_SHADOW_ALLOWANCE_PX: f32 = 24.0; + /// Rasterised icon textures for the five tab-bar actions. pub(super) struct TabIcons { undo: Handle, @@ -179,6 +196,8 @@ pub(super) fn spawn_glass_tab_bar( base_bottom: TAB_BAR_MARGIN, }, HudActionBar, + TabBarDuck { progress: 0.0 }, + UiTransform::IDENTITY, )) .with_children(|bar| { // The main pill: Undo · [Draw] · Hint · Pause. @@ -453,6 +472,46 @@ pub(super) fn animate_tab_expansion( } } +/// Slides the bar off the bottom edge while a card drag is committed and +/// back once it ends. Effectively touch-only: the desktop docked bar +/// never carries [`TabBarDuck`], so the query is empty there. +/// +/// The slide animates `UiTransform::translation` (a render-time offset, +/// +y = down) rather than `Node::bottom`, for two reasons: layout is +/// never dirtied mid-drag (the whole point is not to cost frames while +/// the player is dragging), and `Node::bottom` is owned by the safe-area +/// anchor system — two writers on one field would fight. +pub(super) fn duck_tab_bar_during_drag( + time: Res