chromium/ash/wm/splitview/split_view_utils.cc

// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ash/wm/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;

// The animation speed at which the highlights fade in or out.
constexpr base::TimeDelta kHighlightsFadeInOut = base::Milliseconds(250);
// The animation speed which the other highlight fades in or out.
constexpr base::TimeDelta kOtherFadeInOut = base::Milliseconds(133);
// The delay before the other highlight starts fading in.
constexpr base::TimeDelta kOtherFadeInDelay = base::Milliseconds(117);
// The animation speed at which the preview area fades out (when you snap a
// window).
constexpr base::TimeDelta kPreviewAreaFadeOut = base::Milliseconds(67);
// The time duration for the indicator label opacity animations.
constexpr base::TimeDelta kLabelAnimation = base::Milliseconds(83);
// The delay before the indicator labels start fading in.
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);
  }
}

// Returns the corresponding snap action source metric string component with
// given `snap_action_source`.
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::kTest:
      return "Test";
    case WindowSnapActionSource::kLacrosSnapButtonOrWindowLayoutMenu:
      return "SnapByLacrosSnapButtonOrWindowLayoutMenu";
    case WindowSnapActionSource::kSnapBySwapWindowsInSnapGroup:
      return "SnapBySwapWindowsInSnapGroup";
  }
}

void AppendUIModeToHistogram(std::string& histogram_name) {
  histogram_name.append(display::Screen::GetScreen()->InTabletMode()
                            ? ".TabletMode"
                            : ".ClamshellMode");
}

// Returns true if there is no window in partial overview (excluding the given
// `window`).
bool IsPartialOverviewEmptyForActiveDesk(aura::Window* window) {
  // Use `BuildMruWindowList()` to include all window types, e.g. always-on-top
  // windows and floated windows.
  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;
}

}  // namespace

WindowTransformAnimationObserver::WindowTransformAnimationObserver(
    aura::Window* window)
    : window_(window) {
  window_->AddObserver(this);
}

WindowTransformAnimationObserver::~WindowTransformAnimationObserver() {
  if (window_)
    window_->RemoveObserver(this);
}

void WindowTransformAnimationObserver::OnImplicitAnimationsCompleted() {
  // After window transform animation is done and if the window's transform is
  // set to identity transform, force to relayout all its transient bubble
  // dialogs.
  if (!window_->layer()->GetTargetTransform().IsIdentity()) {
    delete this;
    return;
  }

  for (aura::Window* transient_window :
       wm::TransientWindowManager::GetOrCreate(window_)->transient_children()) {
    // For now we only care about bubble dialog type 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) {
    // If this is the secondary window, translate the other direction.
    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::GetScreen()->InTabletMode()) {
    return;
  }

  // Search for snapped windows to detect if the now active user session, or
  // desk were in split view. In case multiple windows were snapped to one side,
  // one window after another, there may be multiple windows in a LEFT_SNAPPED
  // state or multiple windows in a RIGHT_SNAPPED state. For each of those two
  // state types that belongs to multiple windows, the relevant window will be
  // listed first among those windows, and a null check in the loop body below
  // will filter out the rest of them.
  // TODO(amusbach): The windows that were in split view may have later been
  // destroyed or changed to non-snapped states. Then the following for loop
  // could snap windows that were not in split view. Also, a window may have
  // become full screen, and if so, then it would be better not to reactivate
  // split view. See https://crbug.com/944134.
  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)) {
        // Since we are in tablet mode, and this window is not snappable, we
        // should maximize it.
        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;
      }
    }
  }

  // Ensure that overview mode is active if there is a window snapped to one of
  // the sides. Ensure overview mode is not active if there are two snapped
  // windows.
  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() {
  // Don't allow split view if we're in pinned mode.
  if (Shell::Get()->screen_pinning_controller()->IsPinned())
    return false;

  // Disallow window dragging and split screen while ChromeVox is on in tablet
  // mode.
  if (display::Screen::GetScreen()->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,
                /*visible_on_lock_screen=*/false,
                /*has_dismiss_button=*/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);

  // Check to see if the current event location |location_in_screen| is within
  // the drag indicators bounds.
  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;
  }

  // To avoid accidental snap, the window needs to be dragged inside
  // |snap_distance_from_edge| from edge or dragged toward the edge for at least
  // |minimum_drag_distance| until it's dragged into |horizontal_edge_inset| or
  // |vertical_edge_inset| region.
  // The window should always be snapped if inside |snap_distance_from_edge|
  // from edge.
  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) {
    // Check how far the window has been dragged.
    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::GetScreen()->GetDisplayNearestWindow(window));
}

bool IsLayoutHorizontal(const display::Display& display) {
  if (display::Screen::GetScreen()->InTabletMode()) {
    return IsCurrentScreenOrientationLandscape();
  }

  // TODO(crbug.com/40191408): add DCHECK to avoid square size display.
  DCHECK(display.is_valid());
  return chromeos::IsLandscapeOrientation(GetSnapDisplayOrientation(display));
}

bool IsLayoutPrimary(aura::Window* window) {
  return IsLayoutPrimary(
      display::Screen::GetScreen()->GetDisplayNearestWindow(window));
}

bool IsLayoutPrimary(const display::Display& display) {
  if (display::Screen::GetScreen()->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();
}

// Returns the minimum length of the window according to the screen orientation.
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;

  // `snap_length` needs to be a float so that the rounding is performed at the
  // end of the computation of `next_divider_position`. It's important because a
  // 1-DIP gap between snapped windows precludes multiresizing. See b/262011280.
  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::GetScreen()->InTabletMode();
  const int work_area_size = GetDividerPositionUpperLimit(root_window);

  // Edit `divider_position` if window restore is currently restoring a snapped
  // window; take into account the snap percentage saved by the window. Only do
  // this for clamshell mode; in tablet mode we are OK with restoring to the
  // default half snap state.
  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) {
      // If window with `window_for_minimum_size` gets snapped, the
      // `split_view_divider_` will then be adjusted to its default position and
      // `window_size` will be computed accordingly.
      window_size = (work_area_size - kSplitviewDividerShortSideLength) / 2;
      // If `work_area_size` is odd, then the default divider position is
      // rounded down, toward the left or top, but then if `snap_left_or_top` is
      // false, that means `window_size` should now be rounded up.
      if (!snap_left_or_top && work_area_size % 2 == 1) {
        ++window_size;
      }
    } else {
      window_size = minimum;
    }
  }

  if (window_for_minimum_size && !in_tablet_mode) {
    // Apply the unresizable snapping constraint to the snapped bounds if we're
    // in the clamshell 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);
  // Get the parameter values for which `gfx::Rect::SetByBounds` would recreate
  // `work_area_bounds_in_screen`.
  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();

  // Make `snapped_window_bounds_in_screen` by modifying one of the above four
  // values: the one that represents the inner edge of the snapped bounds.
  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::kDragWindowToEdgeToSnap:
    case WindowSnapActionSource::kSnapByWindowLayoutMenu:
    case WindowSnapActionSource::kLongPressCaptionButtonToSnap:
    case WindowSnapActionSource::kDragOrSelectOverviewWindowToSnap:
    case WindowSnapActionSource::kTest:
    case WindowSnapActionSource::kLacrosSnapButtonOrWindowLayoutMenu:
      // We only start partial overview for the above snap sources.
      return true;
    default:
      return false;
  }
}

bool ShouldExcludeForOcclusionCheck(const aura::Window* window,
                                    const aura::Window* target_root) {
  // `window` should be excluded for occlusion check under the following
  // conditions:
  // 1. When `window` is not on the same root window as `target_root`;
  // 2. When `window` does not belong to the active desk container, for example
  // always-on-top window, float or pip window;
  // 3. When it is not visible or minimized;
  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();
  // Iterate through the desk container's children in reversed order.
  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) {
  // `GetActiveDeskAppWindowsInZOrder()` will exclude transient windows like the
  // window layout menu and other bubble widgets.
  aura::Window::Windows windows = GetActiveDeskAppWindowsInZOrder(target_root);
  const chromeos::WindowStateType target_state_type =
      ToWindowStateType(snap_type);
  auto* overview_session = GetOverviewSession();

  // Track the union bounds of the windows that are more recently used than the
  // currently iterated window, i.e. `top_window` below to check the occlusion
  // state of the opposite snapped window.
  gfx::Rect union_bounds;
  for (aura::Window* top_window : windows) {
    // The `top_window` should be excluded for occlusion check when it is the
    // `window_to_ignore` itself or if `ShouldExcludeForOcclusionCheck()` is
    // true.
    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)) {
      // Skip any windows that are in overview, since they are visually not
      // snapped to the user.
      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) {
      // Ensure that `top_window` is fully visible by checking:
      // 1. There is no window stacked above `top_window` with bounds
      // confined or confining `top_window`. Note that if `union_bounds` is
      // empty, `top_window` will be the topmost window snapped on the
      // opposite position;
      // 2. There is no window with bounds that intersect with `top_window`.
      // See http://b/320759574#comment3 for more details with graphs.
      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);
  // Use a more relaxed tolerance to allow approximate gaps.
  const float diff = snap_ratio_gap - kSnapToReplaceRatioDiffThreshold;
  return diff <= /*tolerance=*/0.01f;
}

float GetAutoSnapRatio(aura::Window* to_be_snapped_window,
                       aura::Window* target_root,
                       SnapViewType snap_type) {
  if (IsSnapGroupEnabledInClamshellMode()) {
    // `GetTopmostVisibleWindowOfSnapType()` will include windows in snap
    // groups.
    if (aura::Window* opposite_window =
            GetTopmostVisibleWindowOfSnapType(to_be_snapped_window, target_root,
                                              GetOppositeSnapType(snap_type))) {
      // If the gap between `opposite_window` and `to_be_snapped_window`,
      // which will always be the default snap ratio for drag to snap, exceeds
      // the threshold, we won't allow auto grouping, so we also don't update
      // the phantom snap ratio.
      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();
  }

  // Skip starting `SplitViewOverviewSession` if a fully visible window snapped
  // on the opposite side. `GetOppositeVisibleSnappedWindow()` will exclude
  // windows that are *in* overview.
  if (GetOppositeVisibleSnappedWindow(window)) {
    return false;
  }

  return ShouldConsiderWindowForSplitViewSetupView(window, snap_action_source);
}

bool IsSnapGroupEnabledInClamshellMode() {
  return features::IsSnapGroupEnabled() &&
         !display::Screen::GetScreen()->InTabletMode();
}

int GetWindowComponentForResize(aura::Window* window) {
  chromeos::WindowStateType state_type =
      WindowState::Get(window)->GetStateType();
  CHECK(chromeos::IsSnappedWindowStateType(state_type));
  // TODO(b/288356322): Update the component for vertical splitview.
  return state_type == chromeos::WindowStateType::kPrimarySnapped ? HTRIGHT
                                                                  : HTLEFT;
}

bool ShouldConsiderDivider(aura::Window* window) {
  if (IsSnapGroupEnabledInClamshellMode()) {
    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::GetScreen()
                                  ->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;
}

}  // namespace ash