#include "ash/wm/splitview/split_view_utils.h"
#include <vector>
#include "ash/accessibility/accessibility_controller.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/constants/notifier_catalogs.h"
#include "ash/display/screen_orientation_controller.h"
#include "ash/public/cpp/system/toast_data.h"
#include "ash/public/cpp/window_properties.h"
#include "ash/root_window_controller.h"
#include "ash/screen_util.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/system/toast/toast_manager_impl.h"
#include "ash/wm/desks/desks_util.h"
#include "ash/wm/mru_window_tracker.h"
#include "ash/wm/overview/overview_controller.h"
#include "ash/wm/overview/overview_session.h"
#include "ash/wm/overview/overview_utils.h"
#include "ash/wm/screen_pinning_controller.h"
#include "ash/wm/snap_group/snap_group.h"
#include "ash/wm/snap_group/snap_group_constants.h"
#include "ash/wm/snap_group/snap_group_controller.h"
#include "ash/wm/splitview/layout_divider_controller.h"
#include "ash/wm/splitview/split_view_constants.h"
#include "ash/wm/splitview/split_view_controller.h"
#include "ash/wm/splitview/split_view_overview_session.h"
#include "ash/wm/splitview/split_view_types.h"
#include "ash/wm/window_positioning_utils.h"
#include "ash/wm/window_restore/window_restore_controller.h"
#include "ash/wm/window_state.h"
#include "ash/wm/window_util.h"
#include "ash/wm/wm_metrics.h"
#include "base/containers/adapters.h"
#include "base/numerics/ranges.h"
#include "base/time/time.h"
#include "chromeos/ui/frame/caption_buttons/snap_controller.h"
#include "components/app_restore/window_properties.h"
#include "components/prefs/pref_service.h"
#include "ui/aura/window_delegate.h"
#include "ui/base/hit_test.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/layer_animator.h"
#include "ui/compositor/scoped_layer_animation_settings.h"
#include "ui/display/screen.h"
#include "ui/gfx/geometry/point.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/views/widget/widget_delegate.h"
#include "ui/wm/core/transient_window_manager.h"
#include "ui/wm/core/window_util.h"
namespace ash {
namespace {
using chromeos::WindowStateType;
constexpr base::TimeDelta kHighlightsFadeInOut = base::Milliseconds(250);
constexpr base::TimeDelta kOtherFadeInOut = base::Milliseconds(133);
constexpr base::TimeDelta kOtherFadeInDelay = base::Milliseconds(117);
constexpr base::TimeDelta kPreviewAreaFadeOut = base::Milliseconds(67);
constexpr base::TimeDelta kLabelAnimation = base::Milliseconds(83);
constexpr base::TimeDelta kLabelAnimationDelay = base::Milliseconds(167);
constexpr char kSnapWindowSuggestionsHistogramPrefix[] =
"Ash.SnapWindowSuggestions.";
constexpr char kHistogramPrefix[] = "Ash.SplitViewOverviewSession.";
constexpr char kWindowLayoutCompleteOnSessionExitRootWord[] =
"WindowLayoutCompleteOnSessionExit";
constexpr char kExitPointRootWord[] = "ExitPoint";
struct AnimationValues {
base::TimeDelta duration;
gfx::Tween::Type tween_type;
ui::LayerAnimator::PreemptionStrategy preemption_strategy =
ui::LayerAnimator::IMMEDIATELY_SET_NEW_TARGET;
base::TimeDelta delay;
};
AnimationValues GetAnimationValuesForType(SplitviewAnimationType type) {
switch (type) {
case SPLITVIEW_ANIMATION_HIGHLIGHT_FADE_IN:
case SPLITVIEW_ANIMATION_HIGHLIGHT_FADE_IN_CANNOT_SNAP:
case SPLITVIEW_ANIMATION_HIGHLIGHT_FADE_OUT:
case SPLITVIEW_ANIMATION_PREVIEW_AREA_FADE_IN:
case SPLITVIEW_ANIMATION_OVERVIEW_ITEM_FADE_IN:
case SPLITVIEW_ANIMATION_OVERVIEW_ITEM_FADE_OUT:
case SPLITVIEW_ANIMATION_TEXT_FADE_IN_WITH_HIGHLIGHT:
case SPLITVIEW_ANIMATION_TEXT_FADE_OUT_WITH_HIGHLIGHT:
case SPLITVIEW_ANIMATION_PREVIEW_AREA_SLIDE_IN:
case SPLITVIEW_ANIMATION_PREVIEW_AREA_SLIDE_OUT:
case SPLITVIEW_ANIMATION_PREVIEW_AREA_TEXT_SLIDE_IN:
case SPLITVIEW_ANIMATION_PREVIEW_AREA_TEXT_SLIDE_OUT:
return {.duration = kHighlightsFadeInOut,
.tween_type = gfx::Tween::FAST_OUT_SLOW_IN};
case SPLITVIEW_ANIMATION_OTHER_HIGHLIGHT_FADE_IN:
case SPLITVIEW_ANIMATION_OTHER_HIGHLIGHT_FADE_IN_CANNOT_SNAP:
case SPLITVIEW_ANIMATION_OTHER_HIGHLIGHT_SLIDE_IN:
case SPLITVIEW_ANIMATION_OTHER_HIGHLIGHT_TEXT_SLIDE_IN:
return {.duration = kOtherFadeInOut,
.tween_type = gfx::Tween::LINEAR_OUT_SLOW_IN,
.preemption_strategy = ui::LayerAnimator::ENQUEUE_NEW_ANIMATION,
.delay = kOtherFadeInDelay};
case SPLITVIEW_ANIMATION_OTHER_HIGHLIGHT_FADE_OUT:
case SPLITVIEW_ANIMATION_OTHER_HIGHLIGHT_SLIDE_OUT:
case SPLITVIEW_ANIMATION_OTHER_HIGHLIGHT_TEXT_SLIDE_OUT:
return {.duration = kOtherFadeInOut,
.tween_type = gfx::Tween::FAST_OUT_LINEAR_IN};
case SPLITVIEW_ANIMATION_PREVIEW_AREA_FADE_OUT:
case SPLITVIEW_ANIMATION_PREVIEW_AREA_NIX_INSET:
return {.duration = kPreviewAreaFadeOut,
.tween_type = gfx::Tween::FAST_OUT_LINEAR_IN};
case SPLITVIEW_ANIMATION_TEXT_FADE_IN:
return {.duration = kLabelAnimation,
.tween_type = gfx::Tween::LINEAR_OUT_SLOW_IN,
.preemption_strategy = ui::LayerAnimator::ENQUEUE_NEW_ANIMATION,
.delay = kLabelAnimationDelay};
case SPLITVIEW_ANIMATION_TEXT_FADE_OUT:
return {.duration = kLabelAnimation,
.tween_type = gfx::Tween::FAST_OUT_LINEAR_IN};
case SPLITVIEW_ANIMATION_SET_WINDOW_TRANSFORM:
return {.duration = kSplitviewWindowTransformDuration,
.tween_type = gfx::Tween::FAST_OUT_SLOW_IN,
.preemption_strategy =
ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET};
}
NOTREACHED();
}
void ApplyAnimationSettings(
ui::LayerAnimator* animator,
ui::LayerAnimationElement::AnimatableProperties animated_property,
base::TimeDelta duration,
gfx::Tween::Type tween,
ui::LayerAnimator::PreemptionStrategy preemption_strategy,
base::TimeDelta delay,
ui::ScopedLayerAnimationSettings& out_settings) {
CHECK_EQ(out_settings.GetAnimator(), animator);
out_settings.SetTransitionDuration(duration);
out_settings.SetTweenType(tween);
out_settings.SetPreemptionStrategy(preemption_strategy);
if (!delay.is_zero()) {
animator->SchedulePauseForProperties(delay, animated_property);
}
}
const char* GetSnapActionSourceMetricComponent(
WindowSnapActionSource snap_action_source) {
switch (snap_action_source) {
case WindowSnapActionSource::kNotSpecified:
return "NotSpecified";
case WindowSnapActionSource::kDragWindowToEdgeToSnap:
return "DragWindowToEdgeToSnap";
case WindowSnapActionSource::kLongPressCaptionButtonToSnap:
return "LongPressCaptionButtonToSnap";
case WindowSnapActionSource::kKeyboardShortcutToSnap:
return "KeyboardShortcutToSnap";
case WindowSnapActionSource::kDragOrSelectOverviewWindowToSnap:
return "DragOrSelectOverviewWindowToSnap";
case WindowSnapActionSource::kLongPressOverviewButtonToSnap:
return "LongPressOverviewButtonToSnap";
case WindowSnapActionSource::kDragUpFromShelfToSnap:
return "DragUpFromShelfToSnap";
case WindowSnapActionSource::kDragDownFromTopToSnap:
return "DragDownFromTopToSnap";
case WindowSnapActionSource::kDragTabToSnap:
return "DragTabToSnap";
case WindowSnapActionSource::kAutoSnapInSplitView:
return "AutoSnapInSplitView";
case WindowSnapActionSource::kSnapByWindowStateRestore:
return "SnapByWindowStateRestore";
case WindowSnapActionSource::kSnapByWindowLayoutMenu:
return "SnapByWindowLayoutMenu";
case WindowSnapActionSource::kSnapByFullRestoreOrDeskTemplateOrSavedDesk:
return "SnapByFullRestoreOrDeskTemplateOrSavedDesk";
case WindowSnapActionSource::kSnapByClamshellTabletTransition:
return "SnapByClamshellTabletTransition";
case WindowSnapActionSource::kSnapByDeskOrSessionChange:
return "SnapByDeskOrSessionChange";
case WindowSnapActionSource::kSnapGroupWindowUpdate:
return "SnapGroupWindowUpdate";
case WindowSnapActionSource::kSnapBySwapWindowsInSnapGroup:
return "SnapBySwapWindowsInSnapGroup";
}
}
void AppendUIModeToHistogram(std::string& histogram_name) {
histogram_name.append(display::Screen::Get()->InTabletMode()
? ".TabletMode"
: ".ClamshellMode");
}
bool IsPartialOverviewEmptyForActiveDesk(aura::Window* window) {
for (auto win :
Shell::Get()->mru_window_tracker()->BuildMruWindowList(kActiveDesk)) {
if (win != window && wm::GetTransientRoot(win) != window &&
win->GetRootWindow() == window->GetRootWindow()) {
return false;
}
}
return true;
}
}
WindowTransformAnimationObserver::WindowTransformAnimationObserver(
aura::Window* window)
: window_(window) {
window_->AddObserver(this);
}
WindowTransformAnimationObserver::~WindowTransformAnimationObserver() {
if (window_)
window_->RemoveObserver(this);
}
void WindowTransformAnimationObserver::OnImplicitAnimationsCompleted() {
if (!window_->layer()->GetTargetTransform().IsIdentity()) {
delete this;
return;
}
for (aura::Window* transient_window :
wm::TransientWindowManager::GetOrCreate(window_)->transient_children()) {
views::BubbleDialogDelegate* bubble_delegate_view =
window_util::AsBubbleDialogDelegate(transient_window);
if (bubble_delegate_view) {
if (!bubble_delegate_view->GetAnchorRect().IsEmpty() ||
bubble_delegate_view->GetAnchorView()) {
bubble_delegate_view->OnAnchorBoundsChanged();
}
}
}
delete this;
}
void WindowTransformAnimationObserver::OnWindowDestroying(
aura::Window* window) {
delete this;
}
void DoSplitviewOpacityAnimation(ui::Layer* layer,
SplitviewAnimationType type) {
float target_opacity = 0.f;
switch (type) {
case SPLITVIEW_ANIMATION_HIGHLIGHT_FADE_OUT:
case SPLITVIEW_ANIMATION_OTHER_HIGHLIGHT_FADE_OUT:
case SPLITVIEW_ANIMATION_OVERVIEW_ITEM_FADE_OUT:
case SPLITVIEW_ANIMATION_PREVIEW_AREA_FADE_OUT:
case SPLITVIEW_ANIMATION_TEXT_FADE_OUT:
case SPLITVIEW_ANIMATION_TEXT_FADE_OUT_WITH_HIGHLIGHT:
target_opacity = 0.f;
break;
case SPLITVIEW_ANIMATION_PREVIEW_AREA_FADE_IN:
case SPLITVIEW_ANIMATION_HIGHLIGHT_FADE_IN:
case SPLITVIEW_ANIMATION_OTHER_HIGHLIGHT_FADE_IN:
case SPLITVIEW_ANIMATION_HIGHLIGHT_FADE_IN_CANNOT_SNAP:
case SPLITVIEW_ANIMATION_OTHER_HIGHLIGHT_FADE_IN_CANNOT_SNAP:
target_opacity = kHighlightOpacity;
break;
case SPLITVIEW_ANIMATION_OVERVIEW_ITEM_FADE_IN:
case SPLITVIEW_ANIMATION_TEXT_FADE_IN:
case SPLITVIEW_ANIMATION_TEXT_FADE_IN_WITH_HIGHLIGHT:
target_opacity = 1.f;
break;
default:
NOTREACHED() << "Not a valid split view opacity animation type.";
}
if (layer->GetTargetOpacity() == target_opacity)
return;
const AnimationValues values = GetAnimationValuesForType(type);
ui::LayerAnimator* animator = layer->GetAnimator();
ui::ScopedLayerAnimationSettings settings(animator);
ApplyAnimationSettings(animator, ui::LayerAnimationElement::OPACITY,
values.duration, values.tween_type,
values.preemption_strategy, values.delay, settings);
layer->SetOpacity(target_opacity);
}
void DoSplitviewTransformAnimation(
ui::Layer* layer,
SplitviewAnimationType type,
const gfx::Transform& target_transform,
const std::vector<ui::ImplicitAnimationObserver*>& animation_observers) {
if (layer->GetTargetTransform() == target_transform)
return;
switch (type) {
case SPLITVIEW_ANIMATION_OTHER_HIGHLIGHT_TEXT_SLIDE_IN:
case SPLITVIEW_ANIMATION_OTHER_HIGHLIGHT_TEXT_SLIDE_OUT:
case SPLITVIEW_ANIMATION_PREVIEW_AREA_NIX_INSET:
case SPLITVIEW_ANIMATION_PREVIEW_AREA_TEXT_SLIDE_IN:
case SPLITVIEW_ANIMATION_PREVIEW_AREA_TEXT_SLIDE_OUT:
case SPLITVIEW_ANIMATION_SET_WINDOW_TRANSFORM:
break;
default:
NOTREACHED() << "Not a valid split view transform type.";
}
const AnimationValues values = GetAnimationValuesForType(type);
ui::LayerAnimator* animator = layer->GetAnimator();
ui::ScopedLayerAnimationSettings settings(animator);
for (ui::ImplicitAnimationObserver* animation_observer :
animation_observers) {
settings.AddObserver(animation_observer);
}
ApplyAnimationSettings(animator, ui::LayerAnimationElement::TRANSFORM,
values.duration, values.tween_type,
values.preemption_strategy, values.delay, settings);
layer->SetTransform(target_transform);
}
void DoSplitviewClipRectAnimation(
ui::Layer* layer,
SplitviewAnimationType type,
const gfx::Rect& target_clip_rect,
std::unique_ptr<ui::ImplicitAnimationObserver> animation_observer) {
ui::LayerAnimator* animator = layer->GetAnimator();
if (animator->GetTargetClipRect() == target_clip_rect)
return;
switch (type) {
case SPLITVIEW_ANIMATION_OTHER_HIGHLIGHT_SLIDE_IN:
case SPLITVIEW_ANIMATION_OTHER_HIGHLIGHT_SLIDE_OUT:
case SPLITVIEW_ANIMATION_PREVIEW_AREA_NIX_INSET:
case SPLITVIEW_ANIMATION_PREVIEW_AREA_SLIDE_IN:
case SPLITVIEW_ANIMATION_PREVIEW_AREA_SLIDE_OUT:
break;
default:
NOTREACHED() << "Not a valid split view clip rect type.";
}
const AnimationValues values = GetAnimationValuesForType(type);
ui::ScopedLayerAnimationSettings settings(animator);
if (animation_observer.get()) {
settings.AddObserver(animation_observer.release());
}
ApplyAnimationSettings(animator, ui::LayerAnimationElement::CLIP,
values.duration, values.tween_type,
values.preemption_strategy, values.delay, settings);
layer->SetClipRect(target_clip_rect);
}
int GetWindowLength(aura::Window* window, bool horizontal) {
const auto& bounds = window->GetTargetBounds();
return horizontal ? bounds.width() : bounds.height();
}
WindowStateType GetWindowStateTypeFromSnapPosition(SnapPosition snap_position) {
switch (snap_position) {
case SnapPosition::kPrimary:
return WindowStateType::kPrimarySnapped;
case SnapPosition::kSecondary:
return WindowStateType::kSecondarySnapped;
default:
NOTREACHED();
}
}
SnapPosition ToSnapPosition(chromeos::WindowStateType type) {
switch (type) {
case WindowStateType::kPrimarySnapped:
return SnapPosition::kPrimary;
case WindowStateType::kSecondarySnapped:
return SnapPosition::kSecondary;
default:
NOTREACHED();
}
}
SplitViewOverviewSession* GetSplitViewOverviewSession(aura::Window* window) {
return RootWindowController::ForWindow(window)->split_view_overview_session();
}
bool IsSnapped(aura::Window* window) {
return window && WindowState::Get(window)->IsSnapped();
}
void SetWindowTransformDuringResizing(aura::Window* window,
int divider_position) {
const bool is_primary_window = IsPhysicallyLeftOrTop(window);
aura::Window* root_window = window->GetRootWindow();
const int window_size = is_primary_window
? divider_position
: GetDividerPositionUpperLimit(root_window) -
divider_position -
kSplitviewDividerShortSideLength;
const bool horizontal = IsLayoutHorizontal(root_window);
int distance = window_size - GetWindowLength(window, horizontal);
gfx::Transform transform;
if (distance < 0) {
distance = is_primary_window ? distance : -distance;
transform.Translate(horizontal ? distance : 0, horizontal ? 0 : distance);
}
window_util::SetTransform(window, transform);
}
void MaybeRestoreSplitView(bool refresh_snapped_windows) {
if (!ShouldAllowSplitView() || !display::Screen::Get()->InTabletMode()) {
return;
}
SplitViewController* split_view_controller =
SplitViewController::Get(Shell::GetPrimaryRootWindow());
if (refresh_snapped_windows) {
const MruWindowTracker::WindowList windows =
Shell::Get()->mru_window_tracker()->BuildWindowListIgnoreModal(
kActiveDesk);
for (aura::Window* window : windows) {
if (!split_view_controller->CanSnapWindow(window,
chromeos::kDefaultSnapRatio)) {
WindowState::Get(window)->Maximize();
continue;
}
switch (WindowState::Get(window)->GetStateType()) {
case WindowStateType::kPrimarySnapped:
if (!split_view_controller->primary_window()) {
split_view_controller->SnapWindow(
window, SnapPosition::kPrimary,
WindowSnapActionSource::kSnapByDeskOrSessionChange);
}
break;
case WindowStateType::kSecondarySnapped:
if (!split_view_controller->secondary_window()) {
split_view_controller->SnapWindow(
window, SnapPosition::kSecondary,
WindowSnapActionSource::kSnapByDeskOrSessionChange);
}
break;
default:
break;
}
if (split_view_controller->state() ==
SplitViewController::State::kBothSnapped) {
break;
}
}
}
OverviewController* overview_controller = Shell::Get()->overview_controller();
SplitViewController::State state = split_view_controller->state();
if (state == SplitViewController::State::kPrimarySnapped ||
state == SplitViewController::State::kSecondarySnapped) {
overview_controller->StartOverview(OverviewStartAction::kSplitView);
} else if (state == SplitViewController::State::kBothSnapped) {
overview_controller->EndOverview(OverviewEndAction::kSplitView);
}
}
bool ShouldAllowSplitView() {
if (Shell::Get()->screen_pinning_controller()->IsPinned())
return false;
if (display::Screen::Get()->InTabletMode() &&
Shell::Get()->accessibility_controller()->spoken_feedback().enabled()) {
return false;
}
return true;
}
void ShowAppCannotSnapToast() {
Shell::Get()->toast_manager()->Show(
ToastData(kAppCannotSnapToastId, ToastCatalogName::kAppCannotSnap,
l10n_util::GetStringUTF16(IDS_ASH_SPLIT_VIEW_CANNOT_SNAP),
ToastData::kDefaultToastDuration,
false,
true));
}
SnapPosition GetSnapPositionForLocation(
aura::Window* root_window,
const gfx::Point& location_in_screen,
const std::optional<gfx::Point>& initial_location_in_screen,
int snap_distance_from_edge,
int minimum_drag_distance,
int horizontal_edge_inset,
int vertical_edge_inset) {
if (!ShouldAllowSplitView())
return SnapPosition::kNone;
const bool horizontal = IsLayoutHorizontal(root_window);
const bool right_side_up = IsLayoutPrimary(root_window);
const gfx::Rect work_area(
screen_util::GetDisplayWorkAreaBoundsInScreenForActiveDeskContainer(
root_window));
SnapPosition snap_position = SnapPosition::kNone;
if (horizontal) {
gfx::Rect area(work_area);
area.Inset(gfx::Insets::VH(0, horizontal_edge_inset));
if (location_in_screen.x() <= area.x()) {
snap_position =
right_side_up ? SnapPosition::kPrimary : SnapPosition::kSecondary;
} else if (location_in_screen.x() >= area.right() - 1) {
snap_position =
right_side_up ? SnapPosition::kSecondary : SnapPosition::kPrimary;
}
} else {
gfx::Rect area(work_area);
area.Inset(gfx::Insets::VH(vertical_edge_inset, 0));
if (location_in_screen.y() <= area.y()) {
snap_position =
right_side_up ? SnapPosition::kPrimary : SnapPosition::kSecondary;
} else if (location_in_screen.y() >= area.bottom() - 1) {
snap_position =
right_side_up ? SnapPosition::kSecondary : SnapPosition::kPrimary;
}
}
if (snap_position == SnapPosition::kNone) {
return snap_position;
}
bool drag_end_near_edge = false;
gfx::Rect area(work_area);
area.Inset(snap_distance_from_edge);
if (horizontal ? location_in_screen.x() < area.x() ||
location_in_screen.x() > area.right()
: location_in_screen.y() < area.y() ||
location_in_screen.y() > area.bottom()) {
drag_end_near_edge = true;
}
if (!drag_end_near_edge && initial_location_in_screen) {
const auto distance = location_in_screen - *initial_location_in_screen;
const int primary_axis_distance = horizontal ? distance.x() : distance.y();
const bool is_left_or_top =
IsPhysicallyLeftOrTop(snap_position, root_window);
if ((is_left_or_top && primary_axis_distance > -minimum_drag_distance) ||
(!is_left_or_top && primary_axis_distance < minimum_drag_distance)) {
snap_position = SnapPosition::kNone;
}
}
return snap_position;
}
SnapPosition GetSnapPosition(aura::Window* root_window,
aura::Window* window,
const gfx::Point& location_in_screen,
const gfx::Point& initial_location_in_screen,
int snap_distance_from_edge,
int minimum_drag_distance,
int horizontal_edge_inset,
int vertical_edge_inset) {
if (!SplitViewController::Get(root_window)
->CanSnapWindow(window, chromeos::kDefaultSnapRatio)) {
return SnapPosition::kNone;
}
std::optional<gfx::Point> initial_location_in_current_screen = std::nullopt;
if (window->GetRootWindow() == root_window)
initial_location_in_current_screen = initial_location_in_screen;
return GetSnapPositionForLocation(
root_window, location_in_screen, initial_location_in_current_screen,
snap_distance_from_edge, minimum_drag_distance, horizontal_edge_inset,
vertical_edge_inset);
}
bool IsLayoutHorizontal(aura::Window* window) {
return IsLayoutHorizontal(
display::Screen::Get()->GetDisplayNearestWindow(window));
}
bool IsLayoutHorizontal(const display::Display& display) {
if (display::Screen::Get()->InTabletMode()) {
return IsCurrentScreenOrientationLandscape();
}
DCHECK(display.is_valid());
return chromeos::IsLandscapeOrientation(GetSnapDisplayOrientation(display));
}
bool IsLayoutPrimary(aura::Window* window) {
return IsLayoutPrimary(
display::Screen::Get()->GetDisplayNearestWindow(window));
}
bool IsLayoutPrimary(const display::Display& display) {
if (display::Screen::Get()->InTabletMode()) {
return IsCurrentScreenOrientationPrimary();
}
DCHECK(display.is_valid());
return chromeos::IsPrimaryOrientation(GetSnapDisplayOrientation(display));
}
bool IsPhysicallyLeftOrTop(SnapPosition position, aura::Window* window) {
DCHECK_NE(SnapPosition::kNone, position);
return position == (IsLayoutPrimary(window) ? SnapPosition::kPrimary
: SnapPosition::kSecondary);
}
bool IsPhysicallyLeftOrTop(SnapPosition position,
const display::Display& display) {
DCHECK_NE(SnapPosition::kNone, position);
return position == (IsLayoutPrimary(display) ? SnapPosition::kPrimary
: SnapPosition::kSecondary);
}
bool IsPhysicallyLeftOrTop(aura::Window* window) {
chromeos::WindowStateType state_type =
WindowState::Get(window)->GetStateType();
CHECK(chromeos::IsSnappedWindowStateType(state_type));
if (IsLayoutPrimary(window)) {
return state_type == chromeos::WindowStateType::kPrimarySnapped;
}
return state_type == chromeos::WindowStateType::kSecondarySnapped;
}
int GetDividerPositionUpperLimit(aura::Window* root_window) {
const gfx::Rect work_area_bounds =
screen_util::GetDisplayWorkAreaBoundsInScreenForActiveDeskContainer(
root_window);
return IsLayoutHorizontal(root_window) ? work_area_bounds.width()
: work_area_bounds.height();
}
int GetMinimumWindowLength(aura::Window* window, bool horizontal) {
int minimum_width = 0;
if (window && window->delegate()) {
gfx::Size minimum_size = window->delegate()->GetMinimumSize();
minimum_width = horizontal ? minimum_size.width() : minimum_size.height();
}
return minimum_width;
}
int CalculateDividerPosition(aura::Window* root_window,
SnapPosition snap_position,
float snap_ratio,
bool account_for_divider_width) {
const int divider_upper_limit = GetDividerPositionUpperLimit(root_window);
const int divider_delta =
account_for_divider_width ? kSplitviewDividerShortSideLength : 0;
const float snap_length = (divider_upper_limit - divider_delta) * snap_ratio;
const bool is_layout_primary = IsLayoutPrimary(root_window);
const bool snap_to_left_or_top =
(is_layout_primary && snap_position == SnapPosition::kPrimary) ||
(!is_layout_primary && snap_position == SnapPosition::kSecondary);
return std::clamp(
static_cast<int>(snap_to_left_or_top
? snap_length
: divider_upper_limit - snap_length - divider_delta),
0, divider_upper_limit);
}
int GetEquivalentDividerPosition(aura::Window* window,
bool account_for_divider_width) {
aura::Window* root_window = window->GetRootWindow();
const bool horizontal = IsLayoutHorizontal(root_window);
const int window_length = GetWindowLength(window, horizontal);
const int divider_delta =
account_for_divider_width ? kSplitviewDividerShortSideLength / 2.f : 0;
return IsPhysicallyLeftOrTop(window)
? window_length - divider_delta
: GetDividerPositionUpperLimit(root_window) - window_length -
divider_delta;
}
gfx::Rect CalculateSnappedWindowBoundsInScreen(
SnapPosition snap_position,
aura::Window* root_window,
aura::Window* window_for_minimum_size,
bool account_for_divider_width,
int divider_position,
bool is_resizing_with_divider) {
const bool snap_left_or_top =
IsPhysicallyLeftOrTop(snap_position, root_window);
const bool in_tablet_mode = display::Screen::Get()->InTabletMode();
const int work_area_size = GetDividerPositionUpperLimit(root_window);
if (divider_position < 0 && !in_tablet_mode) {
if (auto* window = WindowRestoreController::Get()->to_be_snapped_window()) {
app_restore::WindowInfo* window_info =
window->GetProperty(app_restore::kWindowInfoKey);
if (window_info && window_info->snap_percentage) {
const int snap_percentage = *window_info->snap_percentage;
divider_position = snap_percentage * work_area_size / 100;
if (!snap_left_or_top) {
divider_position = work_area_size - divider_position;
}
}
}
}
const int divider_width =
account_for_divider_width ? kSplitviewDividerShortSideLength : 0;
int window_size = snap_left_or_top
? divider_position
: work_area_size - divider_position - divider_width;
const bool horizontal = IsLayoutHorizontal(root_window);
const int minimum =
GetMinimumWindowLength(window_for_minimum_size, horizontal);
DCHECK(window_for_minimum_size || minimum == 0);
if (window_size < minimum) {
if (in_tablet_mode && !is_resizing_with_divider) {
window_size = (work_area_size - kSplitviewDividerShortSideLength) / 2;
if (!snap_left_or_top && work_area_size % 2 == 1) {
++window_size;
}
} else {
window_size = minimum;
}
}
if (window_for_minimum_size && !in_tablet_mode) {
const gfx::Size* preferred_size =
window_for_minimum_size->GetProperty(kUnresizableSnappedSizeKey);
if (preferred_size &&
!WindowState::Get(window_for_minimum_size)->CanResize()) {
if (horizontal && preferred_size->width() > 0) {
window_size = preferred_size->width();
}
if (!horizontal && preferred_size->height() > 0) {
window_size = preferred_size->height();
}
}
}
const gfx::Rect work_area_bounds_in_screen =
screen_util::GetDisplayWorkAreaBoundsInScreenForActiveDeskContainer(
root_window);
int left = work_area_bounds_in_screen.x();
int top = work_area_bounds_in_screen.y();
int right = work_area_bounds_in_screen.right();
int bottom = work_area_bounds_in_screen.bottom();
int& left_or_top = horizontal ? left : top;
int& right_or_bottom = horizontal ? right : bottom;
if (snap_left_or_top) {
right_or_bottom = left_or_top + window_size;
} else {
left_or_top = right_or_bottom - window_size;
}
gfx::Rect snapped_window_bounds_in_screen;
snapped_window_bounds_in_screen.SetByBounds(left, top, right, bottom);
return snapped_window_bounds_in_screen;
}
SnapViewType ToSnapViewType(chromeos::WindowStateType state_type) {
switch (state_type) {
case chromeos::WindowStateType::kPrimarySnapped:
return SnapViewType::kPrimary;
case chromeos::WindowStateType::kSecondarySnapped:
return SnapViewType::kSecondary;
default:
NOTREACHED();
}
}
chromeos::WindowStateType ToWindowStateType(SnapViewType snap_type) {
switch (snap_type) {
case SnapViewType::kPrimary:
return chromeos::WindowStateType::kPrimarySnapped;
case SnapViewType::kSecondary:
return chromeos::WindowStateType::kSecondarySnapped;
}
}
SnapViewType GetOppositeSnapType(SnapViewType snap_type) {
switch (snap_type) {
case SnapViewType::kPrimary:
return SnapViewType::kSecondary;
case SnapViewType::kSecondary:
return SnapViewType::kPrimary;
}
}
SnapViewType GetOppositeSnapType(aura::Window* window) {
return GetOppositeSnapType(
ToSnapViewType(WindowState::Get(window)->GetStateType()));
}
bool CanSnapActionSourceStartFasterSplitView(
WindowSnapActionSource snap_action_source) {
switch (snap_action_source) {
case WindowSnapActionSource::kDragOrSelectOverviewWindowToSnap:
case WindowSnapActionSource::kDragWindowToEdgeToSnap:
case WindowSnapActionSource::kLongPressCaptionButtonToSnap:
case WindowSnapActionSource::kSnapByWindowLayoutMenu:
return true;
default:
return false;
}
}
bool ShouldExcludeForOcclusionCheck(const aura::Window* window,
const aura::Window* target_root) {
if (window->GetRootWindow() != target_root || !window->IsVisible()) {
return true;
}
if (!desks_util::IsActiveDeskContainer(window->parent())) {
return true;
}
return WindowState::Get(window)->IsMinimized();
}
aura::Window::Windows GetActiveDeskAppWindowsInZOrder(aura::Window* root) {
aura::Window::Windows windows;
const auto children =
desks_util::GetActiveDeskContainerForRoot(root)->children();
for (const auto& child : base::Reversed(children)) {
if (CanIncludeWindowInAppMruList(child)) {
windows.push_back(child.get());
}
}
return windows;
}
aura::Window* GetTopmostVisibleWindowOfSnapType(aura::Window* window_to_ignore,
aura::Window* target_root,
SnapViewType snap_type) {
aura::Window::Windows windows = GetActiveDeskAppWindowsInZOrder(target_root);
const chromeos::WindowStateType target_state_type =
ToWindowStateType(snap_type);
auto* overview_session = GetOverviewSession();
gfx::Rect union_bounds;
for (aura::Window* top_window : windows) {
const bool should_be_excluded_for_occlusion_check =
top_window == window_to_ignore ||
ShouldExcludeForOcclusionCheck(top_window, target_root);
if (should_be_excluded_for_occlusion_check) {
continue;
}
if (overview_session && overview_session->IsWindowInOverview(top_window)) {
continue;
}
const auto* top_window_state = WindowState::Get(top_window);
const gfx::Rect top_window_bounds = top_window->GetBoundsInScreen();
if (top_window_state->GetStateType() == target_state_type) {
if (!top_window_bounds.Intersects(union_bounds) &&
!union_bounds.Intersects(top_window_bounds)) {
return top_window;
}
}
union_bounds.Union(top_window_bounds);
}
return nullptr;
}
aura::Window* GetOppositeVisibleSnappedWindow(aura::Window* window) {
return GetTopmostVisibleWindowOfSnapType(window, window->GetRootWindow(),
GetOppositeSnapType(window));
}
float GetSnapRatioGap(aura::Window* to_be_snapped,
aura::Window* opposite_snapped) {
return std::abs(1.f - window_util::GetSnapRatioForWindow(to_be_snapped) -
window_util::GetSnapRatioForWindow(opposite_snapped));
}
bool IsSnapRatioGapWithinThreshold(aura::Window* to_be_snapped,
aura::Window* opposite_snapped) {
const float snap_ratio_gap = GetSnapRatioGap(to_be_snapped, opposite_snapped);
const float diff = snap_ratio_gap - kSnapToReplaceRatioDiffThreshold;
return diff <= 0.01f;
}
float GetAutoSnapRatio(aura::Window* to_be_snapped_window,
aura::Window* target_root,
SnapViewType snap_type) {
if (!display::Screen::Get()->InTabletMode()) {
if (aura::Window* opposite_window =
GetTopmostVisibleWindowOfSnapType(to_be_snapped_window, target_root,
GetOppositeSnapType(snap_type))) {
if (!IsSnapRatioGapWithinThreshold(to_be_snapped_window,
opposite_window)) {
return chromeos::kDefaultSnapRatio;
}
return 1.f - window_util::GetSnapRatioForWindow(opposite_window);
}
}
return chromeos::kDefaultSnapRatio;
}
bool ShouldConsiderWindowForSplitViewSetupView(
aura::Window* window,
WindowSnapActionSource snap_action_source) {
if (!OverviewController::Get()->CanEnterOverview() ||
IsPartialOverviewEmptyForActiveDesk(window)) {
return false;
}
if (PrefService* pref =
Shell::Get()->session_controller()->GetActivePrefService();
pref && !pref->GetBoolean(prefs::kSnapWindowSuggestions)) {
return false;
}
if (!CanSnapActionSourceStartFasterSplitView(snap_action_source)) {
return false;
}
return !IsInOverviewSession();
}
bool CanStartSplitViewOverviewSessionInClamshell(
aura::Window* window,
WindowSnapActionSource snap_action_source) {
if (IsInOverviewSession() && WindowState::Get(window)->IsSnapped()) {
return !RootWindowController::ForWindow(window)
->split_view_overview_session();
}
if (GetOppositeVisibleSnappedWindow(window)) {
return false;
}
return ShouldConsiderWindowForSplitViewSetupView(window, snap_action_source);
}
int GetWindowComponentForResize(aura::Window* window) {
chromeos::WindowStateType state_type =
WindowState::Get(window)->GetStateType();
CHECK(chromeos::IsSnappedWindowStateType(state_type));
return state_type == chromeos::WindowStateType::kPrimarySnapped ? HTRIGHT
: HTLEFT;
}
bool ShouldConsiderDivider(aura::Window* window) {
if (!display::Screen::Get()->InTabletMode()) {
if (auto* snap_group =
SnapGroupController::Get()->GetSnapGroupForGivenWindow(window)) {
return snap_group->snap_group_divider()->divider_widget();
}
}
SplitViewController* split_view_controller =
SplitViewController::Get(window->GetRootWindow());
return split_view_controller->InSplitViewMode() &&
split_view_controller->split_view_divider()->divider_widget();
}
bool CanWindowsFitInWorkArea(aura::Window* window1, aura::Window* window2) {
DCHECK_EQ(window1->GetRootWindow(), window2->GetRootWindow());
aura::Window* root_window = window1->GetRootWindow();
const bool horizontal = IsLayoutHorizontal(root_window);
const gfx::Rect work_area =
display::Screen::Get()->GetDisplayNearestWindow(root_window).work_area();
const int work_area_length =
horizontal ? work_area.width() : work_area.height();
return GetMinimumWindowLength(window1, horizontal) +
GetMinimumWindowLength(window2, horizontal) +
kSplitviewDividerShortSideLength <=
work_area_length;
}
ASH_EXPORT std::string BuildWindowLayoutCompleteOnSessionExitHistogram() {
std::string histogram_name(kHistogramPrefix);
histogram_name.append(kWindowLayoutCompleteOnSessionExitRootWord);
AppendUIModeToHistogram(histogram_name);
return histogram_name;
}
ASH_EXPORT std::string BuildSplitViewOverviewExitPointHistogramName(
WindowSnapActionSource snap_action_source) {
std::string histogram_name(kHistogramPrefix);
histogram_name.append(GetSnapActionSourceMetricComponent(snap_action_source));
histogram_name.append(".");
histogram_name.append(kExitPointRootWord);
AppendUIModeToHistogram(histogram_name);
return histogram_name;
}
std::string BuildSnapWindowSuggestionsHistogramName(
WindowSnapActionSource snap_action_source) {
std::string histogram_name(kSnapWindowSuggestionsHistogramPrefix);
histogram_name.append(GetSnapActionSourceMetricComponent(snap_action_source));
return histogram_name;
}
}