// Copyright 2019 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/public/cpp/shelf_config.h"
#include <optional>
#include "ash/accessibility/accessibility_controller.h"
#include "ash/accessibility/accessibility_observer.h"
#include "ash/app_list/app_list_controller_impl.h"
#include "ash/constants/ash_features.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/style/ash_color_id.h"
#include "ash/style/dark_light_mode_controller_impl.h"
#include "ash/system/model/system_tray_model.h"
#include "ash/wm/overview/overview_controller.h"
#include "base/functional/bind.h"
#include "base/metrics/histogram_functions.h"
#include "base/scoped_observation.h"
#include "chromeos/constants/chromeos_features.h"
#include "ui/chromeos/styles/cros_tokens_color_mappings.h"
#include "ui/display/tablet_state.h"
namespace ash {
namespace {
// When any edge of the primary display is less than or equal to this threshold,
// dense shelf will be active.
constexpr int kDenseShelfScreenSizeThreshold = 600;
// Drags on the shelf that are greater than this number times the shelf size
// will trigger shelf visibility changes.
constexpr float kDragHideRatioThreshold = 0.4f;
constexpr int kSystemShelfSizeTabletModeDense = 48;
constexpr int kSystemShelfSizeTabletModeNormal = 56;
constexpr int kElevatedSystemShelfSizeTabletMode = 136;
int IsDenseForCurrentScreen() {
const gfx::Rect screen_size =
display::Screen::GetScreen()->GetPrimaryDisplay().bounds();
return screen_size.width() <= kDenseShelfScreenSizeThreshold ||
screen_size.height() <= kDenseShelfScreenSizeThreshold;
}
} // namespace
class ShelfConfig::ShelfAccessibilityObserver : public AccessibilityObserver {
public:
explicit ShelfAccessibilityObserver(
const base::RepeatingClosure& accessibility_state_changed_callback)
: accessibility_state_changed_callback_(
accessibility_state_changed_callback) {
observation_.Observe(Shell::Get()->accessibility_controller());
}
ShelfAccessibilityObserver(const ShelfAccessibilityObserver& other) = delete;
ShelfAccessibilityObserver& operator=(
const ShelfAccessibilityObserver& other) = delete;
~ShelfAccessibilityObserver() override = default;
// AccessibilityObserver:
void OnAccessibilityStatusChanged() override {
accessibility_state_changed_callback_.Run();
}
void OnAccessibilityControllerShutdown() override { observation_.Reset(); }
private:
base::RepeatingClosure accessibility_state_changed_callback_;
base::ScopedObservation<AccessibilityController, AccessibilityObserver>
observation_{this};
};
class ShelfConfig::ShelfSplitViewObserver : public SplitViewObserver {
public:
explicit ShelfSplitViewObserver(
SplitViewController* controller,
const base::RepeatingCallback<void(SplitViewController::State,
SplitViewController::State)>&
split_view_state_changed_callback)
: split_view_state_changed_callback_(split_view_state_changed_callback) {
observation_.Observe(controller);
}
ShelfSplitViewObserver(const ShelfSplitViewObserver& other) = delete;
ShelfSplitViewObserver& operator=(const ShelfSplitViewObserver& other) =
delete;
~ShelfSplitViewObserver() override = default;
// SplitViewObserver:
void OnSplitViewStateChanged(SplitViewController::State previous_state,
SplitViewController::State state) override {
split_view_state_changed_callback_.Run(previous_state, state);
}
private:
base::RepeatingCallback<void(SplitViewController::State,
SplitViewController::State)>
split_view_state_changed_callback_;
base::ScopedObservation<SplitViewController, SplitViewObserver> observation_{
this};
};
ShelfConfig::ShelfConfig()
: shelf_button_icon_size_(44),
shelf_button_icon_size_median_(40),
shelf_button_icon_size_dense_(36),
shelf_shortcut_icon_size_(30),
shelf_shortcut_icon_border_size_(3),
shelf_shortcut_host_badge_icon_size_(14),
shelf_shortcut_host_badge_border_size_(2),
shelf_shortcut_teardrop_corner_radius_(8),
shelf_button_size_(56),
shelf_button_size_median_(52),
shelf_button_size_dense_(48),
shelf_button_spacing_(8),
shelf_status_area_hit_region_padding_(4),
shelf_status_area_hit_region_padding_dense_(2),
app_icon_group_margin_tablet_(16),
app_icon_group_margin_clamshell_(12),
workspace_area_visible_inset_(2),
workspace_area_auto_hide_inset_(5),
hidden_shelf_in_screen_portion_(3),
status_indicator_offset_from_shelf_edge_(1),
scrollable_shelf_ripple_padding_(2),
shelf_tooltip_preview_height_(128),
shelf_tooltip_preview_max_width_(192),
shelf_tooltip_preview_max_ratio_(1.5), // = 3/2
shelf_tooltip_preview_min_ratio_(0.666), // = 2/3
shelf_blur_radius_(30),
mousewheel_scroll_offset_threshold_(20),
in_app_control_button_height_inset_(4),
app_icon_end_padding_(4) {
accessibility_observer_ = std::make_unique<ShelfAccessibilityObserver>(
base::BindRepeating(&ShelfConfig::UpdateConfigForAccessibilityState,
base::Unretained(this)));
}
ShelfConfig::~ShelfConfig() = default;
// static
ShelfConfig* ShelfConfig::Get() {
return Shell::HasInstance() ? Shell::Get()->shelf_config() : nullptr;
}
void ShelfConfig::AddObserver(Observer* observer) {
observers_.AddObserver(observer);
}
void ShelfConfig::RemoveObserver(Observer* observer) {
observers_.RemoveObserver(observer);
}
void ShelfConfig::Init() {
Shell* const shell = Shell::Get();
shell->app_list_controller()->AddObserver(this);
shell->system_tray_model()->virtual_keyboard()->AddObserver(this);
shell->overview_controller()->AddObserver(this);
shell->session_controller()->AddObserver(this);
in_tablet_mode_ = display::Screen::GetScreen()->InTabletMode();
UpdateConfig(is_app_list_visible_, /*tablet_mode_changed=*/false);
}
void ShelfConfig::Shutdown() {
Shell* const shell = Shell::Get();
shell->session_controller()->RemoveObserver(this);
shell->overview_controller()->RemoveObserver(this);
shell->system_tray_model()->virtual_keyboard()->RemoveObserver(this);
shell->app_list_controller()->RemoveObserver(this);
}
void ShelfConfig::OnOverviewModeWillStart() {
DCHECK(!overview_mode_);
use_in_app_shelf_in_overview_ = is_in_app_;
overview_mode_ = true;
auto* split_view_controller =
SplitViewController::Get(Shell::GetPrimaryRootWindow());
in_split_view_with_overview_ = split_view_controller->InSplitViewMode();
split_view_observer_ = std::make_unique<ShelfSplitViewObserver>(
split_view_controller,
base::BindRepeating(&ShelfConfig::OnSplitViewStateChanged,
base::Unretained(this)));
}
void ShelfConfig::OnOverviewModeEnding(OverviewSession* overview_session) {
split_view_observer_.reset();
overview_mode_ = false;
in_split_view_with_overview_ = false;
use_in_app_shelf_in_overview_ = false;
UpdateConfig(is_app_list_visible_, /*tablet_mode_changed=*/false);
}
void ShelfConfig::OnSplitViewStateChanged(
SplitViewController::State previous_state,
SplitViewController::State state) {
in_split_view_with_overview_ = (state != SplitViewController::State::kNoSnap);
UpdateConfig(is_app_list_visible_, /*tablet_mode_changed=*/false);
}
void ShelfConfig::OnSessionStateChanged(session_manager::SessionState state) {
UpdateConfig(is_app_list_visible_, /*tablet_mode_changed=*/false);
}
void ShelfConfig::UpdateForTabletMode(bool in_tablet_mode) {
in_tablet_mode_ = in_tablet_mode;
UpdateConfig(is_app_list_visible_, /*tablet_mode_changed=*/true);
if (!in_tablet_mode_) {
has_shown_elevated_app_bar_ = std::nullopt;
}
}
void ShelfConfig::OnDisplayMetricsChanged(const display::Display& display,
uint32_t changed_metrics) {
UpdateConfig(is_app_list_visible_, /*tablet_mode_changed=*/false);
}
void ShelfConfig::OnVirtualKeyboardVisibilityChanged() {
UpdateConfig(is_app_list_visible_, /*tablet_mode_changed=*/false);
}
void ShelfConfig::OnAppListVisibilityWillChange(bool shown,
int64_t display_id) {
// Let's check that the app visibility mechanism isn't mis-firing, which
// would lead to a lot of extraneous relayout work.
DCHECK_NE(is_app_list_visible_, shown);
UpdateConfig(/*new_is_app_list_visible=*/shown,
/*tablet_mode_changed=*/false);
}
bool ShelfConfig::ShelfControlsForcedShownForAccessibility() const {
auto* accessibility_controller = Shell::Get()->accessibility_controller();
return accessibility_controller->spoken_feedback().enabled() ||
accessibility_controller->autoclick().enabled() ||
accessibility_controller->switch_access().enabled() ||
accessibility_controller
->tablet_mode_shelf_navigation_buttons_enabled();
}
int ShelfConfig::GetShelfButtonSize(HotseatDensity density) const {
if (is_dense_)
return shelf_button_size_dense_;
switch (density) {
case HotseatDensity::kNormal:
return shelf_button_size_;
case HotseatDensity::kSemiDense:
return shelf_button_size_median_;
case HotseatDensity::kDense:
return shelf_button_size_dense_;
}
}
int ShelfConfig::GetShelfButtonIconSize(HotseatDensity density) const {
if (is_dense_)
return shelf_button_icon_size_dense_;
switch (density) {
case HotseatDensity::kNormal:
return shelf_button_icon_size_;
case HotseatDensity::kSemiDense:
return shelf_button_icon_size_median_;
case HotseatDensity::kDense:
return shelf_button_icon_size_dense_;
}
}
int ShelfConfig::GetShelfShortcutIconSize() const {
return shelf_shortcut_icon_size_;
}
int ShelfConfig::GetShelfShortcutIconBorderSize() const {
return shelf_shortcut_icon_border_size_;
}
int ShelfConfig::GetShelfShortcutHostBadgeIconSize() const {
return shelf_shortcut_host_badge_icon_size_;
}
int ShelfConfig::GetShelfShortcutHostBadgeBorderSize() const {
return shelf_shortcut_host_badge_border_size_;
}
int ShelfConfig::GetShelfShortcutTeardropCornerRadiusSize() const {
return shelf_shortcut_teardrop_corner_radius_;
}
int ShelfConfig::GetHotseatSize(HotseatDensity density) const {
if (!in_tablet_mode_)
return shelf_size();
return GetShelfButtonSize(density);
}
int ShelfConfig::GetHomecherElevatedAppBarOffset() const {
return 8;
}
int ShelfConfig::shelf_size() const {
return GetShelfSize(false /*ignore_in_app_state*/);
}
int ShelfConfig::in_app_shelf_size() const {
return is_dense_ ? 36 : 40;
}
int ShelfConfig::system_shelf_size() const {
return GetShelfSize(true /*ignore_in_app_state*/);
}
int ShelfConfig::shelf_drag_handle_centering_size() const {
const session_manager::SessionState session_state =
Shell::Get()->session_controller()->GetSessionState();
return session_state == session_manager::SessionState::ACTIVE
? in_app_shelf_size()
: 28;
}
int ShelfConfig::hotseat_bottom_padding() const {
return 8;
}
int ShelfConfig::button_spacing() const {
return shelf_button_spacing_;
}
int ShelfConfig::control_size() const {
if (!in_tablet_mode_)
return 36;
return is_dense_ ? 36 : 40;
}
int ShelfConfig::control_border_radius() const {
return (is_in_app_ && in_tablet_mode_)
? control_size() / 2 - in_app_control_button_height_inset_
: control_size() / 2;
}
int ShelfConfig::control_button_edge_spacing(bool is_primary_axis_edge) const {
if (is_primary_axis_edge) {
return in_tablet_mode_ ? (is_in_app_ ? 0 : 8) : 6;
}
return (shelf_size() - control_size()) / 2;
}
base::TimeDelta ShelfConfig::hotseat_background_animation_duration() const {
// This matches the duration of the maximize/minimize animation.
return base::Milliseconds(300);
}
base::TimeDelta ShelfConfig::shelf_animation_duration() const {
return hotseat_background_animation_duration();
}
int ShelfConfig::status_area_hit_region_padding() const {
return is_dense_ ? shelf_status_area_hit_region_padding_dense_
: shelf_status_area_hit_region_padding_;
}
float ShelfConfig::drag_hide_ratio_threshold() const {
return kDragHideRatioThreshold;
}
void ShelfConfig::UpdateConfig(bool new_is_app_list_visible,
bool tablet_mode_changed) {
const bool new_is_dense = !in_tablet_mode_ || IsDenseForCurrentScreen();
const bool can_hide_shelf_controls =
in_tablet_mode_ && features::IsHideShelfControlsInTabletModeEnabled();
const bool new_shelf_controls_shown =
!can_hide_shelf_controls || ShelfControlsForcedShownForAccessibility();
// TODO(https://crbug.com/1058205): Test this behavior.
// If the virtual keyboard is shown, the back button and in-app shelf should
// be shown so users can exit the keyboard. SystemTrayModel may be null in
// tests.
const bool new_is_virtual_keyboard_shown = Shell::Get()->system_tray_model()
? Shell::Get()
->system_tray_model()
->virtual_keyboard()
->arc_keyboard_visible()
: false;
const bool new_is_in_app =
CalculateIsInApp(new_is_app_list_visible, new_is_virtual_keyboard_shown);
const bool changed =
tablet_mode_changed || is_dense_ != new_is_dense ||
is_in_app_ != new_is_in_app ||
shelf_controls_shown_ != new_shelf_controls_shown ||
is_virtual_keyboard_shown_ != new_is_virtual_keyboard_shown ||
is_app_list_visible_ != new_is_app_list_visible;
if (!changed)
return;
is_dense_ = new_is_dense;
shelf_controls_shown_ = new_shelf_controls_shown;
is_virtual_keyboard_shown_ = new_is_virtual_keyboard_shown;
is_app_list_visible_ = new_is_app_list_visible;
is_in_app_ = new_is_in_app;
OnShelfConfigUpdated();
}
int ShelfConfig::GetShelfSize(bool ignore_in_app_state) const {
// In clamshell mode, the shelf always has the same size.
if (!in_tablet_mode_)
return 48;
// Use in app shelf when split view is enabled.
if (!ignore_in_app_state && (is_in_app_ || in_split_view_with_overview_))
return in_app_shelf_size();
return is_dense_ ? kSystemShelfSizeTabletModeDense
: kSystemShelfSizeTabletModeNormal;
}
SkColor ShelfConfig::GetShelfControlButtonColor(
const views::Widget* widget) const {
DCHECK(widget);
const session_manager::SessionState session_state =
Shell::Get()->session_controller()->GetSessionState();
if (in_tablet_mode_ &&
session_state == session_manager::SessionState::ACTIVE) {
return is_in_app_ ? SK_ColorTRANSPARENT : GetDefaultShelfColor(widget);
}
return widget->GetColorProvider()->GetColor(
cros_tokens::kCrosSysSystemOnBase);
}
SkColor ShelfConfig::GetMaximizedShelfColor(const views::Widget* widget) const {
return widget->GetColorProvider()->GetColor(cros_tokens::kCrosSysSystemBase);
}
ui::ColorId ShelfConfig::GetShelfBaseLayerColorId() const {
if (in_tablet_mode_ && is_in_app_) {
// In tablet mode with an app, we use the same opaque color as maximized.
return cros_tokens::kCrosSysSystemBase;
}
return cros_tokens::kCrosSysSystemBaseElevated;
}
SkColor ShelfConfig::GetDefaultShelfColor(const views::Widget* widget) const {
DCHECK(widget);
const auto* color_provider = widget->GetColorProvider();
if (!features::IsBackgroundBlurEnabled())
return color_provider->GetColor(kColorAshShieldAndBase90);
return color_provider->GetColor(GetShelfBaseLayerColorId());
}
int ShelfConfig::GetShelfControlButtonBlurRadius() const {
if (features::IsBackgroundBlurEnabled() && in_tablet_mode_ && !is_in_app_)
return shelf_blur_radius_;
return 0;
}
int ShelfConfig::GetAppIconEndPadding() const {
return app_icon_end_padding_;
}
int ShelfConfig::GetAppIconGroupMargin() const {
return in_tablet_mode_ ? app_icon_group_margin_tablet_
: app_icon_group_margin_clamshell_;
}
base::TimeDelta ShelfConfig::DimAnimationDuration() const {
return base::Milliseconds(1000);
}
gfx::Tween::Type ShelfConfig::DimAnimationTween() const {
return gfx::Tween::LINEAR;
}
gfx::Size ShelfConfig::DragHandleSize() const {
const session_manager::SessionState session_state =
Shell::Get()->session_controller()->GetSessionState();
return session_state == session_manager::SessionState::ACTIVE
? gfx::Size(80, 4)
: gfx::Size(120, 4);
}
int ShelfConfig::GetSystemShelfSizeInTabletMode() const {
// Note that existing `is_dense_` takes in account current tablet/clamshell
// mode, but sometimes there is a need to get shelf size in tablet mode
// staying in clamshell mode.
return IsDenseForCurrentScreen() ? kSystemShelfSizeTabletModeDense
: kSystemShelfSizeTabletModeNormal;
}
int ShelfConfig::GetTabletModeShelfInsetsAndRecordUMA() {
if (!has_shown_elevated_app_bar_.has_value() ||
has_shown_elevated_app_bar_.value() != elevate_tablet_mode_app_bar_) {
has_shown_elevated_app_bar_ = elevate_tablet_mode_app_bar_;
// This method can be called more than once during the app bar rendering.
// Records only once when `elevate_tablet_mode_app_bar_` changes.
base::UmaHistogramBoolean("Ash.Shelf.ShowStackedHotseat",
elevate_tablet_mode_app_bar_);
}
return elevate_tablet_mode_app_bar_ ? kElevatedSystemShelfSizeTabletMode
: GetSystemShelfSizeInTabletMode();
}
int ShelfConfig::GetMinimumInlineAppBarSize() const {
return 6 * kSystemShelfSizeTabletModeDense + 5 * shelf_button_spacing_ +
2 * app_icon_end_padding_;
}
void ShelfConfig::UpdateShowElevatedAppBar(
const gfx::Size& inline_app_bar_size) {
elevate_tablet_mode_app_bar_ =
inline_app_bar_size.width() < GetMinimumInlineAppBarSize();
}
void ShelfConfig::UpdateConfigForAccessibilityState() {
UpdateConfig(is_app_list_visible_, /*tablet_mode_changed=*/false);
}
bool ShelfConfig::CalculateIsInApp(bool app_list_visible,
bool virtual_keyboard_shown) const {
Shell* shell = Shell::Get();
const auto* session = shell->session_controller();
if (!session ||
session->GetSessionState() != session_manager::SessionState::ACTIVE) {
return false;
}
if (virtual_keyboard_shown)
return true;
if (app_list_visible)
return false;
if (in_split_view_with_overview_)
return true;
if (overview_mode_)
return use_in_app_shelf_in_overview_;
return true;
}
void ShelfConfig::OnShelfConfigUpdated() {
for (auto& observer : observers_)
observer.OnShelfConfigUpdated();
}
} // namespace ash