//! A narrow dispatch boundary for Allocation work gestures. Iced rows visit //! siblings after capture; refusing inside Canvas alone still blurs an inspector //! field. Refuse before dispatching that press to any ordinary child instead. use crate::{ app::{ App, Message, allocation_drag::{self, Event as WorkEvent}, }, diagram::DiagramCanvas, }; use iced::advanced::{ Clipboard, Layout, Shell, Widget, layout, overlay, renderer, widget::{Id, Operation, Tree, tree}, }; use iced::{ Element, Event, Length, Point, Rectangle, Renderer, Size, Theme, Vector, keyboard, mouse, }; pub const CANVAS_ID: &str = "allocation-work-canvas"; pub const ARRANGE_SHORTCUT: &str = "Alt+A"; struct Root<'a> { app: &'a App, content: Element<'a, Message>, } #[derive(Default)] struct State { pointer: Option, refused_left: bool, } #[derive(Default)] struct CanvasBounds(Option); impl Operation for CanvasBounds { fn traverse(&mut self, visit: &mut dyn FnMut(&mut dyn Operation)) { visit(self); } fn container(&mut self, id: Option<&Id>, bounds: Rectangle) { if id == Some(&Id::new(CANVAS_ID)) { self.0 = Some(bounds); } } } pub fn root<'a>(app: &'a App, content: impl Into>) -> Element<'a, Message> { Element::new(Root { app, content: content.into(), }) } fn arrange_key(event: &Event) -> bool { matches!(event, Event::Keyboard(keyboard::Event::KeyPressed { key: keyboard::Key::Character(key), modifiers, repeat: false, .. }) if key.eq_ignore_ascii_case("a") && *modifiers == keyboard::Modifiers::ALT) } impl Root<'_> { fn dirty_object(&self, bounds: Rectangle, screen: Point) -> bool { let Some(diagram) = self.app.active_diagram else { return false; }; if !allocation_drag::contains(bounds, screen) { return false; } let canvas = DiagramCanvas { allocation: Some(self.app.allocation_capability()), project: self.app.project(), view_index: Some(self.app.document.view_index()), diagram, selected: self.app.selected, navigation_focus: None, zoom: self.app.zoom, pan: self.app.pan, context: self.app.route_context().unwrap(), selected_bend: None, editable: false, placing: false, connection: None, }; canvas.work_dirty_point(bounds, screen) } } impl Widget for Root<'_> { fn tag(&self) -> tree::Tag { tree::Tag::of::() } fn state(&self) -> tree::State { tree::State::new(State::default()) } fn children(&self) -> Vec { vec![Tree::new(self.content.as_widget())] } fn diff(&self, tree: &mut Tree) { tree.diff_children(std::slice::from_ref(&self.content)); } fn size(&self) -> Size { self.content.as_widget().size() } fn layout( &mut self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { self.content .as_widget_mut() .layout(&mut tree.children[0], renderer, limits) } fn operate( &mut self, tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, operation: &mut dyn Operation, ) { self.content .as_widget_mut() .operate(&mut tree.children[0], layout, renderer, operation); } fn update( &mut self, tree: &mut Tree, event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, ) { let state = tree.state.downcast_mut::(); match event { Event::Mouse(mouse::Event::CursorMoved { position }) => state.pointer = Some(*position), Event::Mouse(mouse::Event::CursorLeft) => state.pointer = None, Event::Window(iced::window::Event::RedrawRequested(_)) => { state.pointer = cursor.position() } Event::Window(iced::window::Event::Unfocused) => { state.pointer = None; state.refused_left = false; } _ => {} } if state.refused_left { if matches!( event, Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) ) { state.refused_left = false; shell.capture_event(); return; } if matches!(event, Event::Mouse(mouse::Event::CursorMoved { .. })) { shell.capture_event(); return; } } if self.app.allocation_input_owner() && !shell.is_event_captured() { if arrange_key(event) { // App rechecks clean ownership before arming. A dirty field // retains its actual widget focus and selection on refusal. shell.publish(Message::WorkDrag(WorkEvent::ArrangeNext)); shell.capture_event(); return; } if self.app.allocation_drag.arrange_next && matches!( event, Event::Keyboard(keyboard::Event::KeyPressed { key: keyboard::Key::Named(keyboard::key::Named::Escape), .. }) ) { shell.publish(Message::Escape); shell.capture_event(); return; } if !self.app.allocation_drag_clean() && matches!( event, Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) ) && let Some(screen) = state.pointer.or_else(|| cursor.position()) { let mut canvas = CanvasBounds::default(); self.content.as_widget_mut().operate( &mut tree.children[0], layout, renderer, &mut canvas, ); if canvas .0 .is_some_and(|bounds| self.dirty_object(bounds, screen)) { state.refused_left = true; shell.publish(Message::WorkDrag(WorkEvent::Refused( self.app.allocation_drag.epoch, allocation_drag::FINISH_EDIT.into(), ))); shell.capture_event(); return; } } } self.content.as_widget_mut().update( &mut tree.children[0], event, layout, cursor, renderer, clipboard, shell, viewport, ); } fn draw( &self, tree: &Tree, renderer: &mut Renderer, theme: &Theme, style: &renderer::Style, layout: Layout<'_>, cursor: mouse::Cursor, viewport: &Rectangle, ) { self.content.as_widget().draw( &tree.children[0], renderer, theme, style, layout, cursor, viewport, ); } fn mouse_interaction( &self, tree: &Tree, layout: Layout<'_>, cursor: mouse::Cursor, viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { self.content.as_widget().mouse_interaction( &tree.children[0], layout, cursor, viewport, renderer, ) } fn overlay<'b>( &'b mut self, tree: &'b mut Tree, layout: Layout<'b>, renderer: &Renderer, viewport: &Rectangle, translation: Vector, ) -> Option> { self.content.as_widget_mut().overlay( &mut tree.children[0], layout, renderer, viewport, translation, ) } }