chromium/ash/style/system_toast_style.cc

// Copyright 2021 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/style/system_toast_style.h"

#include <string>

#include "ash/accessibility/accessibility_controller.h"
#include "ash/accessibility/scoped_a11y_override_window_setter.h"
#include "ash/public/cpp/style/color_provider.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/ash_color_id.h"
#include "ash/style/pill_button.h"
#include "ash/style/system_shadow.h"
#include "ash/style/typography.h"
#include "ash/system/toast/toast_overlay.h"
#include "ash/wm/work_area_insets.h"
#include "base/strings/strcat.h"
#include "chromeos/constants/chromeos_features.h"
#include "ui/accessibility/ax_enums.mojom.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/insets.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/gfx/text_utils.h"
#include "ui/gfx/vector_icon_types.h"
#include "ui/views/background.h"
#include "ui/views/controls/focus_ring.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/label.h"
#include "ui/views/highlight_border.h"
#include "ui/views/layout/box_layout.h"

namespace ash::deprecated {

namespace {

// UI constants in DIP (Density Independent Pixel).
constexpr int kToastTextMaximumWidth = 512;

constexpr int kOneLineHorizontalPadding = 16;
constexpr int kTwoLineHorizontalPadding = 24;

constexpr int kOneLineVerticalPadding = 8;
constexpr int kTwoLineVerticalPadding = 12;

constexpr int kOneLineButtonPadding = 2;
constexpr int kTwoLineButtonRightPadding = 12;

constexpr int kLeadingIconSize = 20;
constexpr int kLeadingIconLeftPadding = 18;
constexpr int kLeadingIconRightPadding = 14;

// Inset for the focus ring around the dismiss button.
constexpr int kDismissButtonFocusRingHaloInset = 1;

// The label inside SystemToastStyle, which allows two lines at maximum.
class SystemToastInnerLabel : public views::Label {
  METADATA_HEADER(SystemToastInnerLabel, views::Label)

 public:
  explicit SystemToastInnerLabel(const std::u16string& text)
      : views::Label(text) {
    SetHorizontalAlignment(gfx::ALIGN_LEFT);
    SetAutoColorReadabilityEnabled(false);
    SetMultiLine(true);
    SetMaximumWidth(kToastTextMaximumWidth);
    SetMaxLines(2);
    SetSubpixelRenderingEnabled(false);
    SetEnabledColorId(cros_tokens::kTextColorPrimary);

    SetFontList(TypographyProvider::Get()->ResolveTypographyToken(
        TypographyToken::kLegacyBody1));
  }

  SystemToastInnerLabel(const SystemToastInnerLabel&) = delete;
  SystemToastInnerLabel& operator=(const SystemToastInnerLabel&) = delete;
  ~SystemToastInnerLabel() override = default;
};

BEGIN_METADATA(SystemToastInnerLabel)
END_METADATA

// Returns the vertical padding for the layout given the presence of the dismiss
// button and whether the toast is multi-line.
int ComputeVerticalPadding(bool has_button, bool two_line) {
  if (two_line) {
    return kTwoLineVerticalPadding;
  }

  // For one line, the button is taller so it determines the height of the toast
  // so we use the button's padding.
  return has_button ? kOneLineButtonPadding : kOneLineVerticalPadding;
}

// Computes the outer insets for the Box Layout container that holds toast
// elements. Horizontal spacing may vary depending if there's a dismiss button
// or a leading icon present.
gfx::Insets ComputeInsets(bool has_button, bool two_line, bool has_icon) {
  int left_inset;
  int right_inset;

  if (has_icon) {
    left_inset = kLeadingIconLeftPadding;
  } else {
    left_inset =
        two_line ? kTwoLineHorizontalPadding : kOneLineHorizontalPadding;
  }

  if (has_button) {
    right_inset = two_line ? kTwoLineButtonRightPadding : kOneLineButtonPadding;
  } else {
    right_inset =
        two_line ? kTwoLineHorizontalPadding : kOneLineHorizontalPadding;
  }

  const int vertical_insets = ComputeVerticalPadding(has_button, two_line);

  return gfx::Insets::TLBR(vertical_insets, left_inset, vertical_insets,
                           right_inset);
}

}  // namespace

SystemToastStyle::SystemToastStyle(base::RepeatingClosure dismiss_callback,
                                   const std::u16string& text,
                                   const std::u16string& dismiss_text,
                                   const gfx::VectorIcon& leading_icon)
    : leading_icon_(&leading_icon),
      scoped_a11y_overrider_(
          std::make_unique<ScopedA11yOverrideWindowSetter>()) {
  SetPaintToLayer();
  layer()->SetFillsBoundsOpaquely(false);
  layer()->SetBackgroundBlur(ColorProvider::kBackgroundBlurSigma);
  SetBackground(views::CreateThemedSolidBackground(kColorAshShieldAndBase80));

  if (!leading_icon_->is_empty()) {
    leading_icon_view_ = AddChildView(std::make_unique<views::ImageView>());
    leading_icon_view_->SetPreferredSize(
        gfx::Size(kLeadingIconSize, kLeadingIconSize));
    leading_icon_view_->SetImage(ui::ImageModel::FromVectorIcon(
        *leading_icon_, cros_tokens::kCrosSysOnSurface));

    auto* icon_padding = AddChildView(std::make_unique<views::View>());
    icon_padding->SetPreferredSize(
        gfx::Size(kLeadingIconRightPadding, kLeadingIconSize));
  }

  label_ = AddChildView(std::make_unique<SystemToastInnerLabel>(text));

  if (!dismiss_text.empty()) {
    dismiss_button_ = AddChildView(std::make_unique<PillButton>(
        std::move(dismiss_callback), dismiss_text,
        PillButton::Type::kAccentFloatingWithoutIcon,
        /*icon=*/nullptr));
    dismiss_button_->SetFocusBehavior(
        views::View::FocusBehavior::ACCESSIBLE_ONLY);
  }

  // Requesting size forces layout. Otherwise, we don't know how many lines
  // are needed.
  label_->GetPreferredSize(views::SizeBounds(label_->width(), {}));

  auto* layout = SetLayoutManager(std::make_unique<views::BoxLayout>());
  layout->set_cross_axis_alignment(
      views::BoxLayout::CrossAxisAlignment::kCenter);
  layout->SetFlexForView(label_, 1);
  UpdateInsideBorderInsets();

  const int toast_height = GetPreferredSize().height();
  const float toast_corner_radius = toast_height / 2.0f;
  layer()->SetRoundedCornerRadius(gfx::RoundedCornersF(toast_corner_radius));
  SetBorder(std::make_unique<views::HighlightBorder>(
      toast_corner_radius,
      chromeos::features::IsJellyrollEnabled()
          ? views::HighlightBorder::Type::kHighlightBorderOnShadow
          : views::HighlightBorder::Type::kHighlightBorder1));

  // Since system toast has a very large corner radius, we should use the shadow
  // on texture layer. Refer to `ash::SystemShadowOnTextureLayer` for more
  // details.
  shadow_ = SystemShadow::CreateShadowOnTextureLayer(
      SystemShadow::Type::kElevation12);
  shadow_->SetRoundedCornerRadius(toast_corner_radius);
}

SystemToastStyle::~SystemToastStyle() = default;

bool SystemToastStyle::ToggleA11yFocus() {
  if (!dismiss_button_) {
    return false;
  }

  auto* focus_ring = views::FocusRing::Get(dismiss_button_);
  focus_ring->SetHaloInset(kDismissButtonFocusRingHaloInset);
  focus_ring->SetOutsetFocusRingDisabled(true);
  focus_ring->SetHasFocusPredicate(base::BindRepeating(
      [](const SystemToastStyle* style, const views::View* view) {
        return style->is_dismiss_button_highlighted_;
      },
      base::Unretained(this)));

  is_dismiss_button_highlighted_ = !is_dismiss_button_highlighted_;
  if (is_dismiss_button_highlighted_) {
    scoped_a11y_overrider_->MaybeUpdateA11yOverrideWindow(
        dismiss_button_->GetWidget()->GetNativeWindow());
    dismiss_button_->NotifyAccessibilityEvent(ax::mojom::Event::kSelection,
                                              true);
  }

  focus_ring->SetVisible(is_dismiss_button_highlighted_);
  focus_ring->SchedulePaint();
  return is_dismiss_button_highlighted_;
}

void SystemToastStyle::SetText(const std::u16string& text) {
  label_->SetText(text);
}

void SystemToastStyle::AddedToWidget() {
  // Attach the shadow at the bottom of the widget layer.
  auto* shadow_layer = shadow_->GetLayer();
  auto* widget_layer = GetWidget()->GetLayer();

  widget_layer->Add(shadow_layer);
  widget_layer->StackAtBottom(shadow_layer);

  // Update shadow content bounds with the bounds of widget layer.
  shadow_->SetContentBounds(gfx::Rect(widget_layer->bounds().size()));

  // Make shadow observe the theme change of the widget.
  shadow_->ObserveColorProviderSource(GetWidget());
}

void SystemToastStyle::UpdateInsideBorderInsets() {
  static_cast<views::BoxLayout*>(GetLayoutManager())
      ->set_inside_border_insets(ComputeInsets(!!dismiss_button_,
                                               label_->GetRequiredLines() > 1,
                                               !leading_icon_->is_empty()));
  InvalidateLayout();
}

BEGIN_METADATA(SystemToastStyle)
END_METADATA

}  // namespace ash::deprecated