blob: 744c092596c4edd462059574c71066a65fa7be12 [file] [log] [blame]
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ash/wm/splitview/split_view_controller.h"
#include <cmath>
#include <memory>
#include "ash/display/screen_orientation_controller_chromeos.h"
#include "ash/public/cpp/app_types.h"
#include "ash/public/cpp/ash_switches.h"
#include "ash/public/cpp/shell_window_ids.h"
#include "ash/screen_util.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/system/toast/toast_data.h"
#include "ash/system/toast/toast_manager.h"
#include "ash/wm/mru_window_tracker.h"
#include "ash/wm/overview/window_selector_controller.h"
#include "ash/wm/splitview/split_view_divider.h"
#include "ash/wm/tablet_mode/tablet_mode_controller.h"
#include "ash/wm/window_state.h"
#include "ash/wm/window_util.h"
#include "ash/wm/wm_event.h"
#include "base/command_line.h"
#include "base/metrics/histogram_macros.h"
#include "base/metrics/user_metrics.h"
#include "base/optional.h"
#include "ui/aura/client/aura_constants.h"
#include "ui/aura/window.h"
#include "ui/aura/window_delegate.h"
#include "ui/base/class_property.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/compositor/layer.h"
#include "ui/views/widget/widget.h"
#include "ui/wm/core/coordinate_conversion.h"
#include "ui/wm/public/activation_client.h"
namespace ash {
namespace {
// Three fixed position ratios of the divider, which means the divider can
// always be moved to these three positions.
constexpr float kFixedPositionRatios[] = {0.f, 0.5f, 1.0f};
// Two optional position ratios of the divider. Whether the divider can be moved
// to these two positions depends on the minimum size of the snapped windows.
constexpr float kOneThirdPositionRatio = 0.33f;
constexpr float kTwoThirdPositionRatio = 0.67f;
// Toast data.
constexpr char kAppCannotSnapToastId[] = "split_view_app_cannot_snap";
constexpr int kAppCannotSnapToastDurationMs = 2500;
gfx::Point GetBoundedPosition(const gfx::Point& location_in_screen,
const gfx::Rect& bounds_in_screen) {
return gfx::Point(
std::max(std::min(location_in_screen.x(), bounds_in_screen.right() - 1),
bounds_in_screen.x()),
std::max(std::min(location_in_screen.y(), bounds_in_screen.bottom() - 1),
bounds_in_screen.y()));
}
// Transpose the given |rect|.
void TransposeRect(gfx::Rect* rect) {
rect->SetRect(rect->y(), rect->x(), rect->height(), rect->width());
}
} // namespace
SplitViewController::SplitViewController() {
screen_orientation_ =
Shell::Get()->screen_orientation_controller()->GetCurrentOrientation();
display::Screen::GetScreen()->AddObserver(this);
}
SplitViewController::~SplitViewController() {
display::Screen::GetScreen()->RemoveObserver(this);
EndSplitView();
}
// static
bool SplitViewController::ShouldAllowSplitView() {
if (base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kAshDisableTabletSplitView)) {
return false;
}
if (!Shell::Get()
->tablet_mode_controller()
->IsTabletModeWindowManagerEnabled()) {
return false;
}
return true;
}
// static
bool SplitViewController::IsLeftWindowOnTopOrLeftOfScreen(
blink::WebScreenOrientationLockType screen_orientation) {
return screen_orientation ==
blink::kWebScreenOrientationLockLandscapePrimary ||
screen_orientation ==
blink::kWebScreenOrientationLockPortraitSecondary;
}
bool SplitViewController::CanSnap(aura::Window* window) {
// In M65, some ARC apps can be freely resized and thus are capble of
// displaying in splitscreen, but splitscreen is not supported for ARC apps
// windows yet in M65, thus we should explicity return false here for ARC apps
// windows. Otherwise we may see issues as in https://crbug.com/808748. It
// will be reverted later in M66.
if (window->GetProperty(aura::client::kAppType) ==
static_cast<int>(AppType::ARC_APP)) {
return false;
}
if (!wm::CanActivateWindow(window))
return false;
if (!wm::GetWindowState(window)->CanSnap())
return false;
if (window->delegate()) {
// If the window's minimum size is larger than half of the display's work
// area size, the window can't be snapped in this case.
const gfx::Size min_size = window->delegate()->GetMinimumSize();
const gfx::Rect display_area = GetDisplayWorkAreaBoundsInScreen(window);
bool is_landscape = (display_area.width() > display_area.height());
if ((is_landscape && min_size.width() > display_area.width() / 2) ||
(!is_landscape && min_size.height() > display_area.height() / 2)) {
return false;
}
}
return true;
}
bool SplitViewController::IsSplitViewModeActive() const {
return state_ != NO_SNAP;
}
bool SplitViewController::IsCurrentScreenOrientationLandscape() const {
return screen_orientation_ ==
blink::kWebScreenOrientationLockLandscapePrimary ||
screen_orientation_ ==
blink::kWebScreenOrientationLockLandscapeSecondary;
}
bool SplitViewController::IsCurrentScreenOrientationPrimary() const {
return screen_orientation_ ==
blink::kWebScreenOrientationLockLandscapePrimary ||
screen_orientation_ == blink::kWebScreenOrientationLockPortraitPrimary;
}
void SplitViewController::SnapWindow(aura::Window* window,
SnapPosition snap_position) {
DCHECK(window && CanSnap(window));
DCHECK_NE(snap_position, NONE);
if (state_ == NO_SNAP) {
// Add observers when the split view mode starts.
Shell::Get()->AddShellObserver(this);
Shell::Get()->activation_client()->AddObserver(this);
Shell::Get()->tablet_mode_controller()->AddObserver(this);
Shell::Get()->NotifySplitViewModeStarting();
divider_position_ = GetDefaultDividerPosition(window);
default_snap_position_ = snap_position;
split_view_divider_ =
std::make_unique<SplitViewDivider>(this, window->GetRootWindow());
splitview_start_time_ = base::Time::Now();
}
State previous_state = state_;
if (snap_position == LEFT) {
if (left_window_ != window) {
StopObserving(left_window_);
left_window_ = window;
}
right_window_ = (window == right_window_) ? nullptr : right_window_;
} else if (snap_position == RIGHT) {
if (right_window_ != window) {
StopObserving(right_window_);
right_window_ = window;
}
left_window_ = (window == left_window_) ? nullptr : left_window_;
}
if (left_window_ && right_window_)
state_ = BOTH_SNAPPED;
else if (left_window_)
state_ = LEFT_SNAPPED;
else if (right_window_)
state_ = RIGHT_SNAPPED;
// Update the divider position and window bounds before snapping a new window.
// Since the minimum size of |window| maybe larger than currently bounds in
// |snap_position|.
MoveDividerToClosestFixedPosition();
UpdateSnappedWindowsAndDividerBounds();
StartObserving(window);
const wm::WMEvent event((snap_position == LEFT) ? wm::WM_EVENT_SNAP_LEFT
: wm::WM_EVENT_SNAP_RIGHT);
wm::GetWindowState(window)->OnWMEvent(&event);
wm::ActivateWindow(window);
// Stack the other snapped window below the current active window so that
// the snapped two windows are always the top two windows while resizing.
aura::Window* stacking_target =
(window == left_window_) ? right_window_ : left_window_;
if (stacking_target)
window->parent()->StackChildBelow(stacking_target, window);
NotifySplitViewStateChanged(previous_state, state_);
base::RecordAction(base::UserMetricsAction("SplitView_SnapWindow"));
}
void SplitViewController::SwapWindows() {
if (state_ != BOTH_SNAPPED)
return;
DCHECK(left_window_ && right_window_);
aura::Window* new_left_window = right_window_;
aura::Window* new_right_window = left_window_;
SnapWindow(new_left_window, LEFT);
SnapWindow(new_right_window, RIGHT);
base::RecordAction(
base::UserMetricsAction("SplitView_DoubleTapDividerSwapWindows"));
}
aura::Window* SplitViewController::GetDefaultSnappedWindow() {
if (default_snap_position_ == LEFT)
return left_window_;
if (default_snap_position_ == RIGHT)
return right_window_;
return nullptr;
}
gfx::Rect SplitViewController::GetSnappedWindowBoundsInParent(
aura::Window* window,
SnapPosition snap_position) {
gfx::Rect bounds = GetSnappedWindowBoundsInScreen(window, snap_position);
::wm::ConvertRectFromScreen(window->GetRootWindow(), &bounds);
return bounds;
}
gfx::Rect SplitViewController::GetSnappedWindowBoundsInScreen(
aura::Window* window,
SnapPosition snap_position) {
const gfx::Rect work_area_bounds_in_screen =
GetDisplayWorkAreaBoundsInScreen(window);
if (snap_position == NONE)
return work_area_bounds_in_screen;
// |divide_position_| might not be properly initialized yet.
divider_position_ = (divider_position_ < 0)
? GetDefaultDividerPosition(window)
: divider_position_;
const gfx::Rect divider_bounds = SplitViewDivider::GetDividerBoundsInScreen(
work_area_bounds_in_screen, screen_orientation_, divider_position_,
false /* is_dragging */);
gfx::Rect left_or_top_rect;
gfx::Rect right_or_bottom_rect;
SplitRect(work_area_bounds_in_screen, divider_bounds,
IsCurrentScreenOrientationLandscape(), &left_or_top_rect,
&right_or_bottom_rect);
// Adjust the bounds for |left_or_top_rect| and |right_or_bottom_rect| if the
// desired bound is smaller than the minimum bounds of the window.
AdjustSnappedWindowBounds(&left_or_top_rect, &right_or_bottom_rect);
if (IsLeftWindowOnTopOrLeftOfScreen(screen_orientation_))
return (snap_position == LEFT) ? left_or_top_rect : right_or_bottom_rect;
else
return (snap_position == LEFT) ? right_or_bottom_rect : left_or_top_rect;
}
gfx::Rect SplitViewController::GetDisplayWorkAreaBoundsInParent(
aura::Window* window) const {
aura::Window* root_window = window->GetRootWindow();
return screen_util::GetDisplayWorkAreaBoundsInParent(
root_window->GetChildById(kShellWindowId_DefaultContainer));
}
gfx::Rect SplitViewController::GetDisplayWorkAreaBoundsInScreen(
aura::Window* window) const {
gfx::Rect bounds = GetDisplayWorkAreaBoundsInParent(window);
::wm::ConvertRectToScreen(window->GetRootWindow(), &bounds);
return bounds;
}
void SplitViewController::StartResize(const gfx::Point& location_in_screen) {
DCHECK(IsSplitViewModeActive());
is_resizing_ = true;
split_view_divider_->UpdateDividerBounds(is_resizing_);
previous_event_location_ = location_in_screen;
base::RecordAction(base::UserMetricsAction("SplitView_ResizeWindows"));
}
void SplitViewController::Resize(const gfx::Point& location_in_screen) {
DCHECK(IsSplitViewModeActive());
if (!is_resizing_)
return;
const gfx::Rect work_area_bounds =
GetDisplayWorkAreaBoundsInScreen(GetDefaultSnappedWindow());
gfx::Point modified_location_in_screen =
GetBoundedPosition(location_in_screen, work_area_bounds);
// Update |divider_position_|.
UpdateDividerPosition(modified_location_in_screen);
NotifyDividerPositionChanged();
// Update the black scrim layer's bounds and opacity.
UpdateBlackScrim(modified_location_in_screen);
// Update the snapped window/windows and divider's position.
UpdateSnappedWindowsAndDividerBounds();
previous_event_location_ = modified_location_in_screen;
}
void SplitViewController::EndResize(const gfx::Point& location_in_screen) {
DCHECK(IsSplitViewModeActive());
// TODO(xdai): Use fade out animation instead of just removing it.
black_scrim_layer_.reset();
is_resizing_ = false;
const gfx::Rect work_area_bounds =
GetDisplayWorkAreaBoundsInScreen(GetDefaultSnappedWindow());
gfx::Point modified_location_in_screen =
GetBoundedPosition(location_in_screen, work_area_bounds);
UpdateDividerPosition(modified_location_in_screen);
MoveDividerToClosestFixedPosition();
NotifyDividerPositionChanged();
// Need to update snapped windows bounds even if the split view mode may have
// to exit. Otherwise it's possible for a snapped window stuck in the edge of
// of the screen while overview mode is active.
UpdateSnappedWindowsAndDividerBounds();
// Check if one of the snapped windows needs to be closed.
if (ShouldEndSplitViewAfterResizing()) {
aura::Window* active_window = GetActiveWindowAfterResizingUponExit();
// Track the window that needs to be put back into the overview list if we
// remain in overview mode.
aura::Window* insert_overview_window = nullptr;
if (Shell::Get()->window_selector_controller()->IsSelecting())
insert_overview_window = GetDefaultSnappedWindow();
EndSplitView();
if (active_window) {
EndOverview();
wm::ActivateWindow(active_window);
} else if (insert_overview_window) {
Shell::Get()->window_selector_controller()->window_selector()->AddItem(
insert_overview_window);
}
}
}
void SplitViewController::ShowAppCannotSnapToast() {
ash::ToastData toast(
kAppCannotSnapToastId,
l10n_util::GetStringUTF16(IDS_ASH_SPLIT_VIEW_CANNOT_SNAP),
kAppCannotSnapToastDurationMs, base::Optional<base::string16>());
ash::Shell::Get()->toast_manager()->Show(toast);
}
void SplitViewController::EndSplitView() {
if (!IsSplitViewModeActive())
return;
// Remove observers when the split view mode ends.
Shell::Get()->RemoveShellObserver(this);
Shell::Get()->activation_client()->RemoveObserver(this);
Shell::Get()->tablet_mode_controller()->RemoveObserver(this);
StopObserving(left_window_);
StopObserving(right_window_);
left_window_ = nullptr;
right_window_ = nullptr;
split_view_divider_.reset();
black_scrim_layer_.reset();
default_snap_position_ = NONE;
divider_position_ = -1;
State previous_state = state_;
state_ = NO_SNAP;
NotifySplitViewStateChanged(previous_state, state_);
Shell::Get()->NotifySplitViewModeEnded();
base::RecordAction(base::UserMetricsAction("SplitView_EndSplitView"));
UMA_HISTOGRAM_LONG_TIMES("Ash.SplitView.TimeInSplitView",
base::Time::Now() - splitview_start_time_);
}
void SplitViewController::AddObserver(Observer* observer) {
observers_.AddObserver(observer);
}
void SplitViewController::RemoveObserver(Observer* observer) {
observers_.RemoveObserver(observer);
}
void SplitViewController::OnWindowDestroying(aura::Window* window) {
DCHECK(IsSplitViewModeActive());
DCHECK(window == left_window_ || window == right_window_);
OnSnappedWindowMinimizedOrDestroyed(window);
}
void SplitViewController::OnPostWindowStateTypeChange(
ash::wm::WindowState* window_state,
ash::mojom::WindowStateType old_type) {
DCHECK(IsSplitViewModeActive());
if (window_state->IsFullscreen() || window_state->IsMaximized()) {
// End split view mode if one of the snapped windows gets maximized /
// full-screened. Also end overview mode if overview mode is active at the
// moment.
EndSplitView();
EndOverview();
} else if (window_state->IsMinimized()) {
OnSnappedWindowMinimizedOrDestroyed(window_state->window());
}
}
void SplitViewController::OnWindowActivated(ActivationReason reason,
aura::Window* gained_active,
aura::Window* lost_active) {
DCHECK(IsSplitViewModeActive());
// If |gained_active| was activated as a side effect of a window disposition
// change, do nothing. For example, when a snapped window is closed, another
// window will be activated before OnWindowDestroying() is called. We should
// not try to snap another window in this case.
if (reason == ActivationReason::WINDOW_DISPOSITION_CHANGED)
return;
// Only snap window that can be snapped but hasn't been snapped.
if (!gained_active || gained_active == left_window_ ||
gained_active == right_window_ || !CanSnap(gained_active)) {
return;
}
// Only window in MRU list can be snapped.
aura::Window::Windows windows =
Shell::Get()->mru_window_tracker()->BuildMruWindowList();
if (std::find(windows.begin(), windows.end(), gained_active) == windows.end())
return;
// Snap the window on the non-default side of the screen if split view mode
// is active.
if (default_snap_position_ == LEFT)
SnapWindow(gained_active, SplitViewController::RIGHT);
else if (default_snap_position_ == RIGHT)
SnapWindow(gained_active, SplitViewController::LEFT);
}
void SplitViewController::OnOverviewModeStarting() {
DCHECK(IsSplitViewModeActive());
// If split view mode is active, reset |state_| to make it be able to select
// another window from overview window grid.
State previous_state = state_;
if (default_snap_position_ == LEFT) {
StopObserving(right_window_);
right_window_ = nullptr;
state_ = LEFT_SNAPPED;
} else if (default_snap_position_ == RIGHT) {
StopObserving(left_window_);
left_window_ = nullptr;
state_ = RIGHT_SNAPPED;
}
NotifySplitViewStateChanged(previous_state, state_);
}
void SplitViewController::OnOverviewModeEnded() {
DCHECK(IsSplitViewModeActive());
// If split view mode is active but only has one snapped window, use the MRU
// window list to auto select another window to snap.
if (state_ != BOTH_SNAPPED) {
aura::Window::Windows windows =
Shell::Get()->mru_window_tracker()->BuildMruWindowList();
for (auto* window : windows) {
if (CanSnap(window) && window != GetDefaultSnappedWindow()) {
if (default_snap_position_ == LEFT)
SnapWindow(window, SplitViewController::RIGHT);
else if (default_snap_position_ == RIGHT)
SnapWindow(window, SplitViewController::LEFT);
break;
}
}
}
}
void SplitViewController::OnDisplayMetricsChanged(
const display::Display& display,
uint32_t metrics) {
if (!display.IsInternal())
return;
// We need update |screen_orientation_| even though split view mode is not
// active at the moment.
blink::WebScreenOrientationLockType previous_screen_orientation =
screen_orientation_;
screen_orientation_ =
Shell::Get()->screen_orientation_controller()->GetCurrentOrientation();
if (!IsSplitViewModeActive())
return;
display::Display current_display =
display::Screen::GetScreen()->GetDisplayNearestWindow(
GetDefaultSnappedWindow());
if (display.id() != current_display.id())
return;
// If one of the snapped windows becomes unsnappable, end the split view mode
// directly.
if ((left_window_ && !CanSnap(left_window_)) ||
(right_window_ && !CanSnap(right_window_))) {
EndSplitView();
return;
}
// Update |divider_position_| if the top/left window changes.
if ((metrics & (display::DisplayObserver::DISPLAY_METRIC_ROTATION)) &&
(IsLeftWindowOnTopOrLeftOfScreen(previous_screen_orientation) !=
IsLeftWindowOnTopOrLeftOfScreen(screen_orientation_))) {
const int work_area_long_length = GetDividerEndPosition();
const gfx::Size divider_size = SplitViewDivider::GetDividerSize(
display.work_area(), screen_orientation_, false /* is_dragging */);
const int divider_short_length =
std::min(divider_size.width(), divider_size.height());
divider_position_ =
work_area_long_length - divider_short_length - divider_position_;
}
// For other display configuration changes, we only move the divider to the
// closest fixed position.
if (!is_resizing_)
MoveDividerToClosestFixedPosition();
NotifyDividerPositionChanged();
UpdateSnappedWindowsAndDividerBounds();
}
void SplitViewController::OnTabletModeEnding() {
EndSplitView();
}
void SplitViewController::StartObserving(aura::Window* window) {
if (window && !window->HasObserver(this)) {
window->AddObserver(this);
wm::GetWindowState(window)->AddObserver(this);
split_view_divider_->AddObservedWindow(window);
}
}
void SplitViewController::StopObserving(aura::Window* window) {
if (window && window->HasObserver(this)) {
window->RemoveObserver(this);
wm::GetWindowState(window)->RemoveObserver(this);
split_view_divider_->RemoveObservedWindow(window);
}
}
void SplitViewController::NotifySplitViewStateChanged(State previous_state,
State state) {
// It's possible that |previous_state| equals to |state| (e.g., snap a window
// to LEFT and then snap another window to LEFT too.) In this case we still
// should notify its observers.
for (Observer& observer : observers_)
observer.OnSplitViewStateChanged(previous_state, state);
}
void SplitViewController::NotifyDividerPositionChanged() {
for (Observer& observer : observers_)
observer.OnSplitViewDividerPositionChanged();
}
int SplitViewController::GetDefaultDividerPosition(aura::Window* window) const {
const gfx::Rect work_area_bounds_in_screen =
GetDisplayWorkAreaBoundsInScreen(window);
const gfx::Size divider_size = SplitViewDivider::GetDividerSize(
work_area_bounds_in_screen, screen_orientation_, false /* is_dragging */);
if (IsCurrentScreenOrientationLandscape())
return (work_area_bounds_in_screen.width() - divider_size.width()) * 0.5f;
else
return (work_area_bounds_in_screen.height() - divider_size.height()) * 0.5f;
}
void SplitViewController::UpdateBlackScrim(
const gfx::Point& location_in_screen) {
DCHECK(IsSplitViewModeActive());
if (!black_scrim_layer_) {
// Create an invisible black scrim layer.
black_scrim_layer_ = std::make_unique<ui::Layer>(ui::LAYER_SOLID_COLOR);
black_scrim_layer_->SetColor(SK_ColorBLACK);
GetDefaultSnappedWindow()->GetRootWindow()->layer()->Add(
black_scrim_layer_.get());
GetDefaultSnappedWindow()->GetRootWindow()->layer()->StackAtTop(
black_scrim_layer_.get());
}
// Decide where the black scrim should show and update its bounds.
SnapPosition position = GetBlackScrimPosition(location_in_screen);
if (position == NONE) {
black_scrim_layer_.reset();
return;
}
black_scrim_layer_->SetBounds(
GetSnappedWindowBoundsInScreen(GetDefaultSnappedWindow(), position));
// Update its opacity. The opacity increases as it gets closer to the edge of
// the screen.
float opacity = 0.f;
const gfx::Rect work_area_bounds =
GetDisplayWorkAreaBoundsInScreen(GetDefaultSnappedWindow());
if (IsCurrentScreenOrientationLandscape()) {
int distance_x =
std::min(std::abs(location_in_screen.x() - work_area_bounds.x()),
std::abs(work_area_bounds.right() - location_in_screen.x()));
opacity = 1.f - float(distance_x) / float(work_area_bounds.width());
} else {
int distance_y =
std::min(std::abs(location_in_screen.y() - work_area_bounds.y()),
std::abs(work_area_bounds.bottom() - location_in_screen.y()));
opacity = 1.f - float(distance_y) / float(work_area_bounds.height());
}
black_scrim_layer_->SetOpacity(opacity);
}
void SplitViewController::UpdateSnappedWindowsAndDividerBounds() {
DCHECK(IsSplitViewModeActive());
// Update the snapped windows' bounds.
if (left_window_ && wm::GetWindowState(left_window_)->IsSnapped()) {
const wm::WMEvent left_window_event(wm::WM_EVENT_SNAP_LEFT);
wm::GetWindowState(left_window_)->OnWMEvent(&left_window_event);
}
if (right_window_ && wm::GetWindowState(right_window_)->IsSnapped()) {
const wm::WMEvent right_window_event(wm::WM_EVENT_SNAP_RIGHT);
wm::GetWindowState(right_window_)->OnWMEvent(&right_window_event);
}
// Update divider's bounds.
split_view_divider_->UpdateDividerBounds(is_resizing_);
}
SplitViewController::SnapPosition SplitViewController::GetBlackScrimPosition(
const gfx::Point& location_in_screen) {
const gfx::Rect work_area_bounds =
GetDisplayWorkAreaBoundsInScreen(GetDefaultSnappedWindow());
if (!work_area_bounds.Contains(location_in_screen))
return NONE;
gfx::Size left_window_min_size, right_window_min_size;
if (left_window_ && left_window_->delegate())
left_window_min_size = left_window_->delegate()->GetMinimumSize();
if (right_window_ && right_window_->delegate())
right_window_min_size = right_window_->delegate()->GetMinimumSize();
bool is_primary = IsCurrentScreenOrientationPrimary();
int long_length = GetDividerEndPosition();
// The distance from the current resizing position to the left or right side
// of the screen. Note: left or right side here means the side of the
// |left_window_| or |right_window_|.
int left_window_distance = 0, right_window_distance = 0;
int min_left_length = 0, min_right_length = 0;
if (IsCurrentScreenOrientationLandscape()) {
int left_distance = location_in_screen.x() - work_area_bounds.x();
int right_distance = work_area_bounds.right() - location_in_screen.x();
left_window_distance = is_primary ? left_distance : right_distance;
right_window_distance = is_primary ? right_distance : left_distance;
min_left_length = left_window_min_size.width();
min_right_length = right_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();
left_window_distance = is_primary ? bottom_distance : top_distance;
right_window_distance = is_primary ? top_distance : bottom_distance;
min_left_length = left_window_min_size.height();
min_right_length = right_window_min_size.height();
}
if (left_window_distance < long_length * kOneThirdPositionRatio ||
left_window_distance < min_left_length) {
return LEFT;
}
if (right_window_distance < long_length * kOneThirdPositionRatio ||
right_window_distance < min_right_length) {
return RIGHT;
}
return NONE;
}
void SplitViewController::UpdateDividerPosition(
const gfx::Point& location_in_screen) {
if (IsCurrentScreenOrientationLandscape()) {
divider_position_ += location_in_screen.x() - previous_event_location_.x();
} else {
divider_position_ += location_in_screen.y() - previous_event_location_.y();
}
divider_position_ = std::max(0, divider_position_);
}
void SplitViewController::SplitRect(const gfx::Rect& work_area_rect,
const gfx::Rect& divider_rect,
const bool is_split_vertically,
gfx::Rect* left_or_top_rect,
gfx::Rect* right_or_bottom_rect) {
if (is_split_vertically) {
left_or_top_rect->SetRect(work_area_rect.x(), work_area_rect.y(),
divider_rect.x() - work_area_rect.x(),
work_area_rect.height());
right_or_bottom_rect->SetRect(divider_rect.right(), work_area_rect.y(),
work_area_rect.width() -
left_or_top_rect->width() -
divider_rect.width(),
work_area_rect.height());
} else {
left_or_top_rect->SetRect(work_area_rect.x(), work_area_rect.y(),
work_area_rect.width(),
divider_rect.y() - work_area_rect.y());
right_or_bottom_rect->SetRect(
work_area_rect.x(), divider_rect.bottom(), work_area_rect.width(),
work_area_rect.height() - left_or_top_rect->height() -
divider_rect.height());
}
}
void SplitViewController::MoveDividerToClosestFixedPosition() {
DCHECK(IsSplitViewModeActive());
const gfx::Rect work_area_bounds_in_screen =
GetDisplayWorkAreaBoundsInScreen(GetDefaultSnappedWindow());
const gfx::Size divider_size = SplitViewDivider::GetDividerSize(
work_area_bounds_in_screen, screen_orientation_, false /* is_dragging */);
const int divider_thickness =
std::min(divider_size.width(), divider_size.height());
// The values in |kFixedPositionRatios| represent the fixed position of the
// center of the divider while |divider_position_| represent the origin of the
// divider rectangle. So, before calling FindClosestFixedPositionRatio,
// extract the center from |divider_position_|. The result will also be the
// center of the divider, so extract the origin, unless the result is on of
// the endpoints.
int work_area_long_length = GetDividerEndPosition();
float closest_ratio = FindClosestPositionRatio(
divider_position_ + std::floor(divider_thickness / 2.f),
work_area_long_length);
divider_position_ = std::floor(work_area_long_length * closest_ratio);
if (closest_ratio > 0.f && closest_ratio < 1.f)
divider_position_ -= std::floor(divider_thickness / 2.f);
}
bool SplitViewController::ShouldEndSplitViewAfterResizing() {
DCHECK(IsSplitViewModeActive());
return divider_position_ == 0 || divider_position_ == GetDividerEndPosition();
}
aura::Window* SplitViewController::GetActiveWindowAfterResizingUponExit() {
DCHECK(IsSplitViewModeActive());
if (!ShouldEndSplitViewAfterResizing())
return nullptr;
if (divider_position_ == 0) {
return IsLeftWindowOnTopOrLeftOfScreen(screen_orientation_) ? right_window_
: left_window_;
} else {
return IsLeftWindowOnTopOrLeftOfScreen(screen_orientation_) ? left_window_
: right_window_;
}
}
int SplitViewController::GetDividerEndPosition() {
const gfx::Rect work_area_bounds =
GetDisplayWorkAreaBoundsInScreen(GetDefaultSnappedWindow());
return std::max(work_area_bounds.width(), work_area_bounds.height());
}
void SplitViewController::OnSnappedWindowMinimizedOrDestroyed(
aura::Window* window) {
DCHECK(window);
DCHECK(window == left_window_ || window == right_window_);
if (left_window_ == window) {
StopObserving(left_window_);
left_window_ = nullptr;
} else {
StopObserving(right_window_);
right_window_ = nullptr;
}
if (!left_window_ && !right_window_) {
// If there is no snapped window at this moment, ends split view mode. Note
// this will update overview window grid bounds if the overview mode is
// active at the moment.
EndSplitView();
} else {
// If there is still one snapped window after minimizing/closing one snapped
// window, update its snap state and open overview window grid.
State previous_state = state_;
state_ = left_window_ ? LEFT_SNAPPED : RIGHT_SNAPPED;
default_snap_position_ = left_window_ ? LEFT : RIGHT;
NotifySplitViewStateChanged(previous_state, state_);
StartOverview();
}
}
void SplitViewController::AdjustSnappedWindowBounds(
gfx::Rect* left_or_top_rect,
gfx::Rect* right_or_bottom_rect) {
aura::Window* left_or_top_window =
IsLeftWindowOnTopOrLeftOfScreen(screen_orientation_) ? left_window_
: right_window_;
aura::Window* right_or_bottom_window =
IsLeftWindowOnTopOrLeftOfScreen(screen_orientation_) ? right_window_
: left_window_;
auto get_minimum_size = [](aura::Window* window, bool is_landscape) -> int {
int minimum_width = 0;
if (window && window->delegate()) {
gfx::Size minimum_size = window->delegate()->GetMinimumSize();
minimum_width =
is_landscape ? minimum_size.width() : minimum_size.height();
}
return minimum_width;
};
bool is_landscape = IsCurrentScreenOrientationLandscape();
int left_minimum_width = get_minimum_size(left_or_top_window, is_landscape);
int right_minimum_width =
get_minimum_size(right_or_bottom_window, is_landscape);
if (!is_landscape) {
TransposeRect(left_or_top_rect);
TransposeRect(right_or_bottom_rect);
}
if (left_or_top_rect->width() < left_minimum_width) {
left_or_top_rect->set_x(left_or_top_rect->x() -
(left_minimum_width - left_or_top_rect->width()));
left_or_top_rect->set_width(left_minimum_width);
}
if (right_or_bottom_rect->width() < right_minimum_width)
right_or_bottom_rect->set_width(right_minimum_width);
if (!is_landscape) {
TransposeRect(left_or_top_rect);
TransposeRect(right_or_bottom_rect);
}
}
float SplitViewController::FindClosestPositionRatio(float distance,
float length) {
float current_ratio = distance / length;
float closest_ratio = 0.f;
std::vector<float> position_ratios(
kFixedPositionRatios,
kFixedPositionRatios + sizeof(kFixedPositionRatios) / sizeof(float));
GetDividerOptionalPositionRatios(&position_ratios);
for (float ratio : position_ratios) {
if (std::abs(current_ratio - ratio) <
std::abs(current_ratio - closest_ratio)) {
closest_ratio = ratio;
}
}
return closest_ratio;
}
void SplitViewController::GetDividerOptionalPositionRatios(
std::vector<float>* position_ratios) {
bool is_left_or_top = IsLeftWindowOnTopOrLeftOfScreen(screen_orientation_);
aura::Window* left_or_top_window =
is_left_or_top ? left_window_ : right_window_;
aura::Window* right_or_bottom_window =
is_left_or_top ? right_window_ : left_window_;
bool is_landscape = IsCurrentScreenOrientationLandscape();
int long_length = GetDividerEndPosition();
float min_size_left_ratio = 0.f, min_size_right_ratio = 0.f;
int min_left_size = 0, min_right_size = 0;
if (left_or_top_window && left_or_top_window->delegate()) {
gfx::Size min_size = left_or_top_window->delegate()->GetMinimumSize();
min_left_size = is_landscape ? min_size.width() : min_size.height();
}
if (right_or_bottom_window && right_or_bottom_window->delegate()) {
gfx::Size min_size = right_or_bottom_window->delegate()->GetMinimumSize();
min_right_size = is_landscape ? min_size.width() : min_size.height();
}
min_size_left_ratio = static_cast<float>(min_left_size) / long_length;
min_size_right_ratio = static_cast<float>(min_right_size) / long_length;
if (min_size_left_ratio <= kOneThirdPositionRatio)
position_ratios->push_back(kOneThirdPositionRatio);
if (min_size_right_ratio <= kOneThirdPositionRatio)
position_ratios->push_back(kTwoThirdPositionRatio);
}
void SplitViewController::StartOverview() {
if (!Shell::Get()->window_selector_controller()->IsSelecting())
Shell::Get()->window_selector_controller()->ToggleOverview();
}
void SplitViewController::EndOverview() {
if (Shell::Get()->window_selector_controller()->IsSelecting())
Shell::Get()->window_selector_controller()->ToggleOverview();
}
} // namespace ash