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.
|
/// (12 px) + 4 px breathing room.
|
||||||
const KEYBIND_FOOTER_HEIGHT: f32 = 16.0;
|
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
|
/// How long a held arrow key waits before firing the next repeat
|
||||||
/// step. 100 ms = 10 steps/sec — fast enough to scrub through a
|
/// step. 100 ms = 10 steps/sec — fast enough to scrub through a
|
||||||
/// hundred-move replay in ~10 seconds while held, slow enough that
|
/// hundred-move replay in ~10 seconds while held, slow enough that
|
||||||
@@ -822,45 +837,63 @@ fn spawn_overlay(
|
|||||||
labels.iter().zip(positions.iter()).enumerate()
|
labels.iter().zip(positions.iter()).enumerate()
|
||||||
{
|
{
|
||||||
// Endpoints flush to the row's edges; middle
|
// Endpoints flush to the row's edges; middle
|
||||||
// three labels anchor at their percentage.
|
// three labels use the `translateX(-50%)`
|
||||||
// `i == 0` → flush left (`left: 0`), so the
|
// pattern for Bevy 0.18 UI: a fixed-width
|
||||||
// "0%" caption doesn't get clipped at the
|
// container is placed at `left: Percent(pct)`
|
||||||
// left edge. `i == last` → flush right
|
// then shifted left by half its own width via
|
||||||
// (`right: 0`) so "100%" doesn't overflow
|
// `margin.left: Px(-SCRUB_LABEL_CENTER_WIDTH/2)`.
|
||||||
// the banner. Bevy 0.18 UI has no clean
|
// `Justify::Center` renders the text centred
|
||||||
// CSS-style `translate-x: -50%` centering,
|
// within the container so the text's visual
|
||||||
// so the middle three labels sit slightly
|
// centre coincides with the notch line.
|
||||||
// right-of-notch — visually subtle at this
|
let (node, justify) = if i == 0 {
|
||||||
// font size; explicit polish target if
|
(
|
||||||
// anyone notices.
|
Node {
|
||||||
let mut node = Node {
|
position_type: PositionType::Absolute,
|
||||||
position_type: PositionType::Absolute,
|
top: Val::Px(2.0),
|
||||||
top: Val::Px(2.0),
|
left: Val::Px(0.0),
|
||||||
..default()
|
..default()
|
||||||
};
|
},
|
||||||
if i == 0 {
|
Justify::Left,
|
||||||
node.left = Val::Px(0.0);
|
)
|
||||||
} else if i == labels.len() - 1 {
|
} 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 {
|
} 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((
|
row.spawn((
|
||||||
ReplayOverlayScrubNotchLabel,
|
ReplayOverlayScrubNotchLabel,
|
||||||
node,
|
node,
|
||||||
Text::new(*label),
|
Text::new(*label),
|
||||||
|
TextLayout::new_with_justify(justify),
|
||||||
TextFont {
|
TextFont {
|
||||||
font: font_handle_for_labels.clone(),
|
font: font_handle_for_labels.clone(),
|
||||||
font_size: TYPE_CAPTION,
|
font_size: TYPE_CAPTION,
|
||||||
..default()
|
..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
|
// TEXT_SECONDARY keeps the subdued visual
|
||||||
// hierarchy (caption, not headline) while
|
// hierarchy (caption, not headline) while
|
||||||
// staying readable.
|
// staying readable against BG_ELEVATED_HI.
|
||||||
TextColor(TEXT_SECONDARY),
|
TextColor(TEXT_SECONDARY),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user