// SPDX-License-Identifier: GPL-2.0-only #include #include #include #include #include #include #include #include "common/macros.h" #include "common/mem.h" #include "common/scene-helpers.h" #include "config/rcxml.h" #include "config/mousebind.h" #include "input/cursor.h" #include "input/tablet.h" #include "input/tablet-tool.h" #include "labwc.h" #include "idle.h" #include "action.h" static bool tool_supports_absolute_motion(struct wlr_tablet_tool *tool) { switch (tool->type) { case WLR_TABLET_TOOL_TYPE_MOUSE: case WLR_TABLET_TOOL_TYPE_LENS: return false; default: return true; } } static void adjust_for_tablet_area(double tablet_width, double tablet_height, struct wlr_fbox box, double *x, double *y) { if ((!box.x && !box.y && !box.width && !box.height) || !tablet_width || !tablet_height) { return; } if (!box.width) { box.width = tablet_width - box.x; } if (!box.height) { box.height = tablet_height - box.y; } if (box.x + box.width <= tablet_width) { const double max_x = 1; double width_offset = max_x * box.x / tablet_width; *x = (*x - width_offset) * tablet_width / box.width; } if (box.y + box.height <= tablet_height) { const double max_y = 1; double height_offset = max_y * box.y / tablet_height; *y = (*y - height_offset) * tablet_height / box.height; } } static void adjust_for_rotation(enum rotation rotation, double *x, double *y) { double tmp; switch (rotation) { case LAB_ROTATE_NONE: break; case LAB_ROTATE_90: tmp = *x; *x = 1.0 - *y; *y = tmp; break; case LAB_ROTATE_180: *x = 1.0 - *x; *y = 1.0 - *y; break; case LAB_ROTATE_270: tmp = *x; *x = *y; *y = 1.0 - tmp; break; } } static struct wlr_surface* tablet_get_coords(struct drawing_tablet *tablet, double *x, double *y) { *x = tablet->x; *y = tablet->y; adjust_for_tablet_area(tablet->tablet->width_mm, tablet->tablet->height_mm, rc.tablet.box, x, y); adjust_for_rotation(rc.tablet.rotation, x, y); if (rc.tablet.force_mouse_emulation || !tablet->tablet_v2) { return NULL; } /* Convert coordinates: first [0, 1] => layout, then layout => surface */ double lx, ly; wlr_cursor_absolute_to_layout_coords(tablet->seat->cursor, tablet->wlr_input_device, *x, *y, &lx, &ly); double sx, sy; struct wlr_scene_node *node = wlr_scene_node_at(&tablet->seat->server->scene->tree.node, lx, ly, &sx, &sy); /* Find the surface and return it if it accepts tablet events */ struct wlr_surface *surface = lab_wlr_surface_from_node(node); if (surface && !wlr_surface_accepts_tablet_v2(tablet->tablet_v2, surface)) { return NULL; } return surface; } static void notify_motion(struct drawing_tablet *tablet, struct drawing_tablet_tool *tool, struct wlr_surface *surface, double x, double y, uint32_t time) { idle_manager_notify_activity(tool->seat->seat); if (surface != tool->tool_v2->focused_surface) { wlr_tablet_v2_tablet_tool_notify_proximity_in(tool->tool_v2, tablet->tablet_v2, surface); } wlr_cursor_warp_absolute(tablet->seat->cursor, tablet->wlr_input_device, x, y); double sx, sy; bool notify = cursor_process_motion(tablet->seat->server, time, &sx, &sy); if (notify) { wlr_tablet_v2_tablet_tool_notify_motion(tool->tool_v2, sx, sy); } } static void handle_proximity(struct wl_listener *listener, void *data) { struct wlr_tablet_tool_proximity_event *ev = data; struct drawing_tablet *tablet = ev->tablet->data; struct drawing_tablet_tool *tool = ev->tool->data; if (!tool_supports_absolute_motion(ev->tool)) { if (ev->state == WLR_TABLET_TOOL_PROXIMITY_IN) { wlr_log(WLR_INFO, "ignoring not supporting tablet tool"); } return; } tablet->x = ev->x; tablet->y = ev->y; double x, y; struct wlr_surface *surface = tablet_get_coords(tablet, &x, &y); if (!rc.tablet.force_mouse_emulation && tablet->seat->server->tablet_manager && !tool) { /* * Unfortunately `wlr_tool` is only present in the events, so * use proximity for creating a `wlr_tablet_v2_tablet_tool`. */ tablet_tool_init(tablet->seat, ev->tool); } if (tool && surface) { if (tool->tool_v2 && ev->state == WLR_TABLET_TOOL_PROXIMITY_IN) { notify_motion(tablet, tool, surface, x, y, ev->time_msec); } if (tool->tool_v2 && ev->state == WLR_TABLET_TOOL_PROXIMITY_OUT) { wlr_tablet_v2_tablet_tool_notify_proximity_out(tool->tool_v2); } } } static void handle_axis(struct wl_listener *listener, void *data) { struct wlr_tablet_tool_axis_event *ev = data; struct drawing_tablet *tablet = ev->tablet->data; struct drawing_tablet_tool *tool = ev->tool->data; if (!tool_supports_absolute_motion(ev->tool)) { return; } if (ev->updated_axes & WLR_TABLET_TOOL_AXIS_X) { tablet->x = ev->x; } if (ev->updated_axes & WLR_TABLET_TOOL_AXIS_Y) { tablet->y = ev->y; } if (ev->updated_axes & WLR_TABLET_TOOL_AXIS_TILT_X) { tablet->tilt_x = ev->tilt_x; } if (ev->updated_axes & WLR_TABLET_TOOL_AXIS_TILT_Y) { tablet->tilt_y = ev->tilt_y; } double x, y; struct wlr_surface *surface = tablet_get_coords(tablet, &x, &y); if (tool && ((surface && tablet->seat->server->input_mode == LAB_INPUT_STATE_PASSTHROUGH) || wlr_tablet_tool_v2_has_implicit_grab(tool->tool_v2))) { if (ev->updated_axes & (WLR_TABLET_TOOL_AXIS_X | WLR_TABLET_TOOL_AXIS_Y)) { notify_motion(tablet, tool, surface, x, y, ev->time_msec); } if (ev->updated_axes & WLR_TABLET_TOOL_AXIS_DISTANCE) { wlr_tablet_v2_tablet_tool_notify_distance(tool->tool_v2, ev->distance); } if (ev->updated_axes & WLR_TABLET_TOOL_AXIS_PRESSURE) { wlr_tablet_v2_tablet_tool_notify_pressure(tool->tool_v2, ev->pressure); } if (ev->updated_axes & (WLR_TABLET_TOOL_AXIS_TILT_X | WLR_TABLET_TOOL_AXIS_TILT_Y)) { /* * From https://gitlab.freedesktop.org/wayland/wayland-protocols/-/blob/main/stable/tablet/tablet-v2.xml * "Other extra axes are in physical units as specified in the protocol. * The current extra axes with physical units are tilt, rotation and * wheel rotation. * Sent whenever one or both of the tilt axes on a tool change. Each tilt * value is in degrees, relative to the z-axis of the tablet. * The angle is positive when the top of a tool tilts along the * positive x or y axis." * Based on that we only need to apply rotation but no area transformation. */ double tilt_x = tablet->tilt_x; double tilt_y = tablet->tilt_y; adjust_for_rotation(rc.tablet.rotation, &tilt_x, &tilt_y); wlr_tablet_v2_tablet_tool_notify_tilt(tool->tool_v2, tilt_x, tilt_y); } if (ev->updated_axes & WLR_TABLET_TOOL_AXIS_ROTATION) { wlr_tablet_v2_tablet_tool_notify_rotation(tool->tool_v2, ev->rotation); } if (ev->updated_axes & WLR_TABLET_TOOL_AXIS_SLIDER) { wlr_tablet_v2_tablet_tool_notify_slider(tool->tool_v2, ev->slider); } if (ev->updated_axes & WLR_TABLET_TOOL_AXIS_WHEEL) { wlr_tablet_v2_tablet_tool_notify_wheel(tool->tool_v2, ev->wheel_delta, 0); } } else { if (ev->updated_axes & (WLR_TABLET_TOOL_AXIS_X | WLR_TABLET_TOOL_AXIS_Y)) { if (tool && tool->tool_v2->focused_surface) { wlr_tablet_v2_tablet_tool_notify_proximity_out( tool->tool_v2); } cursor_emulate_move_absolute(tablet->seat, &ev->tablet->base, x, y, ev->time_msec); } } } static uint32_t to_stylus_button(uint32_t button) { /* * Use the mapping that is used by XWayland and GTK, even * if it isn't the order one would expect. This is also * consistent with the mapping for mouse emulation */ switch (button) { case BTN_LEFT: return BTN_TOOL_PEN; case BTN_RIGHT: return BTN_STYLUS2; case BTN_MIDDLE: return BTN_STYLUS; case BTN_SIDE: return BTN_STYLUS3; default: return 0; } } static void handle_tip(struct wl_listener *listener, void *data) { struct wlr_tablet_tool_tip_event *ev = data; struct drawing_tablet *tablet = ev->tablet->data; struct drawing_tablet_tool *tool = ev->tool->data; double x, y; struct wlr_surface *surface = tablet_get_coords(tablet, &x, &y); uint32_t button = tablet_get_mapped_button(BTN_TOOL_PEN); if (tool && (surface || wlr_tablet_tool_v2_has_implicit_grab(tool->tool_v2))) { idle_manager_notify_activity(tool->seat->seat); uint32_t stylus_button = to_stylus_button(button); if (stylus_button != BTN_TOOL_PEN) { wlr_log(WLR_INFO, "ignoring stylus tool pen mapping for tablet mode"); } if (ev->state == WLR_TABLET_TOOL_TIP_DOWN) { bool notify = cursor_process_button_press(tool->seat, BTN_LEFT, ev->time_msec); if (notify) { wlr_tablet_v2_tablet_tool_notify_down(tool->tool_v2); wlr_tablet_tool_v2_start_implicit_grab(tool->tool_v2); } } else if (ev->state == WLR_TABLET_TOOL_TIP_UP) { bool notify = cursor_process_button_release(tool->seat, BTN_LEFT, ev->time_msec); if (notify) { wlr_tablet_v2_tablet_tool_notify_up(tool->tool_v2); } bool exit_interactive = cursor_finish_button_release(tool->seat); if (exit_interactive && tool->tool_v2->focused_surface) { /* * Re-enter the surface after a resize/move to ensure * being back in tablet mode. */ wlr_tablet_v2_tablet_tool_notify_proximity_out(tool->tool_v2); wlr_tablet_v2_tablet_tool_notify_proximity_in(tool->tool_v2, tablet->tablet_v2, surface); } if (!surface) { /* Out-of-surface movement ended on the desktop */ wlr_tablet_v2_tablet_tool_notify_proximity_out(tool->tool_v2); } } } else { if (button) { cursor_emulate_button(tablet->seat, button, ev->state == WLR_TABLET_TOOL_TIP_DOWN ? WLR_BUTTON_PRESSED : WLR_BUTTON_RELEASED, ev->time_msec); } } } static void handle_button(struct wl_listener *listener, void *data) { struct wlr_tablet_tool_button_event *ev = data; struct drawing_tablet *tablet = ev->tablet->data; struct drawing_tablet_tool *tool = ev->tool->data; double x, y; struct wlr_surface *surface = tablet_get_coords(tablet, &x, &y); uint32_t button = tablet_get_mapped_button(ev->button); if (tool && surface) { idle_manager_notify_activity(tool->seat->seat); if (button) { struct view *view = view_from_wlr_surface(surface); struct mousebind *mousebind; wl_list_for_each(mousebind, &rc.mousebinds, link) { if (mousebind->mouse_event == MOUSE_ACTION_PRESS && mousebind->button == button && mousebind->context == LAB_SSD_CLIENT) { actions_run(view, tool->seat->server, &mousebind->actions, 0); } } } uint32_t stylus_button = to_stylus_button(button); if (stylus_button && stylus_button != BTN_TOOL_PEN) { wlr_tablet_v2_tablet_tool_notify_button(tool->tool_v2, stylus_button, ev->state == WLR_BUTTON_PRESSED ? ZWP_TABLET_PAD_V2_BUTTON_STATE_PRESSED : ZWP_TABLET_PAD_V2_BUTTON_STATE_RELEASED); } else { wlr_log(WLR_INFO, "invalid stylus button mapping for tablet mode"); } } else { if (button) { cursor_emulate_button(tablet->seat, button, ev->state, ev->time_msec); } } } static void handle_destroy(struct wl_listener *listener, void *data) { struct drawing_tablet *tablet = wl_container_of(listener, tablet, handlers.destroy); wl_list_remove(&tablet->handlers.tip.link); wl_list_remove(&tablet->handlers.button.link); wl_list_remove(&tablet->handlers.proximity.link); wl_list_remove(&tablet->handlers.axis.link); wl_list_remove(&tablet->handlers.destroy.link); free(tablet); } void tablet_init(struct seat *seat, struct wlr_input_device *wlr_device) { wlr_log(WLR_DEBUG, "setting up tablet"); struct drawing_tablet *tablet = znew(*tablet); tablet->seat = seat; tablet->wlr_input_device = wlr_device; tablet->tablet = wlr_tablet_from_input_device(wlr_device); tablet->tablet->data = tablet; if (seat->server->tablet_manager) { tablet->tablet_v2 = wlr_tablet_create( seat->server->tablet_manager, seat->seat, wlr_device); } tablet->x = 0.0; tablet->y = 0.0; tablet->tilt_x = 0.0; tablet->tilt_y = 0.0; wlr_log(WLR_INFO, "tablet dimensions: %.2fmm x %.2fmm", tablet->tablet->width_mm, tablet->tablet->height_mm); CONNECT_SIGNAL(tablet->tablet, &tablet->handlers, axis); CONNECT_SIGNAL(tablet->tablet, &tablet->handlers, proximity); CONNECT_SIGNAL(tablet->tablet, &tablet->handlers, tip); CONNECT_SIGNAL(tablet->tablet, &tablet->handlers, button); CONNECT_SIGNAL(wlr_device, &tablet->handlers, destroy); }