Compose compact Text workspace with guarded root navigation
This commit is contained in:
@@ -0,0 +1,301 @@
|
||||
//! 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;
|
||||
Reference in New Issue
Block a user