chromium/ash/wm/splitview/split_view_divider.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_divider.h"

#include <algorithm>
#include <memory>

#include "ash/display/screen_orientation_controller.h"
#include "ash/focus_cycler.h"
#include "ash/public/cpp/window_properties.h"
#include "ash/screen_util.h"
#include "ash/shell.h"
#include "ash/wm/desks/desks_util.h"
#include "ash/wm/splitview/split_view_constants.h"
#include "ash/wm/splitview/split_view_controller.h"
#include "ash/wm/splitview/split_view_divider_view.h"
#include "ash/wm/splitview/split_view_utils.h"
#include "ash/wm/window_properties.h"
#include "ash/wm/window_util.h"
#include "base/auto_reset.h"
#include "base/check.h"
#include "base/containers/contains.h"
#include "base/metrics/user_metrics.h"
#include "base/ranges/algorithm.h"
#include "chromeos/ui/base/chromeos_ui_constants.h"
#include "ui/aura/window_targeter.h"
#include "ui/display/screen.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/views/view_targeter_delegate.h"
#include "ui/views/view_utils.h"
#include "ui/views/widget/widget.h"
#include "ui/views/widget/widget_delegate.h"
#include "ui/wm/core/coordinate_conversion.h"
#include "ui/wm/core/transient_window_manager.h"
#include "ui/wm/core/window_util.h"

namespace ash {

namespace {

// Inset value for the transient parent, ensuring the divider remains visible
// and clear of the window resizer border.
constexpr int kTransientParentInset = chromeos::kResizeOutsideBoundsSize + 1;

// Returns the allowed range of `divider_position` within `windows`,
// accounting for the windows' minimum sizes.
gfx::Range GetDividerPositionAllowedRange(const aura::Window::Windows windows) {
  CHECK(!windows.empty());

  aura::Window* root_window = windows.at(0)->GetRootWindow();
  aura::Window* primary_window = nullptr;
  aura::Window* secondary_window = nullptr;
  for (auto window : windows) {
    if (IsPhysicallyLeftOrTop(window)) {
      primary_window = window;
    } else {
      secondary_window = window;
    }
  }
  CHECK(primary_window || secondary_window);

  const bool is_horizontal = IsLayoutHorizontal(root_window);
  const int primary_window_minimum_length =
      GetMinimumWindowLength(primary_window, is_horizontal);
  const int secondary_window_minimum_length =
      GetMinimumWindowLength(secondary_window, is_horizontal);
  const gfx::Rect work_area_bounds_in_screen =
      screen_util::GetDisplayWorkAreaBoundsInScreenForActiveDeskContainer(
          root_window);
  return gfx::Range(primary_window_minimum_length,
                    is_horizontal ? (work_area_bounds_in_screen.width() -
                                     secondary_window_minimum_length -
                                     kSplitviewDividerShortSideLength)
                                  : (work_area_bounds_in_screen.height() -
                                     secondary_window_minimum_length -
                                     kSplitviewDividerShortSideLength));
}

gfx::Point GetBoundedPosition(const gfx::Point& location_in_screen,
                              const gfx::Rect& bounds_in_screen) {
  return gfx::Point(std::clamp(location_in_screen.x(), bounds_in_screen.x(),
                               bounds_in_screen.right() - 1),
                    std::clamp(location_in_screen.y(), bounds_in_screen.y(),
                               bounds_in_screen.bottom() - 1));
}

gfx::Rect GetWorkAreaBoundsInScreen(aura::Window* window) {
  return screen_util::GetDisplayWorkAreaBoundsInScreenForActiveDeskContainer(
      window);
}

// Returns the widget init params needed to create the widget.
views::Widget::InitParams CreateWidgetInitParams(aura::Window* parent_window,
                                                 const gfx::Rect& bounds) {
  views::Widget::InitParams params(
      views::Widget::InitParams::NATIVE_WIDGET_OWNS_WIDGET,
      views::Widget::InitParams::TYPE_POPUP);
  params.opacity = views::Widget::InitParams::WindowOpacity::kOpaque;
  params.activatable = views::Widget::InitParams::Activatable::kYes;
  params.parent = parent_window;
  params.bounds = bounds;
  params.init_properties_container.SetProperty(kExcludeInMruKey, true);
  params.init_properties_container.SetProperty(kIgnoreWindowActivationKey,
                                               true);
  params.init_properties_container.SetProperty(kHideInDeskMiniViewKey, true);
  // Exclude the divider from getting transformed with its transient parent
  // window when we are resizing. The divider will set its own transforms.
  params.init_properties_container.SetProperty(
      kExcludeFromTransientTreeTransformKey, true);
  params.name = "SplitViewDividerWidget";
  return params;
}

}  // namespace

// SplitViewDividerWidget observes its native widget activation change to set
// pane focus on its contents view.
class SplitViewDivider::SplitViewDividerWidget : public views::Widget {
 public:
  SplitViewDividerWidget() = default;
  SplitViewDividerWidget(const SplitViewDividerWidget&) = delete;
  SplitViewDividerWidget& operator=(const SplitViewDividerWidget&) = delete;
  ~SplitViewDividerWidget() override = default;

  // views::Widget:
  bool OnNativeWidgetActivationChanged(bool active) override {
    if (!Widget::OnNativeWidgetActivationChanged(active)) {
      return false;
    }
    // Only set focus and show the focus ring if `this` is being activated by
    // the focus cycler.
    if (!active || this != Shell::Get()->focus_cycler()->widget_activating()) {
      return false;
    }
    base::RecordAction(
        base::UserMetricsAction("SnapGroups_ActivateViaKeyboard"));
    auto* divider_view =
        views::AsViewClass<SplitViewDividerView>(GetContentsView());
    divider_view->SetPaneFocusAndFocusDefault();
    return true;
  }

  // ui::ColorProviderSource:
  ui::ColorProviderKey GetColorProviderKey() const override {
    //  As the transient child of the topmost window, divider uses that window's
    //  theme color. Override `GetColorProviderKey()` to let it use the system's
    //  theme instead.
    return ui::NativeTheme::GetInstanceForNativeUi()->GetColorProviderKey(
        nullptr);
  }
};

SplitViewDivider::SplitViewDivider(LayoutDividerController* controller)
    : controller_(controller) {}

SplitViewDivider::~SplitViewDivider() = default;

// static
gfx::Rect SplitViewDivider::GetDividerBoundsInScreen(
    const gfx::Rect& work_area_bounds_in_screen,
    bool landscape,
    int divider_position,
    bool is_dragging) {
  const int dragging_diff = is_dragging
                                ? (kSplitviewDividerEnlargedShortSideLength -
                                   kSplitviewDividerShortSideLength) /
                                      2
                                : 0;
  if (landscape) {
    return gfx::Rect(
        work_area_bounds_in_screen.x() + divider_position - dragging_diff,
        work_area_bounds_in_screen.y(),
        is_dragging ? kSplitviewDividerEnlargedShortSideLength
                    : kSplitviewDividerShortSideLength,
        work_area_bounds_in_screen.height());
  } else {
    return gfx::Rect(
        work_area_bounds_in_screen.x(),
        work_area_bounds_in_screen.y() + divider_position - dragging_diff,
        work_area_bounds_in_screen.width(),
        is_dragging ? kSplitviewDividerEnlargedShortSideLength
                    : kSplitviewDividerShortSideLength);
  }
}

aura::Window* SplitViewDivider::GetDividerWindow() {
  return divider_widget_ ? divider_widget_->GetNativeWindow() : nullptr;
}

bool SplitViewDivider::HasDividerWidget() const {
  return !!divider_widget_;
}

bool SplitViewDivider::IsDividerWidgetVisible() const {
  return divider_widget_ && divider_widget_->IsVisible();
}

void SplitViewDivider::SetVisible(bool visible) {
  if (target_visibility_ != visible) {
    target_visibility_ = visible;
    RefreshDividerState(/*observed_windows_changed=*/false);
  }
}

void SplitViewDivider::SetDividerPosition(int divider_position) {
  if (divider_position_ == divider_position) {
    return;
  }
  divider_position_ = divider_position;
  // Only clamp within `observed_windows_` if it is not empty; otherwise it
  // will return an invalid range.
  // TODO(michelefan): Fix tablet mode regression: when the divider is dragged
  // below the minimum window size, slide the window out to prevent errors.
  if (!observed_windows_.empty() &&
      !display::Screen::GetScreen()->InTabletMode()) {
    const gfx::Range divider_allowed_range =
        GetDividerPositionAllowedRange(observed_windows_);
    if (!divider_allowed_range.is_reversed()) {
      divider_position_ = std::clamp(
          divider_position_, static_cast<int>(divider_allowed_range.start()),
          static_cast<int>(divider_allowed_range.end()));
    }
    // TODO(b/333623218): If the divider range is reversed, i.e. the windows no
    // longer fit, we will break the group.
  }
  RefreshDividerState(/*observed_windows_changed=*/false);
}

void SplitViewDivider::UpdateDividerPosition(
    const gfx::Point& location_in_screen) {
  aura::Window* root = GetRootWindow();
  const bool horizontal = IsLayoutHorizontal(root);
  if (!display::Screen::GetScreen()->InTabletMode()) {
    // In clamshell mode, we try to keep the center point of the divider as in
    // sync with the mouse event location as possible. `SetDividerPosition()`
    // will clamp the position between the windows' minimum sizes.
    gfx::Point location_in_root(location_in_screen);
    wm::ConvertPointFromScreen(root, &location_in_root);
    // Note `divider_position` needs to be relative to the work area to get the
    // correct bounds in `GetDividerBoundsInScreen()`.
    gfx::Rect work_area = GetWorkAreaBoundsInScreen(root);
    wm::ConvertRectFromScreen(root, &work_area);
    SetDividerPosition(
        horizontal ? location_in_root.x() -
                         kSplitviewDividerShortSideLength / 2 - work_area.x()
                   : location_in_root.y() -
                         kSplitviewDividerShortSideLength / 2 - work_area.y());
    return;
  }

  int potential_divider_position = divider_position_;
  if (horizontal) {
    potential_divider_position +=
        location_in_screen.x() - previous_event_location_.x();
  } else {
    potential_divider_position +=
        location_in_screen.y() - previous_event_location_.y();
  }

  // This line is used for ARC++ windows.
  potential_divider_position = std::max(0, potential_divider_position);

  SetDividerPosition(potential_divider_position);
}

aura::Window* SplitViewDivider::GetRootWindow() const {
  return controller_->GetRootWindow();
}

void SplitViewDivider::StartResizeWithDivider(
    const gfx::Point& location_in_screen) {
  // `is_resizing_with_divider_` may be true here, because you can start
  // dragging the divider with a pointing device while already dragging it by
  // touch, or vice versa. It is possible by using the emulator or
  // chrome://flags/#force-tablet-mode. Bailing out here does not stop the user
  // from dragging by touch and with a pointing device simultaneously; it just
  // avoids duplicate calls to `CreateDragDetails()` and `OnDragStarted()`. We
  // also bail out here if you try to start dragging the divider during its snap
  // animation.
  // TODO(sophiewen): Consider refactoring `DividerSnapAnimation` to here.
  if (is_resizing_with_divider_ ||
      SplitViewController::Get(GetRootWindow())->IsDividerAnimating()) {
    return;
  }

  is_resizing_with_divider_ = true;
  EnlargeOrShrinkDivider(/*should_enlarge=*/true);
  previous_event_location_ = location_in_screen;

  UpdateDividerPosition(location_in_screen);
  controller_->StartResizeWithDivider(location_in_screen);

  for (aura::Window* window : observed_windows_) {
    if (window == nullptr) {
      continue;
    }

    WindowState* window_state = WindowState::Get(window);
    gfx::Point location_in_parent(location_in_screen);
    wm::ConvertPointFromScreen(window->parent(), &location_in_parent);
    int window_component = GetWindowComponentForResize(window);
    window_state->CreateDragDetails(gfx::PointF(location_in_parent),
                                    window_component,
                                    wm::WINDOW_MOVE_SOURCE_TOUCH);

    window_state->OnDragStarted(window_component);
  }
}

void SplitViewDivider::ResizeWithDivider(const gfx::Point& location_in_screen) {
  if (!is_resizing_with_divider_) {
    return;
  }

  base::AutoReset<bool> auto_reset(&processing_resize_event_, true);

  gfx::Point modified_location_in_screen = GetBoundedPosition(
      location_in_screen,
      GetWorkAreaBoundsInScreen(divider_widget_->GetNativeWindow()));

  // Order here matters: we first update `divider_position_`, then the
  // `LayoutDividerController` will update the window and divider bounds in
  // `UpdateResizeWithDivider()`.
  UpdateDividerPosition(modified_location_in_screen);
  EnlargeOrShrinkDivider(/*should_enlarge=*/true);
  controller_->UpdateResizeWithDivider(modified_location_in_screen);

  previous_event_location_ = modified_location_in_screen;
}

void SplitViewDivider::EndResizeWithDivider(
    const gfx::Point& location_in_screen) {
  if (!is_resizing_with_divider_) {
    return;
  }

  is_resizing_with_divider_ = false;

  gfx::Point modified_location_in_screen = GetBoundedPosition(
      location_in_screen, GetWorkAreaBoundsInScreen(GetRootWindow()));

  // Order here matters: we first update `divider_position_`, then the
  // `LayoutDividerController` will transform and update the windows bounds in
  // `EndResizeWithDivider()`.
  UpdateDividerPosition(modified_location_in_screen);
  const gfx::Point cursor_point =
      display::Screen::GetScreen()->GetCursorScreenPoint();
  EnlargeOrShrinkDivider(
      GetDividerBoundsInScreen(/*is_dragging=*/true).Contains(cursor_point));

  // If the delegate is done with resizing, finish resizing and clean up.
  // Otherwise it will be called later, in
  // `DividerSnapAnimation::AnimationEnded()`.
  if (controller_->EndResizeWithDivider(modified_location_in_screen)) {
    CleanUpWindowResizing();
  }
}

void SplitViewDivider::CleanUpWindowResizing() {
  is_resizing_with_divider_ = false;
  // Always call `OnResizeEnding()` since `CleanUpWindowResizing()` may be after
  // an animation and we need to restore the window transforms.
  controller_->OnResizeEnding();
  FinishWindowResizing();
  controller_->OnResizeEnded();
}

void SplitViewDivider::UpdateDividerBounds() {
  if (divider_widget_) {
    divider_widget_->SetBounds(GetDividerBoundsInScreen(/*is_dragging=*/false));
  }
}

gfx::Rect SplitViewDivider::GetDividerBoundsInScreen(bool is_dragging) {
  auto* root_window = GetRootWindow();
  const gfx::Rect work_area_bounds_in_screen =
      GetWorkAreaBoundsInScreen(root_window);
  return GetDividerBoundsInScreen(work_area_bounds_in_screen,
                                  IsLayoutHorizontal(root_window),
                                  divider_position_, is_dragging);
}

void SplitViewDivider::EnlargeOrShrinkDivider(bool should_enlarge) {
  if (!divider_widget_ || !divider_widget_->IsVisible()) {
    return;
  }

  divider_widget_->SetBounds(GetDividerBoundsInScreen(should_enlarge));
  divider_view_->RefreshDividerHandler();

  // Even though the divider is a transient of the topmost window, it's not
  // observed. Mouse/gesture events on the divider may not trigger a refresh of
  // the stacking order which becomes noticeable with the existence of other
  // observed transient windows (divider stacked on top of the transient
  // window). Explicitly call `RefreshStackingOrder()` to apply needed
  // adjustments.
  RefreshStackingOrder();
}

void SplitViewDivider::SetAdjustable(bool adjustable) {
  if (adjustable == IsAdjustable()) {
    return;
  }

  divider_widget_->GetNativeWindow()->SetEventTargetingPolicy(
      adjustable ? aura::EventTargetingPolicy::kTargetAndDescendants
                 : aura::EventTargetingPolicy::kNone);
  divider_view_->SetHandlerBarVisible(adjustable);
}

bool SplitViewDivider::IsAdjustable() const {
  DCHECK(divider_widget_);
  DCHECK(divider_widget_->GetNativeView());
  return divider_widget_->GetNativeWindow()->event_targeting_policy() !=
         aura::EventTargetingPolicy::kNone;
}

void SplitViewDivider::MaybeAddObservedWindow(aura::Window* window) {
  if (base::Contains(observed_windows_, window)) {
    return;
  }
  window->AddObserver(this);
  observed_windows_.push_back(window);
  wm::TransientWindowManager* transient_manager =
      wm::TransientWindowManager::GetOrCreate(window);
  transient_manager->AddObserver(this);
  for (aura::Window* transient_window :
       transient_manager->transient_children()) {
    StartObservingTransientChild(transient_window);
  }

  RefreshDividerState(/*observed_windows_changed=*/true);
}

void SplitViewDivider::MaybeRemoveObservedWindow(aura::Window* window) {
  auto iter = base::ranges::find(observed_windows_, window);
  if (iter != observed_windows_.end()) {
    window->RemoveObserver(this);
    observed_windows_.erase(iter);
    wm::TransientWindowManager* transient_manager =
        wm::TransientWindowManager::GetOrCreate(window);
    transient_manager->RemoveObserver(this);
    for (aura::Window* transient_window :
         transient_manager->transient_children()) {
      StopObservingTransientChild(transient_window);
    }
    RefreshDividerState(/*observed_windows_changed=*/true);
  }
}

void SplitViewDivider::OnKeyboardOccludedBoundsChangedInPortrait(
    const gfx::Rect& work_area,
    int y) {
  // If the divider widget doesn't exist, i.e. in clamshell split view, we are
  // done.
  if (!divider_widget_) {
    return;
  }

  CHECK(!IsLayoutHorizontal(GetRootWindow()));

  // Else subtract the divider width and update the widget bounds. Note we
  // *don't* update `divider_position_` since it may be used to restore the
  // window bounds in `SplitViewController::OnWindowActivated()`.
  // TODO(b/331459348): Investigate why we don't update `divider_position_` and
  // fix this code.
  const int divider_position = y - kSplitviewDividerShortSideLength;
  divider_widget_->SetBounds(
      GetDividerBoundsInScreen(work_area, /*landscape=*/false, divider_position,
                               /*is_dragging=*/false));

  // Make split view divider unadjustable.
  SetAdjustable(false);
}

void SplitViewDivider::OnWindowDragStarted(aura::Window* dragged_window) {
  dragged_window_ = dragged_window;
  RefreshStackingOrder();
}

void SplitViewDivider::OnWindowDragEnded() {
  dragged_window_ = nullptr;
  RefreshStackingOrder();
}

void SplitViewDivider::SwapWindows() {
  controller_->SwapWindows();
}

void SplitViewDivider::OnWindowDestroying(aura::Window* window) {
  MaybeRemoveObservedWindow(window);
}

void SplitViewDivider::OnWindowBoundsChanged(aura::Window* window,
                                             const gfx::Rect& old_bounds,
                                             const gfx::Rect& new_bounds,
                                             ui::PropertyChangeReason reason) {
  if (is_resizing_with_divider_ &&
      display::Screen::GetScreen()->InTabletMode() &&
      base::Contains(observed_windows_, window)) {
    // Bounds may be changed while we are processing a resize event. In this
    // case, we don't update the windows transform here, since it will be done
    // soon anyway. If we are *not* currently processing a resize, it means the
    // bounds of a window have been updated "async", and we need to update the
    // window's transform.
    if (!processing_resize_event_) {
      // TODO(b/308819668): Remove this reference to `SplitViewController` when
      // we move `divider_position` to here.
      const int divider_position =
          SplitViewController::Get(GetRootWindow())->GetDividerPosition();
      for (aura::Window* window_to_transform : observed_windows_) {
        SetWindowTransformDuringResizing(window_to_transform, divider_position);
      }
    }
  }

  // We only care about the bounds change of windows in
  // |transient_windows_observations_|.
  if (!transient_windows_observations_.IsObservingSource(window))
    return;

  // |window|'s transient parent must be one of the windows in
  // |observed_windows_|.
  aura::Window* transient_parent = nullptr;
  for (aura::Window* observed_window : observed_windows_) {
    if (wm::HasTransientAncestor(window, observed_window)) {
      transient_parent = observed_window;
      break;
    }
  }
  DCHECK(transient_parent);

  // Inset the bounds of the `transient_parent` by `kTransientParentInset`
  // to prevent the snapped window's resize border from obscuring the divider.
  // This simplifies resizing when a transient window is present.
  gfx::Rect adjusted_transient_parent_bounds =
      transient_parent->GetBoundsInScreen();
  adjusted_transient_parent_bounds.Inset(gfx::Insets(kTransientParentInset));
  gfx::Rect transient_bounds = window->GetBoundsInScreen();
  transient_bounds.AdjustToFit(adjusted_transient_parent_bounds);

  window->SetBoundsInScreen(
      transient_bounds,
      display::Screen::GetScreen()->GetDisplayNearestWindow(window));
}

void SplitViewDivider::OnWindowStackingChanged(aura::Window* window) {
  RefreshStackingOrder();
}

void SplitViewDivider::OnWindowVisibilityChanged(aura::Window* window,
                                                 bool visible) {
  RefreshStackingOrder();
}

void SplitViewDivider::OnTransientChildAdded(aura::Window* window,
                                             aura::Window* transient) {
  StartObservingTransientChild(transient);
}

void SplitViewDivider::OnTransientChildRemoved(aura::Window* window,
                                               aura::Window* transient) {
  StopObservingTransientChild(transient);
}

void SplitViewDivider::OnDisplayMetricsChanged(const display::Display& display,
                                               uint32_t metrics) {
  if (!(metrics &
        (DISPLAY_METRIC_BOUNDS | DISPLAY_METRIC_ROTATION |
         DISPLAY_METRIC_DEVICE_SCALE_FACTOR | DISPLAY_METRIC_WORK_AREA))) {
    return;
  }

  UpdateDividerBounds();
}

void SplitViewDivider::RefreshDividerState(bool observed_windows_changed) {
  // Avoid any recursive updates.
  if (is_refreshing_state_) {
    return;
  }
  base::AutoReset<bool> lock(&is_refreshing_state_, true);

  if (observed_windows_.empty()) {
    if (divider_widget_) {
      CloseDividerWidget();
    }
    return;
  }

  if (observed_windows_changed) {
    // Re-set the position since a new set of observed windows might mean
    // different allowed range for the divider position.
    SetDividerPosition(divider_position_);
  }

  // Stacking order is refreshed only if the widget has just been created or the
  // observed windows have changed.
  bool refresh_stacking_order = observed_windows_changed;
  if (!divider_widget_) {
    CreateDividerWidget(divider_position_);
    refresh_stacking_order = true;
  }

  const bool update_visibility =
      target_visibility_ != GetActualTargetVisibility();

  if (target_visibility_) {
    UpdateDividerBounds();
    if (update_visibility) {
      // Call `ShowInactive()` to avoid an unnecessary window activation change
      // when the divider is shown or hidden.
      divider_widget_->ShowInactive();
      // Since the divider may be hidden and re-shown during
      // `SnapGroupController::OnOverviewModeStarting|Ending()`,
      // we need to refresh the stacking order when it's shown again.
      refresh_stacking_order = true;
    }
  } else if (update_visibility) {
    divider_widget_->Hide();
    // Else no need to refresh the stacking order if the divider is hidden.
    refresh_stacking_order = false;
  }

  if (refresh_stacking_order) {
    RefreshStackingOrder();
  }
}

void SplitViewDivider::CreateDividerWidget(int divider_position) {
  DCHECK(!divider_widget_);
  CHECK_GE(observed_windows_.size(), 1u);
  // Native widget owns this widget.
  divider_widget_ = new SplitViewDividerWidget;
  divider_widget_->set_focus_on_creation(false);
  aura::Window* parent_container = nullptr;
  aura::Window* top_window = window_util::GetTopMostWindow(observed_windows_);
  CHECK(top_window);
  parent_container = top_window->parent();
  CHECK(parent_container);

  const gfx::Rect initial_divider_bounds = GetDividerBoundsInScreen(
      GetWorkAreaBoundsInScreen(observed_windows_[0].get()),
      IsLayoutHorizontal(observed_windows_[0].get()), divider_position,
      /*is_dragging=*/false);
  divider_widget_->Init(
      CreateWidgetInitParams(parent_container, initial_divider_bounds));
  divider_widget_->SetVisibilityAnimationTransition(
      views::Widget::ANIMATE_NONE);
  divider_view_ = divider_widget_->SetContentsView(
      std::make_unique<SplitViewDividerView>(this));
  auto* divider_widget_native_window = divider_widget_->GetNativeWindow();
  // TODO(michelefan|sophiewen): Evaluate and remove this property if needed.
  divider_widget_native_window->SetProperty(kLockedToRootKey, true);

  // Use a window targeter and enlarge the hit region to allow located events
  // that are slightly outside the divider widget bounds be consumed by
  // `divider_widget_`.
  auto window_targeter = std::make_unique<aura::WindowTargeter>();
  window_targeter->SetInsets(gfx::Insets::VH(-kSplitViewDividerExtraInset,
                                             -kSplitViewDividerExtraInset));
  divider_widget_native_window->SetEventTargeter(std::move(window_targeter));

  // Explicitly `set_parent_controls_lifetime` to false so that the lifetime of
  // the divider will only be managed by `this`, which avoids UAF on window
  // destroying.
  wm::TransientWindowManager::GetOrCreate(divider_widget_native_window)
      ->set_parent_controls_lifetime(false);

  Shell::Get()->focus_cycler()->AddWidget(divider_widget_);
}

void SplitViewDivider::CloseDividerWidget() {
  // If we're shutting down, no need to refresh the stacking order. This isn't
  // needed but simply added for efficiency.
  base::AutoReset<bool> lock(&is_refreshing_stacking_order_, true);
  while (!observed_windows_.empty()) {
    MaybeRemoveObservedWindow(observed_windows_.back());
  }
  CHECK(!transient_windows_observations_.IsObservingAnySource());

  dragged_window_ = nullptr;

  if (divider_widget_) {
    Shell::Get()->focus_cycler()->RemoveWidget(divider_widget_);
    auto* divider_window = divider_widget_->GetNativeWindow();
    if (auto* transient_parent = wm::GetTransientParent(divider_window)) {
      wm::RemoveTransientChild(transient_parent, divider_window);
    }

    // During the asynchronous window closing, there may be a duration when the
    // divider widget is closing, but the pointer to `this` is not cleared in
    // `SplitViewDividerView` yet, i.e. in `Layout()` which is called during
    // clamshell <-> tablet transition.
    divider_view_->OnDividerClosing();
    // Disable any event handling on the divider while we are closing the
    // widget.
    divider_view_->SetCanProcessEventsWithinSubtree(false);
    divider_window->SetEventTargetingPolicy(aura::EventTargetingPolicy::kNone);
    divider_widget_->Close();
    divider_view_ = nullptr;
    divider_widget_ = nullptr;
  }
}

bool SplitViewDivider::GetActualTargetVisibility() const {
  return divider_widget_ &&
         divider_widget_->GetNativeWindow()->TargetVisibility();
}

void SplitViewDivider::RefreshStackingOrder() {
  // Skip the recursive update.
  if (is_refreshing_stacking_order_) {
    return;
  }

  base::AutoReset<bool> lock(&is_refreshing_stacking_order_, true);

  if (observed_windows_.empty() || !divider_widget_ ||
      !divider_widget_->IsVisible()) {
    return;
  }

  aura::Window::Windows visible_observed_windows;
  for (aura::Window* window : observed_windows_) {
    // During desk switches, the `IsVisible()`, which traverses up the
    // parent layer hierarchy to determine visibility, can be unreliable due to
    // the inactive desk's invisibility. Instead, use `TargetVisibility()`,
    // which directly assesses the window's target visibility, regardless of the
    // visibility of its parent's layer.
    if (window->TargetVisibility()) {
      visible_observed_windows.push_back(window);
    }
  }

  // To get `divider_window` prepared to be the transient window of the
  // `top_window` below, remove `divider_window` as the transient child from its
  // transient parent if any.
  auto* divider_window = divider_widget_->GetNativeWindow();
  if (auto* transient_parent = wm::GetTransientParent(divider_window)) {
    wm::RemoveTransientChild(transient_parent, divider_window);
  }

  CHECK(!wm::GetTransientParent(divider_window));

  if (visible_observed_windows.empty()) {
    divider_window->Hide();
    return;
  }

  aura::Window* top_window =
      window_util::GetTopMostWindow(visible_observed_windows);
  if (!top_window) {
    divider_window->Hide();
    return;
  }

  CHECK(top_window->TargetVisibility());

  auto* divider_sibling_window =
      dragged_window_ ? dragged_window_.get() : top_window;
  CHECK(divider_sibling_window);

  // The divider needs to have the same parent of the `divider_sibling_window`
  // otherwise we need to reparent the divider as below.
  if (divider_sibling_window->parent() != divider_window->parent()) {
    views::Widget::ReparentNativeView(divider_window,
                                      divider_sibling_window->parent());
  }

  if (dragged_window_) {
    divider_window->parent()->StackChildBelow(divider_window, dragged_window_);
    return;
  }

  // Refresh the stacking order of the other window.
  aura::Window* top_window_parent = top_window->parent();
  // Keep a copy as the order of children will be changed while iterating.
  const auto children = top_window_parent->children();

  // Iterate through the siblings of the top window in an increasing z-order
  // which reflects the relative order of siblings.
  for (aura::Window* window : children) {
    if (!base::Contains(visible_observed_windows, window) ||
        window == top_window) {
      continue;
    }

    top_window_parent->StackChildAbove(window, top_window);
    top_window_parent->StackChildAbove(top_window, window);
  }

  // Add the `divider_window` as a transient child of the `top_window`. In
  // this way, on new transient window added, the divider will be stacked above
  // the `top_window` but under the new transient window which is handled in
  // `TransientWindowManager::RestackTransientDescendants()`.
  wm::AddTransientChild(top_window, divider_window);

  top_window_parent->StackChildAbove(divider_window, top_window);
}

void SplitViewDivider::StartObservingTransientChild(aura::Window* transient) {
  // Confine the bounds of a transient window if the given `transient` is a
  // bubble dialog or dialog window.
  if (!window_util::AsBubbleDialogDelegate(transient) &&
      !window_util::AsDialogDelegate(transient)) {
    return;
  }

  // Explicitly check and early return if the `transient` is the divider native
  // window.
  if (divider_widget_ && transient == divider_widget_->GetNativeWindow()) {
    return;
  }

  DCHECK(!transient_windows_observations_.IsObservingSource(transient));

  // At this moment, the transient window may not have the valid bounds yet.
  // Start observing the transient window.
  transient_windows_observations_.AddObservation(transient);
}

void SplitViewDivider::StopObservingTransientChild(aura::Window* transient) {
  if (transient_windows_observations_.IsObservingSource(transient))
    transient_windows_observations_.RemoveObservation(transient);
}

gfx::Point SplitViewDivider::GetEndDragLocationInScreen(
    aura::Window* window) const {
  DCHECK(base::Contains(observed_windows_, window));
  gfx::Point end_location(previous_event_location_);

  const SnapPosition snap_position =
      controller_->GetPositionOfSnappedWindow(window);
  const gfx::Rect bounds = controller_->GetSnappedWindowBoundsInScreen(
      snap_position, window, window_util::GetSnapRatioForWindow(window),
      /*account_for_divider_width=*/true);

  const bool is_physical_left_or_top =
      IsPhysicallyLeftOrTop(snap_position, window);
  if (IsLayoutHorizontal(window)) {
    end_location.set_x(is_physical_left_or_top ? bounds.right() : bounds.x());
  } else {
    end_location.set_y(is_physical_left_or_top ? bounds.bottom() : bounds.y());
  }
  return end_location;
}

void SplitViewDivider::FinishWindowResizing() {
  for (aura::Window* window : observed_windows_) {
    WindowState* window_state = WindowState::Get(window);
    if (window_state->is_dragged()) {
      window_state->OnCompleteDrag(
          gfx::PointF(GetEndDragLocationInScreen(window)));
      window_state->DeleteDragDetails();
    }
  }
}

}  // namespace ash