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:
@@ -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 {
|
||||
// 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()
|
||||
};
|
||||
if i == 0 {
|
||||
node.left = Val::Px(0.0);
|
||||
},
|
||||
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),
|
||||
));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user