Keep activation borders above message label backplates

This commit is contained in:
2026-09-12 16:00:25 -07:00
parent 419c78c071
commit d61f1f3877
2 changed files with 124 additions and 16 deletions
+31 -16
View File
@@ -1621,17 +1621,9 @@ impl Scene {
.iter()
.find(|line| {
!line.label.is_empty()
&& (Rectangle::new(
line.label_at,
Size::new(
(line.label.chars().count() as f32 * 6.8 + 8.).min(225.),
25.,
),
)
.contains(point)
|| line.points.windows(2).any(|p| {
crate::diagram::distance_to_segment(point, p[0], p[1]) < 1.5
}))
&& line.points.windows(2).any(|p| {
crate::diagram::distance_to_segment(point, p[0], p[1]) < 1.5
})
})
.map(|line| line.id)
})
@@ -1642,6 +1634,22 @@ impl Scene {
.find(|b| b.rect.contains(point))
.map(|b| b.call)
})
.or_else(|| {
self.lines
.iter()
.find(|line| {
!line.label.is_empty()
&& Rectangle::new(
line.label_at,
Size::new(
(line.label.chars().count() as f32 * 6.8 + 8.).min(225.),
25.,
),
)
.contains(point)
})
.map(|line| line.id)
})
// The generous message tolerance must not steal the exposed
// shoulder of another call's execution when the view is zoomed out.
.or_else(|| {
@@ -1973,6 +1981,18 @@ impl canvas::Program<Message> for Scene {
.with_width(if guide.id == self.selected { 2.5 } else { 1.2 }),
);
}
// Label backplates clear guides, but must not erase execution borders
// or message geometry. Their text remains in the foreground below.
for line in self.lines.iter().filter(|line| !line.label.is_empty()) {
f.fill_rectangle(
line.label_at,
Size::new(
(line.label.chars().count() as f32 * 6.8 + 8.).min(225.),
22.,
),
Color::WHITE,
);
}
for activation in &self.activations {
let path = Path::rectangle(activation.rect.position(), activation.rect.size());
f.fill(&path, Color::from_rgb8(243, 248, 248));
@@ -2054,11 +2074,6 @@ impl canvas::Program<Message> for Scene {
}),
Stroke::default().with_color(color),
);
f.fill_rectangle(
l.label_at,
Size::new((l.label.chars().count() as f32 * 6.8 + 8.).min(225.), 22.),
Color::WHITE,
);
f.fill_text(Text {
content: short(&l.label, 32),
position: l.label_at,
@@ -118,6 +118,99 @@ fn scene(f: &Fixture) -> Scene {
fn bar(scene: &Scene, call: Id) -> &Segment {
scene.activations.iter().find(|a| a.call == call).unwrap()
}
fn long_labels(f: &mut Fixture) {
for occurrence in [f.call, f.inner, f.inner_reply, f.reply] {
f.project.elements.get_mut(&occurrence).unwrap().name =
"Pressure verification with a descriptive operation label".into();
}
}
#[test]
fn activation_shoulders_under_label_backplates_select_their_call() {
for layer in [Layer::Oa, Layer::Sa] {
let mut f = fixture(layer, true);
long_labels(&mut f);
let scene = scene(&f);
let outer = bar(&scene, f.call);
for occurrence in [f.inner, f.inner_reply, f.reply] {
let message = line(&scene, occurrence);
let shoulder = Point::new(outer.rect.x + 1., message.label_at.y + 11.);
assert!(outer.rect.contains(shoulder));
assert!(Rectangle::new(message.label_at, Size::new(225., 22.)).contains(shoulder));
assert!(message.label.chars().count() > 32);
assert!(message.points.windows(2).all(|p| {
crate::diagram::distance_to_segment(shoulder, p[0], p[1]) > 8. / scene.scale
}));
assert_eq!(scene.hit(shoulder), Some(f.call));
assert_eq!(
scene.hit(message.label_at + iced::Vector::new(4., 4.)),
Some(occurrence),
"Label text outside activations remains selectable"
);
}
for occurrence in [f.call, f.inner, f.inner_reply, f.reply] {
let message = line(&scene, occurrence);
for endpoint in [message.points[0], *message.points.last().unwrap()] {
assert_eq!(scene.hit(endpoint), Some(occurrence));
}
}
}
}
#[tokio::test]
async fn long_message_backplates_do_not_erase_selected_activation_border_pixels() {
use iced::advanced::renderer::Headless;
use iced_runtime::user_interface::{Cache, UserInterface};
let mut mounted = crate::behavior::canvas_workspace::tests::Mounted::unshown().await;
let mut f = fixture(Layer::Oa, true);
long_labels(&mut f);
let mut scene = scene(&f);
scene.selected = f.call;
let border_x = bar(&scene, f.call).rect.x;
let label_spans: Vec<_> = [f.inner, f.inner_reply, f.reply]
.into_iter()
.map(|id| {
let message = line(&scene, id);
assert!(
Rectangle::new(message.label_at, Size::new(225., 22.))
.contains(Point::new(border_x, message.label_at.y + 11.))
);
(message.label_at.y + 3.) as usize..(message.label_at.y + 20.) as usize
})
.collect();
let dimensions = Size::new(800., 700.);
let content: Element<'_, Message> = canvas::Canvas::new(scene)
.width(dimensions.width)
.height(dimensions.height)
.into();
let mut ui = UserInterface::build(content, dimensions, Cache::new(), &mut mounted.renderer);
ui.draw(
&mut mounted.renderer,
&style::theme(),
&iced::advanced::renderer::Style {
text_color: style::INK,
},
mouse::Cursor::Unavailable,
);
let pixels = Headless::screenshot(&mut mounted.renderer, Size::new(800, 700), 1., Color::WHITE);
let expected = [style::TEAL.r, style::TEAL.g, style::TEAL.b].map(|v| v * 255.);
for span in label_spans {
for y in span {
assert!(
((border_x as usize - 2)..=(border_x as usize + 2)).any(|x| {
let offset = (y * 800 + x) * 4;
pixels[offset..offset + 3]
.iter()
.zip(expected)
.all(|(actual, target)| (*actual as f32 - target).abs() < 30.)
}),
"Selected activation border was erased at y={y}"
);
}
}
}
fn line(scene: &Scene, occurrence: Id) -> &super::super::Line {
scene.lines.iter().find(|a| a.id == occurrence).unwrap()
}