Add typed data contract editing and bounded dictionary views
This commit is contained in:
@@ -292,6 +292,7 @@ impl DiagramCanvas<'_> {
|
||||
request.placement = Some(node);
|
||||
} else if let Some(edge) = self
|
||||
.hit_label(&scene.routed, local)
|
||||
.or_else(|| self.hit_association_label(&scene.routed, local))
|
||||
.or_else(|| self.hit_edge(&scene.routed, local).map(|(id, _)| id))
|
||||
{
|
||||
request.object = edge;
|
||||
@@ -369,6 +370,33 @@ impl DiagramCanvas<'_> {
|
||||
})
|
||||
.map(|edge| edge.id)
|
||||
}
|
||||
fn hit_association_label(&self, routed: &RoutedView, p: Point) -> Option<Id> {
|
||||
let p = self.world(p);
|
||||
routed
|
||||
.edges
|
||||
.values()
|
||||
.flat_map(|edge| {
|
||||
self.project
|
||||
.relations
|
||||
.get(&edge.id)
|
||||
.into_iter()
|
||||
.flat_map(move |relation| routing::association_end_geometry(relation, edge))
|
||||
.map(move |end| (edge.id, end.label))
|
||||
})
|
||||
.filter(|(_, label)| {
|
||||
Rectangle::new(
|
||||
point(label.bounds.position),
|
||||
Size::new(label.bounds.size.width, label.bounds.size.height),
|
||||
)
|
||||
.contains(p)
|
||||
})
|
||||
.min_by(|a, b| {
|
||||
p.distance(point(a.1.center))
|
||||
.total_cmp(&p.distance(point(b.1.center)))
|
||||
.then(a.0.cmp(&b.0))
|
||||
})
|
||||
.map(|(id, _)| id)
|
||||
}
|
||||
fn preview_diagram(&self, state: &State) -> Diagram {
|
||||
let mut diagram = self.project.diagrams[&self.diagram].clone();
|
||||
if let (Some((id, _, original)), Some(preview)) = (state.drag, state.preview) {
|
||||
@@ -725,7 +753,10 @@ impl canvas::Program<Message> for DiagramCanvas<'_> {
|
||||
state.drag = Some((id, p, position));
|
||||
state.preview = Some(position);
|
||||
Some(Action::publish(Message::SelectNode(id)).and_capture())
|
||||
} else if let Some(id) = self.hit_label(&scene.routed, p) {
|
||||
} else if let Some(id) = self
|
||||
.hit_label(&scene.routed, p)
|
||||
.or_else(|| self.hit_association_label(&scene.routed, p))
|
||||
{
|
||||
Some(Action::publish(Message::Select(id)).and_capture())
|
||||
} else if let Some((id, segment)) = self.hit_edge(&scene.routed, p) {
|
||||
if id == self.selected
|
||||
@@ -1010,6 +1041,37 @@ impl canvas::Program<Message> for DiagramCanvas<'_> {
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
if let Some(relation) = self.project.relations.get(&edge.id) {
|
||||
for end in routing::association_end_geometry(relation, routed) {
|
||||
if end.navigable {
|
||||
let [a, tip, b] = navigation_mark(end.tip, end.inward);
|
||||
f.stroke(
|
||||
&Path::new(|p| {
|
||||
p.move_to(a);
|
||||
p.line_to(tip);
|
||||
p.line_to(b);
|
||||
}),
|
||||
Stroke::default().with_color(color).with_width(1.7),
|
||||
);
|
||||
}
|
||||
let label = &end.label;
|
||||
f.fill_rectangle(
|
||||
point(label.bounds.position),
|
||||
Size::new(label.bounds.size.width, label.bounds.size.height),
|
||||
Color::WHITE,
|
||||
);
|
||||
f.fill_text(Text {
|
||||
content: label.text.clone(),
|
||||
position: point(label.center),
|
||||
color: if active { style::TEAL } else { style::MUTED },
|
||||
size: 13.into(),
|
||||
align_x: iced::widget::text::Alignment::Center,
|
||||
align_y: iced::alignment::Vertical::Center,
|
||||
max_width: f32::INFINITY,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
for n in &view.nodes {
|
||||
if !containers.contains(&n.id) {
|
||||
@@ -1257,6 +1319,17 @@ fn endpoint_position(view: &ProjectedView, id: Id) -> Option<Position> {
|
||||
})
|
||||
})
|
||||
}
|
||||
fn navigation_mark(tip: Position, inward: Position) -> [Point; 3] {
|
||||
let tip = point(tip);
|
||||
let delta = point(inward) - tip;
|
||||
let direction = delta / delta.x.hypot(delta.y).max(0.001);
|
||||
let normal = Vector::new(-direction.y, direction.x) * 4.;
|
||||
[
|
||||
tip + direction * 9. + normal,
|
||||
tip,
|
||||
tip + direction * 9. - normal,
|
||||
]
|
||||
}
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum PortGlyph {
|
||||
Function,
|
||||
@@ -1544,6 +1617,21 @@ pub fn export_svg(p: &Project, diagram: Id) -> String {
|
||||
if !label.text.is_empty() {
|
||||
svg.push_str(&format!("<rect x=\"{}\" y=\"{}\" width=\"{}\" height=\"{}\" fill=\"white\"/><text x=\"{}\" y=\"{}\" text-anchor=\"middle\" dominant-baseline=\"central\" font-family=\"sans-serif\" font-size=\"13\" fill=\"#637487\">{}</text>",label.bounds.position.x,label.bounds.position.y,label.bounds.size.width,label.bounds.size.height,label.center.x,label.center.y,escape(&label.text)));
|
||||
}
|
||||
if let Some(relation) = p.relations.get(&edge.id) {
|
||||
for end in routing::association_end_geometry(relation, route) {
|
||||
let side = if end.source_end { "source" } else { "target" };
|
||||
svg.push_str(&format!(
|
||||
"<g data-association=\"{}\" data-end=\"{side}\">",
|
||||
edge.id
|
||||
));
|
||||
if end.navigable {
|
||||
let [a, tip, b] = navigation_mark(end.tip, end.inward);
|
||||
svg.push_str(&format!("<path data-navigation=\"{side}\" d=\"M {} {} L {} {} L {} {}\" fill=\"none\" stroke=\"#8999a9\" stroke-width=\"1.7\"/>",a.x,a.y,tip.x,tip.y,b.x,b.y));
|
||||
}
|
||||
let label = &end.label;
|
||||
svg.push_str(&format!("<rect x=\"{}\" y=\"{}\" width=\"{}\" height=\"{}\" fill=\"white\"/><text x=\"{}\" y=\"{}\" text-anchor=\"middle\" dominant-baseline=\"central\" font-family=\"sans-serif\" font-size=\"13\" fill=\"#637487\">{}</text></g>",label.bounds.position.x,label.bounds.position.y,label.bounds.size.width,label.bounds.size.height,label.center.x,label.center.y,escape(&label.text)));
|
||||
}
|
||||
}
|
||||
}
|
||||
for n in &view.nodes {
|
||||
if !containers.contains(&n.id) {
|
||||
@@ -2353,6 +2441,98 @@ mod tests {
|
||||
assert!(geometry.bounds.position.y <= route.label.bounds.position.y);
|
||||
}
|
||||
#[test]
|
||||
fn association_end_contracts_share_svg_geometry_and_select_without_moving_name_label() {
|
||||
for self_loop in [false, true] {
|
||||
let mut p = Project::empty();
|
||||
let a = p.add(Kind::DataType, "Tree", 100., 100.);
|
||||
let b = if self_loop {
|
||||
a
|
||||
} else {
|
||||
p.add(Kind::DataType, "Branch", 650., 100.)
|
||||
};
|
||||
for id in BTreeSet::from([a, b]) {
|
||||
*p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::DataType {
|
||||
definition: DataTypeDefinition::Record { supertype: None },
|
||||
default: None,
|
||||
unit: String::new(),
|
||||
};
|
||||
}
|
||||
let edge = p.relate(RelationKind::DataAssociation, "Hierarchy", a, b);
|
||||
p.relations.get_mut(&edge).unwrap().body = RelationBody::DataAssociation {
|
||||
source: a,
|
||||
target: b,
|
||||
source_role: "parent".into(),
|
||||
target_role: "children".into(),
|
||||
source_minimum: 0,
|
||||
source_maximum: Some(1),
|
||||
target_minimum: 0,
|
||||
target_maximum: None,
|
||||
source_navigable: false,
|
||||
target_navigable: true,
|
||||
};
|
||||
let diagram = p.ensure_diagram(Layer::Oa);
|
||||
let projected = project_view(&p, diagram);
|
||||
assert!(
|
||||
!projected
|
||||
.edges
|
||||
.iter()
|
||||
.find(|e| e.id == edge)
|
||||
.unwrap()
|
||||
.oriented
|
||||
);
|
||||
let routed = route_diagram(&p, &p.diagrams[&diagram], &projected);
|
||||
let ends = routing::association_end_geometry(&p.relations[&edge], &routed.edges[&edge]);
|
||||
assert_eq!(ends.len(), 2);
|
||||
let svg = export_svg(&p, diagram);
|
||||
assert!(svg.contains("parent [0..1]</text>"));
|
||||
assert!(svg.contains("children [0..*]</text>"));
|
||||
assert!(!svg.contains("data-navigation=\"source\""));
|
||||
assert!(svg.contains("data-navigation=\"target\""));
|
||||
let mut canvas = DiagramCanvas {
|
||||
project: &p,
|
||||
diagram,
|
||||
selected: edge,
|
||||
zoom: 0.7,
|
||||
pan: Vector::new(350., 350.),
|
||||
context: Context {
|
||||
document: p.id,
|
||||
diagram,
|
||||
revision: 0,
|
||||
epoch: 0,
|
||||
},
|
||||
selected_bend: None,
|
||||
editable: true,
|
||||
connection: None,
|
||||
};
|
||||
for end in ends {
|
||||
assert!(svg.contains(&format!(
|
||||
"x=\"{}\" y=\"{}\" text-anchor",
|
||||
end.label.center.x, end.label.center.y
|
||||
)));
|
||||
let at = Point::new(
|
||||
end.label.center.x * canvas.zoom + canvas.pan.x,
|
||||
end.label.center.y * canvas.zoom + canvas.pan.y,
|
||||
);
|
||||
let mut state = State::default();
|
||||
assert_eq!(canvas.hit_association_label(&routed, at), Some(edge));
|
||||
assert!(
|
||||
matches!(pointer(&canvas,&mut state,mouse::Event::ButtonPressed(mouse::Button::Left),at),Some(Message::Select(id)) if id==edge)
|
||||
);
|
||||
assert!(
|
||||
state.route_drag.is_none(),
|
||||
"Endpoint contract labels never drag the association name"
|
||||
);
|
||||
assert_eq!(
|
||||
canvas
|
||||
.context_request(&state, at, Rectangle::with_size(Size::new(1800., 1200.)))
|
||||
.object,
|
||||
edge
|
||||
);
|
||||
canvas.selected = p.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn detached_label_selects_connection_and_occluding_card_owns_its_hit() {
|
||||
let (mut p, diagram, edge) = route_fixture();
|
||||
p.diagrams.get_mut(&diagram).unwrap().routes[0].label = Some(EdgeLabel {
|
||||
|
||||
Reference in New Issue
Block a user