#include "ash/wm/splitview/split_view_controller.h"
#include <algorithm>
#include <array>
#include <cmath>
#include <cstdint>
#include <limits>
#include <optional>
#include <vector>
#include "ash/accessibility/accessibility_controller.h"
#include "ash/constants/ash_features.h"
#include "ash/display/screen_orientation_controller.h"
#include "ash/display/window_tree_host_manager.h"
#include "ash/keyboard/ui/keyboard_ui_controller.h"
#include "ash/public/cpp/metrics_util.h"
#include "ash/public/cpp/window_properties.h"
#include "ash/root_window_controller.h"
#include "ash/root_window_settings.h"
#include "ash/screen_util.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/style/ash_color_provider.h"
#include "ash/wm/desks/desks_controller.h"
#include "ash/wm/desks/desks_util.h"
#include "ash/wm/float/float_controller.h"
#include "ash/wm/mru_window_tracker.h"
#include "ash/wm/overview/delayed_animation_observer_impl.h"
#include "ash/wm/overview/overview_controller.h"
#include "ash/wm/overview/overview_grid.h"
#include "ash/wm/overview/overview_item.h"
#include "ash/wm/overview/overview_metrics.h"
#include "ash/wm/overview/overview_types.h"
#include "ash/wm/overview/overview_utils.h"
#include "ash/wm/snap_group/snap_group.h"
#include "ash/wm/snap_group/snap_group_controller.h"
#include "ash/wm/snap_group/snap_group_metrics.h"
#include "ash/wm/splitview/auto_snap_controller.h"
#include "ash/wm/splitview/split_view_constants.h"
#include "ash/wm/splitview/split_view_divider.h"
#include "ash/wm/splitview/split_view_metrics_controller.h"
#include "ash/wm/splitview/split_view_observer.h"
#include "ash/wm/splitview/split_view_overview_session.h"
#include "ash/wm/splitview/split_view_utils.h"
#include "ash/wm/tablet_mode/tablet_mode_window_state.h"
#include "ash/wm/window_positioning_utils.h"
#include "ash/wm/window_properties.h"
#include "ash/wm/window_resizer.h"
#include "ash/wm/window_restore/window_restore_controller.h"
#include "ash/wm/window_state.h"
#include "ash/wm/window_transient_descendant_iterator.h"
#include "ash/wm/window_util.h"
#include "ash/wm/wm_metrics.h"
#include "base/auto_reset.h"
#include "base/check_deref.h"
#include "base/containers/flat_map.h"
#include "base/containers/span.h"
#include "base/debug/crash_logging.h"
#include "base/debug/dump_without_crashing.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ref.h"
#include "base/metrics/histogram_macros.h"
#include "base/metrics/user_metrics.h"
#include "base/notreached.h"
#include "base/time/time.h"
#include "chromeos/ui/base/app_types.h"
#include "chromeos/ui/base/window_properties.h"
#include "chromeos/ui/base/window_state_type.h"
#include "chromeos/ui/frame/caption_buttons/snap_controller.h"
#include "components/app_restore/desk_template_read_handler.h"
#include "components/app_restore/window_properties.h"
#include "ui/aura/client/aura_constants.h"
#include "ui/aura/window_delegate.h"
#include "ui/base/ime/ash/ime_bridge.h"
#include "ui/base/ime/input_method.h"
#include "ui/compositor/compositor_metrics_tracker.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/layer_animator.h"
#include "ui/compositor/presentation_time_recorder.h"
#include "ui/display/screen.h"
#include "ui/display/types/display_constants.h"
#include "ui/gfx/animation/slide_animation.h"
#include "ui/gfx/animation/tween.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/transform_util.h"
#include "ui/views/animation/compositor_animation_runner.h"
#include "ui/views/widget/widget.h"
#include "ui/wm/core/coordinate_conversion.h"
#include "ui/wm/core/shadow_controller.h"
#include "ui/wm/core/window_util.h"
#include "ui/wm/public/activation_client.h"
namespace ash {
namespace {
using chromeos::WindowStateType;
constexpr std::array<float, 5> kFixedPositionRatios = {
0.f,
chromeos::kOneThirdSnapRatio,
chromeos::kDefaultSnapRatio,
chromeos::kTwoThirdSnapRatio,
1.0f,
};
constexpr float kBlackScrimFadeInRatio = 0.1f;
constexpr float kBlackScrimOpacity = 0.4f;
constexpr int kSplitViewThresholdPixelsPerSec = 72;
constexpr base::TimeDelta kSplitViewChunkTime = base::Milliseconds(500);
constexpr char kDividerAnimationSmoothness[] =
"Ash.SplitViewResize.AnimationSmoothness.DividerAnimation";
constexpr char kTabletSplitViewResizeSingleHistogram[] =
"Ash.SplitViewResize.PresentationTime.TabletMode.SingleWindow";
constexpr char kTabletSplitViewResizeMultiHistogram[] =
"Ash.SplitViewResize.PresentationTime.TabletMode.MultiWindow";
constexpr char kTabletSplitViewResizeWithOverviewHistogram[] =
"Ash.SplitViewResize.PresentationTime.TabletMode.WithOverview";
constexpr char kTabletSplitViewResizeSingleMaxLatencyHistogram[] =
"Ash.SplitViewResize.PresentationTime.MaxLatency.TabletMode.SingleWindow";
constexpr char kTabletSplitViewResizeMultiMaxLatencyHistogram[] =
"Ash.SplitViewResize.PresentationTime.MaxLatency.TabletMode.MultiWindow";
constexpr char kTabletSplitViewResizeWithOverviewMaxLatencyHistogram[] =
"Ash.SplitViewResize.PresentationTime.MaxLatency.TabletMode.WithOverview";
base::Time g_multi_display_split_view_start_time;
bool g_use_fast_resize_for_testing = false;
bool InTabletMode() {
return display::Screen::Get()->InTabletMode();
}
bool IsExactlyOneRootInSplitView() {
const aura::Window::Windows all_root_windows = Shell::GetAllRootWindows();
return 1 ==
std::ranges::count_if(all_root_windows, [](aura::Window* root_window) {
return SplitViewController::Get(root_window)->InSplitViewMode();
});
}
ui::InputMethod* GetCurrentInputMethod() {
if (auto* bridge = IMEBridge::Get()) {
if (auto* handler = bridge->GetInputContextHandler())
return handler->GetInputMethod();
}
return nullptr;
}
void RemoveSnappingWindowFromOverviewIfApplicable(
OverviewSession* overview_session,
aura::Window* window) {
if (!overview_session) {
return;
}
OverviewItemBase* item = overview_session->GetOverviewItemForWindow(window);
if (!item) {
return;
}
item->EnsureVisible();
item->RestoreWindow(false, true);
overview_session->RemoveItem(item);
}
void TriggerWMEventToSnapWindow(WindowState* window_state,
WMEventType event_type) {
CHECK(event_type == WM_EVENT_SNAP_PRIMARY ||
event_type == WM_EVENT_SNAP_SECONDARY);
const WindowSnapWMEvent window_event(
event_type,
window_state->snap_ratio().value_or(chromeos::kDefaultSnapRatio));
window_state->OnWMEvent(&window_event);
}
bool DidInSplitViewWindowChange(aura::Window* window,
SplitViewController* split_view_controller,
SnapPosition snap_position) {
if (!split_view_controller->IsWindowInSplitView(window)) {
return false;
}
const auto* window_state = WindowState::Get(window);
if (window_state->GetStateType() !=
GetWindowStateTypeFromSnapPosition(snap_position)) {
return true;
}
std::optional<float> snap_ratio = window_state->snap_ratio();
const auto* window_state_in_current_snap_position =
WindowState::Get(split_view_controller->GetSnappedWindow(snap_position));
const bool same_snap_ratio =
snap_ratio && window_state_in_current_snap_position &&
*snap_ratio == window_state_in_current_snap_position->snap_ratio();
return !same_snap_ratio;
}
}
class SplitViewController::DividerSnapAnimation
: public gfx::SlideAnimation,
public gfx::AnimationDelegate {
public:
DividerSnapAnimation(SplitViewController* split_view_controller,
int starting_position,
int ending_position,
base::TimeDelta duration,
gfx::Tween::Type tween_type)
: gfx::SlideAnimation(this),
split_view_controller_(split_view_controller),
starting_position_(starting_position),
ending_position_(ending_position) {
SetSlideDuration(duration);
SetTweenType(tween_type);
aura::Window* window = split_view_controller->primary_window()
? split_view_controller->primary_window()
: split_view_controller->secondary_window();
DCHECK(window);
views::Widget* widget = views::Widget::GetWidgetForNativeWindow(window);
if (!widget)
return;
gfx::AnimationContainer* container = new gfx::AnimationContainer();
container->SetAnimationRunner(
std::make_unique<views::CompositorAnimationRunner>(widget, FROM_HERE));
SetContainer(container);
tracker_.emplace(
widget->GetCompositor()->RequestNewCompositorMetricsTracker());
tracker_->Start(
metrics_util::ForSmoothnessV3(base::BindRepeating([](int smoothness) {
UMA_HISTOGRAM_PERCENTAGE(kDividerAnimationSmoothness, smoothness);
})));
}
DividerSnapAnimation(const DividerSnapAnimation&) = delete;
DividerSnapAnimation& operator=(const DividerSnapAnimation&) = delete;
~DividerSnapAnimation() override = default;
int ending_position() const { return ending_position_; }
private:
void AnimationEnded(const gfx::Animation* animation) override {
DCHECK(split_view_controller_->InSplitViewMode());
DCHECK(!split_view_controller_->IsResizingWithDivider());
DCHECK_EQ(ending_position_, split_view_controller_->GetDividerPosition());
split_view_controller_->EndResizeWithDividerImpl();
split_view_controller_->EndSplitViewAfterResizingAtEdgeIfAppropriate();
if (tracker_)
tracker_->Stop();
}
void AnimationProgressed(const gfx::Animation* animation) override {
DCHECK(split_view_controller_->InSplitViewMode());
DCHECK(!split_view_controller_->IsResizingWithDivider());
const int divider_position_before_tween =
split_view_controller_->GetDividerPosition();
split_view_controller_->split_view_divider()->SetDividerPosition(
CurrentValueBetween(starting_position_, ending_position_));
const int divider_position_after_tween =
split_view_controller_->GetDividerPosition();
split_view_controller_->NotifyDividerPositionChanged();
const int divider_position_after_notify =
split_view_controller_->GetDividerPosition();
split_view_controller_->UpdateSnappedWindowsAndDividerBounds();
const int divider_position_after_update =
split_view_controller_->GetDividerPosition();
if (is_animating()) {
base::flat_map<SplitViewController::TabletResizeMode, std::string>
resize_mode_as_string = {
{SplitViewController::TabletResizeMode::kNormal, "kNormal"},
{SplitViewController::TabletResizeMode::kFast, "kFast"},
};
SCOPED_CRASH_KEY_STRING32(
"b327685487", "tablet_resize_mode",
resize_mode_as_string[split_view_controller_->tablet_resize_mode_]);
SCOPED_CRASH_KEY_BOOL("b327685487", "in_tablet_mode", InTabletMode());
SCOPED_CRASH_KEY_BOOL(
"b327685487", "has_divider_widget",
!!split_view_controller_->split_view_divider()->divider_widget());
SCOPED_CRASH_KEY_BOOL("b327685487", "in_split_view",
split_view_controller_->InSplitViewMode());
SCOPED_CRASH_KEY_BOOL("b327685487", "is_divider_resizing",
split_view_controller_->IsResizingWithDivider());
SCOPED_CRASH_KEY_NUMBER("b327685487", "before_tween",
divider_position_before_tween);
SCOPED_CRASH_KEY_NUMBER("b327685487", "after_tween",
divider_position_after_tween);
SCOPED_CRASH_KEY_NUMBER("b327685487", "after_notify",
divider_position_after_notify);
SCOPED_CRASH_KEY_NUMBER("b327685487", "after_update",
divider_position_after_update);
SCOPED_CRASH_KEY_NUMBER("b327685487", "starting_position",
starting_position_);
SCOPED_CRASH_KEY_NUMBER("b327685487", "ending_position",
ending_position_);
split_view_controller_->UpdateResizeBackdrop();
split_view_controller_->SetWindowsTransformDuringResizing();
}
}
void AnimationCanceled(const gfx::Animation* animation) override {
if (tracker_)
tracker_->Cancel();
}
raw_ptr<SplitViewController> split_view_controller_;
int starting_position_;
int ending_position_;
std::optional<ui::ThroughputTracker> tracker_;
};
class SplitViewController::ToBeSnappedWindowsObserver
: public aura::WindowObserver,
public WindowStateObserver {
public:
explicit ToBeSnappedWindowsObserver(
SplitViewController* split_view_controller)
: split_view_controller_(split_view_controller) {}
ToBeSnappedWindowsObserver(const ToBeSnappedWindowsObserver&) = delete;
ToBeSnappedWindowsObserver& operator=(const ToBeSnappedWindowsObserver&) =
delete;
~ToBeSnappedWindowsObserver() override {
for (auto& to_be_snapped_window : to_be_snapped_windows_) {
if (aura::Window* window = to_be_snapped_window.second.window) {
window->RemoveObserver(this);
WindowState::Get(window)->RemoveObserver(this);
}
}
to_be_snapped_windows_.clear();
}
void AddToBeSnappedWindow(aura::Window* window,
SnapPosition snap_position,
WindowSnapActionSource snap_action_source) {
if (DidInSplitViewWindowChange(window, split_view_controller_,
snap_position)) {
split_view_controller_->AttachToBeSnappedWindow(window, snap_position,
snap_action_source);
return;
}
aura::Window* old_window = to_be_snapped_windows_[snap_position].window;
if (old_window == window) {
return;
}
if (old_window) {
to_be_snapped_windows_.erase(snap_position);
WindowState::Get(old_window)->RemoveObserver(this);
old_window->RemoveObserver(this);
}
WindowState* window_state = WindowState::Get(window);
if (window_state->GetStateType() ==
GetWindowStateTypeFromSnapPosition(snap_position)) {
split_view_controller_->AttachToBeSnappedWindow(window, snap_position,
snap_action_source);
split_view_controller_->OnWindowSnapped(window,
std::nullopt,
snap_action_source);
} else {
to_be_snapped_windows_[snap_position] =
WindowAndSnapSourceInfo{window, snap_action_source};
window_state->AddObserver(this);
window->AddObserver(this);
}
}
bool IsObserving(const aura::Window* window) const {
return FindWindow(window) != to_be_snapped_windows_.end();
}
void OnWindowDestroying(aura::Window* window) override {
auto iter = FindWindow(window);
DCHECK(iter != to_be_snapped_windows_.end());
window->RemoveObserver(this);
WindowState::Get(window)->RemoveObserver(this);
to_be_snapped_windows_.erase(iter);
}
void OnPreWindowStateTypeChange(WindowState* window_state,
WindowStateType old_type) override {
aura::Window* window = window_state->window();
auto iter = FindWindow(window);
DCHECK(iter != to_be_snapped_windows_.end());
SnapPosition snap_position = iter->first;
if (window_state->GetStateType() ==
GetWindowStateTypeFromSnapPosition(snap_position)) {
const auto cached_snap_action_source = iter->second.snap_action_source;
to_be_snapped_windows_.erase(iter);
window_state->RemoveObserver(this);
window->RemoveObserver(this);
split_view_controller_->AttachToBeSnappedWindow(
window, snap_position, cached_snap_action_source);
}
}
private:
struct WindowAndSnapSourceInfo {
raw_ptr<aura::Window> window = nullptr;
WindowSnapActionSource snap_action_source =
WindowSnapActionSource::kNotSpecified;
};
base::flat_map<SnapPosition, WindowAndSnapSourceInfo>::const_iterator
FindWindow(const aura::Window* window) const {
for (auto iter = to_be_snapped_windows_.begin();
iter != to_be_snapped_windows_.end(); iter++) {
if (iter->second.window == window) {
return iter;
}
}
return to_be_snapped_windows_.end();
}
const raw_ptr<SplitViewController> split_view_controller_;
base::flat_map<SnapPosition, WindowAndSnapSourceInfo> to_be_snapped_windows_;
};
class SplitViewController::TabDragWindowObserver : public aura::WindowObserver {
public:
TabDragWindowObserver(SplitViewController* split_view_controller,
aura::Window* drag_window,
SnapPosition desired_snap_position,
const gfx::Point& last_location_in_screen,
WindowSnapActionSource snap_action_source)
: split_view_controller_(CHECK_DEREF(split_view_controller)),
drag_window_(drag_window),
desired_snap_position_(desired_snap_position),
last_location_in_screen_(last_location_in_screen),
snap_action_source_(snap_action_source) {
CHECK(drag_window_);
CHECK(window_util::IsDraggingTabs(drag_window_));
drag_window_->AddObserver(this);
}
~TabDragWindowObserver() override {
if (drag_window_) {
drag_window_->RemoveObserver(this);
}
}
void OnWindowDestroying(aura::Window* window) override {
CHECK_EQ(window, drag_window_);
CHECK(window_util::IsDraggingTabs(drag_window_));
CHECK(drag_window_->is_destroying());
OnTabDragEnded();
}
void OnWindowPropertyChanged(aura::Window* window,
const void* key,
intptr_t old) override {
CHECK_EQ(window, drag_window_);
if (key == kIsDraggingTabsKey &&
!window_util::IsDraggingTabs(drag_window_)) {
OnTabDragEnded();
}
}
private:
void OnTabDragEnded() {
drag_window_->RemoveObserver(this);
auto* window = drag_window_.get();
drag_window_ = nullptr;
split_view_controller_->EndWindowDragImpl(
window, window->is_destroying(), desired_snap_position_,
last_location_in_screen_, snap_action_source_);
}
const raw_ref<SplitViewController> split_view_controller_;
raw_ptr<aura::Window> drag_window_;
const SnapPosition desired_snap_position_;
const gfx::Point last_location_in_screen_;
const WindowSnapActionSource snap_action_source_;
};
SplitViewController* SplitViewController::Get(const aura::Window* window) {
DCHECK(window);
DCHECK(window->GetRootWindow());
DCHECK(RootWindowController::ForWindow(window));
return RootWindowController::ForWindow(window)->split_view_controller();
}
SplitViewController::SplitViewController(aura::Window* root_window)
: root_window_(root_window),
to_be_snapped_windows_observer_(
std::make_unique<ToBeSnappedWindowsObserver>(this)),
split_view_divider_(this),
split_view_metrics_controller_(
std::make_unique<SplitViewMetricsController>(this)) {
Shell::Get()->accessibility_controller()->AddObserver(this);
}
SplitViewController::~SplitViewController() {
if (AccessibilityController* a11y_controller =
Shell::Get()->accessibility_controller()) {
a11y_controller->RemoveObserver(this);
}
EndSplitView(EndReason::kRootWindowDestroyed);
}
int SplitViewController::GetDividerPosition() const {
return split_view_divider_.divider_position();
}
bool SplitViewController::IsResizingWithDivider() const {
return split_view_divider_.HasDividerWidget() &&
split_view_divider_.is_resizing_with_divider();
}
bool SplitViewController::InSplitViewMode() const {
return state_ != State::kNoSnap;
}
bool SplitViewController::InClamshellSplitViewMode() const {
return InSplitViewMode() && !InTabletMode();
}
bool SplitViewController::InTabletSplitViewMode() const {
return InSplitViewMode() && InTabletMode();
}
bool SplitViewController::CanSnapWindow(aura::Window* window,
float snap_ratio) const {
if (!ShouldAllowSplitView())
return false;
if (!WindowState::Get(window)->CanSnapOnDisplay(
display::Screen::Get()->GetDisplayNearestWindow(
const_cast<aura::Window*>(root_window_.get())))) {
return false;
}
const bool is_to_be_restored_window =
window == WindowRestoreController::Get()->to_be_snapped_window();
if (!is_to_be_restored_window && !wm::CanActivateWindow(window)) {
return false;
}
const int divider_delta =
ShouldConsiderDivider() ? kSplitviewDividerShortSideLength / 2 : 0;
return GetMinimumWindowLength(window, IsLayoutHorizontal(window)) <=
GetDividerPositionUpperLimit(root_window_) * snap_ratio -
divider_delta;
}
bool SplitViewController::CanKeepCurrentSnapRatio(
aura::Window* snapped_window) const {
return CanSnapWindow(snapped_window,
WindowState::Get(snapped_window)
->snap_ratio()
.value_or(chromeos::kDefaultSnapRatio));
}
std::optional<float> SplitViewController::ComputeAutoSnapRatio(
aura::Window* window) {
aura::Window* default_window = GetDefaultSnappedWindow();
std::optional<float> default_window_snap_ratio =
default_window ? WindowState::Get(default_window)->snap_ratio()
: std::nullopt;
if (!default_window_snap_ratio) {
return CanSnapWindow(window, chromeos::kDefaultSnapRatio)
? std::make_optional(chromeos::kDefaultSnapRatio)
: std::nullopt;
}
static constexpr auto kOppositeRatiosMap =
base::MakeFixedFlatMap<float, float>(
{{chromeos::kOneThirdSnapRatio, chromeos::kTwoThirdSnapRatio},
{chromeos::kDefaultSnapRatio, chromeos::kDefaultSnapRatio},
{chromeos::kTwoThirdSnapRatio, chromeos::kOneThirdSnapRatio}});
auto it = kOppositeRatiosMap.find(*default_window_snap_ratio);
if (it == kOppositeRatiosMap.end()) {
return CanSnapWindow(window, chromeos::kDefaultSnapRatio)
? std::make_optional(chromeos::kDefaultSnapRatio)
: std::nullopt;
}
float snap_ratio = it->second;
if (CanSnapWindow(window, snap_ratio)) {
return snap_ratio;
}
if (snap_ratio == chromeos::kOneThirdSnapRatio &&
CanSnapWindow(window, chromeos::kDefaultSnapRatio) &&
CanSnapWindow(default_window, chromeos::kDefaultSnapRatio)) {
return chromeos::kDefaultSnapRatio;
}
return std::nullopt;
}
bool SplitViewController::WillStartPartialOverview(aura::Window* window) const {
const bool can_start_in_tablet = InTabletMode() && !IsInOverviewSession();
const bool can_start_in_clamshell =
CanStartSplitViewOverviewSessionInClamshell(
window, WindowState::Get(window)->snap_action_source().value_or(
WindowSnapActionSource::kNotSpecified));
return (can_start_in_tablet || can_start_in_clamshell) &&
!DesksController::Get()->animation() &&
!!primary_window_ != !!secondary_window_;
}
void SplitViewController::SnapWindow(aura::Window* window,
SnapPosition snap_position,
WindowSnapActionSource snap_action_source,
bool activate_window,
float snap_ratio) {
DCHECK(window);
DCHECK_NE(snap_position, SnapPosition::kNone);
DCHECK(CanSnapWindow(window, snap_ratio));
if (IsDividerAnimating()) {
StopSnapAnimation();
}
OverviewSession* overview_session = GetOverviewSession();
if (activate_window ||
(overview_session &&
overview_session->IsWindowActiveWindowBeforeOverview(window))) {
to_be_activated_window_ = window;
}
to_be_snapped_windows_observer_->AddToBeSnappedWindow(window, snap_position,
snap_action_source);
if (root_window_ != window->GetRootWindow()) {
window_util::MoveWindowToDisplay(
window,
display::Screen::Get()->GetDisplayNearestWindow(root_window_).id());
}
const WindowSnapWMEvent event(snap_position == SnapPosition::kPrimary
? WM_EVENT_SNAP_PRIMARY
: WM_EVENT_SNAP_SECONDARY,
snap_ratio, snap_action_source);
WindowState::Get(window)->OnWMEvent(&event);
base::RecordAction(base::UserMetricsAction("SplitView_SnapWindow"));
}
bool SplitViewController::ShouldWindowBeManagedBySplitViewController(
aura::Window* window,
WindowSnapActionSource snap_action_source,
WindowSnapGrouping grouping_request) const {
if (!ShouldAllowSplitView()) {
return false;
}
const bool in_overview = IsInOverviewSession();
const int32_t window_id =
window->GetProperty(app_restore::kRestoreWindowIdKey);
if (in_overview &&
window == WindowRestoreController::Get()->to_be_snapped_window() &&
app_restore::DeskTemplateReadHandler::Get()->GetWindowInfo(window_id)) {
return false;
}
return InTabletMode() || in_overview ||
grouping_request == WindowSnapGrouping::kGrouped ||
ShouldConsiderWindowForSplitViewSetupView(window,
snap_action_source) ||
snap_action_source == WindowSnapActionSource::kKeyboardShortcutToSnap;
}
void SplitViewController::OnSnapEvent(aura::Window* window,
WMEventType event_type,
WindowSnapActionSource snap_action_source,
WindowSnapGrouping grouping_request) {
CHECK(event_type == WM_EVENT_SNAP_PRIMARY ||
event_type == WM_EVENT_SNAP_SECONDARY);
if (to_be_snapped_windows_observer_->IsObserving(window)) {
return;
}
if (ShouldWindowBeManagedBySplitViewController(window, snap_action_source,
grouping_request)) {
const SnapPosition to_snap_position = event_type == WM_EVENT_SNAP_PRIMARY
? SnapPosition::kPrimary
: SnapPosition::kSecondary;
to_be_snapped_windows_observer_->AddToBeSnappedWindow(
window, to_snap_position, snap_action_source);
}
}
void SplitViewController::AttachToBeSnappedWindow(
aura::Window* window,
SnapPosition snap_position,
WindowSnapActionSource snap_action_source) {
UpdateSnappingWindowTransformedBounds(window);
OverviewSession* overview_session = GetOverviewSession();
RemoveSnappingWindowFromOverviewIfApplicable(overview_session, window);
if (state_ == State::kNoSnap) {
default_snap_position_ = snap_position;
splitview_start_time_ = base::Time::Now();
if (IsExactlyOneRootInSplitView()) {
base::RecordAction(
base::UserMetricsAction("SplitView_MultiDisplaySplitView"));
g_multi_display_split_view_start_time = splitview_start_time_;
}
}
aura::Window* previous_snapped_window = nullptr;
aura::Window* other_window = nullptr;
if (snap_position == SnapPosition::kPrimary) {
if (primary_window_ != window) {
previous_snapped_window = primary_window_;
StopObserving(SnapPosition::kPrimary);
primary_window_ = window;
}
if (secondary_window_ == window) {
secondary_window_ = nullptr;
default_snap_position_ = SnapPosition::kPrimary;
}
other_window = secondary_window_;
} else if (snap_position == SnapPosition::kSecondary) {
if (secondary_window_ != window) {
previous_snapped_window = secondary_window_;
StopObserving(SnapPosition::kSecondary);
secondary_window_ = window;
}
if (primary_window_ == window) {
primary_window_ = nullptr;
default_snap_position_ = SnapPosition::kSecondary;
}
other_window = primary_window_;
}
StartObserving(window);
DCHECK_EQ(overview_session, GetOverviewSession());
if (previous_snapped_window && overview_session) {
InsertWindowToOverview(previous_snapped_window);
overview_session->GetOverviewItemForWindow(previous_snapped_window)
->OnOverviewItemDragEnded(true);
}
bool do_snap_animation = false;
int divider_position =
split_view_divider_.divider_widget() ? GetDividerPosition() : -1;
if (std::optional<float> snap_ratio = WindowState::Get(window)->snap_ratio();
snap_ratio) {
divider_position = CalculateDividerPosition(
root_window_, snap_position, *snap_ratio, ShouldConsiderDivider());
do_snap_animation =
other_window && !CanSnapWindow(other_window, 1.f - *snap_ratio);
} else if (divider_position < 0) {
divider_position = CalculateDividerPosition(root_window_, snap_position,
chromeos::kDefaultSnapRatio,
ShouldConsiderDivider());
}
split_view_divider_.SetDividerPosition(divider_position);
base::RecordAction(base::UserMetricsAction("SplitView_SnapWindow"));
if (!InTabletMode()) {
return;
}
split_view_divider_.SetVisible(true);
CHECK(split_view_divider_.HasDividerWidget());
const int fixed_divider_position =
GetClosestFixedDividerPosition(divider_position);
if (do_snap_animation) {
tablet_resize_mode_ = TabletResizeMode::kFast;
divider_snap_animation_ = std::make_unique<DividerSnapAnimation>(
this, divider_position,
fixed_divider_position,
2 * kBouncingAnimationOneWayDuration, gfx::Tween::FAST_OUT_SLOW_IN_3);
divider_snap_animation_->Hide();
divider_snap_animation_->Show();
}
split_view_divider_.SetDividerPosition(fixed_divider_position);
}
aura::Window* SplitViewController::GetSnappedWindow(SnapPosition position) {
DCHECK_NE(SnapPosition::kNone, position);
return position == SnapPosition::kPrimary ? primary_window_.get()
: secondary_window_.get();
}
aura::Window* SplitViewController::GetDefaultSnappedWindow() {
if (default_snap_position_ == SnapPosition::kPrimary)
return primary_window_;
if (default_snap_position_ == SnapPosition::kSecondary)
return secondary_window_;
return nullptr;
}
gfx::Rect SplitViewController::GetSnappedWindowBoundsInParent(
SnapPosition snap_position,
aura::Window* window_for_minimum_size,
float snap_ratio) {
gfx::Rect bounds =
GetSnappedWindowBoundsInScreen(snap_position, window_for_minimum_size,
snap_ratio, ShouldConsiderDivider());
wm::ConvertRectFromScreen(root_window_, &bounds);
return bounds;
}
bool SplitViewController::ShouldConsiderDivider() const {
return split_view_divider_.HasDividerWidget() || InTabletMode();
}
bool SplitViewController::IsDividerAnimating() const {
return divider_snap_animation_ && divider_snap_animation_->is_animating();
}
void SplitViewController::EndSplitView(EndReason end_reason) {
if (!InSplitViewMode()) {
return;
}
end_reason_ = end_reason;
const bool is_divider_animating = IsDividerAnimating();
if ((IsResizingWithDivider() || is_divider_animating) &&
end_reason != EndReason::kRootWindowDestroyed) {
if (is_divider_animating) {
StopSnapAnimation();
}
EndResizeWithDividerImpl();
}
presentation_time_recorder_.reset();
Shell* shell = Shell::Get();
shell->RemoveShellObserver(this);
OverviewController::Get()->RemoveObserver(this);
keyboard::KeyboardUIController::Get()->RemoveObserver(this);
shell->activation_client()->RemoveObserver(this);
auto_snap_controller_.reset();
if (end_reason != EndReason::kRootWindowDestroyed) {
const SplitViewOverviewSessionExitPoint exit_point =
end_reason == EndReason::kSnapGroups
? SplitViewOverviewSessionExitPoint::kCompleteByActivating
: SplitViewOverviewSessionExitPoint::kShutdown;
RootWindowController::ForWindow(root_window_)
->EndSplitViewOverviewSession(exit_point);
}
StopObserving(SnapPosition::kPrimary);
StopObserving(SnapPosition::kSecondary);
black_scrim_layer_.reset();
default_snap_position_ = SnapPosition::kNone;
divider_closest_ratio_ = std::numeric_limits<float>::quiet_NaN();
snapping_window_transformed_bounds_map_.clear();
UpdateStateAndNotifyObservers();
split_view_divider_.SetVisible(false);
base::RecordAction(base::UserMetricsAction("SplitView_EndSplitView"));
const base::Time now = base::Time::Now();
UMA_HISTOGRAM_LONG_TIMES("Ash.SplitView.TimeInSplitView",
now - splitview_start_time_);
if (IsExactlyOneRootInSplitView()) {
UMA_HISTOGRAM_LONG_TIMES("Ash.SplitView.TimeInMultiDisplaySplitView",
now - g_multi_display_split_view_start_time);
}
}
bool SplitViewController::IsWindowInSplitView(
const aura::Window* window) const {
return window && (window == primary_window_ || window == secondary_window_);
}
bool SplitViewController::IsWindowInTransitionalState(
const aura::Window* window) const {
return to_be_snapped_windows_observer_->IsObserving(window);
}
void SplitViewController::OnOverviewButtonTrayLongPressed(
const gfx::Point& event_location) {
if (!ShouldAllowSplitView())
return;
MruWindowTracker::WindowList mru_window_list =
Shell::Get()->mru_window_tracker()->BuildWindowForCycleList(kActiveDesk);
if (mru_window_list.empty())
return;
auto* overview_controller = Shell::Get()->overview_controller();
aura::Window* target_window = mru_window_list[0];
if (InSplitViewMode()) {
DCHECK(IsWindowInSplitView(target_window));
DCHECK(target_window);
EndSplitView();
overview_controller->EndOverview(
OverviewEndAction::kOverviewButtonLongPress);
MaximizeIfSnapped(target_window);
wm::ActivateWindow(target_window);
base::RecordAction(
base::UserMetricsAction("Tablet_LongPressOverviewButtonExitSplitView"));
return;
}
if (!CanSnapWindow(target_window, chromeos::kDefaultSnapRatio)) {
ShowAppCannotSnapToast();
return;
}
overview_start_action_ = OverviewStartAction::kOverviewButtonLongPress;
enter_exit_overview_type_ = OverviewEnterExitType::kImmediateEnter;
SnapWindow(target_window, SnapPosition::kPrimary,
WindowSnapActionSource::kLongPressOverviewButtonToSnap,
true);
base::RecordAction(
base::UserMetricsAction("Tablet_LongPressOverviewButtonEnterSplitView"));
}
void SplitViewController::OnWindowDragStarted(aura::Window* dragged_window) {
DCHECK(dragged_window);
if (IsWindowInSplitView(dragged_window)) {
OnSnappedWindowDetached(dragged_window,
WindowDetachedReason::kWindowDragged);
}
if (split_view_divider_.divider_widget()) {
split_view_divider_.OnWindowDragStarted(dragged_window);
}
}
void SplitViewController::OnWindowDragEnded(
aura::Window* dragged_window,
SnapPosition desired_snap_position,
const gfx::Point& last_location_in_screen,
WindowSnapActionSource snap_action_source) {
if (window_util::IsDraggingTabs(dragged_window)) {
tab_drag_window_observer_ = std::make_unique<TabDragWindowObserver>(
this, dragged_window, desired_snap_position, last_location_in_screen,
snap_action_source);
} else {
EndWindowDragImpl(dragged_window, dragged_window->is_destroying(),
desired_snap_position, last_location_in_screen,
snap_action_source);
}
}
void SplitViewController::OnWindowDragCanceled() {
if (split_view_divider_.divider_widget()) {
split_view_divider_.OnWindowDragEnded();
}
}
SnapPosition SplitViewController::ComputeSnapPosition(
const gfx::Point& last_location_in_screen) {
const int divider_position =
InSplitViewMode()
? GetDividerPosition()
: CalculateDividerPosition(root_window_, SnapPosition::kPrimary,
chromeos::kDefaultSnapRatio,
ShouldConsiderDivider());
const int position = IsLayoutHorizontal(root_window_)
? last_location_in_screen.x()
: last_location_in_screen.y();
return (position <= divider_position) == IsLayoutPrimary(root_window_)
? SnapPosition::kPrimary
: SnapPosition::kSecondary;
}
bool SplitViewController::BoundsChangeIsFromVKAndAllowed(
aura::Window* window) const {
return changing_bounds_by_vk_ &&
window == (IsLayoutPrimary(window) ? secondary_window_.get()
: primary_window_.get());
}
void SplitViewController::AddObserver(SplitViewObserver* observer) {
observers_.AddObserver(observer);
}
void SplitViewController::RemoveObserver(SplitViewObserver* observer) {
observers_.RemoveObserver(observer);
}
void SplitViewController::OnWindowPropertyChanged(aura::Window* window,
const void* key,
intptr_t old) {
if (key != aura::client::kResizeBehaviorKey)
return;
if (window->GetProperty(aura::client::kResizeBehaviorKey) ==
static_cast<int>(old)) {
return;
}
if (CanKeepCurrentSnapRatio(window)) {
return;
}
EndSplitView();
Shell::Get()->overview_controller()->EndOverview(
OverviewEndAction::kSplitView);
ShowAppCannotSnapToast();
}
void SplitViewController::OnWindowBoundsChanged(
aura::Window* window,
const gfx::Rect& old_bounds,
const gfx::Rect& new_bounds,
ui::PropertyChangeReason reason) {
if (!InClamshellSplitViewMode() || split_view_divider_.divider_widget()) {
return;
}
if (WindowState* window_state = WindowState::Get(window);
window_state->is_dragged()) {
if (presentation_time_recorder_) {
presentation_time_recorder_->RequestNext();
}
}
}
void SplitViewController::OnWindowDestroyed(aura::Window* window) {
DCHECK(InSplitViewMode());
DCHECK(IsWindowInSplitView(window));
OnSnappedWindowDetached(window, WindowDetachedReason::kWindowDestroyed);
}
void SplitViewController::OnWindowRemovingFromRootWindow(
aura::Window* window,
aura::Window* new_root) {
if (new_root) {
OnSnappedWindowDetached(window,
WindowDetachedReason::kWindowMovedToAnotherDisplay);
}
}
void SplitViewController::OnPostWindowStateTypeChange(
WindowState* window_state,
WindowStateType old_type) {
DCHECK_EQ(window_state->GetDisplay().id(),
display::Screen::Get()->GetDisplayNearestWindow(root_window_).id());
aura::Window* window = window_state->window();
if (window_state->IsSnapped()) {
OnWindowSnapped(window, old_type,
window_state->snap_action_source().value_or(
WindowSnapActionSource::kNotSpecified));
} else if (window_state->IsNormalStateType() || window_state->IsMaximized() ||
window_state->IsFullscreen() || window_state->IsFloated()) {
EndSplitView();
Shell::Get()->overview_controller()->EndOverview(
OverviewEndAction::kSplitView);
} else if (window_state->IsMinimized()) {
OnSnappedWindowDetached(window, WindowDetachedReason::kWindowMinimized);
if (!InSplitViewMode()) {
if (InTabletMode()) {
InsertWindowToOverview(window);
} else {
Shell::Get()->overview_controller()->EndOverview(
OverviewEndAction::kSplitView);
}
}
}
}
void SplitViewController::OnPinnedStateChanged(aura::Window* pinned_window) {
if (WindowState::Get(pinned_window)->IsPinned() && InSplitViewMode())
EndSplitView(EndReason::kUnsnappableWindowActivated);
}
void SplitViewController::OnOverviewModeStarting() {
CHECK(InSplitViewMode());
if (InClamshellSplitViewMode() &&
!RootWindowController::ForWindow(root_window_)
->split_view_overview_session()) {
EndSplitView();
return;
}
if (default_snap_position_ == SnapPosition::kPrimary) {
StopObserving(SnapPosition::kSecondary);
} else if (default_snap_position_ == SnapPosition::kSecondary) {
StopObserving(SnapPosition::kPrimary);
}
UpdateStateAndNotifyObservers();
}
void SplitViewController::OnOverviewModeEnding(
OverviewSession* overview_session) {
DCHECK(InSplitViewMode());
if (state_ == State::kBothSnapped)
overview_session->SetWindowListNotAnimatedWhenExiting(root_window_);
if (!InTabletMode()) {
return;
}
if (state_ == State::kBothSnapped) {
return;
}
OverviewGrid* current_grid =
overview_session->GetGridWithRootWindow(root_window_);
if (!current_grid || current_grid->empty()) {
return;
}
for (const auto& overview_item : current_grid->item_list()) {
for (aura::Window* window : overview_item->GetWindows()) {
CHECK(window);
if (window == GetDefaultSnappedWindow()) {
continue;
}
std::optional<float> snap_ratio = ComputeAutoSnapRatio(window);
if (!snap_ratio.has_value()) {
continue;
}
const bool was_active =
overview_session->IsWindowActiveWindowBeforeOverview(window);
overview_item->RestoreWindow(false,
true);
overview_session->RemoveItem(overview_item.get());
SnapWindow(window,
(default_snap_position_ == SnapPosition::kPrimary)
? SnapPosition::kSecondary
: SnapPosition::kPrimary,
WindowSnapActionSource::kAutoSnapInSplitView,
false, *snap_ratio);
if (was_active) {
wm::ActivateWindow(window);
}
overview_session->SetWindowListNotAnimatedWhenExiting(root_window_);
return;
}
}
if (DesksController::Get()->AreDesksBeingModified()) {
return;
}
EndSplitView();
ShowAppCannotSnapToast();
}
void SplitViewController::OnOverviewModeEnded() {
DCHECK(InSplitViewMode());
if (InClamshellSplitViewMode()) {
EndSplitView();
}
}
void SplitViewController::OnDisplaysRemoved(
const display::Displays& removed_displays) {
if (GetRootWindowSettings(root_window_)->display_id ==
display::kInvalidDisplayId) {
split_view_metrics_controller_.reset();
return;
}
if (state_ == State::kPrimarySnapped || state_ == State::kSecondarySnapped) {
aura::Window* window =
primary_window_ ? primary_window_ : secondary_window_;
RootWindowController::ForWindow(window)->StartSplitViewOverviewSession(
window, OverviewStartAction::kSplitView,
OverviewEnterExitType::kImmediateEnter,
WindowSnapActionSource::kNotSpecified);
}
}
void SplitViewController::OnDisplayMetricsChanged(
const display::Display& display,
uint32_t metrics) {
if (GetRootWindowSettings(root_window_)->display_id != display.id())
return;
const bool is_previous_layout_right_side_up =
is_previous_layout_right_side_up_;
is_previous_layout_right_side_up_ = IsLayoutPrimary(display);
if (!InSplitViewMode())
return;
if ((primary_window_ && !CanKeepCurrentSnapRatio(primary_window_)) ||
(secondary_window_ && !CanKeepCurrentSnapRatio(secondary_window_))) {
if (!Shell::Get()->session_controller()->IsUserSessionBlocked())
EndSplitView();
return;
}
if (!InTabletMode() || !split_view_divider_.divider_widget()) {
return;
}
if (IsDividerAnimating()) {
StopAndShoveAnimatedDivider();
EndResizeWithDividerImpl();
}
if (!InTabletSplitViewMode()) {
return;
}
if ((metrics & display::DisplayObserver::DISPLAY_METRIC_ROTATION) ||
(metrics & display::DisplayObserver::DISPLAY_METRIC_WORK_AREA)) {
if (std::isnan(divider_closest_ratio_))
divider_closest_ratio_ = kFixedPositionRatios[1];
if (is_previous_layout_right_side_up != IsLayoutPrimary(display))
divider_closest_ratio_ = 1.f - divider_closest_ratio_;
split_view_divider_.SetDividerPosition(
static_cast<int>(divider_closest_ratio_ *
GetDividerPositionUpperLimit(root_window_)) -
kSplitviewDividerShortSideLength / 2);
}
if (!IsResizingWithDivider()) {
split_view_divider_.SetDividerPosition(
GetClosestFixedDividerPosition(GetDividerPosition()));
}
EndSplitViewAfterResizingAtEdgeIfAppropriate();
NotifyDividerPositionChanged();
UpdateSnappedWindowsAndDividerBounds();
}
void SplitViewController::OnDisplayTabletStateChanged(
display::TabletState state) {
switch (state) {
case display::TabletState::kInClamshellMode:
OnTabletModeEnded();
break;
case display::TabletState::kEnteringTabletMode:
break;
case display::TabletState::kInTabletMode:
OnTabletModeStarted();
break;
case display::TabletState::kExitingTabletMode:
OnTabletModeEnding();
break;
}
}
void SplitViewController::OnAccessibilityStatusChanged() {
if (InTabletMode() &&
Shell::Get()->accessibility_controller()->spoken_feedback().enabled()) {
EndSplitView();
}
}
void SplitViewController::OnAccessibilityControllerShutdown() {
Shell::Get()->accessibility_controller()->RemoveObserver(this);
}
void SplitViewController::OnKeyboardOccludedBoundsChanged(
const gfx::Rect& screen_bounds) {
if (IsLayoutHorizontal(root_window_))
return;
aura::Window* bottom_window = GetPhysicallyRightOrBottomWindow();
if (!bottom_window &&
!bottom_window->Contains(window_util::GetActiveWindow())) {
return;
}
if (screen_bounds.IsEmpty()) {
UpdateSnappedWindowsAndDividerBounds();
return;
}
auto* text_input_client = GetCurrentInputMethod()->GetTextInputClient();
if (!text_input_client) {
return;
}
const gfx::Rect caret_bounds = text_input_client->GetCaretBounds();
if (caret_bounds == gfx::Rect()) {
return;
}
const int keyboard_occluded_y = screen_bounds.y();
if (keyboard_occluded_y - caret_bounds.bottom() > kMinCaretKeyboardDist)
return;
gfx::Rect bottom_bounds = bottom_window->GetBoundsInScreen();
const gfx::Rect work_area =
screen_util::GetDisplayWorkAreaBoundsInScreenForActiveDeskContainer(
root_window_);
const int y =
std::max(keyboard_occluded_y - bottom_bounds.height(),
static_cast<int>(work_area.y() +
work_area.height() * kMinDividerPositionRatio));
bottom_bounds.set_y(y);
bottom_bounds.set_height(keyboard_occluded_y - y);
{
base::AutoReset<bool> enable_bounds_change(&changing_bounds_by_vk_, true);
bottom_window->SetBoundsInScreen(
bottom_bounds,
display::Screen::Get()->GetDisplayNearestWindow(root_window_));
}
split_view_divider_.OnKeyboardOccludedBoundsChangedInPortrait(work_area, y);
}
void SplitViewController::OnWindowActivated(ActivationReason reason,
aura::Window* gained_active,
aura::Window* lost_active) {
if (!split_view_divider_.divider_widget() ||
split_view_divider_.IsAdjustable()) {
return;
}
if (IsLayoutHorizontal(root_window_)) {
return;
}
aura::Window* bottom_window = GetPhysicallyRightOrBottomWindow();
if (!bottom_window)
return;
if (bottom_window->Contains(lost_active) &&
!bottom_window->Contains(gained_active)) {
UpdateSnappedWindowsAndDividerBounds();
}
}
aura::Window* SplitViewController::GetRootWindow() const {
return root_window_;
}
void SplitViewController::StartResizeWithDivider(
const gfx::Point& location_in_screen) {
base::RecordAction(base::UserMetricsAction("SplitView_ResizeWindows"));
if (state_ == State::kBothSnapped) {
presentation_time_recorder_ = CreatePresentationTimeHistogramRecorder(
split_view_divider_.divider_widget()->GetCompositor(),
kTabletSplitViewResizeMultiHistogram,
kTabletSplitViewResizeMultiMaxLatencyHistogram);
return;
}
if (!IsInOverviewSession()) {
return;
}
if (GetOverviewSession()->GetGridWithRootWindow(root_window_)->empty()) {
presentation_time_recorder_ = CreatePresentationTimeHistogramRecorder(
split_view_divider_.divider_widget()->GetCompositor(),
kTabletSplitViewResizeSingleHistogram,
kTabletSplitViewResizeSingleMaxLatencyHistogram);
} else {
presentation_time_recorder_ = CreatePresentationTimeHistogramRecorder(
split_view_divider_.divider_widget()->GetCompositor(),
kTabletSplitViewResizeWithOverviewHistogram,
kTabletSplitViewResizeWithOverviewMaxLatencyHistogram);
}
accumulated_drag_time_ticks_ = base::TimeTicks::Now();
accumulated_drag_distance_ = 0;
tablet_resize_mode_ = TabletResizeMode::kNormal;
}
void SplitViewController::UpdateResizeWithDivider(
const gfx::Point& location_in_screen) {
UpdateTabletResizeMode(base::TimeTicks::Now(), location_in_screen);
NotifyDividerPositionChanged();
UpdateSnappedWindowsBounds();
UpdateResizeBackdrop();
UpdateBlackScrim(location_in_screen);
SetWindowsTransformDuringResizing();
}
bool SplitViewController::EndResizeWithDivider(
const gfx::Point& location_in_screen) {
NotifyDividerPositionChanged();
UpdateSnappedWindowsBounds();
NotifyWindowResized();
presentation_time_recorder_.reset();
black_scrim_layer_.reset();
resize_timer_.Stop();
tablet_resize_mode_ = TabletResizeMode::kNormal;
const int divider_position = GetDividerPosition();
const int target_divider_position =
GetClosestFixedDividerPosition(divider_position);
if (divider_position == target_divider_position ||
!display::Screen::Get()->InTabletMode()) {
return true;
}
divider_snap_animation_ = std::make_unique<DividerSnapAnimation>(
this, divider_position, target_divider_position,
base::Milliseconds(300), gfx::Tween::EASE_IN);
divider_snap_animation_->Show();
return false;
}
void SplitViewController::OnResizeEnding() {
CHECK(InSplitViewMode());
left_resize_backdrop_layer_.reset();
right_resize_backdrop_layer_.reset();
resize_timer_.Stop();
presentation_time_recorder_.reset();
RestoreWindowsTransformAfterResizing();
}
void SplitViewController::OnResizeEnded() {
EndSplitViewAfterResizingAtEdgeIfAppropriate();
}
void SplitViewController::SwapWindows() {
DCHECK(InSplitViewMode());
if (IsDividerAnimating()) {
return;
}
SwapWindowsAndUpdateBounds();
if (IsSnapped(primary_window_)) {
TriggerWMEventToSnapWindow(WindowState::Get(primary_window_),
WM_EVENT_SNAP_PRIMARY);
}
if (IsSnapped(secondary_window_)) {
TriggerWMEventToSnapWindow(WindowState::Get(secondary_window_),
WM_EVENT_SNAP_SECONDARY);
}
if (!primary_window_ || !secondary_window_) {
default_snap_position_ =
primary_window_ ? SnapPosition::kPrimary : SnapPosition::kSecondary;
}
split_view_divider_.SetDividerPosition(
GetClosestFixedDividerPosition(GetDividerPosition()));
UpdateStateAndNotifyObservers();
NotifyWindowSwapped();
base::RecordAction(
base::UserMetricsAction("SplitView_DoubleTapDividerSwapWindows"));
}
gfx::Rect SplitViewController::GetSnappedWindowBoundsInScreen(
SnapPosition snap_position,
aura::Window* window_for_minimum_size,
float snap_ratio,
bool account_for_divider_width) const {
if (snap_position == SnapPosition::kNone) {
return screen_util::GetDisplayWorkAreaBoundsInScreenForActiveDeskContainer(
root_window_);
}
const bool should_use_window_bounds_in_fast_resize =
g_use_fast_resize_for_testing ||
(IsResizingWithDivider() &&
tablet_resize_mode_ == TabletResizeMode::kFast);
if (window_for_minimum_size && should_use_window_bounds_in_fast_resize) {
gfx::Rect bounds_in_screen(window_for_minimum_size->GetTargetBounds());
wm::ConvertRectToScreen(root_window_, &bounds_in_screen);
return bounds_in_screen;
}
const int divider_position =
split_view_divider_.HasDividerWidget()
? GetDividerPosition()
: CalculateDividerPosition(root_window_, snap_position, snap_ratio,
account_for_divider_width);
return CalculateSnappedWindowBoundsInScreen(
snap_position, root_window_, window_for_minimum_size,
account_for_divider_width, divider_position, IsResizingWithDivider());
}
SnapPosition SplitViewController::GetPositionOfSnappedWindow(
const aura::Window* window) const {
DCHECK(IsWindowInSplitView(window));
return window == primary_window_ ? SnapPosition::kPrimary
: SnapPosition::kSecondary;
}
void SplitViewController::SetUseFastResizeForTesting(bool val) {
g_use_fast_resize_for_testing = val;
}
aura::Window* SplitViewController::GetPhysicallyLeftOrTopWindow() {
DCHECK(root_window_);
return IsLayoutPrimary(root_window_) ? primary_window_.get()
: secondary_window_.get();
}
aura::Window* SplitViewController::GetPhysicallyRightOrBottomWindow() {
DCHECK(root_window_);
return IsLayoutPrimary(root_window_) ? secondary_window_.get()
: primary_window_.get();
}
void SplitViewController::StartObserving(aura::Window* window) {
if (window && !window->HasObserver(this)) {
Shell::Get()->shadow_controller()->UpdateShadowForWindow(window);
window->AddObserver(this);
WindowState::Get(window)->AddObserver(this);
}
if (InTabletMode()) {
split_view_divider_.MaybeAddObservedWindow(window);
}
}
void SplitViewController::StopObserving(SnapPosition snap_position) {
aura::Window* window = GetSnappedWindow(snap_position);
if (window == primary_window_) {
primary_window_ = nullptr;
} else {
secondary_window_ = nullptr;
}
if (window && window->HasObserver(this)) {
window->RemoveObserver(this);
WindowState::Get(window)->RemoveObserver(this);
split_view_divider_.MaybeRemoveObservedWindow(window);
Shell::Get()->shadow_controller()->UpdateShadowForWindow(window);
RestoreTransformIfApplicable(window);
}
}
void SplitViewController::UpdateStateAndNotifyObservers() {
const State previous_state = state_;
if (IsSnapped(primary_window_) && IsSnapped(secondary_window_)) {
state_ = State::kBothSnapped;
} else if (IsSnapped(primary_window_)) {
state_ = State::kPrimarySnapped;
default_snap_position_ = SnapPosition::kPrimary;
} else if (IsSnapped(secondary_window_)) {
state_ = State::kSecondarySnapped;
default_snap_position_ = SnapPosition::kSecondary;
} else {
state_ = State::kNoSnap;
default_snap_position_ = SnapPosition::kNone;
}
DCHECK(previous_state != State::kNoSnap || state_ != State::kNoSnap ||
end_reason_ == EndReason::kSnapGroups);
for (auto& observer : observers_) {
observer.OnSplitViewStateChanged(previous_state, state_);
}
const bool was_in_split_view = previous_state != State::kNoSnap;
const bool is_in_split_view = InSplitViewMode();
if (!was_in_split_view && is_in_split_view) {
Shell* shell = Shell::Get();
shell->AddShellObserver(this);
OverviewController::Get()->AddObserver(this);
keyboard::KeyboardUIController::Get()->AddObserver(this);
shell->activation_client()->AddObserver(this);
auto_snap_controller_ = std::make_unique<AutoSnapController>(root_window_);
}
if (was_in_split_view && !is_in_split_view) {
CHECK(!auto_snap_controller_);
}
}
void SplitViewController::NotifyDividerPositionChanged() {
for (auto& observer : observers_) {
observer.OnSplitViewDividerPositionChanged();
}
}
void SplitViewController::NotifyWindowResized() {
for (auto& observer : observers_) {
observer.OnSplitViewWindowResized();
}
}
void SplitViewController::NotifyWindowSwapped() {
for (auto& observer : observers_)
observer.OnSplitViewWindowSwapped();
}
void SplitViewController::UpdateBlackScrim(
const gfx::Point& location_in_screen) {
DCHECK(InSplitViewMode());
if (!black_scrim_layer_) {
black_scrim_layer_ = std::make_unique<ui::Layer>(ui::LAYER_SOLID_COLOR);
black_scrim_layer_->SetColor(AshColorProvider::Get()->GetBackgroundColor());
auto* divider_layer = split_view_divider_.GetDividerWindow()->layer();
auto* divider_parent_layer = divider_layer->parent();
divider_parent_layer->Add(black_scrim_layer_.get());
divider_parent_layer->StackBelow(black_scrim_layer_.get(), divider_layer);
}
SnapPosition position = GetBlackScrimPosition(location_in_screen);
if (position == SnapPosition::kNone) {
black_scrim_layer_.reset();
return;
}
black_scrim_layer_->SetBounds(GetSnappedWindowBoundsInScreen(
position, nullptr,
chromeos::kDefaultSnapRatio, ShouldConsiderDivider()));
const int location = IsLayoutHorizontal(root_window_)
? location_in_screen.x()
: location_in_screen.y();
gfx::Rect work_area_bounds =
screen_util::GetDisplayWorkAreaBoundsInScreenForActiveDeskContainer(
root_window_);
if (!IsLayoutHorizontal(root_window_))
work_area_bounds.Transpose();
float opacity = kBlackScrimOpacity;
const float ratio = chromeos::kOneThirdSnapRatio - kBlackScrimFadeInRatio;
const int distance = std::min(std::abs(location - work_area_bounds.x()),
std::abs(work_area_bounds.right() - location));
if (distance > work_area_bounds.width() * ratio) {
opacity -= kBlackScrimOpacity *
(distance - work_area_bounds.width() * ratio) /
(work_area_bounds.width() * kBlackScrimFadeInRatio);
opacity = std::max(opacity, 0.f);
}
black_scrim_layer_->SetOpacity(opacity);
}
void SplitViewController::UpdateResizeBackdrop() {
auto create_backdrop = [](aura::Window* window) {
auto resize_backdrop_layer =
std::make_unique<ui::Layer>(ui::LAYER_SOLID_COLOR);
ui::Layer* parent = window->layer()->parent();
ui::Layer* stacking_target = window->layer();
parent->Add(resize_backdrop_layer.get());
parent->StackBelow(resize_backdrop_layer.get(), stacking_target);
return resize_backdrop_layer;
};
auto update_backdrop = [this](SnapPosition position, aura::Window* window,
ui::Layer* backdrop) {
backdrop->SetBounds(GetSnappedWindowBoundsInParent(
position, nullptr, chromeos::kDefaultSnapRatio));
backdrop->SetColor(window->GetProperty(
wm::IsActiveWindow(window) ? chromeos::kFrameActiveColorKey
: chromeos::kFrameInactiveColorKey));
};
if (state_ == State::kPrimarySnapped || state_ == State::kBothSnapped) {
if (!left_resize_backdrop_layer_)
left_resize_backdrop_layer_ = create_backdrop(primary_window_);
update_backdrop(SnapPosition::kPrimary, primary_window_,
left_resize_backdrop_layer_.get());
}
if (state_ == State::kSecondarySnapped || state_ == State::kBothSnapped) {
if (!right_resize_backdrop_layer_)
right_resize_backdrop_layer_ = create_backdrop(secondary_window_);
update_backdrop(SnapPosition::kSecondary, secondary_window_,
right_resize_backdrop_layer_.get());
}
}
void SplitViewController::UpdateSnappedWindowBounds(aura::Window* window) {
DCHECK(IsWindowInSplitView(window));
WindowState* window_state = WindowState::Get(window);
if (InTabletMode()) {
if (window_state->is_client_controlled()) {
const gfx::Rect requested_bounds =
TabletModeWindowState::GetBoundsInTabletMode(window_state);
const SetBoundsWMEvent event(requested_bounds,
true);
window_state->OnWMEvent(&event);
} else {
TabletModeWindowState::UpdateWindowPosition(
window_state, WindowState::BoundsChangeAnimationType::kAnimate);
}
} else {
const gfx::Rect requested_bounds = GetSnappedWindowBoundsInParent(
GetPositionOfSnappedWindow(window), window,
window_util::GetSnapRatioForWindow(window));
const SetBoundsWMEvent event(requested_bounds, true);
window_state->OnWMEvent(&event);
}
}
void SplitViewController::UpdateSnappedWindowsBounds() {
if (IsSnapped(primary_window_)) {
UpdateSnappedWindowBounds(primary_window_);
}
if (IsSnapped(secondary_window_)) {
UpdateSnappedWindowBounds(secondary_window_);
}
}
void SplitViewController::UpdateSnappedWindowsAndDividerBounds() {
UpdateSnappedWindowsBounds();
if (split_view_divider_.divider_widget()) {
split_view_divider_.UpdateDividerBounds();
split_view_divider_.SetAdjustable(true);
}
}
SnapPosition SplitViewController::GetBlackScrimPosition(
const gfx::Point& location_in_screen) {
const gfx::Rect work_area_bounds =
screen_util::GetDisplayWorkAreaBoundsInScreenForActiveDeskContainer(
root_window_);
if (!work_area_bounds.Contains(location_in_screen))
return SnapPosition::kNone;
gfx::Size primary_window_min_size, secondary_window_min_size;
if (primary_window_ && primary_window_->delegate())
primary_window_min_size = primary_window_->delegate()->GetMinimumSize();
if (secondary_window_ && secondary_window_->delegate())
secondary_window_min_size = secondary_window_->delegate()->GetMinimumSize();
bool right_side_up = IsLayoutPrimary(root_window_);
int divider_upper_limit = GetDividerPositionUpperLimit(root_window_);
int primary_window_distance = 0, secondary_window_distance = 0;
int min_left_length = 0, min_right_length = 0;
if (IsLayoutHorizontal(root_window_)) {
int left_distance = location_in_screen.x() - work_area_bounds.x();
int right_distance = work_area_bounds.right() - location_in_screen.x();
primary_window_distance = right_side_up ? left_distance : right_distance;
secondary_window_distance = right_side_up ? right_distance : left_distance;
min_left_length = primary_window_min_size.width();
min_right_length = secondary_window_min_size.width();
} else {
int top_distance = location_in_screen.y() - work_area_bounds.y();
int bottom_distance = work_area_bounds.bottom() - location_in_screen.y();
primary_window_distance = right_side_up ? top_distance : bottom_distance;
secondary_window_distance = right_side_up ? bottom_distance : top_distance;
min_left_length = primary_window_min_size.height();
min_right_length = secondary_window_min_size.height();
}
if (primary_window_distance <
divider_upper_limit * chromeos::kOneThirdSnapRatio ||
primary_window_distance < min_left_length) {
return SnapPosition::kPrimary;
}
if (secondary_window_distance <
divider_upper_limit * chromeos::kOneThirdSnapRatio ||
secondary_window_distance < min_right_length) {
return SnapPosition::kSecondary;
}
return SnapPosition::kNone;
}
int SplitViewController::GetClosestFixedDividerPosition(int divider_position) {
int divider_upper_limit = GetDividerPositionUpperLimit(root_window_);
divider_closest_ratio_ = FindClosestPositionRatio(
float(divider_position + kSplitviewDividerShortSideLength / 2) /
divider_upper_limit);
int fixed_position = divider_upper_limit * divider_closest_ratio_;
if (divider_closest_ratio_ > 0.f && divider_closest_ratio_ < 1.f) {
fixed_position -= kSplitviewDividerShortSideLength / 2;
}
return std::clamp(fixed_position, 0, divider_upper_limit);
}
void SplitViewController::StopAndShoveAnimatedDivider() {
CHECK(IsDividerAnimating());
StopSnapAnimation();
NotifyDividerPositionChanged();
UpdateSnappedWindowsAndDividerBounds();
}
void SplitViewController::StopSnapAnimation() {
divider_snap_animation_->Stop();
split_view_divider_.SetDividerPosition(
divider_snap_animation_->ending_position());
}
bool SplitViewController::ShouldEndSplitViewAfterResizingAtEdge() {
if (!InTabletSplitViewMode()) {
return false;
}
const int divider_position = GetDividerPosition();
return divider_position == 0 ||
divider_position == GetDividerPositionUpperLimit(root_window_);
}
void SplitViewController::EndSplitViewAfterResizingAtEdgeIfAppropriate() {
if (!ShouldEndSplitViewAfterResizingAtEdge()) {
return;
}
aura::Window* active_window = GetActiveWindowAfterResizingUponExit();
aura::Window* insert_overview_window = nullptr;
if (IsInOverviewSession()) {
insert_overview_window = GetDefaultSnappedWindow();
}
EndSplitView();
if (active_window) {
Shell::Get()->overview_controller()->EndOverview(
OverviewEndAction::kSplitView);
wm::ActivateWindow(active_window);
} else if (insert_overview_window) {
InsertWindowToOverview(insert_overview_window, false);
}
}
aura::Window* SplitViewController::GetActiveWindowAfterResizingUponExit() {
DCHECK(InSplitViewMode());
if (!ShouldEndSplitViewAfterResizingAtEdge()) {
return nullptr;
}
return GetDividerPosition() == 0 ? GetPhysicallyRightOrBottomWindow()
: GetPhysicallyLeftOrTopWindow();
}
void SplitViewController::OnWindowSnapped(
aura::Window* window,
std::optional<chromeos::WindowStateType> previous_state,
WindowSnapActionSource snap_action_source) {
RestoreTransformIfApplicable(window);
if (!display::Screen::Get()->InTabletMode()) {
if (SnapGroupController::Get()->OnWindowSnapped(window,
snap_action_source)) {
OnSnappedWindowDetached(window, WindowDetachedReason::kAddedToSnapGroup);
CHECK(!InSplitViewMode());
return;
}
}
UpdateStateAndNotifyObservers();
if (to_be_activated_window_ == window) {
to_be_activated_window_ = nullptr;
wm::ActivateWindow(window);
}
if (InTabletMode() && previous_state &&
*previous_state == chromeos::WindowStateType::kFloated &&
state_ != State::kBothSnapped) {
for (aura::Window* mru_window :
Shell::Get()->mru_window_tracker()->BuildWindowForCycleList(
kActiveDesk)) {
auto* window_state = WindowState::Get(mru_window);
if (mru_window != window && !window_state->IsMinimized() &&
window_state->CanSnap()) {
const SnapPosition snap_position =
GetPositionOfSnappedWindow(window) == SnapPosition::kPrimary
? SnapPosition::kSecondary
: SnapPosition::kPrimary;
WindowSnapWMEvent event(snap_position == SnapPosition::kPrimary
? WM_EVENT_SNAP_PRIMARY
: WM_EVENT_SNAP_SECONDARY,
WindowSnapActionSource::kAutoSnapInSplitView);
WindowState::Get(mru_window)->OnWMEvent(&event);
return;
}
}
}
if (WillStartPartialOverview(window)) {
if (!InTabletMode()) {
base::RecordAction(
base::UserMetricsAction("SnapGroups_StartPartialOverview"));
}
RootWindowController::ForWindow(window)->StartSplitViewOverviewSession(
window, overview_start_action_, enter_exit_overview_type_,
snap_action_source);
overview_start_action_.reset();
enter_exit_overview_type_.reset();
return;
}
if (!InTabletMode() &&
!RootWindowController::ForWindow(window)->split_view_overview_session() &&
snap_action_source !=
WindowSnapActionSource::kSnapByClamshellTabletTransition) {
base::RecordAction(
base::UserMetricsAction("SnapGroups_SkipFormSnapGroupAfterSnapping"));
EndSplitView(EndReason::kNormal);
return;
}
UpdateSnappedWindowsAndDividerBounds();
}
void SplitViewController::OnSnappedWindowDetached(aura::Window* window,
WindowDetachedReason reason) {
auto iter = snapping_window_transformed_bounds_map_.find(window);
if (iter != snapping_window_transformed_bounds_map_.end()) {
snapping_window_transformed_bounds_map_.erase(iter);
}
if (to_be_activated_window_ == window) {
to_be_activated_window_ = nullptr;
}
const bool is_window_moved =
reason == WindowDetachedReason::kWindowMovedToAnotherDisplay;
const bool is_window_destroyed_or_moved =
reason == WindowDetachedReason::kWindowDestroyed || is_window_moved;
const SnapPosition position_of_snapped_window =
GetPositionOfSnappedWindow(window);
if (is_window_destroyed_or_moved) {
StopObserving(position_of_snapped_window);
}
const bool is_divider_animating = IsDividerAnimating();
if (IsResizingWithDivider() || is_divider_animating) {
if (is_divider_animating) {
StopAndShoveAnimatedDivider();
}
EndResizeWithDividerImpl();
}
if (!is_window_destroyed_or_moved) {
StopObserving(position_of_snapped_window);
}
auto should_end_split_view = [&]() -> bool {
if (!primary_window_ && !secondary_window_) {
return true;
}
return InClamshellSplitViewMode() &&
(!primary_window_ || !secondary_window_);
};
if (should_end_split_view()) {
EndReason end_reason = EndReason::kNormal;
switch (reason) {
case WindowDetachedReason::kWindowDragged:
end_reason = EndReason::kWindowDragStarted;
break;
case WindowDetachedReason::kAddedToSnapGroup:
end_reason = EndReason::kSnapGroups;
break;
default:
break;
}
EndSplitView(end_reason);
if (is_window_moved) {
Shell::Get()->overview_controller()->EndOverview(
OverviewEndAction::kSplitView);
}
} else {
DCHECK(InTabletSplitViewMode());
aura::Window* other_window =
GetSnappedWindow(position_of_snapped_window == SnapPosition::kPrimary
? SnapPosition::kSecondary
: SnapPosition::kPrimary);
if (reason == WindowDetachedReason::kWindowFloated || is_window_moved) {
WMEvent event(WM_EVENT_MAXIMIZE);
WindowState::Get(other_window)->OnWMEvent(&event);
return;
}
default_snap_position_ =
primary_window_ ? SnapPosition::kPrimary : SnapPosition::kSecondary;
UpdateStateAndNotifyObservers();
RootWindowController::ForWindow(other_window)
->StartSplitViewOverviewSession(
other_window, OverviewStartAction::kFasterSplitScreenSetup,
reason == WindowDetachedReason::kWindowDragged
? OverviewEnterExitType::kImmediateEnter
: OverviewEnterExitType::kNormal,
WindowSnapActionSource::kNotSpecified);
}
}
void SplitViewController::ModifyPositionRatios(
std::vector<float>& out_position_ratios) {
const bool landscape = IsCurrentScreenOrientationLandscape();
const int min_left_size =
GetMinimumWindowLength(GetPhysicallyLeftOrTopWindow(), landscape);
const int min_right_size =
GetMinimumWindowLength(GetPhysicallyRightOrBottomWindow(), landscape);
const int divider_upper_limit = GetDividerPositionUpperLimit(root_window_);
const float min_size_left_ratio =
static_cast<float>(min_left_size) / divider_upper_limit;
const float min_size_right_ratio =
static_cast<float>(min_right_size) / divider_upper_limit;
if (min_size_left_ratio > chromeos::kOneThirdSnapRatio) {
std::erase(out_position_ratios, chromeos::kOneThirdSnapRatio);
}
if (min_size_right_ratio > chromeos::kOneThirdSnapRatio) {
std::erase(out_position_ratios, chromeos::kTwoThirdSnapRatio);
}
if (min_size_left_ratio > chromeos::kDefaultSnapRatio ||
min_size_right_ratio > chromeos::kDefaultSnapRatio) {
std::erase(out_position_ratios, chromeos::kDefaultSnapRatio);
}
}
float SplitViewController::FindClosestPositionRatio(float current_ratio) {
float closest_ratio = 0.f;
base::span<const float> ratio_span = kFixedPositionRatios;
std::vector<float> position_ratios(ratio_span.begin(), ratio_span.end());
ModifyPositionRatios(position_ratios);
float min_ratio_diff = std::numeric_limits<float>::max();
for (const float ratio : position_ratios) {
const float ratio_diff = std::abs(current_ratio - ratio);
if (ratio_diff < min_ratio_diff) {
min_ratio_diff = ratio_diff;
closest_ratio = ratio;
}
}
return closest_ratio;
}
void SplitViewController::RestoreTransformIfApplicable(aura::Window* window) {
auto iter = snapping_window_transformed_bounds_map_.find(window);
if (iter == snapping_window_transformed_bounds_map_.end())
return;
const gfx::Rect item_bounds = iter->second;
snapping_window_transformed_bounds_map_.erase(iter);
if (!window->layer()->GetTargetTransform().IsIdentity()) {
const gfx::Rect snapped_bounds = GetSnappedWindowBoundsInScreen(
GetPositionOfSnappedWindow(window), window,
window_util::GetSnapRatioForWindow(window), ShouldConsiderDivider());
const gfx::Transform starting_transform = gfx::TransformBetweenRects(
gfx::RectF(snapped_bounds), gfx::RectF(item_bounds));
SetTransformWithAnimation(window, starting_transform, gfx::Transform());
}
}
void SplitViewController::SetWindowsTransformDuringResizing() {
CHECK(InTabletSplitViewMode() || !display::Screen::Get()->InTabletMode());
const int divider_position = GetDividerPosition();
CHECK_GE(divider_position, 0);
aura::Window* left_or_top_window = GetPhysicallyLeftOrTopWindow();
aura::Window* right_or_bottom_window = GetPhysicallyRightOrBottomWindow();
if (left_or_top_window) {
SetWindowTransformDuringResizing(left_or_top_window, divider_position);
}
if (right_or_bottom_window) {
SetWindowTransformDuringResizing(right_or_bottom_window, divider_position);
}
}
void SplitViewController::RestoreWindowsTransformAfterResizing() {
DCHECK(InSplitViewMode());
if (primary_window_)
window_util::SetTransform(primary_window_, gfx::Transform());
if (secondary_window_)
window_util::SetTransform(secondary_window_, gfx::Transform());
if (black_scrim_layer_.get()) {
black_scrim_layer_->SetTransform(gfx::Transform());
}
}
void SplitViewController::SetTransformWithAnimation(
aura::Window* window,
const gfx::Transform& start_transform,
const gfx::Transform& target_transform) {
for (auto* window_iter : GetTransientTreeIterator(window)) {
const gfx::PointF target_origin =
GetUnionScreenBoundsForWindow(window).origin();
gfx::RectF original_bounds(window_iter->GetTargetBounds());
wm::TranslateRectToScreen(window_iter->parent(), &original_bounds);
const gfx::PointF pivot(target_origin.x() - original_bounds.x(),
target_origin.y() - original_bounds.y());
const gfx::Transform new_start_transform =
TransformAboutPivot(pivot, start_transform);
const gfx::Transform new_target_transform =
TransformAboutPivot(pivot, target_transform);
if (new_start_transform != window_iter->layer()->GetTargetTransform())
window_iter->SetTransform(new_start_transform);
std::vector<ui::ImplicitAnimationObserver*> animation_observers;
if (window_iter == window) {
animation_observers.push_back(
new WindowTransformAnimationObserver(window));
OverviewController* overview_controller = OverviewController::Get();
OverviewSession* overview_session =
overview_controller->overview_session();
if (overview_controller->IsCompletingShutdownAnimations() ||
(overview_session && overview_session->is_shutting_down() &&
overview_session->enter_exit_overview_type() !=
OverviewEnterExitType::kImmediateExit)) {
auto overview_exit_animation_observer =
std::make_unique<ExitAnimationObserver>();
animation_observers.push_back(overview_exit_animation_observer.get());
overview_controller->AddExitAnimationObserver(
std::move(overview_exit_animation_observer));
}
}
DoSplitviewTransformAnimation(window_iter->layer(),
SPLITVIEW_ANIMATION_SET_WINDOW_TRANSFORM,
new_target_transform, animation_observers);
}
}
void SplitViewController::UpdateSnappingWindowTransformedBounds(
aura::Window* window) {
if (!window->layer()->GetTargetTransform().IsIdentity()) {
snapping_window_transformed_bounds_map_[window] = gfx::ToEnclosedRect(
window_util::GetTransformedBounds(window, 0));
}
}
void SplitViewController::InsertWindowToOverview(aura::Window* window,
bool animate) {
if (!window || !GetOverviewSession())
return;
GetOverviewSession()->AddItemInMruOrder(window, true, animate,
true,
false);
}
void SplitViewController::EndResizeWithDividerImpl() {
split_view_divider_.CleanUpWindowResizing();
}
void SplitViewController::OnResizeTimer() {
if (InSplitViewMode() && split_view_divider_.divider_widget()) {
split_view_divider_.ResizeWithDivider(
split_view_divider_.previous_event_location());
}
}
void SplitViewController::UpdateTabletResizeMode(
base::TimeTicks event_time_ticks,
const gfx::Point& event_location) {
if (!presentation_time_recorder_) {
base::debug::DumpWithoutCrashing();
} else {
presentation_time_recorder_->RequestNext();
}
if (IsLayoutHorizontal(root_window_)) {
accumulated_drag_distance_ += std::abs(
event_location.x() - split_view_divider_.previous_event_location().x());
} else {
accumulated_drag_distance_ += std::abs(
event_location.y() - split_view_divider_.previous_event_location().y());
}
const base::TimeDelta chunk_time_ticks =
event_time_ticks - accumulated_drag_time_ticks_;
if (chunk_time_ticks >= kSplitViewChunkTime) {
int drag_per_second =
accumulated_drag_distance_ / chunk_time_ticks.InSecondsF();
tablet_resize_mode_ = drag_per_second > kSplitViewThresholdPixelsPerSec
? TabletResizeMode::kFast
: TabletResizeMode::kNormal;
accumulated_drag_time_ticks_ = event_time_ticks;
accumulated_drag_distance_ = 0;
}
if (tablet_resize_mode_ == TabletResizeMode::kFast) {
resize_timer_.Start(FROM_HERE, kSplitViewChunkTime, this,
&SplitViewController::OnResizeTimer);
}
}
void SplitViewController::OnTabletModeStarted() {
is_previous_layout_right_side_up_ = IsCurrentScreenOrientationPrimary();
if (InSplitViewMode()) {
CHECK(primary_window_ || secondary_window_);
const int divider_position =
GetClosestFixedDividerPosition(GetEquivalentDividerPosition(
primary_window_ ? primary_window_ : secondary_window_,
true));
split_view_divider_.SetDividerPosition(divider_position);
UpdateSnappedWindowsAndDividerBounds();
NotifyDividerPositionChanged();
RootWindowController* root_window_controller =
RootWindowController::ForWindow(root_window_);
if (root_window_controller->split_view_overview_session()) {
root_window_controller->EndSplitViewOverviewSession(
SplitViewOverviewSessionExitPoint::kTabletConversion);
}
}
}
void SplitViewController::OnTabletModeEnding() {
const bool is_divider_animating = IsDividerAnimating();
if (IsResizingWithDivider() || is_divider_animating) {
if (is_divider_animating) {
StopAndShoveAnimatedDivider();
}
EndResizeWithDividerImpl();
}
split_view_divider_.SetVisible(false);
}
void SplitViewController::OnTabletModeEnded() {
is_previous_layout_right_side_up_ = true;
}
void SplitViewController::EndWindowDragImpl(
aura::Window* window,
bool is_being_destroyed,
SnapPosition desired_snap_position,
const gfx::Point& last_location_in_screen,
WindowSnapActionSource snap_action_source) {
if (split_view_divider_.divider_widget()) {
split_view_divider_.OnWindowDragEnded();
}
if (is_being_destroyed)
return;
if (GetOverviewSession() && GetOverviewSession()->IsWindowInOverview(window))
return;
if (WindowState::Get(window)->IsFloated()) {
return;
}
DCHECK_EQ(root_window_, window->GetRootWindow());
if (desired_snap_position == SnapPosition::kNone) {
if (InSplitViewMode()) {
SnapWindow(window, ComputeSnapPosition(last_location_in_screen),
snap_action_source,
true);
} else {
SetTransformWithAnimation(window, window->layer()->GetTargetTransform(),
gfx::Transform());
OverviewSession* overview_session = GetOverviewSession();
if (overview_session) {
overview_session->SetWindowListNotAnimatedWhenExiting(root_window_);
overview_session->set_enter_exit_overview_type(
OverviewEnterExitType::kImmediateExit);
}
wm::ActivateWindow(window);
Shell::Get()->overview_controller()->EndOverview(
OverviewEndAction::kSplitView);
TabletModeWindowState::UpdateWindowPosition(
WindowState::Get(window),
WindowState::BoundsChangeAnimationType::kAnimate);
if (InTabletMode()) {
MaximizeIfSnapped(window);
}
}
} else {
SnapWindow(window, desired_snap_position, snap_action_source,
true);
}
}
void SplitViewController::SwapWindowsAndUpdateBounds() {
gfx::Rect primary_window_bounds =
primary_window_ ? primary_window_->GetBoundsInScreen() : gfx::Rect();
gfx::Rect secondary_window_bounds =
secondary_window_ ? secondary_window_->GetBoundsInScreen() : gfx::Rect();
aura::Window* cached_window = primary_window_;
primary_window_ = secondary_window_;
secondary_window_ = cached_window;
const auto dst_display =
display::Screen::Get()->GetDisplayNearestWindow(root_window_);
if (primary_window_) {
primary_window_->SetBoundsInScreen(secondary_window_bounds, dst_display);
}
if (secondary_window_) {
secondary_window_->SetBoundsInScreen(primary_window_bounds, dst_display);
}
}
}