Files
ARCADIA-Studio/crates/oa-desktop/src/diagram/multiselect_tests.rs
T

759 lines
22 KiB
Rust

//! Production canvas event regressions for exact multi-selection and group gestures.
use super::{DiagramCanvas, State};
use crate::app::{
Message,
multiselect::{Choice, Event as SelectionEvent, context},
};
use crate::diagram_selection::Selection;
use crate::diagram_usage::Target;
use iced::widget::canvas::{self, Program};
use iced::{Point, Rectangle, Size, Vector, event::Status, keyboard, mouse};
use oa_core::*;
fn position(x: f32, y: f32) -> Position {
Position { x, y }
}
fn bounds() -> Rectangle {
Rectangle::new(Point::new(260., 100.), Size::new(2400., 1800.))
}
struct Fixture {
project: Project,
diagram: Id,
a: Id,
b: Id,
child: Id,
internal: Id,
context: crate::route_edit::Context,
}
impl Fixture {
fn new() -> Self {
let mut project = Project::empty();
let a = project
.add_element(Kind::Activity, Layer::Oa, "A", None)
.unwrap();
let b = project
.add_element(Kind::Activity, Layer::Oa, "B", None)
.unwrap();
let c = project
.add_element(Kind::Activity, Layer::Oa, "C", None)
.unwrap();
let child = project
.add_element(Kind::Activity, Layer::Oa, "Child", Some(a))
.unwrap();
let internal = project.relate(RelationKind::Interaction, "Internal", a, b);
let external = project.relate(RelationKind::Interaction, "External", b, c);
let diagram = project.ensure_diagram(Layer::Oa);
project.diagrams.get_mut(&diagram).unwrap().projection = ProjectionKind::Overview;
let a = project.place(diagram, a, position(100., 200.));
let b = project.place(diagram, b, position(500., 200.));
let child = project.place(diagram, child, position(120., 280.));
project.place(diagram, c, position(900., 200.));
let view = project.diagrams.get_mut(&diagram).unwrap();
view.nodes.get_mut(&a).unwrap().collapsed = true;
view.routes = [
(internal, position(400., 320.)),
(external, position(800., 340.)),
]
.into_iter()
.map(|(edge, point)| EdgeRoute {
edge,
source_attachment: None,
target_attachment: None,
waypoints: vec![point],
label: Some(EdgeLabel {
fraction: 0.4,
offset: position(3., -9.),
}),
})
.collect();
validate_candidate(&project).unwrap();
Self {
project,
diagram,
a,
b,
child,
internal,
context: crate::route_edit::Context {
document: Id::new_v4(),
diagram,
revision: 7,
epoch: 11,
},
}
}
fn selection(&self, targets: impl IntoIterator<Item = Target>) -> Selection {
let mut selection = Selection::new(context(self.context).scope);
for target in targets {
assert!(selection.add(target, |_| true));
}
selection
}
fn pair(&self) -> Selection {
self.selection([Target::Placement(self.a), Target::Placement(self.b)])
}
fn canvas<'a>(&'a self, selection: &'a Selection, zoom: f32) -> DiagramCanvas<'a> {
let selected = match selection.primary() {
Some(Target::Placement(id)) => self.project.diagrams[&self.diagram].nodes[id].element,
Some(Target::Edge(id)) => *id,
Some(Target::Port { port, .. }) => *port,
_ => self.diagram,
};
DiagramCanvas {
selection: Some(selection),
project: &self.project,
view_index: None,
diagram: self.diagram,
selected,
navigation_focus: None,
zoom,
pan: Vector::new(20., 30.),
context: self.context,
selected_bend: None,
selected_attachment: None,
editable: true,
placing: false,
connection: None,
allocation: None,
}
}
fn screen(&self, placement: Id, zoom: f32) -> Point {
let p = self.project.diagrams[&self.diagram].nodes[&placement].position;
Point::new(
bounds().x + (p.x + 30.) * zoom + 20.,
bounds().y + (p.y + 25.) * zoom + 30.,
)
}
fn expected_move(&self) -> Diagram {
// Independent literal expectations: the folded child moves with A,
// only the internal edge's authored world pin translates.
let mut expected = self.project.diagrams[&self.diagram].clone();
expected.nodes.get_mut(&self.a).unwrap().position = position(140., 260.);
expected.nodes.get_mut(&self.b).unwrap().position = position(540., 260.);
expected.nodes.get_mut(&self.child).unwrap().position = position(160., 340.);
expected.routes[0].waypoints = vec![position(440., 380.)];
expected
}
}
fn input(
c: &DiagramCanvas<'_>,
state: &mut State,
event: canvas::Event,
viewport: Rectangle,
final_cursor: Point,
) -> (Option<Message>, Status) {
c.update(
state,
&event,
viewport,
mouse::Cursor::Available(final_cursor),
)
.map_or((None, Status::Ignored), |action| {
let (message, _, status) = action.into_inner();
(message, status)
})
}
fn pointer(
c: &DiagramCanvas<'_>,
state: &mut State,
event: mouse::Event,
final_cursor: Point,
) -> (Option<Message>, Status) {
input(
c,
state,
canvas::Event::Mouse(event),
bounds(),
final_cursor,
)
}
fn start(c: &DiagramCanvas<'_>, state: &mut State, press: Point, final_cursor: Point) {
assert!(
pointer(
c,
state,
mouse::Event::CursorMoved { position: press },
final_cursor
)
.0
.is_none()
);
let (message, status) = pointer(
c,
state,
mouse::Event::ButtonPressed(mouse::Button::Left),
final_cursor,
);
assert!(message.is_none());
assert_eq!(status, Status::Captured);
assert!(state.group.is_some());
assert!(state.drag.is_none());
}
fn key(named: keyboard::key::Named) -> canvas::Event {
canvas::Event::Keyboard(keyboard::Event::KeyPressed {
key: keyboard::Key::Named(named),
modified_key: keyboard::Key::Named(named),
physical_key: keyboard::key::Physical::Code(if named == keyboard::key::Named::Escape {
keyboard::key::Code::Escape
} else {
keyboard::key::Code::Delete
}),
location: keyboard::Location::Standard,
modifiers: keyboard::Modifiers::empty(),
text: None,
repeat: false,
})
}
fn expect_move(
message: Option<Message>,
selection: &Selection,
current: crate::route_edit::Context,
expected: Position,
) -> crate::diagram_selection::Snapshot {
let Some(Message::DiagramSelection(SelectionEvent::Move(snapshot, delta))) = message else {
panic!("expected one captured group Move");
};
assert_eq!(delta, expected);
assert!(selection.matches(&snapshot, context(current)));
snapshot
}
#[test]
fn shift_press_uses_ordered_hit_not_native_batch_final_cursor() {
let f = Fixture::new();
let selection = f.selection([Target::Placement(f.b)]);
let before = selection.snapshot(context(f.context)).unwrap();
let c = f.canvas(&selection, 1.);
let mut state = State::default();
let at_a = f.screen(f.a, 1.);
let final_cursor = f.screen(f.b, 1.);
assert!(
input(
&c,
&mut state,
canvas::Event::Keyboard(keyboard::Event::ModifiersChanged(
keyboard::Modifiers::SHIFT
)),
bounds(),
final_cursor
)
.0
.is_none()
);
assert!(
pointer(
&c,
&mut state,
mouse::Event::CursorMoved { position: at_a },
final_cursor
)
.0
.is_none()
);
let (message, status) = pointer(
&c,
&mut state,
mouse::Event::ButtonPressed(mouse::Button::Left),
final_cursor,
);
assert_eq!(status, Status::Captured);
assert!(
matches!(message, Some(Message::DiagramSelection(SelectionEvent::Choose(captured, Target::Placement(id), Choice::Toggle))) if captured == context(f.context) && id == f.a)
);
assert!(
state.group.is_none()
&& state.drag.is_none()
&& state.route_drag.is_none()
&& state.port_drag.is_none()
);
assert!(
pointer(
&c,
&mut state,
mouse::Event::ButtonReleased(mouse::Button::Left),
final_cursor
)
.0
.is_none()
);
assert!(
selection.matches(&before, context(f.context)),
"canvas emits intent without mutating selection"
);
}
#[test]
fn group_canvas_preview_and_release_match_independent_expected_geometry() {
for zoom in [0.5, 1., 2.] {
let f = Fixture::new();
let selection = f.pair();
let captured = selection.snapshot(context(f.context)).unwrap();
let c = f.canvas(&selection, zoom);
let mut state = State::default();
let press = f.screen(f.a, zoom);
let end = press + Vector::new(40. * zoom, 60. * zoom);
start(&c, &mut state, press, end);
assert_eq!(c.preview_diagram(&state), f.project.diagrams[&f.diagram]);
let (message, status) = pointer(
&c,
&mut state,
mouse::Event::CursorMoved { position: end },
end,
);
assert!(message.is_none());
assert_eq!(status, Status::Captured);
assert_eq!(c.preview_diagram(&state), f.expected_move());
let (message, status) = pointer(
&c,
&mut state,
mouse::Event::ButtonReleased(mouse::Button::Left),
end,
);
assert_eq!(status, Status::Captured);
let snapshot = expect_move(message, &selection, f.context, position(40., 60.));
assert_eq!(snapshot, captured);
assert!(state.group.is_none());
let ids = crate::app::multiselect::placements(&snapshot).unwrap();
let command =
diagram_edit::translate_placements(&f.project, f.diagram, &ids, position(40., 60.))
.unwrap();
let actual = prepare_command(&f.project, &command).unwrap().project;
let mut expected = f.project.clone();
expected.diagrams.insert(f.diagram, f.expected_move());
assert_eq!(actual, expected);
assert!(selection.matches(&captured, context(f.context)));
}
}
#[test]
fn stationary_group_click_does_not_use_motion_after_release_in_same_batch() {
let f = Fixture::new();
let selection = f.pair();
let c = f.canvas(&selection, 1.);
let mut state = State::default();
let press = f.screen(f.a, 1.);
let end = press + Vector::new(80., 70.);
start(&c, &mut state, press, end);
let (message, _) = pointer(
&c,
&mut state,
mouse::Event::ButtonReleased(mouse::Button::Left),
end,
);
assert!(
matches!(message, Some(Message::DiagramSelection(SelectionEvent::Collapse(captured, Target::Placement(id)))) if selection.matches(&captured, context(f.context)) && id == f.a)
);
assert!(
pointer(
&c,
&mut state,
mouse::Event::CursorMoved { position: end },
end
)
.0
.is_none()
);
assert_eq!(c.preview_diagram(&state), f.project.diagrams[&f.diagram]);
}
#[test]
fn mixed_set_keeps_click_lifecycle_and_refuses_only_actual_drag() {
for moved in [false, true] {
let f = Fixture::new();
let selection = f.selection([Target::Placement(f.a), Target::Edge(f.internal)]);
let captured = selection.snapshot(context(f.context)).unwrap();
let c = f.canvas(&selection, 1.);
let mut state = State::default();
let press = f.screen(f.a, 1.);
let end = if moved {
press + Vector::new(40., 60.)
} else {
press + Vector::new(2., 1.)
};
start(&c, &mut state, press, end);
assert!(
pointer(
&c,
&mut state,
mouse::Event::CursorMoved { position: end },
end
)
.0
.is_none()
);
assert_eq!(c.preview_diagram(&state), f.project.diagrams[&f.diagram]);
let (message, status) = pointer(
&c,
&mut state,
mouse::Event::ButtonReleased(mouse::Button::Left),
end,
);
assert_eq!(status, Status::Captured);
if moved {
assert!(
matches!(message, Some(Message::DiagramSelection(SelectionEvent::Refuse(snapshot, reason))) if snapshot == captured && reason.contains("only placed elements"))
);
} else {
assert!(
matches!(message, Some(Message::DiagramSelection(SelectionEvent::Collapse(current, Target::Placement(id)))) if current == captured && id == f.a)
);
}
assert!(state.group.is_none());
assert!(selection.matches(&captured, context(f.context)));
}
}
#[test]
fn escape_cancels_live_group_without_clearing_or_publishing_a_model_intent() {
let f = Fixture::new();
let selection = f.pair();
let captured = selection.snapshot(context(f.context)).unwrap();
let c = f.canvas(&selection, 1.);
let mut state = State::default();
let press = f.screen(f.a, 1.);
let end = press + Vector::new(40., 60.);
start(&c, &mut state, press, end);
assert!(
pointer(
&c,
&mut state,
mouse::Event::CursorMoved { position: end },
end
)
.0
.is_none()
);
assert_eq!(c.preview_diagram(&state), f.expected_move());
let (message, status) = input(
&c,
&mut state,
key(keyboard::key::Named::Escape),
bounds(),
end,
);
assert!(message.is_none());
assert_eq!(status, Status::Captured);
assert!(state.group.is_none());
assert_eq!(c.preview_diagram(&state), f.project.diagrams[&f.diagram]);
assert!(
pointer(
&c,
&mut state,
mouse::Event::ButtonReleased(mouse::Button::Left),
end
)
.0
.is_none()
);
assert!(selection.matches(&captured, context(f.context)));
// A subsequent Escape, after the gesture is idle, may clear selection.
assert!(matches!(
input(
&c,
&mut state,
key(keyboard::key::Named::Escape),
bounds(),
end
)
.0,
Some(Message::DiagramSelection(SelectionEvent::Clear(_)))
));
}
#[test]
fn delete_is_captured_without_hiding_during_live_group_then_release_moves_once() {
let f = Fixture::new();
let selection = f.pair();
let c = f.canvas(&selection, 1.);
let mut state = State::default();
let press = f.screen(f.a, 1.);
let end = press + Vector::new(40., 60.);
start(&c, &mut state, press, end);
assert!(
pointer(
&c,
&mut state,
mouse::Event::CursorMoved { position: end },
end
)
.0
.is_none()
);
let (message, status) = input(
&c,
&mut state,
key(keyboard::key::Named::Delete),
bounds(),
end,
);
assert!(message.is_none());
assert_eq!(status, Status::Captured);
assert!(state.group.is_some());
assert_eq!(c.preview_diagram(&state), f.expected_move());
expect_move(
pointer(
&c,
&mut state,
mouse::Event::ButtonReleased(mouse::Button::Left),
end,
)
.0,
&selection,
f.context,
position(40., 60.),
);
}
#[test]
fn middle_pan_excludes_group_start_and_delete_even_when_left_is_pressed() {
let f = Fixture::new();
let selection = f.pair();
let captured = selection.snapshot(context(f.context)).unwrap();
let c = f.canvas(&selection, 1.);
let mut state = State::default();
let press = f.screen(f.a, 1.);
let end = press + Vector::new(40., 60.);
assert!(
pointer(
&c,
&mut state,
mouse::Event::CursorMoved { position: press },
end
)
.0
.is_none()
);
assert!(
pointer(
&c,
&mut state,
mouse::Event::ButtonPressed(mouse::Button::Middle),
end
)
.0
.is_none()
);
let (message, status) = pointer(
&c,
&mut state,
mouse::Event::ButtonPressed(mouse::Button::Left),
end,
);
assert!(message.is_none());
assert_eq!(status, Status::Captured);
assert!(state.group.is_none() && state.drag.is_none());
assert!(
input(
&c,
&mut state,
key(keyboard::key::Named::Delete),
bounds(),
end
)
.0
.is_none()
);
assert!(
matches!(pointer(&c, &mut state, mouse::Event::CursorMoved { position: end }, end).0, Some(Message::Pan(delta)) if delta == Vector::new(40., 60.))
);
assert!(
pointer(
&c,
&mut state,
mouse::Event::ButtonReleased(mouse::Button::Left),
end
)
.0
.is_none()
);
assert!(
pointer(
&c,
&mut state,
mouse::Event::ButtonReleased(mouse::Button::Middle),
end
)
.0
.is_none()
);
assert_eq!(c.preview_diagram(&state), f.project.diagrams[&f.diagram]);
assert!(selection.matches(&captured, context(f.context)));
}
#[test]
fn changed_viewport_or_context_retires_group_without_a_release_intent() {
for change in 0..7 {
let f = Fixture::new();
let selection = f.pair();
let c = f.canvas(&selection, 1.);
let mut state = State::default();
let press = f.screen(f.a, 1.);
let end = press + Vector::new(40., 60.);
start(&c, &mut state, press, end);
assert!(
pointer(
&c,
&mut state,
mouse::Event::CursorMoved { position: end },
end
)
.0
.is_none()
);
let mut changed = c;
let mut viewport = bounds();
match change {
0 => viewport.x += 1.,
1 => viewport.width -= 1.,
2 => changed.pan.x += 1.,
3 => changed.zoom = 2.,
4 => changed.context.epoch += 1,
5 => changed.context.revision += 1,
_ => changed.context.document = Id::new_v4(),
}
let (message, _) = input(
&changed,
&mut state,
canvas::Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)),
viewport,
end,
);
assert!(
message.is_none(),
"changed viewport/context {change} must not publish a commit or click"
);
assert!(state.group.is_none());
assert_eq!(
changed.preview_diagram(&state),
f.project.diagrams[&f.diagram]
);
}
}
#[test]
fn changed_selection_generation_rejects_press_even_after_membership_is_restored() {
let f = Fixture::new();
let mut selection = f.pair();
let press = f.screen(f.a, 1.);
let end = press + Vector::new(40., 60.);
let mut state = State::default();
{
let c = f.canvas(&selection, 1.);
start(&c, &mut state, press, end);
assert!(
pointer(
&c,
&mut state,
mouse::Event::CursorMoved { position: end },
end
)
.0
.is_none()
);
}
assert!(selection.remove(&Target::Placement(f.b)));
assert!(selection.add(Target::Placement(f.b), |_| true));
let c = f.canvas(&selection, 1.);
let (message, status) = pointer(
&c,
&mut state,
mouse::Event::ButtonReleased(mouse::Button::Left),
end,
);
assert!(message.is_none());
assert_eq!(status, Status::Captured);
assert!(state.group.is_none());
assert_eq!(c.preview_diagram(&state), f.project.diagrams[&f.diagram]);
}
#[test]
fn initial_press_without_pointer_history_consumes_release_only_displacement() {
let f = Fixture::new();
let selection = f.pair();
let c = f.canvas(&selection, 1.);
let mut state = State::default();
let press = f.screen(f.a, 1.);
let end = press + Vector::new(40., 60.);
// With no earlier ordered move, the supplied release cursor is authoritative.
assert!(
pointer(
&c,
&mut state,
mouse::Event::ButtonPressed(mouse::Button::Left),
press
)
.0
.is_none()
);
assert!(state.group.is_some());
expect_move(
pointer(
&c,
&mut state,
mouse::Event::ButtonReleased(mouse::Button::Left),
end,
)
.0,
&selection,
f.context,
position(40., 60.),
);
}
#[test]
fn threshold_crossing_then_return_to_origin_emits_zero_move_not_singleton_click() {
let f = Fixture::new();
let selection = f.pair();
let c = f.canvas(&selection, 1.);
let mut state = State::default();
let press = f.screen(f.a, 1.);
let end = press + Vector::new(40., 60.);
start(&c, &mut state, press, end);
assert!(
pointer(
&c,
&mut state,
mouse::Event::CursorMoved { position: end },
end
)
.0
.is_none()
);
assert!(
pointer(
&c,
&mut state,
mouse::Event::CursorMoved { position: press },
end
)
.0
.is_none()
);
assert_eq!(c.preview_diagram(&state), f.project.diagrams[&f.diagram]);
expect_move(
pointer(
&c,
&mut state,
mouse::Event::ButtonReleased(mouse::Button::Left),
end,
)
.0,
&selection,
f.context,
position(0., 0.),
);
assert_eq!(selection.targets().len(), 2);
}