//! Production App tree and its actual keyboard subscription, without a canvas click. use super::*; use crate::behavior::canvas_workspace::tests::{Mounted, key}; use iced::{ advanced::subscription, futures::{StreamExt, stream, task::noop_waker_ref}, keyboard::key::Named, mouse, }; use std::task::{Context as PollContext, Poll}; async fn press(mounted: &mut Mounted, app: &mut App, named: Named) -> Vec { let input = key(named); let (mut messages, statuses) = mounted.input_with_status( app, std::slice::from_ref(&input), mouse::Cursor::Unavailable, ); let interaction = subscription::Event::Interaction { window: iced::window::Id::unique(), event: input, status: statuses[0], }; for recipe in subscription::into_recipes(app.subscription()) { let mut output = recipe.stream(Box::pin(stream::iter([interaction.clone()]))); let mut cx = PollContext::from_waker(noop_waker_ref()); while let Poll::Ready(Some(message)) = output.poll_next_unpin(&mut cx) { messages.push(message); } } for message in messages.iter().cloned() { let task = app.update(message); mounted.perform(app, task).await; } mounted.settle(app).await; messages } async fn armed() -> (App, Mounted, Id) { let mut f = fixture(); let diagram = create_view(&mut f).diagram; let mut project = f.app.project().clone(); let relation = project.relate(RelationKind::StateTransition, "Switch", f.a, f.b); let mut app = App::with_project(project); app.window_size = iced::Size::new(1800., 1100.); send(&mut app, Message::RevealInDiagram(diagram, relation)); let mut mounted = Mounted::new(&mut app).await; app.behavior.diagram.fit = false; let context = app.state_context().unwrap(); event( &mut app, Event::ContextMenu { context, object: relation, placement: None, selection_target: None, anchor: iced::Point::new(450., 400.), bend: None, route_segment: None, }, ); mounted.settle(&mut app).await; let token = app.model_menu.as_ref().unwrap().token; send( &mut app, Message::Menu(crate::model_menu::Event::Invoke( token, Box::new(Message::StateDiagram(Event::Choose( Tool::AdjustAttachment { relation, end: EdgeEnd::Source, }, ))), )), ); mounted.settle(&mut app).await; assert!(app.model_menu.is_none()); assert_eq!( app.behavior.diagram.tool, Tool::AdjustAttachment { relation, end: EdgeEnd::Source } ); (app, mounted, relation) } fn canvas_focus(mounted: &mut Mounted, app: &App) -> (iced::Rectangle, bool) { let id = iced::advanced::widget::Id::from("diagram-keyboard-owner"); let (_, bounds, focused) = mounted .geometry(app) .focused .into_iter() .find(|(candidate, _, _)| candidate.as_ref() == Some(&id)) .unwrap(); (bounds, focused) } #[tokio::test] async fn mounted_menu_armed_attachment_escape_without_canvas_click_preserves_document_and_focus() { let (mut app, mut mounted, _) = armed().await; let before = app.document.snapshot(); let history = (app.document.can_undo(), app.document.can_redo()); let camera = ( app.behavior.diagram.zoom, app.behavior.diagram.pan, app.behavior.diagram.fit, ); assert!(!canvas_focus(&mut mounted, &app).1); let messages = press(&mut mounted, &mut app, Named::Escape).await; assert!(messages.iter().any(|m| matches!(m, Message::Escape))); assert_eq!(app.behavior.diagram.tool, Tool::Select); assert!(!canvas_focus(&mut mounted, &app).1); press(&mut mounted, &mut app, Named::Delete).await; assert!( !app.delete_pending, "Escape must not grant canvas Delete ownership" ); assert!(Arc::ptr_eq(&before, &app.document.snapshot())); assert_eq!(app.document.bundle(), &before.bundle); assert_eq!(app.project(), &before.decoded.project); assert_eq!((app.document.can_undo(), app.document.can_redo()), history); assert_eq!( ( app.behavior.diagram.zoom, app.behavior.diagram.pan, app.behavior.diagram.fit ), camera ); } #[tokio::test] async fn mounted_attachment_escape_preserves_modal_priority_and_focused_canvas_control() { let (mut app, mut mounted, _) = armed().await; let before = app.document.snapshot(); // Existing modal Escape consumes the first key without canceling the tool underneath. send(&mut app, Message::Delete); assert!(app.delete_pending && app.delete_target.is_some()); mounted.settle(&mut app).await; press(&mut mounted, &mut app, Named::Escape).await; assert!(!app.delete_pending); assert!(matches!( app.behavior.diagram.tool, Tool::AdjustAttachment { .. } )); // A real canvas click grants ownership; inner-canvas Escape remains live. let bounds = canvas_focus(&mut mounted, &app).0; mounted .click(&mut app, iced::Point::new(bounds.x + 10., bounds.y + 10.)) .await; assert!(canvas_focus(&mut mounted, &app).1); let messages = press(&mut mounted, &mut app, Named::Escape).await; assert!( messages .iter() .any(|m| matches!(m, Message::StateDiagram(Event::Choose(Tool::Select)))) ); assert_eq!(app.behavior.diagram.tool, Tool::Select); assert!(Arc::ptr_eq(&before, &app.document.snapshot())); }