feat(engine): replace docked touch bar with floating glass tab bar
Touch layout only (desktop bar unchanged): a floating glass pill (Undo / Draw / Hint / Pause) plus a detached circular Menu button, margin above the bottom edge on top of the safe-area inset. Draw is the persistently expanded accent pill (icon + label); other buttons are icon-only and slide their label out while pressed, snapping under reduce-motion. Buttons keep the existing marker components so click handlers, tooltips, focus ring, and chrome toggle work unchanged. Icons: None degrades to text-fallback labels (MinimalPlugins tests / SVG regression), keeping the bar fully usable. Toast stack clearance now derives from TAB_BAR_CLEARANCE_PX; the legacy touch bar spawn path and primary-button metrics are removed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -907,6 +907,10 @@ fn hud_focus_only_engages_when_button_hovered() {
|
||||
/// Bare app (no `HudPlugin`) so bar-content assertions count only the
|
||||
/// buttons the tested spawn function creates — `headless_app` would
|
||||
/// pre-spawn the platform-default bar and pollute the counts.
|
||||
///
|
||||
/// The touch branch spawns the glass tab bar with `icons: None`
|
||||
/// (`MinimalPlugins` has no `Assets<Image>`), which exercises the
|
||||
/// text-fallback path — same markers, labels pinned expanded.
|
||||
fn bar_only_app(touch: bool) -> App {
|
||||
let mut app = App::new();
|
||||
app.add_plugins(MinimalPlugins);
|
||||
@@ -914,13 +918,13 @@ fn bar_only_app(touch: bool) -> App {
|
||||
let font = TextFont::default();
|
||||
let world = app.world_mut();
|
||||
let mut commands = world.commands();
|
||||
commands.spawn(Node::default()).with_children(|row| {
|
||||
if touch {
|
||||
spawn_touch_action_bar(row, &font);
|
||||
} else {
|
||||
if touch {
|
||||
spawn_glass_tab_bar(&mut commands, &font, None);
|
||||
} else {
|
||||
commands.spawn(Node::default()).with_children(|row| {
|
||||
spawn_desktop_action_bar(row, &font);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
app.update();
|
||||
app
|
||||
}
|
||||
@@ -933,7 +937,7 @@ fn count_buttons<C: Component>(app: &mut App) -> usize {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn touch_action_bar_has_thumb_trio_plus_menu_and_pause() {
|
||||
fn touch_tab_bar_has_thumb_trio_plus_menu_and_pause() {
|
||||
let mut app = bar_only_app(true);
|
||||
|
||||
assert_eq!(count_buttons::<UndoButton>(&mut app), 1);
|
||||
@@ -949,6 +953,82 @@ fn touch_action_bar_has_thumb_trio_plus_menu_and_pause() {
|
||||
assert_eq!(count_buttons::<NewGameButton>(&mut app), 0);
|
||||
}
|
||||
|
||||
/// The glass bar floats: its container must carry a bottom margin (not be
|
||||
/// docked at 0) both before and after safe-area insets arrive.
|
||||
#[test]
|
||||
fn glass_tab_bar_floats_above_bottom_edge() {
|
||||
let mut app = bar_only_app(true);
|
||||
let world = app.world_mut();
|
||||
let mut q = world.query_filtered::<(&Node, &SafeAreaAnchoredBottom), With<HudActionBar>>();
|
||||
let (node, anchor) = q.single(world).expect("glass bar container exists");
|
||||
assert!(anchor.base_bottom > 0.0, "bar must float, not dock");
|
||||
assert_eq!(node.bottom, Val::Px(anchor.base_bottom));
|
||||
}
|
||||
|
||||
/// Draw is the persistently-expanded active pill (user decision
|
||||
/// 2026-07-15); with `icons: None` every wrapper is pinned expanded via
|
||||
/// text fallback, so instead assert on the *persistent* flag's effect:
|
||||
/// all wrappers spawn at full reveal and target full reveal.
|
||||
#[test]
|
||||
fn glass_tab_bar_text_fallback_pins_labels_expanded() {
|
||||
let mut app = bar_only_app(true);
|
||||
let world = app.world_mut();
|
||||
let mut q = world.query::<&TabLabelWrap>();
|
||||
let wraps: Vec<_> = q.iter(world).collect();
|
||||
assert_eq!(wraps.len(), 4, "Undo/Draw/Hint/Pause each carry a wrapper");
|
||||
for wrap in wraps {
|
||||
assert_eq!(wrap.progress, 1.0);
|
||||
assert_eq!(wrap.target, 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Focus order must match visual reading order: pill buttons left to
|
||||
/// right, then the detached Menu circle.
|
||||
#[test]
|
||||
fn glass_tab_bar_focus_order_reads_left_to_right() {
|
||||
let mut app = bar_only_app(true);
|
||||
|
||||
for (f, expected) in [
|
||||
(focusable_for::<UndoButton>(&mut app), 0),
|
||||
(focusable_for::<DrawButton>(&mut app), 1),
|
||||
(focusable_for::<HintButton>(&mut app), 2),
|
||||
(focusable_for::<PauseButton>(&mut app), 3),
|
||||
(focusable_for::<MenuButton>(&mut app), 4),
|
||||
] {
|
||||
assert_eq!(f.group, FocusGroup::Hud);
|
||||
assert_eq!(f.order, expected);
|
||||
}
|
||||
}
|
||||
|
||||
/// `expansion_width` is the single source of truth for the label reveal:
|
||||
/// zero when collapsed, lead + monospace advance when expanded, linear
|
||||
/// in between.
|
||||
#[test]
|
||||
fn expansion_width_is_linear_in_progress() {
|
||||
assert_eq!(expansion_width(4, 16.0, 0.0), 0.0);
|
||||
let full = expansion_width(4, 16.0, 1.0);
|
||||
assert!(full > 4.0 * 16.0 * 0.62, "full width includes the lead gap");
|
||||
let half = expansion_width(4, 16.0, 0.5);
|
||||
assert!((half - full / 2.0).abs() < f32::EPSILON * 100.0);
|
||||
// Out-of-range progress clamps rather than extrapolating.
|
||||
assert_eq!(expansion_width(4, 16.0, 1.5), full);
|
||||
}
|
||||
|
||||
/// The tween must move at the `MOTION_SLIDE_SECS` rate, never overshoot,
|
||||
/// and snap instantly under reduce-motion.
|
||||
#[test]
|
||||
fn step_expansion_tweens_and_respects_reduce_motion() {
|
||||
// One 60 fps frame covers 1/(0.18*60) ≈ 9.3% of the reveal.
|
||||
let one_frame = step_expansion(0.0, 1.0, 1.0 / 60.0, false);
|
||||
assert!(one_frame > 0.0 && one_frame < 0.2);
|
||||
// A huge dt clamps at the target instead of overshooting.
|
||||
assert_eq!(step_expansion(0.0, 1.0, 10.0, false), 1.0);
|
||||
// Collapse works symmetrically.
|
||||
assert!(step_expansion(1.0, 0.0, 1.0 / 60.0, false) < 1.0);
|
||||
// Reduce-motion snaps regardless of dt.
|
||||
assert_eq!(step_expansion(0.0, 1.0, 0.0, true), 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn desktop_action_bar_keeps_all_seven_and_no_draw() {
|
||||
let mut app = bar_only_app(false);
|
||||
|
||||
Reference in New Issue
Block a user