WIP: save unverified diagram Delete candidate at user pause

This commit is contained in:
2026-09-12 04:58:44 -07:00
parent 0c38980c70
commit 60ea483430
14 changed files with 1308 additions and 36 deletions
@@ -291,7 +291,7 @@ fn pending_behavior_blocks_navigation_property_edits_and_model_history() {
Message::BrowseGroup(crate::explorer::Group::Work),
Message::Field("/name".into(), "Overwrite properties".into()),
Message::Command(Command::Rename(fixture.source, "Overwrite model".into())),
Message::ConfirmDelete,
Message::ConfirmDelete(0),
Message::Undo,
Message::Redo,
Message::ShortcutUndo(false),
@@ -0,0 +1,152 @@
//! Captured diagram removal and model-confirmation lifetimes.
use super::*;
use crate::{model_menu, route_edit::Context};
#[derive(Debug, Clone)]
pub struct Intent {
pub context: Context,
pub object: Id,
pub placement: Option<Id>,
pub model: bool,
}
pub struct Confirmation {
pub snapshot: model_menu::Opened,
pub selection: Id,
pub placement: Option<Id>,
}
pub fn enabled(app: &App) -> bool {
crate::behavior::canvas_workspace::keyboard_enabled(app)
&& !app.draft_dirty()
&& !app.source_dirty()
&& !app.behavior.has_pending()
&& app
.architecture
.as_ref()
.is_none_or(|state| !state.inline())
}
impl App {
pub(super) fn remove_from_diagram(&mut self, intent: Intent) {
if !enabled(self) || self.connection.is_some() || self.selected_bend.is_some() {
return;
}
let current = match self.page {
Page::Diagram if self.architecture.as_ref().is_none_or(|s| !s.saved.placing) => {
self.route_context()
}
Page::Behavior
if self.behavior.diagram.tool == crate::behavior::state_edit::Tool::Select =>
{
self.state_context()
}
_ => None,
};
if current != Some(intent.context) || self.selected != intent.object {
return;
}
let view = projection::project_view(self.project(), intent.context.diagram);
let placement = if self.page == Page::Behavior {
self.behavior.diagram.placement
} else {
self.selected_node
};
if intent.placement != placement || intent.object == intent.context.diagram {
return;
}
let visible = if let Some(id) = intent.placement {
self.project().diagrams[&intent.context.diagram]
.nodes
.get(&id)
.is_some_and(|node| node.element == intent.object)
&& view
.nodes
.iter()
.any(|node| node.id == id && node.element == intent.object)
} else {
view.edges.iter().any(|edge| edge.id == intent.object)
|| view.ports.iter().any(|port| port.element == intent.object)
};
if !visible {
return;
}
if !intent.model
&& let Some(placement) = intent.placement
{
if self.execute(Command::Delete(placement)) {
self.selected_node = None;
self.behavior.diagram.placement = None;
self.selected_bend = None;
self.status = "Hidden from this diagram · Model element preserved · Undo restores the placement".into();
}
} else {
self.begin_model_delete(Some(intent.context));
}
}
pub(super) fn begin_model_delete(&mut self, canvas: Option<Context>) {
if self.selected == self.project().id || self.behavior.has_pending() {
return;
}
let mut request = model_menu::Request::object(self.selected, iced::Point::ORIGIN);
request.canvas = canvas;
request.placement = if self.page == Page::Behavior {
self.behavior.diagram.placement
} else {
self.selected_node
};
// Opening a modal retires any pending canvas intent before capturing
// the confirmation; every new dialog receives its own replay token.
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
if canvas.is_some() {
request.canvas = if self.page == Page::Behavior {
self.state_context()
} else {
self.route_context()
};
}
self.menu_sequence = self.menu_sequence.wrapping_add(1);
self.delete_target =
model_menu::Opened::new(self, request, self.menu_sequence).map(|snapshot| {
Confirmation {
snapshot,
selection: self.selected,
placement: if self.page == Page::Behavior {
self.behavior.diagram.placement
} else {
self.selected_node
},
}
});
self.delete_pending = self.delete_target.is_some();
}
pub(super) fn confirm_model_delete(&mut self, token: u64) {
let Some(target) = self.delete_target.as_ref() else {
return;
};
if target.snapshot.token != token {
return;
}
let valid = self.delete_pending
&& target.snapshot.valid(self)
&& target.selection == self.selected
&& target.placement
== if self.page == Page::Behavior {
self.behavior.diagram.placement
} else {
self.selected_node
}
&& !self.behavior.has_pending()
&& !self.source_dirty()
&& !self.draft_dirty();
let id = target.snapshot.request.object;
self.delete_pending = false;
self.delete_target = None;
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
if valid {
self.execute(Command::Delete(id));
}
}
}
@@ -0,0 +1,681 @@
//! Real App tree regression tests; no private canvas focus or gesture seeding.
use super::*;
use crate::behavior::canvas_workspace::tests::{Mounted, key};
use iced::advanced::widget::{Operation, operation};
use iced::{Event as Input, Point, Rectangle, Size, Vector, keyboard, mouse};
struct Fixture {
app: App,
object: Id,
node: Id,
other: Id,
diagram: Id,
copy: Id,
}
fn fixture(layer: Layer) -> Fixture {
let mut p = Project::empty();
let kind = if layer == Layer::Oa {
Kind::Entity
} else {
Kind::System
};
let object = p.add_element(kind, layer, "Target", None).unwrap();
let other = p
.add_element(
if layer == Layer::Oa {
Kind::Actor
} else {
Kind::SystemActor
},
layer,
"Other",
None,
)
.unwrap();
let diagram = p.ensure_diagram(layer);
p.diagrams.get_mut(&diagram).unwrap().projection = ProjectionKind::Overview;
let node = p.place(diagram, object, Position { x: 80., y: 80. });
p.place(diagram, other, Position { x: 500., y: 80. });
let mut second = p.diagrams[&diagram].clone();
second.id = Id::new_v4();
second.name = "Other diagram".into();
second.alias = "other_diagram".into();
second.nodes.clear();
second.routes.clear();
let copy = second.id;
p.diagrams.insert(copy, second);
p.place(copy, object, Position { x: 70., y: 70. });
let mut app = App::with_project(p);
app.page = Page::Diagram;
app.layer = layer;
app.active_diagram = Some(diagram);
app.window_size = Size::new(2000., 1400.);
Fixture {
app,
object,
node,
other,
diagram,
copy,
}
}
async fn mount(layer: Layer) -> (Fixture, Mounted) {
let mut f = fixture(layer);
let mut mounted = Mounted::new(&mut f.app).await;
f.app.zoom = 1.;
f.app.pan = Vector::ZERO;
f.app.fit_to_view = false;
mounted.settle(&mut f.app).await;
select(&mut mounted, &mut f).await;
(f, mounted)
}
async fn select(mounted: &mut Mounted, f: &mut Fixture) {
let bounds = mounted
.geometry(&f.app)
.bounds(crate::allocation_input::CANVAS_ID);
let node = projection::project_view(f.app.project(), f.diagram)
.nodes
.into_iter()
.find(|node| node.id == f.node)
.unwrap();
let point = Point::new(
bounds.x + (node.position.x + 40.) * f.app.zoom + f.app.pan.x,
bounds.y + (node.position.y + 35.) * f.app.zoom + f.app.pan.y,
);
mounted.click(&mut f.app, point).await;
assert_eq!(f.app.selected, f.object);
assert_eq!(f.app.selected_node, Some(f.node));
}
fn modified(named: keyboard::key::Named, modifiers: keyboard::Modifiers) -> Input {
let mut input = key(named);
if let Input::Keyboard(keyboard::Event::KeyPressed {
modifiers: value, ..
}) = &mut input
{
*value = modifiers;
}
input
}
async fn delete(mounted: &mut Mounted, app: &mut App, shift: bool) -> Vec<Message> {
mounted
.batch(
app,
&[modified(
keyboard::key::Named::Delete,
if shift {
keyboard::Modifiers::SHIFT
} else {
keyboard::Modifiers::empty()
},
)],
mouse::Cursor::Unavailable,
)
.await
}
fn consistent(app: &App) {
assert_eq!(
source_project::compile_bundle(app.document.bundle(), LIMITS)
.unwrap()
.decoded
.project,
*app.project()
);
assert_eq!(
source_workspace::reachable(&app.source_workspace.bundle()).unwrap(),
*app.document.bundle()
);
}
#[tokio::test]
async fn mounted_oa_sa_delete_hides_exact_placement_and_round_trips_source_and_model() {
for layer in [Layer::Oa, Layer::Sa] {
let (mut f, mut mounted) = mount(layer).await;
let before = f.app.document.snapshot();
let messages = delete(&mut mounted, &mut f.app, false).await;
assert!(
messages
.iter()
.any(|message| matches!(message, Message::DiagramRemove(_)))
);
assert!(f.app.project().contains(f.object));
assert!(
!f.app.project().diagrams[&f.diagram]
.nodes
.contains_key(&f.node)
);
assert!(
f.app.project().diagrams[&f.copy]
.nodes
.values()
.any(|node| node.element == f.object)
);
assert!(!f.app.delete_pending);
consistent(&f.app);
let after = f.app.document.snapshot();
let task = f.app.update(Message::Undo);
mounted.perform(&mut f.app, task).await;
assert_eq!(f.app.project(), &before.decoded.project);
assert_eq!(f.app.document.bundle(), &before.bundle);
assert!(!f.app.document.can_undo(), "one removal, one undo unit");
let task = f.app.update(Message::Redo);
mounted.perform(&mut f.app, task).await;
assert_eq!(f.app.project(), &after.decoded.project);
assert_eq!(f.app.document.bundle(), &after.bundle);
consistent(&f.app);
}
}
#[tokio::test]
async fn mounted_shift_delete_captures_target_cancel_escape_and_confirmation_replay() {
let (mut f, mut mounted) = mount(Layer::Oa).await;
let before = f.app.document.snapshot();
delete(&mut mounted, &mut f.app, true).await;
let target = f.app.delete_target.as_ref().unwrap();
assert_eq!(target.snapshot.request.object, f.object);
let stale_token = target.snapshot.token;
let task = f.app.update(Message::CancelDelete);
mounted.perform(&mut f.app, task).await;
let task = f.app.update(Message::ConfirmDelete(stale_token));
mounted.perform(&mut f.app, task).await;
assert_eq!(f.app.document.bundle(), &before.bundle);
select(&mut mounted, &mut f).await;
delete(&mut mounted, &mut f.app, true).await;
let token = f.app.delete_target.as_ref().unwrap().snapshot.token;
let task = f.app.update(Message::ConfirmDelete(stale_token));
mounted.perform(&mut f.app, task).await;
assert!(
f.app.delete_pending,
"old dialog must not accept a new confirmation"
);
let task = f.app.update(Message::ConfirmDelete(token));
mounted.perform(&mut f.app, task).await;
assert!(!f.app.project().contains(f.object));
assert!(f.app.project().contains(f.other));
consistent(&f.app);
let task = f.app.update(Message::Undo);
mounted.perform(&mut f.app, task).await;
assert_eq!(f.app.document.bundle(), &before.bundle);
select(&mut mounted, &mut f).await;
delete(&mut mounted, &mut f.app, true).await;
let task = f.app.update(Message::Escape);
mounted.perform(&mut f.app, task).await;
assert!(!f.app.delete_pending);
assert!(f.app.delete_target.is_none());
assert_eq!(f.app.document.bundle(), &before.bundle);
}
struct FocusField(Rectangle);
impl Operation for FocusField {
fn traverse(&mut self, visit: &mut dyn FnMut(&mut dyn Operation)) {
visit(self);
}
fn focusable(
&mut self,
_: Option<&iced::advanced::widget::Id>,
bounds: Rectangle,
state: &mut dyn operation::Focusable,
) {
if bounds == self.0 {
state.focus();
} else {
state.unfocus();
}
}
}
fn name_field(mounted: &mut Mounted, app: &App) -> Rectangle {
mounted
.geometry(app)
.text_inputs
.into_iter()
.filter(|bounds| bounds.x > app.window_size.width - 320.)
.min_by(|a, b| a.y.total_cmp(&b.y))
.unwrap()
}
#[tokio::test]
async fn mounted_clean_text_programmatic_focus_and_tab_remove_canvas_keyboard_ownership() {
for programmatic in [true, false] {
let (mut f, mut mounted) = mount(Layer::Oa).await;
let before = f.app.document.snapshot();
let field = name_field(&mut mounted, &f.app);
if programmatic {
mounted.operate(&f.app, &mut FocusField(field));
} else {
let messages = mounted
.batch(
&mut f.app,
&[
key(keyboard::key::Named::Tab),
key(keyboard::key::Named::Delete),
],
mouse::Cursor::Unavailable,
)
.await;
assert!(
!messages
.iter()
.any(|message| matches!(message, Message::DiagramRemove(_))),
"Tab retires canvas admission before its queued focus operation runs"
);
}
let geometry = mounted.geometry(&f.app);
assert!(
geometry
.focused
.iter()
.any(|(_, bounds, focused)| *bounds == field && *focused),
"clean Inspector receives real focus"
);
assert!(!f.app.draft_dirty());
assert!(
delete(&mut mounted, &mut f.app, true)
.await
.iter()
.all(|message| !matches!(message, Message::DiagramRemove(_)))
);
assert!(!f.app.delete_pending);
mounted
.batch(
&mut f.app,
&[key(keyboard::key::Named::Home)],
mouse::Cursor::Unavailable,
)
.await;
let messages = delete(&mut mounted, &mut f.app, false).await;
assert!(
messages
.iter()
.any(|message| matches!(message, Message::Field(path, _) if path == "/name"))
);
assert_eq!(f.app.draft.as_ref().unwrap().inputs["/name"].raw, "arget");
assert_eq!(f.app.document.bundle(), &before.bundle);
assert!(f.app.project().contains(f.node));
assert!(!f.app.delete_pending);
}
}
#[tokio::test]
async fn mounted_removal_intents_reject_stale_selection_revision_owner_and_document() {
for boundary in 0..5 {
let (mut f, mut mounted) = mount(Layer::Oa).await;
let messages = mounted.input(
&f.app,
&[key(keyboard::key::Named::Delete)],
mouse::Cursor::Unavailable,
);
let intent = messages
.into_iter()
.find(|message| matches!(message, Message::DiagramRemove(_)))
.unwrap();
match boundary {
0 => {
let _ = f.app.update(Message::Select(f.other));
}
1 => {
f.app.canvas_epoch = f.app.canvas_epoch.wrapping_add(1);
}
2 => {
f.app.active_diagram = Some(f.copy);
}
3 => {
f.app.canvas_document = Id::new_v4();
}
_ => {
let _ = f
.app
.update(Message::Command(Command::Rename(f.other, "Renamed".into())));
}
}
let before = f.app.document.snapshot();
let task = f.app.update(intent);
mounted.perform(&mut f.app, task).await;
assert_eq!(f.app.document.bundle(), &before.bundle);
assert!(f.app.project().contains(f.node));
assert!(!f.app.delete_pending);
}
}
#[tokio::test]
async fn mounted_window_modal_dirty_and_modifier_boundaries_retire_delete_focus() {
for boundary in 0..5 {
let (mut f, mut mounted) = mount(Layer::Oa).await;
let before = f.app.document.snapshot();
match boundary {
0 => {
mounted
.batch(
&mut f.app,
&[Input::Window(iced::window::Event::Unfocused)],
mouse::Cursor::Unavailable,
)
.await;
}
1 => {
let _ = f.app.update(Message::Menu(crate::model_menu::Event::Open(
crate::model_menu::Request::object(f.object, Point::ORIGIN),
)));
mounted.settle(&mut f.app).await;
let _ = f.app.update(Message::Menu(crate::model_menu::Event::Close));
}
2 => {
let _ = f.app.update(Message::Field("/name".into(), "Dirty".into()));
}
3 => {
let _ = f.app.update(Message::Delete);
}
_ => {
f.app.canvas_epoch = f.app.canvas_epoch.wrapping_add(1);
}
}
delete(&mut mounted, &mut f.app, false).await;
assert_eq!(f.app.document.bundle(), &before.bundle);
assert!(f.app.project().contains(f.node));
}
let (mut f, mut mounted) = mount(Layer::Oa).await;
let before = f.app.document.snapshot();
mounted
.batch(
&mut f.app,
&[modified(
keyboard::key::Named::Delete,
keyboard::Modifiers::CTRL,
)],
mouse::Cursor::Unavailable,
)
.await;
assert_eq!(f.app.document.bundle(), &before.bundle);
assert!(!f.app.delete_pending);
// Refusal is temporary: a fresh live canvas click owns Delete again.
select(&mut mounted, &mut f).await;
delete(&mut mounted, &mut f.app, false).await;
assert!(!f.app.project().contains(f.node));
}
#[tokio::test]
async fn mounted_bend_delete_has_priority_and_semantic_edge_delete_is_confirmed() {
let mut p = Project::empty();
let a = p.add(Kind::Activity, "Send", 0., 0.);
let b = p.add(Kind::Activity, "Receive", 650., 0.);
let edge = p.relate(RelationKind::Interaction, "Report", a, b);
let diagram = p.ensure_diagram(Layer::Oa);
let p = execute_batch(
&p,
&[Command::SetEdgeRoute {
diagram,
route: EdgeRoute {
edge,
waypoints: vec![Position { x: 400., y: -100. }],
label: None,
},
}],
)
.unwrap();
let mut app = App::with_project(p);
app.page = Page::Diagram;
app.active_diagram = Some(diagram);
app.window_size = Size::new(2000., 1400.);
let _ = app.update(Message::Select(edge));
let mut mounted = Mounted::new(&mut app).await;
app.zoom = 1.;
app.pan = Vector::new(100., 250.);
app.fit_to_view = false;
mounted.settle(&mut app).await;
let bounds = mounted
.geometry(&app)
.bounds(crate::allocation_input::CANVAS_ID);
let pin = Point::new(bounds.x + 500., bounds.y + 150.);
mounted.click(&mut app, pin).await;
assert_eq!(
app.selected_bend,
Some((edge, 0)),
"real pointer selects route pin"
);
let before = app.document.snapshot();
let messages = delete(&mut mounted, &mut app, false).await;
assert!(messages.iter().any(|message| matches!(
message,
Message::Route(crate::route_edit::Event::RemoveBend)
)));
assert!(
!messages
.iter()
.any(|message| matches!(message, Message::DiagramRemove(_)))
);
assert!(app.project().contains(edge) && app.project().contains(a) && app.project().contains(b));
assert!(
crate::route_edit::stored_route(&app.project().diagrams[&diagram], edge)
.waypoints
.is_empty()
);
assert!(!app.delete_pending);
consistent(&app);
let task = app.update(Message::Undo);
mounted.perform(&mut app, task).await;
assert_eq!(app.document.bundle(), &before.bundle);
// Select a real edge segment away from its handle, then Delete opens the
// same semantic confirmation instead of inventing an edge hide operation.
let view = projection::project_view(app.project(), diagram);
let routed = routing::route_diagram(app.project(), &app.project().diagrams[&diagram], &view);
let route = &routed.edges[&edge];
let segment = route
.points
.windows(2)
.max_by(|a, b| {
let len = |s: &[Position]| (s[1].x - s[0].x).abs() + (s[1].y - s[0].y).abs();
len(a).total_cmp(&len(b))
})
.unwrap();
let at = Point::new(
bounds.x + (segment[0].x + segment[1].x) / 2. + app.pan.x,
bounds.y + (segment[0].y + segment[1].y) / 2. + app.pan.y,
);
mounted.click(&mut app, at).await;
assert_eq!(app.selected, edge);
assert!(app.selected_bend.is_none());
delete(&mut mounted, &mut app, false).await;
assert_eq!(
app.delete_target.as_ref().unwrap().snapshot.request.object,
edge
);
assert_eq!(app.document.bundle(), &before.bundle);
}
#[tokio::test]
async fn mounted_state_delete_uses_state_placement_and_preserves_architecture_placement() {
let mut p = Project::empty();
let machine = p
.add_element(Kind::StateMachine, Layer::Sa, "Machine", None)
.unwrap();
let region = p
.add_element(Kind::Region, Layer::Sa, "Main", Some(machine))
.unwrap();
let state = p
.add_element(Kind::State, Layer::Sa, "Ready", Some(region))
.unwrap();
let architecture = p.ensure_diagram(Layer::Sa);
let architecture_node = p.place(architecture, state, Position { x: 50., y: 50. });
let plan = crate::behavior::state_edit::create_view(&p, machine).unwrap();
let diagram = plan.diagram;
let p = execute_batch(&p, &[plan.command]).unwrap();
let node = p.diagrams[&diagram]
.nodes
.values()
.find(|node| node.element == state)
.unwrap()
.id;
let mut app = App::with_project(p);
app.layer = Layer::Sa;
app.page = Page::Behavior;
app.active_diagram = Some(architecture);
app.behavior.active_owner = Some(machine);
app.behavior.diagram.diagram = Some(diagram);
app.window_size = Size::new(2000., 1400.);
let _ = app.update(Message::Select(state));
let mut mounted = Mounted::new(&mut app).await;
let bounds = mounted
.geometry(&app)
.focused
.into_iter()
.find(|(id, _, _)| {
id.as_ref() == Some(&iced::advanced::widget::Id::from("diagram-keyboard-owner"))
})
.unwrap()
.1;
let projected = projection::project_view(app.project(), diagram)
.nodes
.into_iter()
.find(|n| n.id == node)
.unwrap();
let at = Point::new(
bounds.x
+ (projected.position.x + 35.) * app.behavior.diagram.zoom
+ app.behavior.diagram.pan.x,
bounds.y
+ (projected.position.y + 35.) * app.behavior.diagram.zoom
+ app.behavior.diagram.pan.y,
);
mounted.click(&mut app, at).await;
assert_eq!(app.selected, state);
assert_eq!(app.behavior.diagram.placement, Some(node));
let before = app.document.snapshot();
let messages = delete(&mut mounted, &mut app, false).await;
assert!(messages.iter().any(|message| matches!(
message,
Message::StateDiagram(crate::behavior::state_edit::Event::Remove(_))
)));
assert!(!app.project().contains(node));
assert!(app.project().contains(architecture_node) && app.project().contains(state));
consistent(&app);
let task = app.update(Message::Undo);
mounted.perform(&mut app, task).await;
assert_eq!(app.document.bundle(), &before.bundle);
}
#[tokio::test]
async fn captured_confirmation_rejects_selection_document_revision_and_placement_changes() {
for boundary in 0..5 {
let (mut f, mut mounted) = mount(Layer::Oa).await;
delete(&mut mounted, &mut f.app, true).await;
let token = f.app.delete_target.as_ref().unwrap().snapshot.token;
// Model asynchronous adoption / retained stale UI using a changed App
// state while preserving the old confirmation payload deliberately.
match boundary {
0 => {
f.app.selected = f.other;
}
1 => {
f.app.canvas_document = Id::new_v4();
}
2 => {
f.app.canvas_epoch = f.app.canvas_epoch.wrapping_add(1);
}
3 => {
f.app.selected_node = None;
}
_ => {
assert!(
f.app
.document
.execute(Command::Rename(f.other, "New revision".into()))
.is_ok()
);
}
}
let before = f.app.document.snapshot();
let task = f.app.update(Message::ConfirmDelete(token));
mounted.perform(&mut f.app, task).await;
assert_eq!(f.app.document.bundle(), &before.bundle);
assert!(f.app.project().contains(f.object));
assert!(!f.app.delete_pending);
}
}
#[tokio::test]
async fn mounted_programmatic_canvas_entry_needs_no_mouse_focus_boolean() {
let mut f = fixture(Layer::Oa);
let _ = f.app.update(Message::Select(f.object));
let mut mounted = Mounted::new(&mut f.app).await;
let task =
iced::widget::operation::focus(iced::advanced::widget::Id::from("diagram-keyboard-owner"));
mounted.perform(&mut f.app, task).await;
let messages = delete(&mut mounted, &mut f.app, false).await;
assert!(
messages
.iter()
.any(|message| matches!(message, Message::DiagramRemove(_)))
);
assert!(!f.app.project().contains(f.node));
assert!(f.app.project().contains(f.object));
}
#[tokio::test]
async fn mounted_allocation_work_click_then_delete_without_redraw_removes_placement_once() {
for layer in [Layer::Oa, Layer::Sa] {
let mut p = Project::empty();
let performer = p
.add_element(
if layer == Layer::Oa {
Kind::Entity
} else {
Kind::System
},
layer,
"Performer",
None,
)
.unwrap();
let work = p
.add_element(
if layer == Layer::Oa {
Kind::Activity
} else {
Kind::Function
},
layer,
"Work",
None,
)
.unwrap();
let diagram = p.ensure_diagram(layer);
p.diagrams.get_mut(&diagram).unwrap().projection = ProjectionKind::Allocation;
p.place(diagram, performer, Position { x: 100., y: 100. });
let node = p.place(diagram, work, Position { x: 100., y: 500. });
let mut app = App::with_project(p);
app.page = Page::Diagram;
app.layer = layer;
app.active_diagram = Some(diagram);
app.window_size = Size::new(2000., 1400.);
let mut mounted = Mounted::new(&mut app).await;
app.zoom = 1.;
app.pan = Vector::ZERO;
app.fit_to_view = false;
mounted.settle(&mut app).await;
let bounds = mounted
.geometry(&app)
.bounds(crate::allocation_input::CANVAS_ID);
let point = Point::new(bounds.x + 140., bounds.y + 535.);
let messages = mounted.click(&mut app, point).await;
assert!(messages.iter().any(|message| matches!(
message,
Message::WorkDrag(crate::app::allocation_drag::Event::Press(_))
)));
assert_eq!(app.selected, work);
assert_eq!(app.selected_node, Some(node));
let before = app.document.snapshot();
// Deliberately do not settle/redraw between completed click and key.
let messages = delete(&mut mounted, &mut app, false).await;
assert!(
messages
.iter()
.any(|message| matches!(message, Message::DiagramRemove(_))),
"retired allocation press cannot consume first Delete"
);
assert!(!app.project().contains(node));
assert!(app.project().contains(work) && app.project().contains(performer));
consistent(&app);
let task = app.update(Message::Undo);
mounted.perform(&mut app, task).await;
assert_eq!(app.document.bundle(), &before.bundle);
assert!(!app.document.can_undo());
}
}
@@ -114,6 +114,7 @@ impl App {
}
}
match event {
Event::Remove(intent) => self.remove_from_diagram(intent),
Event::Viewport(_, size) => {
if size.width.is_finite()
&& size.height.is_finite()