Restrict SVG filters to accessible security origins
[WebKit-https.git] / Source / WebCore / platform / Scrollbar.cpp
1 /*
2  * Copyright (C) 2004, 2006, 2008, 2015 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. ``AS IS'' AND ANY
14  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
17  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
18  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
19  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
20  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
21  * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 #include "config.h"
27 #include "Scrollbar.h"
28
29 #include "FrameView.h"
30 #include "GraphicsContext.h"
31 #include "PlatformMouseEvent.h"
32 #include "ScrollAnimator.h"
33 #include "ScrollView.h"
34 #include "ScrollableArea.h"
35 #include "ScrollbarTheme.h"
36 #include <algorithm>
37
38 #if PLATFORM(GTK)
39 // The position of the scrollbar thumb affects the appearance of the steppers, so
40 // when the thumb moves, we have to invalidate them for painting.
41 #define THUMB_POSITION_AFFECTS_BUTTONS
42 #endif
43
44 namespace WebCore {
45
46 Ref<Scrollbar> Scrollbar::createNativeScrollbar(ScrollableArea& scrollableArea, ScrollbarOrientation orientation, ScrollbarControlSize size)
47 {
48     return adoptRef(*new Scrollbar(scrollableArea, orientation, size));
49 }
50
51 int Scrollbar::maxOverlapBetweenPages()
52 {
53     static int maxOverlapBetweenPages = ScrollbarTheme::theme().maxOverlapBetweenPages();
54     return maxOverlapBetweenPages;
55 }
56
57 Scrollbar::Scrollbar(ScrollableArea& scrollableArea, ScrollbarOrientation orientation, ScrollbarControlSize controlSize, ScrollbarTheme* customTheme, bool isCustomScrollbar)
58     : m_scrollableArea(scrollableArea)
59     , m_orientation(orientation)
60     , m_controlSize(controlSize)
61     , m_theme(customTheme ? *customTheme : ScrollbarTheme::theme())
62     , m_visibleSize(0)
63     , m_totalSize(0)
64     , m_currentPos(0)
65     , m_dragOrigin(0)
66     , m_lineStep(0)
67     , m_pageStep(0)
68     , m_pixelStep(1)
69     , m_hoveredPart(NoPart)
70     , m_pressedPart(NoPart)
71     , m_pressedPos(0)
72     , m_scrollPos(0)
73     , m_draggingDocument(false)
74     , m_documentDragPos(0)
75     , m_enabled(true)
76     , m_scrollTimer(*this, &Scrollbar::autoscrollTimerFired)
77     , m_suppressInvalidation(false)
78     , m_isAlphaLocked(false)
79     , m_isCustomScrollbar(isCustomScrollbar)
80     , m_weakPtrFactory(this)
81 {
82     theme().registerScrollbar(*this);
83
84     // FIXME: This is ugly and would not be necessary if we fix cross-platform code to actually query for
85     // scrollbar thickness and use it when sizing scrollbars (rather than leaving one dimension of the scrollbar
86     // alone when sizing).
87     int thickness = theme().scrollbarThickness(controlSize);
88     Widget::setFrameRect(IntRect(0, 0, thickness, thickness));
89
90     m_currentPos = static_cast<float>(m_scrollableArea.scrollOffset(m_orientation));
91 }
92
93 Scrollbar::~Scrollbar()
94 {
95     stopTimerIfNeeded();
96     
97     theme().unregisterScrollbar(*this);
98 }
99
100 int Scrollbar::occupiedWidth() const
101 {
102     return isOverlayScrollbar() ? 0 : width();
103 }
104
105 int Scrollbar::occupiedHeight() const
106 {
107     return isOverlayScrollbar() ? 0 : height();
108 }
109
110 void Scrollbar::offsetDidChange()
111 {
112     float position = static_cast<float>(m_scrollableArea.scrollOffset(m_orientation));
113     if (position == m_currentPos)
114         return;
115
116     int oldThumbPosition = theme().thumbPosition(*this);
117     m_currentPos = position;
118     updateThumbPosition();
119     if (m_pressedPart == ThumbPart)
120         setPressedPos(m_pressedPos + theme().thumbPosition(*this) - oldThumbPosition);
121 }
122
123 void Scrollbar::setProportion(int visibleSize, int totalSize)
124 {
125     if (visibleSize == m_visibleSize && totalSize == m_totalSize)
126         return;
127
128     m_visibleSize = visibleSize;
129     m_totalSize = totalSize;
130
131     updateThumbProportion();
132 }
133
134 void Scrollbar::setSteps(int lineStep, int pageStep, int pixelsPerStep)
135 {
136     m_lineStep = lineStep;
137     m_pageStep = pageStep;
138     m_pixelStep = 1.0f / pixelsPerStep;
139 }
140
141 void Scrollbar::updateThumb()
142 {
143 #ifdef THUMB_POSITION_AFFECTS_BUTTONS
144     invalidate();
145 #else
146     theme().invalidateParts(*this, ForwardTrackPart | BackTrackPart | ThumbPart);
147 #endif
148 }
149
150 void Scrollbar::updateThumbPosition()
151 {
152     updateThumb();
153 }
154
155 void Scrollbar::updateThumbProportion()
156 {
157     updateThumb();
158 }
159
160 void Scrollbar::paint(GraphicsContext& context, const IntRect& damageRect, Widget::SecurityOriginPaintPolicy)
161 {
162     if (context.updatingControlTints() && theme().supportsControlTints()) {
163         invalidate();
164         return;
165     }
166
167     if (context.paintingDisabled() || !frameRect().intersects(damageRect))
168         return;
169
170     if (!theme().paint(*this, context, damageRect))
171         Widget::paint(context, damageRect);
172 }
173
174 void Scrollbar::autoscrollTimerFired()
175 {
176     autoscrollPressedPart(theme().autoscrollTimerDelay());
177 }
178
179 static bool thumbUnderMouse(Scrollbar* scrollbar)
180 {
181     int thumbPos = scrollbar->theme().trackPosition(*scrollbar) + scrollbar->theme().thumbPosition(*scrollbar);
182     int thumbLength = scrollbar->theme().thumbLength(*scrollbar);
183     return scrollbar->pressedPos() >= thumbPos && scrollbar->pressedPos() < thumbPos + thumbLength;
184 }
185
186 void Scrollbar::autoscrollPressedPart(Seconds delay)
187 {
188     // Don't do anything for the thumb or if nothing was pressed.
189     if (m_pressedPart == ThumbPart || m_pressedPart == NoPart)
190         return;
191
192     // Handle the track.
193     if ((m_pressedPart == BackTrackPart || m_pressedPart == ForwardTrackPart) && thumbUnderMouse(this)) {
194         theme().invalidatePart(*this, m_pressedPart);
195         setHoveredPart(ThumbPart);
196         return;
197     }
198
199     // Handle the arrows and track.
200     if (m_scrollableArea.scroll(pressedPartScrollDirection(), pressedPartScrollGranularity()))
201         startTimerIfNeeded(delay);
202 }
203
204 void Scrollbar::startTimerIfNeeded(Seconds delay)
205 {
206     // Don't do anything for the thumb.
207     if (m_pressedPart == ThumbPart)
208         return;
209
210     // Handle the track.  We halt track scrolling once the thumb is level
211     // with us.
212     if ((m_pressedPart == BackTrackPart || m_pressedPart == ForwardTrackPart) && thumbUnderMouse(this)) {
213         theme().invalidatePart(*this, m_pressedPart);
214         setHoveredPart(ThumbPart);
215         return;
216     }
217
218     // We can't scroll if we've hit the beginning or end.
219     ScrollDirection dir = pressedPartScrollDirection();
220     if (dir == ScrollUp || dir == ScrollLeft) {
221         if (m_currentPos == 0)
222             return;
223     } else {
224         if (m_currentPos == maximum())
225             return;
226     }
227
228     m_scrollTimer.startOneShot(delay);
229 }
230
231 void Scrollbar::stopTimerIfNeeded()
232 {
233     if (m_scrollTimer.isActive())
234         m_scrollTimer.stop();
235 }
236
237 ScrollDirection Scrollbar::pressedPartScrollDirection()
238 {
239     if (m_orientation == HorizontalScrollbar) {
240         if (m_pressedPart == BackButtonStartPart || m_pressedPart == BackButtonEndPart || m_pressedPart == BackTrackPart)
241             return ScrollLeft;
242         return ScrollRight;
243     } else {
244         if (m_pressedPart == BackButtonStartPart || m_pressedPart == BackButtonEndPart || m_pressedPart == BackTrackPart)
245             return ScrollUp;
246         return ScrollDown;
247     }
248 }
249
250 ScrollGranularity Scrollbar::pressedPartScrollGranularity()
251 {
252     if (m_pressedPart == BackButtonStartPart || m_pressedPart == BackButtonEndPart ||  m_pressedPart == ForwardButtonStartPart || m_pressedPart == ForwardButtonEndPart)
253         return ScrollByLine;
254     return ScrollByPage;
255 }
256
257 void Scrollbar::moveThumb(int pos, bool draggingDocument)
258 {
259     int delta = pos - m_pressedPos;
260
261     if (draggingDocument) {
262         if (m_draggingDocument)
263             delta = pos - m_documentDragPos;
264         m_draggingDocument = true;
265         FloatPoint currentPosition = m_scrollableArea.scrollAnimator().currentPosition();
266         int destinationPosition = (m_orientation == HorizontalScrollbar ? currentPosition.x() : currentPosition.y()) + delta;
267         if (delta > 0)
268             destinationPosition = std::min(destinationPosition + delta, maximum());
269         else if (delta < 0)
270             destinationPosition = std::max(destinationPosition + delta, 0);
271         m_scrollableArea.scrollToOffsetWithoutAnimation(m_orientation, destinationPosition);
272         m_documentDragPos = pos;
273         return;
274     }
275
276     if (m_draggingDocument) {
277         delta += m_pressedPos - m_documentDragPos;
278         m_draggingDocument = false;
279     }
280
281     // Drag the thumb.
282     int thumbPos = theme().thumbPosition(*this);
283     int thumbLen = theme().thumbLength(*this);
284     int trackLen = theme().trackLength(*this);
285     int maxPos = trackLen - thumbLen;
286     if (delta > 0)
287         delta = std::min(maxPos - thumbPos, delta);
288     else if (delta < 0)
289         delta = std::max(-thumbPos, delta);
290     
291     if (delta) {
292         float newPosition = static_cast<float>(thumbPos + delta) * maximum() / (trackLen - thumbLen);
293         m_scrollableArea.scrollToOffsetWithoutAnimation(m_orientation, newPosition);
294     }
295 }
296
297 void Scrollbar::setHoveredPart(ScrollbarPart part)
298 {
299     if (part == m_hoveredPart)
300         return;
301
302     if ((m_hoveredPart == NoPart || part == NoPart) && theme().invalidateOnMouseEnterExit())
303         invalidate();  // Just invalidate the whole scrollbar, since the buttons at either end change anyway.
304     else if (m_pressedPart == NoPart) {  // When there's a pressed part, we don't draw a hovered state, so there's no reason to invalidate.
305         theme().invalidatePart(*this, part);
306         theme().invalidatePart(*this, m_hoveredPart);
307     }
308     m_hoveredPart = part;
309 }
310
311 void Scrollbar::setPressedPart(ScrollbarPart part)
312 {
313     if (m_pressedPart != NoPart)
314         theme().invalidatePart(*this, m_pressedPart);
315     m_pressedPart = part;
316     if (m_pressedPart != NoPart)
317         theme().invalidatePart(*this, m_pressedPart);
318     else if (m_hoveredPart != NoPart)  // When we no longer have a pressed part, we can start drawing a hovered state on the hovered part.
319         theme().invalidatePart(*this, m_hoveredPart);
320 }
321
322 #if !PLATFORM(IOS)
323 bool Scrollbar::mouseMoved(const PlatformMouseEvent& evt)
324 {
325     if (m_pressedPart == ThumbPart) {
326         if (theme().shouldSnapBackToDragOrigin(*this, evt))
327             m_scrollableArea.scrollToOffsetWithoutAnimation(m_orientation, m_dragOrigin);
328         else {
329             moveThumb(m_orientation == HorizontalScrollbar ? 
330                       convertFromContainingWindow(evt.position()).x() :
331                       convertFromContainingWindow(evt.position()).y(), theme().shouldDragDocumentInsteadOfThumb(*this, evt));
332         }
333         return true;
334     }
335
336     if (m_pressedPart != NoPart)
337         m_pressedPos = (orientation() == HorizontalScrollbar ? convertFromContainingWindow(evt.position()).x() : convertFromContainingWindow(evt.position()).y());
338
339     ScrollbarPart part = theme().hitTest(*this, evt.position());
340     if (part != m_hoveredPart) {
341         if (m_pressedPart != NoPart) {
342             if (part == m_pressedPart) {
343                 // The mouse is moving back over the pressed part.  We
344                 // need to start up the timer action again.
345                 startTimerIfNeeded(theme().autoscrollTimerDelay());
346                 theme().invalidatePart(*this, m_pressedPart);
347             } else if (m_hoveredPart == m_pressedPart) {
348                 // The mouse is leaving the pressed part.  Kill our timer
349                 // if needed.
350                 stopTimerIfNeeded();
351                 theme().invalidatePart(*this, m_pressedPart);
352             }
353         } 
354         
355         setHoveredPart(part);
356     } 
357
358     return true;
359 }
360 #endif
361
362 void Scrollbar::mouseEntered()
363 {
364     m_scrollableArea.mouseEnteredScrollbar(this);
365 }
366
367 bool Scrollbar::mouseExited()
368 {
369     m_scrollableArea.mouseExitedScrollbar(this);
370     setHoveredPart(NoPart);
371     return true;
372 }
373
374 bool Scrollbar::mouseUp(const PlatformMouseEvent& mouseEvent)
375 {
376     setPressedPart(NoPart);
377     m_pressedPos = 0;
378     m_draggingDocument = false;
379     stopTimerIfNeeded();
380
381     m_scrollableArea.mouseIsDownInScrollbar(this, false);
382
383     // m_hoveredPart won't be updated until the next mouseMoved or mouseDown, so we have to hit test
384     // to really know if the mouse has exited the scrollbar on a mouseUp.
385     ScrollbarPart part = theme().hitTest(*this, mouseEvent.position());
386     if (part == NoPart)
387         m_scrollableArea.mouseExitedScrollbar(this);
388
389     return true;
390 }
391
392 bool Scrollbar::mouseDown(const PlatformMouseEvent& evt)
393 {
394     ScrollbarPart pressedPart = theme().hitTest(*this, evt.position());
395     auto action = theme().handleMousePressEvent(*this, evt, pressedPart);
396     if (action == ScrollbarButtonPressAction::None)
397         return true;
398
399     m_scrollableArea.mouseIsDownInScrollbar(this, true);
400     setPressedPart(pressedPart);
401
402     int pressedPosition = (orientation() == HorizontalScrollbar ? convertFromContainingWindow(evt.position()).x() : convertFromContainingWindow(evt.position()).y());
403     if (action == ScrollbarButtonPressAction::CenterOnThumb) {
404         setHoveredPart(ThumbPart);
405         setPressedPart(ThumbPart);
406         m_dragOrigin = m_currentPos;
407         // Set the pressed position to the middle of the thumb so that when we do the move, the delta
408         // will be from the current pixel position of the thumb to the new desired position for the thumb.
409         m_pressedPos = theme().trackPosition(*this) + theme().thumbPosition(*this) + theme().thumbLength(*this) / 2;
410         moveThumb(pressedPosition);
411         return true;
412     }
413
414     m_pressedPos = pressedPosition;
415
416     if (action == ScrollbarButtonPressAction::StartDrag)
417         m_dragOrigin = m_currentPos;
418
419     if (action == ScrollbarButtonPressAction::Scroll)
420         autoscrollPressedPart(theme().initialAutoscrollTimerDelay());
421
422     return true;
423 }
424
425 void Scrollbar::setEnabled(bool e)
426
427     if (m_enabled == e)
428         return;
429     m_enabled = e;
430     theme().updateEnabledState(*this);
431     invalidate();
432 }
433
434 bool Scrollbar::isOverlayScrollbar() const
435 {
436     return theme().usesOverlayScrollbars();
437 }
438
439 bool Scrollbar::shouldParticipateInHitTesting()
440 {
441     // Non-overlay scrollbars should always participate in hit testing.
442     if (!isOverlayScrollbar())
443         return true;
444     return m_scrollableArea.scrollAnimator().shouldScrollbarParticipateInHitTesting(this);
445 }
446
447 bool Scrollbar::isWindowActive() const
448 {
449     return m_scrollableArea.isActive();
450 }
451
452 void Scrollbar::invalidateRect(const IntRect& rect)
453 {
454     if (suppressInvalidation())
455         return;
456
457     m_scrollableArea.invalidateScrollbar(*this, rect);
458 }
459
460 IntRect Scrollbar::convertToContainingView(const IntRect& localRect) const
461 {
462     return m_scrollableArea.convertFromScrollbarToContainingView(*this, localRect);
463 }
464
465 IntRect Scrollbar::convertFromContainingView(const IntRect& parentRect) const
466 {
467     return m_scrollableArea.convertFromContainingViewToScrollbar(*this, parentRect);
468 }
469
470 IntPoint Scrollbar::convertToContainingView(const IntPoint& localPoint) const
471 {
472     return m_scrollableArea.convertFromScrollbarToContainingView(*this, localPoint);
473 }
474
475 IntPoint Scrollbar::convertFromContainingView(const IntPoint& parentPoint) const
476 {
477     return m_scrollableArea.convertFromContainingViewToScrollbar(*this, parentPoint);
478 }
479
480 bool Scrollbar::supportsUpdateOnSecondaryThread() const
481 {
482     // It's unfortunate that this needs to be done with an ifdef. Ideally there would be a way to feature-detect
483     // the necessary support within AppKit.
484 #if ENABLE(ASYNC_SCROLLING) && PLATFORM(MAC)
485     return !m_scrollableArea.forceUpdateScrollbarsOnMainThreadForPerformanceTesting()
486         && (m_scrollableArea.hasLayerForVerticalScrollbar() || m_scrollableArea.hasLayerForHorizontalScrollbar())
487         && m_scrollableArea.usesAsyncScrolling();
488 #else
489     return false;
490 #endif
491 }
492
493 } // namespace WebCore