diff --git a/components/compositing/compositor.rs b/components/compositing/compositor.rs index 9548457039d..8259c3a87a2 100644 --- a/components/compositing/compositor.rs +++ b/components/compositing/compositor.rs @@ -27,7 +27,7 @@ use layers::rendergl; use layers::rendergl::RenderContext; use layers::scene::Scene; use layout_traits::LayoutControlChan; -use msg::compositor_msg::{Epoch, EventResult, FrameTreeId, LayerId, LayerKind}; +use msg::compositor_msg::{Epoch, FrameTreeId, LayerId, LayerKind}; use msg::compositor_msg::{LayerProperties, ScrollPolicy}; use msg::constellation_msg::{AnimationState, Image, PixelFormat}; use msg::constellation_msg::{Key, KeyModifiers, KeyState, LoadData, MouseButton}; @@ -48,6 +48,7 @@ use std::sync::mpsc::Sender; use style_traits::viewport::ViewportConstraints; use surface_map::SurfaceMap; use time::{precise_time_ns, precise_time_s}; +use touch::{TouchHandler, TouchAction}; use url::Url; use util::geometry::{PagePx, ScreenPx, ViewportPx}; use util::opts; @@ -78,9 +79,6 @@ const BUFFER_MAP_SIZE: usize = 10000000; const MAX_ZOOM: f32 = 8.0; const MIN_ZOOM: f32 = 0.1; -/// Minimum number of ScreenPx to begin touch scrolling. -const TOUCH_PAN_MIN_SCREEN_PX: f32 = 20.0; - /// Holds the state when running reftests that determines when it is /// safe to save the output image. #[derive(Copy, Clone, PartialEq)] @@ -179,7 +177,7 @@ pub struct IOCompositor { fragment_point: Option>, /// Touch input state machine - touch_gesture_state: TouchState, + touch_handler: TouchHandler, /// Pending scroll events. pending_scroll_events: Vec, @@ -199,28 +197,6 @@ pub struct IOCompositor { last_mouse_move_recipient: Option, } -/// The states of the touch input state machine. -/// -/// TODO: Currently Add support for "flinging" (scrolling inertia), pinch zooming. -enum TouchState { - /// Not tracking any touch point - Nothing, - /// A touchstart event was dispatched to the page, but the response wasn't received yet. - /// Contains the number of active touch points and the initial touch location. - WaitingForScript(u32, TypedPoint2D), - /// Script is consuming the current touch sequence; don't perform default actions. - /// Contains the number of active touch points. - DefaultPrevented(u32), - /// A single touch point is active and may perform click or pan default actions. - /// Contains the initial touch location. - Touching(TypedPoint2D), - /// A single touch point is active and has started panning. Contains the latest touch location. - Panning(TypedPoint2D), - /// A multi-touch gesture is in progress. Contains the number of active touch points. - MultiTouch(u32), -} - - pub struct ScrollEvent { delta: TypedPoint2D, cursor: TypedPoint2D, @@ -347,7 +323,7 @@ impl IOCompositor { channel_to_self: state.sender.clone_compositor_proxy(), scrolling_timer: ScrollingTimerProxy::new(state.sender), composition_request: CompositionRequest::NoCompositingNecessary, - touch_gesture_state: TouchState::Nothing, + touch_handler: TouchHandler::new(), pending_scroll_events: Vec::new(), composite_target: composite_target, shutdown_state: ShutdownState::NotShuttingDown, @@ -556,19 +532,7 @@ impl IOCompositor { } (Msg::TouchEventProcessed(result), ShutdownState::NotShuttingDown) => { - match self.touch_gesture_state { - TouchState::WaitingForScript(n, p) => { - self.touch_gesture_state = match result { - EventResult::DefaultPrevented => TouchState::DefaultPrevented(n), - EventResult::DefaultAllowed => if n > 1 { - TouchState::MultiTouch(n) - } else { - TouchState::Touching(p) - } - }; - } - _ => {} - } + self.touch_handler.on_event_processed(result); } (Msg::SetCursor(cursor), ShutdownState::NotShuttingDown) => { @@ -1220,25 +1184,7 @@ impl IOCompositor { } fn on_touch_down(&mut self, identifier: TouchId, point: TypedPoint2D) { - self.touch_gesture_state = match self.touch_gesture_state { - TouchState::Nothing => { - TouchState::WaitingForScript(1, point) - } - TouchState::Touching(_) | - TouchState::Panning(_) => { - // Was a single-touch sequence; now a multi-touch sequence: - TouchState::MultiTouch(2) - } - TouchState::WaitingForScript(n, p) => { - TouchState::WaitingForScript(n + 1, p) - } - TouchState::DefaultPrevented(n) => { - TouchState::DefaultPrevented(n + 1) - } - TouchState::MultiTouch(n) => { - TouchState::MultiTouch(n + 1) - } - }; + self.touch_handler.on_touch_down(identifier, point); if let Some(result) = self.find_topmost_layer_at_point(point / self.scene.scale) { result.layer.send_event(self, TouchEvent(TouchEventType::Down, identifier, result.point.to_untyped())); @@ -1246,89 +1192,45 @@ impl IOCompositor { } fn on_touch_move(&mut self, identifier: TouchId, point: TypedPoint2D) { - match self.touch_gesture_state { - TouchState::Nothing => warn!("Got unexpected touch move event"), - - TouchState::WaitingForScript(..) => { - // TODO: Queue events while waiting for script? - } - TouchState::Touching(p0) => { - let delta = point - p0; - let px: TypedPoint2D = delta / self.device_pixels_per_screen_px(); - let px = px.to_untyped(); - - if px.x.abs() > TOUCH_PAN_MIN_SCREEN_PX || - px.y.abs() > TOUCH_PAN_MIN_SCREEN_PX - { - self.touch_gesture_state = TouchState::Panning(point); - self.on_scroll_window_event(delta, point.cast().unwrap()); - } - } - TouchState::Panning(p0) => { - let delta = point - p0; + match self.touch_handler.on_touch_move(identifier, point) { + TouchAction::Scroll(delta) => { self.on_scroll_window_event(delta, point.cast().unwrap()); - self.touch_gesture_state = TouchState::Panning(point); } - TouchState::DefaultPrevented(_) => { - // Send the event to script. + TouchAction::Zoom(magnification, scroll_delta) => { + let cursor = Point2D::typed(-1, -1); // Make sure this hits the base layer. + self.on_pinch_zoom_window_event(magnification); + self.on_scroll_window_event(scroll_delta, cursor); + } + TouchAction::DispatchEvent => { if let Some(result) = self.find_topmost_layer_at_point(point / self.scene.scale) { result.layer.send_event(self, TouchEvent(TouchEventType::Move, identifier, result.point.to_untyped())); } } - TouchState::MultiTouch(_) => { - // TODO: Pinch zooming. - } + _ => {} } } fn on_touch_up(&mut self, identifier: TouchId, point: TypedPoint2D) { - // Send the event to script. if let Some(result) = self.find_topmost_layer_at_point(point / self.scene.scale) { result.layer.send_event(self, TouchEvent(TouchEventType::Up, identifier, result.point.to_untyped())); } - - self.touch_gesture_state = match self.touch_gesture_state { - TouchState::Touching(_) => { - // TODO: If the duration exceeds some threshold, send a contextmenu event instead. - // TODO: Don't send a click if preventDefault is called on the touchend event. + match self.touch_handler.on_touch_up(identifier, point) { + TouchAction::Click => { self.simulate_mouse_click(point); - TouchState::Nothing } - TouchState::Nothing | - TouchState::Panning(_) | - TouchState::WaitingForScript(1, _) | - TouchState::DefaultPrevented(1) | - TouchState::MultiTouch(1) => { - TouchState::Nothing - } - TouchState::WaitingForScript(n, p) => TouchState::WaitingForScript(n - 1, p), - TouchState::DefaultPrevented(n) => TouchState::DefaultPrevented(n - 1), - TouchState::MultiTouch(n) => TouchState::MultiTouch(n - 1), - }; + _ => {} + } } fn on_touch_cancel(&mut self, identifier: TouchId, point: TypedPoint2D) { // Send the event to script. + self.touch_handler.on_touch_cancel(identifier, point); if let Some(result) = self.find_topmost_layer_at_point(point / self.scene.scale) { result.layer.send_event(self, TouchEvent(TouchEventType::Cancel, identifier, result.point.to_untyped())); } - - self.touch_gesture_state = match self.touch_gesture_state { - TouchState::Nothing | - TouchState::Touching(_) | - TouchState::Panning(_) | - TouchState::WaitingForScript(1, _) | - TouchState::DefaultPrevented(1) | - TouchState::MultiTouch(1) => { - TouchState::Nothing - } - TouchState::WaitingForScript(n, p) => TouchState::WaitingForScript(n - 1, p), - TouchState::DefaultPrevented(n) => TouchState::DefaultPrevented(n - 1), - TouchState::MultiTouch(n) => TouchState::MultiTouch(n - 1), - }; } /// http://w3c.github.io/touch-events/#mouse-events diff --git a/components/compositing/lib.rs b/components/compositing/lib.rs index 338a802d627..1ea72338c34 100644 --- a/components/compositing/lib.rs +++ b/components/compositing/lib.rs @@ -71,6 +71,7 @@ pub mod sandboxing; mod scrolling; mod surface_map; mod timer_scheduler; +mod touch; pub mod windowing; /// Messages from the compositor to the constellation. diff --git a/components/compositing/touch.rs b/components/compositing/touch.rs new file mode 100644 index 00000000000..46d38c378ca --- /dev/null +++ b/components/compositing/touch.rs @@ -0,0 +1,237 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use euclid::point::TypedPoint2D; +use euclid::scale_factor::ScaleFactor; +use layers::geometry::DevicePixel; +use msg::compositor_msg::EventResult; +use script_traits::TouchId; +use self::TouchState::*; + +/// Minimum number of ScreenPx to begin touch scrolling. +const TOUCH_PAN_MIN_SCREEN_PX: f32 = 20.0; + +pub struct TouchHandler { + pub state: TouchState, + pub active_touch_points: Vec, +} + +#[derive(Clone, Copy, Debug)] +pub struct TouchPoint { + pub id: TouchId, + pub point: TypedPoint2D +} + +impl TouchPoint { + pub fn new(id: TouchId, point: TypedPoint2D) -> Self { + TouchPoint { id: id, point: point } + } +} + +/// The states of the touch input state machine. +/// +/// TODO: Add support for "flinging" (scrolling inertia) +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum TouchState { + /// Not tracking any touch point + Nothing, + /// A touchstart event was dispatched to the page, but the response wasn't received yet. + /// Contains the initial touch point. + WaitingForScript, + /// Script is consuming the current touch sequence; don't perform default actions. + DefaultPrevented, + /// A single touch point is active and may perform click or pan default actions. + /// Contains the initial touch location. + Touching, + /// A single touch point is active and has started panning. + Panning, + /// A two-finger pinch zoom gesture is active. + Pinching, + /// A multi-touch gesture is in progress. Contains the number of active touch points. + MultiTouch, +} + +/// The action to take in response to a touch event +#[derive(Clone, Copy, Debug)] +pub enum TouchAction { + /// Simulate a mouse click. + Click, + /// Scroll by the provided offset. + Scroll(TypedPoint2D), + /// Zoom by a magnification factor and scroll by the provided offset. + Zoom(f32, TypedPoint2D), + /// Send a JavaScript event to content. + DispatchEvent, + /// Don't do anything. + NoAction, +} + +impl TouchHandler { + pub fn new() -> Self { + TouchHandler { + state: Nothing, + active_touch_points: Vec::new(), + } + } + + pub fn on_touch_down(&mut self, id: TouchId, point: TypedPoint2D) { + let point = TouchPoint::new(id, point); + self.active_touch_points.push(point); + + self.state = match self.state { + Nothing => WaitingForScript, + Touching | Panning => Pinching, + WaitingForScript => WaitingForScript, + DefaultPrevented => DefaultPrevented, + Pinching | MultiTouch => MultiTouch, + }; + } + + pub fn on_touch_move(&mut self, id: TouchId, point: TypedPoint2D) + -> TouchAction { + let idx = match self.active_touch_points.iter_mut().position(|t| t.id == id) { + Some(i) => i, + None => { + warn!("Got a touchmove event for a non-active touch point"); + return TouchAction::NoAction; + } + }; + let old_point = self.active_touch_points[idx].point; + + let action = match self.state { + Touching => { + let delta = point - old_point; + // TODO let delta: TypedPoint2D = delta / self.device_pixels_per_screen_px(); + + if delta.x.get().abs() > TOUCH_PAN_MIN_SCREEN_PX || + delta.y.get().abs() > TOUCH_PAN_MIN_SCREEN_PX + { + self.state = Panning; + TouchAction::Scroll(delta) + } else { + TouchAction::NoAction + } + } + Panning => { + let delta = point - old_point; + TouchAction::Scroll(delta) + } + DefaultPrevented => { + TouchAction::DispatchEvent + } + Pinching => { + let (d0, c0) = self.pinch_distance_and_center(); + self.active_touch_points[idx].point = point; + let (d1, c1) = self.pinch_distance_and_center(); + + let magnification = d1 / d0; + let scroll_delta = c1 - c0 * ScaleFactor::new(magnification); + + TouchAction::Zoom(magnification, scroll_delta) + } + WaitingForScript => TouchAction::NoAction, + MultiTouch => TouchAction::NoAction, + Nothing => unreachable!(), + }; + + // If we're still waiting to see whether this is a click or pan, remember the original + // location. Otherwise, update the touch point with the latest location. + if self.state != Touching && self.state != WaitingForScript { + self.active_touch_points[idx].point = point; + } + action + } + + pub fn on_touch_up(&mut self, id: TouchId, _point: TypedPoint2D) + -> TouchAction { + match self.active_touch_points.iter().position(|t| t.id == id) { + Some(i) => { + self.active_touch_points.swap_remove(i); + } + None => { + warn!("Got a touch up event for a non-active touch point"); + } + } + match self.state { + Touching => { + // FIXME: If the duration exceeds some threshold, send a contextmenu event instead. + // FIXME: Don't send a click if preventDefault is called on the touchend event. + self.state = Nothing; + TouchAction::Click + } + Nothing | Panning => { + self.state = Nothing; + TouchAction::NoAction + } + Pinching => { + self.state = Panning; + TouchAction::NoAction + } + WaitingForScript | DefaultPrevented | MultiTouch => { + if self.active_touch_points.is_empty() { + self.state = Nothing; + } + TouchAction::NoAction + } + } + } + + pub fn on_touch_cancel(&mut self, id: TouchId, _point: TypedPoint2D) { + match self.active_touch_points.iter().position(|t| t.id == id) { + Some(i) => { + self.active_touch_points.swap_remove(i); + } + None => { + warn!("Got a touchcancel event for a non-active touch point"); + return; + } + } + match self.state { + Nothing => {} + Touching | Panning => { + self.state = Nothing; + } + Pinching => { + self.state = Panning; + } + WaitingForScript | DefaultPrevented | MultiTouch => { + if self.active_touch_points.is_empty() { + self.state = Nothing; + } + } + } + } + + pub fn on_event_processed(&mut self, result: EventResult) { + match self.state { + WaitingForScript => self.state = match result { + EventResult::DefaultPrevented => DefaultPrevented, + EventResult::DefaultAllowed => match self.touch_count() { + 1 => Touching, + 2 => Pinching, + _ => MultiTouch, + } + }, + _ => {} + } + } + + fn touch_count(&self) -> usize { + self.active_touch_points.len() + } + + fn pinch_distance_and_center(&self) -> (f32, TypedPoint2D) { + debug_assert!(self.touch_count() == 2); + let p0 = self.active_touch_points[0].point; + let p1 = self.active_touch_points[1].point; + let center = (p0 + p1) / ScaleFactor::new(2.0); + + let d = p0 - p1; + let dx = d.x.get(); + let dy = d.y.get(); + let distance = f32::sqrt(dx * dx + dy * dy); + + (distance, center) + } +}