#include "ash/wm/float/float_controller.h"
#include <algorithm>
#include <cstddef>
#include <vector>
#include "ash/constants/ash_features.h"
#include "ash/display/screen_orientation_controller.h"
#include "ash/public/cpp/shell_window_ids.h"
#include "ash/public/cpp/window_properties.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/root_window_controller.h"
#include "ash/rotator/screen_rotation_animator.h"
#include "ash/screen_util.h"
#include "ash/shell.h"
#include "ash/style/dark_light_mode_controller_impl.h"
#include "ash/wm/desks/desk.h"
#include "ash/wm/desks/desks_util.h"
#include "ash/wm/float/tablet_mode_tuck_education.h"
#include "ash/wm/mru_window_tracker.h"
#include "ash/wm/scoped_window_tucker.h"
#include "ash/wm/screen_pinning_controller.h"
#include "ash/wm/tablet_mode/tablet_mode_window_state.h"
#include "ash/wm/window_state.h"
#include "ash/wm/window_util.h"
#include "ash/wm/wm_default_layout_manager.h"
#include "ash/wm/wm_event.h"
#include "ash/wm/workspace/workspace_event_handler.h"
#include "ash/wm/workspace/workspace_layout_manager.h"
#include "base/auto_reset.h"
#include "base/check.h"
#include "base/check_op.h"
#include "base/memory/raw_ptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "chromeos/ui/base/app_types.h"
#include "chromeos/ui/base/window_properties.h"
#include "chromeos/ui/wm/constants.h"
#include "chromeos/ui/wm/window_util.h"
#include "components/app_restore/window_properties.h"
#include "ui/aura/client/aura_constants.h"
#include "ui/aura/window.h"
#include "ui/aura/window_delegate.h"
#include "ui/aura/window_observer.h"
#include "ui/display/screen.h"
#include "ui/display/tablet_state.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/wm/core/coordinate_conversion.h"
#include "ui/wm/core/scoped_animation_disabler.h"
namespace ash {
namespace {
constexpr float kFloatedWindowClamshellWidthRatio = 1.f / 3.f;
constexpr float kFloatedWindowClamshellHeightRatio = 0.7f;
constexpr char kFloatWindowCountsPerSessionHistogramName[] =
"Ash.Float.FloatWindowCountsPerSession";
constexpr char kFloatWindowDurationHistogramName[] =
"Ash.Float.FloatWindowDuration";
constexpr char kFloatWindowMoveToAnotherDeskCountsHistogramName[] =
"Ash.Float.FloatWindowMoveToAnotherDeskCounts";
bool DisableAndGetOriginalPositionAutoManaged(aura::Window* window) {
auto* window_state = WindowState::Get(window);
const bool original_position_auto_managed =
window_state->GetWindowPositionManaged();
if (original_position_auto_managed)
window_state->SetWindowPositionManaged(false);
return original_position_auto_managed;
}
void UpdateWindowBoundsForTablet(
aura::Window* window,
WindowState::BoundsChangeAnimationType animation_type) {
WindowState* window_state = WindowState::Get(window);
DCHECK(window_state);
if (window_state->is_client_controlled()) {
if (animation_type != WindowState::BoundsChangeAnimationType::kNone) {
TabletModeWindowState::UpdateWindowPosition(window_state, animation_type);
}
const SetBoundsWMEvent event(
TabletModeWindowState::GetBoundsInTabletMode(window_state),
animation_type !=
WindowState::BoundsChangeAnimationType::kNone);
window_state->OnWMEvent(&event);
return;
}
TabletModeWindowState::UpdateWindowPosition(window_state, animation_type);
}
void HideFloatedWindow(aura::Window* floated_window) {
DCHECK(floated_window);
wm::ScopedAnimationDisabler disabler(floated_window);
floated_window->Hide();
}
void ShowFloatedWindow(aura::Window* floated_window) {
DCHECK(floated_window);
if (floated_window->IsVisible()) {
return;
}
wm::ScopedAnimationDisabler disabler(floated_window);
floated_window->Show();
}
gfx::Rect GetFloatBounds(const gfx::Size& size,
const gfx::Rect& work_area_bounds,
chromeos::FloatStartLocation location) {
const int padding_dp = chromeos::wm::kFloatedWindowPaddingDp;
int origin_x;
const int origin_y = work_area_bounds.bottom() - size.height() - padding_dp;
switch (location) {
case chromeos::FloatStartLocation::kBottomLeft: {
origin_x = padding_dp;
break;
}
case chromeos::FloatStartLocation::kBottomRight: {
origin_x = work_area_bounds.right() - size.width() - padding_dp;
break;
}
}
return gfx::Rect(gfx::Point(origin_x, origin_y), size);
}
class FloatLayoutManager : public WmDefaultLayoutManager {
public:
FloatLayoutManager() = default;
FloatLayoutManager(const FloatLayoutManager&) = delete;
FloatLayoutManager& operator=(const FloatLayoutManager&) = delete;
~FloatLayoutManager() override = default;
void OnWindowAddedToLayout(aura::Window* child) override {
if (display::Screen::Get()->InTabletMode()) {
return;
}
WindowState* window_state = WindowState::Get(child);
WMEvent event(WM_EVENT_ADDED_TO_WORKSPACE);
window_state->OnWMEvent(&event);
}
void OnWillRemoveWindowFromLayout(aura::Window* child) override {
WindowState::Get(child)->set_pre_added_to_workspace_window_bounds(
child->bounds());
}
void SetChildBounds(aura::Window* child,
const gfx::Rect& requested_bounds) override {
WindowState* window_state = WindowState::Get(child);
SetBoundsWMEvent event(requested_bounds);
window_state->OnWMEvent(&event);
}
};
class FloatScopedWindowTuckerDelegate : public ScopedWindowTucker::Delegate {
public:
FloatScopedWindowTuckerDelegate() = default;
FloatScopedWindowTuckerDelegate(const FloatScopedWindowTuckerDelegate&) =
delete;
FloatScopedWindowTuckerDelegate& operator=(
const FloatScopedWindowTuckerDelegate&) = delete;
~FloatScopedWindowTuckerDelegate() override = default;
void PaintTuckHandle(gfx::Canvas* canvas, int width, bool left) override {
if (left) {
canvas->Translate(gfx::Vector2d(width, 0));
canvas->Scale(-1, 1);
}
const bool dark_mode =
DarkLightModeControllerImpl::Get()->IsDarkModeEnabled();
SkColor color = dark_mode ? gfx::kGoogleGrey500 : gfx::kGoogleGrey600;
const SkColor bottom_color =
SkColorSetA(color, std::round(SkColorGetA(color) * 0.8f));
const gfx::ImageSkia& tuck_container_bottom = gfx::CreateVectorIcon(
kTuckHandleContainerBottomIcon, ScopedWindowTucker::kTuckHandleWidth,
bottom_color);
canvas->DrawImageInt(tuck_container_bottom, 0, 0);
color = dark_mode ? gfx::kGoogleGrey200 : gfx::kGoogleGrey600;
const SkColor top_color =
SkColorSetA(color, std::round(SkColorGetA(color) * 0.12f));
const gfx::ImageSkia& tuck_container_top =
gfx::CreateVectorIcon(kTuckHandleContainerTopIcon,
ScopedWindowTucker::kTuckHandleWidth, top_color);
canvas->DrawImageInt(tuck_container_top, 0, 0);
const gfx::ImageSkia& tuck_icon = gfx::CreateVectorIcon(
kTuckHandleChevronIcon, ScopedWindowTucker::kTuckHandleWidth,
SK_ColorWHITE);
canvas->DrawImageInt(tuck_icon, 0, 0);
}
int ParentContainerId() const override {
return kShellWindowId_FloatContainer;
}
void UpdateWindowPosition(aura::Window* window, bool left) override {
TabletModeWindowState::UpdateWindowPosition(
WindowState::Get(window),
WindowState::BoundsChangeAnimationType::kNone);
}
void UntuckWindow(aura::Window* window) override {
Shell::Get()->float_controller()->MaybeUntuckFloatedWindowForTablet(window);
}
void OnAnimateTuckEnded(aura::Window* window) override {
wm::ScopedAnimationDisabler disable(window);
window->Hide();
}
gfx::Rect GetTuckHandleBounds(bool left,
const gfx::Rect& window_bounds) const override {
const gfx::Point tuck_handle_origin =
left ? window_bounds.right_center() -
gfx::Vector2d(0, ScopedWindowTucker::kTuckHandleHeight / 2)
: window_bounds.left_center() -
gfx::Vector2d(ScopedWindowTucker::kTuckHandleWidth,
ScopedWindowTucker::kTuckHandleHeight / 2);
return gfx::Rect(tuck_handle_origin,
gfx::Size(ScopedWindowTucker::kTuckHandleWidth,
ScopedWindowTucker::kTuckHandleHeight));
}
};
}
class FloatController::FloatedWindowInfo : public aura::WindowObserver,
public views::WidgetObserver {
public:
FloatedWindowInfo(aura::Window* floated_window, const Desk* desk)
: floated_window_(floated_window),
was_position_auto_managed_(
DisableAndGetOriginalPositionAutoManaged(floated_window)),
desk_(desk) {
DCHECK(floated_window_);
floated_window_observation_.Observe(floated_window);
if (auto* widget =
views::Widget::GetWidgetForNativeWindow(floated_window)) {
floated_widget_observation_.Observe(widget);
last_minimum_size_ = widget->GetMinimumSize();
last_maximum_size_ = widget->GetMaximumSize();
}
if (desk->is_active()) {
float_start_time_ = base::TimeTicks::Now();
}
if (display::Screen::Get()->InTabletMode() &&
TabletModeTuckEducation::CanActivateTuckEducation() &&
!Shell::Get()
->float_controller()
->disable_tuck_education_for_testing_) {
tuck_education_ =
std::make_unique<TabletModeTuckEducation>(floated_window);
}
}
FloatedWindowInfo(const FloatedWindowInfo&) = delete;
FloatedWindowInfo& operator=(const FloatedWindowInfo&) = delete;
~FloatedWindowInfo() override {
if (was_position_auto_managed_) {
WindowState::Get(floated_window_)->SetWindowPositionManaged(true);
}
MaybeRecordFloatWindowDuration();
}
const Desk* desk() const { return desk_; }
void set_desk(const Desk* desk) { desk_ = desk; }
bool is_tucked_for_tablet() const { return is_tucked_for_tablet_; }
MagnetismCorner magnetism_corner() const { return magnetism_corner_; }
void set_magnetism_corner(MagnetismCorner magnetism_corner) {
magnetism_corner_ = magnetism_corner;
}
void MaybeRecordFloatWindowDuration() {
if (!float_start_time_.is_null()) {
base::UmaHistogramCustomCounts(
kFloatWindowDurationHistogramName,
(base::TimeTicks::Now() - float_start_time_).InMinutes(), 1,
base::Days(7).InMinutes(), 50);
float_start_time_ = base::TimeTicks();
}
}
void MaybeTuckWindow(bool left) {
is_tucked_for_tablet_ = true;
scoped_window_tucker_ = std::make_unique<ScopedWindowTucker>(
std::make_unique<FloatScopedWindowTuckerDelegate>(), floated_window_,
left);
scoped_window_tucker_->AnimateTuck();
TabletModeTuckEducation::OnWindowTucked();
}
void OnUntuckAnimationEnded() {
scoped_window_tucker_.reset();
UpdateWindowBoundsForTablet(floated_window_,
WindowState::BoundsChangeAnimationType::kNone);
}
void MaybeUntuckWindow(bool animate) {
is_tucked_for_tablet_ = false;
if (!animate) {
scoped_window_tucker_.reset();
UpdateWindowBoundsForTablet(
floated_window_, WindowState::BoundsChangeAnimationType::kNone);
return;
}
if (scoped_window_tucker_) {
scoped_window_tucker_->AnimateUntuck(
base::BindOnce(&FloatedWindowInfo::OnUntuckAnimationEnded,
weak_ptr_factory_.GetWeakPtr()));
}
}
views::Widget* GetTuckHandleWidget() {
DCHECK(scoped_window_tucker_);
return scoped_window_tucker_->tuck_handle_widget();
}
void OnWindowDestroying(aura::Window* window) override {
DCHECK_EQ(floated_window_, window);
DCHECK(
floated_window_observation_.IsObservingSource(floated_window_.get()));
Shell::Get()->float_controller()->OnFloatedWindowDestroying(window);
}
void OnWindowVisibilityChanged(aura::Window* window, bool visible) override {
if (window != floated_window_) {
return;
}
if (visible && desk_->is_active()) {
if (float_start_time_.is_null()) {
float_start_time_ = base::TimeTicks::Now();
}
return;
}
if (!visible && !desk_->is_active()) {
MaybeRecordFloatWindowDuration();
}
}
void OnWindowPropertyChanged(aura::Window* window,
const void* key,
intptr_t old) override {
CHECK_EQ(floated_window_, window);
if (key == aura::client::kWindowWorkspaceKey &&
desks_util::IsZOrderTracked(window)) {
auto* desks_controller = Shell::Get()->desks_controller();
if (desks_util::IsWindowVisibleOnAllWorkspaces(window)) {
desks_controller->AddVisibleOnAllDesksWindow(window);
} else {
desks_controller->MaybeRemoveVisibleOnAllDesksWindow(window);
}
return;
}
if (key == aura::client::kZOrderingKey) {
if (window->GetProperty(aura::client::kZOrderingKey) !=
ui::ZOrderLevel::kNormal) {
Shell::Get()->float_controller()->ResetFloatedWindow(floated_window_);
}
return;
}
if (key == aura::client::kResizeBehaviorKey &&
static_cast<int>(old) !=
window->GetProperty(aura::client::kResizeBehaviorKey)) {
OnResizabilityOrSizeConstraintsChanged();
}
}
void OnWidgetSizeConstraintsChanged(views::Widget* widget) override {
CHECK_EQ(views::Widget::GetWidgetForNativeWindow(floated_window_), widget);
if (last_minimum_size_ != widget->GetMinimumSize() ||
last_maximum_size_ != widget->GetMaximumSize()) {
OnResizabilityOrSizeConstraintsChanged();
last_minimum_size_ = widget->GetMinimumSize();
last_maximum_size_ = widget->GetMaximumSize();
}
}
void OnWidgetDestroyed(views::Widget* widget) override {
floated_widget_observation_.Reset();
}
private:
void OnResizabilityOrSizeConstraintsChanged() {
if (SplitViewController::Get(floated_window_)
->IsWindowInTransitionalState(floated_window_)) {
return;
}
if (!chromeos::wm::CanFloatWindow(floated_window_)) {
Shell::Get()->float_controller()->ResetFloatedWindow(floated_window_);
return;
}
if (display::Screen::Get()->InTabletMode()) {
if (in_bounds_update_) {
return;
}
base::AutoReset<bool> resetter(&in_bounds_update_, true);
UpdateWindowBoundsForTablet(
floated_window_, WindowState::BoundsChangeAnimationType::kNone);
}
}
raw_ptr<aura::Window> floated_window_;
const bool was_position_auto_managed_;
std::unique_ptr<ScopedWindowTucker> scoped_window_tucker_;
std::unique_ptr<TabletModeTuckEducation> tuck_education_;
bool is_tucked_for_tablet_ = false;
raw_ptr<const Desk, DanglingUntriaged> desk_;
base::TimeTicks float_start_time_;
MagnetismCorner magnetism_corner_ = MagnetismCorner::kBottomRight;
base::ScopedObservation<aura::Window, aura::WindowObserver>
floated_window_observation_{this};
base::ScopedObservation<views::Widget, views::WidgetObserver>
floated_widget_observation_{this};
gfx::Size last_minimum_size_;
gfx::Size last_maximum_size_;
bool in_bounds_update_ = false;
base::WeakPtrFactory<FloatedWindowInfo> weak_ptr_factory_{this};
};
FloatController::FloatController() {
shell_observation_.Observe(Shell::Get());
for (aura::Window* root : Shell::GetAllRootWindows())
OnRootWindowAdded(root);
}
FloatController::~FloatController() {
base::UmaHistogramCounts100(kFloatWindowCountsPerSessionHistogramName,
floated_window_counter_);
base::UmaHistogramCounts100(kFloatWindowMoveToAnotherDeskCountsHistogramName,
floated_window_move_to_another_desk_counter_);
}
gfx::Rect FloatController::GetFloatWindowClamshellBounds(
aura::Window* window,
chromeos::FloatStartLocation location) {
DCHECK(chromeos::wm::CanFloatWindow(window));
if (window->GetProperty(app_restore::kLaunchedFromAppRestoreKey)) {
return window->bounds();
}
const gfx::Rect work_area =
screen_util::GetDisplayWorkAreaBoundsInParent(window);
const int padding_dp = chromeos::wm::kFloatedWindowPaddingDp;
if ((window->GetProperty(aura::client::kResizeBehaviorKey) &
aura::client::kResizeBehaviorCanResize) == 0) {
return GetFloatBounds(window->bounds().size(), work_area, location);
}
const gfx::Size minimum_size = window->delegate()->GetMinimumSize();
gfx::Rect preferred_bounds =
gfx::Rect(std::max(static_cast<int>(work_area.width() *
kFloatedWindowClamshellWidthRatio),
minimum_size.width()),
std::max(static_cast<int>(work_area.height() *
kFloatedWindowClamshellHeightRatio),
minimum_size.height()));
if (window->bounds().height() <= preferred_bounds.height() &&
window->bounds().width() <= preferred_bounds.width()) {
preferred_bounds = window->bounds();
}
const int preferred_width =
std::min(preferred_bounds.width(), work_area.width() - 2 * padding_dp);
const int preferred_height =
std::min(preferred_bounds.height(), work_area.height() - 2 * padding_dp);
return GetFloatBounds(gfx::Size(preferred_width, preferred_height), work_area,
location);
}
gfx::Rect FloatController::GetFloatWindowTabletBounds(aura::Window* window) {
const gfx::Size preferred_size =
chromeos::wm::GetFloatedWindowTabletSize(window);
const int width = preferred_size.width();
const int height = preferred_size.height();
auto* floated_window_info =
Shell::Get()->float_controller()->MaybeGetFloatedWindowInfo(window);
#if DCHECK_IS_ON()
if (!WindowState::Get(window)->is_client_controlled()) {
DCHECK(floated_window_info);
}
#endif
const gfx::Rect work_area =
screen_util::GetDisplayWorkAreaBoundsInParent(window);
gfx::Point origin;
const MagnetismCorner magnetism_corner =
floated_window_info ? floated_window_info->magnetism_corner()
: MagnetismCorner::kBottomRight;
const int padding_dp = chromeos::wm::kFloatedWindowPaddingDp;
switch (magnetism_corner) {
case MagnetismCorner::kTopLeft:
origin =
gfx::Point(work_area.x() + padding_dp, work_area.y() + padding_dp);
break;
case MagnetismCorner::kTopRight:
origin = gfx::Point(work_area.right() - width - padding_dp,
work_area.y() + padding_dp);
break;
case MagnetismCorner::kBottomLeft:
origin = gfx::Point(work_area.x() + padding_dp,
work_area.bottom() - height - padding_dp);
break;
case MagnetismCorner::kBottomRight:
origin = gfx::Point(work_area.right() - width - padding_dp,
work_area.bottom() - height - padding_dp);
break;
}
if (floated_window_info && floated_window_info->is_tucked_for_tablet()) {
int x_offset;
switch (magnetism_corner) {
case MagnetismCorner::kTopLeft:
case MagnetismCorner::kBottomLeft:
x_offset = -width - padding_dp;
break;
case MagnetismCorner::kTopRight:
case MagnetismCorner::kBottomRight:
x_offset = width + padding_dp;
break;
}
origin.Offset(x_offset, 0);
}
return gfx::Rect(origin, gfx::Size(width, height));
}
void FloatController::ToggleFloat(aura::Window* window) {
if (WindowState::Get(window)->IsFloated()) {
UnsetFloat(window);
} else {
SetFloat(window, chromeos::FloatStartLocation::kBottomRight);
}
}
void FloatController::MaybeUntuckFloatedWindowForTablet(
aura::Window* floated_window) {
auto* floated_window_info = MaybeGetFloatedWindowInfo(floated_window);
DCHECK(floated_window_info);
floated_window_info->MaybeUntuckWindow(true);
}
bool FloatController::IsFloatedWindowTuckedForTablet(
const aura::Window* floated_window) const {
auto* floated_window_info = MaybeGetFloatedWindowInfo(floated_window);
if (!floated_window_info) {
return false;
}
return floated_window_info->is_tucked_for_tablet();
}
bool FloatController::IsFloatedWindowAlignedWithShelf(
aura::Window* floated_window) const {
auto* floated_window_info = MaybeGetFloatedWindowInfo(floated_window);
DCHECK(floated_window_info);
if (floated_window_info->is_tucked_for_tablet()) {
return false;
}
MagnetismCorner magnetism_corner = floated_window_info->magnetism_corner();
return magnetism_corner == MagnetismCorner::kBottomLeft ||
magnetism_corner == MagnetismCorner::kBottomRight;
}
views::Widget* FloatController::GetTuckHandleWidget(
const aura::Window* floated_window) const {
auto* floated_window_info = MaybeGetFloatedWindowInfo(floated_window);
DCHECK(floated_window_info);
return floated_window_info->GetTuckHandleWidget();
}
void FloatController::OnDragCompletedForTablet(aura::Window* floated_window) {
auto* floated_window_info = MaybeGetFloatedWindowInfo(floated_window);
DCHECK(floated_window_info);
floated_window_info->set_magnetism_corner(
GetMagnetismCornerForBounds(floated_window->GetBoundsInScreen()));
UpdateWindowBoundsForTablet(floated_window,
WindowState::BoundsChangeAnimationType::kAnimate);
}
void FloatController::OnFlingOrSwipeForTablet(aura::Window* floated_window,
float velocity_x,
float velocity_y) {
auto* floated_window_info = MaybeGetFloatedWindowInfo(floated_window);
DCHECK(floated_window_info);
MagnetismCorner magnetism_corner = floated_window_info->magnetism_corner();
bool start_left = magnetism_corner == MagnetismCorner::kTopLeft ||
magnetism_corner == MagnetismCorner::kBottomLeft;
if (velocity_y < 0.f) {
floated_window_info->set_magnetism_corner(
start_left ? MagnetismCorner::kTopLeft : MagnetismCorner::kTopRight);
} else if (velocity_y > 0.f) {
floated_window_info->set_magnetism_corner(
start_left ? MagnetismCorner::kBottomLeft
: MagnetismCorner::kBottomRight);
}
magnetism_corner = floated_window_info->magnetism_corner();
bool start_top = magnetism_corner == MagnetismCorner::kTopLeft ||
magnetism_corner == MagnetismCorner::kTopRight;
if (velocity_x < 0.f) {
floated_window_info->set_magnetism_corner(
start_top ? MagnetismCorner::kTopLeft : MagnetismCorner::kBottomLeft);
} else if (velocity_x > 0.f) {
floated_window_info->set_magnetism_corner(
start_top ? MagnetismCorner::kTopRight : MagnetismCorner::kBottomRight);
}
if ((start_left && velocity_x < 0.f) || (!start_left && velocity_x > 0.f)) {
floated_window_info->MaybeTuckWindow(start_left);
return;
}
UpdateWindowBoundsForTablet(floated_window,
WindowState::BoundsChangeAnimationType::kAnimate);
}
const Desk* FloatController::FindDeskOfFloatedWindow(
const aura::Window* window) const {
if (auto* info = MaybeGetFloatedWindowInfo(window))
return info->desk();
return nullptr;
}
aura::Window* FloatController::FindFloatedWindowOfDesk(const Desk* desk) const {
DCHECK(desk);
for (const auto& [window, info] : floated_window_info_map_) {
if (info->desk() == desk)
return window;
}
return nullptr;
}
void FloatController::OnMovingAllWindowsOutToDesk(Desk* original_desk,
Desk* target_desk) {
auto* original_desk_floated_window = FindFloatedWindowOfDesk(original_desk);
if (!original_desk_floated_window)
return;
++floated_window_move_to_another_desk_counter_;
auto* target_desk_floated_window = FindFloatedWindowOfDesk(target_desk);
ShowFloatedWindow(original_desk_floated_window);
if (target_desk_floated_window) {
ResetFloatedWindow(original_desk_floated_window);
} else {
floated_window_info_map_[original_desk_floated_window]->set_desk(
target_desk);
Shell::Get()->mru_window_tracker()->OnWindowMovedOutFromRemovingDesk(
original_desk_floated_window);
}
}
void FloatController::OnMovingFloatedWindowToDesk(aura::Window* floated_window,
Desk* active_desk,
Desk* target_desk,
aura::Window* target_root) {
auto* target_desk_floated_window = FindFloatedWindowOfDesk(target_desk);
aura::Window* root = floated_window->GetRootWindow();
if (target_desk_floated_window) {
ResetFloatedWindow(target_desk_floated_window);
}
auto* float_info = MaybeGetFloatedWindowInfo(floated_window);
DCHECK(float_info);
DCHECK_EQ(float_info->desk(), active_desk);
float_info->set_desk(target_desk);
++floated_window_move_to_another_desk_counter_;
if (root != target_root) {
window_util::MoveWindowToDisplay(
floated_window,
display::Screen::Get()->GetDisplayNearestWindow(target_root).id());
}
if (!desks_util::IsWindowVisibleOnAllWorkspaces(floated_window)) {
if (target_desk->is_active()) {
ShowFloatedWindow(floated_window);
} else {
HideFloatedWindow(floated_window);
}
}
active_desk->NotifyContentChanged();
target_desk->NotifyContentChanged();
}
void FloatController::ClearWorkspaceEventHandler(aura::Window* root) {
workspace_event_handlers_.erase(root);
}
void FloatController::OnDeskActivationChanged(const Desk* activated,
const Desk* deactivated) {
auto deactivated_desk_floated_window_info_iter = std::ranges::find_if(
floated_window_info_map_, [deactivated](const auto& floated_window_info) {
return floated_window_info.second->desk() == deactivated;
});
if (deactivated_desk_floated_window_info_iter !=
floated_window_info_map_.end()) {
if (display::Screen::Get()->InTabletMode()) {
deactivated_desk_floated_window_info_iter->second->MaybeUntuckWindow(
false);
}
HideFloatedWindow(deactivated_desk_floated_window_info_iter->first);
}
if (auto* activated_desk_floated_window =
FindFloatedWindowOfDesk(activated)) {
ShowFloatedWindow(activated_desk_floated_window);
if (auto* top_window = window_util::GetTopWindow();
top_window == activated_desk_floated_window) {
wm::ActivateWindow(top_window);
}
}
}
void FloatController::OnDisplayMetricsChanged(const display::Display& display,
uint32_t metrics) {
display::TabletState tablet_state = display::Screen::Get()->GetTabletState();
if (tablet_state == display::TabletState::kEnteringTabletMode ||
tablet_state == display::TabletState::kExitingTabletMode) {
return;
}
const uint32_t filter = DISPLAY_METRIC_BOUNDS | DISPLAY_METRIC_WORK_AREA;
if ((filter & metrics) == 0) {
return;
}
DCHECK(!floated_window_info_map_.empty());
std::vector<aura::Window*> windows_need_reset;
for (auto& [window, info] : floated_window_info_map_) {
if (!chromeos::wm::CanFloatWindow(window)) {
windows_need_reset.push_back(window);
} else {
if (metrics & DISPLAY_METRIC_BOUNDS ||
metrics & DISPLAY_METRIC_WORK_AREA) {
const DisplayMetricsChangedWMEvent wm_event(metrics);
WindowState::Get(window)->OnWMEvent(&wm_event);
}
}
}
for (auto* window : windows_need_reset)
ResetFloatedWindow(window);
if (DISPLAY_METRIC_ROTATION & metrics) {
if (auto* root_controller =
Shell::GetRootWindowControllerWithDisplayId(display.id())) {
if (auto* animator = root_controller->GetScreenRotationAnimator();
animator &&
!screen_rotation_observations_.IsObservingSource(animator)) {
screen_rotation_observations_.AddObservation(animator);
}
}
}
}
void FloatController::OnDisplayTabletStateChanged(display::TabletState state) {
switch (state) {
case display::TabletState::kInClamshellMode:
case display::TabletState::kEnteringTabletMode:
break;
case display::TabletState::kInTabletMode:
OnTabletModeStarted();
break;
case display::TabletState::kExitingTabletMode:
OnTabletModeEnding();
break;
}
}
void FloatController::OnRootWindowAdded(aura::Window* root_window) {
workspace_event_handlers_[root_window] =
std::make_unique<WorkspaceEventHandler>(
root_window->GetChildById(kShellWindowId_FloatContainer));
root_window->GetChildById(kShellWindowId_FloatContainer)
->SetLayoutManager(std::make_unique<FloatLayoutManager>());
}
void FloatController::OnRootWindowWillShutdown(aura::Window* root_window) {
if (auto* const animator = RootWindowController::ForWindow(root_window)
->GetScreenRotationAnimator();
animator && screen_rotation_observations_.IsObservingSource(animator)) {
screen_rotation_observations_.RemoveObservation(animator);
}
}
void FloatController::OnScreenCopiedBeforeRotation() {}
void FloatController::OnScreenRotationAnimationFinished(
ScreenRotationAnimator* animator,
bool canceled) {
for (auto& [window, info] : floated_window_info_map_) {
if (WindowState::Get(window)->is_client_controlled()) {
const gfx::Rect bounds =
display::Screen::Get()->InTabletMode()
? GetFloatWindowTabletBounds(window)
: GetFloatWindowClamshellBounds(
window, chromeos::FloatStartLocation::kBottomRight);
const SetBoundsWMEvent event(bounds);
WindowState::Get(window)->OnWMEvent(&event);
if (IsFloatedWindowTuckedForTablet(window)) {
TabletModeWindowState::UpdateWindowPosition(
WindowState::Get(window),
WindowState::BoundsChangeAnimationType::kNone);
}
}
}
}
void FloatController::OnPinnedStateChanged(aura::Window* pinned_window) {
if (aura::Window* floated_window =
window_util::GetFloatedWindowForActiveDesk()) {
if (aura::Window* to_be_pinned_window =
Shell::Get()->screen_pinning_controller()->pinned_window()) {
if (to_be_pinned_window != floated_window) {
HideFloatedWindow(floated_window);
}
} else {
ShowFloatedWindow(floated_window);
}
}
}
void FloatController::SetFloat(
aura::Window* window,
chromeos::FloatStartLocation float_start_location) {
auto* window_state = WindowState::Get(window);
if (!window_state->IsFloated()) {
const WindowFloatWMEvent float_event(float_start_location);
window_state->OnWMEvent(&float_event);
}
}
void FloatController::UnsetFloat(aura::Window* window) {
auto* window_state = WindowState::Get(window);
if (window_state->IsFloated()) {
const WMEvent restore_event(WM_EVENT_RESTORE);
window_state->OnWMEvent(&restore_event);
}
}
FloatController::MagnetismCorner FloatController::GetMagnetismCornerForBounds(
const gfx::Rect& bounds_in_screen) {
const gfx::Point display_bounds_center =
display::Screen::Get()
->GetDisplayMatching(bounds_in_screen)
.bounds()
.CenterPoint();
const gfx::Point center_point = bounds_in_screen.CenterPoint();
const bool is_left_half = center_point.x() < display_bounds_center.x();
if (center_point.y() < display_bounds_center.y()) {
return is_left_half ? FloatController::MagnetismCorner::kTopLeft
: FloatController::MagnetismCorner::kTopRight;
}
return is_left_half ? FloatController::MagnetismCorner::kBottomLeft
: FloatController::MagnetismCorner::kBottomRight;
}
void FloatController::FloatForTablet(aura::Window* window,
chromeos::WindowStateType old_state_type) {
CHECK(display::Screen::Get()->InTabletMode());
FloatImpl(window);
std::optional<MagnetismCorner> magnetism_corner;
if (chromeos::IsMinimizedWindowStateType(old_state_type)) {
magnetism_corner = GetMagnetismCornerForBounds(window->GetBoundsInScreen());
} else if (chromeos::IsSnappedWindowStateType(old_state_type)) {
const bool left_or_top =
old_state_type == chromeos::WindowStateType::kPrimarySnapped;
const bool landscape = IsCurrentScreenOrientationLandscape();
if (!left_or_top) {
magnetism_corner = MagnetismCorner::kBottomRight;
} else if (landscape) {
magnetism_corner = MagnetismCorner::kBottomLeft;
} else {
CHECK(left_or_top && !landscape);
magnetism_corner = MagnetismCorner::kTopRight;
}
}
if (!magnetism_corner) {
return;
}
auto* floated_window_info = MaybeGetFloatedWindowInfo(window);
CHECK(floated_window_info);
floated_window_info->set_magnetism_corner(*magnetism_corner);
}
void FloatController::FloatImpl(aura::Window* window) {
if (floated_window_info_map_.contains(window))
return;
auto* desk_controller = DesksController::Get();
const Desk* desk = desks_util::GetDeskForContext(window);
if (!desk) {
return;
}
const bool reset_all_desks =
desks_util::IsWindowVisibleOnAllWorkspaces(window);
std::vector<aura::Window*> windows_to_reset;
for (const auto& [floated_window, info] : floated_window_info_map_) {
if (reset_all_desks || info->desk() == desk) {
windows_to_reset.push_back(floated_window);
}
}
if (reset_all_desks) {
desk_controller->UntrackWindowFromAllDesks(window);
}
floated_window_info_map_.emplace(
window, std::make_unique<FloatedWindowInfo>(window, desk));
for (auto* reset_window : windows_to_reset) {
ResetFloatedWindow(reset_window);
}
aura::Window* floated_container =
window->GetRootWindow()->GetChildById(kShellWindowId_FloatContainer);
DCHECK_NE(window->parent(), floated_container);
floated_container->AddChild(window);
if (!desk->is_active())
HideFloatedWindow(window);
++floated_window_counter_;
if (!desks_controller_observation_.IsObserving())
desks_controller_observation_.Observe(desk_controller);
if (!display_observer_)
display_observer_.emplace(this);
}
void FloatController::UnfloatImpl(aura::Window* window) {
auto* floated_window_info = MaybeGetFloatedWindowInfo(window);
if (!floated_window_info)
return;
ShowFloatedWindow(window);
floated_window_info->desk()
->GetDeskContainerForRoot(window->GetRootWindow())
->AddChild(window);
floated_window_info_map_.erase(window);
if (floated_window_info_map_.empty()) {
desks_controller_observation_.Reset();
display_observer_.reset();
}
if (desks_util::IsWindowVisibleOnAllWorkspaces(window)) {
DesksController::Get()->TrackWindowOnAllDesks(window);
}
}
void FloatController::ResetFloatedWindow(aura::Window* floated_window) {
DCHECK(floated_window);
DCHECK(WindowState::Get(floated_window)->IsFloated());
UnsetFloat(floated_window);
}
FloatController::FloatedWindowInfo* FloatController::MaybeGetFloatedWindowInfo(
const aura::Window* window) const {
const auto iter = floated_window_info_map_.find(window);
if (iter == floated_window_info_map_.end())
return nullptr;
return iter->second.get();
}
void FloatController::OnFloatedWindowDestroying(aura::Window* floated_window) {
DesksController::Get()->MaybeRemoveVisibleOnAllDesksWindow(floated_window);
floated_window_info_map_.erase(floated_window);
if (floated_window_info_map_.empty()) {
desks_controller_observation_.Reset();
display_observer_.reset();
}
}
void FloatController::OnTabletModeStarted() {
DCHECK(!floated_window_info_map_.empty());
for (auto& [window, info] : floated_window_info_map_) {
if (chromeos::wm::CanFloatWindow(window)) {
info->set_magnetism_corner(
GetMagnetismCornerForBounds(window->GetBoundsInScreen()));
UpdateWindowBoundsForTablet(
window, WindowState::BoundsChangeAnimationType::kCrossFade);
} else {
ResetFloatedWindow(window);
}
}
}
void FloatController::OnTabletModeEnding() {
for (auto& [window, info] : floated_window_info_map_) {
info->MaybeUntuckWindow(false);
}
}
}