Files
ARCADIA-Studio/crates/oa-desktop/src/diagnostic_input.rs
T

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;