Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6cd8c6c013 | |||
| ec94cb34aa | |||
| 40768f3b0a |
@@ -1147,7 +1147,7 @@ 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;
|
||||||
|
|
||||||
// Background covers the PNG's baked-in small corner text.
|
// 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.
|
// 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.)
|
// (CARD_FACE_COLOUR is the Terminal theme's dark face colour — wrong here.)
|
||||||
parent.spawn((
|
parent.spawn((
|
||||||
@@ -1163,6 +1163,20 @@ fn add_android_corner_label(
|
|||||||
0.015,
|
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
|
// 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
|
||||||
|
|||||||
@@ -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()
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -964,7 +989,7 @@ fn spawn_action_button<M: Component>(
|
|||||||
// centred with room to breathe. On desktop, keep the comfortable 48 dp
|
// centred with room to breathe. On desktop, keep the comfortable 48 dp
|
||||||
// floor and 8 dp side padding.
|
// floor and 8 dp side padding.
|
||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
let (pad, min_w, min_h) = (UiRect::axes(Val::Px(4.0), Val::Px(4.0)), Val::Px(44.0), Val::Px(44.0));
|
let (pad, min_w, min_h) = (UiRect::axes(Val::Px(4.0), Val::Px(4.0)), Val::Px(52.0), Val::Px(44.0));
|
||||||
#[cfg(not(target_os = "android"))]
|
#[cfg(not(target_os = "android"))]
|
||||||
let (pad, min_w, min_h) = (UiRect::axes(VAL_SPACE_2, VAL_SPACE_2), Val::Px(48.0), Val::Px(48.0));
|
let (pad, min_w, min_h) = (UiRect::axes(VAL_SPACE_2, VAL_SPACE_2), Val::Px(48.0), Val::Px(48.0));
|
||||||
|
|
||||||
@@ -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>,
|
||||||
|
|||||||
@@ -107,6 +107,23 @@ pub const HUD_BAND_HEIGHT: f32 = 64.0;
|
|||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
pub const HUD_BAND_HEIGHT: f32 = 112.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];
|
||||||
|
|
||||||
@@ -190,9 +207,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;
|
||||||
@@ -241,7 +262,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 {
|
||||||
|
|||||||
@@ -473,8 +473,11 @@ fn radial_open_on_long_press(
|
|||||||
mut state: ResMut<RightClickRadialState>,
|
mut state: ResMut<RightClickRadialState>,
|
||||||
) {
|
) {
|
||||||
// Guard: only count while a touch is down, uncommitted, and radial is idle.
|
// Guard: only count while a touch is down, uncommitted, and radial is idle.
|
||||||
let active_id = drag.active_touch_id;
|
let Some(active_id) = drag.active_touch_id else {
|
||||||
if active_id.is_none() || drag.committed || state.is_active() || paused.is_some_and(|p| p.0) {
|
*hold_timer = 0.0;
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if drag.committed || state.is_active() || paused.is_some_and(|p| p.0) {
|
||||||
*hold_timer = 0.0;
|
*hold_timer = 0.0;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -487,7 +490,7 @@ fn radial_open_on_long_press(
|
|||||||
|
|
||||||
// Resolve current touch world position.
|
// Resolve current touch world position.
|
||||||
let Some(touches) = touches else { return };
|
let Some(touches) = touches else { return };
|
||||||
let Some(touch) = touches.iter().find(|t| t.id() == active_id.unwrap()) else {
|
let Some(touch) = touches.iter().find(|t| t.id() == active_id) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let Some((camera, cam_xf)) = cameras.single().ok() else { return };
|
let Some((camera, cam_xf)) = cameras.single().ok() else { return };
|
||||||
|
|||||||
Reference in New Issue
Block a user