// Copyright 2024 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/game_dashboard/game_dashboard_button_reveal_controller.h"
#include "ash/game_dashboard/game_dashboard_context.h"
#include "ash/game_dashboard/game_dashboard_utils.h"
#include "ash/wm/window_state.h"
#include "base/functional/bind.h"
#include "base/functional/callback_forward.h"
#include "chromeos/ui/frame/immersive/immersive_fullscreen_controller.h"
#include "ui/aura/window.h"
#include "ui/display/screen.h"
#include "ui/gfx/geometry/point.h"
#include "ui/views/animation/animation_builder.h"
#include "ui/views/widget/widget.h"
namespace ash {
namespace {
// The delay in between the mouse stopping at the top edge of the screen and the
// game dashboard button revealing.
constexpr base::TimeDelta kMouseRevealDelay = base::Milliseconds(200);
constexpr base::TimeDelta kSlideAnimationDuration = base::Milliseconds(200);
constexpr base::TimeDelta kNoSlideAnimationDuration = base::Milliseconds(0);
} // namespace
GameDashboardButtonRevealController::GameDashboardButtonRevealController(
GameDashboardContext* context)
: context_(context) {
DCHECK(context_);
context_->game_window()->AddPreTargetHandler(
this, ui::EventTarget::Priority::kSystem);
UpdateVisibility(/*target_visibility=*/false, /*animate=*/false);
}
GameDashboardButtonRevealController::~GameDashboardButtonRevealController() {
UpdateVisibility(/*target_visibility=*/true, /*animate=*/false);
context_->game_window()->RemovePreTargetHandler(this);
}
void GameDashboardButtonRevealController::UpdateVisibility(
bool target_visibility,
bool animate) {
if (target_visibility ==
context_->game_dashboard_button_widget()->IsVisible()) {
return;
}
context_->SetGameDashboardButtonVisibility(/*visible=*/true);
views::AnimationBuilder()
.SetPreemptionStrategy(ui::LayerAnimator::PreemptionStrategy::
IMMEDIATELY_ANIMATE_TO_NEW_TARGET)
.OnEnded(
base::BindOnce(&GameDashboardButtonRevealController::OnAnimationEnd,
weak_ptr_factory_.GetWeakPtr(), target_visibility))
.Once()
.SetDuration(animate ? kSlideAnimationDuration
: kNoSlideAnimationDuration)
.SetTransform(
context_->game_dashboard_button_widget()->GetLayer(),
target_visibility
? gfx::Transform()
: gfx::Transform::MakeTranslation(
/*tx=*/0,
/*ty=*/-game_dashboard_utils::GetFrameHeaderHeight(
context_->game_window())),
gfx::Tween::EASE_OUT);
}
void GameDashboardButtonRevealController::OnMouseEvent(ui::MouseEvent* event) {
const auto event_type = event->type();
if (event_type != ui::EventType::kMouseMoved &&
event_type != ui::EventType::kMouseReleased &&
event_type != ui::EventType::kMouseCaptureChanged) {
return;
}
// Start `top_edge_hover_timer_` to show the game dashboard button, if the
// mouse cursor is within the top edge of the game window in fullscreen.
const gfx::Point mouse_screen_location =
event->target()->GetScreenLocation(*event);
if (IsEventWithinButtonRevealBounds(
mouse_screen_location,
chromeos::ImmersiveFullscreenController::kMouseRevealBoundsHeight)) {
if (!top_edge_hover_timer_.IsRunning()) {
top_edge_hover_timer_.Start(
FROM_HERE, kMouseRevealDelay, this,
&GameDashboardButtonRevealController::OnTopEdgeHoverTimeout);
}
return;
}
// The mouse cursor is not within the top edge of the window.
top_edge_hover_timer_.Stop();
// If the main menu is closed, try to hide the game dashboard button.
if (CanHideGameDashboardButton(mouse_screen_location)) {
UpdateVisibility(/*target_visibility=*/false, /*animate=*/true);
}
}
void GameDashboardButtonRevealController::OnGestureEvent(
ui::GestureEvent* event) {
switch (event->type()) {
case ui::EventType::kGestureScrollBegin:
// Record the start location of a scroll gesture.
gesture_scroll_start_pos_ = event->location();
return;
case ui::EventType::kGestureScrollUpdate: {
if (!gesture_scroll_start_pos_.has_value()) {
return;
}
// If scroll started at top and is going towards the bottom, show the game
// dashboard button; otherwise, hide the game dashboard button.
// Everything following is called at most once, as the optional variable
// is immediately reset.
const bool target_visibility =
event->details().scroll_y() > 0 &&
IsEventWithinButtonRevealBounds(
gesture_scroll_start_pos_.value(),
game_dashboard_utils::GetFrameHeaderHeight(
context_->game_window()));
UpdateVisibility(target_visibility, /*animate=*/true);
gesture_scroll_start_pos_.reset();
return;
}
case ui::EventType::kGestureScrollEnd:
case ui::EventType::kScrollFlingStart:
gesture_scroll_start_pos_.reset();
return;
default:
return;
}
}
void GameDashboardButtonRevealController::OnTouchEvent(ui::TouchEvent* event) {
// If the main menu is open, or the Game Dashboard button is not visible, do
// nothing.
if (event->type() != ui::EventType::kTouchPressed ||
context_->IsMainMenuOpen() ||
!context_->game_dashboard_button_widget()->IsVisible()) {
return;
}
// If the touch event is within the Game Dashboard button, do nothing.
const gfx::Point touch_event_location =
event->target()->GetScreenLocation(*event);
if (context_->game_dashboard_button_widget()
->GetWindowBoundsInScreen()
.Contains(touch_event_location)) {
return;
}
// Hide the button. The touch event is outside the Game Dashboard button.
UpdateVisibility(/*target_visibility=*/false, /*animate=*/true);
}
bool GameDashboardButtonRevealController::CanShowGameDashboardButton(
const gfx::Point& mouse_screen_location) {
return !context_->game_dashboard_button_widget()->IsVisible() &&
IsEventWithinButtonRevealBounds(
mouse_screen_location,
chromeos::ImmersiveFullscreenController::kMouseRevealBoundsHeight);
}
bool GameDashboardButtonRevealController::CanHideGameDashboardButton(
const gfx::Point& mouse_screen_location) {
return !context_->IsMainMenuOpen() &&
context_->game_dashboard_button_widget()->IsVisible() &&
IsMouseOutsideHeaderBounds(mouse_screen_location);
}
bool GameDashboardButtonRevealController::IsEventWithinButtonRevealBounds(
const gfx::Point& event_screen_location,
int reveal_height) {
gfx::Rect button_reveal_bounds = context_->game_window()->GetBoundsInScreen();
button_reveal_bounds.set_height(reveal_height);
return button_reveal_bounds.Contains(event_screen_location);
}
bool GameDashboardButtonRevealController::IsMouseOutsideHeaderBounds(
const gfx::Point& mouse_screen_location) {
gfx::Rect header_bounds = context_->game_window()->GetBoundsInScreen();
header_bounds.set_height(
game_dashboard_utils::GetFrameHeaderHeight(context_->game_window()));
// Allow the cursor to move slightly off the top-of-window views before hiding
// the button. This matches the behavior in
// `ImmersiveFullscreenController::UpdateLocatedEventRevealedLock()`.
header_bounds.Inset(
gfx::Insets::TLBR(0, 0,
-chromeos::ImmersiveFullscreenController::
kImmersiveFullscreenTopEdgeInset,
0));
return !header_bounds.Contains(mouse_screen_location);
}
void GameDashboardButtonRevealController::OnTopEdgeHoverTimeout() {
if (CanShowGameDashboardButton(
display::Screen::GetScreen()->GetCursorScreenPoint())) {
UpdateVisibility(/*target_visibility=*/true, /*animate=*/true);
}
}
void GameDashboardButtonRevealController::OnAnimationEnd(
bool target_visibility) {
if (!target_visibility) {
// The slide up animation has ended. Make the Game Dashboard button
// widget not visible.
context_->SetGameDashboardButtonVisibility(/*visible=*/false);
}
}
} // namespace ash