fix: keep saved diagram viewport stable across group selection
This commit is contained in:
@@ -154,9 +154,6 @@ async fn mounted_f2_requires_canvas_focus_and_yields_to_clean_inspector() {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Append to app/diagram_focus_tests.rs after the frozen desktop gate.
|
||||
// Reuses its imports and FocusField, plus Mounted/key from canvas_workspace tests.
|
||||
|
||||
async fn modifier_focus_fixture() -> (App, Mounted, Id, Id, Id) {
|
||||
let mut project = Project::empty();
|
||||
let a = project
|
||||
@@ -498,3 +495,99 @@ async fn mounted_window_unfocus_resets_held_shift_before_next_canvas_press() {
|
||||
assert_eq!(app.document.bundle(), &before.bundle);
|
||||
assert!(!app.document.can_undo());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn mounted_selection_strip_keeps_canvas_and_fitted_camera_fixed_for_empty_single_group_and_mixed()
|
||||
{
|
||||
use crate::diagram_usage::Target;
|
||||
let mut project = Project::empty();
|
||||
let a = project
|
||||
.add_element(Kind::Activity, Layer::Oa, "First activity", None)
|
||||
.unwrap();
|
||||
let b = project
|
||||
.add_element(
|
||||
Kind::Activity,
|
||||
Layer::Oa,
|
||||
&format!(
|
||||
"Second activity {}",
|
||||
"with a very long primary name ".repeat(20)
|
||||
),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
let edge = project.relate(RelationKind::Interaction, "Activity exchange", a, b);
|
||||
let diagram = project.ensure_diagram(Layer::Oa);
|
||||
project.diagrams.get_mut(&diagram).unwrap().projection = ProjectionKind::Overview;
|
||||
let first = project.place(diagram, a, Position { x: 80., y: 80. });
|
||||
let second = project.place(diagram, b, Position { x: 500., y: 80. });
|
||||
let mut app = App::with_project(project);
|
||||
app.page = Page::Diagram;
|
||||
app.layer = Layer::Oa;
|
||||
app.active_diagram = Some(diagram);
|
||||
app.window_size = Size::new(1442., 932.);
|
||||
let _ = app.update(Message::Select(diagram));
|
||||
app.fit_to_view = true;
|
||||
let mut mounted = Mounted::new(&mut app).await;
|
||||
let baseline = app.document.snapshot();
|
||||
let bounds = mounted
|
||||
.geometry(&app)
|
||||
.bounds(crate::allocation_input::CANVAS_ID);
|
||||
let camera = (app.zoom, app.pan, app.fit_to_view, app.canvas_size);
|
||||
assert!(camera.2, "exercise the native resize-triggered refit path");
|
||||
assert!(app.multiselect_snapshot().unwrap().targets().is_empty());
|
||||
assert!(bounds.width > 500. && bounds.height > 300.);
|
||||
for (target, expected) in [
|
||||
(
|
||||
Some(Target::Placement(first)),
|
||||
vec![Target::Placement(first)],
|
||||
),
|
||||
(
|
||||
Some(Target::Placement(second)),
|
||||
vec![Target::Placement(first), Target::Placement(second)],
|
||||
),
|
||||
(
|
||||
Some(Target::Edge(edge)),
|
||||
vec![
|
||||
Target::Placement(first),
|
||||
Target::Placement(second),
|
||||
Target::Edge(edge),
|
||||
],
|
||||
),
|
||||
(
|
||||
Some(Target::Edge(edge)),
|
||||
vec![Target::Placement(first), Target::Placement(second)],
|
||||
),
|
||||
(
|
||||
Some(Target::Placement(second)),
|
||||
vec![Target::Placement(first)],
|
||||
),
|
||||
(None, vec![]),
|
||||
] {
|
||||
let context = app.multiselect_context().unwrap();
|
||||
let event = match target {
|
||||
Some(target) => {
|
||||
multiselect::Event::Choose(context, target, multiselect::Choice::Toggle)
|
||||
}
|
||||
None => multiselect::Event::Clear(context),
|
||||
};
|
||||
let task = app.update(Message::DiagramSelection(event));
|
||||
mounted.perform(&mut app, task).await;
|
||||
mounted.settle(&mut app).await;
|
||||
assert_eq!(app.multiselect_snapshot().unwrap().targets(), expected);
|
||||
assert_eq!(
|
||||
mounted
|
||||
.geometry(&app)
|
||||
.bounds(crate::allocation_input::CANVAS_ID),
|
||||
bounds,
|
||||
"selection feedback must not change the measured canvas rectangle"
|
||||
);
|
||||
assert_eq!(
|
||||
(app.zoom, app.pan, app.fit_to_view, app.canvas_size),
|
||||
camera
|
||||
);
|
||||
assert_eq!(app.project(), &baseline.decoded.project);
|
||||
assert_eq!(app.document.bundle(), &baseline.bundle);
|
||||
assert!(!app.document.can_undo());
|
||||
assert!(!app.draft_dirty());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -429,32 +429,7 @@ fn diagram(app: &App) -> View<'_, Message> {
|
||||
chooser = chooser.push(action("View properties", Message::Select(id)));
|
||||
controls = controls.push(action("Auto-layout", Message::Command(Command::Layout(id))));
|
||||
}
|
||||
let mut toolbar = column![chooser, controls].spacing(6);
|
||||
if let Some(snapshot) = app.multiselect_snapshot().filter(|s| s.targets().len() > 1) {
|
||||
let mut group_controls = row![label(format!("{} selected", snapshot.targets().len()))]
|
||||
.spacing(7)
|
||||
.align_y(iced::Center);
|
||||
if crate::app::multiselect::placements(&snapshot).is_some() {
|
||||
group_controls = group_controls.push(action(
|
||||
"Hide selected",
|
||||
Message::DiagramSelection(crate::app::multiselect::Event::Hide(snapshot.clone())),
|
||||
));
|
||||
}
|
||||
group_controls = group_controls.push(action(
|
||||
"Clear selection",
|
||||
Message::DiagramSelection(crate::app::multiselect::Event::Clear(snapshot.context())),
|
||||
));
|
||||
toolbar = toolbar.push(
|
||||
column![
|
||||
group_controls,
|
||||
label(format!(
|
||||
"Properties edit only {}",
|
||||
short(app.project().named(app.selected), 36)
|
||||
))
|
||||
]
|
||||
.spacing(3),
|
||||
);
|
||||
}
|
||||
let toolbar = column![chooser, controls].spacing(6);
|
||||
let projection = app
|
||||
.active_diagram
|
||||
.and_then(|id| app.project().diagrams.get(&id))
|
||||
@@ -634,6 +609,37 @@ fn diagram(app: &App) -> View<'_, Message> {
|
||||
)
|
||||
.is_some_and(|(source, target)| source != edge.source || target != edge.target)
|
||||
});
|
||||
let selection = app.multiselect_snapshot();
|
||||
let count = selection.as_ref().map_or(0, |s| s.targets().len());
|
||||
let mut selection_controls =
|
||||
row![label(format!("{count} selected")).wrapping(widget::text::Wrapping::None)]
|
||||
.spacing(7)
|
||||
.align_y(iced::Center)
|
||||
.height(32);
|
||||
if let Some(snapshot) = selection.as_ref().filter(|s| s.targets().len() > 1) {
|
||||
if crate::app::multiselect::placements(snapshot).is_some() {
|
||||
selection_controls = selection_controls.push(action(
|
||||
"Hide selected",
|
||||
Message::DiagramSelection(crate::app::multiselect::Event::Hide(
|
||||
snapshot.clone(),
|
||||
)),
|
||||
));
|
||||
}
|
||||
selection_controls = selection_controls
|
||||
.push(action(
|
||||
"Clear selection",
|
||||
Message::DiagramSelection(crate::app::multiselect::Event::Clear(
|
||||
snapshot.context(),
|
||||
)),
|
||||
))
|
||||
.push(
|
||||
label(format!(
|
||||
"Properties edit only {}",
|
||||
short(app.project().named(app.selected), 36)
|
||||
))
|
||||
.wrapping(widget::text::Wrapping::None),
|
||||
);
|
||||
}
|
||||
let hint = if let Some(feedback) = &app.allocation_drag.feedback {
|
||||
feedback.text.as_str()
|
||||
} else if let Some(session) = &app.connection {
|
||||
@@ -646,16 +652,37 @@ fn diagram(app: &App) -> View<'_, Message> {
|
||||
} else {
|
||||
session.tool.hint()
|
||||
}
|
||||
} else if count > 1 {
|
||||
if selection
|
||||
.as_ref()
|
||||
.is_some_and(|s| crate::app::multiselect::placements(s).is_some())
|
||||
&& projection != Some(ProjectionKind::Allocation)
|
||||
{
|
||||
"Shift-click to add/remove · drag selected to move · right-click for actions · scroll to zoom"
|
||||
} else {
|
||||
"Shift-click to add/remove · right-click for selection actions · Esc clears selection"
|
||||
}
|
||||
} else if promoted {
|
||||
"Expand endpoint containers to edit this connection route"
|
||||
} else if edge.is_some() {
|
||||
"Right-click for routing · drag line / bend / label · Insert adds a bend · Esc cancels"
|
||||
} else {
|
||||
"Right-click for actions · drag to arrange · scroll to zoom"
|
||||
"Shift-click to add/remove · right-click for actions · drag to arrange · scroll to zoom"
|
||||
};
|
||||
// Status is outside the measured canvas. Clipping prevents long text
|
||||
// from painting over work headers or the dedicated destination slot.
|
||||
tools = tools.push(container(label(hint)).height(30).width(Fill).clip(true));
|
||||
// Selection changes must not resize the measured canvas or refit its
|
||||
// camera. Reserve both rows even when no group actions are available.
|
||||
tools = tools.push(
|
||||
container(
|
||||
column![
|
||||
selection_controls,
|
||||
label(hint).wrapping(widget::text::Wrapping::None)
|
||||
]
|
||||
.spacing(3),
|
||||
)
|
||||
.height(56)
|
||||
.width(Fill)
|
||||
.clip(true),
|
||||
);
|
||||
}
|
||||
column![container(tools).padding(14), canvas].into()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user