From b2cd7363173269cd4372bf4e0bbf3a6af4587023 Mon Sep 17 00:00:00 2001 From: Patrick Walton Date: Wed, 2 Mar 2016 16:17:25 -0800 Subject: [PATCH] Implement support for overscrolling on the Mac. This generalizes scroll info to a separate structure, which paves the way toward supporting zoom as well. --- Cargo.lock | 2 +- src/frame.rs | 138 +++++++++++++++++++++++++++++++----------- src/internal_types.rs | 10 ++- src/layer.rs | 62 +++++++++++++++++-- src/lib.rs | 1 + src/render_backend.rs | 16 ++++- src/renderer.rs | 10 ++- src/spring.rs | 102 +++++++++++++++++++++++++++++++ 8 files changed, 291 insertions(+), 50 deletions(-) create mode 100644 src/spring.rs diff --git a/Cargo.lock b/Cargo.lock index 683f3924b5..1ed3efa9d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -365,7 +365,7 @@ dependencies = [ [[package]] name = "webrender_traits" version = "0.1.0" -source = "git+https://github.com/servo/webrender_traits#94f16f55e65d735a9c1dc38733937cb2774322e1" +source = "git+https://github.com/servo/webrender_traits#6495e6b0ccd05f6769e79c393c23709ecf29ca11" dependencies = [ "app_units 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "core-graphics 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/src/frame.rs b/src/frame.rs index 51aa0a2cd8..e30bd0f0b6 100644 --- a/src/frame.rs +++ b/src/frame.rs @@ -14,21 +14,27 @@ use internal_types::{CompositeBatchInfo, CompositeBatchJob, MaskRegion}; use internal_types::{RendererFrame, StackingContextInfo, BatchInfo, DrawCall, StackingContextIndex}; use internal_types::{ANGLE_FLOAT_TO_FIXED, MAX_RECT, BatchUpdate, BatchUpdateOp, DrawLayer}; use internal_types::{DrawCommand, ClearInfo, RenderTargetId, DrawListGroupId}; -use layer::Layer; +use layer::{Layer, ScrollingState}; use node_compiler::NodeCompiler; use renderer::CompositionOpHelpers; use resource_cache::ResourceCache; use resource_list::BuildRequiredResources; use scene::{SceneStackingContext, ScenePipeline, Scene, SceneItem, SpecificSceneItem}; use scoped_threadpool; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::hash::BuildHasherDefault; use std::mem; use texture_cache::TexturePage; use util; use webrender_traits::{AuxiliaryLists, PipelineId, Epoch, ScrollPolicy, ScrollLayerId}; use webrender_traits::{StackingContext, FilterOp, ImageFormat, MixBlendMode, StackingLevel}; -use webrender_traits::{ScrollLayerInfo}; +use webrender_traits::{ScrollEventPhase, ScrollLayerInfo}; + +#[cfg(target_os = "macos")] +const CAN_OVERSCROLL: bool = true; + +#[cfg(not(target_os = "macos"))] +const CAN_OVERSCROLL: bool = false; #[derive(Copy, Clone, PartialEq, PartialOrd, Debug)] pub struct FrameId(pub u32); @@ -588,7 +594,7 @@ impl Frame { } pub fn reset(&mut self, resource_cache: &mut ResourceCache) - -> HashMap, BuildHasherDefault> { + -> HashMap> { self.draw_list_groups.clear(); self.pipeline_epoch_map.clear(); self.stacking_context_info.clear(); @@ -599,16 +605,16 @@ impl Frame { // Free any render targets from last frame. // TODO: This should really re-use existing targets here... - let mut old_layer_offsets = HashMap::with_hasher(Default::default()); + let mut old_layer_scrolling_states = HashMap::with_hasher(Default::default()); for (layer_id, mut old_layer) in &mut self.layers.drain() { old_layer.reset(&mut self.pending_updates); - old_layer_offsets.insert(layer_id, old_layer.scroll_offset); + old_layer_scrolling_states.insert(layer_id, old_layer.scrolling); } // Advance to the next frame. self.id.0 += 1; - old_layer_offsets + old_layer_scrolling_states } fn next_render_target_id(&mut self) -> RenderTargetId { @@ -673,32 +679,76 @@ impl Frame { } pub fn scroll(&mut self, - delta: Point2D, - cursor: Point2D) { - if let Some(root_scroll_layer_id) = self.root_scroll_layer_id { - let scroll_layer_id = self.get_scroll_layer(&cursor, - root_scroll_layer_id, - &Matrix4::identity()); + mut delta: Point2D, + cursor: Point2D, + phase: ScrollEventPhase) { + let root_scroll_layer_id = match self.root_scroll_layer_id { + Some(root_scroll_layer_id) => root_scroll_layer_id, + None => return, + }; - if let Some(scroll_layer_id) = scroll_layer_id { - let layer = self.layers.get_mut(&scroll_layer_id).unwrap(); + let scroll_layer_id = match self.get_scroll_layer(&cursor, + root_scroll_layer_id, + &Matrix4::identity()) { + Some(scroll_layer_id) => scroll_layer_id, + None => return, + }; - if layer.layer_size.width > layer.viewport_size.width { - layer.scroll_offset.x = layer.scroll_offset.x + delta.x; - layer.scroll_offset.x = layer.scroll_offset.x.min(0.0); - layer.scroll_offset.x = layer.scroll_offset.x.max(-layer.layer_size.width + layer.viewport_size.width); - } + let layer = self.layers.get_mut(&scroll_layer_id).unwrap(); + if layer.scrolling.started_bouncing_back && phase == ScrollEventPhase::Move(false) { + return + } - if layer.layer_size.height > layer.viewport_size.height { - layer.scroll_offset.y = layer.scroll_offset.y + delta.y; - layer.scroll_offset.y = layer.scroll_offset.y.min(0.0); - layer.scroll_offset.y = layer.scroll_offset.y.max(-layer.layer_size.height + layer.viewport_size.height); - } + let overscroll_amount = layer.overscroll_amount(); + let overscrolling = overscroll_amount.width != 0.0 || overscroll_amount.height != 0.0; + if overscrolling { + if overscroll_amount.width != 0.0 { + delta.x /= overscroll_amount.width.abs() + } + if overscroll_amount.height != 0.0 { + delta.y /= overscroll_amount.height.abs() + } + } + + let is_unscrollable = layer.layer_size.width <= layer.viewport_size.width && + layer.layer_size.height <= layer.viewport_size.height; + + if layer.layer_size.width > layer.viewport_size.width { + layer.scrolling.offset.x = layer.scrolling.offset.x + delta.x; + if is_unscrollable || !CAN_OVERSCROLL { + layer.scrolling.offset.x = layer.scrolling.offset.x.min(0.0); + layer.scrolling.offset.x = layer.scrolling.offset.x.max(-layer.layer_size.width + + layer.viewport_size.width); + } + } - layer.scroll_offset.x = layer.scroll_offset.x.round(); - layer.scroll_offset.y = layer.scroll_offset.y.round(); + if layer.layer_size.height > layer.viewport_size.height { + layer.scrolling.offset.y = layer.scrolling.offset.y + delta.y; + if is_unscrollable || !CAN_OVERSCROLL { + layer.scrolling.offset.y = layer.scrolling.offset.y.min(0.0); + layer.scrolling.offset.y = + layer.scrolling.offset.y.max(-layer.layer_size.height + + layer.viewport_size.height); } } + + if phase == ScrollEventPhase::Start || phase == ScrollEventPhase::Move(true) { + layer.scrolling.started_bouncing_back = false + } else if overscrolling && + ((delta.x < 1.0 && delta.y < 1.0) || phase == ScrollEventPhase::End) { + layer.scrolling.started_bouncing_back = true + } + + layer.scrolling.offset.x = layer.scrolling.offset.x.round(); + layer.scrolling.offset.y = layer.scrolling.offset.y.round(); + + layer.stretch_overscroll_spring(); + } + + pub fn tick_scrolling_bounce_animations(&mut self) { + for (_, layer) in &mut self.layers { + layer.tick_scrolling_bounce_animation() + } } pub fn create(&mut self, @@ -708,7 +758,7 @@ impl Frame { device_pixel_ratio: f32) { if let Some(root_pipeline_id) = scene.root_pipeline_id { if let Some(root_pipeline) = scene.pipeline_map.get(&root_pipeline_id) { - let old_layer_offsets = self.reset(resource_cache); + let old_layer_scrolling_states = self.reset(resource_cache); self.pipeline_auxiliary_lists = scene.pipeline_auxiliary_lists.clone(); @@ -775,12 +825,12 @@ impl Frame { // TODO(gw): These are all independent - can be run through thread pool if it shows up in the profile! for (scroll_layer_id, layer) in &mut self.layers { - let scroll_offset = match old_layer_offsets.get(&scroll_layer_id) { - Some(old_offset) => *old_offset, - None => Point2D::zero(), + let scrolling_state = match old_layer_scrolling_states.get(&scroll_layer_id) { + Some(old_scrolling_state) => *old_scrolling_state, + None => ScrollingState::new(), }; - layer.finalize(scroll_offset); + layer.finalize(&scrolling_state); } } } @@ -1135,8 +1185,11 @@ impl Frame { match self.layers.get_mut(&layer_id) { Some(layer) => { layer.world_transform = parent_transform.mul(&layer.local_transform) - .translate(layer.world_origin.x, layer.world_origin.y, 0.0) - .translate(layer.scroll_offset.x, layer.scroll_offset.y, 0.0); + .translate(layer.world_origin.x, + layer.world_origin.y, 0.0) + .translate(layer.scrolling.offset.x, + layer.scrolling.offset.y, + 0.0); (layer.world_transform, layer.children.clone()) } None => { @@ -1289,6 +1342,21 @@ impl Frame { } }; - RendererFrame::new(self.pipeline_epoch_map.clone(), root_layer) + let layers_bouncing_back = self.collect_layers_bouncing_back(); + RendererFrame::new(self.pipeline_epoch_map.clone(), layers_bouncing_back, root_layer) + } + + fn collect_layers_bouncing_back(&self) + -> HashSet> { + let mut layers_bouncing_back = HashSet::with_hasher(Default::default()); + for (scroll_layer_id, layer) in &self.layers { + if layer.scrolling.started_bouncing_back { + let overscroll_amount = layer.overscroll_amount(); + if overscroll_amount.width.abs() >= 0.1 || overscroll_amount.height.abs() >= 0.1 { + layers_bouncing_back.insert(*scroll_layer_id); + } + } + } + layers_bouncing_back } } diff --git a/src/internal_types.rs b/src/internal_types.rs index 1521f41932..6cdc18ffd0 100644 --- a/src/internal_types.rs +++ b/src/internal_types.rs @@ -10,7 +10,7 @@ use fnv::FnvHasher; use freelist::{FreeListItem, FreeListItemId}; use num::Zero; use profiler::BackendProfileCounters; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::hash::BuildHasherDefault; use std::ops::{Add, Sub}; use std::path::PathBuf; @@ -18,7 +18,7 @@ use std::sync::Arc; use texture_cache::BorderType; use util::{self, RectVaryings}; use webrender_traits::{FontKey, Epoch, ColorF, PipelineId}; -use webrender_traits::{ImageFormat, MixBlendMode, NativeFontHandle, DisplayItem}; +use webrender_traits::{ImageFormat, MixBlendMode, NativeFontHandle, DisplayItem, ScrollLayerId}; #[derive(Hash, Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)] pub struct DevicePixel(i32); @@ -559,14 +559,18 @@ impl DrawLayer { pub struct RendererFrame { pub pipeline_epoch_map: HashMap>, + pub layers_bouncing_back: HashSet>, pub root_layer: DrawLayer, } impl RendererFrame { pub fn new(pipeline_epoch_map: HashMap>, - root_layer: DrawLayer) -> RendererFrame { + layers_bouncing_back: HashSet>, + root_layer: DrawLayer) + -> RendererFrame { RendererFrame { pipeline_epoch_map: pipeline_epoch_map, + layers_bouncing_back: layers_bouncing_back, root_layer: root_layer, } } diff --git a/src/layer.rs b/src/layer.rs index acc0936545..2309c5eb41 100644 --- a/src/layer.rs +++ b/src/layer.rs @@ -6,12 +6,13 @@ use aabbtree::{AABBTree, NodeIndex}; use euclid::{Point2D, Rect, Size2D, Matrix4}; use internal_types::{BatchUpdate, BatchUpdateList, BatchUpdateOp}; use internal_types::{DrawListItemIndex, DrawListId, DrawListGroupId}; +use spring::{DAMPING, STIFFNESS, Spring}; use webrender_traits::ScrollLayerId; pub struct Layer { // TODO: Remove pub from here if possible in the future pub aabb_tree: AABBTree, - pub scroll_offset: Point2D, + pub scrolling: ScrollingState, pub viewport_size: Size2D, pub layer_size: Size2D, pub world_origin: Point2D, @@ -30,7 +31,7 @@ impl Layer { Layer { aabb_tree: aabb_tree, - scroll_offset: Point2D::zero(), + scrolling: ScrollingState::new(), viewport_size: viewport_size, world_origin: world_origin, layer_size: layer_size, @@ -68,16 +69,15 @@ impl Layer { item_index); } - pub fn finalize(&mut self, - initial_scroll_offset: Point2D) { - self.scroll_offset = initial_scroll_offset; + pub fn finalize(&mut self, scrolling: &ScrollingState) { + self.scrolling = *scrolling; self.aabb_tree.finalize(); } pub fn cull(&mut self) { // TODO(gw): Take viewport_size into account here!!! let viewport_rect = Rect::new(Point2D::zero(), self.viewport_size); - let adjusted_viewport = viewport_rect.translate(&-self.scroll_offset); + let adjusted_viewport = viewport_rect.translate(&-self.scrolling.offset); self.aabb_tree.cull(&adjusted_viewport); } @@ -85,4 +85,54 @@ impl Layer { pub fn print(&self) { self.aabb_tree.print(NodeIndex(0), 0); } + + pub fn overscroll_amount(&self) -> Size2D { + let overscroll_x = if self.scrolling.offset.x > 0.0 { + -self.scrolling.offset.x + } else if self.scrolling.offset.x < self.viewport_size.width - self.layer_size.width { + self.viewport_size.width - self.layer_size.width - self.scrolling.offset.x + } else { + 0.0 + }; + + let overscroll_y = if self.scrolling.offset.y > 0.0 { + -self.scrolling.offset.y + } else if self.scrolling.offset.y < self.viewport_size.height - self.layer_size.height { + self.viewport_size.height - self.layer_size.height - self.scrolling.offset.y + } else { + 0.0 + }; + + Size2D::new(overscroll_x, overscroll_y) + } + + pub fn stretch_overscroll_spring(&mut self) { + let overscroll_amount = self.overscroll_amount(); + self.scrolling.spring.coords(self.scrolling.offset, + self.scrolling.offset, + self.scrolling.offset + overscroll_amount); + } + + pub fn tick_scrolling_bounce_animation(&mut self) { + self.scrolling.spring.animate(); + self.scrolling.offset = self.scrolling.spring.current(); + } } + +#[derive(Copy, Clone)] +pub struct ScrollingState { + pub offset: Point2D, + pub spring: Spring, + pub started_bouncing_back: bool, +} + +impl ScrollingState { + pub fn new() -> ScrollingState { + ScrollingState { + offset: Point2D::new(0.0, 0.0), + spring: Spring::at(Point2D::new(0.0, 0.0), STIFFNESS, DAMPING), + started_bouncing_back: false, + } + } +} + diff --git a/src/lib.rs b/src/lib.rs index d3dc7af279..bffc205943 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -24,6 +24,7 @@ mod render_backend; mod resource_cache; mod resource_list; mod scene; +mod spring; mod tessellator; mod texture_cache; mod util; diff --git a/src/render_backend.rs b/src/render_backend.rs index 0a29355157..cffbd203d4 100644 --- a/src/render_backend.rs +++ b/src/render_backend.rs @@ -157,9 +157,17 @@ impl RenderBackend { self.publish_frame(frame, &mut profile_counters); } - ApiMsg::Scroll(delta, cursor) => { + ApiMsg::Scroll(delta, cursor, move_phase) => { let frame = profile_counters.total_time.profile(|| { - self.frame.scroll(delta, cursor); + self.frame.scroll(delta, cursor, move_phase); + self.render() + }); + + self.publish_frame(frame, &mut profile_counters); + } + ApiMsg::TickScrollingBounce => { + let frame = profile_counters.total_time.profile(|| { + self.frame.tick_scrolling_bounce_animations(); self.render() }); @@ -172,7 +180,9 @@ impl RenderBackend { Some(root_pipeline_id) => { match self.frame.layers.get_mut(&ScrollLayerId::new(root_pipeline_id, 0)) { None => tx.send(point).unwrap(), - Some(layer) => tx.send(point - layer.scroll_offset).unwrap(), + Some(layer) => { + tx.send(point - layer.scrolling.offset).unwrap() + } } } None => { diff --git a/src/renderer.rs b/src/renderer.rs index 901fa8e110..cba723bc7a 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -376,8 +376,7 @@ impl Renderer { } } - pub fn render(&mut self, - framebuffer_size: Size2D) { + pub fn render(&mut self, framebuffer_size: Size2D) { let mut profile_timers = RendererProfileTimers::new(); profile_timers.total_time.profile(|| { @@ -414,6 +413,13 @@ impl Renderer { self.last_time = current_time; } + pub fn layers_are_bouncing_back(&self) -> bool { + match self.current_frame { + None => false, + Some(ref current_frame) => !current_frame.layers_bouncing_back.is_empty(), + } + } + fn update_batches(&mut self) { let mut pending_batch_updates = mem::replace(&mut self.pending_batch_updates, vec![]); for update_list in pending_batch_updates.drain(..) { diff --git a/src/spring.rs b/src/spring.rs new file mode 100644 index 0000000000..6b37b1f562 --- /dev/null +++ b/src/spring.rs @@ -0,0 +1,102 @@ +/* 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::Point2D; + +/// Some arbitrarily small positive number used as threshold value. +pub const EPSILON: f32 = 0.001; + +/// The default stiffness factor. +pub const STIFFNESS: f32 = 0.2; + +/// The default damping factor. +pub const DAMPING: f32 = 1.0; + +#[derive(Copy, Clone, Debug)] +pub struct Spring { + /// The current position of spring. + cur: Point2D, + /// The position of spring at previous tick. + prev: Point2D, + /// The destination of spring. + dest: Point2D, + /// How hard it springs back. + stiffness: f32, + /// Friction. 1.0 means no bounce. + damping: f32, +} + +impl Spring { + /// Create a new spring at location. + pub fn at(pos: Point2D, stiffness: f32, damping: f32) -> Spring { + Spring { + cur: pos, + prev: pos, + dest: pos, + stiffness: stiffness, + damping: damping, + } + } + + /// Set coords on a spring, mutating spring + pub fn coords(&mut self, cur: Point2D, prev: Point2D, dest: Point2D) { + self.cur = cur; + self.prev = prev; + self.dest = dest + } + + pub fn current(&self) -> Point2D { + self.cur + } + + pub fn animate(&mut self) { + if !is_resting(self.cur.x, self.prev.x, self.dest.x) || + !is_resting(self.cur.y, self.prev.y, self.dest.y) { + let next = Point2D::new(next(self.cur.x, + self.prev.x, + self.dest.x, + self.stiffness, + self.damping), + next(self.cur.y, + self.prev.y, + self.dest.y, + self.stiffness, + self.damping)); + let (cur, dest) = (self.cur, self.dest); + self.coords(next, cur, dest) + } else { + let dest = self.dest; + self.coords(dest, dest, dest) + } + } +} + +/// Given numbers, calculate the next position for a spring. +fn next(cur: f32, prev: f32, dest: f32, stiffness: f32, damping: f32) -> f32 { + // Calculate spring force + let fspring = -stiffness * (cur - dest); + + // Calculate velocity + let vel = cur - prev; + + // Calculate damping force. + let fdamping = -damping * vel; + + // Calc acceleration by adjusting spring force to damping force + let acc = fspring + fdamping; + + // Calculate new velocity after adding acceleration. Scale to framerate. + let nextv = vel + acc; + + // Calculate next position by integrating velocity. Scale to framerate. + let next = cur + nextv; + next +} + +/// Given numbers, calcluate if a spring is at rest. +fn is_resting(cur: f32, prev: f32, dest: f32) -> bool { + (cur - prev).abs() < EPSILON && (cur - dest).abs() < EPSILON +} + +