chromium/ash/wm/drag_window_controller.cc

// Copyright 2012 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/drag_window_controller.h"

#include "ash/public/cpp/shell_window_ids.h"
#include "ash/public/cpp/window_properties.h"
#include "ash/shell.h"
#include "ash/wm/window_mirror_view.h"
#include "base/memory/raw_ptr.h"
#include "ui/aura/client/aura_constants.h"
#include "ui/aura/client/screen_position_client.h"
#include "ui/aura/window.h"
#include "ui/aura/window_event_dispatcher.h"
#include "ui/aura/window_tree_host.h"
#include "ui/base/hit_test.h"
#include "ui/compositor/compositor.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/paint_context.h"
#include "ui/compositor_extra/shadow.h"
#include "ui/display/display.h"
#include "ui/gfx/geometry/transform_util.h"
#include "ui/views/view.h"
#include "ui/views/widget/widget.h"
#include "ui/wm/core/coordinate_conversion.h"
#include "ui/wm/core/cursor_manager.h"
#include "ui/wm/core/shadow_types.h"
#include "ui/wm/core/window_util.h"

namespace ash {

namespace {

// The maximum opacity of the drag phantom window.
constexpr float kDragPhantomMaxOpacity = 0.8f;

// Computes an opacity value for |dragged_window| or for a drag window that
// represents |dragged_window| on |root_window|.
float GetDragWindowOpacity(aura::Window* root_window,
                           aura::Window* dragged_window,
                           bool is_touch_dragging) {
  // For touch dragging, the present function should only need to be used for
  // the drag windows; the opacity of the original window can simply be set to 1
  // in the constructor and reverted in the destructor.
  DCHECK(!is_touch_dragging || dragged_window->GetRootWindow() != root_window);
  // For mouse dragging, if the mouse is in |root_window|, then return 1.
  if (!is_touch_dragging && Shell::Get()->cursor_manager()->GetDisplay().id() ==
                                display::Screen::GetScreen()
                                    ->GetDisplayNearestWindow(root_window)
                                    .id()) {
    return 1.f;
  }

  // Return an opacity value based on what fraction of |dragged_window| is
  // contained in |root_window|.
  gfx::RectF dragged_window_bounds(dragged_window->bounds());
  ::wm::TranslateRectToScreen(dragged_window->parent(), &dragged_window_bounds);
  dragged_window_bounds =
      gfx::TransformAboutPivot(dragged_window_bounds.origin(),
                               dragged_window->transform())
          .MapRect(dragged_window_bounds);
  gfx::RectF visible_bounds(root_window->GetBoundsInScreen());
  visible_bounds.Intersect(dragged_window_bounds);
  return kDragPhantomMaxOpacity * visible_bounds.size().GetArea() /
         dragged_window_bounds.size().GetArea();
}

}  // namespace

// This keeps track of the drag window's state. It creates/destroys/updates
// bounds and opacity based on the current bounds.
class DragWindowController::DragWindowDetails {
 public:
  explicit DragWindowDetails(const display::Display& display)
      : root_window_(Shell::GetRootWindowForDisplayId(display.id())) {}
  DragWindowDetails(const DragWindowDetails&) = delete;
  DragWindowDetails& operator=(const DragWindowDetails&) = delete;
  ~DragWindowDetails() = default;

  void Update(aura::Window* original_window,
              bool is_touch_dragging,
              const std::optional<gfx::Rect>& shadow_bounds) {
    const float opacity =
        GetDragWindowOpacity(root_window_, original_window, is_touch_dragging);
    if (opacity == 0.f) {
      shadow_.reset();
      widget_.reset();
      return;
    }

    if (!widget_)
      CreateDragWindow(original_window, shadow_bounds);

    gfx::Rect bounds = original_window->bounds();
    aura::Window* window = widget_->GetNativeWindow();
    aura::Window::ConvertRectToTarget(original_window->parent(),
                                      window->parent(), &bounds);
    window->SetBounds(bounds);
    window->SetTransform(original_window->transform());
    widget_->SetOpacity(opacity);
  }

 private:
  friend class DragWindowController;

  void CreateDragWindow(aura::Window* original_window,
                        const std::optional<gfx::Rect>& shadow_bounds) {
    DCHECK(!widget_);
    views::Widget::InitParams params(
        views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET,
        views::Widget::InitParams::TYPE_POPUP);
    params.opacity = views::Widget::InitParams::WindowOpacity::kTranslucent;
    params.layer_type = ui::LAYER_NOT_DRAWN;
    params.name = "DragWindow";
    params.activatable = views::Widget::InitParams::Activatable::kNo;
    params.accept_events = false;
    params.init_properties_container.SetProperty(kHideInDeskMiniViewKey, true);
    params.parent =
        root_window_->GetChildById(original_window->parent()->GetId());

    widget_ = std::make_unique<views::Widget>();
    widget_->set_focus_on_creation(false);
    widget_->Init(std::move(params));

    // TODO(b/252525521): Change this to WindowPreviewView.
    // WindowPreviewView can show transient children, but currently does not
    // show popups due to performance reasons. WindowPreviewView also needs to
    // be modified so that it can optionally be clipped to the main window's
    // bounds.
    widget_->SetContentsView(std::make_unique<WindowMirrorView>(
        original_window, /*show_non_client_view=*/true, /*sync_bounds=*/true));

    aura::Window* window = widget_->GetNativeWindow();
    window->SetId(kShellWindowId_PhantomWindow);
    window->SetProperty(aura::client::kAnimationsDisabledKey, true);
    gfx::Rect bounds = original_window->bounds();
    ::wm::ConvertRectToScreen(original_window->parent(), &bounds);
    window->SetBounds(bounds);

    if (shadow_bounds) {
      shadow_ = std::make_unique<ui::Shadow>();
      shadow_->Init(::wm::kShadowElevationActiveWindow);
      shadow_->SetContentBounds(*shadow_bounds);
      widget_->GetLayer()->Add(shadow_->layer());
    } else {
      ::wm::SetShadowElevation(window, ::wm::kShadowElevationActiveWindow);
    }

    // Show the widget the setup is done.
    widget_->Show();
  }

  // The root window of |widget_|.
  raw_ptr<aura::Window> root_window_;

  // Contains a WindowMirrorView which is a copy of the original window.
  std::unique_ptr<views::Widget> widget_;

  // Optional custom shadow if one is given.
  std::unique_ptr<ui::Shadow> shadow_;
};

DragWindowController::DragWindowController(
    aura::Window* window,
    bool is_touch_dragging,
    const std::optional<gfx::Rect>& shadow_bounds)
    : window_(window),
      is_touch_dragging_(is_touch_dragging),
      shadow_bounds_(shadow_bounds),
      old_opacity_(window->layer()->GetTargetOpacity()) {
  window->layer()->SetOpacity(1.f);

  DCHECK(drag_windows_.empty());
  display::Screen* screen = display::Screen::GetScreen();
  display::Display current = screen->GetDisplayNearestWindow(window_);
  for (const display::Display& display : screen->GetAllDisplays()) {
    if (current.id() == display.id())
      continue;
    drag_windows_.push_back(std::make_unique<DragWindowDetails>(display));
  }
}

DragWindowController::~DragWindowController() {
  LOG_IF(ERROR, old_opacity_ < 1.0f)
      << "Ended drag and restored window to opacity < 1.0f, which is likely "
         "not intended.";
  window_->layer()->SetOpacity(old_opacity_);
}

void DragWindowController::Update() {
  // For mouse dragging, update the opacity of the original window. For touch
  // dragging, just leave that opacity at 1.
  if (!is_touch_dragging_) {
    window_->layer()->SetOpacity(GetDragWindowOpacity(
        window_->GetRootWindow(), window_, /*is_touch_dragging=*/false));
  }

  for (std::unique_ptr<DragWindowDetails>& details : drag_windows_)
    details->Update(window_, is_touch_dragging_, shadow_bounds_);
}

int DragWindowController::GetDragWindowsCountForTest() const {
  int count = 0;
  for (const std::unique_ptr<DragWindowDetails>& details : drag_windows_) {
    if (details->widget_)
      count++;
  }
  return count;
}

const aura::Window* DragWindowController::GetDragWindowForTest(
    size_t index) const {
  for (const std::unique_ptr<DragWindowDetails>& details : drag_windows_) {
    if (details->widget_) {
      if (index == 0)
        return details->widget_->GetNativeWindow();
      index--;
    }
  }
  return nullptr;
}

const ui::Shadow* DragWindowController::GetDragWindowShadowForTest(
    size_t index) const {
  for (const std::unique_ptr<DragWindowDetails>& details : drag_windows_) {
    if (details->widget_) {
      if (index == 0)
        return details->shadow_.get();
      index--;
    }
  }
  return nullptr;
}

void DragWindowController::RequestLayerPaintForTest() {
  auto list = base::MakeRefCounted<cc::DisplayItemList>();
  ui::PaintContext context(list.get(), 1.0f, gfx::Rect(),
                           window_->GetHost()->compositor()->is_pixel_canvas());
  for (auto& details : drag_windows_) {
    std::vector<ui::Layer*> layers;
    layers.push_back(details->widget_->GetLayer());
    while (layers.size()) {
      ui::Layer* layer = layers.back();
      layers.pop_back();
      if (layer->delegate())
        layer->delegate()->OnPaintLayer(context);
      for (ui::Layer* child : layer->children()) {
        layers.push_back(child);
      }
    }
  }
}

}  // namespace ash