Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pan/Flick gestures for Portal #562

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions masonry/examples/to_do_list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,10 @@ impl AppDriver for Driver {
fn on_action(&mut self, ctx: &mut DriverCtx<'_>, _widget_id: WidgetId, action: Action) {
match action {
Action::ButtonPressed(_) => {
let mut root: WidgetMut<RootWidget<Portal<Flex>>> = ctx.get_root();
let mut root = root.get_element();
let mut flex = root.child_mut();
let mut root: WidgetMut<RootWidget<Portal>> = ctx.get_root();
let mut element = root.get_element();
let mut root_mut = element.child_mut();
let mut flex = root_mut.downcast::<Flex>();
flex.add_child(Label::new(self.next_task.clone()));

let mut first_row = flex.child_mut(0).unwrap();
Expand Down
17 changes: 16 additions & 1 deletion masonry/src/contexts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand Down Expand Up @@ -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;
}
Expand Down
89 changes: 88 additions & 1 deletion masonry/src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -230,6 +234,89 @@ pub struct PointerState {
pub force: Option<Force>,
}

#[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<f64> {
self.state().position()
}
}

#[derive(Debug, Clone, Copy)]
pub struct TouchFrame {
pub time: Instant,
pub position: LogicalPosition<f64>,
pub force: Option<Force>,
}

#[derive(Debug, Clone)]
pub struct TouchState {
id: u64,
frames: Vec<TouchFrame>,
}

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<f64>, LogicalUnit<f64>) {
// TODO: impl
(0.0.into(), 0.0.into())
}

pub fn position(&self) -> LogicalPosition<f64> {
self.last_frame().position
}

pub fn displacement(&self) -> Vec2 {
let LogicalPosition::<f64> { x: ax, y: ay } = self.first_frame().position;
let LogicalPosition::<f64> { x: bx, y: by } = self.last_frame().position;
Vec2::new(ax - bx, ay - by)
}

pub fn force(&self) -> Option<Force> {
self.last_frame().force
}

pub fn id(&self) -> u64 {
self.id
}
}

#[derive(Debug, Clone)]
pub enum WindowTheme {
Light,
Expand Down
46 changes: 44 additions & 2 deletions masonry/src/event_loop_runner.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand All @@ -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 {
Expand Down Expand Up @@ -74,6 +76,7 @@ pub struct MasonryState<'a> {
render_cx: RenderContext,
render_root: RenderRoot,
pointer_state: PointerState,
touches: BTreeMap<u64, TouchState>,
renderer: Option<Renderer>,
// TODO: Winit doesn't seem to let us create these proxies from within the loop
// The reasons for this are unclear
Expand Down Expand Up @@ -237,6 +240,7 @@ impl MasonryState<'_> {
),
renderer: None,
pointer_state: PointerState::empty(),
touches: Default::default(),
proxy: event_loop.create_proxy(),

window: WindowState::Uninitialized(window),
Expand Down Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion masonry/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
55 changes: 54 additions & 1 deletion masonry/src/passes/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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,
Expand Down
29 changes: 24 additions & 5 deletions masonry/src/render_root.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand All @@ -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};
Expand All @@ -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 ---
Expand Down Expand Up @@ -62,6 +64,7 @@ pub(crate) struct RenderRootState {
pub(crate) next_focused_widget: Option<WidgetId>,
pub(crate) hovered_path: Vec<WidgetId>,
pub(crate) pointer_capture_target: Option<WidgetId>,
pub(crate) touch_capture_targets: BTreeMap<u64, WidgetId>,
pub(crate) cursor_icon: CursorIcon,
pub(crate) font_context: FontContext,
pub(crate) text_layout_context: LayoutContext<TextBrush>,
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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());
Expand Down
Loading