Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ec94cb34aa | |||
| 40768f3b0a | |||
| 2186f55913 |
@@ -355,7 +355,7 @@ Must always be handled explicitly:
|
|||||||
* The gesture/navigation bar at the bottom (≈132px physical on common
|
* The gesture/navigation bar at the bottom (≈132px physical on common
|
||||||
devices) is inside the Bevy viewport; use `SafeAreaInsets.bottom` to
|
devices) is inside the Bevy viewport; use `SafeAreaInsets.bottom` to
|
||||||
avoid placing interactive elements in that zone
|
avoid placing interactive elements in that zone
|
||||||
* `HUD_BAND_HEIGHT` is 128px on Android (two-row wrap) vs 64px on desktop;
|
* `HUD_BAND_HEIGHT` is 112px on Android vs 64px on desktop;
|
||||||
layout constants are `#[cfg(target_os = "android")]` gated
|
layout constants are `#[cfg(target_os = "android")]` gated
|
||||||
* JNI calls must use `attach_current_thread_permanently` — not
|
* JNI calls must use `attach_current_thread_permanently` — not
|
||||||
`attach_current_thread` — to avoid detach-on-drop panics
|
`attach_current_thread` — to avoid detach-on-drop panics
|
||||||
|
|||||||
@@ -1147,11 +1147,13 @@ fn add_android_corner_label(
|
|||||||
let bg_w = font_size * 2.0;
|
let bg_w = font_size * 2.0;
|
||||||
let bg_h = font_size * 1.25;
|
let bg_h = font_size * 1.25;
|
||||||
|
|
||||||
// Solid background that hides the card art's small corner label.
|
// Background covers the PNG's baked-in small corner text.
|
||||||
|
// 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((
|
parent.spawn((
|
||||||
AndroidCornerBg,
|
AndroidCornerBg,
|
||||||
Sprite {
|
Sprite {
|
||||||
color: CARD_FACE_COLOUR,
|
color: Color::WHITE,
|
||||||
custom_size: Some(Vec2::new(bg_w, bg_h)),
|
custom_size: Some(Vec2::new(bg_w, bg_h)),
|
||||||
..default()
|
..default()
|
||||||
},
|
},
|
||||||
@@ -1165,6 +1167,22 @@ fn add_android_corner_label(
|
|||||||
// Large rank+suit text drawn on top of the background. FiraMono must be
|
// 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
|
// 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.
|
// 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);
|
let label_text = mobile_label_for(card);
|
||||||
parent.spawn((
|
parent.spawn((
|
||||||
AndroidCornerLabel(label_text.clone()),
|
AndroidCornerLabel(label_text.clone()),
|
||||||
@@ -1175,7 +1193,7 @@ fn add_android_corner_label(
|
|||||||
font_size,
|
font_size,
|
||||||
..default()
|
..default()
|
||||||
},
|
},
|
||||||
TextColor(text_colour(card, color_blind, high_contrast)),
|
TextColor(text_col),
|
||||||
Anchor::TOP_LEFT,
|
Anchor::TOP_LEFT,
|
||||||
Transform::from_xyz(
|
Transform::from_xyz(
|
||||||
-card_size.x / 2.0 + inset,
|
-card_size.x / 2.0 + inset,
|
||||||
|
|||||||
@@ -140,6 +140,12 @@ pub struct HudColumn;
|
|||||||
#[derive(Component, Debug)]
|
#[derive(Component, Debug)]
|
||||||
pub struct HudActionBar;
|
pub struct HudActionBar;
|
||||||
|
|
||||||
|
/// Marker on the text node inside each action-bar button (Android only).
|
||||||
|
/// Used by `resize_action_bar_labels` to update font size on window resize.
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
#[derive(Component, Debug)]
|
||||||
|
struct ActionButtonLabel;
|
||||||
|
|
||||||
/// Marker on the circular profile-picture button anchored to the
|
/// Marker on the circular profile-picture button anchored to the
|
||||||
/// top-right of the HUD band. Pressing it opens the Profile overlay.
|
/// top-right of the HUD band. Pressing it opens the Profile overlay.
|
||||||
/// Shows the server avatar image when loaded; falls back to the player's
|
/// Shows the server avatar image when loaded; falls back to the player's
|
||||||
@@ -489,6 +495,11 @@ impl Plugin for HudPlugin {
|
|||||||
.after(TouchDragSet::AfterStartDrag)
|
.after(TouchDragSet::AfterStartDrag)
|
||||||
.in_set(TouchDragSet::BeforeEndDrag),
|
.in_set(TouchDragSet::BeforeEndDrag),
|
||||||
);
|
);
|
||||||
|
app.add_systems(
|
||||||
|
Update,
|
||||||
|
resize_action_bar_labels
|
||||||
|
.run_if(resource_exists_and_changed::<crate::layout::LayoutResource>),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -843,11 +854,25 @@ fn handle_avatar_button(
|
|||||||
/// on its own visual edge.
|
/// on its own visual edge.
|
||||||
fn spawn_action_buttons(
|
fn spawn_action_buttons(
|
||||||
font_res: Option<Res<FontResource>>,
|
font_res: Option<Res<FontResource>>,
|
||||||
|
windows: Query<&Window>,
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
) {
|
) {
|
||||||
|
// On Android the glyph labels must scale with the viewport so they remain
|
||||||
|
// legible on any screen density. Use the window width at startup; the
|
||||||
|
// resize_action_bar_labels system keeps this current on window changes.
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
let action_font_size = {
|
||||||
|
let w = windows.iter().next().map_or(900.0, |win| win.width());
|
||||||
|
action_bar_font_size(w)
|
||||||
|
};
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
|
let action_font_size = TYPE_BODY;
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
|
let _windows = windows;
|
||||||
|
|
||||||
let font = TextFont {
|
let font = TextFont {
|
||||||
font: font_res.as_ref().map(|f| f.0.clone()).unwrap_or_default(),
|
font: font_res.as_ref().map(|f| f.0.clone()).unwrap_or_default(),
|
||||||
font_size: TYPE_BODY,
|
font_size: action_font_size,
|
||||||
..default()
|
..default()
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -992,6 +1017,9 @@ fn spawn_action_button<M: Component>(
|
|||||||
HighContrastBorder::with_default(BORDER_SUBTLE),
|
HighContrastBorder::with_default(BORDER_SUBTLE),
|
||||||
))
|
))
|
||||||
.with_children(|b| {
|
.with_children(|b| {
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
b.spawn((ActionButtonLabel, Text::new(label), font.clone(), TextColor(text_color)));
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
b.spawn((Text::new(label), font.clone(), TextColor(text_color)));
|
b.spawn((Text::new(label), font.clone(), TextColor(text_color)));
|
||||||
if let Some(key) = hotkey {
|
if let Some(key) = hotkey {
|
||||||
// Hotkey hint rendered as a dim caption next to the label —
|
// Hotkey hint rendered as a dim caption next to the label —
|
||||||
@@ -2483,6 +2511,32 @@ fn restore_hud_on_modal(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the action-bar glyph font size for a given logical window width.
|
||||||
|
/// Scales linearly so glyphs remain legible at any phone density.
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
fn action_bar_font_size(window_width: f32) -> f32 {
|
||||||
|
// ~1/40 of the window width gives ~22 px on a 900 logical-px phone.
|
||||||
|
// Clamped so it never goes too tiny on narrow viewports or too large
|
||||||
|
// on landscape tablets.
|
||||||
|
(window_width / 40.0).clamp(16.0, 30.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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")]
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
fn toggle_hud_on_tap(
|
fn toggle_hud_on_tap(
|
||||||
mut touch_events: MessageReader<bevy::input::touch::TouchInput>,
|
mut touch_events: MessageReader<bevy::input::touch::TouchInput>,
|
||||||
|
|||||||
@@ -96,13 +96,32 @@ const MAX_TABLEAU_CARDS: f32 = 13.0;
|
|||||||
/// below this band so the HUD doesn't bleed into the play surface.
|
/// below this band so the HUD doesn't bleed into the play surface.
|
||||||
///
|
///
|
||||||
/// Desktop: 64 px fits the score/moves/time + mode badge rows.
|
/// Desktop: 64 px fits the score/moves/time + mode badge rows.
|
||||||
/// Android: 80 px gives the same content rows comfortable clearance.
|
/// Android: 112 px — the HUD column has 4 flex tiers with 3 inter-tier
|
||||||
/// (Previously 128 px when action buttons lived in the top band; those are
|
/// gaps (4 px each) plus a SPACE_2 = 8 px top offset. With empty tiers
|
||||||
/// now in the bottom bar so the larger reserve is no longer needed.)
|
/// still contributing gap height in Bevy's flex layout, the actual HUD
|
||||||
|
/// height can reach ~80 px before the grid starts; 112 px gives ~28 px
|
||||||
|
/// of clearance between the HUD bottom and the top card edge, preventing
|
||||||
|
/// the overlap seen with the previous 80 px value.
|
||||||
#[cfg(not(target_os = "android"))]
|
#[cfg(not(target_os = "android"))]
|
||||||
pub const HUD_BAND_HEIGHT: f32 = 64.0;
|
pub const HUD_BAND_HEIGHT: f32 = 64.0;
|
||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
pub const HUD_BAND_HEIGHT: f32 = 80.0;
|
pub const HUD_BAND_HEIGHT: f32 = 112.0;
|
||||||
|
|
||||||
|
/// Height of the bottom action-bar (the row of ≡ ← || ? ! M + buttons).
|
||||||
|
///
|
||||||
|
/// The action bar sits *above* the OS gesture/navigation zone, so it is NOT
|
||||||
|
/// covered by `safe_area_bottom`. `compute_layout` adds this constant to
|
||||||
|
/// `safe_area_bottom` before computing the height-based card-size candidate
|
||||||
|
/// and the available tableau height, ensuring the deepest fanned column
|
||||||
|
/// never scrolls behind the button row.
|
||||||
|
///
|
||||||
|
/// Derivation (Android): `min_height 44 px` buttons
|
||||||
|
/// + `padding.top 8 px` + `padding.bottom 8 px` outer bar padding = **60 px**.
|
||||||
|
/// Desktop: no persistent bottom bar, so 0.
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
|
const BOTTOM_BAR_HEIGHT: f32 = 0.0;
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
const BOTTOM_BAR_HEIGHT: f32 = 60.0;
|
||||||
|
|
||||||
/// Table background colour (dark green felt).
|
/// Table background colour (dark green felt).
|
||||||
pub const TABLE_COLOUR: [f32; 3] = [0.059, 0.322, 0.196];
|
pub const TABLE_COLOUR: [f32; 3] = [0.059, 0.322, 0.196];
|
||||||
@@ -187,9 +206,13 @@ pub fn compute_layout(window: Vec2, safe_area_top: f32, safe_area_bottom: f32, h
|
|||||||
// Substituting h_gap = w/4 and h = CARD_ASPECT * w and solving for the
|
// Substituting h_gap = w/4 and h = CARD_ASPECT * w and solving for the
|
||||||
// largest w that fits gives:
|
// largest w that fits gives:
|
||||||
// (window.y - HUD_BAND_HEIGHT) = w * (0.5 + (1 + fan_factor + VERTICAL_GAP_FRAC) * CARD_ASPECT)
|
// (window.y - HUD_BAND_HEIGHT) = w * (0.5 + (1 + fan_factor + VERTICAL_GAP_FRAC) * CARD_ASPECT)
|
||||||
|
// Reserve space for both the OS gesture/nav bar and the app's own action
|
||||||
|
// bar, which sits above it and is invisible to safe_area_bottom.
|
||||||
|
let effective_safe_bottom = safe_area_bottom + BOTTOM_BAR_HEIGHT;
|
||||||
|
|
||||||
let fan_factor = 1.0 + (MAX_TABLEAU_CARDS - 1.0) * TABLEAU_FAN_FRAC;
|
let fan_factor = 1.0 + (MAX_TABLEAU_CARDS - 1.0) * TABLEAU_FAN_FRAC;
|
||||||
let height_denom = 0.5 + (1.0 + fan_factor + VERTICAL_GAP_FRAC) * CARD_ASPECT;
|
let height_denom = 0.5 + (1.0 + fan_factor + VERTICAL_GAP_FRAC) * CARD_ASPECT;
|
||||||
let card_width_height_based = (window.y - safe_area_top - safe_area_bottom - band_h).max(0.0) / height_denom;
|
let card_width_height_based = (window.y - safe_area_top - effective_safe_bottom - band_h).max(0.0) / height_denom;
|
||||||
|
|
||||||
let card_width = card_width_width_based.min(card_width_height_based);
|
let card_width = card_width_width_based.min(card_width_height_based);
|
||||||
let card_height = card_width * CARD_ASPECT;
|
let card_height = card_width * CARD_ASPECT;
|
||||||
@@ -238,7 +261,7 @@ pub fn compute_layout(window: Vec2, safe_area_top: f32, safe_area_bottom: f32, h
|
|||||||
//
|
//
|
||||||
// avail = distance from the top of the first tableau card to the bottom
|
// avail = distance from the top of the first tableau card to the bottom
|
||||||
// margin — i.e. the space available for 12 fan steps.
|
// margin — i.e. the space available for 12 fan steps.
|
||||||
let avail = (tableau_y - (-window.y / 2.0 + safe_area_bottom + h_gap) - card_height / 2.0).max(0.0);
|
let avail = (tableau_y - (-window.y / 2.0 + effective_safe_bottom + h_gap) - card_height / 2.0).max(0.0);
|
||||||
let ideal_fan_frac = if card_height > 0.0 {
|
let ideal_fan_frac = if card_height > 0.0 {
|
||||||
avail / ((MAX_TABLEAU_CARDS - 1.0) * card_height)
|
avail / ((MAX_TABLEAU_CARDS - 1.0) * card_height)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user