#include "ash/wm/splitview/split_view_divider.h"
#include <algorithm>
#include <memory>
#include "ash/accessibility/ui/accessibility_focusable_widget_delegate.h"
#include "ash/display/screen_orientation_controller.h"
#include "ash/focus/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 "chromeos/ui/base/chromeos_ui_constants.h"
#include "ui/aura/window_targeter.h"
#include "ui/compositor/scoped_layer_animation_settings.h"
#include "ui/display/screen.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/views/bubble/bubble_dialog_delegate_view.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 {
constexpr float kOpacityForTransientDuringResize = 0.5f;
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);
}
views::Widget::InitParams CreateWidgetInitParams(
aura::Window* parent_window,
const gfx::Rect& bounds,
views::WidgetDelegate* delegate) {
views::Widget::InitParams params(
views::Widget::InitParams::CLIENT_OWNS_WIDGET,
views::Widget::InitParams::TYPE_POPUP);
params.opacity = views::Widget::InitParams::WindowOpacity::kOpaque;
params.activatable = views::Widget::InitParams::Activatable::kNo;
params.parent = parent_window;
params.delegate = delegate;
params.bounds = bounds;
params.init_properties_container.SetProperty(kExcludeInMruKey, true);
params.init_properties_container.SetProperty(kIgnoreWindowActivationKey,
true);
params.init_properties_container.SetProperty(kHideInDeskMiniViewKey, true);
params.init_properties_container.SetProperty(
kExcludeFromTransientTreeTransformKey, true);
params.name = "SplitViewDividerWidget";
return params;
}
}
class SplitViewDivider::SplitViewDividerWidget : public views::Widget {
public:
SplitViewDividerWidget() = default;
SplitViewDividerWidget(const SplitViewDividerWidget&) = delete;
SplitViewDividerWidget& operator=(const SplitViewDividerWidget&) = delete;
~SplitViewDividerWidget() override = default;
bool OnNativeWidgetActivationChanged(bool active) override {
if (!Widget::OnNativeWidgetActivationChanged(active)) {
return false;
}
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::ColorProviderKey GetColorProviderKey() const override {
return ui::NativeTheme::GetInstanceForNativeUi()->GetColorProviderKey(
nullptr);
}
};
SplitViewDivider::SplitViewDivider(LayoutDividerController* controller)
: controller_(controller) {}
SplitViewDivider::~SplitViewDivider() = default;
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_window_observation_.GetSource();
}
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(false);
}
}
void SplitViewDivider::SetDividerPosition(int divider_position) {
if (divider_position_ == divider_position) {
return;
}
divider_position_ = divider_position;
if (!observed_windows_.empty() && !display::Screen::Get()->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()));
}
}
RefreshDividerState(false);
}
void SplitViewDivider::UpdateDividerPosition(
const gfx::Point& location_in_screen) {
aura::Window* root = GetRootWindow();
const bool horizontal = IsLayoutHorizontal(root);
if (!display::Screen::Get()->InTabletMode()) {
gfx::Point location_in_root(location_in_screen);
wm::ConvertPointFromScreen(root, &location_in_root);
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();
}
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) {
if (is_resizing_with_divider_ ||
SplitViewController::Get(GetRootWindow())->IsDividerAnimating()) {
return;
}
is_resizing_with_divider_ = true;
EnlargeOrShrinkDivider(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);
}
for (auto transient_window : transient_windows_observations_.sources()) {
ui::ScopedLayerAnimationSettings settings(
transient_window->layer()->GetAnimator());
transient_window->layer()->SetOpacity(kOpacityForTransientDuringResize);
}
}
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()));
UpdateDividerPosition(modified_location_in_screen);
EnlargeOrShrinkDivider(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()));
UpdateDividerPosition(modified_location_in_screen);
const gfx::Point cursor_point =
display::Screen::Get()->GetCursorScreenPoint();
EnlargeOrShrinkDivider(
GetDividerBoundsInScreen(true).Contains(cursor_point));
if (controller_->EndResizeWithDivider(modified_location_in_screen)) {
CleanUpWindowResizing();
}
}
void SplitViewDivider::CleanUpWindowResizing() {
is_resizing_with_divider_ = false;
controller_->OnResizeEnding();
FinishWindowResizing();
controller_->OnResizeEnded();
}
void SplitViewDivider::UpdateDividerBounds() {
if (divider_widget_) {
divider_widget_->SetBounds(GetDividerBoundsInScreen(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();
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(true);
}
void SplitViewDivider::MaybeRemoveObservedWindow(aura::Window* window) {
auto iter = std::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(true);
}
}
void SplitViewDivider::OnKeyboardOccludedBoundsChangedInPortrait(
const gfx::Rect& work_area,
int y) {
if (!divider_widget_) {
return;
}
CHECK(!IsLayoutHorizontal(GetRootWindow()));
const int divider_position = y - kSplitviewDividerShortSideLength;
divider_widget_->SetBounds(
GetDividerBoundsInScreen(work_area, false, divider_position,
false));
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) {
if (divider_window_observation_.IsObservingSource(window)) {
DCHECK_EQ(window, GetDividerWindow());
divider_window_observation_.Reset();
divider_view_ = nullptr;
divider_widget_.reset();
return;
}
MaybeRemoveObservedWindow(window);
}
void SplitViewDivider::OnWindowStackingChanged(aura::Window* window) {
if (divider_window_observation_.IsObservingSource(window)) {
DCHECK_EQ(window, GetDividerWindow());
return;
}
RefreshStackingOrder();
}
void SplitViewDivider::OnWindowVisibilityChanged(aura::Window* window,
bool visible) {
if (divider_window_observation_.IsObservingSource(window)) {
DCHECK_EQ(window, GetDividerWindow());
return;
}
if (transient_windows_observations_.IsObservingSource(window) && visible &&
is_resizing_with_divider_) {
window->layer()->SetOpacity(kOpacityForTransientDuringResize);
}
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) {
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) {
SetDividerPosition(divider_position_);
}
bool refresh_stacking_order = observed_windows_changed;
if (!divider_widget_ && target_visibility_) {
CreateDividerWidget(divider_position_);
refresh_stacking_order = true;
}
const bool update_visibility =
target_visibility_ != GetActualTargetVisibility();
if (target_visibility_) {
UpdateDividerBounds();
if (update_visibility) {
divider_widget_->ShowInactive();
refresh_stacking_order = true;
}
} else if (update_visibility) {
divider_widget_->Hide();
refresh_stacking_order = false;
}
if (refresh_stacking_order) {
RefreshStackingOrder();
}
}
void SplitViewDivider::CreateDividerWidget(int divider_position) {
DCHECK(!divider_widget_);
CHECK_GE(observed_windows_.size(), 1u);
widget_delegate_ = std::make_unique<AccessibilityFocusableWidgetDelegate>(
true);
divider_widget_ = std::make_unique<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,
false);
divider_widget_->Init(CreateWidgetInitParams(
parent_container, initial_divider_bounds, widget_delegate_.get()));
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();
divider_widget_native_window->SetProperty(kLockedToRootKey, true);
divider_window_observation_.Observe(divider_widget_native_window);
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));
wm::TransientWindowManager::GetOrCreate(divider_widget_native_window)
->set_parent_controls_lifetime(false);
}
void SplitViewDivider::CloseDividerWidget() {
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_) {
auto* const divider_window = divider_widget_->GetNativeWindow();
if (divider_window) {
divider_window_observation_.Reset();
if (auto* const transient_parent =
wm::GetTransientParent(divider_window)) {
wm::RemoveTransientChild(transient_parent, divider_window);
}
}
divider_view_->OnDividerClosing();
divider_view_->SetCanProcessEventsWithinSubtree(false);
if (divider_window) {
divider_window->SetEventTargetingPolicy(
aura::EventTargetingPolicy::kNone);
}
divider_view_ = nullptr;
divider_widget_.reset();
widget_delegate_.reset();
}
}
bool SplitViewDivider::GetActualTargetVisibility() const {
return divider_widget_ && divider_widget_->GetNativeWindow() &&
divider_widget_->GetNativeWindow()->TargetVisibility();
}
void SplitViewDivider::RefreshStackingOrder() {
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_) {
if (window->TargetVisibility()) {
visible_observed_windows.push_back(window);
}
}
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);
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;
}
aura::Window* top_window_parent = top_window->parent();
const auto children = top_window_parent->children();
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);
}
wm::AddTransientChild(top_window, divider_window);
top_window_parent->StackChildAbove(divider_window, top_window);
}
void SplitViewDivider::StartObservingTransientChild(aura::Window* transient) {
if (!window_util::AsBubbleDialogDelegate(transient) &&
!window_util::AsDialogDelegate(transient)) {
return;
}
auto* widget = views::Widget::GetTopLevelWidgetForNativeView(transient);
if (widget->widget_delegate() &&
widget->widget_delegate()->AsBubbleDialogDelegate()) {
auto* bubble_dialog_delegate =
widget->widget_delegate()->AsBubbleDialogDelegate();
bool has_anchor = bubble_dialog_delegate->GetAnchorView() &&
!bubble_dialog_delegate->GetAnchorRect().IsEmpty();
if (has_anchor) {
return;
}
}
if (divider_widget_ && transient == divider_widget_->GetNativeWindow()) {
return;
}
DCHECK(!transient_windows_observations_.IsObservingSource(transient));
transient_windows_observations_.AddObservation(transient);
}
void SplitViewDivider::StopObservingTransientChild(aura::Window* transient) {
if (transient_windows_observations_.IsObservingSource(transient)) {
transient_windows_observations_.RemoveObservation(transient);
if (is_resizing_with_divider_ && transient->layer()->GetTargetOpacity() ==
kOpacityForTransientDuringResize) {
transient->layer()->SetOpacity(1.0f);
}
}
}
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),
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();
}
}
for (auto transient_window : transient_windows_observations_.sources()) {
ui::ScopedLayerAnimationSettings settings(
transient_window->layer()->GetAnimator());
transient_window->layer()->SetOpacity(1.0f);
}
}
}