blob: 70285b7c4666742c9b621482e38555b720caaca8 [file] [log] [blame]
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "third_party/blink/renderer/modules/media_controls/elements/media_control_loading_panel_element.h"
#include "third_party/blink/public/strings/grit/blink_strings.h"
#include "third_party/blink/renderer/core/css/css_style_declaration.h"
#include "third_party/blink/renderer/core/dom/events/event.h"
#include "third_party/blink/renderer/core/dom/events/event_listener.h"
#include "third_party/blink/renderer/core/dom/shadow_root.h"
#include "third_party/blink/renderer/core/html/html_div_element.h"
#include "third_party/blink/renderer/core/html/html_style_element.h"
#include "third_party/blink/renderer/core/html/media/html_media_element.h"
#include "third_party/blink/renderer/core/style/computed_style.h"
#include "third_party/blink/renderer/modules/media_controls/elements/media_control_elements_helper.h"
#include "third_party/blink/renderer/modules/media_controls/media_controls_impl.h"
#include "third_party/blink/renderer/modules/media_controls/media_controls_resource_loader.h"
#include "third_party/blink/renderer/platform/heap/heap.h"
#include "third_party/blink/renderer/platform/text/platform_locale.h"
namespace {
static const char kInfinite[] = "infinite";
bool IsInLoadingState(blink::MediaControlsImpl& controls) {
return controls.State() == blink::MediaControlsImpl::kLoadingMetadataPaused ||
controls.State() ==
blink::MediaControlsImpl::kLoadingMetadataPlaying ||
controls.State() == blink::MediaControlsImpl::kBuffering;
}
} // namespace
namespace blink {
MediaControlLoadingPanelElement::MediaControlLoadingPanelElement(
MediaControlsImpl& media_controls)
: MediaControlDivElement(media_controls) {
SetShadowPseudoId(AtomicString("-internal-media-controls-loading-panel"));
setAttribute(
html_names::kAriaLabelAttr,
WTF::AtomicString(GetLocale().QueryString(IDS_AX_MEDIA_LOADING_PANEL)));
setAttribute(html_names::kAriaLiveAttr, "polite");
CreateUserAgentShadowRoot();
// The loading panel should always start hidden.
SetIsWanted(false);
}
// The shadow DOM structure looks like:
//
// #root
// +- #spinner-frame
// +- #spinner
// +- #layer
// | +- #spinner-mask-1
// | | +- #spinner-mask-1-background
// \ +- #spinner-mask-2
// +- #spinner-mask-2-background
void MediaControlLoadingPanelElement::PopulateShadowDOM() {
ShadowRoot* shadow_root = GetShadowRoot();
DCHECK(!shadow_root->HasChildren());
// This stylesheet element and will contain rules that are specific to the
// loading panel. The shadow DOM protects these rules and rules from the
// parent DOM from bleeding across the shadow DOM boundary.
auto* style = MakeGarbageCollected<HTMLStyleElement>(GetDocument(),
CreateElementFlags());
style->setTextContent(
MediaControlsResourceLoader::GetShadowLoadingStyleSheet());
shadow_root->ParserAppendChild(style);
// The spinner frame is centers the spinner in the middle of the element and
// cuts off any overflowing content. It also contains a SVG mask which will
// overlay the spinner and cover up any rough edges created by the moving
// elements.
HTMLDivElement* spinner_frame =
MediaControlElementsHelper::CreateDivWithId("spinner-frame", shadow_root);
spinner_frame->SetShadowPseudoId(
"-internal-media-controls-loading-panel-spinner-frame");
// The spinner is responsible for rotating the elements below. The square
// edges will be cut off by the frame above.
HTMLDivElement* spinner =
MediaControlElementsHelper::CreateDivWithId("spinner", spinner_frame);
// The layer performs a secondary "fill-unfill-rotate" animation.
HTMLDivElement* layer =
MediaControlElementsHelper::CreateDivWithId("layer", spinner);
// The spinner is split into two halves, one on the left (1) and the other
// on the right (2). The mask elements stop the background from overlapping
// each other. The background elements rotate a SVG mask from the bottom to
// the top. The mask contains a white background with a transparent cutout
// that forms the look of the transparent spinner. The background should
// always be bigger than the mask in order to ensure there are no gaps
// created by the animation.
HTMLDivElement* mask1 =
MediaControlElementsHelper::CreateDivWithId("spinner-mask-1", layer);
mask1_background_ = MediaControlElementsHelper::CreateDiv(
"-internal-media-controls-loading-panel-spinner-mask-1-background",
mask1);
HTMLDivElement* mask2 =
MediaControlElementsHelper::CreateDivWithId("spinner-mask-2", layer);
mask2_background_ = MediaControlElementsHelper::CreateDiv(
"-internal-media-controls-loading-panel-spinner-mask-2-background",
mask2);
event_listener_ =
MakeGarbageCollected<MediaControlAnimationEventListener>(this);
}
void MediaControlLoadingPanelElement::RemovedFrom(
ContainerNode& insertion_point) {
if (event_listener_) {
event_listener_->Detach();
event_listener_.Clear();
}
MediaControlDivElement::RemovedFrom(insertion_point);
}
void MediaControlLoadingPanelElement::CleanupShadowDOM() {
// Clear the shadow DOM children and all references to it.
ShadowRoot* shadow_root = GetShadowRoot();
DCHECK(shadow_root->HasChildren());
if (event_listener_) {
event_listener_->Detach();
event_listener_.Clear();
}
shadow_root->RemoveChildren();
mask1_background_.Clear();
mask2_background_.Clear();
}
void MediaControlLoadingPanelElement::SetAnimationIterationCount(
const String& count_value) {
if (mask1_background_) {
mask1_background_->SetInlineStyleProperty(
CSSPropertyID::kAnimationIterationCount, count_value);
}
if (mask2_background_) {
mask2_background_->SetInlineStyleProperty(
CSSPropertyID::kAnimationIterationCount, count_value);
}
}
void MediaControlLoadingPanelElement::UpdateDisplayState() {
// If the media consols are playing then we should hide the element as
// soon as possible since we are obscuring the video.
if (GetMediaControls().State() == MediaControlsImpl::kPlaying &&
state_ != State::kHidden) {
HideAnimation();
return;
}
switch (state_) {
case State::kHidden:
// If the media controls are loading metadata then we should show the
// loading panel and insert it into the DOM.
if (IsInLoadingState(GetMediaControls()) && !controls_hidden_) {
PopulateShadowDOM();
SetIsWanted(true);
SetAnimationIterationCount(kInfinite);
state_ = State::kPlaying;
}
break;
case State::kPlaying:
// If the media controls are stopped then we should hide the loading
// panel, but not until the current cycle of animations is complete.
if (!IsInLoadingState(GetMediaControls())) {
SetAnimationIterationCount(WTF::String::Number(animation_count_ + 1));
state_ = State::kCoolingDown;
}
break;
case State::kCoolingDown:
// Do nothing.
break;
}
}
void MediaControlLoadingPanelElement::OnControlsHidden() {
controls_hidden_ = true;
// If the animation is currently playing, clean it up.
if (state_ != State::kHidden)
HideAnimation();
}
void MediaControlLoadingPanelElement::HideAnimation() {
DCHECK(state_ != State::kHidden);
SetIsWanted(false);
state_ = State::kHidden;
animation_count_ = 0;
CleanupShadowDOM();
}
void MediaControlLoadingPanelElement::OnControlsShown() {
controls_hidden_ = false;
UpdateDisplayState();
}
void MediaControlLoadingPanelElement::OnAnimationEnd() {
// If we have gone back to the loading metadata state (e.g. the source
// changed). Then we should jump back to playing.
if (IsInLoadingState(GetMediaControls())) {
state_ = State::kPlaying;
SetAnimationIterationCount(kInfinite);
return;
}
// The animation has finished so we can go back to the hidden state and
// cleanup the shadow DOM.
HideAnimation();
}
void MediaControlLoadingPanelElement::OnAnimationIteration() {
animation_count_ += 1;
}
Element& MediaControlLoadingPanelElement::WatchedAnimationElement() const {
DCHECK(mask1_background_);
return *mask1_background_;
}
void MediaControlLoadingPanelElement::Trace(Visitor* visitor) const {
MediaControlAnimationEventListener::Observer::Trace(visitor);
MediaControlDivElement::Trace(visitor);
visitor->Trace(event_listener_);
visitor->Trace(mask1_background_);
visitor->Trace(mask2_background_);
}
} // namespace blink