fix(variables): clamp variant-header hover wash to its hit column

A long variant name could make the fit-content variant-header hover wash
wider than its column and bleed into the neighbouring variant column. Clamp
the wash right edge to the hit column (`col_w.min(176)`), matching the
size-checkbox / footer clamps. Wash regression tests moved to a sibling
variables_panel/wash_tests.rs (keeps tests.rs under the 800-line cap) with a
new long-variant clamp case.
This commit is contained in:
Kayshen-X 2026-06-20 23:35:22 +08:00
parent d41cf9c61f
commit 88cc8e94d8
4 changed files with 149 additions and 88 deletions

View file

@ -776,3 +776,5 @@ impl Widget for VariablesPanel {
#[cfg(test)]
mod tests;
#[cfg(test)]
mod wash_tests;

View file

@ -279,13 +279,16 @@ fn paint_variant_header(
let variant_width = cx.backend.measure_text(variant, 13.0);
// Hover/press wash hugs `value v` (+ small L/R padding) instead of
// washing the whole value-column cell. The hit target stays the
// wider `variant_header_rect` so the trigger is still easy to click.
// wider `variant_header_rect` so the trigger is still easy to click,
// and the wash is clamped to that hit column so a long variant name
// can't bleed into the neighbouring variant column.
let wash_left = x - HOVER_WASH_PAD_X;
let cell_right = wash_left + col_w.min(176.0);
let wash_right =
(wash_left + variant_width + 6.0 + 11.0 + HOVER_WASH_PAD_X * 2.0).min(cell_right);
let wash = Rect {
origin: Point2D::new(x - HOVER_WASH_PAD_X, header_bottom + 4.0),
size: Point2D::new(
variant_width + 6.0 + 11.0 + HOVER_WASH_PAD_X * 2.0,
30.0,
),
origin: Point2D::new(wash_left, header_bottom + 4.0),
size: Point2D::new((wash_right - wash_left).max(0.0), 30.0),
};
paint_feedback(panel, cx, VariablesPanelButton::VariantHeader(idx), wash, 8.0);
paint_text(

View file

@ -813,85 +813,6 @@ fn axis_chip_table_mirrors_active_theme_btree_order() {
assert_eq!(p.chips[1].axis, "z-axis");
}
// ── fit-content hover wash (#26 variant header + #3 add-variable footer) ─────
//
// Both the variant-header value (`Default v`) and the footer `+ <label> v`
// button sit inside a much wider hit target, but their hover highlight should
// hug just the visible content (+ small L/R padding) rather than washing the
// whole cell / footer width.
#[test]
fn variant_header_hover_wash_hugs_value_not_full_column() {
let mut s = EditorState::new();
s.editor_ui.variables_panel_hover = Some(VariablesPanelButton::VariantHeader(0));
let p = VariablesPanel::for_editor(&s);
let rect = Rect {
origin: Point2D::new(0.0, 0.0),
size: Point2D::new(VARIABLES_PANEL_WIDTH, 480.0),
};
let mut backend = TextCaptureBackend::default();
let mut cx = PaintCx {
backend: &mut backend,
};
p.paint(&mut cx, rect);
let theme = p.theme;
let wash = backend
.round_fills
.iter()
.find(|(_, _, color)| color_eq(*color, theme.button_hover))
.map(|(r, _, _)| *r)
.expect("hovered variant header should paint a button_hover wash");
// "Default" ≈ 7 ASCII chars @13px ≈ 50px → wash hugs value + chevron +
// padding (~80px), far under the >=156px value-column cell.
assert!(
wash.size.x < 110.0,
"variant-header wash should hug `value v`, got {}",
wash.size.x
);
}
#[test]
fn footer_add_variable_hover_wash_left_aligned_and_hugs_content() {
let mut s = EditorState::new();
// Short CJK label → fit-content pill is clearly narrower than the button.
s.editor_ui.locale = Locale::ZhCn;
s.editor_ui.variables_panel_hover = Some(VariablesPanelButton::AddVariable);
let p = VariablesPanel::for_editor(&s);
let rect = Rect {
origin: Point2D::new(0.0, 0.0),
size: Point2D::new(VARIABLES_PANEL_WIDTH, 480.0),
};
let mut backend = TextCaptureBackend::default();
let mut cx = PaintCx {
backend: &mut backend,
};
p.paint(&mut cx, rect);
let theme = p.theme;
let button = add_variable_rect(rect);
let wash = backend
.round_fills
.iter()
.find(|(_, _, color)| color_eq(*color, theme.button_hover))
.map(|(r, _, _)| *r)
.expect("hovered add-variable footer should paint a button_hover wash");
// Pill is flush with the footer's (and rows') left edge — no centering gap.
assert!(
(wash.origin.x - button.origin.x).abs() < 0.01,
"footer wash should be left-aligned with the button, got {} vs {}",
wash.origin.x,
button.origin.x
);
// …hugs the `+ label v` content, well under the full 164px button…
assert!(
wash.size.x < button.size.x - 20.0,
"footer wash should hug content, got {} vs button {}",
wash.size.x,
button.size.x
);
// …and stays inside the footer button bounds.
assert!(wash.origin.x + wash.size.x <= button.origin.x + button.size.x + 0.01);
}
// Fit-content hover-wash tests (#26 variant header + #3 add-variable footer)
// live in the sibling `variables_panel/wash_tests.rs` to keep this file under
// the 800-line cap.

View file

@ -0,0 +1,135 @@
//! Fit-content hover-wash tests for the VariablesPanel (#26 variant-header
//! value + #3 add-variable footer). Split into its own file so
//! `variables_panel/tests.rs` doesn't grow past the 800-line cap.
//!
//! Both the variant-header value (`Default v`) and the footer `+ <label> v`
//! button sit inside a much wider hit target, but their hover highlight should
//! hug just the visible content (+ small L/R padding) rather than washing the
//! whole cell / footer width — and must stay clamped to the hit rect so a long
//! localized value can't bleed into a neighbouring column.
use super::*;
use crate::widgets::PaintCx;
use crate::{Color, Point2D, Rect};
/// Captures `fill_round_rect` calls so the hover wash rect/colour can be
/// asserted without a real GPU.
#[derive(Default)]
struct RoundFillCapture {
round_fills: Vec<(Rect, f32, Color)>,
}
impl crate::RenderBackend for RoundFillCapture {
fn begin_frame(&mut self) {}
fn end_frame(&mut self) {}
fn fill_rect(&mut self, _: Rect, _: Color) {}
fn stroke_rect(&mut self, _: Rect, _: Color, _: f32) {}
fn draw_text(&mut self, _: &crate::TextLayout, _: Point2D) {}
fn clip_rect(&mut self, _: Rect) {}
fn stroke_line(&mut self, _: Point2D, _: Point2D, _: Color, _: f32) {}
fn fill_round_rect(&mut self, rect: Rect, radius: f32, color: Color) {
self.round_fills.push((rect, radius, color));
}
fn stroke_round_rect(&mut self, _: Rect, _: f32, _: Color, _: f32) {}
fn stroke_svg_path(&mut self, _: &str, _: Point2D, _: f32, _: Color, _: f32) {}
fn save(&mut self) {}
fn restore(&mut self) {}
fn translate(&mut self, _: Point2D) {}
fn resize(&mut self, _: u32, _: u32) {}
fn dpi_scale(&self) -> f32 {
1.0
}
}
fn color_eq(a: Color, b: Color) -> bool {
(a.r - b.r).abs() < 0.001
&& (a.g - b.g).abs() < 0.001
&& (a.b - b.b).abs() < 0.001
&& (a.a - b.a).abs() < 0.001
}
fn hover_wash(s: &EditorState) -> Option<Rect> {
let p = VariablesPanel::for_editor(s);
let rect = Rect {
origin: Point2D::new(0.0, 0.0),
size: Point2D::new(VARIABLES_PANEL_WIDTH, 480.0),
};
let mut backend = RoundFillCapture::default();
let mut cx = PaintCx {
backend: &mut backend,
};
p.paint(&mut cx, rect);
let button_hover = p.theme.button_hover;
backend
.round_fills
.iter()
.find(|(_, _, color)| color_eq(*color, button_hover))
.map(|(r, _, _)| *r)
}
#[test]
fn variant_header_hover_wash_hugs_value_not_full_column() {
let mut s = EditorState::new();
s.editor_ui.variables_panel_hover = Some(VariablesPanelButton::VariantHeader(0));
let wash = hover_wash(&s).expect("hovered variant header should paint a button_hover wash");
// "Default" ≈ 7 ASCII chars @13px ≈ 50px → wash hugs value + chevron +
// padding (~80px), far under the >=156px value-column cell.
assert!(
wash.size.x < 110.0,
"variant-header wash should hug `value v`, got {}",
wash.size.x
);
}
#[test]
fn variant_header_hover_wash_clamped_to_column_for_long_variant() {
// A long variant name must clamp the wash to its hit column (`col_w.min(176)`)
// rather than overflowing into the neighbouring variant column.
let mut s = EditorState::new();
let long = "a-very-long-variant-name-that-would-overflow-the-column";
s.doc
.themes
.get_or_insert_with(Default::default)
.insert("Theme-1".into(), vec![long.to_string()]);
s.editor_ui.variables_panel_hover = Some(VariablesPanelButton::VariantHeader(0));
let wash = hover_wash(&s).expect("hovered variant header should paint a button_hover wash");
// Unclamped this would be ~315px (54 ASCII chars). The hit column is
// `col_w.min(176)` (>= the 156px floor), so a clamped long-variant wash
// lands in [156, 176] — a short "Default" fallback (~79px, if the long
// variant failed to render) would fall below this range.
assert!(
(150.0..=177.0).contains(&wash.size.x),
"long-variant wash should clamp to its [156,176]px column, got {}",
wash.size.x
);
}
#[test]
fn footer_add_variable_hover_wash_left_aligned_and_hugs_content() {
let mut s = EditorState::new();
// Short CJK label → fit-content pill is clearly narrower than the button.
s.editor_ui.locale = Locale::ZhCn;
s.editor_ui.variables_panel_hover = Some(VariablesPanelButton::AddVariable);
let rect = Rect {
origin: Point2D::new(0.0, 0.0),
size: Point2D::new(VARIABLES_PANEL_WIDTH, 480.0),
};
let button = add_variable_rect(rect);
let wash = hover_wash(&s).expect("hovered add-variable footer should paint a button_hover wash");
// Pill is flush with the footer's (and rows') left edge — no centering gap.
assert!(
(wash.origin.x - button.origin.x).abs() < 0.01,
"footer wash should be left-aligned with the button, got {} vs {}",
wash.origin.x,
button.origin.x
);
// …hugs the `+ label v` content, well under the full 164px button…
assert!(
wash.size.x < button.size.x - 20.0,
"footer wash should hug content, got {} vs button {}",
wash.size.x,
button.size.x
);
// …and stays inside the footer button bounds.
assert!(wash.origin.x + wash.size.x <= button.origin.x + button.size.x + 0.01);
}