c8fc3e7a09
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>
612 lines
21 KiB
Rust
612 lines
21 KiB
Rust
//! Per-frame HUD text/typography/visibility updater systems.
|
||
|
||
use super::*;
|
||
|
||
use bevy::window::WindowResized;
|
||
use solitaire_core::Suit;
|
||
use solitaire_core::{DrawStockConfig, game_state::GameMode};
|
||
use solitaire_core::{Foundation, KlondikePile, Tableau};
|
||
|
||
use crate::auto_complete_plugin::AutoCompleteState;
|
||
|
||
/// Formats a time-limit value in seconds as `"mm:ss"` for HUD display.
|
||
///
|
||
/// For example `format_time_limit(300)` returns `"5:00"`.
|
||
pub fn format_time_limit(secs: u64) -> String {
|
||
let m = secs / 60;
|
||
let s = secs % 60;
|
||
format!("{m}:{s:02}")
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Score-change feedback (G2)
|
||
//
|
||
// The flow for each Update tick:
|
||
// 1. `detect_score_change` diffs `GameStateResource.score` against
|
||
// `PreviousScore`. On any positive delta it inserts/refreshes
|
||
// `ScorePulse` on the score readout; on a delta ≥
|
||
// `SCORE_FLOATER_THRESHOLD` it also spawns a floating "+N" UI text
|
||
// anchored just below the score.
|
||
// 2. `advance_score_pulse` ticks the pulse component, applies the
|
||
// triangular 1.0 → 1.1 → 1.0 scale curve, and removes the
|
||
// component on completion.
|
||
// 3. `advance_score_floater` drifts each floater upward, fades it to
|
||
// transparent, and despawns it when its lifetime expires.
|
||
//
|
||
// The threshold of 50 (a foundation promotion's typical bonus) keeps
|
||
// floaters rare and meaningful — see `SCORE_FLOATER_THRESHOLD`.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/// Sets the [`HudWonPreviously`] text to "✓ Won before" whenever the
|
||
/// current deal's seed + draw_mode + mode triple matches an entry in
|
||
/// the rolling [`ReplayHistory`]. Cleared while the active game is won
|
||
/// (the on-screen "Game won!" cue already conveys victory) and on
|
||
/// fresh deals the player hasn't won before.
|
||
///
|
||
/// Lives in its own system rather than `update_hud` to keep this
|
||
/// orthogonal: `update_hud`'s query disambiguation is already busy
|
||
/// enough; threading another marker through every Without filter
|
||
/// would touch ~10 unrelated queries for no benefit.
|
||
pub(super) fn update_won_previously(
|
||
game: Res<GameStateResource>,
|
||
// Optional because the HUD plugin's headless tests run without
|
||
// `StatsPlugin` and therefore without this resource. With the
|
||
// resource absent there's no history to compare against; the
|
||
// indicator just stays empty.
|
||
history: Option<Res<crate::stats_plugin::ReplayHistoryResource>>,
|
||
mut q: Query<&mut Text, With<HudWonPreviously>>,
|
||
) {
|
||
let Ok(mut text) = q.single_mut() else {
|
||
return;
|
||
};
|
||
let won_before = !game.0.is_won()
|
||
&& history.as_ref().is_some_and(|h| {
|
||
h.0.replays.iter().any(|r| {
|
||
r.seed == game.0.seed && r.draw_mode == game.0.draw_mode() && r.mode == game.0.mode
|
||
})
|
||
});
|
||
let next = if won_before {
|
||
"\u{2713} Won before"
|
||
} else {
|
||
""
|
||
};
|
||
if text.0 != next {
|
||
text.0 = next.to_string();
|
||
}
|
||
}
|
||
|
||
#[allow(clippy::type_complexity, clippy::too_many_arguments)]
|
||
pub(super) fn update_hud(
|
||
game: Res<GameStateResource>,
|
||
time_attack: Option<Res<TimeAttackResource>>,
|
||
daily: Option<Res<DailyChallengeResource>>,
|
||
auto_complete: Option<Res<AutoCompleteState>>,
|
||
mut score_q: Query<
|
||
&mut Text,
|
||
(
|
||
With<HudScore>,
|
||
Without<HudMoves>,
|
||
Without<HudTime>,
|
||
Without<HudMode>,
|
||
Without<HudChallenge>,
|
||
Without<HudUndos>,
|
||
Without<HudAutoComplete>,
|
||
Without<HudRecycles>,
|
||
Without<HudDrawCycle>,
|
||
Without<HudSelection>,
|
||
),
|
||
>,
|
||
mut moves_q: Query<
|
||
&mut Text,
|
||
(
|
||
With<HudMoves>,
|
||
Without<HudScore>,
|
||
Without<HudTime>,
|
||
Without<HudMode>,
|
||
Without<HudChallenge>,
|
||
Without<HudUndos>,
|
||
Without<HudAutoComplete>,
|
||
Without<HudRecycles>,
|
||
Without<HudDrawCycle>,
|
||
Without<HudSelection>,
|
||
),
|
||
>,
|
||
mut time_q: Query<
|
||
&mut Text,
|
||
(
|
||
With<HudTime>,
|
||
Without<HudScore>,
|
||
Without<HudMoves>,
|
||
Without<HudMode>,
|
||
Without<HudChallenge>,
|
||
Without<HudUndos>,
|
||
Without<HudAutoComplete>,
|
||
Without<HudRecycles>,
|
||
Without<HudDrawCycle>,
|
||
Without<HudSelection>,
|
||
),
|
||
>,
|
||
mut mode_q: Query<
|
||
&mut Text,
|
||
(
|
||
With<HudMode>,
|
||
Without<HudScore>,
|
||
Without<HudMoves>,
|
||
Without<HudTime>,
|
||
Without<HudChallenge>,
|
||
Without<HudUndos>,
|
||
Without<HudAutoComplete>,
|
||
Without<HudRecycles>,
|
||
Without<HudDrawCycle>,
|
||
Without<HudSelection>,
|
||
),
|
||
>,
|
||
mut challenge_q: Query<
|
||
(&mut Text, &mut TextColor),
|
||
(
|
||
With<HudChallenge>,
|
||
Without<HudScore>,
|
||
Without<HudMoves>,
|
||
Without<HudTime>,
|
||
Without<HudMode>,
|
||
Without<HudUndos>,
|
||
Without<HudAutoComplete>,
|
||
Without<HudRecycles>,
|
||
Without<HudDrawCycle>,
|
||
Without<HudSelection>,
|
||
),
|
||
>,
|
||
mut undos_q: Query<
|
||
(&mut Text, &mut TextColor),
|
||
(
|
||
With<HudUndos>,
|
||
Without<HudScore>,
|
||
Without<HudMoves>,
|
||
Without<HudTime>,
|
||
Without<HudMode>,
|
||
Without<HudChallenge>,
|
||
Without<HudAutoComplete>,
|
||
Without<HudRecycles>,
|
||
Without<HudDrawCycle>,
|
||
Without<HudSelection>,
|
||
),
|
||
>,
|
||
mut auto_q: Query<
|
||
&mut Text,
|
||
(
|
||
With<HudAutoComplete>,
|
||
Without<HudScore>,
|
||
Without<HudMoves>,
|
||
Without<HudTime>,
|
||
Without<HudMode>,
|
||
Without<HudChallenge>,
|
||
Without<HudUndos>,
|
||
Without<HudRecycles>,
|
||
Without<HudDrawCycle>,
|
||
Without<HudSelection>,
|
||
),
|
||
>,
|
||
mut recycles_q: Query<
|
||
&mut Text,
|
||
(
|
||
With<HudRecycles>,
|
||
Without<HudScore>,
|
||
Without<HudMoves>,
|
||
Without<HudTime>,
|
||
Without<HudMode>,
|
||
Without<HudChallenge>,
|
||
Without<HudUndos>,
|
||
Without<HudAutoComplete>,
|
||
Without<HudDrawCycle>,
|
||
Without<HudSelection>,
|
||
),
|
||
>,
|
||
mut draw_cycle_q: Query<
|
||
&mut Text,
|
||
(
|
||
With<HudDrawCycle>,
|
||
Without<HudScore>,
|
||
Without<HudMoves>,
|
||
Without<HudTime>,
|
||
Without<HudMode>,
|
||
Without<HudChallenge>,
|
||
Without<HudUndos>,
|
||
Without<HudAutoComplete>,
|
||
Without<HudRecycles>,
|
||
Without<HudSelection>,
|
||
),
|
||
>,
|
||
) {
|
||
let ta_active = time_attack.as_ref().is_some_and(|ta| ta.active);
|
||
|
||
// Score, moves, mode, challenge, and undos only need updating when game state changes.
|
||
if game.is_changed() {
|
||
let g = &game.0;
|
||
let is_zen = g.mode == GameMode::Zen;
|
||
if let Ok(mut t) = score_q.single_mut() {
|
||
// Zen mode suppresses score display per spec ("No score display").
|
||
**t = if is_zen {
|
||
String::new()
|
||
} else {
|
||
format!("Score: {}", g.score())
|
||
};
|
||
}
|
||
if let Ok(mut t) = moves_q.single_mut() {
|
||
**t = format!("Moves: {}", g.move_count());
|
||
}
|
||
if let Ok(mut t) = mode_q.single_mut() {
|
||
**t = match g.mode {
|
||
GameMode::Classic => match g.draw_mode() {
|
||
DrawStockConfig::DrawOne => String::new(),
|
||
DrawStockConfig::DrawThree => "Draw 3".to_string(),
|
||
},
|
||
GameMode::Zen => "ZEN".to_string(),
|
||
GameMode::Challenge => "CHALLENGE".to_string(),
|
||
GameMode::TimeAttack => "TIME ATTACK".to_string(),
|
||
GameMode::Difficulty(level) => level.label().to_uppercase(),
|
||
};
|
||
}
|
||
|
||
// --- Daily challenge constraint (with time-low colour warning) ---
|
||
if let Ok((mut t, mut color)) = challenge_q.single_mut() {
|
||
if g.is_won() {
|
||
**t = String::new();
|
||
} else if let Some(dc) = daily.as_deref() {
|
||
**t = challenge_hud_text(dc);
|
||
if let Some(max_secs) = dc.max_time_secs {
|
||
let remaining = max_secs.saturating_sub(g.elapsed_seconds);
|
||
*color = TextColor(challenge_time_color(remaining));
|
||
}
|
||
} else {
|
||
**t = String::new();
|
||
}
|
||
}
|
||
|
||
// --- Undo count ---
|
||
if let Ok((mut t, mut color)) = undos_q.single_mut() {
|
||
let count = g.undo_count();
|
||
if count == 0 {
|
||
**t = String::new();
|
||
*color = TextColor(TEXT_PRIMARY);
|
||
} else {
|
||
**t = format!("Undos: {count}");
|
||
// STATE_WARNING signals "you took a penalty" — same hue
|
||
// as the Recycles counter so they read as one category.
|
||
*color = TextColor(STATE_WARNING);
|
||
}
|
||
}
|
||
|
||
// --- Recycle counter (both modes, hidden until first recycle) ---
|
||
if let Ok(mut t) = recycles_q.single_mut() {
|
||
**t = if g.recycle_count() > 0 {
|
||
format!("Recycles: {}", g.recycle_count())
|
||
} else {
|
||
String::new()
|
||
};
|
||
}
|
||
|
||
// --- Draw-cycle indicator (Draw-Three mode only) ---
|
||
if let Ok(mut t) = draw_cycle_q.single_mut() {
|
||
**t = if g.is_won() || g.draw_mode() != DrawStockConfig::DrawThree {
|
||
// Hide when not in Draw-Three or after the game is won.
|
||
String::new()
|
||
} else {
|
||
let stock_len = g.stock_cards().len();
|
||
let next_draw = stock_len.min(3);
|
||
format!("Cycle: {next_draw}/3")
|
||
};
|
||
}
|
||
}
|
||
|
||
// Time display: show Time Attack countdown every frame when active;
|
||
// Zen mode suppresses the timer per spec ("No timer") — cleared unconditionally
|
||
// every frame so it disappears immediately on the frame Z is pressed.
|
||
// Otherwise show game elapsed time (updates once per second via game.is_changed()).
|
||
let is_zen = game.0.mode == GameMode::Zen;
|
||
let update_time = (ta_active || game.is_changed()) && !is_zen;
|
||
if update_time {
|
||
if let Ok(mut t) = time_q.single_mut() {
|
||
if let Some(ta) = time_attack.as_ref().filter(|ta| ta.active) {
|
||
let remaining = ta.remaining_secs.max(0.0) as u64;
|
||
let m = remaining / 60;
|
||
let s = remaining % 60;
|
||
**t = format!("{m}:{s:02}");
|
||
} else {
|
||
let secs = game.0.elapsed_seconds;
|
||
let m = secs / 60;
|
||
let s = secs % 60;
|
||
**t = format!("{m}:{s:02}");
|
||
}
|
||
}
|
||
} else if is_zen {
|
||
// Clear the time display immediately whenever Zen mode is active —
|
||
// do not guard on game.is_changed() so it clears on the same frame
|
||
// the player presses Z, before any move is made.
|
||
if let Ok(mut t) = time_q.single_mut() {
|
||
**t = String::new();
|
||
}
|
||
}
|
||
|
||
// --- Auto-complete badge ---
|
||
// Reflects the AutoCompleteState resource; update whenever it changes or game changes.
|
||
let ac_active = auto_complete.as_ref().is_some_and(|ac| ac.active);
|
||
let ac_changed = auto_complete.as_ref().is_some_and(|ac| ac.is_changed());
|
||
if (ac_changed || game.is_changed())
|
||
&& let Ok(mut t) = auto_q.single_mut()
|
||
{
|
||
**t = if ac_active {
|
||
"AUTO".to_string()
|
||
} else {
|
||
String::new()
|
||
};
|
||
}
|
||
}
|
||
|
||
/// Updates the `HudSelection` text node to show which pile is Tab-selected.
|
||
///
|
||
/// Displays `"▶ {pile_name}"` while `SelectionState::selected_pile` is `Some`,
|
||
/// or an empty string when no pile is selected. Runs every frame so the
|
||
/// indicator stays in sync with the selection resource.
|
||
pub(super) fn update_selection_hud(
|
||
selection: Option<Res<SelectionState>>,
|
||
game: Option<Res<GameStateResource>>,
|
||
mut q: Query<&mut Text, With<HudSelection>>,
|
||
) {
|
||
let Ok(mut t) = q.single_mut() else { return };
|
||
let label = match selection.as_deref().and_then(|s| s.selected_pile.as_ref()) {
|
||
None => String::new(),
|
||
Some(KlondikePile::Stock) => "▶ Waste".to_string(),
|
||
Some(KlondikePile::Foundation(slot)) => match game.as_deref() {
|
||
Some(g) => foundation_selection_label(*slot, &g.0),
|
||
// No game resource means we can't probe claimed_suit; show the
|
||
// slot-based placeholder so the HUD still surfaces the selection.
|
||
None => format!("▶ Foundation {}", foundation_number(*slot)),
|
||
},
|
||
Some(KlondikePile::Tableau(idx)) => format!("▶ Column {}", tableau_number(*idx)),
|
||
};
|
||
**t = label;
|
||
}
|
||
|
||
/// Returns the HUD selection label for a foundation slot.
|
||
///
|
||
/// When the slot has a claimed suit (any card has landed) the announcement is
|
||
/// "▶ {Suit} Foundation"; while the slot is empty it falls back to a
|
||
/// "▶ Foundation N" placeholder labelled by the 1-based slot index.
|
||
pub(super) fn foundation_selection_label(
|
||
slot: Foundation,
|
||
game: &solitaire_core::game_state::GameState,
|
||
) -> String {
|
||
let claimed = game
|
||
.pile(KlondikePile::Foundation(slot))
|
||
.first()
|
||
.map(|c| c.0.suit());
|
||
match claimed {
|
||
Some(suit) => {
|
||
let s = match suit {
|
||
Suit::Clubs => "Clubs",
|
||
Suit::Diamonds => "Diamonds",
|
||
Suit::Hearts => "Hearts",
|
||
Suit::Spades => "Spades",
|
||
};
|
||
format!("▶ {s} Foundation")
|
||
}
|
||
None => format!("▶ Foundation {}", foundation_number(slot)),
|
||
}
|
||
}
|
||
|
||
const fn foundation_number(foundation: Foundation) -> u8 {
|
||
match foundation {
|
||
Foundation::Foundation1 => 1,
|
||
Foundation::Foundation2 => 2,
|
||
Foundation::Foundation3 => 3,
|
||
Foundation::Foundation4 => 4,
|
||
}
|
||
}
|
||
|
||
const fn tableau_number(tableau: Tableau) -> u8 {
|
||
match tableau {
|
||
Tableau::Tableau1 => 1,
|
||
Tableau::Tableau2 => 2,
|
||
Tableau::Tableau3 => 3,
|
||
Tableau::Tableau4 => 4,
|
||
Tableau::Tableau5 => 5,
|
||
Tableau::Tableau6 => 6,
|
||
Tableau::Tableau7 => 7,
|
||
}
|
||
}
|
||
|
||
/// Fires `InfoToastEvent("Auto-completing...")` exactly once each time
|
||
/// `AutoCompleteState` transitions from inactive to active. Uses a `Local<bool>`
|
||
/// to debounce so the toast only appears on the leading edge.
|
||
pub(super) fn announce_auto_complete(
|
||
auto_complete: Option<Res<AutoCompleteState>>,
|
||
mut toast: MessageWriter<InfoToastEvent>,
|
||
mut was_active: Local<bool>,
|
||
) {
|
||
let now_active = auto_complete.as_ref().is_some_and(|ac| ac.active);
|
||
if now_active && !*was_active {
|
||
toast.write(InfoToastEvent("Auto-completing...".to_string()));
|
||
}
|
||
*was_active = now_active;
|
||
}
|
||
|
||
/// Builds the HUD text for the active daily challenge constraints.
|
||
///
|
||
/// Returns `"Limit: mm:ss"` when a time limit is set, `"Goal: N pts"` when a
|
||
/// score target is set, or an empty string when the challenge has no extra
|
||
/// constraints.
|
||
pub(super) fn challenge_hud_text(dc: &DailyChallengeResource) -> String {
|
||
if let Some(secs) = dc.max_time_secs {
|
||
format!("Limit: {}", format_time_limit(secs))
|
||
} else if let Some(score) = dc.target_score {
|
||
format!("Goal: {score} pts")
|
||
} else {
|
||
String::new()
|
||
}
|
||
}
|
||
|
||
/// Returns the colour for the challenge time-limit HUD label based on
|
||
/// seconds remaining. Uses theme tokens so the urgency ramp picks up
|
||
/// palette changes for free.
|
||
///
|
||
/// | Remaining | Token |
|
||
/// |-------------|------------------|
|
||
/// | ≥ 60 s | `STATE_INFO` |
|
||
/// | 30 – 59 s | `STATE_WARNING` |
|
||
/// | < 30 s | `STATE_DANGER` |
|
||
pub fn challenge_time_color(remaining: u64) -> Color {
|
||
if remaining < 30 {
|
||
STATE_DANGER
|
||
} else if remaining < 60 {
|
||
STATE_WARNING
|
||
} else {
|
||
STATE_INFO
|
||
}
|
||
}
|
||
|
||
/// Scales HUD Tier-1 font sizes to fit a narrow viewport.
|
||
///
|
||
/// Fires on every `WindowResized` event. Below 480 logical pixels wide the
|
||
/// score drops from `TYPE_HEADLINE` (26 px) to `TYPE_BODY_LG` (18 px) and the
|
||
/// Moves/Timer labels drop from `TYPE_BODY_LG` to `TYPE_CAPTION` (11 px), so
|
||
/// all three items remain on one row inside the 50 %-wide HUD column
|
||
/// (≈ 180 dp on a 360 dp phone). At ≥ 480 px the original sizes are
|
||
/// restored so desktop/tablet layouts are unaffected.
|
||
type HudScoreFont<'w, 's> =
|
||
Query<'w, 's, &'static mut TextFont, (With<HudScore>, Without<HudMoves>, Without<HudTime>)>;
|
||
type HudMovesFont<'w, 's> =
|
||
Query<'w, 's, &'static mut TextFont, (With<HudMoves>, Without<HudScore>, Without<HudTime>)>;
|
||
type HudTimeFont<'w, 's> =
|
||
Query<'w, 's, &'static mut TextFont, (With<HudTime>, Without<HudScore>, Without<HudMoves>)>;
|
||
|
||
pub(super) fn update_hud_typography(
|
||
mut events: MessageReader<WindowResized>,
|
||
mut score_q: HudScoreFont,
|
||
mut moves_q: HudMovesFont,
|
||
mut time_q: HudTimeFont,
|
||
) {
|
||
let Some(ev) = events.read().last() else {
|
||
return;
|
||
};
|
||
let (score_size, secondary_size) = if ev.width < 480.0 {
|
||
(TYPE_BODY_LG, TYPE_CAPTION)
|
||
} else {
|
||
(TYPE_HEADLINE, TYPE_BODY_LG)
|
||
};
|
||
for mut font in &mut score_q {
|
||
font.font_size = score_size;
|
||
}
|
||
for mut font in &mut moves_q {
|
||
font.font_size = secondary_size;
|
||
}
|
||
for mut font in &mut time_q {
|
||
font.font_size = secondary_size;
|
||
}
|
||
}
|
||
|
||
pub(super) fn apply_hud_visibility(
|
||
hud_vis: Res<HudVisibility>,
|
||
mut action_bar: Query<&mut Visibility, With<HudActionBar>>,
|
||
) {
|
||
if !hud_vis.is_changed() {
|
||
return;
|
||
}
|
||
let v = if *hud_vis == HudVisibility::Visible {
|
||
Visibility::Visible
|
||
} else {
|
||
Visibility::Hidden
|
||
};
|
||
for mut vis in &mut action_bar {
|
||
*vis = v;
|
||
}
|
||
// The bottom action bar is a pure overlay — it does not claim any
|
||
// space in the card layout, so no WindowResized event is needed.
|
||
}
|
||
|
||
pub(super) fn restore_hud_on_modal(
|
||
new_scrims: Query<(), (With<ModalScrim>, Added<ModalScrim>)>,
|
||
mut hud_vis: ResMut<HudVisibility>,
|
||
) {
|
||
if !new_scrims.is_empty() {
|
||
*hud_vis = HudVisibility::Visible;
|
||
}
|
||
}
|
||
|
||
/// Returns the action-bar label font size for a given logical window width.
|
||
pub(super) fn action_bar_font_size(window_width: f32) -> f32 {
|
||
if USE_TOUCH_UI_LAYOUT {
|
||
// Seven word-labels ("Menu","Undo","Pause","Help","Hint","Mode","New")
|
||
// must share one row. The widest characters are in FiraMono (a
|
||
// monospace whose advance is ~0.62 of the font size). On a 900
|
||
// logical-px phone the row budget after bar padding (2*12) and six
|
||
// 4 px column gaps is ~852 px for ~28 label chars + 7*2*3 px button
|
||
// padding. Solving 28*0.62*size + 42 <= 852 gives size <= ~46, so the
|
||
// labels are advance-bound only on very narrow viewports; the real
|
||
// constraint is legibility, not fit. ~1/60 of the width yields ~15 px
|
||
// at 900 px — comfortably one row with margin to spare — clamped so it
|
||
// never drops below the 12 px legibility floor or grows past 18 px on
|
||
// landscape tablets where it would crowd the row again.
|
||
(window_width / 60.0).clamp(12.0, 18.0)
|
||
} else {
|
||
TYPE_BODY
|
||
}
|
||
}
|
||
|
||
pub(super) fn action_button_metrics() -> (UiRect, Val, Val) {
|
||
if USE_TOUCH_UI_LAYOUT {
|
||
// Tight 3 px horizontal padding keeps the compact buttons narrow;
|
||
// Phase F trimmed the touch bar to 5 buttons (2 compact + 3
|
||
// primary), so width pressure is lower than the old 7-button row.
|
||
// min_height stays at 44 px to preserve the comfortable touch
|
||
// target on the compact buttons.
|
||
(
|
||
UiRect::axes(Val::Px(3.0), Val::Px(4.0)),
|
||
Val::Px(44.0),
|
||
Val::Px(44.0),
|
||
)
|
||
} else {
|
||
(
|
||
UiRect::axes(VAL_SPACE_2, VAL_SPACE_2),
|
||
Val::Px(48.0),
|
||
Val::Px(48.0),
|
||
)
|
||
}
|
||
}
|
||
|
||
pub(super) fn spawn_action_button_label(
|
||
parent: &mut ChildSpawnerCommands,
|
||
label: &str,
|
||
font: &TextFont,
|
||
text_color: Color,
|
||
) {
|
||
if USE_TOUCH_UI_LAYOUT {
|
||
parent.spawn((
|
||
ActionButtonLabel,
|
||
Text::new(label),
|
||
font.clone(),
|
||
TextColor(text_color),
|
||
));
|
||
} else {
|
||
parent.spawn((Text::new(label), font.clone(), TextColor(text_color)));
|
||
}
|
||
}
|
||
|
||
/// Resizes the glyph text inside every [`ActionButtonLabel`] to match the
|
||
/// current viewport width whenever [`LayoutResource`] changes (orientation
|
||
/// change or window resize).
|
||
#[cfg(target_os = "android")]
|
||
pub(super) fn resize_action_bar_labels(
|
||
layout: Res<crate::layout::LayoutResource>,
|
||
windows: Query<&Window>,
|
||
mut labels: Query<&mut TextFont, With<ActionButtonLabel>>,
|
||
) {
|
||
let w = windows
|
||
.iter()
|
||
.next()
|
||
.map_or(layout.0.card_size.x * 7.25, |win| win.width());
|
||
let new_size = action_bar_font_size(w);
|
||
for mut font in &mut labels {
|
||
font.font_size = new_size;
|
||
}
|
||
}
|