ext-action-binder-v1: new protocol implementation

Signed-off-by: Anna (navi) Figueiredo Gomes <navi@vlhl.dev>
This commit is contained in:
Anna (navi) Figueiredo Gomes 2023-10-09 22:12:46 +01:00
parent 7debaced03
commit 1495db3e16
5 changed files with 711 additions and 0 deletions

View file

@ -0,0 +1,82 @@
/*
* This an unstable interface of wlroots. No guarantees are made regarding the
* future consistency of this API.
*/
#ifndef WLR_USE_UNSTABLE
#error "Add -DWLR_USE_UNSTABLE to enable unstable wlroots features"
#endif
#ifndef WLR_TYPES_WLR_ACTION_BINDER_V1_H
#define WLR_TYPES_WLR_ACTION_BINDER_V1_H
#include <wayland-server-core.h>
#include <wayland-server-protocol.h>
#include "ext-action-binder-v1-protocol.h"
struct wlr_action_binder_v1 {
struct wl_global *global;
struct wl_list states; // wlr_action_binder_v1_state.link
struct wl_listener display_destroy;
struct {
struct wl_signal bind;
struct wl_signal destroy;
} events;
void *data;
};
struct wlr_action_binder_v1_state {
struct wl_list binds; // wlr_action_binding_v1.link
struct wl_list bind_queue; // wlr_action_binding_v1.link
struct wlr_action_binder_v1 *binder;
struct wl_resource *resource;
struct wl_list link;
};
struct wlr_action_binding_hint_v1 {
enum {
WLR_ACTION_BINDING_HINT_V1_NONE,
WLR_ACTION_BINDING_HINT_V1_KEYBOARD,
WLR_ACTION_BINDING_HINT_V1_MOUSE,
WLR_ACTION_BINDING_HINT_V1_GESTURE,
} type;
union {
char *keycombo; // WLR_ACTION_BINDING_HINT_V1_KEYBOARD
uint32_t mouse_button; // WLR_ACTION_BINDING_HINT_V1_MOUSE
struct {
enum ext_action_binding_v1_gesture_type type;
enum ext_action_binding_v1_gesture_direction direction;
uint32_t fingers;
} gesture; // WLR_ACTION_BINDING_HINT_V1_GESTURE
};
};
struct wlr_action_binding_v1 {
struct wl_resource *resource;
struct wlr_action_binder_v1_state *state;
char *category, *name;
char *description; // may be NULL when the client doesn't set a description
struct wlr_action_binding_hint_v1 hint;
char *app_id; // may be NULL when the client doesn't set an app_id
struct wlr_seat *seat; // may be NULL when the client doesn't set a seat
struct wl_listener seat_destroy;
struct {
struct wl_signal destroy;
} events;
bool bound;
struct wl_list link;
};
struct wlr_action_binder_v1 *wlr_action_binder_v1_create(struct wl_display *display);
void wlr_action_binding_v1_bind(struct wlr_action_binding_v1 *bind, const char *trigger);
void wlr_action_binding_v1_reject(struct wlr_action_binding_v1 *bind);
void wlr_action_binding_v1_trigger(struct wlr_action_binding_v1 *binding, uint32_t trigger_type, uint32_t time_msec);
#endif

View file

@ -0,0 +1,266 @@
<?xml version="1.0" encoding="UTF-8"?>
<protocol name="ext_action_binder_v1">
<copyright>
Copyright © 2023-2024 Anna "navi" Figueiredo Gomes
Permission to use, copy, modify, distribute, and sell this
software and its documentation for any purpose is hereby granted
without fee, provided that the above copyright notice appear in
all copies and that both that copyright notice and this permission
notice appear in supporting documentation, and that the name of
the copyright holders not be used in advertising or publicity
pertaining to distribution of the software without specific,
written prior permission. The copyright holders make no
representations about the suitability of this software for any
purpose. It is provided "as is" without express or implied
warranty.
THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS
SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY
SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
THIS SOFTWARE.
</copyright>
<description summary="binds actions">
This protocol allows clients to register "actions" as a set of triggers
and metadata, and get notified when those actions are triggered by the user.
Warning! The protocol described in this file is currently in the testing
phase. Backward compatible changes may be added together with the
corresponding interface version bump. Backward incompatible changes can
only be done by creating a new major version of the extension.
The key words "must", "must not", "required", "shall", "shall not", "should",
"should not", "recommended", "may", and "optional" in this document are to
be interpreted as described in IETF RFC 2119.
</description>
<interface name="ext_action_binder_v1" version="1">
<description summary="action binder">
This interface is designed to allow any application to bind an action.
It is left to the compositor to determine which client will get events.
The choice can be based on policy, heuristic, user configuration, or any
other mechanism that may be relevant.
Here are some examples of dispatching choice: all applications, last
focused, user-defined preference order, latest fullscreened application.
This interface is exposed as global.
</description>
<request name="destroy" type="destructor">
<description summary="unbind the actions">
The client no longer wants to receive events for any action.
</description>
</request>
<request name="create_binding">
<description summary="create a binding">
Creates a binding.
After setting the metadata on all bindings created, the client must
call ext_action_binder_v1.commit for the binding to take effect.
</description>
<arg name="binding" type="new_id" interface="ext_action_binding_v1" summary="the new binding" />
</request>
<request name="commit">
<description summary="commits all created bindings">
Commits all bindings created from this interface.
This request may be called again if new bindings are created,
already bound bindings are unaffected.
After calling bind, either the "bound" or "rejected" event shall
be sent for each binding created.
If no action has been set for any binding, the error "invalid_binding" is raised.
</description>
</request>
<enum name="error">
<entry name="invalid_binding" value="0" summary="the binding has no action set"/>
</enum>
</interface>
<interface name="ext_action_binding_v1" version="1">
<description summary="a binding for an action">
This interface defines an individual binding, allowing the client to register metadata about
the action and receive triggered events.
</description>
<request name="destroy" type="destructor">
<description summary="unbind the actions">
The client no longer wants to receive events for this binding.
</description>
</request>
<request name="set_name">
<description summary="sets the category and name of a binding">
Sets an arbitrary couple of a category and a name describing the
wanted behaviour. Some categories are well-known and shared by
applications while each application can have its own category
for internal actions.
This name may be used by the compositor to render a ui for bindings.
A name must be set before attempting to commit a binding.
Attempting to send this request twice or after the binding was bound raises an already set error.
</description>
<arg name="category" type="string" summary="the action category" />
<arg name="name" type="string" summary="the action name" />
</request>
<request name="set_description">
<description summary="set the human-readable description of a binding">
This setting is optional.
This description may be used by the compositor to render a ui for bindings.
Attempting to send this request twice or after the binding was bound raises an already_set error.
</description>
<arg name="description" type="string" summary="a human-readable description of what the binding does" />
</request>
<request name="set_app_id">
<description summary="set the application ID">
This setting is optional.
The app ID identifies the general class of applications to which
the binding belongs. The compositor may use this to select which
client will receive an event.
Attempting to send this request twice or after the binding was bound raises an already_set error.
</description>
<arg name="app_id" type="string" summary="app_id of the application requesting this bind"/>
</request>
<request name="set_seat">
<description summary="sets a target seat">
This setting is optional.
If set, the actions shall only be triggered by the specific seat.
Attempting to send this request twice or after the binding was bound raises an already_set error.
</description>
<arg name="seat" type="object" interface="wl_seat" summary="target seat"/>
</request>
<request name="set_keyboard_hint">
<description summary="Sets a keycombo as a trigger hint.">
This setting is optional.
This hint is a suggestion to the compositor, and the action must not rely
on being set to this specific trigger.
The keycombo must follow the XDG Shortcut standard.
Only a maximum of one hint shall be set per action.
</description>
<arg name="keycombo" type="string" summary="the keycombo that the client would like to trigger the action"/>
</request>
<request name="set_mouse_hint">
<description summary="Sets a mouse button as a trigger hint.">
This setting is optional.
This hint is a suggestion to the compositor, and the action must not rely
on being set to this specific trigger.
Button is a value mapped to their x11 values (1=left, 2=middle, 3=right,
4=scroll up, 5=scroll down, 6=scroll left, 7=scroll right, 8=back, 9=forward).
Only a maximum of one hint shall be set per action.
</description>
<arg name="button" type="uint" summary="the mouse button that the client would like to trigger the action"/>
</request>
<enum name="gesture_type">
<description summary="Type of the hint gesture.">
</description>
<entry name="hold" value="0"/>
<entry name="swipe" value="1"/>
<entry name="pinch" value="2"/>
</enum>
<enum name="gesture_direction">
<description summary="Direction of the hint gesture.">
</description>
<entry name="none" value="0"/>
<entry name="up" value="1"/>
<entry name="down" value="2"/>
<entry name="left" value="3"/>
<entry name="right" value="4"/>
<entry name="inward" value="5"/>
<entry name="outward" value="6"/>
<entry name="clockwise" value="7"/>
<entry name="counterclockwise" value="8"/>
</enum>
<request name="set_gesture_hint">
<description summary="sets a touchpad gesture as a trigger hint">
This setting is optional.
This hint is a suggestion to the compositor, and the action must not rely
on being set to this specific trigger.
Only a maximum of one trigger hint may be set per action.
</description>
<arg name="type" type="uint" enum="gesture_type" summary="the type of trigger that was sent"/>
<arg name="direction" type="uint" enum="gesture_direction" summary="the type of trigger that was sent"/>
<arg name="fingers" type="uint" summary="the number of fingers on the gesture"/>
</request>
<event name="bound">
<description summary="the compositor bound the binding to an action">
Sent after the event was processed, and was bound to one or more of the actions set.
</description>
<arg name="trigger" type="string" summary="human-readable string describing the trigger for the action" />
</event>
<event name="rejected">
<description summary="the compositor rejected the binding">
Sent after the event was processed and got rejected,
or at any time should the compositor want to remove the binding.
The compositor must not send further events after this event.
The client should destroy the object at this point.
</description>
</event>
<enum name="trigger_type">
<description summary="type of binding triggered">
Depending on the user configuration, an action can be either one_shot or
sustained. The client must handle all the three event types and either make
sense of them or ignore them properly.
one_shot actions are for events that don't have a defined "end", like a laptop
lid closing, or a gesture. The client should not expect to recieve a released or
ending event for that action.
sustained actions have a start and an end. after a 'pressed' event is sent, a
'released' event should eventually be sent as well.
</description>
<entry name="one_shot" value="0"
summary="a one shot action was triggered" />
<entry name="pressed" value="1"
summary="a sustained action was started" />
<entry name="released" value="2"
summary="a sustained action ended" />
</enum>
<event name="triggered">
<description summary="the action triggered">
This event is sent when actions are triggered.
</description>
<arg name="time" type="uint" summary="timestamp with millisecond granularity"/>
<arg name="type" type="uint" enum="trigger_type" summary="the type of trigger that was sent"/>
</event>
<enum name="error">
<entry name="already_set" value="0" summary="property was already set"/>
</enum>
</interface>
</protocol>

View file

@ -54,6 +54,7 @@ protocols = {
'drm': 'drm.xml',
'input-method-unstable-v2': 'input-method-unstable-v2.xml',
'kde-server-decoration': 'server-decoration.xml',
'ext-action-binder-v1': 'ext-action-binder-v1.xml',
'virtual-keyboard-unstable-v1': 'virtual-keyboard-unstable-v1.xml',
'wlr-data-control-unstable-v1': 'wlr-data-control-unstable-v1.xml',
'wlr-export-dmabuf-unstable-v1': 'wlr-export-dmabuf-unstable-v1.xml',

View file

@ -33,6 +33,7 @@ wlr_files += files(
'buffer/dmabuf.c',
'buffer/readonly_data.c',
'buffer/resource.c',
'wlr_action_binder_v1.c',
'wlr_alpha_modifier_v1.c',
'wlr_compositor.c',
'wlr_content_type_v1.c',

View file

@ -0,0 +1,361 @@
#define _POSIX_C_SOURCE 200809L
#include <assert.h>
#include <stdlib.h>
#include <string.h>
#include <wayland-server-core.h>
#include <wlr/types/wlr_compositor.h>
#include <wlr/types/wlr_action_binder_v1.h>
#include <wlr/types/wlr_output.h>
#include <wlr/types/wlr_xdg_shell.h>
#include <wlr/util/log.h>
#include <util/time.h>
#include "ext-action-binder-v1-protocol.h"
static void resource_handle_destroy(struct wl_client *client, struct wl_resource *resource) {
wl_resource_destroy(resource);
}
static const struct ext_action_binder_v1_interface ext_action_binder_v1_implementation;
static struct wlr_action_binder_v1_state *wlr_action_binder_v1_state_from_resource(struct wl_resource *resource) {
assert(wl_resource_instance_of(resource, &ext_action_binder_v1_interface, &ext_action_binder_v1_implementation));
return wl_resource_get_user_data(resource);
}
static const struct ext_action_binding_v1_interface ext_action_binding_v1_implementation;
static struct wlr_action_binding_v1 *wlr_action_binding_v1_from_resource(struct wl_resource *resource) {
assert(wl_resource_instance_of(resource, &ext_action_binding_v1_interface, &ext_action_binding_v1_implementation));
return wl_resource_get_user_data(resource);
}
static void destroy_binding(struct wlr_action_binding_v1 *binding) {
if (!binding) {
return;
}
free(binding->category);
free(binding->name);
if (binding->hint.type == WLR_ACTION_BINDING_HINT_V1_KEYBOARD) {
free(binding->hint.keycombo);
}
free(binding->description);
free(binding->app_id);
wl_list_remove(&binding->link);
wl_list_remove(&binding->seat_destroy.link);
wl_resource_set_user_data(binding->resource, NULL); // make resource inert
free(binding);
}
static void handle_seat_destroy(struct wl_listener *listener, void *data) {
struct wlr_action_binding_v1 *binding = wl_container_of(listener, binding, seat_destroy);
wl_list_remove(&binding->seat_destroy.link);
binding->seat = NULL;
wlr_action_binding_v1_reject(binding);
}
static void action_binding_destroy(struct wl_resource *resource) {
struct wlr_action_binding_v1 *binding = wlr_action_binding_v1_from_resource(resource);
destroy_binding(binding);
}
static void action_binding_set_name(struct wl_client *client,
struct wl_resource *resource, const char *category, const char *name) {
struct wlr_action_binding_v1 *binding = wlr_action_binding_v1_from_resource(resource);
if (!binding) {
return;
}
if (binding->bound || binding->name || binding->category) {
wl_resource_post_error(binding->resource,
EXT_ACTION_BINDING_V1_ERROR_ALREADY_SET,
"attempted to set a binding property twice");
return;
}
binding->name = strdup(name);
if (binding->name == NULL) {
wl_client_post_no_memory(client);
}
binding->category = strdup(category);
if (binding->category == NULL) {
wl_client_post_no_memory(client);
}
}
static struct wlr_action_binding_hint_v1 *action_binding_hint_from_resource(struct wl_resource *resource) {
struct wlr_action_binding_v1 *binding = wlr_action_binding_v1_from_resource(resource);
if (!binding) {
return NULL;
}
if (binding->bound || binding->hint.type != WLR_ACTION_BINDING_HINT_V1_NONE) {
wl_resource_post_error(binding->resource,
EXT_ACTION_BINDING_V1_ERROR_ALREADY_SET, "attempted to set a hint twice");
return NULL;
}
return &binding->hint;
}
static void action_binding_set_mouse_hint(struct wl_client *client,
struct wl_resource *resource, uint32_t button) {
struct wlr_action_binding_hint_v1 *hint = action_binding_hint_from_resource(resource);
if (!hint) {
return;
}
hint->type = WLR_ACTION_BINDING_HINT_V1_MOUSE;
hint->mouse_button = button;
}
static void action_binding_set_keyboard_hint(struct wl_client *client,
struct wl_resource *resource, const char *keycombo) {
struct wlr_action_binding_hint_v1 *hint = action_binding_hint_from_resource(resource);
if (!hint) {
return;
}
hint->type = WLR_ACTION_BINDING_HINT_V1_KEYBOARD;
hint->keycombo = strdup(keycombo);
}
static void action_binding_set_gesture_hint(struct wl_client *client,
struct wl_resource *resource, uint32_t type, uint32_t direction, uint32_t fingers) {
struct wlr_action_binding_hint_v1 *hint = action_binding_hint_from_resource(resource);
if (!hint) {
return;
}
hint->type = WLR_ACTION_BINDING_HINT_V1_GESTURE;
hint->gesture.type = type;
hint->gesture.direction = direction;
hint->gesture.fingers = fingers;
}
static void action_binding_set_desc(struct wl_client *client,
struct wl_resource *resource, const char *description) {
struct wlr_action_binding_v1 *binding = wlr_action_binding_v1_from_resource(resource);
if (!binding) {
return;
}
if (binding->bound || binding->description) {
wl_resource_post_error(binding->resource,
EXT_ACTION_BINDING_V1_ERROR_ALREADY_SET,
"attempted to set a binding property twice");
return;
}
binding->description = strdup(description);
if (binding->description == NULL) {
wl_client_post_no_memory(client);
}
}
static void action_binding_set_app_id(struct wl_client *client,
struct wl_resource *resource, const char *app_id) {
struct wlr_action_binding_v1 *binding = wlr_action_binding_v1_from_resource(resource);
if (!binding) {
return;
}
if (binding->bound || binding->app_id) {
wl_resource_post_error(binding->resource,
EXT_ACTION_BINDING_V1_ERROR_ALREADY_SET,
"attempted to set a binding property twice");
return;
}
binding->app_id = strdup(app_id);
if (binding->app_id == NULL) {
wl_client_post_no_memory(client);
}
}
static void action_binding_set_seat(struct wl_client *client,
struct wl_resource *resource, struct wl_resource *seat_resource) {
struct wlr_action_binding_v1 *binding = wlr_action_binding_v1_from_resource(resource);
if (!binding) {
return;
}
if (binding->bound || binding->seat) {
wl_resource_post_error(binding->resource,
EXT_ACTION_BINDING_V1_ERROR_ALREADY_SET,
"attempted to set a binding property twice");
return;
}
struct wlr_seat_client *seat_client = wlr_seat_client_from_resource(seat_resource);
binding->seat = seat_client ? seat_client->seat : NULL;
if (binding->seat) {
wl_signal_add(&binding->seat->events.destroy, &binding->seat_destroy);
}
}
static const struct ext_action_binding_v1_interface ext_action_binding_v1_implementation = {
.destroy = resource_handle_destroy,
.set_mouse_hint = action_binding_set_mouse_hint,
.set_keyboard_hint = action_binding_set_keyboard_hint,
.set_gesture_hint = action_binding_set_gesture_hint,
.set_description = action_binding_set_desc,
.set_name = action_binding_set_name,
.set_app_id = action_binding_set_app_id,
.set_seat = action_binding_set_seat,
};
static void action_binder_create_binding(struct wl_client *client,
struct wl_resource *resource, uint32_t binding) {
struct wlr_action_binder_v1_state *state = wlr_action_binder_v1_state_from_resource(resource);
struct wl_resource *bind_resource = wl_resource_create(client,
&ext_action_binding_v1_interface, ext_action_binding_v1_interface.version, binding);
if (bind_resource == NULL) {
wl_client_post_no_memory(client);
return;
}
struct wlr_action_binding_v1 *bind = calloc(1, sizeof(*bind));
if (bind == NULL) {
wl_client_post_no_memory(client);
wl_resource_destroy(bind_resource);
return;
}
bind->resource = bind_resource;
bind->state = state;
bind->bound = false;
wl_list_init(&bind->link);
wl_signal_init(&bind->events.destroy);
wl_list_insert(&state->bind_queue, &bind->link);
wl_list_init(&bind->seat_destroy.link);
bind->seat_destroy.notify = handle_seat_destroy;
wl_resource_set_implementation(bind_resource,
&ext_action_binding_v1_implementation, bind, action_binding_destroy);
}
static void action_binder_commit(struct wl_client *client, struct wl_resource *resource) {
struct wlr_action_binder_v1_state *state = wlr_action_binder_v1_state_from_resource(resource);
struct wlr_action_binding_v1 *binding = NULL;
wl_list_for_each(binding, &state->bind_queue, link) {
if (!binding->category || !binding->name) {
wl_resource_post_error(binding->resource,
EXT_ACTION_BINDER_V1_ERROR_INVALID_BINDING,
"attempted to bind a unactionable binding");
return;
}
}
wl_signal_emit_mutable(&state->binder->events.bind, state);
}
static const struct ext_action_binder_v1_interface ext_action_binder_v1_implementation = {
.create_binding = action_binder_create_binding,
.commit = action_binder_commit,
.destroy = resource_handle_destroy,
};
static void action_binder_destroy(struct wl_resource *resource) {
struct wlr_action_binder_v1_state *state = wlr_action_binder_v1_state_from_resource(resource);
struct wlr_action_binding_v1 *binding = NULL, *tmp = NULL;
wl_list_for_each_safe(binding, tmp, &state->binds, link) {
wl_signal_emit(&binding->events.destroy, NULL);
destroy_binding(binding);
}
wl_list_for_each_safe(binding, tmp, &state->bind_queue, link) {
destroy_binding(binding);
}
wl_list_remove(&state->link);
free(state);
}
static void action_binder_bind(struct wl_client *wl_client,
void *data, uint32_t version, uint32_t id) {
struct wlr_action_binder_v1 *binder = data;
struct wl_resource *resource = wl_resource_create(wl_client,
&ext_action_binder_v1_interface, version, id);
if (resource == NULL) {
wl_client_post_no_memory(wl_client);
return;
}
struct wlr_action_binder_v1_state *state = calloc(1, sizeof(*state));
if (state == NULL) {
wl_client_post_no_memory(wl_client);
wl_resource_destroy(resource);
return;
}
wl_list_init(&state->binds);
wl_list_init(&state->bind_queue);
state->binder = binder;
state->resource = resource;
wl_list_insert(&binder->states, &state->link);
wl_resource_set_implementation(resource,
&ext_action_binder_v1_implementation, state, action_binder_destroy);
}
static void handle_display_destroy(struct wl_listener *listener, void *data) {
struct wlr_action_binder_v1 *binder = wl_container_of(listener, binder, display_destroy);
wl_signal_emit(&binder->events.destroy, NULL);
wl_list_remove(&binder->display_destroy.link);
wl_global_destroy(binder->global);
free(binder);
}
struct wlr_action_binder_v1 *wlr_action_binder_v1_create(struct wl_display *display) {
struct wlr_action_binder_v1 *action_binder = calloc(1, sizeof(*action_binder));
if (!action_binder) {
return NULL;
}
struct wl_global *global = wl_global_create(display,
&ext_action_binder_v1_interface, 1, action_binder, action_binder_bind);
if (!global) {
free(action_binder);
return NULL;
}
action_binder->global = global;
wl_signal_init(&action_binder->events.bind);
wl_signal_init(&action_binder->events.destroy);
wl_list_init(&action_binder->states);
action_binder->display_destroy.notify = handle_display_destroy;
wl_display_add_destroy_listener(display, &action_binder->display_destroy);
return action_binder;
}
void wlr_action_binding_v1_bind(struct wlr_action_binding_v1 *binding, const char *trigger) {
assert(!binding->bound);
binding->bound = true;
wl_list_remove(&binding->link);
wl_list_insert(&binding->state->binds, &binding->link);
ext_action_binding_v1_send_bound(binding->resource, trigger);
}
void wlr_action_binding_v1_reject(struct wlr_action_binding_v1 *binding) {
ext_action_binding_v1_send_rejected(binding->resource);
wl_signal_emit(&binding->events.destroy, NULL);
destroy_binding(binding);
}
void wlr_action_binding_v1_trigger(struct wlr_action_binding_v1 *binding, uint32_t trigger_type, uint32_t time_msec) {
ext_action_binding_v1_send_triggered(binding->resource, time_msec, trigger_type);
}