chromium/ash/fast_ink/laser/laser_pointer_controller.cc

// Copyright 2016 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/fast_ink/laser/laser_pointer_controller.h"

#include <memory>

#include "ash/fast_ink/laser/laser_pointer_view.h"
#include "ash/public/cpp/shell_window_ids.h"
#include "ash/shell.h"
#include "ash/system/palette/palette_utils.h"
#include "base/check.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "ui/display/screen.h"
#include "ui/events/base_event_utils.h"
#include "ui/events/types/event_type.h"
#include "ui/views/widget/widget.h"
#include "ui/wm/core/coordinate_conversion.h"

namespace ash {
namespace {

// A point gets removed from the collection if it is older than
// |kPointLifeDurationMs|.
const int kPointLifeDurationMs = 200;

// When no move events are being received we add a new point every
// |kAddStationaryPointsDelayMs| so that points older than
// |kPointLifeDurationMs| can get removed.
// Note: Using a delay less than the screen refresh interval will not
// provide a visual benefit but instead just waste time performing
// unnecessary updates. 16ms is the refresh interval on most devices.
// TODO(reveman): Use real VSYNC interval instead of a hard-coded delay.
const int kAddStationaryPointsDelayMs = 16;

}  // namespace

// A class to hide and lock mouse cursor while it is alive.
class LaserPointerController::ScopedLockedHiddenCursor {
 public:
  ScopedLockedHiddenCursor() : cursor_manager_(Shell::Get()->cursor_manager()) {
    DCHECK(cursor_manager_);
    // Hide and lock the cursor.
    cursor_manager_->HideCursor();
    cursor_manager_->LockCursor();
  }
  ~ScopedLockedHiddenCursor() {
    // Unlock the cursor.
    cursor_manager_->UnlockCursor();
  }

 private:
  const raw_ptr<wm::CursorManager> cursor_manager_;
};

LaserPointerController::LaserPointerController() {
  Shell::Get()->AddPreTargetHandler(this);
}

LaserPointerController::~LaserPointerController() {
  Shell::Get()->RemovePreTargetHandler(this);
}

void LaserPointerController::AddObserver(LaserPointerObserver* observer) {
  observers_.AddObserver(observer);
}

void LaserPointerController::RemoveObserver(LaserPointerObserver* observer) {
  observers_.RemoveObserver(observer);
}

void LaserPointerController::OnWindowBoundsChanged(
    aura::Window* window,
    const gfx::Rect& old_bounds,
    const gfx::Rect& new_bounds,
    ui::PropertyChangeReason reason) {
  DCHECK_EQ(window, root_window_observation_.GetSource());

  // If the display is rotated or the device scale factor is changed during a
  // gesture, the last point segment will be in a different position on the
  // display. Since the root window bounds are synced with the display, the
  // controller will be notified if the display's rotation or device scale
  // factor changes.
  // Resetting the state of the buffer associated with the
  // `laser_pointer_view_widget_` is non-trivial, so the view is destroyed
  // instead to start with a fresh state. Since it is very rare to have an
  // ongoing gesture with display changes, we can safely assume that there is no
  // performance penalty.
  DestroyPointerView();
}

void LaserPointerController::OnWindowDestroyed(aura::Window* window) {
  DCHECK_EQ(window, root_window_observation_.GetSource());
  root_window_observation_.Reset();
}

void LaserPointerController::SetEnabled(bool enabled) {
  if (enabled == is_enabled())
    return;

  FastInkPointerController::SetEnabled(enabled);
  if (!enabled) {
    DestroyPointerView();
    // Unlock mouse cursor when disabling.
    scoped_locked_hidden_cursor_.reset();
  }
  NotifyStateChanged(enabled);
}

views::View* LaserPointerController::GetPointerView() const {
  return laser_pointer_view_widget_
             ? laser_pointer_view_widget_->GetContentsView()
             : nullptr;
}

void LaserPointerController::CreatePointerView(
    base::TimeDelta presentation_delay,
    aura::Window* root_window) {
  laser_pointer_view_widget_ = LaserPointerView::Create(
      base::Milliseconds(kPointLifeDurationMs), presentation_delay,
      base::Milliseconds(kAddStationaryPointsDelayMs),
      Shell::GetContainer(root_window, kShellWindowId_OverlayContainer));

  DCHECK(!root_window_observation_.IsObserving());
  root_window_observation_.Observe(root_window);
}

void LaserPointerController::UpdatePointerView(ui::TouchEvent* event) {
  LaserPointerView* laser_pointer_view = GetLaserPointerView();

  if (IsPointerInExcludedWindows(event)) {
    // Destroy the |LaserPointerView| since the pointer is in the bound of
    // excluded windows.
    DestroyPointerView();
    return;
  }

  if (event->type() != ui::EventType::kTouchCancelled) {
    // Unlock mouse cursor when switch to touch event.
    scoped_locked_hidden_cursor_.reset();

    laser_pointer_view->AddNewPoint(event->root_location_f(),
                                    event->time_stamp());
  }

  // Upon disabling the touch display, a canceled touch event is received. The
  // `laser_pointer_view` should be destroyed to avoid visual artifacts. See
  // b/303924797 for more information.
  if (event->type() == ui::EventType::kTouchReleased ||
      event->type() == ui::EventType::kTouchCancelled) {
    laser_pointer_view->FadeOut(base::BindOnce(
        &LaserPointerController::DestroyPointerView, base::Unretained(this)));
  }
}

void LaserPointerController::UpdatePointerView(ui::MouseEvent* event) {
  LaserPointerView* laser_pointer_view = GetLaserPointerView();
  if (event->type() == ui::EventType::kMouseMoved) {
    if (IsPointerInExcludedWindows(event)) {
      // Destroy the |LaserPointerView| and unlock the cursor since the cursor
      // is in the bound of excluded windows.
      DestroyPointerView();
      scoped_locked_hidden_cursor_.reset();
      return;
    }

    if (!scoped_locked_hidden_cursor_) {
      scoped_locked_hidden_cursor_ =
          std::make_unique<ScopedLockedHiddenCursor>();
    }
  }

  laser_pointer_view->AddNewPoint(event->root_location_f(),
                                  event->time_stamp());
  if (event->type() == ui::EventType::kMouseReleased) {
    laser_pointer_view->FadeOut(base::BindOnce(
        &LaserPointerController::DestroyPointerView, base::Unretained(this)));
  }
}

void LaserPointerController::DestroyPointerView() {
  root_window_observation_.Reset();
  laser_pointer_view_widget_.reset();
}

bool LaserPointerController::CanStartNewGesture(ui::LocatedEvent* event) {
  // Ignore events over the palette.
  // TODO(llin): Register palette as a excluded window instead.
  aura::Window* target = static_cast<aura::Window*>(event->target());
  gfx::Point screen_point = event->location();
  wm::ConvertPointToScreen(target, &screen_point);
  if (palette_utils::PaletteContainsPointInScreen(screen_point))
    return false;
  return FastInkPointerController::CanStartNewGesture(event);
}

bool LaserPointerController::ShouldProcessEvent(ui::LocatedEvent* event) {
  // Allow clicking when laser pointer is enabled.
  if (event->type() == ui::EventType::kMousePressed ||
      event->type() == ui::EventType::kMouseReleased) {
    return false;
  }

  return FastInkPointerController::ShouldProcessEvent(event);
}

void LaserPointerController::NotifyStateChanged(bool enabled) {
  for (LaserPointerObserver& observer : observers_)
    observer.OnLaserPointerStateChanged(enabled);
}

LaserPointerView* LaserPointerController::GetLaserPointerView() const {
  return static_cast<LaserPointerView*>(GetPointerView());
}

}  // namespace ash