#include "ash/system/toast/toast_overlay.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/compositor/layer_type.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/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 {
constexpr int kSlideAnimationDurationMs = 100;
gfx::Rect GetUserWorkAreaBounds(aura::Window* window) {
return WorkAreaInsets::ForWindow(window)->user_work_area_bounds();
}
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());
}
SystemToastView::ButtonType GetToastViewButtonType(
ToastData::ButtonType button_type) {
switch (button_type) {
case ToastData::ButtonType::kNone:
return SystemToastView::ButtonType::kNone;
case ToastData::ButtonType::kTextButton:
return SystemToastView::ButtonType::kTextButton;
case ToastData::ButtonType::kIconButton:
return SystemToastView::ButtonType::kIconButton;
}
}
}
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};
};
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(
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;
void OnEvent(const ui::Event& event) override {
switch (event.type()) {
case ui::EventType::kMouseEntered:
on_hover_state_changed_.Run(true);
break;
case ui::EventType::kMouseExited:
on_hover_state_changed_.Run(false);
break;
default:
NOTREACHED();
}
}
private:
std::unique_ptr<views::EventMonitor> event_monitor_;
HoverStateChangeCallback on_hover_state_changed_;
};
ToastOverlay::ToastOverlay(Delegate* delegate,
const ToastData& toast_data,
aura::Window* root_window)
: delegate_(delegate),
text_(toast_data.text),
overlay_widget_(new views::Widget),
display_observer_(std::make_unique<ToastDisplayObserver>(this)),
root_window_(root_window),
button_callback_(std::move(toast_data.button_callback)) {
overlay_view_ = std::make_unique<SystemToastView>(
toast_data.text, GetToastViewButtonType(toast_data.button_type),
toast_data.button_text, toast_data.button_icon,
base::BindRepeating(
&ToastOverlay::OnButtonClicked,
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.accept_events = true;
params.z_order = ui::ZOrderLevel::kFloatingUIElement;
params.layer_type = ui::LAYER_NOT_DRAWN;
params.shadow_type = views::Widget::InitParams::ShadowType::kNone;
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));
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));
auto* window_controller = RootWindowController::ForWindow(root_window_);
if (window_controller->GetStatusAreaWidget()) {
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);
animation_settings.SetTransitionDuration(original_duration);
animation_settings.AddObserver(this);
if (visible) {
overlay_widget_->ShowInactive();
overlay_view_->NotifyAccessibilityEventDeprecated(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::RequestFocusOnActiveToastButton() {
if (views::Button* button = overlay_view_->button()) {
button->RequestFocus();
return button->HasFocus();
}
return false;
}
bool ToastOverlay::IsButtonFocused() const {
if (auto* button = overlay_view_->button()) {
return button->HasFocus();
}
return false;
}
void ToastOverlay::OnSliderBubbleHeightChanged() {
UpdateOverlayBounds();
}
gfx::Rect ToastOverlay::CalculateOverlayBounds() {
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);
}
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);
}
int ToastOverlay::CalculateSliderBubbleOffset() {
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 (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 (button_callback_) {
button_callback_.Run();
}
Show(false);
}
void ToastOverlay::OnHoverStateChanged(bool is_hovering) {
DCHECK(hover_observer_);
if (!overlay_widget_->IsVisible())
return;
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) {
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::Button* ToastOverlay::button_for_testing() {
return overlay_view_->button();
}
}