// Copyright 2016 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/toast/toast_overlay.h"
#include "ash/constants/ash_features.h"
#include "ash/keyboard/ui/keyboard_ui_controller.h"
#include "ash/public/cpp/ash_typography.h"
#include "ash/public/cpp/shelf_types.h"
#include "ash/public/cpp/shell_window_ids.h"
#include "ash/public/cpp/system/toast_data.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/root_window_controller.h"
#include "ash/shelf/hotseat_widget.h"
#include "ash/shelf/shelf.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/ash_color_provider.h"
#include "ash/style/pill_button.h"
#include "ash/system/toast/system_toast_view.h"
#include "ash/wm/window_properties.h"
#include "ash/wm/work_area_insets.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/aura/window.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/compositor/layer.h"
#include "ui/display/display_observer.h"
#include "ui/events/event_observer.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/font_list.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/views/animation/ink_drop.h"
#include "ui/views/animation/ink_drop_highlight.h"
#include "ui/views/background.h"
#include "ui/views/border.h"
#include "ui/views/controls/button/label_button.h"
#include "ui/views/controls/highlight_path_generator.h"
#include "ui/views/controls/label.h"
#include "ui/views/event_monitor.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/view.h"
#include "ui/views/widget/widget.h"
#include "ui/views/widget/widget_delegate.h"
#include "ui/wm/core/window_animations.h"
namespace ash {
namespace {
// Duration of slide animation when overlay is shown or hidden.
constexpr int kSlideAnimationDurationMs = 100;
// Returns the work area bounds for the root window where new windows are added
// (including new toasts).
gfx::Rect GetUserWorkAreaBounds(aura::Window* window) {
return WorkAreaInsets::ForWindow(window)->user_work_area_bounds();
}
// Offsets the bottom of bounds for toast to accommodate the hotseat, based on
// the current hotseat state
void AdjustWorkAreaBoundsForHotseatState(gfx::Rect& bounds,
const HotseatWidget* hotseat_widget) {
if (hotseat_widget->state() == HotseatState::kExtended) {
bounds.set_height(bounds.height() - hotseat_widget->GetHotseatSize() -
ShelfConfig::Get()->hotseat_bottom_padding());
}
if (hotseat_widget->state() == HotseatState::kShownHomeLauncher)
bounds.set_height(hotseat_widget->GetTargetBounds().y() - bounds.y());
}
} // namespace
///////////////////////////////////////////////////////////////////////////////
// ToastDisplayObserver
class ToastOverlay::ToastDisplayObserver : public display::DisplayObserver {
public:
explicit ToastDisplayObserver(ToastOverlay* overlay) : overlay_(overlay) {}
ToastDisplayObserver(const ToastDisplayObserver&) = delete;
ToastDisplayObserver& operator=(const ToastDisplayObserver&) = delete;
~ToastDisplayObserver() override {}
void OnDisplayMetricsChanged(const display::Display& display,
uint32_t changed_metrics) override {
overlay_->UpdateOverlayBounds();
}
private:
const raw_ptr<ToastOverlay> overlay_;
display::ScopedDisplayObserver display_observer_{this};
};
///////////////////////////////////////////////////////////////////////////////
// ToastHoverObserver
class ToastOverlay::ToastHoverObserver : public ui::EventObserver {
public:
using HoverStateChangeCallback =
base::RepeatingCallback<void(bool is_hovering)>;
ToastHoverObserver(aura::Window* widget_window,
HoverStateChangeCallback on_hover_state_changed)
: event_monitor_(views::EventMonitor::CreateWindowMonitor(
/*event_observer=*/this,
widget_window,
{ui::EventType::kMouseEntered, ui::EventType::kMouseExited})),
on_hover_state_changed_(std::move(on_hover_state_changed)) {}
ToastHoverObserver(const ToastHoverObserver&) = delete;
ToastHoverObserver& operator=(const ToastHoverObserver&) = delete;
~ToastHoverObserver() override = default;
// ui::EventObserver:
void OnEvent(const ui::Event& event) override {
switch (event.type()) {
case ui::EventType::kMouseEntered:
on_hover_state_changed_.Run(/*is_hovering=*/true);
break;
case ui::EventType::kMouseExited:
on_hover_state_changed_.Run(/*is_hovering=*/false);
break;
default:
NOTREACHED();
}
}
private:
// While this `EventMonitor` object exists, this object will only look for
// `ui::EventType::kMouseEntered` and `ui::EventType::kMouseExited` events
// that occur in the `widget_window` indicated in the constructor.
std::unique_ptr<views::EventMonitor> event_monitor_;
// This is run whenever the mouse enters or exits the observed window with a
// parameter to indicate whether the window is being hovered.
HoverStateChangeCallback on_hover_state_changed_;
};
///////////////////////////////////////////////////////////////////////////////
// ToastOverlay
ToastOverlay::ToastOverlay(Delegate* delegate,
const ToastData& toast_data,
aura::Window* root_window)
: delegate_(delegate),
text_(toast_data.text),
dismiss_text_(toast_data.dismiss_text),
overlay_widget_(new views::Widget),
display_observer_(std::make_unique<ToastDisplayObserver>(this)),
root_window_(root_window),
dismiss_callback_(std::move(toast_data.dismiss_callback)) {
// The provided callback is stored in the overlay's `dismiss_callback_`.
overlay_view_ = std::make_unique<SystemToastView>(
toast_data.text, toast_data.dismiss_text, /*dismiss_callback=*/
base::BindRepeating(
&ToastOverlay::OnButtonClicked,
// Unretained is safe because `this` owns `overlay_view_`.
base::Unretained(this)),
toast_data.leading_icon);
views::Widget::InitParams params(
views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET,
views::Widget::InitParams::TYPE_POPUP);
params.name = "ToastOverlay";
params.opacity = views::Widget::InitParams::WindowOpacity::kTranslucent;
params.accept_events = true;
params.z_order = ui::ZOrderLevel::kFloatingUIElement;
params.bounds = CalculateOverlayBounds();
params.parent =
root_window_->GetChildById(kShellWindowId_SettingBubbleContainer);
params.activatable = toast_data.activatable
? views::Widget::InitParams::Activatable::kYes
: views::Widget::InitParams::Activatable::kNo;
params.init_properties_container.SetProperty(kStayInOverviewOnActivationKey,
true);
overlay_widget_->Init(std::move(params));
overlay_widget_->SetVisibilityChangedAnimationsEnabled(true);
overlay_widget_->SetContentsView(overlay_view_.get());
UpdateOverlayBounds();
aura::Window* overlay_window = overlay_widget_->GetNativeWindow();
::wm::SetWindowVisibilityAnimationType(
overlay_window, ::wm::WINDOW_VISIBILITY_ANIMATION_TYPE_VERTICAL);
::wm::SetWindowVisibilityAnimationDuration(
overlay_window, base::Milliseconds(kSlideAnimationDurationMs));
// Only toasts that expire should be able to persist on hover (i.e. toasts
// with infinite duration persist regardless of hover).
if (toast_data.persist_on_hover &&
(toast_data.duration != ToastData::kInfiniteDuration)) {
hover_observer_ = std::make_unique<ToastHoverObserver>(
overlay_window, base::BindRepeating(&ToastOverlay::OnHoverStateChanged,
base::Unretained(this)));
}
keyboard::KeyboardUIController::Get()->AddObserver(this);
shelf_observation_.Observe(Shelf::ForWindow(overlay_window));
if (features::AreSideAlignedToastsEnabled()) {
auto* window_controller = RootWindowController::ForWindow(root_window_);
if (window_controller->GetStatusAreaWidget()) {
// `UnifiedSystemTray` is observed when side aligned toasts are enabled so
// we can shift the toast baseline up when slider bubbles are visible.
// The observation is safe on external monitor disconnect because
// ToastManagerImpl deletes the ToastOverlay before the root window is
// destroyed.
scoped_unified_system_tray_observer_.Observe(
window_controller->GetStatusAreaWidget()->unified_system_tray());
}
}
}
ToastOverlay::~ToastOverlay() {
keyboard::KeyboardUIController::Get()->RemoveObserver(this);
overlay_widget_->Close();
}
void ToastOverlay::Show(bool visible) {
if (overlay_widget_->GetLayer()->GetTargetVisibility() == visible)
return;
ui::LayerAnimator* animator = overlay_widget_->GetLayer()->GetAnimator();
DCHECK(animator);
base::TimeDelta original_duration = animator->GetTransitionDuration();
ui::ScopedLayerAnimationSettings animation_settings(animator);
// ScopedLayerAnimationSettings ctor changes the transition duration, so
// change it back to the original value (should be zero).
animation_settings.SetTransitionDuration(original_duration);
animation_settings.AddObserver(this);
if (visible) {
overlay_widget_->ShowInactive();
// Notify accessibility about the overlay.
overlay_view_->NotifyAccessibilityEvent(ax::mojom::Event::kAlert, false);
} else {
overlay_widget_->Hide();
}
}
void ToastOverlay::UpdateOverlayBounds() {
overlay_widget_->SetBounds(CalculateOverlayBounds());
}
const std::u16string ToastOverlay::GetText() const {
return text_;
}
bool ToastOverlay::RequestFocusOnActiveToastDismissButton() {
overlay_view_->dismiss_button()->RequestFocus();
return overlay_view_->dismiss_button()->HasFocus();
}
bool ToastOverlay::IsDismissButtonFocused() const {
if (auto* dismiss_button = overlay_view_->dismiss_button()) {
return dismiss_button->HasFocus();
}
return false;
}
void ToastOverlay::OnSliderBubbleHeightChanged() {
// We only update toast baseline if they are aligned to the side.
if (features::AreSideAlignedToastsEnabled()) {
UpdateOverlayBounds();
}
}
gfx::Rect ToastOverlay::CalculateOverlayBounds() {
// If the native window has not been initialized, as in the first call, get
// the default root window. Otherwise get the window for this `overlay_widget`
// to handle multiple monitors properly.
auto* window = overlay_widget_->IsNativeWidgetInitialized()
? overlay_widget_->GetNativeWindow()
: root_window_.get();
DCHECK(window);
auto* window_controller = RootWindowController::ForWindow(window);
auto* hotseat_widget = window_controller->shelf()->hotseat_widget();
auto widget_size = overlay_view_->GetPreferredSize();
gfx::Rect bounds = GetUserWorkAreaBounds(window);
if (hotseat_widget) {
AdjustWorkAreaBoundsForHotseatState(bounds, hotseat_widget);
}
if (features::AreSideAlignedToastsEnabled()) {
// Toasts should always follow the status area and will usually show on the
// bottom-right of the screen. They will show at the bottom-left whenever
// the shelf is left-aligned or for RTL when the shelf is not right aligned.
auto alignment = window_controller->shelf()->alignment();
const int target_x =
((base::i18n::IsRTL() && alignment != ShelfAlignment::kRight) ||
alignment == ShelfAlignment::kLeft)
? bounds.x() + ToastOverlay::kOffset
: bounds.right() - widget_size.width() - ToastOverlay::kOffset;
const int target_y = bounds.bottom() - widget_size.height() -
ToastOverlay::kOffset - CalculateSliderBubbleOffset();
return gfx::Rect(gfx::Point(target_x, target_y), widget_size);
}
const int target_y =
bounds.bottom() - widget_size.height() - ToastOverlay::kOffset;
bounds.ClampToCenteredSize(widget_size);
bounds.set_y(target_y);
return bounds;
}
// Calculates the y offset used to shift side aligned toasts up whenever case a
// slider bubble is visible.
int ToastOverlay::CalculateSliderBubbleOffset() {
// Slider bubble offset is only used for side aligned toasts.
if (!features::AreSideAlignedToastsEnabled()) {
return 0;
}
auto* window_controller = RootWindowController::ForWindow(root_window_);
if (!window_controller) {
return 0;
}
auto* status_area_widget = window_controller->GetStatusAreaWidget();
if (!status_area_widget) {
return 0;
}
auto* unified_system_tray = status_area_widget->unified_system_tray();
if (!unified_system_tray) {
return 0;
}
// If a slider bubble is visible, the toast baseline will be shifted
// up by the slider bubble's height + a default spacing offset.
if (unified_system_tray->IsSliderBubbleShown()) {
auto* slider_view = unified_system_tray->GetSliderView();
if (!slider_view) {
return 0;
}
return slider_view->height() + ToastOverlay::kOffset;
}
return 0;
}
void ToastOverlay::OnButtonClicked() {
if (dismiss_callback_) {
dismiss_callback_.Run();
}
Show(/*visible=*/false);
}
void ToastOverlay::OnHoverStateChanged(bool is_hovering) {
DCHECK(hover_observer_);
if (!overlay_widget_->IsVisible())
return;
// We want to update the `delegate_` here in case this toast is also
// displaying on other monitors.
delegate_->OnToastHoverStateChanged(is_hovering);
}
void ToastOverlay::OnImplicitAnimationsScheduled() {}
void ToastOverlay::OnImplicitAnimationsCompleted() {
if (!overlay_widget_->GetLayer()->GetTargetVisibility())
delegate_->CloseToast();
}
void ToastOverlay::OnKeyboardOccludedBoundsChanged(
const gfx::Rect& new_bounds_in_screen) {
// TODO(crbug.com/40619022): Observe changes in user work area bounds
// directly instead of listening for keyboard bounds changes.
UpdateOverlayBounds();
}
void ToastOverlay::OnShelfWorkAreaInsetsChanged() {
UpdateOverlayBounds();
}
void ToastOverlay::OnHotseatStateChanged(HotseatState old_state,
HotseatState new_state) {
UpdateOverlayBounds();
}
views::Widget* ToastOverlay::widget_for_testing() {
return overlay_widget_.get();
}
views::LabelButton* ToastOverlay::dismiss_button_for_testing() {
return overlay_view_->dismiss_button();
}
} // namespace ash