Match generic State label input to foreground paint order
This commit is contained in:
@@ -35,6 +35,9 @@ mod lifecycle_tests;
|
||||
#[path = "diagram/port_hit_tests.rs"]
|
||||
mod port_hit_tests;
|
||||
#[cfg(test)]
|
||||
#[path = "diagram/state_label_input_tests.rs"]
|
||||
mod state_label_input_tests;
|
||||
#[cfg(test)]
|
||||
#[path = "diagram/view_options_tests.rs"]
|
||||
mod view_options_tests;
|
||||
|
||||
@@ -326,7 +329,12 @@ impl DiagramCanvas<'_> {
|
||||
request.bend = Some(index);
|
||||
return request;
|
||||
}
|
||||
if let Some(port) = self.hit_port(&scene.view, local) {
|
||||
if let Some(edge) = self.hit_state_label(&scene.routed, local) {
|
||||
request.object = edge;
|
||||
request.route_point =
|
||||
routing::project_onto_polyline(&scene.routed.edges[&edge].points, position(world))
|
||||
.map(|p| p.point);
|
||||
} else if let Some(port) = self.hit_port(&scene.view, local) {
|
||||
request.object = port.element;
|
||||
} else if let Some(node) = self.hit(&scene.view, local) {
|
||||
request.object = self.project.diagrams[&self.diagram].nodes[&node].element;
|
||||
@@ -425,13 +433,35 @@ impl DiagramCanvas<'_> {
|
||||
})
|
||||
.map(|(end, owner, _)| (end, owner))
|
||||
}
|
||||
fn hit_state_label(&self, routed: &RoutedView, p: Point) -> Option<Id> {
|
||||
let world = self.world(p);
|
||||
// State labels paint after nodes, in BTree order. Hit the frontmost first.
|
||||
routed
|
||||
.edges
|
||||
.values()
|
||||
.rev()
|
||||
.find(|edge| {
|
||||
edge.label.state_layout.is_some()
|
||||
&& !edge.label.text.is_empty()
|
||||
&& Rectangle::new(
|
||||
point(edge.label.bounds.position),
|
||||
Size::new(edge.label.bounds.size.width, edge.label.bounds.size.height),
|
||||
)
|
||||
.contains(world)
|
||||
})
|
||||
.map(|edge| edge.id)
|
||||
}
|
||||
fn hit_label(&self, routed: &RoutedView, p: Point) -> Option<Id> {
|
||||
if let Some(edge) = self.hit_state_label(routed, p) {
|
||||
return Some(edge);
|
||||
}
|
||||
let p = self.world(p);
|
||||
routed
|
||||
.edges
|
||||
.values()
|
||||
.filter(|edge| {
|
||||
!edge.label.text.is_empty()
|
||||
edge.label.state_layout.is_none()
|
||||
&& !edge.label.text.is_empty()
|
||||
&& Rectangle::new(
|
||||
point(edge.label.bounds.position),
|
||||
Size::new(edge.label.bounds.size.width, edge.label.bounds.size.height),
|
||||
@@ -963,8 +993,11 @@ impl canvas::Program<Message> for DiagramCanvas<'_> {
|
||||
);
|
||||
}
|
||||
let label = &edge.label;
|
||||
if self.hit(view, p).is_none()
|
||||
&& !label.text.is_empty()
|
||||
if (if label.state_layout.is_some() {
|
||||
self.hit_state_label(&scene.routed, p) == Some(edge.id)
|
||||
} else {
|
||||
self.hit(view, p).is_none()
|
||||
}) && !label.text.is_empty()
|
||||
&& Rectangle::new(
|
||||
point(label.bounds.position),
|
||||
Size::new(label.bounds.size.width, label.bounds.size.height),
|
||||
@@ -983,6 +1016,9 @@ impl canvas::Program<Message> for DiagramCanvas<'_> {
|
||||
return Some(Action::capture());
|
||||
}
|
||||
}
|
||||
if let Some(edge) = self.hit_state_label(&scene.routed, p) {
|
||||
return Some(Action::publish(Message::Select(edge)).and_capture());
|
||||
}
|
||||
if let Some(port) = self.hit_port(view, p) {
|
||||
if self.editable
|
||||
&& let Some(owner) = view.nodes.iter().find(|n| n.id == port.owner)
|
||||
|
||||
@@ -196,8 +196,27 @@ impl DiagramCanvas<'_> {
|
||||
let pointer = cursor.position()?;
|
||||
let local = Point::new(pointer.x - bounds.x, pointer.y - bounds.y);
|
||||
let scene = self.scene(state);
|
||||
let world = self.world(local);
|
||||
// Preserve foreground selected handles without bypassing Shift or the
|
||||
// mixed-group admission/refusal lifecycle below.
|
||||
let state_handle = self.project.diagrams[&self.diagram]
|
||||
.projection
|
||||
.state_machine()
|
||||
.is_some()
|
||||
&& scene.routed.edges.get(&self.selected).is_some_and(|edge| {
|
||||
!edge.custom_suspended
|
||||
&& (self.attachment_at(&scene, world).is_some()
|
||||
|| edge
|
||||
.pins
|
||||
.iter()
|
||||
.any(|pin| world.distance(point(*pin)) <= 8. / self.zoom))
|
||||
});
|
||||
let target = if self.allocation_surface() {
|
||||
self.allocation_press_target(&scene, local)
|
||||
} else if state_handle {
|
||||
Some(Target::Edge(self.selected))
|
||||
} else if let Some(id) = self.hit_state_label(&scene.routed, local) {
|
||||
Some(Target::Edge(id))
|
||||
} else if let Some(port) = self.hit_port(&scene.view, local) {
|
||||
Some(Target::Port {
|
||||
port: port.element,
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
use super::*;
|
||||
use crate::{
|
||||
app::multiselect::{Choice, Event, context},
|
||||
diagram_selection::Selection,
|
||||
diagram_usage::Target,
|
||||
};
|
||||
|
||||
fn fixture() -> (Project, Id, Id, Id, Id) {
|
||||
let mut p = Project::empty();
|
||||
let owner = p
|
||||
.add_element(Kind::Entity, Layer::Oa, "Crew", None)
|
||||
.unwrap();
|
||||
let machine = p
|
||||
.add_element(Kind::StateMachine, Layer::Oa, "Modes", Some(owner))
|
||||
.unwrap();
|
||||
let region = p
|
||||
.add_element(Kind::Region, Layer::Oa, "Main", Some(machine))
|
||||
.unwrap();
|
||||
let a = p
|
||||
.add_element(Kind::State, Layer::Oa, "A", Some(region))
|
||||
.unwrap();
|
||||
let b = p
|
||||
.add_element(Kind::State, Layer::Oa, "B", Some(region))
|
||||
.unwrap();
|
||||
let mut edges = [
|
||||
p.relate(RelationKind::StateTransition, "Ready\nfor review", a, b),
|
||||
p.relate(RelationKind::StateTransition, "Ready\nfor review", a, b),
|
||||
];
|
||||
edges.sort();
|
||||
let diagram = Id::new_v4();
|
||||
p.diagrams.insert(
|
||||
diagram,
|
||||
Diagram {
|
||||
id: diagram,
|
||||
alias: "state_input".into(),
|
||||
name: "State input".into(),
|
||||
description: String::new(),
|
||||
layer: Layer::Oa,
|
||||
projection: ProjectionKind::StateMachine { machine },
|
||||
view_options: Default::default(),
|
||||
nodes: BTreeMap::new(),
|
||||
layout: None,
|
||||
routes: edges
|
||||
.iter()
|
||||
.map(|edge| EdgeRoute {
|
||||
edge: *edge,
|
||||
source_attachment: None,
|
||||
target_attachment: None,
|
||||
waypoints: vec![Position { x: 200., y: 150. }],
|
||||
label: None,
|
||||
})
|
||||
.collect(),
|
||||
},
|
||||
);
|
||||
let node = p.place(diagram, a, Position { x: 100., y: 100. });
|
||||
p.place(diagram, b, Position { x: 650., y: 100. });
|
||||
let routed = route_diagram(&p, &p.diagrams[&diagram], &project_view(&p, diagram));
|
||||
for route in &mut p.diagrams.get_mut(&diagram).unwrap().routes {
|
||||
let label = &routed.edges[&route.edge].label;
|
||||
route.label = Some(EdgeLabel {
|
||||
fraction: label.fraction,
|
||||
offset: Position {
|
||||
x: 200. - label.anchor.x,
|
||||
y: 150. - label.anchor.y,
|
||||
},
|
||||
});
|
||||
}
|
||||
(p, diagram, edges[0], edges[1], node)
|
||||
}
|
||||
|
||||
fn canvas(p: &Project, diagram: Id, selected: Id, zoom: f32) -> DiagramCanvas<'_> {
|
||||
DiagramCanvas {
|
||||
selection: None,
|
||||
project: p,
|
||||
view_index: None,
|
||||
diagram,
|
||||
selected,
|
||||
navigation_focus: None,
|
||||
zoom,
|
||||
pan: Vector::new(100., 100.),
|
||||
context: Context {
|
||||
document: p.id,
|
||||
diagram,
|
||||
revision: 0,
|
||||
epoch: 0,
|
||||
},
|
||||
selected_bend: None,
|
||||
selected_attachment: None,
|
||||
editable: true,
|
||||
placing: false,
|
||||
connection: None,
|
||||
allocation: None,
|
||||
}
|
||||
}
|
||||
fn screen(canvas: &DiagramCanvas<'_>, y: f32) -> Point {
|
||||
Point::new(
|
||||
200. * canvas.zoom + canvas.pan.x,
|
||||
y * canvas.zoom + canvas.pan.y,
|
||||
)
|
||||
}
|
||||
fn pointer(
|
||||
canvas: &DiagramCanvas<'_>,
|
||||
state: &mut State,
|
||||
event: mouse::Event,
|
||||
at: Point,
|
||||
) -> Option<Message> {
|
||||
canvas::Program::update(
|
||||
canvas,
|
||||
state,
|
||||
&canvas::Event::Mouse(event),
|
||||
Rectangle::with_size(Size::new(1800., 1200.)),
|
||||
mouse::Cursor::Available(at),
|
||||
)
|
||||
.and_then(|a| a.into_inner().0)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn state_generic_front_label_beats_node_and_selected_back_label_at_three_zooms() {
|
||||
for zoom in [0.5, 1., 1.5] {
|
||||
let (p, diagram, low, high, _) = fixture();
|
||||
let mut canvas = canvas(&p, diagram, low, zoom);
|
||||
// Ten world pixels below center remains on both labels but avoids bends.
|
||||
let at = screen(&canvas, 160.);
|
||||
let scene = canvas.scene(&State::default());
|
||||
assert!(canvas.hit(&scene.view, at).is_some());
|
||||
assert_eq!(canvas.hit_label(&scene.routed, at), Some(high));
|
||||
// Selected bends have a screen radius: move further down at half zoom.
|
||||
let at = if zoom == 0.5 {
|
||||
screen(&canvas, 167.)
|
||||
} else {
|
||||
at
|
||||
};
|
||||
assert_eq!(
|
||||
canvas
|
||||
.context_request(
|
||||
&State::default(),
|
||||
at,
|
||||
Rectangle::with_size(Size::new(1800., 1200.))
|
||||
)
|
||||
.object,
|
||||
high
|
||||
);
|
||||
let mut state = State::default();
|
||||
assert!(
|
||||
matches!(pointer(&canvas, &mut state, mouse::Event::ButtonPressed(mouse::Button::Left), at), Some(Message::Select(id)) if id == high)
|
||||
);
|
||||
assert!(state.drag.is_none() && state.route_drag.is_none());
|
||||
canvas.selected = high;
|
||||
let mut state = State::default();
|
||||
pointer(
|
||||
&canvas,
|
||||
&mut state,
|
||||
mouse::Event::ButtonPressed(mouse::Button::Left),
|
||||
at,
|
||||
);
|
||||
assert!(matches!(
|
||||
state.route_drag.as_ref().map(|g| &g.handle),
|
||||
Some(Handle::Label { .. })
|
||||
));
|
||||
canvas.editable = false;
|
||||
let mut state = State::default();
|
||||
assert!(
|
||||
matches!(pointer(&canvas, &mut state, mouse::Event::ButtonPressed(mouse::Button::Left), at), Some(Message::Select(id)) if id == high)
|
||||
);
|
||||
assert!(state.drag.is_none() && state.route_drag.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn state_generic_shift_label_and_handle_keep_exact_targets_and_mixed_refusal() {
|
||||
let (p, diagram, low, high, node) = fixture();
|
||||
let base = canvas(&p, diagram, low, 1.);
|
||||
let mut single_state = State::default();
|
||||
assert!(
|
||||
matches!(pointer(&base, &mut single_state, mouse::Event::ButtonPressed(mouse::Button::Left), screen(&base, 150.)), Some(Message::Route(RouteEvent::SelectBend(id, 0))) if id == low)
|
||||
);
|
||||
assert!(matches!(
|
||||
single_state.route_drag.as_ref().map(|g| &g.handle),
|
||||
Some(Handle::Bend(0))
|
||||
));
|
||||
let mut selection = Selection::new(context(base.context).scope);
|
||||
selection.add(Target::Edge(low), |_| true);
|
||||
selection.add(Target::Placement(node), |_| true);
|
||||
let canvas = DiagramCanvas {
|
||||
selection: Some(&selection),
|
||||
..base
|
||||
};
|
||||
for (y, expected) in [(160., high), (150., low)] {
|
||||
let mut state = State {
|
||||
modifiers: iced::keyboard::Modifiers::SHIFT,
|
||||
..State::default()
|
||||
};
|
||||
assert!(
|
||||
matches!(pointer(&canvas, &mut state, mouse::Event::ButtonPressed(mouse::Button::Left), screen(&canvas,y)), Some(Message::DiagramSelection(Event::Choose(_, Target::Edge(id), Choice::Toggle))) if id == expected)
|
||||
);
|
||||
assert!(state.drag.is_none() && state.route_drag.is_none());
|
||||
}
|
||||
let mut state = State::default();
|
||||
let at = screen(&canvas, 150.);
|
||||
assert_eq!(
|
||||
canvas
|
||||
.context_request(&state, at, Rectangle::with_size(Size::new(1800., 1200.)))
|
||||
.bend,
|
||||
Some(0)
|
||||
);
|
||||
pointer(
|
||||
&canvas,
|
||||
&mut state,
|
||||
mouse::Event::ButtonPressed(mouse::Button::Left),
|
||||
at,
|
||||
);
|
||||
assert!(state.group.is_some() && state.route_drag.is_none());
|
||||
let moved = at + Vector::new(20., 20.);
|
||||
pointer(
|
||||
&canvas,
|
||||
&mut state,
|
||||
mouse::Event::CursorMoved { position: moved },
|
||||
moved,
|
||||
);
|
||||
assert!(matches!(
|
||||
pointer(
|
||||
&canvas,
|
||||
&mut state,
|
||||
mouse::Event::ButtonReleased(mouse::Button::Left),
|
||||
moved
|
||||
),
|
||||
Some(Message::DiagramSelection(Event::Refuse(_, _)))
|
||||
));
|
||||
assert!(state.route_drag.is_none() && state.drag.is_none());
|
||||
}
|
||||
Reference in New Issue
Block a user