blob: 4174dc058834ebd2bd6a71b5139a431db8626a0c [file] [log] [blame]
// Copyright 2023 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/wm/pip/pip_controller.h"
#include "ash/public/cpp/app_types_util.h"
#include "ash/public/cpp/shell_window_ids.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/screen_util.h"
#include "ash/shell.h"
#include "ash/wm/collision_detection/collision_detection_utils.h"
#include "ash/wm/pip/pip_positioner.h"
#include "ash/wm/window_dimmer.h"
#include "ash/wm/window_util.h"
#include "ash/wm/wm_event.h"
#include "chromeos/ui/base/chromeos_ui_constants.h"
#include "ui/aura/client/aura_constants.h"
#include "ui/aura/window_delegate.h"
#include "ui/compositor/layer.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/geometry/resize_utils.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/wm/core/coordinate_conversion.h"
namespace ash {
namespace {
// The maximum opacity for the `WindowDimmer`.
constexpr float kPipTuckDimMaximumOpacity = 0.5f;
constexpr base::TimeDelta kSizeChangeAnimationDuration =
base::Milliseconds(300);
constexpr float kDefaultPipSizeFromWorkAreaPercent = 0.20f;
// Calculates the new size of a given buffer size while preserving the aspect
// ratio of the PiP window.
gfx::Size PreserveAspectRatio(aura::Window* pip_window,
const gfx::Size& buffer_size) {
gfx::Size max_size =
pip_window->delegate()->GetMaximumSize().value_or(gfx::Size());
gfx::Size min_size = pip_window->delegate()->GetMinimumSize();
gfx::Size buffer_size_clone = buffer_size;
gfx::SizeF* aspect_ratio_size =
pip_window->GetProperty(aura::client::kAspectRatio);
buffer_size_clone.SetToMin(max_size);
buffer_size_clone.SetToMax(min_size);
float aspect_ratio = aspect_ratio_size->width() / aspect_ratio_size->height();
gfx::Rect window_rect(pip_window->bounds().origin(), buffer_size_clone);
gfx::SizeRectToAspectRatio(gfx::ResizeEdge::kBottomRight, aspect_ratio,
min_size, max_size, &window_rect);
return window_rect.size();
}
gfx::Size GetMaxSize(WindowState* pip_window_state) {
// Calculates the max size of the PiP window preserving the aspect ratio.
gfx::Size max_size =
pip_window_state->window()->delegate()->GetMaximumSize().value_or(
gfx::Size());
return PreserveAspectRatio(pip_window_state->window(), max_size);
}
gfx::Size GetDefaultSize(WindowState* pip_window_state) {
gfx::Size work_area_size =
display::Screen::GetScreen()
->GetDisplayNearestWindow(pip_window_state->window()->GetRootWindow())
.work_area_size();
gfx::Size window_size = gfx::ScaleToRoundedSize(
work_area_size, kDefaultPipSizeFromWorkAreaPercent);
return PreserveAspectRatio(pip_window_state->window(), window_size);
}
bool IsAtMaxSize(int width, int height, const gfx::Size& max_size) {
return width == max_size.width() && height == max_size.height();
}
bool IsDoubleTap(const ui::Event& event) {
return (event.IsMouseEvent() && event.flags() & ui::EF_IS_DOUBLE_CLICK) ||
(event.IsGestureEvent() &&
event.AsGestureEvent()->details().tap_count() == 2);
}
class PipScopedWindowTuckerDelegate : public ScopedWindowTucker::Delegate {
public:
explicit PipScopedWindowTuckerDelegate() {}
PipScopedWindowTuckerDelegate(const PipScopedWindowTuckerDelegate&) = delete;
PipScopedWindowTuckerDelegate& operator=(
const PipScopedWindowTuckerDelegate&) = delete;
~PipScopedWindowTuckerDelegate() override = default;
void PaintTuckHandle(gfx::Canvas* canvas, int width, bool left) override {
// Flip the canvas horizontally for `left` tuck handle.
if (left) {
canvas->Translate(gfx::Vector2d(width, 0));
canvas->Scale(-1, 1);
}
const gfx::ImageSkia& tuck_icon = gfx::CreateVectorIcon(
kTuckHandleChevronIcon, ScopedWindowTucker::kTuckHandleWidth,
SK_ColorWHITE);
canvas->DrawImageInt(tuck_icon, 0, 0);
}
int ParentContainerId() const override { return kShellWindowId_PipContainer; }
void UpdateWindowPosition(aura::Window* window, bool left) override {
const gfx::Rect work_area =
screen_util::GetDisplayWorkAreaBoundsInParent(window);
gfx::Rect bounds_in_parent = window->bounds();
int bounds_left;
if (Shell::Get()->pip_controller()->is_tucked()) {
if (left) {
bounds_left =
-bounds_in_parent.width() + ScopedWindowTucker::kTuckHandleWidth;
} else {
bounds_left = work_area.width() - ScopedWindowTucker::kTuckHandleWidth;
}
} else {
if (left) {
bounds_left = kCollisionWindowWorkAreaInsetsDp;
} else {
bounds_left = work_area.width() - window->bounds().width() -
kCollisionWindowWorkAreaInsetsDp;
}
}
bounds_in_parent.set_origin(gfx::Point(bounds_left, bounds_in_parent.y()));
window->SetBounds(bounds_in_parent);
}
void UntuckWindow(aura::Window* window) override {
Shell::Get()->pip_controller()->UntuckWindow();
}
void OnAnimateTuckEnded(aura::Window* window) override {}
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(ScopedWindowTucker::kTuckHandleWidth,
ScopedWindowTucker::kTuckHandleHeight / 2)
: window_bounds.left_center() -
gfx::Vector2d(0, ScopedWindowTucker::kTuckHandleHeight / 2);
return gfx::Rect(tuck_handle_origin,
gfx::Size(ScopedWindowTucker::kTuckHandleWidth,
ScopedWindowTucker::kTuckHandleHeight));
}
};
} // namespace
PipController::PipController() = default;
PipController::~PipController() = default;
void PipController::SetPipWindow(aura::Window* window) {
if (!window || pip_window_ == window) {
return;
}
if (pip_window_) {
// As removing ARC/Lacros PiP is async, a new PiP could be created before
// the currently one is fully removed.
UnsetPipWindow(pip_window_);
}
pip_window_ = window;
is_tucked_ = false;
scoped_window_tucker_.reset();
dimmer_.reset();
pip_window_observation_.Reset();
pip_window_observation_.Observe(window);
}
void PipController::UnsetPipWindow(aura::Window* window) {
if (!pip_window_ || pip_window_ != window) {
// This function can be called with one of window state, visibility, or
// existence changes, all of which are valid.
return;
}
pip_window_observation_.Reset();
pip_window_ = nullptr;
scoped_window_tucker_.reset();
is_tucked_ = false;
dimmer_.reset();
}
bool PipController::CanResizePip() {
if (!pip_window_) {
return false;
}
gfx::Size max_size =
pip_window_->delegate()->GetMaximumSize().value_or(gfx::Size());
gfx::Size min_size = pip_window_->delegate()->GetMinimumSize();
return !max_size.IsEmpty() && !min_size.IsEmpty() &&
max_size.width() > min_size.width() &&
max_size.height() > min_size.height();
}
void PipController::UpdatePipBounds() {
if (!pip_window_) {
// It's a bit hard for the caller of this function to tell when PiP is
// really active (v.s. A PiP window just exists), so allow calling this
// when not appropriate.
return;
}
if (is_tucked_) {
// If the window is tucked, we do not want to move it to the resting
// position.
return;
}
WindowState* window_state = WindowState::Get(pip_window_);
if (!ash::PipPositioner::HasSnapFraction(window_state) &&
IsArcWindow(pip_window_)) {
// Prevent PiP bounds from being updated between window state change into
// PiP and initial bounds change for PiP. This is only needed for ARC
// because chrome PiP becomes visible only after both window state and
// initial bounds are set properly while in the case of ARC a normal visible
// window can trainsition to PiP. Also, in fact, this check is only valid
// for ARC PiP as the first timing snap fraction is set is different between
// chrome PiP and ARC PiP.
return;
}
gfx::Rect new_bounds =
PipPositioner::GetPositionAfterMovementAreaChange(window_state);
wm::ConvertRectFromScreen(pip_window_->GetRootWindow(), &new_bounds);
if (pip_window_->bounds() != new_bounds) {
SetBoundsWMEvent event(new_bounds, /*animate=*/true);
window_state->OnWMEvent(&event);
}
}
void PipController::TuckWindow(bool left) {
CHECK(pip_window_);
SetDimOpacity(kPipTuckDimMaximumOpacity);
is_tucked_ = true;
scoped_window_tucker_ = std::make_unique<ScopedWindowTucker>(
std::make_unique<PipScopedWindowTuckerDelegate>(), pip_window_, left);
scoped_window_tucker_->AnimateTuck();
}
void PipController::OnUntuckAnimationEnded() {
scoped_window_tucker_.reset();
}
void PipController::UntuckWindow() {
CHECK(pip_window_);
// The order here matters: `is_tucked_` must be set to true
// before `UpdateWindowPosition()` or `AnimateUntuck()` gets
// the untucked window bounds.
is_tucked_ = false;
SetDimOpacity(0.f);
if (scoped_window_tucker_) {
scoped_window_tucker_->AnimateUntuck(
base::BindOnce(&PipController::OnUntuckAnimationEnded,
weak_ptr_factory_.GetWeakPtr()));
}
}
views::Widget* PipController::GetTuckHandleWidget() {
CHECK(scoped_window_tucker_);
return scoped_window_tucker_->tuck_handle_widget();
}
void PipController::SetDimOpacity(float opacity) {
if (!pip_window_) {
// This function is invoked during drag move, and PiP can get killed during
// drag move too.
return;
}
if (opacity == 0.f) {
if (dimmer_) {
dimmer_->window()->Hide();
}
} else {
if (!dimmer_) {
// The dimmer is created when it is first needed. It is not created
// with `SetPipWindow()` because it is called in
// `OnPrePipStateChange()` before the window fully enters the PiP state.
dimmer_ = std::make_unique<WindowDimmer>(pip_window_);
dimmer_->SetDimOpacity(kPipTuckDimMaximumOpacity);
dimmer_->window()->layer()->SetIsFastRoundedCorner(true);
dimmer_->window()->layer()->SetRoundedCornerRadius(
gfx::RoundedCornersF(chromeos::kPipRoundedCornerRadius));
}
dimmer_->SetDimOpacity(opacity);
dimmer_->window()->Show();
}
}
void PipController::OnWindowDestroying(aura::Window* window) {
// Ensure to clean up when PiP is gone especially in unit tests.
UnsetPipWindow(window);
}
bool PipController::HandleDoubleTap(const ui::Event& event) {
return pip_size_switch_handler_.ProcessDoubleTapEvent(event);
}
bool PipController::HandleKeyboardShortcut() {
return pip_size_switch_handler_.ProcessShortcutEvent(pip_window_);
}
bool PipController::PipSizeSwitchHandler::ProcessDoubleTapEvent(
const ui::Event& event) {
if (!IsDoubleTap(event)) {
return false;
}
aura::Window* target = static_cast<aura::Window*>(event.target());
WindowState* window_state = WindowState::Get(target->GetToplevelWindow());
if (!window_state) {
return false;
}
if (window_util::IsArcPipWindow(window_state->window())) {
return false;
}
return ResizePip(window_state);
}
bool PipController::PipSizeSwitchHandler::ProcessShortcutEvent(aura::Window* pip_window) {
WindowState* window_state = WindowState::Get(pip_window);
if (!window_state) {
return false;
}
return ResizePip(window_state);
}
bool PipController::PipSizeSwitchHandler::ResizePip(WindowState* window_state) {
if (!window_state) {
return false;
}
if (!window_state->IsPip()) {
return false;
}
if (!Shell::Get()->pip_controller()->CanResizePip()) {
return false;
}
gfx::Rect bounds = window_state->window()->bounds();
window_state->SetBoundsChangedByUser(true);
gfx::Size calculated_max_size = GetMaxSize(window_state);
// If the window is not at max size, we will expand it.
if (IsAtMaxSize(bounds.width(), bounds.height(), calculated_max_size)) {
// If the PiP window is max from the result of a drag resize (without ever
// double tapping) then go back to default.
if (prev_bounds_.IsEmpty()) {
bounds = gfx::Rect(bounds.origin(), GetDefaultSize(window_state));
} else {
bounds = prev_bounds_;
}
} else { // Is not max size.
// Save the bounds of the window prior to expanding.
prev_bounds_ = bounds;
bounds.set_size(calculated_max_size);
}
// Note that WindowState will place the position of the PiP window in the
// correct location.
SetBoundsWMEvent bounds_event(bounds, /*animate=*/true,
kSizeChangeAnimationDuration);
window_state->OnWMEvent(&bounds_event);
return true;
}
} // namespace ash