113a933170
The repo was formatted under an older stable; rustfmt 1.9 (Rust 1.95) wraps signatures and call sites differently, so every touched file was picking up unrelated formatting hunks. One mechanical pass, and a 'cargo fmt --check' step in the test workflow (same pinned 1.95.0 toolchain) so drift can't accumulate again. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
207 lines
7.1 KiB
Rust
207 lines
7.1 KiB
Rust
//! Card face labels: desktop text labels and Android corner labels.
|
||
|
||
use super::*;
|
||
|
||
use bevy::color::Color;
|
||
use bevy::sprite::Anchor;
|
||
use solitaire_core::{Card, Rank, Suit};
|
||
|
||
use crate::ui_theme::TEXT_PRIMARY_HC;
|
||
|
||
pub(super) fn label_for(card: &Card) -> String {
|
||
let rank = match card.rank() {
|
||
Rank::Ace => "A",
|
||
Rank::Two => "2",
|
||
Rank::Three => "3",
|
||
Rank::Four => "4",
|
||
Rank::Five => "5",
|
||
Rank::Six => "6",
|
||
Rank::Seven => "7",
|
||
Rank::Eight => "8",
|
||
Rank::Nine => "9",
|
||
Rank::Ten => "10",
|
||
Rank::Jack => "J",
|
||
Rank::Queen => "Q",
|
||
Rank::King => "K",
|
||
};
|
||
let suit = match card.suit() {
|
||
Suit::Clubs => "C",
|
||
Suit::Diamonds => "D",
|
||
Suit::Hearts => "H",
|
||
Suit::Spades => "S",
|
||
};
|
||
format!("{rank}{suit}")
|
||
}
|
||
|
||
/// Suit colour for the rank/suit overlay rendered atop the constant
|
||
/// fallback sprite (only fires under `MinimalPlugins` — production
|
||
/// renders the suit glyph baked into the PNG). 2-colour traditional
|
||
/// pairing — hearts + diamonds share the saturated red, clubs +
|
||
/// spades share the near-white. Two accessibility flags compose:
|
||
///
|
||
/// - `color_blind`: red-suit cards swap to `RED_SUIT_COLOUR_CBM`
|
||
/// (lime) — the "Settings toggle swaps red→lime" half of the
|
||
/// design system's colour-blind support. CBM is a hue-replacement
|
||
/// for red, so HC has no further effect on red when CBM is on
|
||
/// (the lime is itself a high-luminance colour).
|
||
/// - `high_contrast`: when CBM is off, red suits boost to
|
||
/// `RED_SUIT_COLOUR_HC` (`#ff6868`); black suits boost from
|
||
/// `#e8e8e8` (near-white) to `#f5f5f5` (`TEXT_PRIMARY_HC`).
|
||
///
|
||
/// The other half of CBM support (always-on filled-vs-outlined
|
||
/// glyph differentiation for ♥♠ vs ♦♣) is baked into the PNG art
|
||
/// and has no constant-fallback equivalent.
|
||
pub(super) fn text_colour(card: &Card, color_blind: bool, high_contrast: bool) -> Color {
|
||
if card.suit().is_red() {
|
||
if color_blind {
|
||
// CBM lime wins — the colour-blind swap replaces the
|
||
// red hue entirely, and the lime is already high-
|
||
// luminance, so an HC boost on top has nothing to do.
|
||
RED_SUIT_COLOUR_CBM
|
||
} else if high_contrast {
|
||
RED_SUIT_COLOUR_HC
|
||
} else {
|
||
RED_SUIT_COLOUR
|
||
}
|
||
} else if high_contrast {
|
||
TEXT_PRIMARY_HC
|
||
} else {
|
||
BLACK_SUIT_COLOUR
|
||
}
|
||
}
|
||
|
||
pub(super) fn label_visibility(face_up: bool) -> Visibility {
|
||
if face_up {
|
||
Visibility::Inherited
|
||
} else {
|
||
Visibility::Hidden
|
||
}
|
||
}
|
||
|
||
/// Rank+suit string for the readability overlay on touch HUD layouts.
|
||
/// Uses Unicode suit glyphs (♠♥♦♣ — U+2660–U+2666, covered by FiraMono).
|
||
pub(super) fn mobile_label_for(card: &Card) -> String {
|
||
let rank = match card.rank() {
|
||
Rank::Ace => "A",
|
||
Rank::Two => "2",
|
||
Rank::Three => "3",
|
||
Rank::Four => "4",
|
||
Rank::Five => "5",
|
||
Rank::Six => "6",
|
||
Rank::Seven => "7",
|
||
Rank::Eight => "8",
|
||
Rank::Nine => "9",
|
||
Rank::Ten => "10",
|
||
Rank::Jack => "J",
|
||
Rank::Queen => "Q",
|
||
Rank::King => "K",
|
||
};
|
||
let suit = match card.suit() {
|
||
Suit::Clubs => "♣",
|
||
Suit::Diamonds => "♦",
|
||
Suit::Hearts => "♥",
|
||
Suit::Spades => "♠",
|
||
};
|
||
format!("{rank}{suit}")
|
||
}
|
||
|
||
/// Spawns the [`AndroidCornerLabel`] + [`AndroidCornerBg`] children on
|
||
/// face-up cards. The background sprite covers the card art's own small
|
||
/// corner text so only the large overlay is visible.
|
||
/// Spawns the [`AndroidCornerLabel`] + [`AndroidCornerBg`] children on
|
||
/// face-up cards using FiraMono (passed via `font_handle`) so that the
|
||
/// suit Unicode glyphs U+2660–U+2666 render correctly. Without an explicit
|
||
/// font handle Bevy falls back to its built-in face which does not include
|
||
/// those glyphs, causing a coloured missing-glyph rectangle to appear in
|
||
/// the text colour — the root cause of the "red square on face-down cards"
|
||
/// visual bug (the box bleeds through near the card edge at z=0.02).
|
||
pub(super) fn add_android_corner_label(
|
||
parent: &mut ChildSpawnerCommands,
|
||
card: &Card,
|
||
face_up: bool,
|
||
card_size: Vec2,
|
||
color_blind: bool,
|
||
high_contrast: bool,
|
||
font_handle: Option<&Handle<Font>>,
|
||
) {
|
||
if !face_up {
|
||
return;
|
||
}
|
||
let font_size = card_size.x * FONT_SIZE_FRAC_MOBILE;
|
||
let inset = 3.0_f32;
|
||
// Background covers ~3 monospace chars wide × 1 line tall.
|
||
// FiraMono char width ≈ 0.6 × font_size; 2.0× gives room for "10♠"
|
||
// (3 chars = 1.8× font_size) plus a small margin.
|
||
let bg_w = font_size * 2.0;
|
||
let bg_h = font_size * 1.25;
|
||
|
||
// Background covers the PNG's baked-in small corner text (top-left).
|
||
// Classic PNG cards have a white face, so the background must be white too.
|
||
// (CARD_FACE_COLOUR is the Terminal theme's dark face colour — wrong here.)
|
||
parent.spawn((
|
||
AndroidCornerBg,
|
||
Sprite {
|
||
color: Color::WHITE,
|
||
custom_size: Some(Vec2::new(bg_w, bg_h)),
|
||
..default()
|
||
},
|
||
Transform::from_xyz(
|
||
-card_size.x / 2.0 + inset + bg_w / 2.0,
|
||
card_size.y / 2.0 - inset - bg_h / 2.0,
|
||
0.015,
|
||
),
|
||
));
|
||
// Cover the matching rotated baked-in text at the bottom-right corner.
|
||
parent.spawn((
|
||
AndroidCornerBg,
|
||
Sprite {
|
||
color: Color::WHITE,
|
||
custom_size: Some(Vec2::new(bg_w, bg_h)),
|
||
..default()
|
||
},
|
||
Transform::from_xyz(
|
||
card_size.x / 2.0 - inset - bg_w / 2.0,
|
||
-card_size.y / 2.0 + inset + bg_h / 2.0,
|
||
0.015,
|
||
),
|
||
));
|
||
|
||
// Large rank+suit text drawn on top of the background. FiraMono must be
|
||
// wired here explicitly — the suit glyphs (U+2660–U+2666) are not in
|
||
// Bevy's built-in font and render as a coloured rectangle without it.
|
||
//
|
||
// Classic PNG cards have a white face: red suits stay the same saturated
|
||
// red, but black suits must use a dark colour (CARD_FACE_COLOUR ≈ #1a1a1a)
|
||
// rather than the near-white BLACK_SUIT_COLOUR designed for the dark
|
||
// Terminal theme background.
|
||
let text_col = if card.suit().is_red() {
|
||
if color_blind {
|
||
RED_SUIT_COLOUR_CBM
|
||
} else if high_contrast {
|
||
RED_SUIT_COLOUR_HC
|
||
} else {
|
||
RED_SUIT_COLOUR
|
||
}
|
||
} else {
|
||
CARD_FACE_COLOUR
|
||
};
|
||
let label_text = mobile_label_for(card);
|
||
parent.spawn((
|
||
AndroidCornerLabel(label_text.clone()),
|
||
CardLabel,
|
||
Text2d::new(label_text),
|
||
TextFont {
|
||
font: font_handle.cloned().unwrap_or_default(),
|
||
font_size,
|
||
..default()
|
||
},
|
||
TextColor(text_col),
|
||
Anchor::TOP_LEFT,
|
||
Transform::from_xyz(-card_size.x / 2.0 + inset, card_size.y / 2.0 - inset, 0.02),
|
||
));
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Task #34 — Card-flip animation systems
|
||
// ---------------------------------------------------------------------------
|