// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/exo/shell_surface.h"
#include <optional>
#include "ash/frame/non_client_frame_view_ash.h"
#include "ash/public/cpp/shell_window_ids.h"
#include "ash/shell.h"
#include "ash/wm/desks/desks_util.h"
#include "ash/wm/toplevel_window_event_handler.h"
#include "ash/wm/window_resizer.h"
#include "ash/wm/window_state.h"
#include "base/containers/adapters.h"
#include "base/debug/crash_logging.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/strings/utf_string_conversions.h"
#include "base/trace_event/trace_event.h"
#include "cc/layers/deadline_policy.h"
#include "chromeos/ui/base/window_properties.h"
#include "chromeos/ui/base/window_state_type.h"
#include "components/exo/custom_window_state_delegate.h"
#include "components/exo/shell_surface_util.h"
#include "components/exo/window_properties.h"
#include "components/viz/common/surfaces/local_surface_id.h"
#include "components/viz/common/surfaces/surface_id.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/aura/client/aura_constants.h"
#include "ui/aura/client/cursor_client.h"
#include "ui/aura/client/screen_position_client.h"
#include "ui/aura/env.h"
#include "ui/aura/window.h"
#include "ui/aura/window_event_dispatcher.h"
#include "ui/aura/window_tree_host.h"
#include "ui/base/ui_base_types.h"
#include "ui/compositor/layer.h"
#include "ui/views/widget/widget.h"
#include "ui/wm/core/coordinate_conversion.h"
#include "ui/wm/core/scoped_animation_disabler.h"
#include "ui/wm/core/transient_window_manager.h"
#include "ui/wm/core/window_util.h"
namespace exo {
namespace {
// Default maximum amount of time to wait for contents to change. For example,
// happens during a maximize, fullscreen or pinned state change, or raster scale
// change.
constexpr int kDefaultCompositorLockTimeoutMs = 100;
// Compositor lock timeout for slower changes (e.g. display scale change).
constexpr int kSlowCompositorLockTimeoutMs = 500;
gfx::Rect GetClientBoundsInScreen(views::Widget* widget) {
gfx::Rect window_bounds = widget->GetWindowBoundsInScreen();
// Account for popup windows not having a non-client view.
if (widget->non_client_view()) {
return static_cast<ash::NonClientFrameViewAsh*>(
widget->non_client_view()->frame_view())
->GetClientBoundsForWindowBounds(window_bounds);
}
return window_bounds;
}
// HTCLIENT can be used to drag the window in specific scenario.
// (e.g. Drag from shelf)
bool IsMoveComponent(int resize_component) {
return resize_component == HTCAPTION || resize_component == HTCLIENT;
}
} // namespace
////////////////////////////////////////////////////////////////////////////////
// ShellSurface, Config:
// Surface state associated with each configure request.
struct ShellSurface::Config {
Config(uint32_t serial,
const gfx::Vector2d& origin_offset,
int resize_component,
const viz::LocalSurfaceId& viz_surface_id,
base::WeakPtr<ui::Layer> old_layer,
std::unique_ptr<ui::CompositorLock> compositor_lock);
~Config() = default;
uint32_t serial;
gfx::Vector2d origin_offset;
int resize_component;
const viz::LocalSurfaceId viz_surface_id;
base::WeakPtr<ui::Layer> old_layer;
std::unique_ptr<ui::CompositorLock> compositor_lock;
};
ShellSurface::Config::Config(
uint32_t serial,
const gfx::Vector2d& origin_offset,
int resize_component,
const viz::LocalSurfaceId& viz_surface_id,
base::WeakPtr<ui::Layer> old_layer,
std::unique_ptr<ui::CompositorLock> compositor_lock)
: serial(serial),
origin_offset(origin_offset),
resize_component(resize_component),
viz_surface_id(viz_surface_id),
old_layer(std::move(old_layer)),
compositor_lock(std::move(compositor_lock)) {}
////////////////////////////////////////////////////////////////////////////////
// ShellSurface, ScopedConfigure:
ShellSurface::ScopedConfigure::ScopedConfigure(ShellSurface* shell_surface,
bool force_configure)
: shell_surface_(shell_surface), force_configure_(force_configure) {
// ScopedConfigure instances cannot be nested.
DCHECK(!shell_surface_->scoped_configure_);
shell_surface_->scoped_configure_ = this;
}
ShellSurface::ScopedConfigure::~ScopedConfigure() {
DCHECK_EQ(shell_surface_->scoped_configure_, this);
shell_surface_->scoped_configure_ = nullptr;
if (needs_configure_ || force_configure_)
shell_surface_->Configure();
// ScopedConfigure instance might have suppressed a widget bounds update.
if (shell_surface_->widget_) {
shell_surface_->UpdateWidgetBounds();
shell_surface_->UpdateShadow();
}
}
////////////////////////////////////////////////////////////////////////////////
// ShellSurface, OcclusionObserver:
ShellSurface::OcclusionObserver::OcclusionObserver(ShellSurface* shell_surface,
aura::Window* window)
: state_(window->GetOcclusionState()), shell_surface_(shell_surface) {
window->TrackOcclusionState();
window_observation_.Observe(window);
}
ShellSurface::OcclusionObserver::~OcclusionObserver() {}
void ShellSurface::OcclusionObserver::OnWindowDestroying(aura::Window* window) {
window_observation_.Reset();
}
void ShellSurface::OcclusionObserver::OnWindowOcclusionChanged(
aura::Window* window) {
MaybeConfigure(window);
}
void ShellSurface::OcclusionObserver::MaybeConfigure(aura::Window* window) {
auto new_state = window->GetOcclusionState();
if (state_ != new_state && shell_surface_->IsReady()) {
state_ = new_state;
shell_surface_->Configure();
}
}
aura::Window::OcclusionState
ShellSurface::OcclusionObserver::GetInitialStateForConfigure(
chromeos::WindowStateType state_type) {
// TODO(crbug.com/328172097): Put this back to sending HIDDEN for minimized
// when we have some guarantee that the client will produce content while
// hidden for the initial configure.
state_ = aura::Window::OcclusionState::VISIBLE;
return state_;
}
////////////////////////////////////////////////////////////////////////////////
// ShellSurface, public:
ShellSurface::ShellSurface(Surface* surface,
const gfx::Point& origin,
bool can_minimize,
int container)
: ShellSurfaceBase(surface, origin, can_minimize, container) {
CHECK(surface->window());
occlusion_observer_.emplace(this, surface->window());
}
ShellSurface::ShellSurface(Surface* surface)
: ShellSurfaceBase(surface,
gfx::Point(),
/*can_minimize=*/true,
ash::desks_util::GetActiveDeskContainerId()) {
CHECK(surface->window());
occlusion_observer_.emplace(this, surface->window());
}
ShellSurface::~ShellSurface() {
DCHECK(!scoped_configure_);
// Client is gone by now, so don't call callback.
configure_callback_.Reset();
origin_change_callback_.Reset();
ash::WindowState* window_state =
widget_ ? ash::WindowState::Get(widget_->GetNativeWindow()) : nullptr;
if (window_state)
window_state->RemoveObserver(this);
for (auto& observer : observers_)
observer.OnShellSurfaceDestroyed();
}
void ShellSurface::AcknowledgeConfigure(uint32_t serial) {
TRACE_EVENT1("exo", "ShellSurface::AcknowledgeConfigure", "serial", serial);
// Apply all configs that are older or equal to |serial|. The result is that
// the origin of the main surface will move and the resize direction will
// change to reflect the acknowledgement of configure request with |serial|
// at the next call to Commit().
while (!pending_configs_.empty()) {
std::unique_ptr<Config> config = std::move(pending_configs_.front());
pending_configs_.pop_front();
// Add the config offset to the accumulated offset that will be applied when
// Commit() is called.
pending_origin_offset_ += config->origin_offset;
// Set the resize direction that will be applied when Commit() is called.
pending_resize_component_ = config->resize_component;
if (config->serial == serial) {
// `config` needs to stay alive until the next Commit() call.
config_waiting_for_commit_ = std::move(config);
break;
}
}
for (auto& observer : observers_)
observer.OnAcknowledgeConfigure(serial);
// Shadow bounds update should be called in the next Commit() when applying
// config instead of updating right when the client acknowledge the config.
}
void ShellSurface::SetParent(ShellSurface* parent) {
TRACE_EVENT1("exo", "ShellSurface::SetParent", "parent",
parent ? base::UTF16ToASCII(parent->GetWindowTitle()) : "null");
SetParentWindow(parent ? parent->GetWidget()->GetNativeWindow() : nullptr);
}
bool ShellSurface::CanMaximize() const {
// Prevent non-resizable windows being resized via maximize.
return ShellSurfaceBase::CanMaximize() && CanResize();
}
void ShellSurface::Maximize() {
TRACE_EVENT0("exo", "ShellSurface::Maximize");
if (!widget_) {
if (initial_show_state_ != ui::SHOW_STATE_FULLSCREEN ||
ShouldExitFullscreenFromRestoreOrMaximized())
initial_show_state_ = ui::SHOW_STATE_MAXIMIZED;
return;
}
if (!widget_->IsFullscreen() ||
ShouldExitFullscreenFromRestoreOrMaximized()) {
// Note: This will ask client to configure its surface even if already
// maximized.
ScopedConfigure scoped_configure(this, true);
widget_->Maximize();
}
}
void ShellSurface::Minimize() {
TRACE_EVENT0("exo", "ShellSurface::Minimize");
if (!widget_) {
initial_show_state_ = ui::SHOW_STATE_MINIMIZED;
return;
}
// Note: This will ask client to configure its surface even if already
// minimized.
ScopedConfigure scoped_configure(this, true);
widget_->Minimize();
}
void ShellSurface::Restore() {
TRACE_EVENT0("exo", "ShellSurface::Restore");
if (!widget_) {
if (initial_show_state_ != ui::SHOW_STATE_FULLSCREEN ||
ShouldExitFullscreenFromRestoreOrMaximized())
initial_show_state_ = ui::SHOW_STATE_NORMAL;
return;
}
if (!widget_->IsFullscreen() ||
ShouldExitFullscreenFromRestoreOrMaximized()) {
// Note: This will ask client to configure its surface even if already
// maximized.
ScopedConfigure scoped_configure(this, true);
widget_->Restore();
}
}
void ShellSurface::SetFullscreen(bool fullscreen, int64_t display_id) {
TRACE_EVENT2("exo", "ShellSurface::SetFullscreen", "fullscreen", fullscreen,
"display_id", display_id);
if (!widget_) {
if (fullscreen) {
initial_show_state_ = ui::SHOW_STATE_FULLSCREEN;
} else if (initial_show_state_ == ui::SHOW_STATE_FULLSCREEN) {
initial_show_state_ = ui::SHOW_STATE_DEFAULT;
}
return;
}
// Note: This will ask client to configure its surface even if fullscreen
// state doesn't change.
ScopedConfigure scoped_configure(this, true);
widget_->SetFullscreen(fullscreen, display_id);
}
void ShellSurface::SetPopup() {
DCHECK(!widget_);
is_popup_ = true;
}
void ShellSurface::AckRotateFocus(uint32_t serial, bool handled) {
CHECK(!rotate_focus_inflight_requests_.empty())
<< "unexpected ack received, no requests currently inflight";
auto request = rotate_focus_inflight_requests_.front();
rotate_focus_inflight_requests_.pop();
CHECK(request.serial == serial)
<< "unexpected ack requests, expected acks to be received in order. Got: "
<< serial << ", expected: " << request.serial;
if (!handled) {
ash::Shell::Get()->focus_cycler()->RotateFocus(
request.direction, /*move_to_next_widget=*/true);
}
}
void ShellSurface::Grab() {
DCHECK(is_popup_);
DCHECK(!widget_);
has_grab_ = true;
}
bool ShellSurface::StartMove() {
TRACE_EVENT0("exo", "ShellSurface::StartMove");
if (!widget_) {
return false;
}
return AttemptToStartDrag(HTCAPTION);
}
bool ShellSurface::RotatePaneFocusFromView(views::View* focused_view,
bool forward,
bool enable_wrapping) {
if (rotate_focus_callback_.is_null()) {
VLOG(1) << "no callback provided, falling back to default behaviour";
return WidgetDelegate::RotatePaneFocusFromView(focused_view, forward,
enable_wrapping);
}
auto direction =
forward ? ash::FocusCycler::FORWARD : ash::FocusCycler::BACKWARD;
auto serial = rotate_focus_callback_.Run(direction, enable_wrapping);
rotate_focus_inflight_requests_.push({
serial,
direction,
});
return true;
}
bool ShellSurface::StartResize(int component) {
TRACE_EVENT1("exo", "ShellSurface::StartResize", "component", component);
if (!widget_) {
return false;
}
return AttemptToStartDrag(component);
}
void ShellSurface::AddObserver(ShellSurfaceObserver* observer) {
observers_.AddObserver(observer);
}
void ShellSurface::RemoveObserver(ShellSurfaceObserver* observer) {
observers_.RemoveObserver(observer);
}
void ShellSurface::MaybeSetCompositorLockForNextConfigure(int milliseconds) {
if (!configure_callback_.is_null()) {
ui::Compositor* compositor =
widget_->GetNativeWindow()->layer()->GetCompositor();
configure_compositor_lock_ = compositor->GetCompositorLock(
nullptr, base::Milliseconds(milliseconds));
}
}
////////////////////////////////////////////////////////////////////////////////
// SurfaceDelegate overrides:
void ShellSurface::OnSetFrame(SurfaceFrameType type) {
ShellSurfaceBase::OnSetFrame(type);
if (!widget_)
return;
widget_->GetNativeWindow()->SetProperty(
aura::client::kUseWindowBoundsForShadow,
frame_type_ != SurfaceFrameType::SHADOW);
}
void ShellSurface::OnSetParent(Surface* parent, const gfx::Point& position) {
views::Widget* parent_widget =
parent ? views::Widget::GetTopLevelWidgetForNativeView(parent->window())
: nullptr;
if (parent_widget) {
// Set parent window if using one of the desks container and the container
// itself is not the parent.
if (ash::desks_util::IsDeskContainerId(container_))
SetParentWindow(parent_widget->GetNativeWindow());
origin_ = position;
views::View::ConvertPointToScreen(
parent_widget->widget_delegate()->GetContentsView(), &origin_);
if (!widget_)
return;
ash::WindowState* window_state =
ash::WindowState::Get(widget_->GetNativeWindow());
if (window_state->is_dragged())
return;
gfx::Rect widget_bounds = widget_->GetWindowBoundsInScreen();
gfx::Rect new_widget_bounds(origin_, widget_bounds.size());
if (new_widget_bounds != widget_bounds) {
base::AutoReset<bool> notify_bounds_changes(¬ify_bounds_changes_,
false);
widget_->SetBounds(new_widget_bounds);
UpdateHostWindowOrigin();
}
} else {
SetParentWindow(nullptr);
}
}
////////////////////////////////////////////////////////////////////////////////
// SurfaceTreeHost overrides:
void ShellSurface::MaybeActivateSurface() {
// Keep `host_window()`'s SurfaceId up to date in case it's queried elsewhere.
host_window()->UpdateLocalSurfaceIdFromEmbeddedClient(
GetCurrentLocalSurfaceId());
// `GetCurrentLocalSurfaceId()` may have a newer `child_sequence_number`, b/c
// Wayland client changed the surface hierarchy bounds or scale factor. Update
// `old_layer` surface range s.t. the range strictly includes
// `GetCurrentLocalSurfaceId()`.
for (auto& config : pending_configs_) {
if (config->old_layer) {
UpdateLayerSurfaceRange(config->old_layer.get(),
GetCurrentLocalSurfaceId());
}
}
// Before the first CompositorFrame is submitted by SurfaceTreeHost,
// `host_window()`'s layer doesn't have a SurfaceId yet, so set it to embed
// the upcoming CompositorFrame.
if (!host_window()->layer()->GetSurfaceId()) {
DCHECK(host_window()->GetLocalSurfaceId().parent_sequence_number() ==
GetCurrentLocalSurfaceId().parent_sequence_number() ||
!pending_configs_.empty());
host_window()->layer()->SetShowSurface(
host_window()->GetSurfaceId(), host_window()->bounds().size(),
SK_ColorWHITE, cc::DeadlinePolicy::UseDefaultDeadline(),
false /* stretch_content_to_fill_bounds */);
host_window()->layer()->SetOldestAcceptableFallback(viz::SurfaceId{});
}
UpdateLayerSurfaceRange(host_window()->layer(), GetCurrentLocalSurfaceId());
}
ui::Layer* ShellSurface::GetCommitTargetLayer() {
return const_cast<ui::Layer*>(
const_cast<const ShellSurface*>(this)->GetCommitTargetLayer());
}
const ui::Layer* ShellSurface::GetCommitTargetLayer() const {
if (!host_window()->layer()->GetSurfaceId()) {
return host_window()->layer();
}
// `commit_target_layer` is the layer that will have current LSI. The order of
// LocalSurfaceId parent_sequence_number is:
// GetCurrentLocalSurfaceId() <= pending_config->old_layer <= old_layer_ <=
// host_window()->layer() <= host_window()
//
// Search from newest to oldest layers, if no parent_sequence_number matches,
// return nullptr, as the `commit_target_layer` is too old and already
// destroyed.
if (host_window()
->layer()
->GetSurfaceId()
->local_surface_id()
.parent_sequence_number() ==
GetCurrentLocalSurfaceId().parent_sequence_number()) {
return host_window()->layer();
}
if (old_layer_ &&
old_layer_->GetSurfaceId()->local_surface_id().parent_sequence_number() ==
GetCurrentLocalSurfaceId().parent_sequence_number()) {
return old_layer_.get();
}
for (const auto& config : base::Reversed(pending_configs_)) {
if (config->old_layer &&
config->old_layer->GetSurfaceId()
->local_surface_id()
.parent_sequence_number() ==
GetCurrentLocalSurfaceId().parent_sequence_number()) {
return config->old_layer.get();
}
}
return nullptr;
}
////////////////////////////////////////////////////////////////////////////////
// ShellSurfaceBase overrides:
void ShellSurface::OnSurfaceCommit() {
// Send configure only after the effect of the commit is finalized.
ScopedConfigure scoped_configure(this, false);
ShellSurfaceBase::OnSurfaceCommit();
}
void ShellSurface::InitializeWindowState(ash::WindowState* window_state) {
window_state->AddObserver(this);
window_state->set_allow_set_bounds_direct(movement_disabled_);
window_state->set_ignore_keyboard_bounds_change(movement_disabled_);
widget_->set_movement_disabled(movement_disabled_);
// If this window is a child of some window, it should be made transient.
MaybeMakeTransient();
}
std::optional<gfx::Rect> ShellSurface::GetWidgetBounds() const {
// Defer if configure requests are pending.
if (!pending_configs_.empty() || scoped_configure_)
return std::nullopt;
gfx::Rect new_widget_bounds = GetWidgetBoundsFromVisibleBounds();
if (movement_disabled_) {
new_widget_bounds.set_origin(origin_);
} else if (IsMoveComponent(resize_component_)) {
// Preserve widget position.
new_widget_bounds.set_origin(widget_->GetWindowBoundsInScreen().origin());
} else {
// Compute widget origin using surface origin if the current location of
// surface is being anchored to one side of the widget as a result of a
// resize operation.
gfx::Rect visible_bounds = GetVisibleBounds();
gfx::Point origin = GetSurfaceOrigin() + visible_bounds.OffsetFromOrigin();
wm::ConvertPointToScreen(widget_->GetNativeWindow(), &origin);
new_widget_bounds.set_origin(origin);
}
return new_widget_bounds;
}
gfx::Point ShellSurface::GetSurfaceOrigin() const {
DCHECK(!movement_disabled_ || IsMoveComponent(resize_component_));
gfx::Rect visible_bounds = GetVisibleBounds();
gfx::Rect client_bounds = GetClientViewBounds();
switch (resize_component_) {
case HTCAPTION:
case HTCLIENT:
return gfx::Point() + origin_offset_ - visible_bounds.OffsetFromOrigin();
case HTBOTTOM:
case HTRIGHT:
case HTBOTTOMRIGHT:
return gfx::Point() - visible_bounds.OffsetFromOrigin();
case HTTOP:
case HTTOPRIGHT:
return gfx::Point(0, client_bounds.height() - visible_bounds.height()) -
visible_bounds.OffsetFromOrigin();
case HTLEFT:
case HTBOTTOMLEFT:
return gfx::Point(client_bounds.width() - visible_bounds.width(), 0) -
visible_bounds.OffsetFromOrigin();
case HTTOPLEFT:
return gfx::Point(client_bounds.width() - visible_bounds.width(),
client_bounds.height() - visible_bounds.height()) -
visible_bounds.OffsetFromOrigin();
default:
NOTREACHED_IN_MIGRATION()
<< "Unsupported component:" << resize_component_;
return gfx::Point();
}
}
void ShellSurface::SetUseImmersiveForFullscreen(bool value) {
ShellSurfaceBase::SetUseImmersiveForFullscreen(value);
// Ensure that the widget has been created before attempting to configure it.
// Otherwise, the positioning of the window could be undefined.
if (widget_)
Configure();
}
void ShellSurface::OnDidProcessDisplayChanges(
const DisplayConfigurationChange& configuration_change) {
ShellSurfaceBase::OnDidProcessDisplayChanges(configuration_change);
// Keep client surface coordinates in sync with the server when display
// layouts change.
const bool should_update_window_position = base::ranges::any_of(
configuration_change.display_metrics_changes,
[id = output_display_id()](
const DisplayManagerObserver::DisplayMetricsChange& change) {
return change.display->id() == id &&
(change.changed_metrics &
display::DisplayObserver::DISPLAY_METRIC_BOUNDS ||
change.changed_metrics &
display::DisplayObserver::DISPLAY_METRIC_WORK_AREA);
});
if (widget_ && should_update_window_position) {
OnWidgetScreenPositionChanged();
}
}
////////////////////////////////////////////////////////////////////////////////
// aura::WindowObserver overrides:
void ShellSurface::OnWindowBoundsChanged(aura::Window* window,
const gfx::Rect& old_bounds,
const gfx::Rect& new_bounds,
ui::PropertyChangeReason reason) {
if (!root_surface() || !notify_bounds_changes_) {
return;
}
if (IsShellSurfaceWindow(window)) {
auto* window_state = ash::WindowState::Get(window);
if (window_state && window_state->is_moving_to_another_display()) {
old_screen_bounds_for_pending_move_ = old_bounds;
wm::ConvertRectToScreen(window->parent(),
&old_screen_bounds_for_pending_move_);
return;
}
if (new_bounds.size() == old_bounds.size()) {
OnWidgetScreenPositionChanged();
return;
}
gfx::Vector2d delta = new_bounds.origin() - old_bounds.origin();
origin_offset_ -= delta;
pending_origin_offset_accumulator_ += delta;
if (!old_layer_) {
// If size changed then give the client a chance to produce new contents
// before origin on screen is changed. Retain the old origin by reverting
// the origin delta until the next configure is acknowledged.
UpdateHostWindowOrigin();
} else {
// `old_layer_` means the current `host_window()->layer()`'s is cloned
// from the `old_layer_`. In this case `host_window()->layer()`'s surface
// dependency won't be fulfilled until corresponding configure
// acknowledgement.
// Synchronize bounds to it, s.t. the fallback surface looks reasonable.
// TODO(crbug.com/40057347): Take non-zero origin introduced by geometry
// or clipping into account.
viz::ScopedSurfaceIdAllocator scoped_suppression =
host_window()->GetSurfaceIdAllocator(base::NullCallback());
host_window()->layer()->SetBounds(
gfx::Rect(GetClientBoundsInScreen(widget_).size()));
}
// The shadow size may be updated to match the widget. Change it back
// to the shadow content size. Note that this relies on
// wm::ShadowController being notified of the change before |this|.
UpdateShadow();
// A window state change will send a configuration event. Avoid sending
// two configuration events for the same change.
if (!window_state_is_changing_) {
// Lock when the display scale changes and we are a maximized window to
// prevent flashes.
if (reason != ui::PropertyChangeReason::FROM_ANIMATION &&
ash::WindowState::Get(window)->IsMaximizedOrFullscreenOrPinned()) {
// TODO(crbug.com/40249858): See if we can rid of the slow lock timeout
// by adjusting the order of resize of windows to top to bottom.
MaybeSetCompositorLockForNextConfigure(kSlowCompositorLockTimeoutMs);
}
Configure();
}
}
}
void ShellSurface::OnWindowAddedToRootWindow(aura::Window* window) {
ShellSurfaceBase::OnWindowAddedToRootWindow(window);
if (!IsShellSurfaceWindow(window)) {
return;
}
auto* window_state = ash::WindowState::Get(window);
if (window_state && window_state->is_moving_to_another_display() &&
!old_screen_bounds_for_pending_move_.IsEmpty()) {
gfx::Rect new_bounds_in_screen = window->bounds();
wm::ConvertRectToScreen(window->parent(), &new_bounds_in_screen);
gfx::Vector2d delta = new_bounds_in_screen.origin() -
old_screen_bounds_for_pending_move_.origin();
old_screen_bounds_for_pending_move_ = gfx::Rect();
origin_offset_ -= delta;
pending_origin_offset_accumulator_ += delta;
UpdateHostWindowOrigin();
UpdateShadow();
if (!window_state_is_changing_)
Configure();
} else {
OnWidgetScreenPositionChanged();
}
}
void ShellSurface::OnWindowPropertyChanged(aura::Window* window,
const void* key,
intptr_t old_value) {
ShellSurfaceBase::OnWindowPropertyChanged(window, key, old_value);
if (IsShellSurfaceWindow(window)) {
if (key == chromeos::kIsShowingInOverviewKey) {
if (!overview_change_callback_.is_null()) {
overview_change_callback_.Run(
window->GetProperty(chromeos::kIsShowingInOverviewKey));
}
}
if (key == aura::client::kRasterScale) {
float raster_scale = window->GetProperty(aura::client::kRasterScale);
if (raster_scale == pending_raster_scale_) {
return;
}
pending_raster_scale_ = raster_scale;
Configure();
}
}
}
////////////////////////////////////////////////////////////////////////////////
// ash::WindowStateObserver overrides:
void ShellSurface::OnPreWindowStateTypeChange(
ash::WindowState* window_state,
chromeos::WindowStateType old_type) {
window_state_is_changing_ = true;
chromeos::WindowStateType new_type = window_state->GetStateType();
if (chromeos::IsMinimizedWindowStateType(old_type) ||
chromeos::IsMinimizedWindowStateType(new_type)) {
return;
}
if (chromeos::IsMaximizedOrFullscreenOrPinnedWindowStateType(old_type) ||
chromeos::IsMaximizedOrFullscreenOrPinnedWindowStateType(new_type) ||
window_state->IsMinimized()) {
CHECK(widget_);
// When transitioning in/out of maximized or fullscreen mode, we need to
// make sure we have a configure callback before we allow the default
// cross-fade animations. The configure callback provides a mechanism for
// the client to inform us that a frame has taken the state change into
// account, and without this cross-fade animations are unreliable.
if (!configure_callback_.is_null()) {
// Give client a chance to produce a frame that takes state change into
// account by acquiring a compositor lock.
MaybeSetCompositorLockForNextConfigure(kDefaultCompositorLockTimeoutMs);
} else {
animations_disabler_ = std::make_unique<wm::ScopedAnimationDisabler>(
widget_->GetNativeWindow());
}
}
}
void ShellSurface::OnPostWindowStateTypeChange(
ash::WindowState* window_state,
chromeos::WindowStateType old_type) {
// Send the new state to the exo-client when the state changes. This is
// important for client presentation. For example exo-client using client-side
// decoration, window-state information is needed to toggle the maximize and
// restore buttons. When the window is restored, we show a maximized button;
// otherwise we show a restore button.
//
// Note that configuration events on bounds change is suppressed during state
// change, because it is assumed that a configuration event will always be
// sent at the end of a state change.
Configure();
if (widget_) {
// This may not be necessary.
set_bounds_is_dirty(true);
UpdateWidgetBounds();
UpdateShadow();
}
if (root_surface() && window_state->GetStateType() != old_type &&
(IsFullscreenOrPinnedWindowStateType(window_state->GetStateType()) ||
IsFullscreenOrPinnedWindowStateType(old_type))) {
root_surface()->OnFullscreenStateChanged(window_state->IsFullscreen() ||
window_state->IsPinned());
}
// Re-enable animations if they were disabled in pre state change handler.
animations_disabler_.reset();
window_state_is_changing_ = false;
}
////////////////////////////////////////////////////////////////////////////////
// wm::ActivationChangeObserver overrides:
void ShellSurface::OnWindowActivated(ActivationReason reason,
aura::Window* gained_active,
aura::Window* lost_active) {
ShellSurfaceBase::OnWindowActivated(reason, gained_active, lost_active);
if (!widget_)
return;
if (gained_active == widget_->GetNativeWindow() ||
lost_active == widget_->GetNativeWindow()) {
Configure();
}
}
////////////////////////////////////////////////////////////////////////////////
// ShellSurfaceBase overrides:
gfx::Rect ShellSurface::ComputeAdjustedBounds(const gfx::Rect& bounds) const {
DCHECK(widget_);
auto min_size = widget_->GetMinimumSize();
auto max_size = widget_->GetMaximumSize();
gfx::Size size = bounds.size();
// use `minimum_size_` as the GetMinimumSize always return min size
// bigger or equal to 1x1.
if (!requested_minimum_size_.IsEmpty() && !min_size.IsEmpty()) {
size.SetToMax(min_size);
}
if (!max_size.IsEmpty()) {
size.SetToMin(max_size);
}
// The size should never be bigger than work area, even if the min size is
// bigger than that.
auto work_area = display::Screen::GetScreen()
->GetDisplayNearestWindow(widget_->GetNativeWindow())
.work_area();
size.SetToMin(work_area.size());
// Keep the origin instead of center.
return gfx::Rect(bounds.origin(), size);
}
void ShellSurface::SetWidgetBounds(const gfx::Rect& bounds,
bool adjusted_by_server) {
if (bounds == widget_->GetWindowBoundsInScreen() && !adjusted_by_server)
return;
// Set |notify_bounds_changes_| as this change to window bounds
// should not result in a configure request unless the bounds is modified by
// the server.
DCHECK(notify_bounds_changes_);
notify_bounds_changes_ = adjusted_by_server;
if (IsDragged()) {
// Do not move the root window.
auto* window = widget_->GetNativeWindow();
auto* screen_position_client =
aura::client::GetScreenPositionClient(window->GetRootWindow());
gfx::PointF origin(bounds.origin());
screen_position_client->ConvertPointFromScreen(window->parent(), &origin);
widget_->GetNativeWindow()->SetBounds(
gfx::Rect(origin.x(), origin.y(), bounds.width(), bounds.height()));
} else {
widget_->SetBounds(bounds);
}
UpdateHostWindowOrigin();
notify_bounds_changes_ = true;
}
bool ShellSurface::OnPreWidgetCommit() {
if (!widget_ && GetEnabled()) {
// Defer widget creation and commit until surface has contents.
if (host_window()->bounds().IsEmpty() &&
root_surface()->surface_hierarchy_content_bounds().IsEmpty()) {
Configure();
if (initial_show_state_ != ui::SHOW_STATE_MINIMIZED)
needs_layout_on_show_ = true;
}
CreateShellSurfaceWidget(initial_show_state_);
}
// Apply the accumulated pending origin offset to reflect acknowledged
// configure requests.
origin_offset_ += pending_origin_offset_;
pending_origin_offset_ = gfx::Vector2d();
// Update resize direction to reflect acknowledged configure requests.
resize_component_ = pending_resize_component_;
if (config_waiting_for_commit_) {
UpdateLocalSurfaceIdFromParent(config_waiting_for_commit_->viz_surface_id);
}
config_waiting_for_commit_.reset();
return true;
}
void ShellSurface::ShowWidget(bool activate) {
ShellSurfaceBase::ShowWidget(activate);
// Now that the shell surface is ready, make sure it has up to date occlusion
// state.
CHECK(IsReady());
occlusion_observer_->MaybeConfigure(root_surface()->window());
}
std::unique_ptr<views::NonClientFrameView>
ShellSurface::CreateNonClientFrameView(views::Widget* widget) {
ash::WindowState* window_state =
ash::WindowState::Get(widget->GetNativeWindow());
window_state->SetDelegate(std::make_unique<CustomWindowStateDelegate>(this));
return CreateNonClientFrameViewInternal(widget);
}
void ShellSurface::SetRootSurface(Surface* root_surface) {
ShellSurfaceBase::SetRootSurface(root_surface);
if (root_surface) {
occlusion_observer_.emplace(this, root_surface->window());
}
}
////////////////////////////////////////////////////////////////////////////////
// ui::LayerOwner::Observer overrides:
void ShellSurface::OnLayerRecreated(ui::Layer* old_layer) {
DCHECK(!old_layer_);
// Layer recreation may happen before the first shell_surface commit with
// content. Disregard the old_layer in this case as the old_layer can't show
// anything.
if (old_layer->GetSurfaceId()) {
old_layer_ = old_layer->AsWeakPtr();
// TODO(b/319939913): Remove this log when the issue is fixed.
old_layer_->SetName(old_layer_->name() + "-old-has-surface");
} else {
old_layer->SetName(old_layer->name() + "-old-no-surface");
}
CHECK(old_layer->parent());
CHECK(host_window()->layer()->parent());
}
////////////////////////////////////////////////////////////////////////////////
// ShellSurface, private:
void ShellSurface::SetParentWindow(aura::Window* new_parent) {
if (new_parent && widget_) {
const aura::Window* window = widget_->GetNativeWindow();
const aura::Window* ancestor = new_parent;
while (ancestor) {
if (ancestor == window) {
// Some apps try to be their own parent, e.g. crbug/1210235, or parent
// to its ancestors, e.g., b/342265753. Ignore them to prevent chrome
// from locking up/crashing.
auto* app_id = GetShellApplicationId(host_window());
LOG(WARNING) << "Client attempts to parent to itself or its transient "
"ancestors: app_id="
<< app_id;
return;
}
auto* transient_window_manager =
wm::TransientWindowManager::GetIfExists(ancestor);
ancestor = transient_window_manager
? transient_window_manager->transient_parent()
: nullptr;
}
}
if (parent()) {
parent()->RemoveObserver(this);
if (widget_) {
aura::Window* child_window = widget_->GetNativeWindow();
wm::TransientWindowManager::GetOrCreate(child_window)
->set_parent_controls_visibility(false);
wm::RemoveTransientChild(parent(), child_window);
}
}
SetParentInternal(new_parent);
if (parent()) {
parent()->AddObserver(this);
MaybeMakeTransient();
}
}
void ShellSurface::MaybeMakeTransient() {
if (!parent() || !widget_)
return;
aura::Window* child_window = widget_->GetNativeWindow();
wm::AddTransientChild(parent(), child_window);
// In the case of activatable non-popups, we also want the parent to control
// the child's visibility.
if (!widget_->is_top_level() || !widget_->CanActivate())
return;
wm::TransientWindowManager::GetOrCreate(child_window)
->set_parent_controls_visibility(true);
}
void ShellSurface::Configure(bool ends_drag) {
// Delay configure callback if |scoped_configure_| is set. But if
// |widget_| is not set yet then it ignores |scoped_configure_| so that an
// initial configure can be sent.
if (widget_ && scoped_configure_) {
scoped_configure_->set_needs_configure();
return;
}
gfx::Vector2d origin_offset = pending_origin_offset_accumulator_;
pending_origin_offset_accumulator_ = gfx::Vector2d();
auto* window_state =
widget_ ? ash::WindowState::Get(widget_->GetNativeWindow()) : nullptr;
int resize_component = HTCAPTION;
// If surface is being resized, save the resize direction.
if (window_state && window_state->is_dragged() && !ends_drag)
resize_component = window_state->drag_details()->window_component;
uint32_t serial = 0;
if (!configure_callback_.is_null()) {
if (window_state) {
auto occlusion_state = occlusion_observer_->state();
auto restore_state_type = std::optional<chromeos::WindowStateType>{
window_state->GetRestoreWindowState()};
serial = configure_callback_.Run(
GetClientBoundsInScreen(widget_), window_state->GetStateType(),
IsResizing(), widget_->IsActive(), origin_offset,
pending_raster_scale_, occlusion_state, restore_state_type);
} else {
auto state = chromeos::ToWindowStateType(initial_show_state_);
auto occlusion_state =
occlusion_observer_->GetInitialStateForConfigure(state);
gfx::Rect bounds = GetInitialBoundsForState(state);
serial = configure_callback_.Run(bounds, state, false, false,
origin_offset, pending_raster_scale_,
occlusion_state, std::nullopt);
}
}
if (!serial) {
pending_origin_offset_ += origin_offset;
pending_resize_component_ = resize_component;
return;
}
if (widget_ && host_window()->GetLocalSurfaceId().parent_sequence_number() !=
GetCurrentLocalSurfaceId().parent_sequence_number()) {
host_window()->layer()->SetShowSurface(
host_window()->GetSurfaceId(), GetClientBoundsInScreen(widget_).size(),
SK_ColorWHITE, cc::DeadlinePolicy::UseDefaultDeadline(),
/*stretch_content_to_fill_bounds=*/true);
host_window()->layer()->SetOldestAcceptableFallback(GetSurfaceId());
}
// Apply origin offset and resize component at the first Commit() after this
// configure request has been acknowledged.
// `host_window()` is changing the window properties of `shell_surface`,
// controlled by a wayland client. `shell_surface` needs to know that the
// advanced LocalSurfaceId can be embedded, by looking at the config `serial`.
pending_configs_.push_back(std::make_unique<Config>(
serial, origin_offset, resize_component,
host_window()->GetLocalSurfaceId(), std::move(old_layer_),
std::move(configure_compositor_lock_)));
LOG_IF(WARNING, pending_configs_.size() > 100)
<< "Number of pending configure acks for shell surface has reached: "
<< pending_configs_.size();
for (auto& observer : observers_)
observer.OnConfigure(serial);
}
bool ShellSurface::GetCanResizeFromSizeConstraints() const {
// Both the default min and max sizes are empty and windows must be resizable
// in that case.
return (requested_minimum_size_.IsEmpty() ||
requested_minimum_size_ != requested_maximum_size_);
}
bool ShellSurface::AttemptToStartDrag(int component) {
ash::WindowState* window_state =
ash::WindowState::Get(widget_->GetNativeWindow());
// Ignore if surface is already being dragged.
if (window_state->is_dragged()) {
return true;
}
aura::Window* target = widget_->GetNativeWindow();
ash::ToplevelWindowEventHandler* toplevel_handler =
ash::Shell::Get()->toplevel_window_event_handler();
aura::Window* mouse_pressed_handler =
target->GetHost()->dispatcher()->mouse_pressed_handler();
// Start dragging only if:
// 1) touch guesture is in progress.
// 2) mouse was pressed on the target or its subsurfaces.
aura::Window* gesture_target = toplevel_handler->gesture_target();
if (!gesture_target && !mouse_pressed_handler &&
target->Contains(mouse_pressed_handler)) {
return false;
}
bool started = false;
if (gesture_target) {
gfx::PointF location = toplevel_handler->event_location_in_gesture_target();
aura::Window::ConvertPointToTarget(
gesture_target, widget_->GetNativeWindow()->GetRootWindow(), &location);
started =
toplevel_handler->AttemptToStartDrag(target, location, component, {});
} else {
gfx::Point location = aura::Env::GetInstance()->last_mouse_location();
::wm::ConvertPointFromScreen(widget_->GetNativeWindow()->GetRootWindow(),
&location);
started = toplevel_handler->AttemptToStartDrag(
target, gfx::PointF(location), component, {});
}
// Notify client that resizing state has changed.
if (IsResizing())
Configure();
return started;
}
void ShellSurface::EndDrag() {
if (!IsMoveComponent(resize_component_))
Configure(/*ends_drag=*/true);
}
gfx::Rect ShellSurface::GetInitialBoundsForState(
const chromeos::WindowStateType state) const {
if (state == chromeos::WindowStateType::kMaximized) {
return GetDisplayForInitialBounds().work_area();
}
if (IsFullscreenOrPinnedWindowStateType(state)) {
return GetDisplayForInitialBounds().bounds();
}
if (initial_bounds_) {
// TODO(oshima): Consider just using the `initial_bounds_`.
return gfx::Rect(initial_bounds_->origin(), {});
}
return gfx::Rect();
}
display::Display ShellSurface::GetDisplayForInitialBounds() const {
auto* screen = display::Screen::GetScreen();
display::Display display = screen->GetDisplayForNewWindows();
// Use `pending_display_id_` as this is called before first commit.
if (!screen->GetDisplayWithDisplayId(pending_display_id_, &display) &&
initial_bounds_ && !initial_bounds_->IsEmpty()) {
display = screen->GetDisplayMatching(*initial_bounds_);
}
return display;
}
void ShellSurface::UpdateLayerSurfaceRange(
ui::Layer* layer,
const viz::LocalSurfaceId& current_lsi) {
auto& layer_lsi = layer->GetSurfaceId()->local_surface_id();
DCHECK_EQ(layer_lsi.embed_token(), current_lsi.embed_token());
// `layer` with old parent seq should be consumed by config acks and not
// appear here.
DCHECK_LE(
layer_lsi.parent_sequence_number() - current_lsi.parent_sequence_number(),
(1u << 31));
// child seq is controlled by client so it should always be newer.
DCHECK_LE(
current_lsi.child_sequence_number() - layer_lsi.child_sequence_number(),
(1u << 31));
if (layer_lsi.parent_sequence_number() !=
current_lsi.parent_sequence_number()) {
// `current_lsi` is behind, specify a surface range, and stretch content.
if (layer_lsi.child_sequence_number() !=
current_lsi.child_sequence_number()) {
layer->SetShowSurface(
viz::SurfaceId(frame_sink_id_, {layer_lsi.parent_sequence_number(),
current_lsi.child_sequence_number(),
current_lsi.embed_token()}),
SK_ColorWHITE, cc::DeadlinePolicy::UseDefaultDeadline(),
true /* stretch_content_to_fill_bounds */);
}
layer->SetOldestAcceptableFallback(
viz::SurfaceId(frame_sink_id_, current_lsi));
} else {
viz::SurfaceId surface_id(frame_sink_id_, current_lsi);
// Update the surface only when the surface id changes or the surface still
// have an fallback, which indicates that the change needs to be
// synchronized due to size change or scale change.
if (!layer->GetSurfaceId() || *layer->GetSurfaceId() != surface_id ||
layer->GetOldestAcceptableFallback()) {
// `current_lsi` has caught up to `layer`. Allow the shell_surface to
// modify the surface layer bounds, clear the oldest fallback and disable
// stretch.
layer->SetShowSurface(surface_id, layer->bounds().size(), SK_ColorWHITE,
cc::DeadlinePolicy::UseDefaultDeadline(),
false /* stretch_content_to_fill_bounds */);
layer->SetOldestAcceptableFallback(viz::SurfaceId{});
}
}
}
void ShellSurface::OnWidgetScreenPositionChanged() {
if (!origin_change_callback_.is_null()) {
origin_change_callback_.Run(GetClientBoundsInScreen(widget_).origin());
}
// Ensure the host window's origin is kept in sync with the widget.
UpdateHostWindowOrigin();
}
} // namespace exo