// Copyright 2022 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/unified/feature_tile.h"
#include <algorithm>
#include <utility>
#include "ash/constants/ash_features.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/ash_color_id.h"
#include "ash/style/dark_light_mode_controller_impl.h"
#include "ash/style/typography.h"
#include "ash/system/tray/tray_constants.h"
#include "base/auto_reset.h"
#include "base/strings/string_number_conversions.h"
#include "cc/paint/paint_flags.h"
#include "components/vector_icons/vector_icons.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/models/image_model.h"
#include "ui/chromeos/styles/cros_tokens_color_mappings.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/skia_conversions.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/gfx/scoped_canvas.h"
#include "ui/gfx/text_constants.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/animation/ink_drop.h"
#include "ui/views/animation/ink_drop_highlight.h"
#include "ui/views/background.h"
#include "ui/views/controls/button/button.h"
#include "ui/views/controls/button/image_button.h"
#include "ui/views/controls/focus_ring.h"
#include "ui/views/controls/highlight_path_generator.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/label.h"
#include "ui/views/layout/flex_layout.h"
#include "ui/views/layout/flex_layout_types.h"
#include "ui/views/layout/flex_layout_view.h"
#include "ui/views/layout/layout_types.h"
#include "ui/views/view_class_properties.h"
using views::FlexLayout;
using views::FlexLayoutView;
using views::InkDropHost;
namespace ash {
namespace {
// Tile constants
constexpr int kIconSize = 20;
constexpr int kDefaultCornerRadius = 16;
constexpr float kFocusRingPadding = 3.0f;
// Primary tile constants
constexpr gfx::Size kIconButtonSize(36, 52);
constexpr int kIconButtonCornerRadius = 12;
constexpr gfx::Insets kIconButtonMargins = gfx::Insets::VH(6, 6);
constexpr gfx::Insets kDrillInArrowMargins = gfx::Insets::TLBR(0, 4, 0, 10);
constexpr gfx::Insets kTitleContainerWithoutDiveInButtonMargins =
gfx::Insets::TLBR(0, 0, 0, 10);
constexpr gfx::Insets kTitleContainerWithDiveInButtonMargins = gfx::Insets();
// Compact tile constants
constexpr int kCompactTitleLineHeight = 14;
constexpr gfx::Size kCompactIconButtonSize(kIconSize, kIconSize);
constexpr gfx::Insets kCompactIconButtonMargins =
gfx::Insets::TLBR(6, 22, 4, 22);
constexpr gfx::Insets kCompactTitlesContainerMargins =
gfx::Insets::TLBR(0, 12, 6, 12);
// Download progress constants.
constexpr int kDownloadProgressBorderThickness = 4;
constexpr int kDownloadProgressLeadingEdgeRadius = 2;
// Creates an ink drop hover highlight for `host` with `color_id`.
std::unique_ptr<views::InkDropHighlight> CreateInkDropHighlight(
views::View* host,
ui::ColorId color_id) {
SkColor color = host->GetColorProvider()->GetColor(color_id);
auto highlight = std::make_unique<views::InkDropHighlight>(
gfx::SizeF(host->size()), color);
// The color has the opacity baked in.
highlight->set_visible_opacity(1.0f);
return highlight;
}
} // namespace
FeatureTile::ProgressBackground::ProgressBackground(
const ui::ColorId progress_color_id,
const ui::ColorId background_color_id)
: progress_color_id_(progress_color_id),
background_color_id_(background_color_id) {}
void FeatureTile::ProgressBackground::Paint(gfx::Canvas* canvas,
views::View* view) const {
FeatureTile* tile = static_cast<FeatureTile*>(view);
// Start with a simple rounded-rect background as the base. This part is
// symmetric so the canvas does not need to be flipped for RTL.
cc::PaintFlags background_flags;
background_flags.setAntiAlias(true);
background_flags.setStyle(cc::PaintFlags::kFill_Style);
background_flags.setColor(
view->GetColorProvider()->GetColor(background_color_id_));
canvas->DrawRoundRect(view->GetLocalBounds(), tile->corner_radius_,
background_flags);
// Then draw the progress bar on top. This part is NOT symmetric, so first
// flip the canvas (if necessary) to handle RTL layouts. Do this using a
// `gfx::ScopedCanvas` so that the canvas does not stay flipped for other
// paint commands later in the pipeline that aren't expecting a flipped
// canvas.
gfx::ScopedCanvas scoped_canvas(canvas);
if (!view->GetFlipCanvasOnPaintForRTLUI()) {
scoped_canvas.FlipIfRTL(view->width());
}
// The progress bar is a rounded-rect (of radius
// `kDownloadProgressLeadingEdgeRadius`) that is clipped to a slightly smaller
// rounded-rect. The clip's radius is set such that there appears to be a
// border of thickness `kDownloadProgressBorderThickness` all around the tile
// (note that this is not an actual `views::Border`).
// Set the clip.
gfx::Rect clip_bounds(view->GetLocalBounds());
clip_bounds.Inset(kDownloadProgressBorderThickness);
int clip_radius =
std::max(0, tile->corner_radius_ - kDownloadProgressBorderThickness);
SkScalar clip_radii[8];
std::fill_n(clip_radii, 8, clip_radius);
SkPath clip;
clip.addRoundRect(gfx::RectToSkRect(clip_bounds), clip_radii);
canvas->ClipPath(clip, /*do_anti_alias=*/true);
// Shrink the width of the progress bar according to the tile's current
// download progress.
float percent = static_cast<float>(tile->download_progress_percent_) / 100.0f;
gfx::Rect progress_bounds(clip_bounds);
progress_bounds.set_width(percent * progress_bounds.width());
// Draw the progress bar.
cc::PaintFlags progress_flags;
progress_flags.setAntiAlias(true);
progress_flags.setStyle(cc::PaintFlags::kFill_Style);
progress_flags.setColor(
view->GetColorProvider()->GetColor(progress_color_id_));
canvas->DrawRoundRect(progress_bounds, kDownloadProgressLeadingEdgeRadius,
progress_flags);
}
FeatureTile::FeatureTile(PressedCallback callback,
bool is_togglable,
TileType type)
: Button(std::move(callback)),
corner_radius_(kDefaultCornerRadius),
is_togglable_(is_togglable),
type_(type) {
// Set up ink drop on click. The corner radius must match the button
// background corner radius, see UpdateColors().
// TODO(jamescook): Consider adding support for highlight-path-based
// backgrounds so we don't have to match the shape manually. For example, add
// something like CreateThemedHighlightPathBackground() to
// ui/views/background.h.
views::InstallRoundRectHighlightPathGenerator(this, gfx::Insets(),
corner_radius_);
auto* ink_drop = views::InkDrop::Get(this);
ink_drop->SetMode(InkDropHost::InkDropMode::ON);
ink_drop->GetInkDrop()->SetShowHighlightOnHover(false);
ink_drop->SetVisibleOpacity(1.0f); // The colors already contain opacity.
// The focus ring appears slightly outside the tile bounds.
views::FocusRing::Get(this)->SetHaloInset(-kFocusRingPadding);
CreateChildViews();
UpdateColors();
UpdateAccessibilityProperties();
enabled_changed_subscription_ = AddEnabledChangedCallback(base::BindRepeating(
[](FeatureTile* feature_tile) {
feature_tile->UpdateColors();
if (feature_tile->is_icon_clickable_) {
feature_tile->icon_button_->SetEnabled(feature_tile->GetEnabled());
}
},
base::Unretained(this)));
}
FeatureTile::~FeatureTile() {
// Remove the InkDrop explicitly so FeatureTile::RemoveLayerFromRegions() is
// called before views::View teardown.
views::InkDrop::Remove(this);
title_container_->RemoveObserver(this);
}
void FeatureTile::AddObserver(Observer* observer) {
observers_.AddObserver(observer);
}
void FeatureTile::RemoveObserver(Observer* observer) {
observers_.RemoveObserver(observer);
}
void FeatureTile::CreateChildViews() {
const bool is_compact = type_ == TileType::kCompact;
SetLayoutManager(std::make_unique<views::FlexLayout>())
->SetOrientation(is_compact ? views::LayoutOrientation::kVertical
: views::LayoutOrientation::kHorizontal)
.SetMainAxisAlignment(views::LayoutAlignment::kCenter);
// TODO(crbug.com/40232718): See View::SetLayoutManagerUseConstrainedSpace.
SetLayoutManagerUseConstrainedSpace(false);
// Set `MaximumFlexSizeRule` to `kUnbounded` so the view takes up all of the
// available space in its parent container.
SetProperty(views::kFlexBehaviorKey,
views::FlexSpecification(views::FlexSpecification(
views::MinimumFlexSizeRule::kScaleToZero,
views::MaximumFlexSizeRule::kUnbounded,
/*adjust_height_for_width=*/true)));
ink_drop_container_ =
AddChildView(std::make_unique<views::InkDropContainerView>());
auto* focus_ring = views::FocusRing::Get(this);
focus_ring->SetColorId(cros_tokens::kCrosSysFocusRing);
icon_button_ = AddChildView(std::make_unique<views::ImageButton>());
icon_button_->SetImageHorizontalAlignment(views::ImageButton::ALIGN_CENTER);
icon_button_->SetImageVerticalAlignment(views::ImageButton::ALIGN_MIDDLE);
icon_button_->SetPreferredSize(is_compact ? kCompactIconButtonSize
: kIconButtonSize);
icon_button_->SetProperty(views::kMarginsKey, is_compact
? kCompactIconButtonMargins
: kIconButtonMargins);
// By default the icon button is not separately clickable.
icon_button_->SetEnabled(false);
icon_button_->SetCanProcessEventsWithinSubtree(false);
title_container_ =
AddChildView(views::Builder<FlexLayoutView>()
.SetCanProcessEventsWithinSubtree(false)
.SetOrientation(views::LayoutOrientation::kVertical)
.SetMainAxisAlignment(views::LayoutAlignment::kCenter)
.SetCrossAxisAlignment(views::LayoutAlignment::kStretch)
.Build());
title_container_->AddObserver(this);
// Set `MaximumFlexSizeRule` to `kUnbounded` so that `title_container_` takes
// up all of the available space in the middle of the primary tile.
title_container_->SetProperty(
views::kFlexBehaviorKey,
views::FlexSpecification(views::FlexSpecification(
is_compact ? views::LayoutOrientation::kVertical
: views::LayoutOrientation::kHorizontal,
views::MinimumFlexSizeRule::kScaleToZero,
views::MaximumFlexSizeRule::kUnbounded,
/*adjust_height_for_width=*/true)));
label_ = title_container_->AddChildView(std::make_unique<views::Label>());
label_->SetAutoColorReadabilityEnabled(false);
sub_label_ = title_container_->AddChildView(std::make_unique<views::Label>());
sub_label_->SetHorizontalAlignment(is_compact ? gfx::ALIGN_CENTER
: gfx::ALIGN_LEFT);
sub_label_->SetAutoColorReadabilityEnabled(false);
if (is_compact) {
title_container_->SetProperty(views::kMarginsKey,
kCompactTitlesContainerMargins);
label_->SetVerticalAlignment(gfx::ALIGN_MIDDLE);
label_->SetHorizontalAlignment(gfx::ALIGN_CENTER);
// By default, assume compact tiles will not support sub-labels.
SetCompactTileLabelPreferences(/*has_sub_label=*/false);
// Compact labels use kCrosAnnotation2 with a shorter custom line height.
const auto font_list = TypographyProvider::Get()->ResolveTypographyToken(
TypographyToken::kCrosAnnotation2);
label_->SetFontList(font_list);
label_->SetLineHeight(kCompactTitleLineHeight);
sub_label_->SetFontList(font_list);
sub_label_->SetLineHeight(kCompactTitleLineHeight);
sub_label_->SetVisible(false);
} else {
// `title_container_` will take all the remaining space of the tile.
title_container_->SetProperty(views::kMarginsKey,
kTitleContainerWithoutDiveInButtonMargins);
label_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
TypographyProvider::Get()->StyleLabel(TypographyToken::kCrosButton2,
*label_);
TypographyProvider::Get()->StyleLabel(TypographyToken::kCrosAnnotation1,
*sub_label_);
}
}
void FeatureTile::SetIconClickable(bool clickable) {
CHECK_EQ(type_, TileType::kPrimary);
is_icon_clickable_ = clickable;
icon_button_->SetCanProcessEventsWithinSubtree(clickable);
icon_button_->SetEnabled(clickable);
UpdateAccessibilityProperties();
if (clickable) {
views::InstallRoundRectHighlightPathGenerator(icon_button_, gfx::Insets(),
kIconButtonCornerRadius);
UpdateIconButtonFocusRingColor();
views::InkDrop::Get(icon_button_)->SetMode(InkDropHost::InkDropMode::ON);
icon_button_->SetHasInkDropActionOnClick(true);
UpdateIconButtonRippleColors();
} else {
views::HighlightPathGenerator::Install(icon_button_, nullptr);
views::InkDrop::Get(icon_button_)->SetMode(InkDropHost::InkDropMode::OFF);
}
}
void FeatureTile::SetIconClickCallback(
base::RepeatingCallback<void()> callback) {
icon_button_->SetCallback(std::move(callback));
}
void FeatureTile::SetOnTitleBoundsChangedCallback(
base::RepeatingCallback<void()> callback) {
on_title_container_bounds_changed_ = std::move(callback);
}
void FeatureTile::SetTitleContainerMargins(const gfx::Insets& insets) {
title_container_->SetProperty(views::kMarginsKey, insets);
}
void FeatureTile::CreateDecorativeDrillInArrow() {
CHECK_EQ(type_, TileType::kPrimary)
<< "Drill-in arrows are just used in Primary tiles";
title_container_->SetProperty(views::kMarginsKey,
kTitleContainerWithDiveInButtonMargins);
drill_in_arrow_ = AddChildView(std::make_unique<views::ImageView>());
// The icon is set in UpdateDrillArrowColor().
drill_in_arrow_->SetPreferredSize(gfx::Size(kIconSize, kIconSize));
drill_in_arrow_->SetProperty(views::kMarginsKey, kDrillInArrowMargins);
// Allow hover events to fall through to show tooltips from the main view.
drill_in_arrow_->SetCanProcessEventsWithinSubtree(false);
drill_in_arrow_->SetFlipCanvasOnPaintForRTLUI(true);
UpdateDrillInArrowColor();
}
void FeatureTile::UpdateColors() {
ui::ColorId background_color;
if (GetEnabled()) {
background_color =
toggled_
? background_toggled_color_.value_or(
cros_tokens::kCrosSysSystemPrimaryContainer)
: background_color_.value_or(cros_tokens::kCrosSysSystemOnBase);
} else {
background_color = background_disabled_color_.value_or(
cros_tokens::kCrosSysDisabledContainer);
}
ui::ColorId foreground_color;
ui::ColorId foreground_optional_color;
// The `DownloadState::kPending` state should have the same colors on the
// labels and images as an enabled button, per the spec. The labels and images
// will only look disabled if the button was disabled for other reasons.
if (GetEnabled() || download_state_ == DownloadState::kPending) {
foreground_color =
toggled_ ? foreground_toggled_color_.value_or(
cros_tokens::kCrosSysSystemOnPrimaryContainer)
: foreground_color_.value_or(cros_tokens::kCrosSysOnSurface);
foreground_optional_color =
toggled_ ? foreground_optional_toggled_color_.value_or(
cros_tokens::kCrosSysSystemOnPrimaryContainer)
: foreground_optional_color_.value_or(
cros_tokens::kCrosSysOnSurfaceVariant);
} else {
foreground_color =
foreground_disabled_color_.value_or(cros_tokens::kCrosSysDisabled);
foreground_optional_color =
foreground_disabled_color_.value_or(cros_tokens::kCrosSysDisabled);
}
SetBackground(
features::IsVcDlcUiEnabled() &&
download_state_ == DownloadState::kDownloading
? std::make_unique<ProgressBackground>(
/*progress_color_id=*/cros_tokens::kCrosSysHighlightShape,
/*background_color_id=*/background_color)
: views::CreateThemedRoundedRectBackground(background_color,
corner_radius_));
auto* ink_drop = views::InkDrop::Get(this);
ink_drop->SetBaseColorId(toggled_
? ink_drop_toggled_base_color_.value_or(
cros_tokens::kCrosSysRipplePrimary)
: cros_tokens::kCrosSysRippleNeutralOnSubtle);
auto icon_image_model = ui::ImageModel::FromVectorIcon(
*vector_icon_, foreground_color, kIconSize);
icon_button_->SetImageModel(views::Button::STATE_NORMAL, icon_image_model);
icon_button_->SetImageModel(views::Button::STATE_DISABLED, icon_image_model);
if (is_icon_clickable_) {
UpdateIconButtonRippleColors();
UpdateIconButtonFocusRingColor();
}
label_->SetEnabledColorId(foreground_color);
if (sub_label_) {
sub_label_->SetEnabledColorId(foreground_optional_color);
}
if (drill_in_arrow_) {
UpdateDrillInArrowColor();
}
}
void FeatureTile::SetToggled(bool toggled) {
if (!is_togglable_ || toggled_ == toggled) {
return;
}
toggled_ = toggled;
UpdateAccessibilityProperties();
UpdateColors();
views::InkDrop::Get(this)->GetInkDrop()->SnapToHidden();
}
bool FeatureTile::IsToggled() const {
return toggled_;
}
void FeatureTile::SetVectorIcon(const gfx::VectorIcon& icon) {
vector_icon_ = &icon;
ui::ColorId color_id = GetIconColorId();
auto image_model = ui::ImageModel::FromVectorIcon(icon, color_id, kIconSize);
icon_button_->SetImageModel(views::Button::STATE_NORMAL, image_model);
icon_button_->SetImageModel(views::Button::STATE_DISABLED, image_model);
}
void FeatureTile::SetBackgroundColorId(ui::ColorId background_color_id) {
if (background_color_ == background_color_id) {
return;
}
background_color_ = background_color_id;
if (!toggled_) {
UpdateColors();
}
}
void FeatureTile::SetBackgroundToggledColorId(
ui::ColorId background_toggled_color_id) {
if (background_toggled_color_ == background_toggled_color_id) {
return;
}
background_toggled_color_ = background_toggled_color_id;
if (toggled_) {
UpdateColors();
}
}
void FeatureTile::SetBackgroundDisabledColorId(
ui::ColorId background_disabled_color_id) {
if (background_disabled_color_ == background_disabled_color_id) {
return;
}
background_disabled_color_ = background_disabled_color_id;
if (!GetEnabled()) {
UpdateColors();
}
}
void FeatureTile::SetButtonCornerRadius(const int radius) {
corner_radius_ = radius;
views::InstallRoundRectHighlightPathGenerator(this, gfx::Insets(),
corner_radius_);
UpdateColors();
}
void FeatureTile::SetForegroundColorId(ui::ColorId foreground_color_id) {
if (foreground_color_ == foreground_color_id) {
return;
}
foreground_color_ = foreground_color_id;
if (!toggled_) {
UpdateColors();
}
}
void FeatureTile::SetForegroundToggledColorId(
ui::ColorId foreground_toggled_color_id) {
if (foreground_toggled_color_ == foreground_toggled_color_id) {
return;
}
foreground_toggled_color_ = foreground_toggled_color_id;
if (toggled_) {
UpdateColors();
}
}
void FeatureTile::SetForegroundDisabledColorId(
ui::ColorId foreground_disabled_color_id) {
if (foreground_disabled_color_ == foreground_disabled_color_id) {
return;
}
foreground_disabled_color_ = foreground_disabled_color_id;
if (!GetEnabled()) {
UpdateColors();
}
}
void FeatureTile::SetForegroundOptionalColorId(
ui::ColorId foreground_optional_color_id) {
if (foreground_optional_color_ == foreground_optional_color_id) {
return;
}
foreground_optional_color_ = foreground_optional_color_id;
if (!GetEnabled()) {
UpdateColors();
}
}
void FeatureTile::SetForegroundOptionalToggledColorId(
ui::ColorId foreground_optional_toggled_color_id) {
if (foreground_optional_toggled_color_ ==
foreground_optional_toggled_color_id) {
return;
}
foreground_optional_toggled_color_ = foreground_optional_toggled_color_id;
if (!GetEnabled()) {
UpdateColors();
}
}
void FeatureTile::SetInkDropToggledBaseColorId(
ui::ColorId ink_drop_toggled_base_color_id) {
if (ink_drop_toggled_base_color_ == ink_drop_toggled_base_color_id) {
return;
}
ink_drop_toggled_base_color_ = ink_drop_toggled_base_color_id;
if (!GetEnabled()) {
UpdateColors();
}
}
void FeatureTile::SetImage(gfx::ImageSkia image) {
auto image_model = ui::ImageModel::FromImageSkia(image);
icon_button_->SetImageModel(views::Button::STATE_NORMAL, image_model);
icon_button_->SetImageModel(views::Button::STATE_DISABLED, image_model);
}
void FeatureTile::SetIconButtonTooltipText(const std::u16string& tooltip_text) {
CHECK(is_icon_clickable_);
icon_button_->SetTooltipText(tooltip_text);
}
void FeatureTile::SetLabel(const std::u16string& label) {
// If `VcDlcUi` is enabled and the tile is currently in a download state that
// requires a non-client-specified label to be shown then store the new
// client-specified label but don't immediately update the UI. The UI will be
// updated to show the new label when the download finishes.
if (features::IsVcDlcUiEnabled()) {
client_specified_label_text_ = label;
if (download_state_ == DownloadState::kPending ||
download_state_ == DownloadState::kDownloading) {
return;
}
}
label_->SetText(label);
}
int FeatureTile::GetSubLabelMaxWidth() const {
return title_container_->size().width();
}
void FeatureTile::SetSubLabel(const std::u16string& sub_label) {
DCHECK(!sub_label.empty())
<< "Attempting to set an empty sub-label. Did you mean to call "
"SubLabelVisibility(false) instead?";
sub_label_->SetText(sub_label);
}
void FeatureTile::SetSubLabelVisibility(bool visible) {
const bool is_compact = type_ == TileType::kCompact;
DCHECK(!(is_compact && visible && sub_label_->GetText().empty()))
<< "Attempting to make the compact tile's sub-label visible when it "
"wasn't set.";
sub_label_->SetVisible(visible);
if (is_compact) {
// When updating a compact tile's `sub_label_` visibility, `label_` needs to
// also be changed to make room for the sub-label. If making a sub-label
// visible, the primary label and sub-label have one line each to display
// text. If disabling sub-label visibility, reset `label_` to allow its text
// to display on two lines.
SetCompactTileLabelPreferences(/*has_sub_label=*/visible);
}
}
void FeatureTile::SetDownloadState(DownloadState state, int progress) {
// Download state is only supported when `VcDlcUi` is enabled.
CHECK(features::IsVcDlcUiEnabled())
<< "Download states are not supported when `VcDlcUi` is disabled";
// Check if this tile is already in a download state such that we can bail out
// without doing any updates.
if (download_state_ == state) {
// We can always bail out early if we're already in the given
// non-downloading state.
if (state != DownloadState::kDownloading) {
return;
}
// We can only bail out early from a downloading state if we're already at
// the given download progress.
if (download_progress_percent_ == progress) {
return;
}
}
download_state_ = state;
switch (download_state_) {
case DownloadState::kDownloading:
CHECK_GE(progress, 0)
<< "Expected download progress to be in the range [0, 100], actual: "
<< progress;
CHECK_LE(progress, 100)
<< "Expected download progress to be in the range [0, 100], actual: "
<< progress;
SetEnabled(true);
download_progress_percent_ = progress;
break;
case DownloadState::kError:
case DownloadState::kPending:
SetEnabled(false);
download_progress_percent_ = 0;
break;
case DownloadState::kDownloaded:
SetEnabled(true);
download_progress_percent_ = 0;
break;
case DownloadState::kNone:
SetEnabled(true);
download_progress_percent_ = 0;
break;
}
UpdateColors();
UpdateLabelForDownloadState();
// Once the tile's UI has been updated, notify any observers of the download
// state change.
NotifyDownloadStateChanged();
}
void FeatureTile::AddLayerToRegion(ui::Layer* layer,
views::LayerRegion region) {
// This routes background layers to `ink_drop_container_` instead of `this` to
// avoid painting effects underneath our background.
ink_drop_container_->AddLayerToRegion(layer, region);
}
void FeatureTile::RemoveLayerFromRegions(ui::Layer* layer) {
// This routes background layers to `ink_drop_container_` instead of `this` to
// avoid painting effects underneath our background.
ink_drop_container_->RemoveLayerFromRegions(layer);
}
void FeatureTile::OnViewBoundsChanged(views::View* observed_view) {
if (observed_view == title_container_ && on_title_container_bounds_changed_) {
on_title_container_bounds_changed_.Run();
}
}
void FeatureTile::OnSetTooltipText(const std::u16string& tooltip_text) {
if (!features::IsVcDlcUiEnabled() || updating_download_state_labels_) {
return;
}
// Keep track of the client set tooltip, so it can be restored if a
// `DownloadState` tooltip has been temporarily set.
client_specified_tooltip_text_ = tooltip_text;
// If the tooltip was set while a temporary downloading tooltip text was set,
// restore it. This will be reset to the `client_specified_tooltip_text_` once
// the download state changes back to the final state (if it's not
// `DownloadState::kError`).
if (download_state_ != DownloadState::kDownloaded &&
download_state_ != DownloadState::kNone) {
UpdateLabelForDownloadState();
}
}
ui::ColorId FeatureTile::GetIconColorId() const {
if (!GetEnabled()) {
return cros_tokens::kCrosSysDisabled;
}
return toggled_ ? foreground_toggled_color_.value_or(
cros_tokens::kCrosSysSystemOnPrimaryContainer)
: foreground_color_.value_or(cros_tokens::kCrosSysOnSurface);
}
void FeatureTile::UpdateIconButtonRippleColors() {
CHECK(is_icon_clickable_);
auto* ink_drop = views::InkDrop::Get(icon_button_);
// Set up the hover highlight.
ink_drop->SetCreateHighlightCallback(
base::BindRepeating(&CreateInkDropHighlight, icon_button_,
toggled_ ? cros_tokens::kCrosSysHighlightShape
: cros_tokens::kCrosSysHoverOnSubtle));
// Set up the ripple color.
ink_drop->SetBaseColorId(toggled_
? cros_tokens::kCrosSysRipplePrimary
: cros_tokens::kCrosSysRippleNeutralOnSubtle);
// The ripple base color includes opacity.
ink_drop->SetVisibleOpacity(1.0f);
// Ensure the new color applies even if the hover highlight or ripple is
// already showing.
ink_drop->GetInkDrop()->HostViewThemeChanged();
}
void FeatureTile::UpdateIconButtonFocusRingColor() {
CHECK(is_icon_clickable_);
views::FocusRing::Get(icon_button_)
->SetColorId(toggled_ ? cros_tokens::kCrosSysFocusRingOnPrimaryContainer
: cros_tokens::kCrosSysFocusRing);
}
void FeatureTile::UpdateDrillInArrowColor() {
CHECK(drill_in_arrow_);
drill_in_arrow_->SetImage(ui::ImageModel::FromVectorIcon(
kQuickSettingsRightArrowIcon, GetIconColorId()));
}
void FeatureTile::UpdateAccessibilityProperties() {
// If the icon is clickable then the main feature tile usually takes the user
// to a detailed page (like Network or Bluetooth). Those tiles act more like a
// regular button than a toggle button.
if (is_togglable_ && !is_icon_clickable_) {
GetViewAccessibility().SetRole(ax::mojom::Role::kToggleButton);
GetViewAccessibility().SetCheckedState(
toggled_ ? ax::mojom::CheckedState::kTrue
: ax::mojom::CheckedState::kFalse);
} else {
GetViewAccessibility().SetRole(ax::mojom::Role::kButton);
GetViewAccessibility().RemoveCheckedState();
}
}
void FeatureTile::SetCompactTileLabelPreferences(bool has_sub_label) {
label_->SetMultiLine(!has_sub_label);
// Elide after 2 lines if there's no sub-label. Otherwise, 1 line.
label_->SetMaxLines(has_sub_label ? 1 : 2);
}
void FeatureTile::SetDownloadLabel(const std::u16string& download_label,
std::optional<std::u16string> tooltip) {
// Download state is only supported when `VcDlcUi` is enabled.
CHECK(features::IsVcDlcUiEnabled())
<< "Download states are not supported when `VcDlcUi` is disabled";
label_->SetText(download_label);
SetTooltipText(tooltip.value_or(download_label));
}
void FeatureTile::UpdateLabelForDownloadState() {
// Download state is only supported when `VcDlcUi` is enabled.
CHECK(features::IsVcDlcUiEnabled())
<< "Download states are not supported when `VcDlcUi` is disabled";
base::AutoReset<bool> reset(&updating_download_state_labels_, true);
switch (download_state_) {
case DownloadState::kError:
SetDownloadLabel(client_specified_label_text_,
/*tooltip=*/l10n_util::GetStringFUTF16(
IDS_ASH_FEATURE_TILE_DOWNLOAD_ERROR,
client_specified_label_text_));
break;
case DownloadState::kNone:
case DownloadState::kDownloaded:
// If a download happened, `SetLabel()` saved the previous label
// in `client_specified_label_text_`, so re-set those here.
label_->SetText(client_specified_label_text_);
SetTooltipText(client_specified_tooltip_text_.empty()
? client_specified_label_text_
: client_specified_tooltip_text_);
break;
case DownloadState::kPending:
SetDownloadLabel(l10n_util::GetStringUTF16(
IDS_ASH_FEATURE_TILE_DOWNLOAD_PENDING_TITLE));
break;
case DownloadState::kDownloading:
SetDownloadLabel(l10n_util::GetStringFUTF16(
IDS_ASH_FEATURE_TILE_DOWNLOAD_IN_PROGRESS_TITLE,
base::NumberToString16(download_progress_percent_)));
break;
}
}
void FeatureTile::NotifyDownloadStateChanged() {
for (Observer& observer : observers_) {
observer.OnDownloadStateChanged(download_state_,
download_progress_percent_);
}
}
BEGIN_METADATA(FeatureTile)
END_METADATA
} // namespace ash