chromium/ash/display/display_alignment_controller.cc

// Copyright 2020 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/display/display_alignment_controller.h"

#include "ash/display/display_alignment_indicator.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/ranges/algorithm.h"
#include "base/timer/timer.h"
#include "ui/display/manager/display_manager.h"
#include "ui/events/event.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/geometry/rect.h"

namespace ash {

namespace {

// Number of times the mouse has to hit the edge to show the indicators.
constexpr int kTriggerThresholdCount = 2;
// Time between last time the mouse leaves a screen edge and the counter
// resetting.
constexpr base::TimeDelta kCounterResetTime = base::Seconds(1);
// How long the indicators are visible for.
constexpr base::TimeDelta kIndicatorVisibilityDuration = base::Seconds(2);

// Returns true if |screen_location| is on the edge of |display|. |display| must
// be valid.
bool IsOnBoundary(const gfx::Point& screen_location,
                  const display::Display& display) {
  DCHECK(display.is_valid());

  const gfx::Rect& bounds = display.bounds();

  const int top = bounds.y();
  const int bottom = bounds.bottom() - 1;
  const int left = bounds.x();
  const int right = bounds.right() - 1;

  // See if current screen_location is within 1px of the display's
  // borders. 1px leniency is necessary as some resolution/size factor
  // combination results in the mouse not being able to reach the edges of the
  // display by 1px.
  if (std::abs(screen_location.x() - left) <= 1)
    return true;
  if (std::abs(screen_location.x() - right) <= 1)
    return true;
  if (std::abs(screen_location.y() - top) <= 1)
    return true;

  return std::abs(screen_location.y() - bottom) <= 1;
}

}  // namespace

DisplayAlignmentController::DisplayAlignmentController()
    : action_trigger_timer_(std::make_unique<base::OneShotTimer>()) {
  Shell* shell = Shell::Get();
  shell->AddPreTargetHandler(this);
  shell->session_controller()->AddObserver(this);
  shell->display_manager()->AddDisplayManagerObserver(this);

  is_locked_ = shell->session_controller()->IsScreenLocked();

  RefreshState();
}

DisplayAlignmentController::~DisplayAlignmentController() {
  Shell* shell = Shell::Get();
  shell->display_manager()->RemoveDisplayManagerObserver(this);
  shell->session_controller()->RemoveObserver(this);
  shell->RemovePreTargetHandler(this);
}

void DisplayAlignmentController::OnDidApplyDisplayChanges() {
  RefreshState();
}

void DisplayAlignmentController::OnDisplaysInitialized() {
  RefreshState();
}

void DisplayAlignmentController::OnMouseEvent(ui::MouseEvent* event) {
  if (current_state_ == DisplayAlignmentState::kDisabled ||
      event->type() != ui::EventType::kMouseMoved) {
    return;
  }

  // If mouse enters the edge of the display.
  const gfx::Point screen_location = event->target()->GetScreenLocation(*event);

  const display::Display& src_display =
      Shell::Get()->display_manager()->FindDisplayContainingPoint(
          screen_location);

  if (!src_display.is_valid())
    return;

  const bool is_on_edge = IsOnBoundary(screen_location, src_display);

  // Restart the reset timer when the mouse moves off an edge.

  if (!is_on_edge) {
    if (current_state_ == DisplayAlignmentState::kOnEdge) {
      current_state_ = DisplayAlignmentState::kIdle;

      // The cursor was moved off the edge. Start the reset timer. If the cursor
      // does not hit an edge on the same display within |kCounterResetTime|,
      // state is reset by ResetState() and indicators will not be shown.
      action_trigger_timer_->Start(
          FROM_HERE, kCounterResetTime,
          base::BindOnce(&DisplayAlignmentController::ResetState,
                         base::Unretained(this)));
    }
    return;
  }

  if (current_state_ != DisplayAlignmentState::kIdle)
    return;

  // |trigger_count_| should only increment when the mouse hits the edges of
  // the same display.
  if (triggered_display_id_ == src_display.id()) {
    trigger_count_++;
  } else {
    triggered_display_id_ = src_display.id();
    trigger_count_ = 1;
  }

  action_trigger_timer_->Stop();
  current_state_ = DisplayAlignmentState::kOnEdge;

  if (trigger_count_ == kTriggerThresholdCount)
    ShowIndicators(src_display);
}

void DisplayAlignmentController::OnLockStateChanged(const bool locked) {
  is_locked_ = locked;
  RefreshState();
}

void DisplayAlignmentController::DisplayDragged(int64_t display_id,
                                                int32_t delta_x,
                                                int32_t delta_y) {
  if (current_state_ != DisplayAlignmentState::kLayoutPreview) {
    // Clear existing indicators. They are all regenerated via
    // OnDidApplyDisplayChanges() after dragging ends.
    ResetState();

    dragged_display_id_ = display_id;
    current_state_ = DisplayAlignmentState::kLayoutPreview;
  }

  // It is not possible to change display being dragged without dropping it
  // first (and causing update on display configuration).
  DCHECK_EQ(dragged_display_id_, display_id);
  DCHECK_NE(dragged_display_id_, display::kInvalidDisplayId);

  dragged_offset_ += gfx::Vector2d(delta_x, delta_y);

  ComputePreviewIndicators();
}

void DisplayAlignmentController::SetTimerForTesting(
    std::unique_ptr<base::OneShotTimer> timer) {
  action_trigger_timer_ = std::move(timer);
}

const std::vector<std::unique_ptr<DisplayAlignmentIndicator>>&
DisplayAlignmentController::GetActiveIndicatorsForTesting() {
  return active_indicators_;
}

int64_t DisplayAlignmentController::GetDraggedDisplayIdForTesting() const {
  return dragged_display_id_;
}

void DisplayAlignmentController::ShowIndicators(
    const display::Display& src_display) {
  DCHECK_EQ(src_display.id(), triggered_display_id_);

  current_state_ = DisplayAlignmentState::kIndicatorsVisible;

  // Iterate through all the active displays and see if they are neighbors to
  // |src_display|.
  display::DisplayManager* display_manager = Shell::Get()->display_manager();
  const display::Displays& display_list =
      display_manager->active_display_list();
  for (const display::Display& peer : display_list) {
    // Skip currently triggered display or it might be detected as a neighbor
    if (peer.id() == triggered_display_id_)
      continue;

    // Check whether |src_display| and |peer| are neighbors.
    gfx::Rect source_edge;
    gfx::Rect peer_edge;
    if (display::ComputeBoundary(src_display, peer, &source_edge, &peer_edge)) {
      // TODO(1070697): Handle pills overlapping for certain display
      // configuration.

      // Pills are created for the indicators in the src display, but not in the
      // peers.
      const std::string& dst_name =
          display_manager->GetDisplayInfo(peer.id()).name();
      active_indicators_.push_back(DisplayAlignmentIndicator::CreateWithPill(
          src_display, source_edge, dst_name));

      active_indicators_.push_back(
          DisplayAlignmentIndicator::Create(peer, peer_edge));
    }
  }

  action_trigger_timer_->Start(
      FROM_HERE, kIndicatorVisibilityDuration,
      base::BindOnce(&DisplayAlignmentController::ResetState,
                     base::Unretained(this)));
}

void DisplayAlignmentController::ResetState() {
  action_trigger_timer_->Stop();
  active_indicators_.clear();
  trigger_count_ = 0;

  dragged_display_id_ = display::kInvalidDisplayId;
  dragged_offset_ = gfx::Vector2d(0, 0);

  // Do not re-enable if disabled.
  if (current_state_ != DisplayAlignmentState::kDisabled)
    current_state_ = DisplayAlignmentState::kIdle;
}

void DisplayAlignmentController::RefreshState() {
  ResetState();

  // This feature is only enabled when the screen is not locked and there is
  // more than one display connected.
  if (is_locked_) {
    current_state_ = DisplayAlignmentState::kDisabled;
    return;
  }

  const display::Displays& display_list =
      Shell::Get()->display_manager()->active_display_list();
  if (display_list.size() < 2) {
    current_state_ = DisplayAlignmentState::kDisabled;
    return;
  }

  if (current_state_ == DisplayAlignmentState::kDisabled)
    current_state_ = DisplayAlignmentState::kIdle;
}

void DisplayAlignmentController::ComputePreviewIndicators() {
  DCHECK_EQ(current_state_, DisplayAlignmentState::kLayoutPreview);
  DCHECK_NE(dragged_display_id_, display::kInvalidDisplayId);

  const display::Display& dragged_display =
      Shell::Get()->display_manager()->GetDisplayForId(dragged_display_id_);
  DCHECK(dragged_display.is_valid());

  gfx::Rect bounds = dragged_display.bounds();
  bounds += dragged_offset_;

  const display::Displays& display_list =
      Shell::Get()->display_manager()->active_display_list();

  // Iterate through all the active displays and see if they are neighbors to
  // |dragged_display|.
  for (const display::Display& peer : display_list) {
    // Skip currently dragged display or it might be detected as a neighbor
    if (peer.id() == dragged_display_id_)
      continue;

    // True if |source| and |peer| are neighbors. Returns |source_edge| and
    // |peer_edge| that denotes shared edges between |source| and |peer|
    // displays.
    gfx::Rect source_edge;
    gfx::Rect peer_edge;
    const bool are_neighbors = display::ComputeBoundary(
        bounds, peer.bounds(), &source_edge, &peer_edge);

    const auto& existing_indicator_it = base::ranges::find(
        active_indicators_, peer.id(), &DisplayAlignmentIndicator::display_id);

    const bool indicator_exists =
        existing_indicator_it != active_indicators_.end();

    if (indicator_exists) {
      if (are_neighbors) {
        // Displays are already neighbors.
        (*existing_indicator_it)->Update(peer, peer_edge);
        (*existing_indicator_it)->Show();
      } else {
        // Displays are no longer neighbors but previously were neighbors.
        (*existing_indicator_it)->Hide();
      }
    } else if (are_neighbors) {
      // Displays are newly-neighbored.
      active_indicators_.push_back(
          DisplayAlignmentIndicator::Create(peer, peer_edge));
    }
  }
}

}  // namespace ash