Keep allocation destinations readable without blocking collapsed header drops

This commit is contained in:
2026-09-12 04:34:02 -07:00
parent 76209cc186
commit 1b3c401a19
7 changed files with 416 additions and 157 deletions
@@ -911,7 +911,7 @@ fn toolbar_status_leaves_transformed_top_edge_work_header_readable() {
}
#[test]
fn toolbar_status_leaves_transformed_top_edge_frozen_target_label_readable() {
fn toolbar_destination_names_transformed_target_whose_header_is_clipped() {
for zoom in [0.75, 1.25] {
let mut f = fixture(Layer::Oa);
let mut project = f.app.project().clone();
@@ -940,11 +940,8 @@ fn toolbar_status_leaves_transformed_top_edge_frozen_target_label_readable() {
let feedback = f.app.allocation_drag.feedback.as_ref().unwrap();
assert!(!feedback.refused, "{}", feedback.text);
let target = &feedback.press.frozen.targets[feedback.targets[0]];
let top = screen(target.label.position).y - bounds().y;
assert!(
top > 0. && top < 14.,
"label starts in the formerly obscured band: {top}"
);
assert!(screen(xy(target.region.x, target.region.y)).y < bounds().y);
assert_eq!(feedback.destination_identity(), "A");
assert!(f.app.allocation_drag.preview.is_some());
let _ = f.app.update(release);
assert_eq!(
@@ -955,7 +952,8 @@ fn toolbar_status_leaves_transformed_top_edge_frozen_target_label_readable() {
}
#[test]
fn oa_sa_label_overlap_refuses_preview_release_and_retries_without_partial_source_or_history() {
fn oa_sa_clipped_selected_header_refuses_preview_release_and_retries_without_partial_source_or_history()
{
for layer in [Layer::Oa, Layer::Sa] {
for (zoom, pan) in [
(1., Vector::ZERO),
@@ -972,6 +970,7 @@ fn oa_sa_label_overlap_refuses_preview_release_and_retries_without_partial_sourc
"Temporary".into(),
)));
let _ = f.app.update(Message::Undo);
let pan = Vector::new(pan.x, pan.y - 100. * zoom);
f.app.zoom = zoom;
f.app.pan = pan;
f.app.fit_to_view = false;
@@ -982,7 +981,10 @@ fn oa_sa_label_overlap_refuses_preview_release_and_retries_without_partial_sourc
)
};
let from = screen(xy(140., 535.));
let overlap = screen(xy(160., 140.));
// Keep the pointer inside the visible target even when the camera
// clips its top. Only the grabbed work header must be clipped.
let overlap = screen(xy(160., 110_f32.max(-pan.y / zoom + 5.)));
assert!(bounds().contains(overlap));
let clear = screen(xy(160., 190.));
let accepted = f.app.project().clone();
let source = f.app.document.bundle().clone();
@@ -994,7 +996,7 @@ fn oa_sa_label_overlap_refuses_preview_release_and_retries_without_partial_sourc
assert!(f.app.allocation_drag.preview.is_none());
let feedback = f.app.allocation_drag.feedback.as_ref().unwrap();
assert!(feedback.refused, "{}", feedback.text);
assert!(feedback.text.contains("Aim below or beside"));
assert!(feedback.text.contains("Make room or adjust the view"));
assert_eq!(
feedback.press.frozen.targets[feedback.targets[0]].element,
f.a
@@ -1014,7 +1016,7 @@ fn oa_sa_label_overlap_refuses_preview_release_and_retries_without_partial_sourc
assert!(!f.app.document.can_undo());
assert!(f.app.document.can_redo());
// In a new gesture, moving out of the collision reuses its semantic
// In a new gesture, moving into the viewport reuses its semantic
// candidate and stable allocation identity; only geometry changes.
let mut events = batch(&f.app, from, overlap);
events.pop();
@@ -1074,3 +1076,115 @@ fn oa_sa_label_overlap_refuses_preview_release_and_retries_without_partial_sourc
}
}
}
#[test]
fn oa_sa_toolbar_identity_accepts_exact_receiver_and_collapsed_header_drag() {
for layer in [Layer::Oa, Layer::Sa] {
for (name, collapsed) in [("Receiver", false), ("Receiving subsystem", true)] {
for (zoom, pan) in [
(1., Vector::ZERO),
(0.75, Vector::new(37., -15.)),
(1.25, Vector::new(-30., 23.)),
(1., Vector::new(0., -100.)),
] {
let mut f = fixture(layer);
let mut project = f.app.project().clone();
project.elements.get_mut(&f.a).unwrap().name = name.into();
let target = project
.diagrams
.get_mut(&f.diagram)
.unwrap()
.nodes
.values_mut()
.find(|n| n.element == f.a)
.unwrap();
target.collapsed = collapsed;
let target_id = target.id;
replace(&mut f, project);
f.app.zoom = zoom;
f.app.pan = pan;
f.app.fit_to_view = false;
let screen = |world: Position| {
Point::new(
bounds().x + world.x * zoom + pan.x,
bounds().y + world.y * zoom + pan.y,
)
};
let accepted = f.app.project().clone();
let source = f.app.document.bundle().clone();
let snapshot = f.app.document.snapshot();
// Normal name grab (40,35), release inside the original header.
let mut events = batch(&f.app, screen(xy(140., 535.)), screen(xy(160., 140.)));
let release = events.pop().unwrap();
send(&mut f.app, events);
let feedback = f
.app
.allocation_drag
.feedback
.as_ref()
.expect(&f.app.status);
assert!(!feedback.refused, "{}", feedback.text);
assert_eq!(feedback.destination_identity(), name);
assert_eq!(
feedback.press.frozen.targets[feedback.targets[0]].placement,
target_id
);
let preview = f.app.allocation_drag.preview.as_ref().expect(&f.app.status);
assert_eq!(
preview.placement.diagram.nodes[&f.node].position,
xy(120., 110.)
);
assert!(!preview.placement.diagram.nodes[&target_id].collapsed);
assert_eq!(
preview.placement.view,
projection::project_diagram_with_index(
preview.semantic.project(),
&preview.placement.diagram,
preview.semantic.index()
)
);
let mut expected = preview.semantic.project().clone();
expected
.diagrams
.insert(f.diagram, preview.placement.diagram.clone());
assert_eq!(f.app.project(), &accepted);
assert_eq!(f.app.document.bundle(), &source);
assert!(Arc::ptr_eq(&snapshot, &f.app.document.snapshot()));
assert!(!f.app.document.can_undo());
let _ = f.app.update(release);
assert_eq!(f.app.project(), &expected, "{}", f.app.status);
assert_eq!(f.app.document.revision(), 1);
assert_eq!(
validation::effective_performer(f.app.project(), f.work),
Some(f.a)
);
let after = f.app.document.bundle().clone();
let _ = f.app.update(Message::Undo);
assert_eq!(f.app.project(), &accepted);
assert_eq!(f.app.document.bundle(), &source);
assert!(!f.app.document.can_undo());
let _ = f.app.update(Message::Redo);
assert_eq!(f.app.project(), &expected);
assert_eq!(f.app.document.bundle(), &after);
assert!(!f.app.document.can_redo());
}
}
}
}
#[test]
fn destination_identity_does_not_describe_blocked_or_outside_viewport_as_unallocation() {
let f = fixture(Layer::Oa);
let p = press(&f);
let mut feedback = Feedback {
press: p,
targets: vec![],
text: "A card blocks this drop".into(),
refused: true,
};
assert_eq!(feedback.destination_identity(), "No eligible destination");
feedback.text = "Release inside the captured canvas".into();
assert_eq!(feedback.destination_identity(), "No eligible destination");
feedback.refused = false;
assert_eq!(feedback.destination_identity(), "Outside performers");
}