1/*
2 * Copyright (C) 2013, 2014 Apple Inc. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
6 * are met:
7 * 1. Redistributions of source code must retain the above copyright
8 * notice, this list of conditions and the following disclaimer.
9 * 2. Redistributions in binary form must reproduce the above copyright
10 * notice, this list of conditions and the following disclaimer in the
11 * documentation and/or other materials provided with the distribution.
12 *
13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23 * THE POSSIBILITY OF SUCH DAMAGE.
24 */
25
26#include "config.h"
27#include "ViewGestureController.h"
28
29#include "DrawingAreaProxy.h"
30#include "WebBackForwardList.h"
31
32namespace WebKit {
33using namespace WebCore;
34
35static const Seconds swipeMinAnimationDuration = 100_ms;
36static const Seconds swipeMaxAnimationDuration = 400_ms;
37static const double swipeAnimationBaseVelocity = 0.002;
38
39// GTK divides all scroll deltas by 10, compensate for that
40static const double gtkScrollDeltaMultiplier = 10;
41static const double swipeTouchpadBaseWidth = 400;
42
43// This is derivative of the easing function at t=0
44static const double swipeAnimationDurationMultiplier = 3;
45
46static const double swipeCancelArea = 0.5;
47static const double swipeCancelVelocityThreshold = 0.001;
48
49static const double swipeOverlayShadowOpacity = 0.06;
50static const double swipeOverlayDimmingOpacity = 0.12;
51static const double swipeOverlayShadowWidth = 81;
52static const double swipeOverlayShadowGradientOffsets[] = { 0, 0.03125, 0.0625, 0.0938, 0.125, 0.1875, 0.25, 0.375, 0.4375, 0.5, 0.5625, 0.625, 0.6875, 0.75, 0.875, 1. };
53static const double swipeOverlayShadowGradientAlpha[] = { 1, 0.99, 0.98, 0.95, 0.92, 0.82, 0.71, 0.46, 0.35, 0.25, 0.17, 0.11, 0.07, 0.04, 0.01, 0. };
54
55static bool isEventStop(GdkEventScroll* event)
56{
57#if GTK_CHECK_VERSION(3, 20, 0)
58 return event->is_stop;
59#else
60 return !event->delta_x && !event->delta_y;
61#endif
62}
63
64void ViewGestureController::platformTeardown()
65{
66 m_swipeProgressTracker.reset();
67
68 if (m_activeGestureType == ViewGestureType::Swipe)
69 removeSwipeSnapshot();
70}
71
72bool ViewGestureController::PendingSwipeTracker::scrollEventCanStartSwipe(GdkEventScroll*)
73{
74 return true;
75}
76
77bool ViewGestureController::PendingSwipeTracker::scrollEventCanEndSwipe(GdkEventScroll* event)
78{
79 return isEventStop(event);
80}
81
82bool ViewGestureController::PendingSwipeTracker::scrollEventCanInfluenceSwipe(GdkEventScroll* event)
83{
84 GdkDevice* device = gdk_event_get_source_device(reinterpret_cast<GdkEvent*>(event));
85 GdkInputSource source = gdk_device_get_source(device);
86
87 // FIXME: Should it maybe be allowed on mice/trackpoints as well? The GDK_SCROLL_SMOOTH
88 // requirement already filters out most mice, and it works pretty well on a trackpoint
89 return event->direction == GDK_SCROLL_SMOOTH && (source == GDK_SOURCE_TOUCHPAD || source == GDK_SOURCE_TOUCHSCREEN);
90}
91
92static bool isTouchEvent(GdkEventScroll* event)
93{
94 GdkDevice* device = gdk_event_get_source_device(reinterpret_cast<GdkEvent*>(event));
95 GdkInputSource source = gdk_device_get_source(device);
96
97 return source == GDK_SOURCE_TOUCHSCREEN;
98}
99
100FloatSize ViewGestureController::PendingSwipeTracker::scrollEventGetScrollingDeltas(GdkEventScroll* event)
101{
102 double multiplier = isTouchEvent(event) ? Scrollbar::pixelsPerLineStep() : gtkScrollDeltaMultiplier;
103
104 // GdkEventScroll deltas are inverted compared to NSEvent, so invert them again
105 return -FloatSize(event->delta_x, event->delta_y) * multiplier;
106}
107
108bool ViewGestureController::handleScrollWheelEvent(GdkEventScroll* event)
109{
110 return m_swipeProgressTracker.handleEvent(event) || m_pendingSwipeTracker.handleEvent(event);
111}
112
113void ViewGestureController::trackSwipeGesture(PlatformScrollEvent event, SwipeDirection direction, RefPtr<WebBackForwardListItem> targetItem)
114{
115 m_swipeProgressTracker.startTracking(WTFMove(targetItem), direction);
116 m_swipeProgressTracker.handleEvent(event);
117}
118
119ViewGestureController::SwipeProgressTracker::SwipeProgressTracker(WebPageProxy& webPageProxy, ViewGestureController& viewGestureController)
120 : m_viewGestureController(viewGestureController)
121 , m_webPageProxy(webPageProxy)
122{
123}
124
125void ViewGestureController::SwipeProgressTracker::startTracking(RefPtr<WebBackForwardListItem>&& targetItem, SwipeDirection direction)
126{
127 if (m_state != State::None)
128 return;
129
130 m_targetItem = targetItem;
131 m_direction = direction;
132 m_state = State::Pending;
133}
134
135void ViewGestureController::SwipeProgressTracker::reset()
136{
137 m_targetItem = nullptr;
138 m_state = State::None;
139
140 if (m_tickCallbackID) {
141 GtkWidget* widget = m_webPageProxy.viewWidget();
142 gtk_widget_remove_tick_callback(widget, m_tickCallbackID);
143 m_tickCallbackID = 0;
144 }
145
146 m_progress = 0;
147 m_startProgress = 0;
148 m_endProgress = 0;
149
150 m_startTime = 0_ms;
151 m_endTime = 0_ms;
152 m_prevTime = 0_ms;
153 m_velocity = 0;
154 m_cancelled = false;
155}
156
157bool ViewGestureController::SwipeProgressTracker::handleEvent(GdkEventScroll* event)
158{
159 // Don't allow scrolling while the next page is loading
160 if (m_state == State::Finishing)
161 return true;
162
163 // Stop current animation, if any
164 if (m_state == State::Animating) {
165 GtkWidget* widget = m_webPageProxy.viewWidget();
166 gtk_widget_remove_tick_callback(widget, m_tickCallbackID);
167 m_tickCallbackID = 0;
168
169 m_cancelled = false;
170 m_state = State::Pending;
171 }
172
173 if (m_state == State::Pending) {
174 m_viewGestureController.beginSwipeGesture(m_targetItem.get(), m_direction);
175 m_state = State::Scrolling;
176 }
177
178 if (m_state != State::Scrolling)
179 return false;
180
181 if (isEventStop(event)) {
182 startAnimation();
183 return false;
184 }
185
186 double deltaX = -event->delta_x;
187 if (isTouchEvent(event))
188 deltaX *= (double) Scrollbar::pixelsPerLineStep() / m_webPageProxy.viewSize().width();
189 else
190 deltaX *= gtkScrollDeltaMultiplier / swipeTouchpadBaseWidth;
191
192 Seconds time = Seconds::fromMilliseconds(event->time);
193 if (time != m_prevTime)
194 m_velocity = deltaX / (time - m_prevTime).milliseconds();
195
196 m_prevTime = time;
197 m_progress += deltaX;
198
199 bool swipingLeft = m_viewGestureController.isPhysicallySwipingLeft(m_direction);
200 float maxProgress = swipingLeft ? 1 : 0;
201 float minProgress = !swipingLeft ? -1 : 0;
202 m_progress = clampTo<float>(m_progress, minProgress, maxProgress);
203
204 m_viewGestureController.handleSwipeGesture(m_targetItem.get(), m_progress, m_direction);
205
206 return true;
207}
208
209bool ViewGestureController::SwipeProgressTracker::shouldCancel()
210{
211 bool swipingLeft = m_viewGestureController.isPhysicallySwipingLeft(m_direction);
212
213 if (swipingLeft && m_velocity < 0)
214 return true;
215
216 if (!swipingLeft && m_velocity > 0)
217 return true;
218
219 return (abs(m_progress) < swipeCancelArea && abs(m_velocity) < swipeCancelVelocityThreshold);
220}
221
222void ViewGestureController::SwipeProgressTracker::startAnimation()
223{
224 m_cancelled = shouldCancel();
225
226 m_state = State::Animating;
227 m_viewGestureController.willEndSwipeGesture(*m_targetItem, m_cancelled);
228
229 m_startProgress = m_progress;
230 if (m_cancelled)
231 m_endProgress = 0;
232 else
233 m_endProgress = m_viewGestureController.isPhysicallySwipingLeft(m_direction) ? 1 : -1;
234
235 double velocity = swipeAnimationBaseVelocity;
236 if ((m_endProgress - m_progress) * m_velocity > 0)
237 velocity = m_velocity;
238
239 Seconds duration = Seconds::fromMilliseconds(std::abs((m_progress - m_endProgress) / velocity * swipeAnimationDurationMultiplier));
240 duration = clampTo<Seconds>(duration, swipeMinAnimationDuration, swipeMaxAnimationDuration);
241
242 GtkWidget* widget = m_webPageProxy.viewWidget();
243 m_startTime = Seconds::fromMicroseconds(gdk_frame_clock_get_frame_time(gtk_widget_get_frame_clock(widget)));
244 m_endTime = m_startTime + duration;
245
246 m_tickCallbackID = gtk_widget_add_tick_callback(widget, [](GtkWidget*, GdkFrameClock* frameClock, gpointer userData) -> gboolean {
247 auto* tracker = static_cast<SwipeProgressTracker*>(userData);
248 return tracker->onAnimationTick(frameClock);
249 }, this, nullptr);
250}
251
252static inline double easeOutCubic(double t)
253{
254 double p = t - 1;
255 return p * p * p + 1;
256}
257
258gboolean ViewGestureController::SwipeProgressTracker::onAnimationTick(GdkFrameClock* frameClock)
259{
260 ASSERT(m_state == State::Animating);
261 ASSERT(m_endTime > m_startTime);
262
263 Seconds frameTime = Seconds::fromMicroseconds(gdk_frame_clock_get_frame_time(frameClock));
264
265 double animationProgress = (frameTime - m_startTime) / (m_endTime - m_startTime);
266 if (animationProgress > 1)
267 animationProgress = 1;
268
269 m_progress = m_startProgress + (m_endProgress - m_startProgress) * easeOutCubic(animationProgress);
270
271 m_viewGestureController.handleSwipeGesture(m_targetItem.get(), m_progress, m_direction);
272 if (frameTime >= m_endTime) {
273 m_tickCallbackID = 0;
274 endAnimation();
275 return G_SOURCE_REMOVE;
276 }
277
278 return G_SOURCE_CONTINUE;
279}
280
281void ViewGestureController::SwipeProgressTracker::endAnimation()
282{
283 m_state = State::Finishing;
284 m_viewGestureController.endSwipeGesture(m_targetItem.get(), m_cancelled);
285}
286
287void ViewGestureController::beginSwipeGesture(WebBackForwardListItem* targetItem, SwipeDirection direction)
288{
289 ASSERT(targetItem);
290
291 m_webPageProxy.navigationGestureDidBegin();
292
293 willBeginGesture(ViewGestureType::Swipe);
294
295 if (auto* snapshot = targetItem->snapshot()) {
296 m_currentSwipeSnapshot = snapshot;
297
298 FloatSize viewSize(m_webPageProxy.viewSize());
299 if (snapshot->hasImage() && shouldUseSnapshotForSize(*snapshot, viewSize, 0))
300 m_currentSwipeSnapshotPattern = adoptRef(cairo_pattern_create_for_surface(snapshot->surface()));
301
302 Color color = snapshot->backgroundColor();
303 if (color.isValid()) {
304 m_backgroundColorForCurrentSnapshot = color;
305 if (!m_currentSwipeSnapshotPattern) {
306 double red, green, blue, alpha;
307 color.getRGBA(red, green, blue, alpha);
308 m_currentSwipeSnapshotPattern = adoptRef(cairo_pattern_create_rgba(red, green, blue, alpha));
309 }
310 }
311 }
312
313 if (!m_currentSwipeSnapshotPattern)
314 m_currentSwipeSnapshotPattern = adoptRef(cairo_pattern_create_rgb(1, 1, 1));
315}
316
317void ViewGestureController::handleSwipeGesture(WebBackForwardListItem*, double, SwipeDirection)
318{
319 gtk_widget_queue_draw(m_webPageProxy.viewWidget());
320}
321
322void ViewGestureController::draw(cairo_t* cr, cairo_pattern_t* pageGroup)
323{
324 bool swipingLeft = isPhysicallySwipingLeft(m_swipeProgressTracker.direction());
325 float progress = m_swipeProgressTracker.progress();
326
327 double width = m_webPageProxy.drawingArea()->size().width();
328 double height = m_webPageProxy.drawingArea()->size().height();
329
330 double swipingLayerOffset = (swipingLeft ? 0 : width) + floor(width * progress);
331
332 double dimmingProgress = swipingLeft ? 1 - progress : -progress;
333
334 double remainingSwipeDistance = dimmingProgress * width;
335 double shadowFadeDistance = swipeOverlayShadowWidth;
336
337 double shadowOpacity = swipeOverlayShadowOpacity;
338 if (remainingSwipeDistance < shadowFadeDistance)
339 shadowOpacity = (remainingSwipeDistance / shadowFadeDistance) * swipeOverlayShadowOpacity;
340
341 RefPtr<cairo_pattern_t> shadowPattern = adoptRef(cairo_pattern_create_linear(0, 0, -swipeOverlayShadowWidth, 0));
342 for (int i = 0; i < 16; i++) {
343 double offset = swipeOverlayShadowGradientOffsets[i];
344 double alpha = swipeOverlayShadowGradientAlpha[i] * shadowOpacity;
345 cairo_pattern_add_color_stop_rgba(shadowPattern.get(), offset, 0, 0, 0, alpha);
346 }
347
348 cairo_save(cr);
349
350 cairo_rectangle(cr, 0, 0, swipingLayerOffset, height);
351 cairo_set_source(cr, swipingLeft ? m_currentSwipeSnapshotPattern.get() : pageGroup);
352 cairo_fill_preserve(cr);
353
354 cairo_set_source_rgba(cr, 0, 0, 0, dimmingProgress * swipeOverlayDimmingOpacity);
355 cairo_fill(cr);
356
357 cairo_translate(cr, swipingLayerOffset, 0);
358
359 if (progress) {
360 cairo_rectangle(cr, -swipeOverlayShadowWidth, 0, swipeOverlayShadowWidth, height);
361 cairo_set_source(cr, shadowPattern.get());
362 cairo_fill(cr);
363 }
364
365 cairo_rectangle(cr, 0, 0, width - swipingLayerOffset, height);
366 cairo_set_source(cr, swipingLeft ? pageGroup : m_currentSwipeSnapshotPattern.get());
367 cairo_fill(cr);
368
369 cairo_restore(cr);
370}
371
372void ViewGestureController::removeSwipeSnapshot()
373{
374 m_snapshotRemovalTracker.reset();
375
376 m_hasOutstandingRepaintRequest = false;
377
378 if (m_activeGestureType != ViewGestureType::Swipe)
379 return;
380
381 m_currentSwipeSnapshotPattern = nullptr;
382
383 m_currentSwipeSnapshot = nullptr;
384
385 m_webPageProxy.navigationGestureSnapshotWasRemoved();
386
387 m_backgroundColorForCurrentSnapshot = Color();
388
389 didEndGesture();
390
391 m_swipeProgressTracker.reset();
392}
393
394bool ViewGestureController::beginSimulatedSwipeInDirectionForTesting(SwipeDirection)
395{
396 return false;
397}
398
399bool ViewGestureController::completeSimulatedSwipeInDirectionForTesting(SwipeDirection)
400{
401 return false;
402}
403
404} // namespace WebKit
405