fix(replay): centre scrub-bar notch labels on their notch ticks

The three middle scrub-bar labels (25%, 50%, 75%) previously had their
left edge anchored at the notch percentage, making them read as
"starting after" the notch. Apply the CSS translateX(-50%) pattern for
Bevy 0.18 UI: give each middle label a fixed-width container
(SCRUB_LABEL_CENTER_WIDTH = 36px), offset the container's left edge by
-width/2 via margin.left, and add Justify::Center so the text renders
centred within the container. The container's centre then coincides with
the notch line at the chosen percentage.

Endpoints (0%, 100%) keep their flush-left / flush-right anchoring
unchanged. 1275 tests pass / 0 failing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
funman300
2026-05-08 18:14:14 -07:00
parent 52407e7256
commit b44d2777ec
+59 -26
View File
@@ -105,6 +105,21 @@ const SCRUB_LABEL_ROW_HEIGHT: f32 = 16.0;
/// (12 px) + 4 px breathing room.
const KEYBIND_FOOTER_HEIGHT: f32 = 16.0;
/// Fixed pixel width of the centred scrub-bar notch-label container.
/// Wide enough to hold the widest label ("100%" at 4 chars) while
/// narrower than the 25 % gap between adjacent notches (≈ banner_w
/// × 0.25; on a 320 px banner that's 80 px). A 36 px container
/// leaves ≥ 44 px of clearance on each side at the narrowest common
/// screen width.
///
/// Container width drives the `margin.left = -width / 2` centering
/// trick: the container's left edge is placed at `left: Percent(pct)`
/// and then shifted left by half its own width, so the container's
/// centre coincides with the notch line. `Justify::Center` then
/// renders the text centred within the container. This is the
/// CSS `translateX(-50%)` pattern adapted for Bevy 0.18 UI.
const SCRUB_LABEL_CENTER_WIDTH: f32 = 36.0;
/// How long a held arrow key waits before firing the next repeat
/// step. 100 ms = 10 steps/sec — fast enough to scrub through a
/// hundred-move replay in ~10 seconds while held, slow enough that
@@ -822,45 +837,63 @@ fn spawn_overlay(
labels.iter().zip(positions.iter()).enumerate()
{
// Endpoints flush to the row's edges; middle
// three labels anchor at their percentage.
// `i == 0` → flush left (`left: 0`), so the
// "0%" caption doesn't get clipped at the
// left edge. `i == last` → flush right
// (`right: 0`) so "100%" doesn't overflow
// the banner. Bevy 0.18 UI has no clean
// CSS-style `translate-x: -50%` centering,
// so the middle three labels sit slightly
// right-of-notch — visually subtle at this
// font size; explicit polish target if
// anyone notices.
let mut node = Node {
position_type: PositionType::Absolute,
top: Val::Px(2.0),
..default()
};
if i == 0 {
node.left = Val::Px(0.0);
// three labels use the `translateX(-50%)`
// pattern for Bevy 0.18 UI: a fixed-width
// container is placed at `left: Percent(pct)`
// then shifted left by half its own width via
// `margin.left: Px(-SCRUB_LABEL_CENTER_WIDTH/2)`.
// `Justify::Center` renders the text centred
// within the container so the text's visual
// centre coincides with the notch line.
let (node, justify) = if i == 0 {
(
Node {
position_type: PositionType::Absolute,
top: Val::Px(2.0),
left: Val::Px(0.0),
..default()
},
Justify::Left,
)
} else if i == labels.len() - 1 {
node.right = Val::Px(0.0);
(
Node {
position_type: PositionType::Absolute,
top: Val::Px(2.0),
right: Val::Px(0.0),
..default()
},
Justify::Right,
)
} else {
node.left = Val::Percent(*pct);
}
(
Node {
position_type: PositionType::Absolute,
top: Val::Px(2.0),
left: Val::Percent(*pct),
width: Val::Px(SCRUB_LABEL_CENTER_WIDTH),
margin: UiRect {
left: Val::Px(-SCRUB_LABEL_CENTER_WIDTH / 2.0),
..default()
},
..default()
},
Justify::Center,
)
};
row.spawn((
ReplayOverlayScrubNotchLabel,
node,
Text::new(*label),
TextLayout::new_with_justify(justify),
TextFont {
font: font_handle_for_labels.clone(),
font_size: TYPE_CAPTION,
..default()
},
// The mockup's `text-outline` (BORDER_SUBTLE)
// would match the notches but reads as too
// low-contrast against `BG_ELEVATED_HI` for
// the labels to actually be legible at 12 px.
// TEXT_SECONDARY keeps the subdued visual
// hierarchy (caption, not headline) while
// staying readable.
// staying readable against BG_ELEVATED_HI.
TextColor(TEXT_SECONDARY),
));
}