chromium/ui/events/ozone/evdev/libinput_event_converter.cc

// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ui/events/ozone/evdev/libinput_event_converter.h"

#include <fcntl.h>

#include <ostream>
#include <string>

#include "base/logging.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "ui/events/ozone/evdev/cursor_delegate_evdev.h"
#include "ui/events/ozone/evdev/device_event_dispatcher_evdev.h"
#include "ui/events/ozone/evdev/event_device_info.h"
#include "ui/events/ozone/evdev/input_device_settings_evdev.h"

namespace ui {

namespace {

double GetAxisValue(libinput_event_pointer* const event,
                    const libinput_pointer_axis axis) {
  // Check has_axis before get_axis_value to avoid spamming the logs
  // with libinput errors
  if (!libinput_event_pointer_has_axis(event, axis)) {
    return 0.0;
  }

  // Libinput's scroll axes are reversed compared to Chromium. For
  // example, libinput produces positive deltas when scrolling down
  // (with natural scrolling off), but Chromium's event system
  // produces negative deltas. Simple fix: negate the axis value.
  return -libinput_event_pointer_get_axis_value(event, axis);
}

// Log the result of setting a device configuration value.
template <typename Value>
void LogConfigStatus(const libinput_config_status status,
                     const char* key,
                     const Value value) {
  switch (status) {
    case LIBINPUT_CONFIG_STATUS_SUCCESS:
      DVLOG(3) << "libinput config: set " << key << " to " << value;
      break;

    case LIBINPUT_CONFIG_STATUS_UNSUPPORTED:
      LOG(ERROR) << "libinput config: " << key << " not supported";
      break;

    case LIBINPUT_CONFIG_STATUS_INVALID:
      LOG(ERROR) << "libinput config: invalid parameter for " << key << ": "
                 << value;
      break;
  }
}

}  // namespace

LibInputEventConverter::LibInputEvent::LibInputEvent(LibInputEvent&& other)
    : event_(other.event_) {
  other.event_ = nullptr;
}

LibInputEventConverter::LibInputEvent::LibInputEvent(
    libinput_event* const event)
    : event_(event) {}

LibInputEventConverter::LibInputEvent::~LibInputEvent() {
  if (event_) {
    libinput_event_destroy(event_);
  }
}

libinput_event_pointer* LibInputEventConverter::LibInputEvent::PointerEvent()
    const {
  auto* const event = libinput_event_get_pointer_event(event_);
  DCHECK(event);
  return event;
}

libinput_event_type LibInputEventConverter::LibInputEvent::Type() const {
  return libinput_event_get_type(event_);
}

LibInputEventConverter::LibInputDevice::LibInputDevice(
    int id,
    libinput_device* const device)
    : device_id_(id), device_(device) {
  DCHECK(device);

  // |libinput_path_add_device| returns a device pointer that is is
  // derefed at the next call to |libinput_dispatch|. Add a ref here
  // so that we can keep using the pointer.
  libinput_device_ref(device);

  DVLOG(2) << "device: \"" << libinput_device_get_name(device_)
           << "\", capabilities: " << GetCapabilitiesString();
}

LibInputEventConverter::LibInputDevice::LibInputDevice(LibInputDevice&& other)
    : device_id_(other.device_id_), device_(other.device_) {
  other.device_ = nullptr;
}

LibInputEventConverter::LibInputDevice::~LibInputDevice() {
  if (device_) {
    libinput_device_unref(device_);
  }
}

void LibInputEventConverter::LibInputDevice::ApplySettings(
    const InputDeviceSettingsEvdev& settings) const {
  const auto& touchpad_settings = settings.GetTouchpadSettings(device_id_);
  SetNaturalScrollEnabled(touchpad_settings.natural_scroll_enabled);
  SetSensitivity(touchpad_settings.sensitivity);
  SetTapToClickEnabled(touchpad_settings.tap_to_click_enabled);
}

// Get a comma-separated string of the device's capabilities
std::string LibInputEventConverter::LibInputDevice::GetCapabilitiesString() {
  // All capabilities exposed by libinput
  std::vector<std::pair<libinput_device_capability, std::string>> caps = {
      {LIBINPUT_DEVICE_CAP_KEYBOARD, "keyboard"},
      {LIBINPUT_DEVICE_CAP_POINTER, "pointer"},
      {LIBINPUT_DEVICE_CAP_TOUCH, "touch"},
      {LIBINPUT_DEVICE_CAP_TABLET_TOOL, "tablet-tool"},
      {LIBINPUT_DEVICE_CAP_TABLET_PAD, "tablet-pad"},
      {LIBINPUT_DEVICE_CAP_GESTURE, "gesture"},
      {LIBINPUT_DEVICE_CAP_SWITCH, "switch"}};

  std::vector<std::string> parts(caps.size());
  for (const auto& pair : caps) {
    if (libinput_device_has_capability(device_, pair.first)) {
      parts.emplace_back(pair.second);
    }
  }

  return base::JoinString(parts, ", ");
}

void LibInputEventConverter::LibInputDevice::SetNaturalScrollEnabled(
    const bool enabled) const {
  const auto status = libinput_device_config_scroll_set_natural_scroll_enabled(
      device_, enabled);
  LogConfigStatus(status, "natural-scroll", enabled);
}

void LibInputEventConverter::LibInputDevice::SetSensitivity(
    const int sensitivity) const {
  // The range of |sensitivity| is [1..5] according to comments in
  // libgestures. Rescale to floating point [-1, 1].
  const double speed = (sensitivity - 3.0) / 2.0;
  const auto status = libinput_device_config_accel_set_speed(device_, speed);
  LogConfigStatus(status, "sensitivity", speed);
}

void LibInputEventConverter::LibInputDevice::SetTapToClickEnabled(
    const bool enabled) const {
  const auto arg =
      (enabled ? LIBINPUT_CONFIG_TAP_ENABLED : LIBINPUT_CONFIG_TAP_DISABLED);

  const auto status = libinput_device_config_tap_set_enabled(device_, arg);
  LogConfigStatus(status, "tap-to-click", arg);
}

LibInputEventConverter::LibInputContext::LibInputContext(
    LibInputContext&& other)
    : li_(other.li_) {
  other.li_ = nullptr;
}

LibInputEventConverter::LibInputContext::LibInputContext(libinput* const li)
    : li_(li) {
  DCHECK(li_);
  libinput_log_set_handler(li_, LogHandler);
  libinput_log_set_priority(li_, LIBINPUT_LOG_PRIORITY_DEBUG);
}

LibInputEventConverter::LibInputContext::~LibInputContext() {
  if (li_) {
    libinput_unref(li_);
  }
}

std::optional<LibInputEventConverter::LibInputContext>
LibInputEventConverter::LibInputContext::Create() {
  libinput* const li = libinput_path_create_context(&interface_, nullptr);
  if (!li) {
    LOG(ERROR) << "libinput_path_create_context failed";
    return std::nullopt;
  }

  return std::make_optional(LibInputEventConverter::LibInputContext(li));
}

std::optional<LibInputEventConverter::LibInputDevice>
LibInputEventConverter::LibInputContext::AddDevice(
    int id,
    const base::FilePath& path) const {
  auto* const dev = libinput_path_add_device(li_, path.value().c_str());
  if (!dev) {
    LOG(ERROR) << "libinput_path_add_device failed with device: " << path;
    return std::nullopt;
  }

  return std::make_optional(LibInputDevice(id, dev));
}

bool LibInputEventConverter::LibInputContext::Dispatch() const {
  const auto rc = libinput_dispatch(li_);
  if (rc != 0) {
    LOG(ERROR) << "libinput_dispatch failed: " << rc;
    return false;
  }
  return true;
}

int LibInputEventConverter::LibInputContext::Fd() {
  return libinput_get_fd(li_);
}

std::optional<LibInputEventConverter::LibInputEvent>
LibInputEventConverter::LibInputContext::NextEvent() const {
  libinput_event* const event = libinput_get_event(li_);
  if (!event) {
    return std::nullopt;
  }
  return std::make_optional(LibInputEvent(event));
}

int LibInputEventConverter::LibInputContext::OpenRestricted(const char* path,
                                                            int flags,
                                                            void* user_data) {
  VLOG(1) << "Open input: " << path;
  int fd = open(path, flags);
  return fd;
}

void LibInputEventConverter::LibInputContext::CloseRestricted(int fd,
                                                              void* user_data) {
  close(fd);
}

void LibInputEventConverter::LibInputContext::LogHandler(
    libinput* libinput,
    enum libinput_log_priority priority,
    const char* format,
    va_list args) {
  switch (priority) {
    case LIBINPUT_LOG_PRIORITY_DEBUG:
      VLOG(4) << "libinput: " << base::StringPrintV(format, args);
      break;

    case LIBINPUT_LOG_PRIORITY_INFO:
      VLOG(1) << "libinput: " << base::StringPrintV(format, args);
      break;

    case LIBINPUT_LOG_PRIORITY_ERROR:
      LOG(ERROR) << "libinput: " << base::StringPrintV(format, args);
      break;
  }
}

constexpr libinput_interface
    LibInputEventConverter::LibInputContext::interface_;

std::unique_ptr<LibInputEventConverter> LibInputEventConverter::Create(
    const base::FilePath& path,
    int id,
    const EventDeviceInfo& devinfo,
    CursorDelegateEvdev* cursor,
    DeviceEventDispatcherEvdev* dispatcher) {
  auto context = LibInputContext::Create();
  if (!context) {
    LOG(ERROR) << "LibInputContext::Create failed";
    return nullptr;
  }

  return std::make_unique<LibInputEventConverter>(
      std::move(context.value()), path, id, devinfo, cursor, dispatcher);
}

LibInputEventConverter::LibInputEventConverter(
    LibInputEventConverter::LibInputContext&& ctx,
    const base::FilePath& path,
    int id,
    const EventDeviceInfo& devinfo,
    CursorDelegateEvdev* cursor,
    DeviceEventDispatcherEvdev* dispatcher)
    : EventConverterEvdev(ctx.Fd(),
                          path,
                          id,
                          devinfo.device_type(),
                          devinfo.name(),
                          devinfo.phys(),
                          devinfo.vendor_id(),
                          devinfo.product_id(),
                          devinfo.version()),
      dispatcher_(dispatcher),
      cursor_(cursor),
      has_keyboard_(devinfo.HasKeyboard()),
      has_mouse_(devinfo.HasMouse()),
      has_touchpad_(devinfo.HasTouchpad()),
      has_touchscreen_(devinfo.HasTouchscreen()),
      context_(std::move(ctx)),
      device_(context_.AddDevice(id, path)) {}

LibInputEventConverter::~LibInputEventConverter() {}

void LibInputEventConverter::ApplyDeviceSettings(
    const InputDeviceSettingsEvdev& settings) {
  if (device_) {
    device_->ApplySettings(settings);
  } else {
    LOG(ERROR)
        << "Unable to apply settings due to libinput_path_add_device failure";
  }
}

bool LibInputEventConverter::HasKeyboard() const {
  return has_keyboard_;
}

bool LibInputEventConverter::HasMouse() const {
  return has_mouse_;
}

bool LibInputEventConverter::HasTouchpad() const {
  return has_touchpad_;
}

bool LibInputEventConverter::HasTouchscreen() const {
  return has_touchscreen_;
}

void LibInputEventConverter::OnFileCanReadWithoutBlocking(int fd) {
  if (!context_.Dispatch()) {
    LOG(ERROR) << "LibInputContext::Dispatch failed";
    return;
  }

  while (auto event = context_.NextEvent()) {
    HandleEvent(*event);
  }
}

void LibInputEventConverter::HandleEvent(const LibInputEvent& event) {
  switch (event.Type()) {
    case LIBINPUT_EVENT_POINTER_MOTION:
      HandlePointerMotion(event);
      break;

    case LIBINPUT_EVENT_POINTER_BUTTON:
      HandlePointerButton(event);
      break;

    case LIBINPUT_EVENT_POINTER_AXIS:
      HandlePointerAxis(event);
      break;

    default:
      DVLOG(3) << "Ignoring libinput event: " << event.Type();
      break;
  }
}

void LibInputEventConverter::HandlePointerMotion(const LibInputEvent& evt) {
  libinput_event_pointer* event = evt.PointerEvent();
  const int flags = EF_NONE;
  const double dx = libinput_event_pointer_get_dx(event);
  const double dy = libinput_event_pointer_get_dy(event);

  cursor_->MoveCursor(gfx::Vector2dF(dx, dy));

  DVLOG(3) << "Pointer motion: dx=" << dx << ", dy=" << dy;

  dispatcher_->DispatchMouseMoveEvent(
      {input_device_.id, flags, cursor_->GetLocation(), nullptr /*delta*/,
       PointerDetails(EventPointerType::kMouse), Timestamp(evt)});
}

void LibInputEventConverter::HandlePointerButton(const LibInputEvent& evt) {
  libinput_event_pointer* event = evt.PointerEvent();
  const int flags = EF_NONE;
  const uint32_t button = libinput_event_pointer_get_button(event);
  const bool down = libinput_event_pointer_get_button_state(event) ==
                    LIBINPUT_BUTTON_STATE_PRESSED;

  // allow_remap: Controls whether or not remapping buttons is allowed; in
  // this case whether or not it should respect the "Swap Left/Right Mouse
  // Button" option in the ChromeOS settings.
  //
  // Since we only deal with touchpad buttons here, this should be false,
  // but if we expand this to handle mice in the future we may need to
  // have more intricate logic.
  const MouseButtonMapType allow_remap = MouseButtonMapType::kNone;

  DVLOG(3) << "Button: " << button << ", pressed: " << down;

  dispatcher_->DispatchMouseButtonEvent(
      {input_device_.id, flags, cursor_->GetLocation(), button, down,
       allow_remap, PointerDetails(EventPointerType::kMouse), Timestamp(evt)});
}

void LibInputEventConverter::HandlePointerAxis(const LibInputEvent& evt) {
  libinput_event_pointer* event = evt.PointerEvent();
  const auto h = GetAxisValue(event, LIBINPUT_POINTER_AXIS_SCROLL_HORIZONTAL);
  const auto v = GetAxisValue(event, LIBINPUT_POINTER_AXIS_SCROLL_VERTICAL);
  const gfx::Vector2d delta(h, v);

  DVLOG(3) << "Pointer axis h:" << h << ", v:" << v;

  dispatcher_->DispatchScrollEvent({input_device_.id, EventType::kScroll,
                                    cursor_->GetLocation(), delta, delta, 2,
                                    Timestamp(evt)});
}

base::TimeTicks LibInputEventConverter::Timestamp(const LibInputEvent& evt) {
  libinput_event_pointer* event = evt.PointerEvent();
  uint64_t time_usec = libinput_event_pointer_get_time_usec(event);
  return base::TimeTicks() + base::Microseconds(time_usec);
}

std::ostream& LibInputEventConverter::DescribeForLog(std::ostream& os) const {
  os << "class=ui::LibInputEventConverter id=" << input_device_.id << std::endl
     << " has_keyboard=" << has_keyboard_ << std::endl
     << " has_mouse=" << has_mouse_ << std::endl
     << " has_touchpad=" << has_touchpad_ << std::endl
     << " has_touchscreen=" << has_touchscreen_ << std::endl
     << "base ";
  return EventConverterEvdev::DescribeForLog(os);
}

}  // namespace ui