diff --git a/masonry/src/contexts.rs b/masonry/src/contexts.rs index e6019c8be..b9f174182 100644 --- a/masonry/src/contexts.rs +++ b/masonry/src/contexts.rs @@ -16,7 +16,9 @@ use crate::text::TextBrush; use crate::text_helpers::{ImeChangeSignal, TextFieldRegistration}; use crate::tree_arena::ArenaMutChildren; use crate::widget::{WidgetMut, WidgetState}; -use crate::{AllowRawMut, CursorIcon, Insets, Point, Rect, Size, Widget, WidgetId, WidgetPod}; +use crate::{ + AllowRawMut, CursorIcon, Insets, Point, Rect, Size, TouchEvent, Widget, WidgetId, WidgetPod, +}; /// A macro for implementing methods on multiple contexts. /// @@ -591,6 +593,19 @@ impl EventCtx<'_> { self.global_state.pointer_capture_target = Some(self.widget_state.id); } + // TODO: process captures + // TODO: clean up captures + pub fn capture_touch(&mut self, event: &TouchEvent) { + debug_assert!( + self.allow_pointer_capture, + "Error in #{}: event does not allow pointer capture", + self.widget_id().to_raw(), + ); + self.global_state + .touch_capture_targets + .insert(event.state().id(), self.widget_id()); + } + pub fn release_pointer(&mut self) { self.global_state.pointer_capture_target = None; } diff --git a/masonry/src/event.rs b/masonry/src/event.rs index 36f0b501b..7bb3e734b 100644 --- a/masonry/src/event.rs +++ b/masonry/src/event.rs @@ -8,9 +8,13 @@ use crate::kurbo::Rect; // TODO - See issue https://github.com/linebender/xilem/issues/367 use crate::WidgetId; +use dpi::LogicalUnit; +use vello::kurbo::Vec2; + use std::path::PathBuf; +use std::time::Instant; -use winit::event::{Force, Ime, KeyEvent, Modifiers}; +use winit::event::{Force, Ime, KeyEvent, Modifiers, TouchPhase}; use winit::keyboard::ModifiersState; // TODO - Occluded(bool) event @@ -217,13 +221,6 @@ pub struct AccessEvent { pub data: Option, } -#[derive(Debug, Clone)] -pub enum PointerType { - Mouse, - Touch, - Pen, -} - #[derive(Debug, Clone)] pub struct PointerState { // TODO @@ -235,7 +232,89 @@ pub struct PointerState { pub count: u8, pub focus: bool, pub force: Option, - pub pointer_type: PointerType, +} + +#[derive(Debug, Clone)] +pub enum TouchEvent { + Start(TouchState), + Move(TouchState), + End(TouchState), + Cancel(TouchState), +} + +impl TouchEvent { + pub fn state(&self) -> &TouchState { + match self { + Self::Start(state) | Self::Move(state) | Self::End(state) | Self::Cancel(state) => { + state + } + } + } + + pub fn position(&self) -> LogicalPosition { + self.state().position() + } +} + +#[derive(Debug, Clone, Copy)] +pub struct TouchFrame { + pub time: Instant, + pub position: LogicalPosition, + pub force: Option, +} + +#[derive(Debug, Clone)] +pub struct TouchState { + id: u64, + frames: Vec, +} + +impl TouchState { + pub fn new(id: u64, frame: TouchFrame) -> Self { + Self { + id, + frames: Vec::from([frame]), + } + } + + pub fn push(&mut self, frame: TouchFrame) { + self.frames.push(frame); + } + + pub fn last_frame(&self) -> &TouchFrame { + self.frames + .last() + .expect("TouchState has at least one frame") + } + + pub fn first_frame(&self) -> &TouchFrame { + self.frames + .first() + .expect("TouchState has at least one frame") + } + + pub fn velocity(&self) -> (LogicalUnit, LogicalUnit) { + // TODO: impl + (0.0.into(), 0.0.into()) + } + + pub fn position(&self) -> LogicalPosition { + self.last_frame().position + } + + pub fn displacement(&self) -> Vec2 { + let LogicalPosition:: { x: ax, y: ay } = self.first_frame().position; + let LogicalPosition:: { x: bx, y: by } = self.last_frame().position; + Vec2::new(ax - bx, ay - by) + } + + pub fn force(&self) -> Option { + self.last_frame().force + } + + pub fn id(&self) -> u64 { + self.id + } } #[derive(Debug, Clone)] @@ -495,7 +574,6 @@ impl PointerState { count: 0, focus: false, force: None, - pointer_type: PointerType::Mouse, } } } diff --git a/masonry/src/event_loop_runner.rs b/masonry/src/event_loop_runner.rs index 3b37f3c42..0f15419c0 100644 --- a/masonry/src/event_loop_runner.rs +++ b/masonry/src/event_loop_runner.rs @@ -1,8 +1,10 @@ // Copyright 2024 the Xilem Authors // SPDX-License-Identifier: Apache-2.0 +use std::collections::BTreeMap; use std::num::NonZeroUsize; use std::sync::Arc; +use std::time::Instant; use accesskit_winit::Adapter; use tracing::{debug, warn}; @@ -21,9 +23,9 @@ use winit::window::{Window, WindowAttributes, WindowId}; use crate::app_driver::{AppDriver, DriverCtx}; use crate::dpi::LogicalPosition; -use crate::event::{PointerButton, PointerState, WindowEvent}; +use crate::event::{PointerButton, PointerState, TouchFrame, TouchState, WindowEvent}; use crate::render_root::{self, RenderRoot, WindowSizePolicy}; -use crate::{PointerEvent, TextEvent, Widget, WidgetId}; +use crate::{Handled, PointerEvent, TextEvent, TouchEvent, Widget, WidgetId}; #[derive(Debug)] pub enum MasonryUserEvent { @@ -74,6 +76,7 @@ pub struct MasonryState<'a> { render_cx: RenderContext, render_root: RenderRoot, pointer_state: PointerState, + touches: BTreeMap, renderer: Option, // TODO: Winit doesn't seem to let us create these proxies from within the loop // The reasons for this are unclear @@ -237,6 +240,7 @@ impl MasonryState<'_> { ), renderer: None, pointer_state: PointerState::empty(), + touches: Default::default(), proxy: event_loop.create_proxy(), window: WindowState::Uninitialized(window), @@ -516,10 +520,48 @@ impl MasonryState<'_> { location, phase, force, + id, .. }) => { // FIXME: This is naïve and should be refined for actual use. // It will also interact with gesture discrimination. + let frame = TouchFrame { + time: Instant::now(), + position: location.to_logical(window.scale_factor()), + force, + }; + let state = self + .touches + .entry(id) + .and_modify(|v| v.push(frame)) + .or_insert_with(|| TouchState::new(id, frame)); + + // Try to dispatch as touch + let handled = match phase { + winit::event::TouchPhase::Started => self + .render_root + .handle_touch_event(TouchEvent::Start(state.clone())), + winit::event::TouchPhase::Ended => match self.touches.remove_entry(&id) { + Some((_, state)) => self + .render_root + .handle_touch_event(TouchEvent::End(state.clone())), + _ => Handled::No, + }, + winit::event::TouchPhase::Moved => self + .render_root + .handle_touch_event(TouchEvent::Move(state.clone())), + winit::event::TouchPhase::Cancelled => match self.touches.remove_entry(&id) { + Some((_, state)) => self + .render_root + .handle_touch_event(TouchEvent::Cancel(state.clone())), + _ => Handled::No, + }, + }; + + if handled == Handled::Yes { + return; + } + self.pointer_state.physical_position = location; self.pointer_state.position = location.to_logical(window.scale_factor()); self.pointer_state.force = force; diff --git a/masonry/src/lib.rs b/masonry/src/lib.rs index 74eec6b5d..3fa55c7df 100644 --- a/masonry/src/lib.rs +++ b/masonry/src/lib.rs @@ -135,7 +135,7 @@ pub use contexts::{ }; pub use event::{ AccessEvent, InternalLifeCycle, LifeCycle, PointerButton, PointerEvent, PointerState, - StatusChange, TextEvent, WindowEvent, WindowTheme, + StatusChange, TextEvent, TouchEvent, WindowEvent, WindowTheme, }; pub use kurbo::{Affine, Insets, Point, Rect, Size, Vec2}; pub use parley::layout::Alignment as TextAlignment; diff --git a/masonry/src/passes/event.rs b/masonry/src/passes/event.rs index 872cc0583..4a94de838 100644 --- a/masonry/src/passes/event.rs +++ b/masonry/src/passes/event.rs @@ -8,7 +8,8 @@ use winit::keyboard::{KeyCode, PhysicalKey}; use crate::passes::merge_state_up; use crate::render_root::RenderRoot; use crate::{ - AccessEvent, EventCtx, Handled, PointerEvent, TextEvent, Widget, WidgetId, WidgetState, + AccessEvent, EventCtx, Handled, PointerEvent, TextEvent, TouchEvent, Widget, WidgetId, + WidgetState, }; fn get_target_widget( @@ -119,6 +120,58 @@ pub(crate) fn root_on_pointer_event( handled } +pub(crate) fn root_on_touch_event( + root: &mut RenderRoot, + root_state: &mut WidgetState, + event: &TouchEvent, +) -> Handled { + let pos = event.position(); + + // Descend from the root when dispatching touches + // to allow portals and other containers to process gestures + let mut is_handled = false; + if let Some(target_widget_id) = root + .get_root_widget() + .find_widget_at_pos((pos.x, pos.y).into()) + .map(|widget| widget.id()) + { + // println!("{:?}", root.widget_arena.path_of(target_widget_id)); + for widget_id in root.widget_arena.path_of(target_widget_id).iter().rev() { + let (widget_mut, state_mut) = root.widget_arena.get_pair_mut(*widget_id); + + let mut ctx = EventCtx { + global_state: &mut root.state, + widget_state: state_mut.item, + widget_state_children: state_mut.children, + widget_children: widget_mut.children, + allow_pointer_capture: matches!(event, TouchEvent::Start(..)), + is_handled: false, + request_pan_to_child: None, + }; + let widget = widget_mut.item; + + if !is_handled { + trace!( + "Widget '{}' #{} visited", + widget.short_type_name(), + widget_id.to_raw(), + ); + + widget.on_touch_event(&mut ctx, event); + is_handled = ctx.is_handled; + if is_handled { + break; + } + } + } + } + + // Pass root widget state to synthetic state create at beginning of pass + root_state.merge_up(root.widget_arena.get_state_mut(root.root.id()).item); + + Handled::from(is_handled) +} + pub(crate) fn root_on_text_event( root: &mut RenderRoot, root_state: &mut WidgetState, diff --git a/masonry/src/render_root.rs b/masonry/src/render_root.rs index b1a5f7313..945f3de66 100644 --- a/masonry/src/render_root.rs +++ b/masonry/src/render_root.rs @@ -1,7 +1,7 @@ // Copyright 2019 the Xilem Authors and the Druid Authors // SPDX-License-Identifier: Apache-2.0 -use std::collections::{HashMap, VecDeque}; +use std::collections::{BTreeMap, HashMap, VecDeque}; use accesskit::{ActionRequest, Tree, TreeUpdate}; use parley::fontique::{self, Collection, CollectionOptions}; @@ -18,10 +18,12 @@ use web_time::Instant; use crate::contexts::{LayoutCtx, LifeCycleCtx}; use crate::debug_logger::DebugLogger; use crate::dpi::{LogicalPosition, LogicalSize, PhysicalSize}; -use crate::event::{PointerEvent, TextEvent, WindowEvent}; +use crate::event::{PointerEvent, TextEvent, TouchState, WindowEvent}; use crate::passes::accessibility::root_accessibility; use crate::passes::compose::root_compose; -use crate::passes::event::{root_on_access_event, root_on_pointer_event, root_on_text_event}; +use crate::passes::event::{ + root_on_access_event, root_on_pointer_event, root_on_text_event, root_on_touch_event, +}; use crate::passes::mutate::{mutate_widget, run_mutate_pass}; use crate::passes::paint::root_paint; use crate::passes::update::{run_update_disabled_pass, run_update_pointer_pass}; @@ -30,8 +32,8 @@ use crate::tree_arena::TreeArena; use crate::widget::WidgetArena; use crate::widget::{WidgetMut, WidgetRef, WidgetState}; use crate::{ - AccessEvent, Action, BoxConstraints, CursorIcon, Handled, InternalLifeCycle, LifeCycle, Widget, - WidgetId, WidgetPod, + AccessEvent, Action, BoxConstraints, CursorIcon, Handled, InternalLifeCycle, LifeCycle, + TouchEvent, Widget, WidgetId, WidgetPod, }; // --- MARK: STRUCTS --- @@ -62,6 +64,7 @@ pub(crate) struct RenderRootState { pub(crate) next_focused_widget: Option, pub(crate) hovered_path: Vec, pub(crate) pointer_capture_target: Option, + pub(crate) touch_capture_targets: BTreeMap, pub(crate) cursor_icon: CursorIcon, pub(crate) font_context: FontContext, pub(crate) text_layout_context: LayoutContext, @@ -134,6 +137,7 @@ impl RenderRoot { next_focused_widget: None, hovered_path: Vec::new(), pointer_capture_target: None, + touch_capture_targets: Default::default(), cursor_icon: CursorIcon::Default, font_context: FontContext { collection: Collection::new(CollectionOptions { @@ -225,6 +229,10 @@ impl RenderRoot { self.root_on_pointer_event(event) } + pub fn handle_touch_event(&mut self, state: TouchEvent) -> Handled { + self.root_on_touch_event(state) + } + pub fn handle_text_event(&mut self, event: TextEvent) -> Handled { self.root_on_text_event(event) } @@ -393,6 +401,17 @@ impl RenderRoot { handled } + fn root_on_touch_event(&mut self, event: TouchEvent) -> Handled { + let mut dummy_state = WidgetState::synthetic(self.root.id(), self.get_kurbo_size()); + + let handled = root_on_touch_event(self, &mut dummy_state, &event); + + self.post_event_processing(&mut dummy_state); + self.get_root_widget().debug_validate(false); + + handled + } + // --- MARK: TEXT_EVENT --- fn root_on_text_event(&mut self, event: TextEvent) -> Handled { let mut dummy_state = WidgetState::synthetic(self.root.id(), self.get_kurbo_size()); diff --git a/masonry/src/widget/portal.rs b/masonry/src/widget/portal.rs index fad45e08a..020d9abe6 100644 --- a/masonry/src/widget/portal.rs +++ b/masonry/src/widget/portal.rs @@ -11,13 +11,44 @@ use tracing::{trace_span, Span}; use vello::kurbo::{Point, Rect, Size, Vec2}; use vello::Scene; +use crate::event::TouchState; use crate::widget::{Axis, ScrollBar, WidgetMut}; use crate::{ AccessCtx, AccessEvent, BoxConstraints, ComposeCtx, EventCtx, LayoutCtx, LifeCycle, - LifeCycleCtx, PaintCtx, PointerEvent, StatusChange, TextEvent, Widget, WidgetId, WidgetPod, + LifeCycleCtx, PaintCtx, PointerEvent, StatusChange, TextEvent, TouchEvent, Widget, WidgetId, + WidgetPod, }; -struct FlickGesture {} +#[derive(Debug, Clone)] +struct PanGesture { + state: TouchState, + start_pos: Point, +} + +impl PanGesture { + fn new(state: TouchState, start_pos: Point) -> Self { + Self { state, start_pos } + } + + fn update_start(&mut self, state: TouchState) -> Point { + self.start_pos = self.clone().pos(); + self.state = state; + self.pos() + } + + fn update(&mut self, state: TouchState) -> Point { + self.state = state; + self.pos() + } + + fn pos(&mut self) -> Point { + self.start_pos + self.state.displacement() + } + + fn start_pos(&mut self) -> Point { + self.start_pos + } +} // TODO - refactor - see https://github.com/linebender/xilem/issues/366 // TODO - rename "Portal" to "ScrollPortal"? @@ -30,8 +61,7 @@ pub struct Portal { // on re-layouts // TODO - rename viewport_pos: Point, - #[allow(dead_code)] - flick_gesture: Option, + pan_gesture: Option, // TODO - test how it looks like constrain_horizontal: bool, constrain_vertical: bool, @@ -48,10 +78,10 @@ impl Portal { Portal { child: WidgetPod::new(child).boxed(), viewport_pos: Point::ORIGIN, - flick_gesture: None, constrain_horizontal: false, constrain_vertical: false, must_fill: false, + pan_gesture: None, // TODO - remove scrollbar_horizontal: WidgetPod::new(ScrollBar::new(Axis::Horizontal, 1.0, 1.0)), scrollbar_horizontal_visible: false, @@ -64,10 +94,10 @@ impl Portal { Portal { child, viewport_pos: Point::ORIGIN, - flick_gesture: None, constrain_horizontal: false, constrain_vertical: false, must_fill: false, + pan_gesture: None, // TODO - remove scrollbar_horizontal: WidgetPod::new(ScrollBar::new(Axis::Horizontal, 1.0, 1.0)), scrollbar_horizontal_visible: false, @@ -319,6 +349,54 @@ impl Widget for Portal { } } + fn on_touch_event(&mut self, ctx: &mut EventCtx, event: &TouchEvent) { + match event { + TouchEvent::Start(state) => { + if let Some(pan_gesture) = self.pan_gesture.as_mut() { + self.viewport_pos = Point { + x: 0.0, + y: pan_gesture.update_start(state.clone()).y, + }; + } else { + self.pan_gesture = Some(PanGesture::new(state.clone(), self.viewport_pos)); + } + + ctx.is_handled = true; + ctx.request_layout(); + ctx.request_compose(); + } + TouchEvent::Move(state) => { + if let Some(ref mut pan_gesture) = self.pan_gesture { + self.viewport_pos = Point { + x: 0.0, + y: pan_gesture.update(state.clone()).y, + }; + ctx.is_handled = true; + ctx.request_layout(); + ctx.request_compose(); + } + } + TouchEvent::End(state) => { + if let Some(pan_gesture) = self.pan_gesture.as_mut() { + self.viewport_pos = Point { + x: 0.0, + y: pan_gesture.update(state.clone()).y, + }; + ctx.is_handled = true; + ctx.request_layout(); + ctx.request_compose(); + } + self.pan_gesture = None; + } + TouchEvent::Cancel(_) => { + if let Some(pan_gesture) = self.pan_gesture.as_mut() { + self.viewport_pos = pan_gesture.start_pos(); + } + self.pan_gesture = None; + } + } + } + // TODO - handle Home/End keys, etc fn on_text_event(&mut self, _ctx: &mut EventCtx, _event: &TextEvent) {} diff --git a/masonry/src/widget/widget.rs b/masonry/src/widget/widget.rs index 52065f7ec..0fb5da6cc 100644 --- a/masonry/src/widget/widget.rs +++ b/masonry/src/widget/widget.rs @@ -13,7 +13,7 @@ use tracing::{trace_span, Span}; use vello::Scene; use crate::contexts::ComposeCtx; -use crate::event::{AccessEvent, PointerEvent, StatusChange, TextEvent}; +use crate::event::{AccessEvent, PointerEvent, StatusChange, TextEvent, TouchEvent}; use crate::{ AccessCtx, AsAny, BoxConstraints, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, Size, }; @@ -65,12 +65,13 @@ pub struct WidgetId(pub(crate) NonZeroU64); /// through a [`WidgetMut`](crate::widget::WidgetMut). #[allow(unused_variables)] pub trait Widget: AsAny { - /// Handle an event - usually user interaction. - /// - /// A number of different events (in the [`Event`] enum) are handled in this - /// method call. A widget can handle these events in a number of ways, such as - /// requesting things from the [`EventCtx`] or mutating the data. + /// Handle a pointer event fn on_pointer_event(&mut self, ctx: &mut EventCtx, event: &PointerEvent) {} + + /// Handle a touch event + fn on_touch_event(&mut self, ctx: &mut EventCtx, event: &TouchEvent) {} + + /// Handle a text event fn on_text_event(&mut self, ctx: &mut EventCtx, event: &TextEvent) {} /// Handle an event from the platform's accessibility API. @@ -290,6 +291,10 @@ impl Widget for Box { self.deref_mut().on_pointer_event(ctx, event); } + fn on_touch_event(&mut self, ctx: &mut EventCtx, event: &TouchEvent) { + self.deref_mut().on_touch_event(ctx, event); + } + fn on_text_event(&mut self, ctx: &mut EventCtx, event: &TextEvent) { self.deref_mut().on_text_event(ctx, event); } diff --git a/masonry/src/widget/widget_arena.rs b/masonry/src/widget/widget_arena.rs index 4b2f858df..9a07551ab 100644 --- a/masonry/src/widget/widget_arena.rs +++ b/masonry/src/widget/widget_arena.rs @@ -28,6 +28,14 @@ impl WidgetArena { Some(WidgetId(id.try_into().unwrap())) } + pub(crate) fn path_of(&self, widget_id: WidgetId) -> Vec { + self.widgets + .get_id_path(widget_id.to_raw()) + .iter() + .map(|x| WidgetId((*x).try_into().unwrap())) + .collect() + } + #[track_caller] pub(crate) fn get_pair( &self, diff --git a/xilem/examples/variable_clock.rs b/xilem/examples/variable_clock.rs index b6abc8794..ed9e5e238 100644 --- a/xilem/examples/variable_clock.rs +++ b/xilem/examples/variable_clock.rs @@ -39,14 +39,17 @@ struct TimeZone { } fn app_logic(data: &mut Clocks) -> impl WidgetView { - let view = portal(flex(( + let view = flex(( // HACK: We add a spacer at the top for Android. See https://github.com/rust-windowing/winit/issues/2308 FlexSpacer::Fixed(40.), local_time(data), controls(), // TODO: When we get responsive layouts, move this into a two-column view. - flex(TIMEZONES.iter().map(|it| it.view(data)).collect::>()), - ))); + portal(flex( + TIMEZONES.iter().map(|it| it.view(data)).collect::>(), + )) + .flex(1.), + )); fork( view, task(