302 lines
10 KiB
Rust
302 lines
10 KiB
Rust
//! 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<Element<'a, Message, Theme, Renderer>>,
|
|
) -> 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<Self> {
|
|
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<Self> {
|
|
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<Pointer>,
|
|
swallowed: Vec<Pointer>,
|
|
}
|
|
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<Message, Theme, Renderer: renderer::Renderer> Widget<Message, Theme, Renderer>
|
|
for Guard<'_, Message, Theme, Renderer>
|
|
{
|
|
fn tag(&self) -> widget::tree::Tag {
|
|
widget::tree::Tag::of::<State>()
|
|
}
|
|
fn state(&self) -> widget::tree::State {
|
|
widget::tree::State::new(State {
|
|
identity: self.identity,
|
|
pressed: None,
|
|
swallowed: Vec::new(),
|
|
})
|
|
}
|
|
fn children(&self) -> Vec<widget::Tree> {
|
|
vec![widget::Tree::new(&self.content)]
|
|
}
|
|
fn diff(&self, tree: &mut widget::Tree) {
|
|
let state = tree.state.downcast_mut::<State>();
|
|
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<Length> {
|
|
self.content.as_widget().size()
|
|
}
|
|
fn size_hint(&self) -> Size<Length> {
|
|
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::<State>();
|
|
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<overlay::Element<'b, Message, Theme, Renderer>> {
|
|
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<Guard<'a, Message, Theme, Renderer>> for Element<'a, Message, Theme, Renderer>
|
|
{
|
|
fn from(guard: Guard<'a, Message, Theme, Renderer>) -> Self {
|
|
Element::new(guard)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests;
|