// 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/wm_event.h"
#include "chromeos/ui/base/chromeos_ui_constants.h"
#include "ui/aura/window_delegate.h"
#include "ui/compositor/layer.h"
#include "ui/gfx/canvas.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;
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();
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);
}
} // namespace ash