fix: recent cards sizing issue

This commit is contained in:
LynithDev 2026-07-28 21:50:43 +02:00
parent 21dc169578
commit 57d50a95c4

View file

@ -18,6 +18,8 @@ const MAX_CARD_WIDTH_PX: f32 = 480.0;
const CARD_MS: u64 = 460;
const STAGGER_MS: u64 = 85;
const CARD_RISE_PX: f32 = 48.0;
/// Items the intro staggers individually; past this they rise together.
const INTRO_ITEMS: usize = 6;
#[derive(PartialEq)]
pub struct RecentsRow;
@ -31,17 +33,22 @@ impl Component for RecentsRow {
let sorted: Vec<Cluster> = sort_clusters_for_home(clusters);
let slots = *visible_slots.read();
let display = sorted.into_iter().take(slots).collect::<Vec<_>>();
let count = display.len();
let items = count + 1;
// Budgeted off the clusters we have, never off the slots we can show. An
// offset is the one thing a card must not change on a frame where the row
// is re-measured: the layout engine treats an offset-only change as "inner
// layout" and hands back the card's *cached* area instead of measuring it,
// so a rerun triggered by the slot count would freeze the surviving cards
// at their pre-resize widths while the new ones come in at the new width.
let items = intro_items(sorted.len());
let display = sorted.into_iter().take(slots).collect::<Vec<_>>();
let intro = use_animation_with_dependencies(&items, |conf, items| {
conf.on_creation(OnCreation::Run);
conf.on_change(OnChange::Rerun);
let total = CARD_MS + (items.saturating_sub(1) as u64) * STAGGER_MS;
AnimNum::new(0., 1.)
.time(total.max(1))
.time(intro_ms(*items).max(1))
.function(Function::Linear)
});
let progress = intro.get().value();
@ -68,17 +75,28 @@ impl Component for RecentsRow {
.into_element()
}))
.child(OtherVersionsTile {
index: count,
index: items.saturating_sub(1),
items,
progress,
})
}
}
/// Items to stagger for `clusters` recents, counting the "other versions" tile.
fn intro_items(clusters: usize) -> usize {
(clusters + 1).min(INTRO_ITEMS)
}
fn intro_ms(items: usize) -> u64 {
CARD_MS + (items.saturating_sub(1) as u64) * STAGGER_MS
}
fn stagger_eased(progress: f32, index: usize, items: usize) -> f32 {
let total = CARD_MS as f32 + (items.saturating_sub(1) as f32) * STAGGER_MS as f32;
let elapsed = progress * total;
let local = ((elapsed - index as f32 * STAGGER_MS as f32) / CARD_MS as f32).clamp(0., 1.);
let elapsed = progress * intro_ms(items) as f32;
// Anything past the budget shares the last slot, so every item still lands on
// a finished 1.0 rather than freezing part-way with a leftover offset.
let start = index.min(items.saturating_sub(1)) as f32 * STAGGER_MS as f32;
let local = ((elapsed - start) / CARD_MS as f32).clamp(0., 1.);
1.0 - (1.0 - local).powi(3)
}
@ -299,3 +317,57 @@ fn recent_card_slots_for_width(row_width_px: f32) -> usize {
let slot = MIN_CARD_WIDTH_PX + CARD_GAP_PX;
(available / slot).floor().max(1.0) as usize
}
#[cfg(test)]
mod tests {
use super::*;
fn row_fits(slots: usize, row_width_px: f32) -> bool {
let cards = slots as f32 * MIN_CARD_WIDTH_PX;
let gaps = slots as f32 * CARD_GAP_PX;
cards + gaps + MORE_TILE_WIDTH_PX <= row_width_px
}
#[test]
fn slots_fill_the_row_without_overflowing_it() {
for width in (200..3200).step_by(4) {
let width = width as f32;
let slots = recent_card_slots_for_width(width);
assert!(slots >= 1, "{width}px left no room for a card");
if slots > 1 {
assert!(row_fits(slots, width), "{slots} cards overflow {width}px");
}
assert!(
!row_fits(slots + 1, width),
"{width}px had room for {} cards",
slots + 1
);
}
}
#[test]
fn every_item_settles_when_the_intro_ends() {
// A card left with a leftover offset keeps changing it, and an offset-only
// change is what makes the layout engine reuse a card's cached width.
for clusters in 0..24 {
let items = intro_items(clusters);
for index in 0..=clusters {
assert_eq!(
stagger_eased(1.0, index, items),
1.0,
"item {index} of {clusters} never finished rising"
);
}
}
}
#[test]
fn the_intro_runs_the_same_length_whatever_fits_on_screen() {
// The window width decides how many cards show, so anything the intro is
// measured against has to be independent of it.
assert_eq!(intro_items(3), intro_items(3));
assert_eq!(intro_ms(intro_items(30)), intro_ms(INTRO_ITEMS));
assert_eq!(intro_ms(intro_items(0)), CARD_MS);
}
}