//! Publication-and-visit-bound input for a mounted diagnostic list. //! //! Keep this wrapper at the same widget-tree position, inside the list's outer //! scroller. Pass `None` with an empty child while hidden/removed; unmounting the //! wrapper loses its ability to consume an outstanding release. Every change to //! row identity/order/action must have a fresh publication Id. Pass the pair //! (publication generation, drawer visit): a new visit resets armed controls even //! if Hide and Show coalesce without an intermediate hidden Tree. The child tree //! resets when either component changes, clearing button presses and row focus; //! the outer scroller and editor trees are not reset. //! //! Capturing stops Iced event bubbling and later consumers that respect capture. //! It cannot undo an earlier sibling's event handling. This is a row-list guard, //! not a root event filter or a guard for actions in child overlays. Diagnostic //! messages still need publication/snapshot and drawer-visit validation at dispatch. use iced::advanced::{Clipboard, Layout, Shell, Widget, layout, overlay, renderer, widget}; use iced::{Element, Event, Length, Rectangle, Size, Vector, mouse, touch}; use oa_core::Id; pub struct Guard<'a, Message, Theme = iced::Theme, Renderer = iced::Renderer> { identity: Option<(Id, Id)>, content: Element<'a, Message, Theme, Renderer>, } impl<'a, Message, Theme, Renderer> Guard<'a, Message, Theme, Renderer> { pub fn new( identity: Option<(Id, Id)>, content: impl Into>, ) -> Self { Self { identity, content: content.into(), } } } #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum Pointer { Mouse, Touch(touch::Finger), } impl Pointer { fn press(event: &Event) -> Option { match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => Some(Self::Mouse), Event::Touch(touch::Event::FingerPressed { id, .. }) => Some(Self::Touch(*id)), _ => None, } } fn release(event: &Event) -> Option { match event { Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => Some(Self::Mouse), Event::Touch( touch::Event::FingerLifted { id, .. } | touch::Event::FingerLost { id, .. }, ) => Some(Self::Touch(*id)), _ => None, } } } struct State { identity: Option<(Id, Id)>, pressed: Option, swallowed: Vec, } impl State { fn quarantine(&mut self, pointer: Pointer) { if !self.swallowed.contains(&pointer) { self.swallowed.push(pointer); } } fn forget(&mut self, pointer: Pointer) -> bool { let found = self.swallowed.contains(&pointer); self.swallowed.retain(|item| *item != pointer); found } } impl Widget for Guard<'_, Message, Theme, Renderer> { fn tag(&self) -> widget::tree::Tag { widget::tree::Tag::of::() } fn state(&self) -> widget::tree::State { widget::tree::State::new(State { identity: self.identity, pressed: None, swallowed: Vec::new(), }) } fn children(&self) -> Vec { vec![widget::Tree::new(&self.content)] } fn diff(&self, tree: &mut widget::Tree) { let state = tree.state.downcast_mut::(); if state.identity != self.identity { state.identity = self.identity; if let Some(pointer) = state.pressed.take() { state.quarantine(pointer); } tree.children = self.children(); } else { tree.diff_children(std::slice::from_ref(&self.content)); } } fn size(&self) -> Size { self.content.as_widget().size() } fn size_hint(&self) -> Size { self.content.as_widget().size_hint() } fn layout( &mut self, tree: &mut widget::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 widget::Tree, layout: Layout<'_>, renderer: &Renderer, operation: &mut dyn widget::Operation, ) { self.content .as_widget_mut() .operate(&mut tree.children[0], layout, renderer, operation); } fn update( &mut self, tree: &mut widget::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::(); let release = Pointer::release(event); if let Some(pointer) = release { // Bounds may have become empty or moved. Consume the physical // gesture's terminal event regardless of its current position. if state.forget(pointer) { shell.capture_event(); return; } // Iced Button has a single is_pressed bit, not a pointer identity. // A different finger/mouse release must not activate that button. if state.pressed.is_some_and(|pressed| pressed != pointer) { shell.capture_event(); return; } } let press = Pointer::press(event); if let Some(pointer) = state.pressed.filter(|pointer| { press == Some(*pointer) || (release == Some(*pointer) && shell.is_event_captured()) }) { // Recover an omitted terminal event, or cancel one intercepted by // an earlier consumer. An off-target mouse release / lost touch // clears Iced Button's pressed state without rebuilding this // publication's tree (which would also discard row focus). state.pressed = None; let cancel = match pointer { Pointer::Mouse => Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)), Pointer::Touch(id) => Event::Touch(touch::Event::FingerLost { id, position: iced::Point::ORIGIN, }), }; let mut discarded = Vec::new(); let mut cancellation = Shell::new(&mut discarded); self.content.as_widget_mut().update( &mut tree.children[0], &cancel, layout, mouse::Cursor::Unavailable, renderer, clipboard, &mut cancellation, viewport, ); // Cancellation must not publish an action or capture the fresh // down. Preserve rendering requests from the state change only. shell.request_redraw(); if cancellation.is_layout_invalid() { shell.invalidate_layout(); } if cancellation.are_widgets_invalid() { shell.invalidate_widgets(); } } if shell.is_event_captured() || self.identity.is_none() { return; } if let Some(pointer) = press { // A fresh down starts a new gesture even if the platform omitted a // previous release (for example while the window lacked focus). state.forget(pointer); if let Some(pressed) = state.pressed { if pressed != pointer { state.quarantine(pointer); } shell.capture_event(); return; } } let was_pressed = release.is_some() && release == state.pressed; if was_pressed { state.pressed = None; } self.content.as_widget_mut().update( &mut tree.children[0], event, layout, cursor, renderer, clipboard, shell, viewport, ); if let Some(pointer) = press && shell.is_event_captured() { state.pressed = Some(pointer); } if was_pressed { // FingerLost cancels Iced Button state but does not itself capture. shell.capture_event(); } } fn draw( &self, tree: &widget::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: &widget::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 widget::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, ) } } impl<'a, Message: 'a, Theme: 'a, Renderer: renderer::Renderer + 'a> From> for Element<'a, Message, Theme, Renderer> { fn from(guard: Guard<'a, Message, Theme, Renderer>) -> Self { Element::new(guard) } } #[cfg(test)] mod tests;