chromium/ash/wm/overview/overview_group_item.cc

// Copyright 2023 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/overview/overview_group_item.h"

#include <algorithm>

#include "ash/shell.h"
#include "ash/style/rounded_label_widget.h"
#include "ash/wm/desks/desks_util.h"
#include "ash/wm/overview/overview_constants.h"
#include "ash/wm/overview/overview_controller.h"
#include "ash/wm/overview/overview_grid.h"
#include "ash/wm/overview/overview_group_container_view.h"
#include "ash/wm/overview/overview_item.h"
#include "ash/wm/overview/overview_item_base.h"
#include "ash/wm/overview/overview_item_view.h"
#include "ash/wm/overview/overview_session.h"
#include "ash/wm/overview/overview_utils.h"
#include "ash/wm/snap_group/snap_group.h"
#include "ash/wm/snap_group/snap_group_controller.h"
#include "ash/wm/splitview/layout_divider_controller.h"
#include "ash/wm/splitview/split_view_utils.h"
#include "ash/wm/window_util.h"
#include "ash/wm/wm_constants.h"
#include "base/check_op.h"
#include "base/containers/unique_ptr_adapters.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "base/notreached.h"
#include "base/trace_event/trace_event.h"
#include "third_party/abseil-cpp/absl/cleanup/cleanup.h"
#include "ui/gfx/geometry/point.h"
#include "ui/gfx/geometry/rect_f.h"
#include "ui/gfx/geometry/rounded_corners_f.h"
#include "ui/views/widget/widget.h"

namespace ash {

OverviewGroupItem::OverviewGroupItem(const Windows& windows,
                                     OverviewSession* overview_session,
                                     OverviewGrid* overview_grid)
    : OverviewItemBase(overview_session,
                       overview_grid,
                       overview_grid->root_window()) {
  CreateItemWidget();

  CHECK_EQ(windows.size(), 2u);

  const aura::Window* topmost_window = window_util::GetTopMostWindow(windows);
  OverviewItem* bottom_item = nullptr;
  for (aura::Window* window : windows) {
    // Create the overview items hosted by `this`, which will be the delegate to
    // handle the window destroying if the overview representation for the
    // window is hosted by `this`. We also need to explicitly disable the shadow
    // to be installed on individual overview item hosted by `this` as the
    // group-level shadow will be installed instead.
    std::unique_ptr<OverviewItem> overview_item =
        std::make_unique<OverviewItem>(window, overview_session_,
                                       overview_grid_,
                                       /*destruction_delegate=*/this,
                                       /*event_handler_delegate=*/this,
                                       /*eligible_for_shadow_config=*/false);
    if (window != topmost_window) {
      bottom_item = overview_item.get();
    }
    overview_items_.push_back(std::move(overview_item));
  }

  // Explicitly stack the window of the group item widget below the item widget
  // whose window is lower in stacking order so that the `OverviewItemView` will
  // be able to receive the events.
  aura::Window* widget_window = item_widget_->GetNativeWindow();
  widget_window->parent()->StackChildBelow(
      widget_window, bottom_item->item_widget()->GetNativeWindow());
}

OverviewGroupItem::~OverviewGroupItem() = default;

void OverviewGroupItem::SetOpacity(float opacity) {
  OverviewItemBase::SetOpacity(opacity);

  for (const auto& overview_item : overview_items_) {
    overview_item->SetOpacity(opacity);
  }
}

aura::Window::Windows OverviewGroupItem::GetWindowsForHomeGesture() {
  aura::Window::Windows windows = OverviewItemBase::GetWindowsForHomeGesture();

  for (const auto& overview_item : overview_items_) {
    aura::Window::Windows item_windows =
        overview_item->GetWindowsForHomeGesture();
    windows.insert(windows.end(), item_windows.begin(), item_windows.end());
  }

  return windows;
}

void OverviewGroupItem::HideForSavedDeskLibrary(bool animate) {
  for (const auto& item : overview_items_) {
    item->HideForSavedDeskLibrary(animate);
  }

  OverviewItemBase::HideForSavedDeskLibrary(animate);
}

void OverviewGroupItem::RevertHideForSavedDeskLibrary(bool animate) {
  for (const auto& item : overview_items_) {
    item->RevertHideForSavedDeskLibrary(animate);
  }

  OverviewItemBase::RevertHideForSavedDeskLibrary(animate);
}

void OverviewGroupItem::UpdateMirrorsForDragging(bool is_touch_dragging) {
  // TODO(http://b/339516036): Revisit whether we should update mirror for the
  // group's `item_widget_` after the blue background issue is resolved.
  for (const auto& overview_item : overview_items_) {
    overview_item->UpdateMirrorsForDragging(is_touch_dragging);
  }
}

void OverviewGroupItem::DestroyMirrorsForDragging() {
  // TODO(http://b/339516036): Revisit whether we should destroy mirror for the
  // group's `item_widget_` after the blue background issue is resolved.
  for (const auto& overview_item : overview_items_) {
    overview_item->DestroyMirrorsForDragging();
  }
}

aura::Window* OverviewGroupItem::GetWindow() {
  // TODO(michelefan): `GetWindow()` will be replaced by `GetWindows()` in a
  // follow-up cl.
  CHECK_LE(overview_items_.size(), 2u);
  return overview_items_.empty() ? nullptr : overview_items_[0]->GetWindow();
}

std::vector<raw_ptr<aura::Window, VectorExperimental>>
OverviewGroupItem::GetWindows() {
  std::vector<raw_ptr<aura::Window, VectorExperimental>> windows;
  for (const auto& item : overview_items_) {
    windows.push_back(item->GetWindow());
  }

  return windows;
}

bool OverviewGroupItem::HasVisibleOnAllDesksWindow() {
  for (const auto& item : overview_items_) {
    if (item->HasVisibleOnAllDesksWindow()) {
      return true;
    }
  }

  return false;
}

bool OverviewGroupItem::Contains(const aura::Window* target) const {
  for (const auto& item : overview_items_) {
    if (item->Contains(target)) {
      return true;
    }
  }

  return false;
}

OverviewItem* OverviewGroupItem::GetLeafItemForWindow(aura::Window* window) {
  for (const auto& item : overview_items_) {
    if (item->GetWindow() == window) {
      return item.get();
    }
  }

  return nullptr;
}

void OverviewGroupItem::RestoreWindow(bool reset_transform, bool animate) {
  for (const auto& item : overview_items_) {
    item->RestoreWindow(reset_transform, animate);
  }
}

void OverviewGroupItem::SetBounds(const gfx::RectF& target_bounds,
                                  OverviewAnimationType animation_type) {
  // Run at the exit of this function to `UpdateRoundedCornersAndShadow()`.
  // TODO(dcheng): This can probably just capture `this`.
  absl::Cleanup exit_runner = [overview_group_item =
                                   weak_ptr_factory_.GetWeakPtr()] {
    CHECK(overview_group_item);
    overview_group_item->UpdateRoundedCornersAndShadow();
  };

  target_bounds_ = target_bounds;

  const int size = overview_items_.size();
  auto& item0 = overview_items_[0];
  if (size == 1) {
    return item0->SetBounds(target_bounds, animation_type);
  }

  CHECK_EQ(2, size);
  auto& item1 = overview_items_[1];

  aura::Window* item0_window = item0->GetWindow();
  aura::Window* item1_window = item1->GetWindow();
  const gfx::Rect work_area = display::Screen::GetScreen()
                                  ->GetDisplayNearestWindow(item0_window)
                                  .work_area();
  const bool is_horizontal = IsLayoutHorizontal(item0_window);
  item_widget_->SetBounds(gfx::ToRoundedRect(target_bounds));

  if (is_horizontal) {
    // Calculate the ratio that reflects how much the windows' widths should be
    // scaled to fit within `target_bounds`.
    const float ratio =
        static_cast<float>(target_bounds.width()) / work_area.width();
    auto item0_bounds = target_bounds;
    item0_bounds.set_width(ratio * item0_window->bounds().width());
    item0->SetBounds(item0_bounds, animation_type);

    const auto item1_width = ratio * item1_window->bounds().width();
    auto item1_bounds = target_bounds;
    item1_bounds.set_width(item1_width);
    item1_bounds.set_x(target_bounds.right() - item1_width);
    item1->SetBounds(item1_bounds, animation_type);

    return;
  }

  const float ratio =
      static_cast<float>(target_bounds.height()) / work_area.height();
  auto item0_bounds = target_bounds;
  item0_bounds.set_height(ratio * item0_window->bounds().height());
  item0->SetBounds(item0_bounds, animation_type);

  const auto item1_height = ratio * item1_window->bounds().height();
  auto item1_bounds = target_bounds;
  item1_bounds.set_height(item1_height);
  item1_bounds.set_y(target_bounds.bottom() - item1_height);
  item1->SetBounds(item1_bounds, animation_type);
}

gfx::Transform OverviewGroupItem::ComputeTargetTransform(
    const gfx::RectF& target_bounds) {
  return gfx::Transform();
}

gfx::RectF OverviewGroupItem::GetWindowsUnionScreenBounds() const {
  gfx::RectF target_bounds;
  for (const auto& item : overview_items_) {
    target_bounds.Union(item->GetWindowsUnionScreenBounds());
  }

  return target_bounds;
}

gfx::RectF OverviewGroupItem::GetTargetBoundsWithInsets() const {
  gfx::RectF target_bounds_with_insets = target_bounds_;
  target_bounds_with_insets.Inset(
      gfx::InsetsF::TLBR(kWindowMiniViewHeaderHeight, 0, 0, 0));
  return target_bounds_with_insets;
}

gfx::RectF OverviewGroupItem::GetTransformedBounds() const {
  return GetWindowsUnionScreenBounds();
}

float OverviewGroupItem::GetItemScale(int height) {
  CHECK(!overview_items_.empty());

  // Calculate the scaling factor for the group item:
  //
  // For horizontal window layout, the title and item height remain consistent
  // across all items in the group. The larger of the two windows'
  // `kTopViewInset` properties is applied for the calculation.
  //
  //     +--------------------++--------------------+
  //     | Window 0 Header    || Window 1 Header    |
  //     +--------------------++--------------------|
  //     |                    ||                    |
  //     | Window 0 Preview   || Window 1 Preview   |
  //     |                    ||                    |
  //     +--------------------++--------------------+
  //
  // In a vertical window layout, double the fixed header height
  // (`kWindowMiniViewHeaderHeight`) and apply the sum of both windows'
  // `kTopViewInset` properties for the calculation.
  //
  //     +--------------------+
  //     | Window 0 Header    |
  //     +--------------------+
  //     | Window 0 Preview   |
  //     |                    |
  //     +--------------------+
  //     +--------------------+
  //     | Window 1 Header    |
  //     +--------------------+
  //     | Window 1 Preview   |
  //     |                    |
  //     +--------------------+

  const bool is_layout_horizontal =
      IsLayoutHorizontal(overview_items_[0]->GetWindow());

  int top_inset = 0;
  for (const auto& overview_item : overview_items_) {
    const int item_top_inset = overview_item->GetTopInset();
    if (is_layout_horizontal) {
      top_inset = std::max(item_top_inset, top_inset);
    } else {
      top_inset += item_top_inset;
    }
  }

  return ScopedOverviewTransformWindow::GetItemScale(
      /*source_height=*/GetWindowsUnionScreenBounds().height(),
      /*target_height=*/height,
      /*top_view_inset=*/top_inset,
      /*title_height=*/
      is_layout_horizontal ? kWindowMiniViewHeaderHeight
                           : 2 * kWindowMiniViewHeaderHeight);
}

void OverviewGroupItem::ScaleUpSelectedItem(
    OverviewAnimationType animation_type) {}

void OverviewGroupItem::EnsureVisible() {
  for (const auto& overview_item : overview_items_) {
    overview_item->EnsureVisible();
  }
}

std::vector<views::Widget*> OverviewGroupItem::GetFocusableWidgets() {
  std::vector<views::Widget*> focusable_widgets;
  for (const auto& overview_item : overview_items_) {
    focusable_widgets.push_back(overview_item->item_widget());
  }
  return focusable_widgets;
}

views::View* OverviewGroupItem::GetBackDropView() const {
  return overview_group_container_view_;
}

bool OverviewGroupItem::ShouldHaveShadow() const {
  return overview_items_.size() > 1u;
}

void OverviewGroupItem::UpdateRoundedCornersAndShadow() {
  for (const auto& overview_item : overview_items_) {
    overview_item->UpdateRoundedCorners();
  }

  RefreshShadowVisuals(/*shadow_visible=*/true);
}

float OverviewGroupItem::GetOpacity() const {
  // TODO(michelefan): This is a temporary placeholder value. The opacity
  // settings will be handled in a separate task.
  return 1.f;
}

void OverviewGroupItem::PrepareForOverview() {
  for (const auto& overview_item : overview_items_) {
    overview_item->PrepareForOverview();
  }

  prepared_for_overview_ = true;
}

void OverviewGroupItem::SetShouldUseSpawnAnimation(bool value) {
  for (const auto& item : overview_items_) {
    item->SetShouldUseSpawnAnimation(value);
  }

  should_use_spawn_animation_ = value;
}

void OverviewGroupItem::OnStartingAnimationComplete() {
  for (const auto& item : overview_items_) {
    item->OnStartingAnimationComplete();
  }
}

void OverviewGroupItem::Restack() {
  if (overview_items_.empty() || !item_widget_) {
    return;
  }

  // Sort the items in `sorted_items` based on their stacking order, starting
  // with the lowest.
  std::vector<OverviewItem*> sorted_items;
  for (const auto& overview_item : overview_items_) {
    sorted_items.push_back(overview_item.get());
  }

  std::sort(sorted_items.begin(), sorted_items.end(),
            [](OverviewItem* a, OverviewItem* b) {
              return window_util::IsStackedBelow(a->GetWindow(),
                                                 b->GetWindow());
            });

  for (auto* overview_item : sorted_items) {
    overview_item->Restack();
  }

  // Then `sorted_items.front()` is the lowest, and `sorted_items.back()` is the
  // topmost.
  aura::Window* group_item_widget_window = item_widget_->GetNativeWindow();
  aura::Window* group_item_widget_window_parent =
      group_item_widget_window->parent();

  // Adjust the stacking order between the two individual items and the group
  // item and stack group item widget below the bottom window between the two.
  group_item_widget_window_parent->StackChildBelow(
      group_item_widget_window,
      sorted_items.front()->item_widget()->GetNativeWindow());

  // And stack the `cannot_snap_widget_` above the window of the topmost item.
  if (cannot_snap_widget_) {
    DCHECK_EQ(group_item_widget_window_parent,
              cannot_snap_widget_->GetNativeWindow()->parent());
    group_item_widget_window_parent->StackChildAbove(
        cannot_snap_widget_->GetNativeWindow(),
        sorted_items.back()->GetWindow());
  }
}

void OverviewGroupItem::StartDrag() {
  for (const auto& item : overview_items_) {
    item->StartDrag();
  }
}

void OverviewGroupItem::OnOverviewItemDragStarted() {
  for (const auto& item : overview_items_) {
    item->OnOverviewItemDragStarted();
  }
}

void OverviewGroupItem::OnOverviewItemDragEnded(bool snap) {
  for (const auto& item : overview_items_) {
    item->OnOverviewItemDragEnded(snap);
  }

  // Refreshes the stacking order of `this` so that the `item_widget_` window of
  // the group is stacked below the two windows allowing the `OverviewItemView`
  // to receive the events.
  Restack();
}

void OverviewGroupItem::OnOverviewItemContinuousScroll(
    const gfx::Transform& target_transform,
    float scroll_ratio) {}

void OverviewGroupItem::UpdateCannotSnapWarningVisibility(bool animate) {}

void OverviewGroupItem::HideCannotSnapWarning(bool animate) {}

void OverviewGroupItem::OnMovingItemToAnotherDesk() {
  is_moving_to_another_desk_ = true;

  for (const auto& overview_item : overview_items_) {
    overview_item->OnMovingItemToAnotherDesk();
  }
}

void OverviewGroupItem::Shutdown() {
  for (const auto& overview_item : overview_items_) {
    overview_item->Shutdown();
  }
}

void OverviewGroupItem::AnimateAndCloseItem(bool up) {
  animating_to_close_ = true;

  for (const auto& overview_item : overview_items_) {
    overview_item->AnimateAndCloseItem(up);
  }
}

void OverviewGroupItem::StopWidgetAnimation() {
  for (const auto& overview_item : overview_items_) {
    overview_item->StopWidgetAnimation();
  }

  item_widget_->GetNativeWindow()->layer()->GetAnimator()->StopAnimating();
}

OverviewItemFillMode OverviewGroupItem::GetOverviewItemFillMode() const {
  return ash::GetOverviewItemFillMode(
      gfx::ToRoundedSize(target_bounds_.size()));
}

void OverviewGroupItem::UpdateOverviewItemFillMode() {
  for (const auto& overview_item : overview_items_) {
    overview_item->UpdateOverviewItemFillMode();
  }
}

const gfx::RoundedCornersF OverviewGroupItem::GetRoundedCorners() const {
  auto& item0 = overview_items_.front();
  const gfx::RoundedCornersF& primary_rounded_corners =
      item0->GetRoundedCorners();
  const gfx::RoundedCornersF& secondary_rounded_corners =
      overview_items_.back()->GetRoundedCorners();
  return IsLayoutHorizontal(item0->GetWindow())
             ? gfx::RoundedCornersF(primary_rounded_corners.upper_left(),
                                    secondary_rounded_corners.upper_right(),
                                    secondary_rounded_corners.lower_right(),
                                    primary_rounded_corners.lower_left())
             : gfx::RoundedCornersF(primary_rounded_corners.upper_left(),
                                    primary_rounded_corners.upper_right(),
                                    secondary_rounded_corners.lower_right(),
                                    secondary_rounded_corners.lower_left());
}

void OverviewGroupItem::OnOverviewItemWindowDestroying(
    OverviewItem* overview_item,
    bool reposition) {
  // We use 2-step removal to ensure that the `overview_item` gets removed from
  // the vector before been destroyed so that all the overview items in
  // `overview_items_` are valid.
  auto iter = base::ranges::find_if(overview_items_,
                                    base::MatchesUniquePtr(overview_item));
  auto to_be_removed = std::move(*iter);
  overview_items_.erase(iter);
  to_be_removed.reset();

  if (overview_items_.empty()) {
    overview_grid_->RemoveItem(this, /*item_destroying=*/true, reposition);
    return;
  }

  for (const auto& item : overview_items_) {
    if (item && item.get() != overview_item) {
      // Remove the group-level shadow and apply it on the window-level to
      // ensure that the shadow bounds get updated properly.
      item->set_eligible_for_shadow_config(/*eligible_for_shadow_config=*/true);

      OverviewItemView* item_view = item->overview_item_view();
      item_view->ResetRoundedCorners();
    }
  }

  overview_grid_->PositionWindows(/*animate=*/true);
}

void OverviewGroupItem::HandleDragEvent(const gfx::PointF& location_in_screen) {
  if (IsDragItem()) {
    overview_session_->Drag(this, location_in_screen);
  }
}

void OverviewGroupItem::CreateItemWidget() {
  TRACE_EVENT0("ui", "OverviewGroupItem::CreateItemWidget");

  item_widget_ = std::make_unique<views::Widget>();
  item_widget_->set_focus_on_creation(false);
  item_widget_->Init(CreateOverviewItemWidgetParams(
      desks_util::GetActiveDeskContainerForRoot(overview_grid_->root_window()),
      "OverviewGroupItemWidget", /*accept_events=*/false));

  CreateShadow();

  overview_group_container_view_ = item_widget_->SetContentsView(
      std::make_unique<OverviewGroupContainerView>(this));
  item_widget_->Show();
  item_widget_->GetLayer()->SetMasksToBounds(/*masks_to_bounds=*/false);
}

}  // namespace ash