0

gd: Add Welcome Dialog

Adds support to display a welcome dialog for 4 seconds on every game
window opened.

Bug: b:304817405
Test: Added unit tests
Demo: b/304817405#comment9
Change-Id: I1b9dfb3278f9f5dad1a4e7af0423edbc09b73523
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5106463
Commit-Queue: Gina Domergue <gdomergue@google.com>
Reviewed-by: Prameet Shah <phshah@chromium.org>
Reviewed-by: Ahmed Fakhry <afakhry@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1250993}
This commit is contained in:
Gina Domergue
2024-01-23 19:55:22 +00:00
committed by Chromium LUCI CQ
parent 16673b7e34
commit 51e76e01a2
13 changed files with 524 additions and 54 deletions

@ -659,6 +659,7 @@ component("ash") {
"frame_throttler/frame_throttling_observer.h",
"game_dashboard/game_dashboard_button.cc",
"game_dashboard/game_dashboard_button.h",
"game_dashboard/game_dashboard_constants.h",
"game_dashboard/game_dashboard_context.cc",
"game_dashboard/game_dashboard_context.h",
"game_dashboard/game_dashboard_controller.cc",
@ -670,6 +671,8 @@ component("ash") {
"game_dashboard/game_dashboard_toolbar_view.h",
"game_dashboard/game_dashboard_utils.cc",
"game_dashboard/game_dashboard_utils.h",
"game_dashboard/game_dashboard_welcome_dialog.cc",
"game_dashboard/game_dashboard_welcome_dialog.h",
"game_dashboard/game_dashboard_widget.cc",
"game_dashboard/game_dashboard_widget.h",
"glanceables/classroom/glanceables_classroom_client.h",

@ -7315,6 +7315,12 @@ To shut down the device, press and hold the power button on the device again.
<message name="IDS_ASH_GAME_DASHBOARD_VISIBLE_STATUS" translateable="false" desc="The visible state for compact Game Dashboard tile sub-labels.">
Visible
</message>
<message name="IDS_ASH_GAME_DASHBOARD_WELCOME_DIALOG_SHORTCUT" translateable="false" desc="The text displayed in the welcome dialog to show how to toggle the Game Dashboard.">
Press <ph name="LAUNCHER_KEY_NAME">$1<ex>Launcher</ex></ph> + g at anytime
</message>
<message name="IDS_ASH_GAME_DASHBOARD_WELCOME_DIALOG_SUB_LABEL" translateable="false" desc="The additional context added to the welcome dialog about the Game Dashboard.">
Customize your gaming experience
</message>
<!-- Game Dashboard / Game Controls strings -->
<message name="IDS_ASH_GAME_DASHBOARD_GC_TILE_VISIBLE" desc="The sub-label for Game Controls tile when input mapping hint is visible.">

@ -0,0 +1 @@
35df49495339e436b0a61bcf783e089300ac7ddf

@ -0,0 +1 @@
8801a2612e169a159f77f7aee64e0b1f8218d08c

@ -757,6 +757,9 @@ TEST_F(GameDashboardCaptureModeTest, CursorAndClickBehaviorWhenAnchored) {
// The game window should be the top most active window.
wm::ActivateWindow(game_window());
// TODO(b/316141148): Remove this call once the welcome dialog is disabled by
// default for tests.
WaitForSeconds(/*seconds=*/4);
auto* controller = StartGameCaptureModeSession();
// Hover over empty space where there is no window.

@ -0,0 +1,22 @@
// 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.
#ifndef ASH_GAME_DASHBOARD_GAME_DASHBOARD_CONSTANTS_H_
#define ASH_GAME_DASHBOARD_GAME_DASHBOARD_CONSTANTS_H_
namespace ash::game_dashboard {
// Toolbar padding from the border of the game window.
inline constexpr int kToolbarEdgePadding = 10;
// Interior margin padding around the game window for the
// `GameDashboardWelcomeDialog`.
inline constexpr int kWelcomeDialogEdgePadding = 8;
// Welcome dialog fixed width.
inline constexpr int kWelcomeDialogFixedWidth = 360;
} // namespace ash::game_dashboard
#endif // ASH_GAME_DASHBOARD_GAME_DASHBOARD_CONSTANTS_H_

@ -8,10 +8,12 @@
#include <string>
#include "ash/game_dashboard/game_dashboard_button.h"
#include "ash/game_dashboard/game_dashboard_constants.h"
#include "ash/game_dashboard/game_dashboard_controller.h"
#include "ash/game_dashboard/game_dashboard_main_menu_view.h"
#include "ash/game_dashboard/game_dashboard_toolbar_view.h"
#include "ash/game_dashboard/game_dashboard_utils.h"
#include "ash/game_dashboard/game_dashboard_welcome_dialog.h"
#include "ash/game_dashboard/game_dashboard_widget.h"
#include "ash/public/cpp/app_types_util.h"
#include "ash/public/cpp/arc_game_controls_flag.h"
@ -24,6 +26,7 @@
#include "ui/base/l10n/time_format.h"
#include "ui/compositor/layer.h"
#include "ui/events/types/event_type.h"
#include "ui/gfx/geometry/point.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/transform.h"
#include "ui/views/animation/animation_builder.h"
@ -39,13 +42,15 @@ constexpr base::TimeDelta kCountUpTimerRefreshInterval = base::Seconds(1);
// Number of pixels to add to the top and bottom of the Game Dashboard button so
// that it's centered within the frame header.
static const int kGameDashboardButtonVerticalPaddingDp = 3;
constexpr int kGameDashboardButtonVerticalPaddingDp = 3;
// Toolbar padding from the border of the game window.
static const int kToolbarEdgePadding = 10;
// Maximum width of the game window that centers the welcome dialog in the
// window instead of right aligned.
constexpr int kMaxCenteredWelcomeDialogWidth =
1.5 * game_dashboard::kWelcomeDialogFixedWidth;
// The animation duration for the bounds change operation on the toolbar widget.
static constexpr base::TimeDelta kToolbarBoundsChangeAnimationDuration =
constexpr base::TimeDelta kToolbarBoundsChangeAnimationDuration =
base::Milliseconds(150);
std::unique_ptr<GameDashboardWidget> CreateTransientChildWidget(
@ -63,6 +68,7 @@ std::unique_ptr<GameDashboardWidget> CreateTransientChildWidget(
params.parent = game_window;
params.name = widget_name;
params.activatable = activatable;
params.opacity = views::Widget::InitParams::WindowOpacity::kTranslucent;
auto widget = std::make_unique<GameDashboardWidget>();
widget->Init(std::move(params));
@ -80,7 +86,15 @@ GameDashboardContext::GameDashboardContext(aura::Window* game_window)
: game_window_(game_window),
toolbar_snap_location_(ToolbarSnapLocation::kTopRight) {
DCHECK(game_window_);
// TODO(b/316141148): Update `show_welcome_dialog_` to reflect the welcome
// dialog state in the settings.
show_welcome_dialog_ = true;
CreateAndAddGameDashboardButtonWidget();
// ARC windows handle displaying the welcome dialog once the
// `game_dashboard_button_` becomes available.
if (!IsArcWindow(game_window_)) {
MaybeShowWelcomeDialog();
}
}
GameDashboardContext::~GameDashboardContext() {
@ -88,6 +102,7 @@ GameDashboardContext::~GameDashboardContext() {
if (main_menu_widget_) {
main_menu_widget_->CloseNow();
}
CloseWelcomeDialog();
}
void GameDashboardContext::SetToolbarSnapLocation(
@ -99,13 +114,20 @@ void GameDashboardContext::SetToolbarSnapLocation(
void GameDashboardContext::OnWindowBoundsChanged() {
UpdateGameDashboardButtonWidgetBounds();
MaybeUpdateToolbarWidgetBounds();
MaybeUpdateWelcomeDialogBounds();
}
void GameDashboardContext::UpdateForGameControlsFlags() {
CHECK(IsArcWindow(game_window_));
game_dashboard_button_->SetEnabled(
game_dashboard_utils::ShouldEnableGameDashboardButton(game_window_));
const bool should_enable_button =
game_dashboard_utils::ShouldEnableGameDashboardButton(game_window_);
game_dashboard_button_->SetEnabled(should_enable_button);
if (should_enable_button) {
// ARC windows handle displaying the welcome dialog once the
// `game_dashboard_button_` becomes available.
MaybeShowWelcomeDialog();
}
if (toolbar_view_) {
toolbar_view_->UpdateViewForGameControls(
@ -227,6 +249,7 @@ void GameDashboardContext::OnViewPreferredSizeChanged(
views::View* observed_view) {
CHECK_EQ(game_dashboard_button_, observed_view);
UpdateGameDashboardButtonWidgetBounds();
MaybeUpdateWelcomeDialogBounds();
}
void GameDashboardContext::OnWidgetDestroying(views::Widget* widget) {
@ -278,47 +301,105 @@ void GameDashboardContext::UpdateGameDashboardButtonWidgetBounds() {
void GameDashboardContext::OnGameDashboardButtonPressed() {
// TODO(b/273640775): Add metrics to know when the Game Dashboard button was
// physically pressed.
// Close the welcome dialog if it's open when a user opens the main menu view.
CloseWelcomeDialog();
ToggleMainMenu();
}
void GameDashboardContext::MaybeShowWelcomeDialog() {
if (!show_welcome_dialog_) {
return;
}
DCHECK(!welcome_dialog_widget_);
show_welcome_dialog_ = false;
auto view = std::make_unique<GameDashboardWelcomeDialog>();
GameDashboardWelcomeDialog* welcome_dialog_view = view.get();
welcome_dialog_widget_ = CreateTransientChildWidget(
game_window_, "GameDashboardWelcomeDialog", std::move(view),
/*activatable=*/views::Widget::InitParams::Activatable::kNo);
welcome_dialog_widget_->AddObserver(this);
MaybeUpdateWelcomeDialogBounds();
welcome_dialog_widget_->Show();
welcome_dialog_view->StartTimer(base::BindRepeating(
&GameDashboardContext::CloseWelcomeDialog, base::Unretained(this)));
}
void GameDashboardContext::MaybeUpdateWelcomeDialogBounds() {
if (!welcome_dialog_widget_) {
return;
}
const gfx::Rect game_bounds = game_window_->GetBoundsInScreen();
const gfx::Size preferred_size =
welcome_dialog_widget_->GetContentsView()->GetPreferredSize();
const int frame_header_height = GetFrameHeaderHeight();
int origin_x;
if (game_bounds.width() > kMaxCenteredWelcomeDialogWidth) {
// Place welcome dialog right aligned in the game window.
origin_x = game_bounds.right() - game_dashboard::kWelcomeDialogEdgePadding -
preferred_size.width();
} else {
// Place welcome dialog centered in the game window.
origin_x =
game_bounds.x() + (game_bounds.width() - preferred_size.width()) / 2;
}
welcome_dialog_widget_->SetBounds(gfx::Rect(
gfx::Point(origin_x, game_bounds.y() +
game_dashboard::kWelcomeDialogEdgePadding +
frame_header_height),
preferred_size));
}
const gfx::Rect GameDashboardContext::CalculateToolbarWidgetBounds() {
const gfx::Rect game_bounds = game_window_->GetBoundsInScreen();
const gfx::Size preferred_size =
toolbar_widget_->GetContentsView()->GetPreferredSize();
auto* frame_header = chromeos::FrameHeader::Get(
views::Widget::GetWidgetForNativeWindow(game_window_));
const int frame_header_height =
(frame_header && frame_header->view()->GetVisible())
? frame_header->GetHeaderHeight()
: 0;
const int frame_header_height = GetFrameHeaderHeight();
gfx::Point origin;
switch (toolbar_snap_location_) {
case ToolbarSnapLocation::kTopRight:
origin = gfx::Point(
game_bounds.right() - kToolbarEdgePadding - preferred_size.width(),
game_bounds.y() + kToolbarEdgePadding + frame_header_height);
origin =
gfx::Point(game_bounds.right() - game_dashboard::kToolbarEdgePadding -
preferred_size.width(),
game_bounds.y() + game_dashboard::kToolbarEdgePadding +
frame_header_height);
break;
case ToolbarSnapLocation::kTopLeft:
origin = gfx::Point(
game_bounds.x() + kToolbarEdgePadding,
game_bounds.y() + kToolbarEdgePadding + frame_header_height);
origin =
gfx::Point(game_bounds.x() + game_dashboard::kToolbarEdgePadding,
game_bounds.y() + game_dashboard::kToolbarEdgePadding +
frame_header_height);
break;
case ToolbarSnapLocation::kBottomRight:
origin = gfx::Point(
game_bounds.right() - kToolbarEdgePadding - preferred_size.width(),
game_bounds.bottom() - kToolbarEdgePadding - preferred_size.height());
game_bounds.right() - game_dashboard::kToolbarEdgePadding -
preferred_size.width(),
game_bounds.bottom() - game_dashboard::kToolbarEdgePadding -
preferred_size.height());
break;
case ToolbarSnapLocation::kBottomLeft:
origin = gfx::Point(
game_bounds.x() + kToolbarEdgePadding,
game_bounds.bottom() - kToolbarEdgePadding - preferred_size.height());
origin = gfx::Point(game_bounds.x() + game_dashboard::kToolbarEdgePadding,
game_bounds.bottom() -
game_dashboard::kToolbarEdgePadding -
preferred_size.height());
break;
}
return gfx::Rect(origin, preferred_size);
}
int GameDashboardContext::GetFrameHeaderHeight() const {
auto* frame_header = chromeos::FrameHeader::Get(
views::Widget::GetWidgetForNativeWindow(game_window_));
return (frame_header && frame_header->view()->GetVisible())
? frame_header->GetHeaderHeight()
: 0;
}
void GameDashboardContext::AnimateToolbarWidgetBoundsChange(
const gfx::Rect& target_screen_bounds) {
DCHECK(toolbar_widget_);
@ -362,4 +443,11 @@ void GameDashboardContext::OnUpdateRecordingTimer() {
}
}
void GameDashboardContext::CloseWelcomeDialog() {
if (welcome_dialog_widget_) {
welcome_dialog_widget_->RemoveObserver(this);
welcome_dialog_widget_.reset();
}
}
} // namespace ash

@ -107,6 +107,12 @@ class ASH_EXPORT GameDashboardContext : public views::ViewObserver,
// views::WidgetObserver:
void OnWidgetDestroying(views::Widget* widget) override;
// TODO(b/316141148): Remove this test function once it's possible to set
// `show_welcome_dialog_` via a property.
void SetShowWelcomeDialogForTesting(bool show_dialog) {
show_welcome_dialog_ = show_dialog;
}
private:
friend class GameDashboardContextTestApi;
@ -121,10 +127,21 @@ class ASH_EXPORT GameDashboardContext : public views::ViewObserver,
// Called when `GameDashboardButton` is pressed, and toggles the main menu.
void OnGameDashboardButtonPressed();
// Shows the Game Dashboard welcome dialog, if it's enabled in the Game
// Dashboard settings.
void MaybeShowWelcomeDialog();
// Updates the Game Dashboard welcome dialog's bounds and location, relative
// to the `game_window_`.
void MaybeUpdateWelcomeDialogBounds();
// Determines the toolbar's physical location on screen based on the
// `toolbar_snap_location_` value.
const gfx::Rect CalculateToolbarWidgetBounds();
// Calculates the height of the app's frame header.
int GetFrameHeaderHeight() const;
// Updates the toolbar widget's bounds and location utilizing an animation as
// it transfers from the previous location.
void AnimateToolbarWidgetBoundsChange(const gfx::Rect& target_screen_bounds);
@ -133,6 +150,10 @@ class ASH_EXPORT GameDashboardContext : public views::ViewObserver,
// recording session duration.
void OnUpdateRecordingTimer();
// Closes and deletes the Game Dashboard welcome dialog once it's no longer
// needed.
void CloseWelcomeDialog();
const raw_ptr<aura::Window> game_window_;
// Game Dashboard button widget for the Game Dashboard.
@ -144,6 +165,9 @@ class ASH_EXPORT GameDashboardContext : public views::ViewObserver,
// The toolbar for the Game Dashboard.
std::unique_ptr<GameDashboardWidget> toolbar_widget_;
// The dialog displayed when the game window first opens.
std::unique_ptr<GameDashboardWidget> welcome_dialog_widget_;
// The indicator of the current corner that the toolbar is placed.
ToolbarSnapLocation toolbar_snap_location_;
@ -170,6 +194,11 @@ class ASH_EXPORT GameDashboardContext : public views::ViewObserver,
// Duration since `recording_timer_` started.
std::u16string recording_duration_;
// Indicates whether the Game Dashboard welcome dialog should be shown. This
// param ensures the welcome dialog is only shown once per game window
// startup.
bool show_welcome_dialog_ = false;
base::WeakPtrFactory<GameDashboardContext> weak_ptr_factory_{this};
};

@ -137,6 +137,10 @@ AnchoredNudge* GameDashboardContextTestApi::GetGameControlsSetupNudge() {
return nullptr;
}
views::Widget* GameDashboardContextTestApi::GetWelcomeDialogWidget() {
return context_->welcome_dialog_widget_.get();
}
void GameDashboardContextTestApi::OpenTheMainMenu() {
ASSERT_FALSE(GetMainMenuView()) << "The main menu view is already open.";
ASSERT_FALSE(GetMainMenuWidget()) << "The main menu widget is already open.";

@ -75,6 +75,9 @@ class GameDashboardContextTestApi {
// Returns the Game Controls setup nudge.
AnchoredNudge* GetGameControlsSetupNudge();
// Returns the Game Dashboard welcome dialog widget.
views::Widget* GetWelcomeDialogWidget();
// Opens the main menu.
// Before opening the main menu, verifies that the main menu is closed.
// After opening the main menu, verifies it opened and waits for the thread to

@ -12,6 +12,7 @@
#include "ash/capture_mode/capture_mode_types.h"
#include "ash/constants/ash_features.h"
#include "ash/game_dashboard/game_dashboard_button.h"
#include "ash/game_dashboard/game_dashboard_constants.h"
#include "ash/game_dashboard/game_dashboard_context_test_api.h"
#include "ash/game_dashboard/game_dashboard_controller.h"
#include "ash/game_dashboard/game_dashboard_main_menu_view.h"
@ -51,10 +52,6 @@ namespace ash {
using ToolbarSnapLocation = GameDashboardContext::ToolbarSnapLocation;
// Toolbar padding copied from `GameDashboardContext`.
static const int kToolbarEdgePadding = 10;
static constexpr gfx::Rect kAppBounds = gfx::Rect(50, 50, 800, 400);
// Sub-label strings.
const std::u16string& hidden_label = u"Hidden";
const std::u16string& visible_label = u"Visible";
@ -69,9 +66,22 @@ class GameDashboardContextTest : public GameDashboardTestBase {
~GameDashboardContextTest() override = default;
void TearDown() override {
CloseGameWindow();
GameDashboardTestBase::TearDown();
}
void CloseGameWindow() {
game_window_.reset();
test_api_.reset();
GameDashboardTestBase::TearDown();
}
const gfx::Rect app_bounds() const { return app_bounds_; }
void SetAppBounds(gfx::Rect app_bounds) {
CHECK(!game_window_)
<< "App bounds cannot be changed after creating window. To set the app "
"bounds, call CloseWindow() and re-call this function.";
app_bounds_ = app_bounds;
}
int GetToolbarHeight() {
@ -94,17 +104,23 @@ class GameDashboardContextTest : public GameDashboardTestBase {
// game window. Otherwise, it creates the window as a GeForceNow window.
// For ARC game windows, if `set_arc_game_controls_flags_prop` is true, then
// the `kArcGameControlsFlagsKey` window property will be set to
// `ArcGameControlsFlag::kKnown`, otherwise the property will not be set.
// `ArcGameControlsFlag::kKnown`, otherwise the property will not be set. If
// `show_welcome_dialog` is true, the welcome dialog displays when the Game
// Window first opens.
void CreateGameWindow(bool is_arc_window,
bool set_arc_game_controls_flags_prop = true) {
bool set_arc_game_controls_flags_prop = true,
bool show_welcome_dialog = false) {
ASSERT_FALSE(game_window_);
ASSERT_FALSE(test_api_);
game_window_ = CreateAppWindow(
(is_arc_window ? TestGameDashboardDelegate::kGameAppId
: extension_misc::kGeForceNowAppId),
(is_arc_window ? AppType::ARC_APP : AppType::NON_APP), kAppBounds);
(is_arc_window ? AppType::ARC_APP : AppType::NON_APP), app_bounds());
auto* context = GameDashboardController::Get()->GetGameDashboardContext(
game_window_.get());
// TODO(b/316141148): Update test logic to set `show_welcome_dialog_` to
// false via a property instead of directly.
context->SetShowWelcomeDialogForTesting(show_welcome_dialog);
ASSERT_TRUE(context);
test_api_ = std::make_unique<GameDashboardContextTestApi>(
context, GetEventGenerator());
@ -436,6 +452,9 @@ class GameDashboardContextTest : public GameDashboardTestBase {
// completes before proceeding.
base::RunLoop().RunUntilIdle();
}
private:
gfx::Rect app_bounds_ = gfx::Rect(50, 50, 800, 400);
};
// Verifies Game Controls tile state.
@ -796,6 +815,64 @@ TEST_F(GameDashboardContextTest, RecordingTimerStringFormat) {
EXPECT_EQ(u"24:00:30", test_api_->GetRecordingDuration());
}
// Verifies the welcome dialog displays when the game window first opens and
// disappears after 4 seconds.
TEST_F(GameDashboardContextTest, WelcomeDialogAutoDismisses) {
// Open the game window with the welcome dialog enabled.
CreateGameWindow(/*is_arc_window=*/true,
/*set_arc_game_controls_flags_prop=*/true,
/*show_welcome_dialog=*/true);
// Verify the welcome dialog is initially shown and is right aligned in the
// app window.
ASSERT_TRUE(test_api_->GetWelcomeDialogWidget());
gfx::Rect welcome_dialog_bounds =
test_api_->GetWelcomeDialogWidget()->GetWindowBoundsInScreen();
EXPECT_EQ(welcome_dialog_bounds.x(),
(game_window_->GetBoundsInScreen().right() -
game_dashboard::kWelcomeDialogEdgePadding -
game_dashboard::kWelcomeDialogFixedWidth));
// Dismiss welcome dialog after 4 seconds and verify the dialog is no longer
// visible.
task_environment()->FastForwardBy(base::Seconds(4));
EXPECT_FALSE(test_api_->GetWelcomeDialogWidget());
}
// Verifies the welcome dialog disappears when the main menu view is opened.
TEST_F(GameDashboardContextTest, WelcomeDialogDismissOnMainMenuOpening) {
// Open the game window with the welcome dialog enabled.
// CloseGameWindow();
CreateGameWindow(/*is_arc_window=*/true,
/*set_arc_game_controls_flags_prop=*/true,
/*show_welcome_dialog=*/true);
ASSERT_TRUE(test_api_->GetWelcomeDialogWidget());
// Open the main menu and verify the welcome dialog dismisses.
test_api_->OpenTheMainMenu();
EXPECT_FALSE(test_api_->GetWelcomeDialogWidget());
}
// Verifies the welcome dialog is centered when the app window width is small
// enough.
TEST_F(GameDashboardContextTest, WelcomeDialogWithSmallWindow) {
// Open a new game window with a width of 450.
SetAppBounds(gfx::Rect(50, 50, 450, 400));
CreateGameWindow(/*is_arc_window=*/true,
/*set_arc_game_controls_flags_prop=*/true,
/*show_welcome_dialog=*/true);
ASSERT_TRUE(test_api_->GetWelcomeDialogWidget());
// Verify the welcome dialog is centered.
gfx::Rect welcome_dialog_bounds =
test_api_->GetWelcomeDialogWidget()->GetWindowBoundsInScreen();
EXPECT_EQ(welcome_dialog_bounds.x(),
(game_window_->GetBoundsInScreen().x() +
(game_window_->GetBoundsInScreen().width() -
game_dashboard::kWelcomeDialogFixedWidth) /
2));
}
// -----------------------------------------------------------------------------
// GameTypeGameDashboardContextTest:
// Test fixture to test both ARC and GeForceNow game window depending on the
@ -825,7 +902,7 @@ TEST_P(GameTypeGameDashboardContextTest,
GameDashboardButtonWidget_InitialLocation) {
const gfx::Point expected_button_center_point(
game_window_->GetBoundsInScreen().top_center().x(),
kAppBounds.y() + frame_header_->GetHeaderHeight() / 2);
app_bounds().y() + frame_header_->GetHeaderHeight() / 2);
EXPECT_EQ(expected_button_center_point,
test_api_->GetGameDashboardButtonWidget()
->GetNativeWindow()
@ -853,8 +930,7 @@ TEST_P(GameTypeGameDashboardContextTest,
TEST_P(GameTypeGameDashboardContextTest, OpenGameDashboardButtonWidget) {
// Close the window and create a new game window without setting the
// `kArcGameControlsFlagsKey` property.
game_window_.reset();
test_api_.reset();
CloseGameWindow();
CreateGameWindow(IsArcGame(), /*set_arc_game_controls_flags_prop=*/false);
// Verifies the main menu is closed.
@ -913,8 +989,9 @@ TEST_P(GameTypeGameDashboardContextTest, CloseMainMenuOutsideButtonWidget) {
// Close the main menu dialog by clicking outside the main menu view bounds.
auto* event_generator = GetEventGenerator();
const gfx::Point& new_location = {kAppBounds.x() + kAppBounds.width(),
kAppBounds.y() + kAppBounds.height()};
gfx::Rect game_bounds = app_bounds();
const gfx::Point& new_location = {game_bounds.x() + game_bounds.width(),
game_bounds.y() + game_bounds.height()};
event_generator->set_current_screen_location(new_location);
event_generator->ClickLeftButton();
@ -1185,10 +1262,11 @@ TEST_P(GameTypeGameDashboardContextTest, MoveToolbarOutOfBounds) {
const int screen_point_bottom = screen_point_y + kScreenBounds.height();
// Verify the screen bounds are larger than the game bounds.
ASSERT_LT(screen_point_x, kAppBounds.x());
ASSERT_LT(screen_point_y, kAppBounds.y());
ASSERT_GT(screen_point_right, kAppBounds.x() + kAppBounds.width());
ASSERT_GT(screen_point_bottom, kAppBounds.y() + kAppBounds.height());
auto game_bounds = app_bounds();
ASSERT_LT(screen_point_x, game_bounds.x());
ASSERT_LT(screen_point_y, game_bounds.y());
ASSERT_GT(screen_point_right, game_bounds.x() + game_bounds.width());
ASSERT_GT(screen_point_bottom, game_bounds.y() + game_bounds.height());
// Drag toolbar, moving the mouse past the game window to the top right corner
// of the screen bounds, and verify the toolbar doesn't go past the game
@ -1296,6 +1374,7 @@ TEST_P(GameTypeGameDashboardContextTest, VerifyToolbarPlacementInQuadrants) {
const int y_offset = window_bounds.height() / 4;
// Verify initial placement in top right quadrant.
auto game_bounds = app_bounds();
const auto* native_window = test_api_->GetToolbarWidget()->GetNativeWindow();
auto toolbar_bounds = native_window->GetBoundsInScreen();
const auto toolbar_size =
@ -1303,36 +1382,44 @@ TEST_P(GameTypeGameDashboardContextTest, VerifyToolbarPlacementInQuadrants) {
const int frame_header_height = frame_header_->GetHeaderHeight();
EXPECT_EQ(test_api_->GetToolbarSnapLocation(),
ToolbarSnapLocation::kTopRight);
EXPECT_EQ(toolbar_bounds.x(),
kAppBounds.right() - kToolbarEdgePadding - toolbar_size.width());
EXPECT_EQ(toolbar_bounds.y(),
kAppBounds.y() + kToolbarEdgePadding + frame_header_height);
EXPECT_EQ(toolbar_bounds.x(), game_bounds.right() -
game_dashboard::kToolbarEdgePadding -
toolbar_size.width());
EXPECT_EQ(toolbar_bounds.y(), game_bounds.y() +
game_dashboard::kToolbarEdgePadding +
frame_header_height);
// Move toolbar to top left quadrant and verify toolbar placement.
DragToolbarToPoint(Movement::kMouse, {window_center_point.x() - x_offset,
window_center_point.y() - y_offset});
EXPECT_EQ(test_api_->GetToolbarSnapLocation(), ToolbarSnapLocation::kTopLeft);
toolbar_bounds = native_window->GetBoundsInScreen();
EXPECT_EQ(toolbar_bounds.x(), kAppBounds.x() + kToolbarEdgePadding);
EXPECT_EQ(toolbar_bounds.y(),
kAppBounds.y() + kToolbarEdgePadding + frame_header_height);
EXPECT_EQ(toolbar_bounds.x(),
game_bounds.x() + game_dashboard::kToolbarEdgePadding);
EXPECT_EQ(toolbar_bounds.y(), game_bounds.y() +
game_dashboard::kToolbarEdgePadding +
frame_header_height);
// Move toolbar to bottom right quadrant and verify toolbar placement.
DragToolbarToPoint(Movement::kMouse, {window_center_point.x() + x_offset,
window_center_point.y() + y_offset});
toolbar_bounds = native_window->GetBoundsInScreen();
EXPECT_EQ(toolbar_bounds.x(),
kAppBounds.right() - kToolbarEdgePadding - toolbar_size.width());
EXPECT_EQ(toolbar_bounds.y(),
kAppBounds.bottom() - kToolbarEdgePadding - toolbar_size.height());
EXPECT_EQ(toolbar_bounds.x(), game_bounds.right() -
game_dashboard::kToolbarEdgePadding -
toolbar_size.width());
EXPECT_EQ(toolbar_bounds.y(), game_bounds.bottom() -
game_dashboard::kToolbarEdgePadding -
toolbar_size.height());
// Move toolbar to bottom left quadrant and verify toolbar placement.
DragToolbarToPoint(Movement::kMouse, {window_center_point.x() - x_offset,
window_center_point.y() + y_offset});
toolbar_bounds = native_window->GetBoundsInScreen();
EXPECT_EQ(toolbar_bounds.x(), kAppBounds.x() + kToolbarEdgePadding);
EXPECT_EQ(toolbar_bounds.y(),
kAppBounds.bottom() - kToolbarEdgePadding - toolbar_size.height());
EXPECT_EQ(toolbar_bounds.x(),
game_bounds.x() + game_dashboard::kToolbarEdgePadding);
EXPECT_EQ(toolbar_bounds.y(), game_bounds.bottom() -
game_dashboard::kToolbarEdgePadding -
toolbar_size.height());
}
// Verifies the toolbar's snap location is preserved even after the visibility

@ -0,0 +1,179 @@
// 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_welcome_dialog.h"
#include "ash/bubble/bubble_utils.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/typography.h"
#include "chromeos/ui/vector_icons/vector_icons.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/chromeos/styles/cros_tokens_color_mappings.h"
#include "ui/events/ash/keyboard_capability.h"
#include "ui/views/background.h"
#include "ui/views/border.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/label.h"
namespace ash {
namespace {
// Corner radius of the welcome dialog.
constexpr float kDialogCornerRadius = 24.0f;
// Fixed width of the welcome dialog.
static constexpr int kDialogWidth = 360;
// Radius of the icon and its background displayed in the dialog.
constexpr float kIconBackgroundRadius = 40.0f;
// The height and width of the dialog's icon.
constexpr int kIconSize = 20;
// Additional padding for the top, left, and right title container border.
constexpr int kPrimaryContainerBorder = 12;
// Border padding surrounding the inside of the entire welcome dialog.
constexpr int kPrimaryLayoutInsideBorder = 8;
// Padding between the `primary_container` and `shortcut_hint` rows.
constexpr int kRowPadding = 20;
// Radius of the container of the shortcut text.
constexpr float kShortcutCornerRadius = 16.0f;
// Padding surrounding the shortcut info text.
constexpr int kShortcutTextBorder = 16;
// Padding between the `title_container` and `icon_container`.
constexpr int kTitleContainerPadding = 20;
// Width of the container containing the text title and sub-label.
constexpr int kTitleTextMaxWidth =
kDialogWidth - kIconBackgroundRadius - kTitleContainerPadding -
/*left and right dialog insets*/ 2 * kPrimaryLayoutInsideBorder -
/*additional `primary_container` left and right padding*/
2 * kPrimaryContainerBorder;
// Maximum duration that the dialog should be displayed.
constexpr base::TimeDelta kDialogDuration = base::Seconds(4);
} // namespace
GameDashboardWelcomeDialog::GameDashboardWelcomeDialog() {
SetOrientation(views::LayoutOrientation::kVertical);
SetIgnoreDefaultMainAxisMargins(true);
SetDefault(views::kMarginsKey, gfx::Insets::TLBR(kRowPadding, 0, 0, 0));
SetInteriorMargin(
gfx::Insets::VH(kPrimaryLayoutInsideBorder, kPrimaryLayoutInsideBorder));
SetBackground(views::CreateThemedRoundedRectBackground(
cros_tokens::kCrosSysSystemBaseElevatedOpaque, kDialogCornerRadius));
AddTitleAndIconRow();
AddShortcutInfoRow();
}
GameDashboardWelcomeDialog::~GameDashboardWelcomeDialog() = default;
void GameDashboardWelcomeDialog::StartTimer(base::OnceClosure on_complete) {
DCHECK(on_complete) << "OnceClosure must be passed to determine what to do "
"when the timer completes.";
timer_.Start(FROM_HERE, kDialogDuration, std::move(on_complete));
}
// Creates a primary container that holds separate sub-containers for the text
// and icon.
// Note: When using `views::FlexLayoutView` it's common to wrap objects in
// additional containers that need a separate alignment than the rest of the
// elements. This creates the following:
//
// +----------------------------------------------------+
// | primary_container |
// | +--------------------------+-------------------+ |
// | | title_container | icon_container | |
// | | +--------------------+ | +--------+ | |
// | | | title | | | | | |
// | | +--------------------+ | | icon | | |
// | | | sub_label | | | | | |
// | | +--------------------+ | +--------+ | |
// | +--------------------------+-------------------+ |
// +----------------------------------------------------+
void GameDashboardWelcomeDialog::AddTitleAndIconRow() {
auto* primary_container =
AddChildView(std::make_unique<views::FlexLayoutView>());
primary_container->SetIgnoreDefaultMainAxisMargins(true);
primary_container->SetDefault(views::kMarginsKey, gfx::Insets::VH(0, 0));
primary_container->SetInteriorMargin(
gfx::Insets::TLBR(kPrimaryContainerBorder, kPrimaryContainerBorder, 0,
kPrimaryContainerBorder));
primary_container->SetOrientation(views::LayoutOrientation::kHorizontal);
// Create title container as a child of the primary container.
auto* title_container = primary_container->AddChildView(
std::make_unique<views::FlexLayoutView>());
title_container->SetOrientation(views::LayoutOrientation::kVertical);
title_container->SetCrossAxisAlignment(views::LayoutAlignment::kStart);
title_container->SetInteriorMargin(
gfx::Insets::TLBR(0, 0, 0, kTitleContainerPadding));
// Add title label to the title container.
auto* title = title_container->AddChildView(bubble_utils::CreateLabel(
TypographyToken::kCrosButton1,
l10n_util::GetStringUTF16(
IDS_ASH_GAME_DASHBOARD_GAME_DASHBOARD_BUTTON_TITLE),
cros_tokens::kCrosSysOnSurface));
title->SetMultiLine(true);
title->SizeToFit(kTitleTextMaxWidth);
title->SetHorizontalAlignment(gfx::ALIGN_LEFT);
// Add sub-label to the title container.
auto* sub_label = title_container->AddChildView(bubble_utils::CreateLabel(
TypographyToken::kCrosAnnotation2,
l10n_util::GetStringUTF16(
IDS_ASH_GAME_DASHBOARD_WELCOME_DIALOG_SUB_LABEL),
cros_tokens::kCrosSysOnSurfaceVariant));
// TODO(b/316138331): Investigate why multi-line support isn't working
// properly.
sub_label->SetMultiLine(true);
sub_label->SizeToFit(kTitleTextMaxWidth);
sub_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
// Create icon container as a child of the primary container.
auto* icon_container = primary_container->AddChildView(
std::make_unique<views::FlexLayoutView>());
icon_container->SetCrossAxisAlignment(views::LayoutAlignment::kEnd);
// Add icon to the icon container.
auto* icon = icon_container->AddChildView(
std::make_unique<views::ImageView>(ui::ImageModel::FromVectorIcon(
chromeos::kGameDashboardGamepadIcon, cros_tokens::kCrosSysOnPrimary,
kIconSize)));
icon->SetPreferredSize(
gfx::Size(kIconBackgroundRadius, kIconBackgroundRadius));
icon->SetBackground(views::CreateThemedRoundedRectBackground(
cros_tokens::kCrosSysPrimary, kIconBackgroundRadius));
}
// Creates a stylized label that holds the hint indicating how open the Game
// Dashboard shortcut. This creates the following:
//
// +----------------------------------------------------+
// | shortcut_hint |
// +----------------------------------------------------+
void GameDashboardWelcomeDialog::AddShortcutInfoRow() {
const std::u16string shortcut_key = l10n_util::GetStringUTF16(
Shell::Get()->keyboard_capability()->HasLauncherButtonOnAnyKeyboard()
? IDS_ASH_SHORTCUT_MODIFIER_LAUNCHER
: IDS_ASH_SHORTCUT_MODIFIER_SEARCH);
auto* shortcut_hint = AddChildView(bubble_utils::CreateLabel(
TypographyToken::kCrosButton2,
l10n_util::GetStringFUTF16(IDS_ASH_GAME_DASHBOARD_WELCOME_DIALOG_SHORTCUT,
shortcut_key),
cros_tokens::kCrosSysPrimary));
shortcut_hint->SetMultiLine(true);
// TODO(b/316138331): Update max width to ensure it matches specs.
shortcut_hint->SizeToFit(kTitleTextMaxWidth);
shortcut_hint->SetHorizontalAlignment(gfx::ALIGN_LEFT);
shortcut_hint->SetBackground(views::CreateThemedRoundedRectBackground(
cros_tokens::kCrosSysSystemOnBase, kShortcutCornerRadius));
shortcut_hint->SetBorder(views::CreateEmptyBorder(
gfx::Insets::VH(kShortcutTextBorder, kShortcutTextBorder)));
}
BEGIN_METADATA(GameDashboardWelcomeDialog, views::FlexLayoutView)
END_METADATA
} // namespace ash

@ -0,0 +1,44 @@
// 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.
#ifndef ASH_GAME_DASHBOARD_GAME_DASHBOARD_WELCOME_DIALOG_H_
#define ASH_GAME_DASHBOARD_GAME_DASHBOARD_WELCOME_DIALOG_H_
#include "ash/ash_export.h"
#include "base/timer/timer.h"
#include "ui/views/layout/flex_layout_view.h"
namespace ash {
// `GameDashboardWelcomeDialog` is a View displayed for a set duration of time
// when first opening any game. It can be disabled via the Game Dashboard
// Settings.
class ASH_EXPORT GameDashboardWelcomeDialog : public views::FlexLayoutView {
public:
METADATA_HEADER(GameDashboardWelcomeDialog);
GameDashboardWelcomeDialog();
GameDashboardWelcomeDialog(const GameDashboardWelcomeDialog&) = delete;
GameDashboardWelcomeDialog& operator=(const GameDashboardWelcomeDialog) =
delete;
~GameDashboardWelcomeDialog() override;
// Starts the `timer_`, which will run the given `on_complete` once the time
// specified by `kDialogDuration` has elapsed.
void StartTimer(base::OnceClosure on_complete);
private:
// Adds a stacked title/sub-label and an icon as a row to the welcome dialog.
void AddTitleAndIconRow();
// Adds a row displaying how to open the dashboard.
void AddShortcutInfoRow();
// Timer for how long to show the welcome dialog.
base::OneShotTimer timer_;
};
} // namespace ash
#endif // ASH_GAME_DASHBOARD_GAME_DASHBOARD_WELCOME_DIALOG_H_