chromium/ash/system/focus_mode/focus_mode_ending_moment_view.cc

// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ash/system/focus_mode/focus_mode_ending_moment_view.h"

#include <string>

#include "ash/public/cpp/resources/grit/ash_public_unscaled_resources.h"
#include "ash/public/cpp/shell_window_ids.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/pill_button.h"
#include "ash/style/typography.h"
#include "ash/system/focus_mode/focus_mode_controller.h"
#include "ash/system/focus_mode/focus_mode_util.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/chromeos/styles/cros_tokens_color_mappings.h"
#include "ui/compositor/layer.h"
#include "ui/gfx/geometry/size.h"
#include "ui/lottie/animation.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/controls/animated_image_view.h"
#include "ui/views/layout/box_layout_view.h"
#include "ui/views/widget/widget.h"

namespace ash {

namespace {

constexpr auto kTextContainerSize = gfx::Size(225, 72);
constexpr int kSpaceBetweenText = 4;
constexpr int kSpaceBetweenButtons = 8;
// The maximum width for the title is 202px, which is based on the width for the
// party-popper is 19px and the width for the space separator between the emoji
// and the title is 4px.
constexpr int kTitleMaximumWidth = 202;
constexpr auto kAnimationSize = gfx::Size(50, 50);

std::unique_ptr<views::Label> CreateTextLabel(
    gfx::HorizontalAlignment alignment,
    TypographyToken token,
    ui::ColorId color_id,
    bool allow_multiline,
    const std::u16string& text) {
  auto label = std::make_unique<views::Label>();
  label->SetAutoColorReadabilityEnabled(false);
  label->SetHorizontalAlignment(alignment);
  TypographyProvider::Get()->StyleLabel(token, *label);
  label->SetEnabledColorId(color_id);
  label->SetText(text);
  label->SetMultiLine(allow_multiline);
  label->SetMaxLines(allow_multiline ? 2 : 1);
  return label;
}

std::unique_ptr<lottie::Animation> GetConfettiAnimation() {
  std::optional<std::vector<uint8_t>> lottie_data =
      ui::ResourceBundle::GetSharedInstance().GetLottieData(
          IDR_FOCUS_MODE_CONFETTI_ANIMATION);
  CHECK(lottie_data.has_value());

  return std::make_unique<lottie::Animation>(
      cc::SkottieWrapper::UnsafeCreateSerializable(lottie_data.value()));
}

}  // namespace

FocusModeEndingMomentView::FocusModeEndingMomentView() {
  SetPaintToLayer();
  layer()->SetFillsBoundsOpaquely(false);

  // The main layout will be horizontal with the text container on the left,
  // and the button container on the right.
  SetOrientation(views::LayoutOrientation::kHorizontal);

  // Add a vertical container on the left for the text.
  auto* text_container = AddChildView(std::make_unique<views::BoxLayoutView>());
  text_container->SetOrientation(views::BoxLayout::Orientation::kVertical);
  text_container->SetMainAxisAlignment(
      views::BoxLayout::MainAxisAlignment::kStart);
  text_container->SetBetweenChildSpacing(kSpaceBetweenText);
  text_container->SetPreferredSize(kTextContainerSize);
  text_container->SetProperty(
      views::kFlexBehaviorKey,
      views::FlexSpecification(views::MinimumFlexSizeRule::kPreferred,
                               views::MaximumFlexSizeRule::kPreferred,
                               /*adjust_height_for_width =*/false));

  // `title_and_emoji_box` contains a congratulatory text in `title_label` and
  // an party-popper emoji.
  auto* title_and_emoji_box =
      text_container->AddChildView(std::make_unique<views::BoxLayoutView>());
  title_and_emoji_box->SetOrientation(
      views::BoxLayout::Orientation::kHorizontal);
  title_and_emoji_box->SetMainAxisAlignment(
      views::BoxLayout::CrossAxisAlignment::kStart);
  title_and_emoji_box->SetBetweenChildSpacing(kSpaceBetweenText);

  auto* focus_mode_controller = FocusModeController::Get();
  const size_t congratulatory_index =
      focus_mode_controller->congratulatory_index();
  auto* title_label = title_and_emoji_box->AddChildView(CreateTextLabel(
      gfx::ALIGN_LEFT, TypographyToken::kCrosHeadline1,
      cros_tokens::kCrosSysOnSurface, /*allow_multiline=*/false,
      focus_mode_util::GetCongratulatoryText(congratulatory_index)));
  title_label->SetMaximumWidthSingleLine(kTitleMaximumWidth);

  emoji_label_ = title_and_emoji_box->AddChildView(CreateTextLabel(
      gfx::ALIGN_LEFT, TypographyToken::kCrosHeadline1,
      cros_tokens::kCrosSysOnSurface,
      /*allow_multiline=*/false,
      focus_mode_util::GetCongratulatoryEmoji(congratulatory_index)));

  text_container->AddChildView(
      CreateTextLabel(gfx::ALIGN_LEFT, TypographyToken::kCrosAnnotation1,
                      cros_tokens::kCrosSysOnSurface, /*allow_multiline=*/true,
                      l10n_util::GetStringUTF16(
                          IDS_ASH_STATUS_TRAY_FOCUS_MODE_ENDING_MOMENT_BODY)));

  // Add a top level spacer in first layout manager, between the text container
  // and button container.
  auto* spacer_view = AddChildView(std::make_unique<views::View>());
  spacer_view->SetProperty(
      views::kFlexBehaviorKey,
      views::FlexSpecification(views::MinimumFlexSizeRule::kScaleToZero,
                               views::MaximumFlexSizeRule::kUnbounded));

  // Add the vertical box layout for the button container that holds the "Done"
  // and "+10 min" buttons.
  auto* button_container =
      AddChildView(std::make_unique<views::BoxLayoutView>());
  button_container->SetOrientation(views::BoxLayout::Orientation::kVertical);
  button_container->SetMainAxisAlignment(
      views::BoxLayout::MainAxisAlignment::kStart);
  button_container->SetCrossAxisAlignment(
      views::BoxLayout::CrossAxisAlignment::kStretch);
  button_container->SetBetweenChildSpacing(kSpaceBetweenButtons);

  // TODO(crbug.com/40232718): See View::SetLayoutManagerUseConstrainedSpace.
  button_container->SetLayoutManagerUseConstrainedSpace(false);

  button_container->AddChildView(std::make_unique<PillButton>(
      base::BindRepeating(&FocusModeController::ResetFocusSession,
                          base::Unretained(focus_mode_controller)),
      l10n_util::GetStringUTF16(
          IDS_ASH_STATUS_TRAY_FOCUS_MODE_ENDING_MOMENT_DONE_BUTTON),
      PillButton::Type::kPrimaryWithoutIcon, /*icon=*/nullptr));

  extend_session_duration_button_ =
      button_container->AddChildView(std::make_unique<PillButton>(
          base::BindRepeating(&FocusModeController::ExtendSessionDuration,
                              base::Unretained(focus_mode_controller)),
          l10n_util::GetStringUTF16(
              IDS_ASH_STATUS_TRAY_FOCUS_MODE_EXTEND_TEN_MINUTES_BUTTON_LABEL),
          PillButton::Type::kSecondaryWithoutIcon,
          /*icon=*/nullptr));
  extend_session_duration_button_->GetViewAccessibility().SetName(
      l10n_util::GetStringUTF16(
          IDS_ASH_STATUS_TRAY_FOCUS_MODE_INCREASE_TEN_MINUTES_BUTTON_ACCESSIBLE_NAME));
}

FocusModeEndingMomentView::~FocusModeEndingMomentView() {
  scoped_animation_observer_.Reset();
}

void FocusModeEndingMomentView::AnimationCycleEnded(
    const lottie::Animation* animation) {
  scoped_animation_observer_.Reset();

  if (animation_widget_) {
    base::SequencedTaskRunner::GetCurrentDefault()->DeleteSoon(
        FROM_HERE, animation_widget_.release());
  }
}

void FocusModeEndingMomentView::ShowEndingMomentContents(
    bool extend_button_enabled) {
  extend_session_duration_button_->SetEnabled(extend_button_enabled);

  // Add a slight delay for the animation so it doesn't show immediately.
  base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
      FROM_HERE,
      base::BindOnce(&FocusModeEndingMomentView::CreateAnimationWidget,
                     weak_ptr_factory_.GetWeakPtr()),
      base::Milliseconds(300));
}

void FocusModeEndingMomentView::CreateAnimationWidget() {
  if (animation_widget_) {
    return;
  }

  auto emoji_bounds = emoji_label_->GetBoundsInScreen();
  if (emoji_bounds.IsEmpty()) {
    LOG(WARNING) << "Emoji label does not have valid bounds";
    return;
  }

  gfx::Rect animation_bounds{
      {emoji_bounds.x(),
       emoji_bounds.y() - (kAnimationSize.height() - emoji_label_->height())},
      kAnimationSize};

  views::Widget::InitParams params(
      views::Widget::InitParams::CLIENT_OWNS_WIDGET,
      views::Widget::InitParams::TYPE_WINDOW_FRAMELESS);
  params.name = "FocusModeAnimationWidget";
  params.parent =
      Shell::GetContainer(Shell::Get()->GetRootWindowForNewWindows(),
                          kShellWindowId_OverlayContainer);
  params.child = true;
  params.bounds = animation_bounds;

  // The animation widget should not receive any events.
  params.activatable = views::Widget::InitParams::Activatable::kNo;
  params.accept_events = false;

  animation_widget_ = std::make_unique<views::Widget>();
  animation_widget_->Init(std::move(params));
  animation_widget_->SetVisibilityAnimationTransition(
      views::Widget::ANIMATE_NONE);
  animation_widget_->Show();

  auto* lottie_animation_view = animation_widget_->SetContentsView(
      std::make_unique<views::AnimatedImageView>());
  lottie_animation_view->SetImageSize(kAnimationSize);
  lottie_animation_view->SetAnimatedImage(GetConfettiAnimation());
  lottie_animation_view->SetBoundsRect(animation_bounds);
  lottie_animation_view->SetVisible(true);
  lottie_animation_view->Play(
      lottie::Animation::PlaybackConfig::CreateWithStyle(
          lottie::Animation::Style::kLinear,
          *lottie_animation_view->animated_image()));

  // Observe animation to know when it finishes playing.
  scoped_animation_observer_.Observe(lottie_animation_view->animated_image());
}

BEGIN_METADATA(FocusModeEndingMomentView)
END_METADATA

}  // namespace ash