Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -4,11 +4,11 @@
|
||||
//! summary in a single scrollable panel. Spawned on the first `P` keypress and
|
||||
//! despawned on the second.
|
||||
|
||||
use bevy::input::mouse::{MouseScrollUnit, MouseWheel};
|
||||
use bevy::input::ButtonInput;
|
||||
use bevy::input::mouse::{MouseScrollUnit, MouseWheel};
|
||||
use bevy::prelude::*;
|
||||
use chrono::{Duration, Local, NaiveDate};
|
||||
use solitaire_core::achievement::{achievement_by_id, ALL_ACHIEVEMENTS};
|
||||
use solitaire_core::achievement::{ALL_ACHIEVEMENTS, achievement_by_id};
|
||||
use solitaire_data::SyncBackend;
|
||||
|
||||
use crate::achievement_plugin::AchievementsResource;
|
||||
@@ -18,10 +18,10 @@ use crate::font_plugin::FontResource;
|
||||
use crate::progress_plugin::ProgressResource;
|
||||
use crate::resources::{SyncStatus, SyncStatusResource};
|
||||
use crate::settings_plugin::SettingsResource;
|
||||
use crate::stats_plugin::{format_fastest_win, format_win_rate, StatsResource};
|
||||
use crate::stats_plugin::{StatsResource, format_fastest_win, format_win_rate};
|
||||
use crate::ui_modal::{
|
||||
spawn_modal, spawn_modal_actions, spawn_modal_button, spawn_modal_header, ButtonVariant,
|
||||
ScrimDismissible,
|
||||
ButtonVariant, ModalScrim, ScrimDismissible, spawn_modal, spawn_modal_actions,
|
||||
spawn_modal_button, spawn_modal_header,
|
||||
};
|
||||
use crate::ui_theme::{
|
||||
ACCENT_PRIMARY, BG_ELEVATED, BORDER_STRONG, SPACE_1, STATE_INFO, STATE_SUCCESS, TEXT_PRIMARY,
|
||||
@@ -31,8 +31,8 @@ use crate::ui_theme::{
|
||||
/// Number of days surfaced in the daily-challenge calendar row.
|
||||
///
|
||||
/// 14 = trailing two weeks ending today. At ~12 px per dot with a 6 px gap
|
||||
/// the row is ~246 px wide — well inside the 360 px minimum modal width on
|
||||
/// the smallest supported window (800 px).
|
||||
/// the row is ~246 px wide — comfortably inside the responsive modal card
|
||||
/// even on narrow phone layouts.
|
||||
const CALENDAR_DAYS: usize = 14;
|
||||
|
||||
/// Diameter of each calendar dot, in pixels.
|
||||
@@ -146,6 +146,7 @@ fn toggle_profile_screen(
|
||||
font_res: Option<Res<FontResource>>,
|
||||
avatar: Option<Res<AvatarResource>>,
|
||||
screens: Query<Entity, With<ProfileScreen>>,
|
||||
scrims: Query<(), With<ModalScrim>>,
|
||||
) {
|
||||
let button_clicked = requests.read().count() > 0;
|
||||
let p_pressed = keys.just_pressed(KeyCode::KeyP);
|
||||
@@ -161,6 +162,9 @@ fn toggle_profile_screen(
|
||||
if !want_open && !want_close {
|
||||
return;
|
||||
}
|
||||
if want_open && !scrims.is_empty() {
|
||||
return;
|
||||
}
|
||||
if let Ok(entity) = screens.single() {
|
||||
commands.entity(entity).despawn();
|
||||
} else {
|
||||
@@ -257,7 +261,10 @@ fn spawn_profile_screen(
|
||||
flex_direction: FlexDirection::Row,
|
||||
align_items: AlignItems::Center,
|
||||
column_gap: Val::Px(10.0),
|
||||
margin: UiRect { bottom: Val::Px(4.0), ..default() },
|
||||
margin: UiRect {
|
||||
bottom: Val::Px(4.0),
|
||||
..default()
|
||||
},
|
||||
..default()
|
||||
})
|
||||
.with_children(|row| {
|
||||
@@ -275,7 +282,13 @@ fn spawn_profile_screen(
|
||||
));
|
||||
} else {
|
||||
// Initials fallback: coloured disc with the first letter.
|
||||
let initial = username.chars().next().unwrap_or('?').to_uppercase().next().unwrap_or('?');
|
||||
let initial = username
|
||||
.chars()
|
||||
.next()
|
||||
.unwrap_or('?')
|
||||
.to_uppercase()
|
||||
.next()
|
||||
.unwrap_or('?');
|
||||
row.spawn((
|
||||
Node {
|
||||
width: Val::Px(SIZE),
|
||||
@@ -335,7 +348,10 @@ fn spawn_profile_screen(
|
||||
let pct = if xp_span == 0 {
|
||||
100u64
|
||||
} else {
|
||||
xp_done.saturating_mul(100).checked_div(xp_span).unwrap_or(100)
|
||||
xp_done
|
||||
.saturating_mul(100)
|
||||
.checked_div(xp_span)
|
||||
.unwrap_or(100)
|
||||
};
|
||||
body.spawn((
|
||||
Text::new(format!(
|
||||
@@ -378,7 +394,10 @@ fn spawn_profile_screen(
|
||||
let records = &ar.0;
|
||||
let unlocked_count = records.iter().filter(|r| r.unlocked).count();
|
||||
body.spawn((
|
||||
Text::new(format!("{unlocked_count} / {} unlocked", ALL_ACHIEVEMENTS.len())),
|
||||
Text::new(format!(
|
||||
"{unlocked_count} / {} unlocked",
|
||||
ALL_ACHIEVEMENTS.len()
|
||||
)),
|
||||
font_row.clone(),
|
||||
TextColor(ACCENT_PRIMARY),
|
||||
));
|
||||
@@ -533,7 +552,11 @@ fn spawn_daily_calendar(
|
||||
// accent border) regardless of completion; past days use a
|
||||
// subtle border so the row reads as a row of pills, not a
|
||||
// strip of bare squares.
|
||||
let border_color = if is_today { ACCENT_PRIMARY } else { BORDER_STRONG };
|
||||
let border_color = if is_today {
|
||||
ACCENT_PRIMARY
|
||||
} else {
|
||||
BORDER_STRONG
|
||||
};
|
||||
let border_width = if is_today { 2.0 } else { 0.0 };
|
||||
row.spawn((
|
||||
DailyCalendarDot {
|
||||
@@ -569,9 +592,7 @@ fn calendar_dot_color(completed: bool) -> Color {
|
||||
fn sync_info(backend: &SyncBackend) -> (&'static str, String) {
|
||||
match backend {
|
||||
SyncBackend::Local => ("Local", "—".to_string()),
|
||||
SyncBackend::SolitaireServer { username, .. } => {
|
||||
("Solitaire Server", username.clone())
|
||||
}
|
||||
SyncBackend::SolitaireServer { username, .. } => ("Solitaire Server", username.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -641,6 +662,25 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pressing_p_does_not_stack_profile_over_existing_modal_scrim() {
|
||||
let mut app = headless_app();
|
||||
app.world_mut().spawn(ModalScrim);
|
||||
app.world_mut()
|
||||
.resource_mut::<ButtonInput<KeyCode>>()
|
||||
.press(KeyCode::KeyP);
|
||||
app.update();
|
||||
|
||||
assert_eq!(
|
||||
app.world_mut()
|
||||
.query::<&ProfileScreen>()
|
||||
.iter(app.world())
|
||||
.count(),
|
||||
0,
|
||||
"Profile should not open when another modal scrim already exists"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn profile_modal_body_is_scrollable() {
|
||||
let mut app = headless_app();
|
||||
|
||||
Reference in New Issue
Block a user