Support atomic Allocation group assignment and Arrange gestures

This commit is contained in:
2026-09-23 20:25:26 -07:00
parent 16b30a6760
commit 495464e84b
14 changed files with 2928 additions and 220 deletions
@@ -8,6 +8,9 @@ use iced::{
#[path = "allocation_drag_full_tree_tests.rs"]
mod full_tree;
#[path = "allocation_drag_tests/group.rs"]
mod group;
#[path = "allocation_drag_tests/navigation_union.rs"]
mod navigation_union;
@@ -48,6 +51,7 @@ fn fixture(layer: Layer) -> Fixture {
app.zoom = 1.;
app.pan = Vector::ZERO;
app.fit_to_view = false;
app.reconcile_multiselect();
Fixture {
app,
a,
@@ -59,7 +63,7 @@ fn fixture(layer: Layer) -> Fixture {
}
fn canvas(app: &App) -> DiagramCanvas<'_> {
DiagramCanvas {
selection: None,
selection: app.diagram_selection.as_ref(),
allocation: Some(app.allocation_capability()),
project: app.project(),
view_index: Some(app.document.view_index()),
@@ -122,6 +126,18 @@ fn press(f: &Fixture) -> Arc<Press> {
})
.expect("actual clean work-body press")
}
fn admit_press(f: &mut Fixture) -> Arc<Press> {
let p = press(f);
let _ = f.app.update(Message::WorkDrag(Event::Press(p.clone())));
assert!(f.app.allocation_drag.active.is_some(), "{}", f.app.status);
p
}
fn singleton(candidate: &Candidate) -> &Arc<plan::SemanticPlan> {
let Candidate::Singleton(plan) = candidate else {
panic!("legacy singleton candidate")
};
plan
}
fn drag(f: &mut Fixture, to: Point) {
let events = batch(&f.app, start(f), to);
assert!(matches!(
@@ -359,7 +375,7 @@ fn arrange_is_one_gesture_and_snapped_noop_preserves_redo_and_source() {
assert!(f.app.allocation_drag.arrange_next);
let p = press(&f);
let _ = f.app.update(Message::WorkDrag(Event::Press(p.clone())));
assert!(p.arrange);
assert_eq!(p.mode, Mode::Arrange);
assert!(!f.app.allocation_drag.arrange_next);
let _ = f.app.update(Message::WorkDrag(Event::Release(
p.stamp,
@@ -370,7 +386,7 @@ fn arrange_is_one_gesture_and_snapped_noop_preserves_redo_and_source() {
assert_eq!(f.app.document.revision(), revision);
assert!(f.app.document.can_redo());
assert!(f.app.project().relations.is_empty());
assert!(!press(&f).arrange);
assert_eq!(press(&f).mode, Mode::Assign);
}
#[test]
@@ -397,14 +413,15 @@ fn candidate_semantics_are_cached_per_destination_with_stable_ids_and_own_index(
let _ = f
.app
.update(Message::WorkDrag(Event::Preview(p.stamp, p.token, Some(a))));
let first = f
.app
.allocation_drag
.preview
.as_ref()
.expect(&f.app.status)
.semantic
.clone();
let first = singleton(
&f.app
.allocation_drag
.preview
.as_ref()
.expect(&f.app.status)
.candidate,
)
.clone();
let stable = first
.project()
.relations
@@ -419,20 +436,20 @@ fn candidate_semantics_are_cached_per_destination_with_stable_ids_and_own_index(
)));
assert!(Arc::ptr_eq(
&first,
&f.app.allocation_drag.preview.as_ref().unwrap().semantic
singleton(&f.app.allocation_drag.preview.as_ref().unwrap().candidate)
));
let _ = f
.app
.update(Message::WorkDrag(Event::Preview(p.stamp, p.token, Some(b))));
let preview = f.app.allocation_drag.preview.as_ref().expect(&f.app.status);
assert!(!Arc::ptr_eq(&first, &preview.semantic));
assert_eq!(preview.semantic.project().relations[&stable].source(), f.b);
assert!(!Arc::ptr_eq(&first, singleton(&preview.candidate)));
assert_eq!(preview.project().relations[&stable].source(), f.b);
assert_eq!(
preview.placement.view,
projection::project_diagram_with_index(
preview.semantic.project(),
preview.project(),
&preview.placement.diagram,
preview.semantic.index()
preview.index()
)
);
assert_ne!(
@@ -448,7 +465,7 @@ fn candidate_semantics_are_cached_per_destination_with_stable_ids_and_own_index(
.update(Message::WorkDrag(Event::Preview(p.stamp, p.token, Some(a))));
assert!(Arc::ptr_eq(
&first,
&f.app.allocation_drag.preview.as_ref().unwrap().semantic
singleton(&f.app.allocation_drag.preview.as_ref().unwrap().candidate)
));
assert_eq!(
f.app.allocation_drag.active.as_ref().unwrap().cache.len(),
@@ -458,9 +475,19 @@ fn candidate_semantics_are_cached_per_destination_with_stable_ids_and_own_index(
#[test]
fn frozen_targets_use_half_open_semantic_priority_and_refuse_incomparable_overlap() {
let f = fixture(Layer::Oa);
let p = press(&f);
let mut frozen = p.frozen.clone();
let mut f = fixture(Layer::Oa);
let p = admit_press(&mut f);
let mut frozen = f
.app
.allocation_drag
.active
.as_ref()
.unwrap()
.frozen
.as_ref()
.unwrap()
.as_ref()
.clone();
let ai = frozen
.targets
.iter()
@@ -540,6 +567,7 @@ fn release_uses_ordered_native_position_before_later_motion_in_same_batch() {
fn replace(f: &mut Fixture, project: Project) {
f.app.document = crate::document::Document::from_project(project, Default::default()).unwrap();
f.app.sync();
f.app.reconcile_multiselect();
}
#[test]
@@ -588,7 +616,7 @@ fn same_snapped_position_on_opposite_shared_border_rebuilds_semantics_even_if_la
.1
.as_ref()
.unwrap();
assert!(!Arc::ptr_eq(a, b));
assert!(!Arc::ptr_eq(singleton(a), singleton(b)));
let stable = a
.project()
.relations
@@ -655,13 +683,24 @@ fn mounted_role_only_outgoing_refusal_retains_placement_and_same_performer_creat
"{}",
f.app.status
);
let target = press(&f)
let admitted = admit_press(&mut f);
let target = f
.app
.allocation_drag
.active
.as_ref()
.unwrap()
.frozen
.as_ref()
.unwrap()
.targets
.iter()
.find(|t| t.element == f.a)
.unwrap()
.region;
let _ = f
.app
.update(Message::WorkDrag(Event::Cancel(admitted.token)));
drag(&mut f, screen(xy(target.x + 45., target.y + 100.)));
assert_eq!(f.app.document.revision(), 1, "{}", f.app.status);
assert_eq!(f.app.project().relations, relations);
@@ -774,8 +813,18 @@ fn frozen_collapsed_target_uses_only_visible_header_and_port_edge_controls_occlu
.unwrap()
.collapsed = true;
replace(&mut f, project);
let p = press(&f);
let mut frozen = p.frozen.clone();
let p = admit_press(&mut f);
let mut frozen = f
.app
.allocation_drag
.active
.as_ref()
.unwrap()
.frozen
.as_ref()
.unwrap()
.as_ref()
.clone();
let t = frozen.targets.iter().find(|t| t.element == f.a).unwrap();
let bottom = xy(t.region.x + 30., t.region.y + t.region.height);
assert!(t.region.height <= 62.);
@@ -945,7 +994,7 @@ fn toolbar_destination_names_transformed_target_whose_header_is_clipped() {
send(&mut f.app, events);
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 target = &feedback.frozen.as_ref().unwrap().targets[feedback.targets[0]];
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());
@@ -1004,7 +1053,7 @@ fn oa_sa_clipped_selected_header_refuses_preview_release_and_retries_without_par
assert!(feedback.refused, "{}", feedback.text);
assert!(feedback.text.contains("Make room or adjust the view"));
assert_eq!(
feedback.press.frozen.targets[feedback.targets[0]].element,
feedback.frozen.as_ref().unwrap().targets[feedback.targets[0]].element,
f.a
);
assert_eq!(f.app.project(), &accepted);
@@ -1029,14 +1078,14 @@ fn oa_sa_clipped_selected_header_refuses_preview_release_and_retries_without_par
send(&mut f.app, events);
let active = f.app.allocation_drag.active.as_ref().unwrap();
let p = active.press.clone();
let semantic = active.cache[0].1.as_ref().unwrap().clone();
let semantic = singleton(active.cache[0].1.as_ref().unwrap()).clone();
let _ = f.app.update(Message::WorkDrag(Event::Preview(
p.stamp,
p.token,
Some(clear),
)));
let preview = f.app.allocation_drag.preview.as_ref().expect(&f.app.status);
assert!(Arc::ptr_eq(&semantic, &preview.semantic));
assert!(Arc::ptr_eq(&semantic, singleton(&preview.candidate)));
assert_eq!(
preview.placement.diagram.nodes[&f.node].position,
xy(120., 160.)
@@ -1132,7 +1181,7 @@ fn oa_sa_toolbar_identity_accepts_exact_receiver_and_collapsed_header_drag() {
assert!(!feedback.refused, "{}", feedback.text);
assert_eq!(feedback.destination_identity(), name);
assert_eq!(
feedback.press.frozen.targets[feedback.targets[0]].placement,
feedback.frozen.as_ref().unwrap().targets[feedback.targets[0]].placement,
target_id
);
let preview = f.app.allocation_drag.preview.as_ref().expect(&f.app.status);
@@ -1144,12 +1193,12 @@ fn oa_sa_toolbar_identity_accepts_exact_receiver_and_collapsed_header_drag() {
assert_eq!(
preview.placement.view,
projection::project_diagram_with_index(
preview.semantic.project(),
preview.project(),
&preview.placement.diagram,
preview.semantic.index()
preview.index()
)
);
let mut expected = preview.semantic.project().clone();
let mut expected = preview.project().clone();
expected
.diagrams
.insert(f.diagram, preview.placement.diagram.clone());
@@ -1184,6 +1233,8 @@ fn destination_identity_does_not_describe_blocked_or_outside_viewport_as_unalloc
let p = press(&f);
let mut feedback = Feedback {
press: p,
frozen: None,
ordinary_performers: false,
targets: vec![],
inspection_targets: vec![],
text: "A card blocks this drop".into(),