//! State selection layout and modifier admission on the mounted production App. use super::*; use crate::behavior::{ canvas_workspace::tests::{Mounted, key}, state_edit, }; use crate::diagram_usage::Target; use iced::advanced::widget::{Operation, operation}; use iced::{Point, Rectangle, Size, Vector, keyboard, mouse}; struct Fixture { app: App, diagram: Id, a: Id, b: Id, a_element: Id, b_element: Id, edge: Id, action: Id, } fn fixture() -> Fixture { let mut p = Project::empty(); let machine = p .add_element(Kind::StateMachine, Layer::Oa, "Operating modes", None) .unwrap(); let region = p .add_element(Kind::Region, Layer::Oa, "Main region", Some(machine)) .unwrap(); let a_element = p .add_element(Kind::State, Layer::Oa, "First state", Some(region)) .unwrap(); let b_element = p .add_element( Kind::State, Layer::Oa, &format!("Second state {}", "with a long primary name ".repeat(20)), Some(region), ) .unwrap(); let work = p .add_element(Kind::Activity, Layer::Oa, "Reusable work", None) .unwrap(); let action = p.relate(RelationKind::StateAction, "On entry", a_element, work); let edge = p.relate( RelationKind::StateTransition, "Next state", a_element, b_element, ); let diagram = p.ensure_diagram(Layer::Oa); p.diagrams.get_mut(&diagram).unwrap().projection = ProjectionKind::StateMachine { machine }; let mut placements = vec![]; // All State elements, including machine/Region, are initially placed. // A fixture already missing its containers would not test picker insertion. for (element, x, y, width, height) in [ (machine, 0., 0., 800., 500.), (region, 20., 70., 760., 410.), (a_element, 60., 160., 230., 180.), (b_element, 430., 160., 260., 180.), ] { let node = p.place(diagram, element, Position { x, y }); p.diagrams .get_mut(&diagram) .unwrap() .nodes .get_mut(&node) .unwrap() .size = oa_core::Size { width, height }; placements.push(node); } let a = placements[2]; let b = placements[3]; validate_candidate(&p).unwrap(); assert!(state_edit::unplaced(&p, diagram).is_empty()); let mut app = App::with_project(p); app.page = Page::Behavior; app.layer = Layer::Oa; app.behavior.active_owner = Some(machine); app.behavior.diagram = crate::behavior::state_view::State { diagram: Some(diagram), fit: true, ..Default::default() }; app.selected = diagram; app.selected_node = None; app.window_size = Size::new(1442., 932.); app.refresh_draft(); app.reconcile_multiselect(); assert_eq!(app.state_context().unwrap().diagram, diagram); Fixture { app, diagram, a, b, a_element, b_element, edge, action, } } fn owner(mounted: &mut Mounted, app: &App) -> (Rectangle, bool) { let geometry = mounted.geometry(app); let (_, bounds, focused) = geometry .focused .into_iter() .find(|(id, _, _)| { id.as_ref() == Some(&iced::advanced::widget::Id::from("diagram-keyboard-owner")) }) .expect("real State focus::Owner rectangle must be measurable"); (bounds, focused) } fn camera(app: &App) -> (f32, Vector, bool, Option) { let state = &app.behavior.diagram; (state.zoom, state.pan, state.fit, state.viewport) } async fn selection(mounted: &mut Mounted, app: &mut App, target: Option) { 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(app, task).await; mounted.settle(app).await; } fn node_point(mounted: &mut Mounted, f: &Fixture, placement: Id) -> Point { let bounds = owner(mounted, &f.app).0; let node = &f.app.project().diagrams[&f.diagram].nodes[&placement]; let state = &f.app.behavior.diagram; Point::new( bounds.x + (node.position.x + 40.) * state.zoom + state.pan.x, bounds.y + (node.position.y + 25.) * state.zoom + state.pan.y, ) } fn modifiers(shift: bool) -> iced::Event { iced::Event::Keyboard(keyboard::Event::ModifiersChanged(if shift { keyboard::Modifiers::SHIFT } else { keyboard::Modifiers::default() })) } 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(); } } } #[tokio::test] async fn mounted_state_selection_and_missing_picker_never_resize_or_refit_the_canvas() { let mut f = fixture(); let mut mounted = Mounted::new(&mut f.app).await; let baseline = f.app.document.snapshot(); let bounds = owner(&mut mounted, &f.app).0; let fitted = camera(&f.app); assert!( fitted.2, "keep Fit active to expose any resize-triggered camera jump" ); assert!(fitted.3.is_some()); assert!(bounds.width > 400. && bounds.height > 200.); assert!(state_edit::unplaced(f.app.project(), f.diagram).is_empty()); assert!(f.app.multiselect_snapshot().unwrap().targets().is_empty()); for (target, expected) in [ (Some(Target::Placement(f.a)), vec![Target::Placement(f.a)]), ( Some(Target::Placement(f.b)), vec![Target::Placement(f.a), Target::Placement(f.b)], ), ( Some(Target::Edge(f.edge)), vec![ Target::Placement(f.a), Target::Placement(f.b), Target::Edge(f.edge), ], ), ( Some(Target::Edge(f.edge)), vec![Target::Placement(f.a), Target::Placement(f.b)], ), ( Some(Target::StateRow { placement: f.a, relation: f.action, }), vec![ Target::Placement(f.a), Target::Placement(f.b), Target::StateRow { placement: f.a, relation: f.action, }, ], ), (None, vec![]), ] { selection(&mut mounted, &mut f.app, target).await; assert_eq!(f.app.multiselect_snapshot().unwrap().targets(), expected); assert_eq!( owner(&mut mounted, &f.app).0, bounds, "selection strip changed the measured State canvas rectangle" ); assert_eq!( camera(&f.app), fitted, "selection feedback silently refitted the camera" ); assert_eq!(f.app.project(), &baseline.decoded.project); assert_eq!(f.app.document.bundle(), &baseline.bundle); assert!(!f.app.document.can_undo()); assert!(!f.app.draft_dirty()); } selection(&mut mounted, &mut f.app, Some(Target::Placement(f.a))).await; selection(&mut mounted, &mut f.app, Some(Target::Placement(f.b))).await; let captured = f.app.multiselect_snapshot().unwrap(); let task = f .app .update(Message::DiagramSelection(multiselect::Event::Hide( captured, ))); mounted.perform(&mut f.app, task).await; mounted.settle(&mut f.app).await; let missing: std::collections::BTreeSet<_> = state_edit::unplaced(f.app.project(), f.diagram) .into_iter() .collect(); assert_eq!( missing, std::collections::BTreeSet::from([f.a_element, f.b_element]) ); let mut expected = baseline.decoded.project.clone(); expected .diagrams .get_mut(&f.diagram) .unwrap() .nodes .remove(&f.a); expected .diagrams .get_mut(&f.diagram) .unwrap() .nodes .remove(&f.b); assert_eq!(f.app.project(), &expected); assert_eq!( owner(&mut mounted, &f.app).0, bounds, "the first unplaced picker changed canvas bounds" ); assert_eq!(camera(&f.app), fitted); let hidden_source = f.app.document.bundle().clone(); let task = f.app.update(Message::Undo); mounted.perform(&mut f.app, task).await; mounted.settle(&mut f.app).await; assert!(state_edit::unplaced(f.app.project(), f.diagram).is_empty()); assert_eq!(f.app.project(), &baseline.decoded.project); assert_eq!(f.app.document.bundle(), &baseline.bundle); assert!(!f.app.document.can_undo()); assert_eq!( owner(&mut mounted, &f.app).0, bounds, "removing the unplaced picker changed canvas bounds" ); assert_eq!(camera(&f.app), fitted); let task = f.app.update(Message::Redo); mounted.perform(&mut f.app, task).await; mounted.settle(&mut f.app).await; assert_eq!(f.app.project(), &expected); assert_eq!(f.app.document.bundle(), &hidden_source); assert_eq!(owner(&mut mounted, &f.app).0, bounds); assert_eq!(camera(&f.app), fitted); } #[tokio::test] async fn mounted_state_shift_held_in_clean_inspector_toggles_on_first_canvas_press() { let mut f = fixture(); let mut mounted = Mounted::new(&mut f.app).await; selection(&mut mounted, &mut f.app, Some(Target::Placement(f.a))).await; let field = mounted .geometry(&f.app) .text_inputs .into_iter() .filter(|bounds| bounds.x > f.app.window_size.width - 320.) .min_by(|a, b| a.y.total_cmp(&b.y)) .expect("State Inspector field is mounted"); mounted.operate(&f.app, &mut FocusField(field)); assert!(!owner(&mut mounted, &f.app).1); let before = f.app.document.snapshot(); mounted .batch(&mut f.app, &[modifiers(true)], mouse::Cursor::Unavailable) .await; assert!( !owner(&mut mounted, &f.app).1, "passive Shift must not acquire State canvas focus" ); let at = node_point(&mut mounted, &f, f.b); let messages = mounted.click(&mut f.app, at).await; assert!(messages.iter().any(|message| matches!(message, Message::DiagramSelection(multiselect::Event::Choose(_, Target::Placement(id), multiselect::Choice::Toggle)) if *id == f.b )), "StateCanvas must map its typed selection event to the common App boundary"); assert_eq!( f.app.multiselect_snapshot().unwrap().targets(), &[Target::Placement(f.a), Target::Placement(f.b)] ); assert_eq!(f.app.behavior.diagram.placement, Some(f.b)); assert_eq!(f.app.selected_node, None); assert_eq!(f.app.project(), &before.decoded.project); assert_eq!(f.app.document.bundle(), &before.bundle); assert!(!f.app.document.can_undo()); assert!(!f.app.draft_dirty()); } #[tokio::test] async fn mounted_state_busy_suspension_tracks_shift_without_granting_focus_or_commands() { let mut f = fixture(); let mut mounted = Mounted::new(&mut f.app).await; selection(&mut mounted, &mut f.app, Some(Target::Placement(f.a))).await; // Explicitly revoke focus rather than relying on incidental initial focus. mounted.operate( &f.app, &mut FocusField(Rectangle { x: -1000., y: -1000., width: 1., height: 1., }), ); let before = f.app.document.snapshot(); let at = node_point(&mut mounted, &f, f.b); f.app.busy = true; mounted.settle(&mut f.app).await; mounted .batch( &mut f.app, &[modifiers(true), key(keyboard::key::Named::Delete)], mouse::Cursor::Unavailable, ) .await; mounted.click(&mut f.app, at).await; assert!(!owner(&mut mounted, &f.app).1); assert_eq!( f.app.multiselect_snapshot().unwrap().targets(), &[Target::Placement(f.a)] ); assert_eq!(f.app.document.bundle(), &before.bundle); f.app.busy = false; mounted.settle(&mut f.app).await; assert!(!owner(&mut mounted, &f.app).1); // No second modifier event: the passive suspended update must survive. mounted.click(&mut f.app, at).await; assert_eq!( f.app.multiselect_snapshot().unwrap().targets(), &[Target::Placement(f.a), Target::Placement(f.b)] ); assert_eq!(f.app.project(), &before.decoded.project); assert_eq!(f.app.document.bundle(), &before.bundle); assert!(!f.app.document.can_undo()); assert!(!f.app.delete_pending); }