// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/exo/wayland/zcr_ui_controls.h"
#include <stdint.h>
#include <ui-controls-unstable-v1-server-protocol.h>
#include <wayland-server-core.h>
#include <limits>
#include <variant>
#include "ash/display/screen_orientation_controller_test_api.h"
#include "ash/shell.h"
#include "base/bit_cast.h"
#include "base/logging.h"
#include "base/memory/raw_ptr.h"
#include "base/notreached.h"
#include "base/strings/stringprintf.h"
#include "base/test/bind.h"
#include "components/exo/display.h"
#include "components/exo/wayland/server.h"
#include "components/exo/wayland/server_util.h"
#include "ui/aura/env.h"
#include "ui/aura/env_input_state_controller.h"
#include "ui/base/test/ui_controls.h"
#include "ui/base/wayland/wayland_display_util.h"
#include "ui/display/manager/managed_display_info.h"
#include "ui/display/test/display_manager_test_api.h"
#include "ui/display/types/display_constants.h"
#include "ui/events/event_constants.h"
#include "ui/events/gesture_detection/gesture_configuration.h"
#include "ui/events/keycodes/dom/dom_code.h"
#include "ui/events/keycodes/dom/keycode_converter.h"
#include "ui/events/keycodes/keyboard_code_conversion.h"
namespace exo::wayland {
struct UiControls::UiControlsState {
explicit UiControlsState(Server* server, const Seat* seat)
: server_(server), seat_(seat) {}
UiControlsState(const UiControlsState&) = delete;
UiControlsState& operator=(const UiControlsState&) = delete;
raw_ptr<Server> server_;
const raw_ptr<const Seat> seat_;
// Keeps track of the IDs of pending requests for that we still need to emit
// request_processed events. This is per wl_resource so that we can drop
// pending requests for a resource when the resource is destroyed.
std::map<wl_resource*, std::set<uint32_t>> pending_request_ids_;
// Keeps track of the original display spec to be restored on destroy.
std::vector<display::ManagedDisplayInfo> original_displays_;
// Pending display info to be added with display_info_done.
std::optional<display::ManagedDisplayInfo> pending_display_;
// Pending display info lists to be committed with display_info_list_done.
std::vector<display::ManagedDisplayInfo> pending_display_info_list_;
};
namespace {
using UiControlsState = UiControls::UiControlsState;
constexpr uint32_t kUiControlsVersion =
ZCR_UI_CONTROLS_V1_DISPLAY_INFO_LIST_DONE_SINCE_VERSION;
base::OnceClosure UpdateStateAndBindEmitProcessed(struct wl_resource* resource,
uint32_t id) {
auto* state = GetUserDataAs<UiControlsState>(resource);
auto pending_request_ids =
state->pending_request_ids_.try_emplace(resource).first->second;
pending_request_ids.insert(id);
return base::BindLambdaForTesting([=]() {
// Ensure |resource| hasn't been destroyed in the meantime.
if (base::Contains(state->pending_request_ids_, resource)) {
zcr_ui_controls_v1_send_request_processed(resource, id);
state->pending_request_ids_[resource].erase(id);
// It can sometimes happen that the request_processed event gets stuck in
// libwayland's queue without ever being sent, because the client is
// waiting for the event and there is nothing else generating events. To
// ensure the client actually receives the event, we need to flush
// manually.
state->server_->Flush();
}
});
}
// Ensures that a crashed test doesn't leave behind pressed keys, mouse buttons,
// or touch points.
void ResetInputs(UiControlsState* state) {
auto* window = ash::Shell::GetPrimaryRootWindow();
auto pressed_keys = state->seat_->pressed_keys();
for (auto key : pressed_keys) {
const ui::DomCode* physical_key = std::get_if<ui::DomCode>(&key.first);
if (!physical_key) {
continue;
}
auto key_code = ui::DomCodeToUsLayoutNonLocatedKeyboardCode(*physical_key);
ui_controls::SendKeyEvents(window, key_code, ui_controls::kKeyRelease);
}
auto* env = aura::Env::GetInstance();
int button_flags = env->mouse_button_flags();
if (button_flags & ui::EF_LEFT_MOUSE_BUTTON) {
ui_controls::SendMouseEvents(ui_controls::LEFT,
ui_controls::MouseButtonState::UP);
}
if (button_flags & ui::EF_MIDDLE_MOUSE_BUTTON) {
ui_controls::SendMouseEvents(ui_controls::MIDDLE,
ui_controls::MouseButtonState::UP);
}
if (button_flags & ui::EF_RIGHT_MOUSE_BUTTON) {
ui_controls::SendMouseEvents(ui_controls::RIGHT,
ui_controls::MouseButtonState::UP);
}
uint32_t touch_ids_down = env->env_controller()->touch_ids_down();
for (uint32_t touch_id = 0; touch_id < 32; ++touch_id) {
if (touch_ids_down & (1 << touch_id)) {
ui_controls::SendTouchEvents(ui_controls::TouchType::kTouchRelease,
touch_id, 0, 0);
}
}
// TODO(crbug.com/40263572): Fix this issue and the code below should not be
// necessary.
ui_controls::SendMouseMove(0, 0);
}
// Ensure that the display is returned to the default setting.
void ResetDisplay(UiControlsState* state) {
display::test::DisplayManagerTestApi(ash::Shell::Get()->display_manager())
.UpdateDisplayWithDisplayInfoList(state->original_displays_);
ash::ScreenOrientationControllerTestApi(
ash::Shell::Get()->screen_orientation_controller())
.UpdateNaturalOrientation();
state->pending_display_.reset();
state->pending_display_info_list_.clear();
}
void ui_controls_send_key_events(struct wl_client* client,
struct wl_resource* resource,
uint32_t key,
uint32_t key_state,
uint32_t pressed_modifiers,
uint32_t id) {
auto emit_processed = UpdateStateAndBindEmitProcessed(resource, id);
auto* window = ash::Shell::GetPrimaryRootWindow();
auto dom_code = ui::KeycodeConverter::EvdevCodeToDomCode(key);
auto key_code = ui::DomCodeToUsLayoutNonLocatedKeyboardCode(dom_code);
ui_controls::SendKeyEventsNotifyWhenDone(window, key_code, key_state,
std::move(emit_processed),
pressed_modifiers);
}
void ui_controls_send_mouse_move(struct wl_client* client,
struct wl_resource* resource,
int32_t x,
int32_t y,
struct wl_resource* surface,
uint32_t id) {
if (surface) {
LOG(WARNING)
<< "The `surface` parameter for ui_controls.send_mouse_move should be "
"NULL on LaCrOS, but it isn't. Why aren't we using screen "
"coordinates?";
}
auto emit_processed = UpdateStateAndBindEmitProcessed(resource, id);
ui_controls::SendMouseMoveNotifyWhenDone(x, y, std::move(emit_processed));
}
void ui_controls_send_mouse_button(struct wl_client* client,
struct wl_resource* resource,
uint32_t button,
uint32_t button_state,
uint32_t pressed_modifiers,
uint32_t id) {
auto emit_processed = UpdateStateAndBindEmitProcessed(resource, id);
ui_controls::SendMouseEventsNotifyWhenDone(
static_cast<ui_controls::MouseButton>(button), button_state,
std::move(emit_processed), pressed_modifiers);
}
void ui_controls_send_touch(struct wl_client* client,
struct wl_resource* resource,
uint32_t action,
uint32_t touch_id,
int32_t x,
int32_t y,
struct wl_resource* surface,
uint32_t id) {
if (surface) {
LOG(WARNING)
<< "The `surface` parameter for ui_controls.send_touch should be NULL "
"on LaCrOS, but it isn't. Why aren't we using screen coordinates?";
}
auto emit_processed = UpdateStateAndBindEmitProcessed(resource, id);
ui_controls::SendTouchEventsNotifyWhenDone(action, touch_id, x, y,
std::move(emit_processed));
}
void ui_controls_set_display_info_id(struct wl_client* client,
struct wl_resource* resource,
uint32_t display_id_hi,
uint32_t display_id_lo) {
auto* state = GetUserDataAs<UiControlsState>(resource);
int64_t display_id =
ui::wayland::FromWaylandDisplayIdPair({display_id_hi, display_id_lo});
if (!state->pending_display_) {
state->pending_display_ =
display::ManagedDisplayInfo::CreateFromSpecWithID({}, display_id);
} else {
state->pending_display_->set_display_id(display_id);
}
}
void ui_controls_set_display_info_size(struct wl_client* client,
struct wl_resource* resource,
uint32_t width,
uint32_t height) {
auto* state = GetUserDataAs<UiControlsState>(resource);
if (!state->pending_display_) {
state->pending_display_ = display::ManagedDisplayInfo::CreateFromSpec(
base::StringPrintf("%dx%d", width, height));
} else {
state->pending_display_->SetBounds(gfx::Rect(width, height));
}
}
void ui_controls_set_display_info_device_scale_factor(
struct wl_client* client,
struct wl_resource* resource,
uint32_t scale_factor) {
auto* state = GetUserDataAs<UiControlsState>(resource);
static_assert(sizeof(uint32_t) == sizeof(float),
"Sizes much match for reinterpret cast to be meaningful");
// bit_cast is needed here because wayland doesn't support
// float as primitive type and we are using 32 bits as storage.
// static_cast won't work because the original value is integer.
float device_scale_factor = base::bit_cast<float>(scale_factor);
if (!state->pending_display_) {
state->pending_display_ = display::ManagedDisplayInfo::CreateFromSpec({});
}
state->pending_display_->set_device_scale_factor(device_scale_factor);
}
void ui_controls_display_info_done(struct wl_client* client,
struct wl_resource* resource) {
auto* state = GetUserDataAs<UiControlsState>(resource);
// Push info to pending display info list.
state->pending_display_info_list_.push_back(*state->pending_display_);
// Reset the state to default values.
state->pending_display_.reset();
}
void ui_controls_display_info_list_done(struct wl_client* client,
struct wl_resource* resource,
uint32_t id) {
auto emit_processed = UpdateStateAndBindEmitProcessed(resource, id);
auto* state = GetUserDataAs<UiControlsState>(resource);
display::test::DisplayManagerTestApi(ash::Shell::Get()->display_manager())
.UpdateDisplayWithDisplayInfoList(state->pending_display_info_list_);
ash::ScreenOrientationControllerTestApi(
ash::Shell::Get()->screen_orientation_controller())
.UpdateNaturalOrientation();
state->pending_display_info_list_.clear();
state->pending_display_.reset();
std::move(emit_processed).Run();
}
const struct zcr_ui_controls_v1_interface ui_controls_implementation = {
ui_controls_send_key_events,
ui_controls_send_mouse_move,
ui_controls_send_mouse_button,
ui_controls_send_touch,
ui_controls_set_display_info_id,
ui_controls_set_display_info_size,
ui_controls_set_display_info_device_scale_factor,
ui_controls_display_info_done,
ui_controls_display_info_list_done};
void destroy_ui_controls_resource(struct wl_resource* resource) {
auto* state = GetUserDataAs<UiControlsState>(resource);
state->pending_request_ids_.erase(resource);
ResetInputs(state);
ResetDisplay(state);
}
void bind_ui_controls(wl_client* client,
void* data,
uint32_t version,
uint32_t id) {
wl_resource* resource =
wl_resource_create(client, &zcr_ui_controls_v1_interface,
std::min(version, kUiControlsVersion), id);
wl_resource_set_implementation(resource, &ui_controls_implementation, data,
destroy_ui_controls_resource);
}
} // namespace
UiControls::UiControls(Server* server)
: state_(std::make_unique<UiControlsState>(server,
server->GetDisplay()->seat())) {
wl_global_create(server->GetWaylandDisplay(), &zcr_ui_controls_v1_interface,
kUiControlsVersion, state_.get(), bind_ui_controls);
auto* const display_manager = ash::Shell::Get()->display_manager();
auto& display_list = display_manager->active_display_list();
for (const display::Display& display : display_list) {
state_->original_displays_.push_back(
display_manager->GetDisplayInfo(display.id()));
}
// TODO(crbug.com/324562919) This hardcodes fling gesture detection to be
// disabled when ui_controls is in use, so that it does not interfere with
// tests that don't intend to trigger fling gestures. Some future tests will
// intentionally trigger fling gestures, so this will need to become
// configurable by clients at some point.
ui::GestureConfiguration::GetInstance()->set_min_fling_velocity(
std::numeric_limits<float>::max());
}
UiControls::~UiControls() = default;
} // namespace exo::wayland