Use "= default" to denote default constructor or destructor
[WebKit-https.git] / Source / WebCore / html / track / VTTRegion.cpp
1 /*
2  * Copyright (C) 2013 Google Inc.  All rights reserved.
3  * Copyright (C) 2014 Apple Inc.  All rights reserved.
4  *
5  * Redistribution and use in source and binary forms, with or without
6  * modification, are permitted provided that the following conditions are
7  * met:
8  *
9  *     * Redistributions of source code must retain the above copyright
10  * notice, this list of conditions and the following disclaimer.
11  *     * Redistributions in binary form must reproduce the above
12  * copyright notice, this list of conditions and the following disclaimer
13  * in the documentation and/or other materials provided with the
14  * distribution.
15  *     * Neither the name of Google Inc. nor the names of its
16  * contributors may be used to endorse or promote products derived from
17  * this software without specific prior written permission.
18  *
19  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30  */
31
32 #include "config.h"
33 #include "VTTRegion.h"
34
35 #if ENABLE(VIDEO_TRACK)
36
37 #include "DOMRect.h"
38 #include "DOMTokenList.h"
39 #include "ElementChildIterator.h"
40 #include "HTMLDivElement.h"
41 #include "HTMLParserIdioms.h"
42 #include "Logging.h"
43 #include "RenderElement.h"
44 #include "VTTCue.h"
45 #include "VTTScanner.h"
46 #include "WebVTTParser.h"
47 #include <wtf/MathExtras.h>
48
49 namespace WebCore {
50
51 // The default values are defined within the WebVTT Regions Spec.
52 // https://dvcs.w3.org/hg/text-tracks/raw-file/default/608toVTT/region.html
53
54 // Default region line-height (vh units)
55 static const float lineHeight = 5.33;
56
57 // Default scrolling animation time period (s).
58 static const Seconds scrollTime { 433_ms };
59
60 VTTRegion::VTTRegion(ScriptExecutionContext& context)
61     : ContextDestructionObserver(&context)
62     , m_id(emptyString())
63     , m_scrollTimer(*this, &VTTRegion::scrollTimerFired)
64 {
65 }
66
67 VTTRegion::~VTTRegion() = default;
68
69 void VTTRegion::setTrack(TextTrack* track)
70 {
71     m_track = track;
72 }
73
74 void VTTRegion::setId(const String& id)
75 {
76     m_id = id;
77 }
78
79 ExceptionOr<void> VTTRegion::setWidth(double value)
80 {
81     if (!(value >= 0 && value <= 100))
82         return Exception { IndexSizeError };
83     m_width = value;
84     return { };
85 }
86
87 ExceptionOr<void> VTTRegion::setHeight(int value)
88 {
89     if (value < 0)
90         return Exception { IndexSizeError };
91     m_heightInLines = value;
92     return { };
93 }
94
95 ExceptionOr<void> VTTRegion::setRegionAnchorX(double value)
96 {
97     if (!(value >= 0 && value <= 100))
98         return Exception { IndexSizeError };
99     m_regionAnchor.setX(value);
100     return { };
101 }
102
103 ExceptionOr<void> VTTRegion::setRegionAnchorY(double value)
104 {
105     if (!(value >= 0 && value <= 100))
106         return Exception { IndexSizeError };
107     m_regionAnchor.setY(value);
108     return { };
109 }
110
111 ExceptionOr<void> VTTRegion::setViewportAnchorX(double value)
112 {
113     if (!(value >= 0 && value <= 100))
114         return Exception { IndexSizeError };
115     m_viewportAnchor.setX(value);
116     return { };
117 }
118
119 ExceptionOr<void> VTTRegion::setViewportAnchorY(double value)
120 {
121     if (!(value >= 0 && value <= 100))
122         return Exception { IndexSizeError };
123     m_viewportAnchor.setY(value);
124     return { };
125 }
126
127 static const AtomicString& upKeyword()
128 {
129     static NeverDestroyed<const AtomicString> upKeyword("up", AtomicString::ConstructFromLiteral);
130     return upKeyword;
131 }
132
133 const AtomicString& VTTRegion::scroll() const
134 {
135     return m_scroll ? upKeyword() : emptyAtom();
136 }
137
138 ExceptionOr<void> VTTRegion::setScroll(const AtomicString& value)
139 {
140     if (value.isEmpty()) {
141         m_scroll = false;
142         return { };
143     }
144     if (value == upKeyword()) {
145         m_scroll = true;
146         return { };
147     }
148     return Exception { SyntaxError };
149 }
150
151 void VTTRegion::updateParametersFromRegion(const VTTRegion& other)
152 {
153     m_heightInLines = other.m_heightInLines;
154     m_width = other.m_width;
155     m_regionAnchor = other.m_regionAnchor;
156     m_viewportAnchor = other.m_viewportAnchor;
157     m_scroll = other.m_scroll;
158 }
159
160 void VTTRegion::setRegionSettings(const String& inputString)
161 {
162     m_settings = inputString;
163     VTTScanner input(inputString);
164
165     while (!input.isAtEnd()) {
166         input.skipWhile<WebVTTParser::isValidSettingDelimiter>();
167         if (input.isAtEnd())
168             break;
169
170         // Scan the name part.
171         RegionSetting name = scanSettingName(input);
172
173         // Verify that we're looking at a '='.
174         if (name == None || !input.scan('=')) {
175             input.skipUntil<isHTMLSpace<UChar>>();
176             continue;
177         }
178
179         // Scan the value part.
180         parseSettingValue(name, input);
181     }
182 }
183
184 VTTRegion::RegionSetting VTTRegion::scanSettingName(VTTScanner& input)
185 {
186     if (input.scan("id"))
187         return Id;
188     if (input.scan("height"))
189         return Height;
190     if (input.scan("width"))
191         return Width;
192     if (input.scan("viewportanchor"))
193         return ViewportAnchor;
194     if (input.scan("regionanchor"))
195         return RegionAnchor;
196     if (input.scan("scroll"))
197         return Scroll;
198
199     return None;
200 }
201
202 static inline bool parsedEntireRun(const VTTScanner& input, const VTTScanner::Run& run)
203 {
204     return input.isAt(run.end()); 
205 }
206
207 void VTTRegion::parseSettingValue(RegionSetting setting, VTTScanner& input)
208 {
209     VTTScanner::Run valueRun = input.collectUntil<isHTMLSpace<UChar>>();
210
211     switch (setting) {
212     case Id: {
213         String stringValue = input.extractString(valueRun);
214         if (stringValue.find("-->") == notFound)
215             m_id = stringValue;
216         break;
217     }
218     case Width: {
219         float floatWidth;
220         if (WebVTTParser::parseFloatPercentageValue(input, floatWidth) && parsedEntireRun(input, valueRun))
221             m_width = floatWidth;
222         else
223             LOG(Media, "VTTRegion::parseSettingValue, invalid Width");
224         break;
225     }
226     case Height: {
227         int number;
228         if (input.scanDigits(number) && parsedEntireRun(input, valueRun))
229             m_heightInLines = number;
230         else
231             LOG(Media, "VTTRegion::parseSettingValue, invalid Height");
232         break;
233     }
234     case RegionAnchor: {
235         FloatPoint anchor;
236         if (WebVTTParser::parseFloatPercentageValuePair(input, ',', anchor) && parsedEntireRun(input, valueRun))
237             m_regionAnchor = anchor;
238         else
239             LOG(Media, "VTTRegion::parseSettingValue, invalid RegionAnchor");
240         break;
241     }
242     case ViewportAnchor: {
243         FloatPoint anchor;
244         if (WebVTTParser::parseFloatPercentageValuePair(input, ',', anchor) && parsedEntireRun(input, valueRun))
245             m_viewportAnchor = anchor;
246         else
247             LOG(Media, "VTTRegion::parseSettingValue, invalid ViewportAnchor");
248         break;
249     }
250     case Scroll:
251         if (input.scanRun(valueRun, upKeyword()))
252             m_scroll = true;
253         else
254             LOG(Media, "VTTRegion::parseSettingValue, invalid Scroll");
255         break;
256     case None:
257         break;
258     }
259
260     input.skipRun(valueRun);
261 }
262
263 const AtomicString& VTTRegion::textTrackCueContainerScrollingClass()
264 {
265     static NeverDestroyed<const AtomicString> trackRegionCueContainerScrollingClass("scrolling", AtomicString::ConstructFromLiteral);
266
267     return trackRegionCueContainerScrollingClass;
268 }
269
270 const AtomicString& VTTRegion::textTrackCueContainerShadowPseudoId()
271 {
272     static NeverDestroyed<const AtomicString> trackRegionCueContainerPseudoId("-webkit-media-text-track-region-container", AtomicString::ConstructFromLiteral);
273
274     return trackRegionCueContainerPseudoId;
275 }
276
277 const AtomicString& VTTRegion::textTrackRegionShadowPseudoId()
278 {
279     static NeverDestroyed<const AtomicString> trackRegionShadowPseudoId("-webkit-media-text-track-region", AtomicString::ConstructFromLiteral);
280
281     return trackRegionShadowPseudoId;
282 }
283
284 void VTTRegion::appendTextTrackCueBox(Ref<VTTCueBox>&& displayBox)
285 {
286     ASSERT(m_cueContainer);
287
288     if (m_cueContainer->contains(displayBox.ptr()))
289         return;
290
291     m_cueContainer->appendChild(displayBox);
292     displayLastTextTrackCueBox();
293 }
294
295 void VTTRegion::displayLastTextTrackCueBox()
296 {
297     ASSERT(m_cueContainer);
298
299     // The container needs to be rendered, if it is not empty and the region is not currently scrolling.
300     if (!m_cueContainer->renderer() || !m_cueContainer->hasChildNodes() || m_scrollTimer.isActive())
301         return;
302
303     // If it's a scrolling region, add the scrolling class.
304     if (isScrollingRegion())
305         m_cueContainer->classList().add(textTrackCueContainerScrollingClass());
306
307     float regionBottom = m_regionDisplayTree->getBoundingClientRect()->bottom();
308
309     // Find first cue that is not entirely displayed and scroll it upwards.
310     for (auto& child : childrenOfType<Element>(*m_cueContainer)) {
311         auto rect = child.getBoundingClientRect();
312         float childTop = rect->top();
313         float childBottom = rect->bottom();
314
315         if (regionBottom >= childBottom)
316             continue;
317
318         float height = childBottom - childTop;
319
320         m_currentTop -= std::min(height, childBottom - regionBottom);
321         m_cueContainer->setInlineStyleProperty(CSSPropertyTop, m_currentTop, CSSPrimitiveValue::CSS_PX);
322
323         startTimer();
324         break;
325     }
326 }
327
328 void VTTRegion::willRemoveTextTrackCueBox(VTTCueBox* box)
329 {
330     LOG(Media, "VTTRegion::willRemoveTextTrackCueBox");
331     ASSERT(m_cueContainer->contains(box));
332
333     double boxHeight = box->getBoundingClientRect()->bottom() - box->getBoundingClientRect()->top();
334
335     m_cueContainer->classList().remove(textTrackCueContainerScrollingClass());
336
337     m_currentTop += boxHeight;
338     m_cueContainer->setInlineStyleProperty(CSSPropertyTop, m_currentTop, CSSPrimitiveValue::CSS_PX);
339 }
340
341 HTMLDivElement& VTTRegion::getDisplayTree()
342 {
343     if (!m_regionDisplayTree) {
344         m_regionDisplayTree = HTMLDivElement::create(downcast<Document>(*m_scriptExecutionContext));
345         prepareRegionDisplayTree();
346     }
347
348     return *m_regionDisplayTree;
349 }
350
351 void VTTRegion::prepareRegionDisplayTree()
352 {
353     ASSERT(m_regionDisplayTree);
354
355     // 7.2 Prepare region CSS boxes
356
357     // FIXME: Change the code below to use viewport units when
358     // http://crbug/244618 is fixed.
359
360     // Let regionWidth be the text track region width.
361     // Let width be 'regionWidth vw' ('vw' is a CSS unit)
362     m_regionDisplayTree->setInlineStyleProperty(CSSPropertyWidth, m_width, CSSPrimitiveValue::CSS_PERCENTAGE);
363
364     // Let lineHeight be '0.0533vh' ('vh' is a CSS unit) and regionHeight be
365     // the text track region height. Let height be 'lineHeight' multiplied
366     // by regionHeight.
367     double height = lineHeight * m_heightInLines;
368     m_regionDisplayTree->setInlineStyleProperty(CSSPropertyHeight, height, CSSPrimitiveValue::CSS_VH);
369
370     // Let viewportAnchorX be the x dimension of the text track region viewport
371     // anchor and regionAnchorX be the x dimension of the text track region
372     // anchor. Let leftOffset be regionAnchorX multiplied by width divided by
373     // 100.0. Let left be leftOffset subtracted from 'viewportAnchorX vw'.
374     double leftOffset = m_regionAnchor.x() * m_width / 100;
375     m_regionDisplayTree->setInlineStyleProperty(CSSPropertyLeft, m_viewportAnchor.x() - leftOffset, CSSPrimitiveValue::CSS_PERCENTAGE);
376
377     // Let viewportAnchorY be the y dimension of the text track region viewport
378     // anchor and regionAnchorY be the y dimension of the text track region
379     // anchor. Let topOffset be regionAnchorY multiplied by height divided by
380     // 100.0. Let top be topOffset subtracted from 'viewportAnchorY vh'.
381     double topOffset = m_regionAnchor.y() * height / 100;
382     m_regionDisplayTree->setInlineStyleProperty(CSSPropertyTop, m_viewportAnchor.y() - topOffset, CSSPrimitiveValue::CSS_PERCENTAGE);
383
384     // The cue container is used to wrap the cues and it is the object which is
385     // gradually scrolled out as multiple cues are appended to the region.
386     m_cueContainer = HTMLDivElement::create(downcast<Document>(*m_scriptExecutionContext));
387     m_cueContainer->setInlineStyleProperty(CSSPropertyTop, 0.0f, CSSPrimitiveValue::CSS_PX);
388
389     m_cueContainer->setPseudo(textTrackCueContainerShadowPseudoId());
390     m_regionDisplayTree->appendChild(*m_cueContainer);
391
392     // 7.5 Every WebVTT region object is initialised with the following CSS
393     m_regionDisplayTree->setPseudo(textTrackRegionShadowPseudoId());
394 }
395
396 void VTTRegion::startTimer()
397 {
398     LOG(Media, "VTTRegion::startTimer");
399
400     if (m_scrollTimer.isActive())
401         return;
402
403     Seconds duration = isScrollingRegion() ? scrollTime : 0_s;
404     m_scrollTimer.startOneShot(duration);
405 }
406
407 void VTTRegion::stopTimer()
408 {
409     LOG(Media, "VTTRegion::stopTimer");
410
411     if (m_scrollTimer.isActive())
412         m_scrollTimer.stop();
413 }
414
415 void VTTRegion::scrollTimerFired()
416 {
417     LOG(Media, "VTTRegion::scrollTimerFired");
418
419     stopTimer();
420     displayLastTextTrackCueBox();
421 }
422
423 } // namespace WebCore
424
425 #endif