ext-workspace protocol implementation

This commit is contained in:
Consolatis 2024-11-18 21:16:10 +01:00
parent b2a45ac8af
commit acde3b4a8b
8 changed files with 1495 additions and 0 deletions

View file

@ -0,0 +1,25 @@
/* SPDX-License-Identifier: GPL-2.0-only */
#ifndef LABWC_PROTOCOLS_EXT_WORKSPACES_INTERNAL_H
#define LABWC_PROTOCOLS_EXT_WORKSPACES_INTERNAL_H
struct wl_resource;
struct lab_ext_workspace_group;
struct lab_ext_workspace_manager;
enum pending_ext_workspaces_change {
/* group events */
WS_PENDING_WS_CREATE = 1 << 0,
/* ws events*/
WS_PENDING_WS_ACTIVATE = 1 << 1,
WS_PENDING_WS_DEACTIVATE = 1 << 2,
WS_PENDING_WS_REMOVE = 1 << 3,
WS_PENDING_WS_ASSIGN = 1 << 4,
};
void ext_group_output_send_initial_state(struct lab_ext_workspace_group *group,
struct wl_resource *group_resource);
void ext_manager_schedule_done_event(struct lab_ext_workspace_manager *manager);
#endif /* LABWC_PROTOCOLS_EXT_WORKSPACES_INTERNAL_H */

View file

@ -0,0 +1,109 @@
/* SPDX-License-Identifier: GPL-2.0-only */
#ifndef LABWC_PROTOCOLS_EXT_WORKSPACES_H
#define LABWC_PROTOCOLS_EXT_WORKSPACES_H
#include <stdbool.h>
#include <wayland-server-core.h>
struct wlr_output;
struct lab_ext_workspace_manager {
struct wl_global *global;
struct wl_list groups;
struct wl_list workspaces;
uint32_t caps;
struct wl_event_source *idle_source;
struct wl_event_loop *event_loop;
struct {
struct wl_listener display_destroy;
} on;
struct wl_list resources;
};
struct lab_ext_workspace_group {
struct lab_ext_workspace_manager *manager;
uint32_t capabilities;
struct {
struct wl_signal create_workspace;
struct wl_signal destroy;
} events;
struct wl_list link;
struct wl_list outputs;
struct wl_list resources;
};
struct lab_ext_workspace {
struct lab_ext_workspace_manager *manager;
struct lab_ext_workspace_group *group;
char *id;
char *name;
struct wl_array coordinates;
uint32_t capabilities;
uint32_t state; /* enum lab_ext_workspace_state */
uint32_t state_pending; /* enum lab_ext_workspace_state */
struct {
struct wl_signal activate;
struct wl_signal deactivate;
struct wl_signal remove;
struct wl_signal assign;
struct wl_signal destroy;
} events;
struct wl_list link;
struct wl_list resources;
};
enum lab_ext_workspace_caps {
WS_CAP_NONE = 0,
WS_CAP_GRP_ALL = 0x0000ffff,
WS_CAP_WS_ALL = 0xffff0000,
/* group caps */
WS_CAP_GRP_WS_CREATE = 1 << 0,
/* workspace caps */
WS_CAP_WS_ACTIVATE = 1 << 16,
WS_CAP_WS_DEACTIVATE = 1 << 17,
WS_CAP_WS_REMOVE = 1 << 18,
WS_CAP_WS_ASSIGN = 1 << 19,
};
struct lab_ext_workspace_manager *lab_ext_workspace_manager_create(
struct wl_display *display, uint32_t caps, uint32_t version);
struct lab_ext_workspace_group *lab_ext_workspace_group_create(
struct lab_ext_workspace_manager *manager);
void lab_ext_workspace_group_output_enter(
struct lab_ext_workspace_group *group, struct wlr_output *output);
void lab_ext_workspace_group_output_leave(
struct lab_ext_workspace_group *group, struct wlr_output *output);
void lab_ext_workspace_group_destroy(struct lab_ext_workspace_group *group);
/* Create a new workspace, id may be NULL */
struct lab_ext_workspace *lab_ext_workspace_create(
struct lab_ext_workspace_manager *manager, const char *id);
void lab_ext_workspace_assign_to_group(struct lab_ext_workspace *workspace,
struct lab_ext_workspace_group *group);
void lab_ext_workspace_set_name(struct lab_ext_workspace *workspace, const char *name);
void lab_ext_workspace_set_active(struct lab_ext_workspace *workspace, bool enabled);
void lab_ext_workspace_set_urgent(struct lab_ext_workspace *workspace, bool enabled);
void lab_ext_workspace_set_hidden(struct lab_ext_workspace *workspace, bool enabled);
void lab_ext_workspace_set_coordinates(struct lab_ext_workspace *workspace,
struct wl_array *coordinates);
void lab_ext_workspace_destroy(struct lab_ext_workspace *workspace);
#endif /* LABWC_PROTOCOLS_EXT_WORKSPACES_H */

View file

@ -0,0 +1,418 @@
<?xml version="1.0" encoding="UTF-8"?>
<protocol name="ext_workspace_v1">
<copyright>
Copyright © 2019 Christopher Billington
Copyright © 2020 Ilia Bozhinov
Copyright © 2022 Victoria Brekenfeld
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>
<interface name="ext_workspace_manager_v1" version="1">
<description summary="list and control workspaces">
Workspaces, also called virtual desktops, are groups of surfaces. A
compositor with a concept of workspaces may only show some such groups of
surfaces (those of 'active' workspaces) at a time. 'Activating' a
workspace is a request for the compositor to display that workspace's
surfaces as normal, whereas the compositor may hide or otherwise
de-emphasise surfaces that are associated only with 'inactive' workspaces.
Workspaces are grouped by which sets of outputs they correspond to, and
may contain surfaces only from those outputs. In this way, it is possible
for each output to have its own set of workspaces, or for all outputs (or
any other arbitrary grouping) to share workspaces. Compositors may
optionally conceptually arrange each group of workspaces in an
N-dimensional grid.
The purpose of this protocol is to enable the creation of taskbars and
docks by providing them with a list of workspaces and their properties,
and allowing them to activate and deactivate workspaces.
After a client binds the ext_workspace_manager_v1, each workspace will be
sent via the workspace event.
</description>
<event name="workspace_group">
<description summary="a workspace group has been created">
This event is emitted whenever a new workspace group has been created.
All initial details of the workspace group (outputs) will be
sent immediately after this event via the corresponding events in
ext_workspace_group_handle_v1 and ext_workspace_handle_v1.
</description>
<arg name="workspace_group" type="new_id" interface="ext_workspace_group_handle_v1"/>
</event>
<event name="workspace">
<description summary="workspace has been created">
This event is emitted whenever a new workspace has been created.
All initial details of the workspace (name, coordinates, state) will
be sent immediately after this event via the corresponding events in
ext_workspace_handle_v1.
Workspaces start off unassigned to any workspace group.
</description>
<arg name="workspace" type="new_id" interface="ext_workspace_handle_v1"/>
</event>
<request name="commit">
<description summary="all requests about the workspaces have been sent">
The client must send this request after it has finished sending other
requests. The compositor must process a series of requests preceding a
commit request atomically.
This allows changes to the workspace properties to be seen as atomic,
even if they happen via multiple events, and even if they involve
multiple ext_workspace_handle_v1 objects, for example, deactivating one
workspace and activating another.
</description>
</request>
<event name="done">
<description summary="all information about the workspaces and workspace groups has been sent">
This event is sent after all changes in all workspaces and workspace groups have been
sent.
This allows changes to one or more ext_workspace_group_handle_v1
properties and ext_workspace_handle_v1 properties
to be seen as atomic, even if they happen via multiple events.
In particular, an output moving from one workspace group to
another sends an output_enter event and an output_leave event to the two
ext_workspace_group_handle_v1 objects in question. The compositor sends
the done event only after updating the output information in both
workspace groups.
</description>
</event>
<event name="finished" type="destructor">
<description summary="the compositor has finished with the workspace_manager">
This event indicates that the compositor is done sending events to the
ext_workspace_manager_v1. The server will destroy the object
immediately after sending this request.
</description>
</event>
<request name="stop">
<description summary="stop sending events">
Indicates the client no longer wishes to receive events for new
workspace groups. However the compositor may emit further workspace
events, until the finished event is emitted. The compositor is expected
to send the finished event eventually once the stop request has been processed.
The client must not send any requests after this one, doing so will raise a wl_display
invalid_object error.
</description>
</request>
</interface>
<interface name="ext_workspace_group_handle_v1" version="1">
<description summary="a workspace group assigned to a set of outputs">
A ext_workspace_group_handle_v1 object represents a workspace group
that is assigned a set of outputs and contains a number of workspaces.
The set of outputs assigned to the workspace group is conveyed to the client via
output_enter and output_leave events, and its workspaces are conveyed with
workspace events.
For example, a compositor which has a set of workspaces for each output may
advertise a workspace group (and its workspaces) per output, whereas a compositor
where a workspace spans all outputs may advertise a single workspace group for all
outputs.
</description>
<enum name="group_capabilities" bitfield="true">
<entry name="create_workspace" value="1" summary="create_workspace request is available"/>
</enum>
<event name="capabilities">
<description summary="compositor capabilities">
This event advertises the capabilities supported by the compositor. If
a capability isn't supported, clients should hide or disable the UI
elements that expose this functionality. For instance, if the
compositor doesn't advertise support for creating workspaces, a button
triggering the create_workspace request should not be displayed.
The compositor will ignore requests it doesn't support. For instance,
a compositor which doesn't advertise support for creating workspaces will ignore
create_workspace requests.
Compositors must send this event once after creation of an
ext_workspace_group_handle_v1. When the capabilities change, compositors
must send this event again.
</description>
<arg name="capabilities" type="uint" summary="capabilities" enum="group_capabilities"/>
</event>
<event name="output_enter">
<description summary="output assigned to workspace group">
This event is emitted whenever an output is assigned to the workspace
group or a new `wl_output` object is bound by the client, which was already
assigned to this workspace_group.
</description>
<arg name="output" type="object" interface="wl_output"/>
</event>
<event name="output_leave">
<description summary="output removed from workspace group">
This event is emitted whenever an output is removed from the workspace
group.
</description>
<arg name="output" type="object" interface="wl_output"/>
</event>
<event name="workspace_enter">
<description summary="workspace added to workspace group">
This event is emitted whenever a workspace is assigned to this group.
A workspace may only ever be assigned to a single group at a single point
in time, but can be re-assigned during it's lifetime.
</description>
<arg name="workspace" type="object" interface="ext_workspace_handle_v1"/>
</event>
<event name="workspace_leave">
<description summary="workspace removed from workspace group">
This event is emitted whenever a workspace is remove from this group.
</description>
<arg name="workspace" type="object" interface="ext_workspace_handle_v1"/>
</event>
<event name="removed">
<description summary="this workspace group has been removed">
This event is send when the group associated with the ext_workspace_group_handle_v1
has been removed. After sending this request the compositor will immediately consider
the object inert. Any requests will be ignored except the destroy request.
It is guaranteed there won't be any more events for this
ext_workspace_group_handle_v1.
The compositor must remove all workspaces belonging to a workspace group
via a workspace_leave event before removing the workspace group.
</description>
</event>
<request name="create_workspace">
<description summary="create a new workspace">
Request that the compositor create a new workspace with the given name
and assign it to this group.
There is no guarantee that the compositor will create a new workspace,
or that the created workspace will have the provided name.
</description>
<arg name="workspace" type="string"/>
</request>
<request name="destroy" type="destructor">
<description summary="destroy the ext_workspace_group_handle_v1 object">
Destroys the ext_workspace_group_handle_v1 object.
This request should be send either when the client does not want to
use the workspace group object any more or after the removed event to finalize
the destruction of the object.
</description>
</request>
</interface>
<interface name="ext_workspace_handle_v1" version="1">
<description summary="a workspace handing a group of surfaces">
A ext_workspace_handle_v1 object represents a workspace that handles a
group of surfaces.
Each workspace has:
- a name, conveyed to the client with the name event
- potentially an id conveyed with the id event
- a list of states, conveyed to the client with the state event
- and optionally a set of coordinates, conveyed to the client with the
coordinates event
The client may request that the compositor activate or deactivate the workspace.
Each workspace can belong to only a single workspace group.
Depepending on the compositor policy, there might be workspaces with
the same name in different workspace groups, but these workspaces are still
separate (e.g. one of them might be active while the other is not).
</description>
<event name="id">
<description summary="workspace id">
If this event is emitted, it will be send immediately after the
ext_workspace_handle_v1 is created or when an id is assigned to
a workspace (at most once during it's lifetime).
An id will never change during the lifetime of the `ext_workspace_handle_v1`
and is guaranteed to be unique during it's lifetime.
Ids are not human-readable and shouldn't be displayed, use `name` for that purpose.
Compositors are expected to only send ids for workspaces likely stable across multiple
sessions and can be used by clients to store preferences for workspaces. Workspaces without
ids should be considered temporary and any data associated with them should be deleted once
the respective object is lost.
</description>
<arg name="id" type="string"/>
</event>
<event name="name">
<description summary="workspace name changed">
This event is emitted immediately after the ext_workspace_handle_v1 is
created and whenever the name of the workspace changes.
A name is meant to be human-readable and can be displayed to a user.
Unlike the id it is neither stable nor unique.
</description>
<arg name="name" type="string"/>
</event>
<event name="coordinates">
<description summary="workspace coordinates changed">
This event is used to organize workspaces into an N-dimensional grid
within a workspace group, and if supported, is emitted immediately after
the ext_workspace_handle_v1 is created and whenever the coordinates of
the workspace change. Compositors may not send this event if they do not
conceptually arrange workspaces in this way. If compositors simply
number workspaces, without any geometric interpretation, they may send
1D coordinates, which clients should not interpret as implying any
geometry. Sending an empty array means that the compositor no longer
orders the workspace geometrically.
Coordinates have an arbitrary number of dimensions N with an uint32
position along each dimension. By convention if N > 1, the first
dimension is X, the second Y, the third Z, and so on. The compositor may
chose to utilize these events for a more novel workspace layout
convention, however. No guarantee is made about the grid being filled or
bounded; there may be a workspace at coordinate 1 and another at
coordinate 1000 and none in between. Within a workspace group, however,
workspaces must have unique coordinates of equal dimensionality.
</description>
<arg name="coordinates" type="array"/>
</event>
<enum name="state" bitfield="true">
<description summary="types of states on the workspace">
The different states that a workspace can have.
</description>
<entry name="active" value="1" summary="the workspace is active"/>
<entry name="urgent" value="2" summary="the workspace requests attention"/>
<entry name="hidden" value="4">
<description summary="the workspace is not visible">
The workspace is not visible in its workspace group, and clients
attempting to visualize the compositor workspace state should not
display such workspaces.
</description>
</entry>
</enum>
<event name="state">
<description summary="the state of the workspace changed">
This event is emitted immediately after the ext_workspace_handle_v1 is
created and each time the workspace state changes, either because of a
compositor action or because of a request in this protocol.
Missing states convey the opposite meaning, e.g. an unset active bit
means the workspace is currently inactive.
</description>
<arg name="state" type="uint" enum="state"/>
</event>
<enum name="workspace_capabilities" bitfield="true">
<entry name="activate" value="1" summary="activate request is available"/>
<entry name="deactivate" value="2" summary="deactivate request is available"/>
<entry name="remove" value="4" summary="remove request is available"/>
<entry name="assign" value="8" summary="assign request is available"/>
</enum>
<event name="capabilities">
<description summary="compositor capabilities">
This event advertises the capabilities supported by the compositor. If
a capability isn't supported, clients should hide or disable the UI
elements that expose this functionality. For instance, if the
compositor doesn't advertise support for removing workspaces, a button
triggering the remove request should not be displayed.
The compositor will ignore requests it doesn't support. For instance,
a compositor which doesn't advertise support for remove will ignore
remove requests.
Compositors must send this event once after creation of an
ext_workspace_handle_v1 . When the capabilities change, compositors
must send this event again.
</description>
<arg name="capabilities" type="uint" summary="capabilities" enum="workspace_capabilities"/>
</event>
<event name="removed">
<description summary="this workspace has been removed">
This event is send when the workspace associated with the ext_workspace_handle_v1
has been removed. After sending this request, the compositor will immediately consider
the object inert. Any requests will be ignored except the destroy request.
It is guaranteed there won't be any more events for this
ext_workspace_handle_v1.
</description>
</event>
<request name="destroy" type="destructor">
<description summary="destroy the ext_workspace_handle_v1 object">
Destroys the ext_workspace_handle_v1 object.
This request should be made either when the client does not want to
use the workspace object any more or after the remove event to finalize
the destruction of the object.
</description>
</request>
<request name="activate">
<description summary="activate the workspace">
Request that this workspace be activated.
There is no guarantee the workspace will be actually activated, and
behaviour may be compositor-dependent. For example, activating a
workspace may or may not deactivate all other workspaces in the same
group.
</description>
</request>
<request name="deactivate">
<description summary="activate the workspace">
Request that this workspace be deactivated.
There is no guarantee the workspace will be actually deactivated.
</description>
</request>
<request name="assign">
<description summary="assign workspace to group">
Requests that this workspace is assigned to the given workspace group.
There is no guarantee the workspace will be assigned.
</description>
<arg name="workspace_group" type="object" interface="ext_workspace_group_handle_v1"/>
</request>
<request name="remove">
<description summary="remove the workspace">
Request that this workspace be removed.
There is no guarantee the workspace will be actually removed.
</description>
</request>
</interface>
</protocol>

View file

@ -22,6 +22,7 @@ server_protocols = [
wl_protocol_dir / 'staging/xwayland-shell/xwayland-shell-v1.xml',
wl_protocol_dir / 'staging/tearing-control/tearing-control-v1.xml',
'cosmic-workspace-unstable-v1.xml',
'ext-workspace-v1.xml',
'wlr-layer-shell-unstable-v1.xml',
'wlr-input-inhibitor-unstable-v1.xml',
'wlr-output-power-management-unstable-v1.xml',

View file

@ -0,0 +1,763 @@
// SPDX-License-Identifier: GPL-2.0-only
#include <assert.h>
#include <wlr/types/wlr_output.h>
#include <wlr/util/log.h>
#include "common/array.h"
#include "common/mem.h"
#include "common/list.h"
#include "ext-workspace-v1-protocol.h"
#include "protocols/ext-workspace.h"
#include "protocols/ext-workspace-internal.h"
#include "protocols/transaction-addon.h"
/*
* .--------------------.
* | TODO |
* |--------------------|
* | - go through xml |
* | and verify impl |
* | - assert pub API |
* `--------------------´
*
*/
/* Only used within an assert() */
#ifndef NDEBUG
#define EXT_WORKSPACE_V1_VERSION 1
#endif
/*
* Set when creating a new workspace state so we
* don't end up having to send the state twice.
*/
#define WS_STATE_INVALID 0xffffffff
struct ws_create_workspace_event {
char *name;
struct {
struct wl_listener transaction_op_destroy;
} on;
};
/* Workspace */
static void
workspace_handle_destroy(struct wl_client *client, struct wl_resource *resource)
{
wl_resource_destroy(resource);
}
static void
workspace_handle_activate(struct wl_client *client, struct wl_resource *resource)
{
struct lab_wl_resource_addon *addon = wl_resource_get_user_data(resource);
if (!addon) {
/* Workspace was destroyed from the compositor side */
return;
}
struct lab_ext_workspace *workspace = addon->data;
lab_transaction_op_add(addon->ctx, WS_PENDING_WS_ACTIVATE,
workspace, /*data*/ NULL);
}
static void
workspace_handle_deactivate(struct wl_client *client, struct wl_resource *resource)
{
struct lab_wl_resource_addon *addon = wl_resource_get_user_data(resource);
if (!addon) {
/* Workspace was destroyed from the compositor side */
return;
}
struct lab_ext_workspace *workspace = addon->data;
lab_transaction_op_add(addon->ctx, WS_PENDING_WS_DEACTIVATE,
workspace, /*data*/ NULL);
}
static void
workspace_handle_assign(struct wl_client *client, struct wl_resource *resource,
struct wl_resource *new_group_resource)
{
struct lab_wl_resource_addon *addon = wl_resource_get_user_data(resource);
if (!addon) {
/* Workspace was destroyed from the compositor side */
return;
}
struct lab_ext_workspace *workspace = addon->data;
struct lab_wl_resource_addon *grp_addon =
wl_resource_get_user_data(new_group_resource);
if (!grp_addon) {
/* Group was destroyed from the compositor side */
return;
}
struct lab_ext_workspace_group *new_grp = grp_addon->data;
lab_transaction_op_add(addon->ctx, WS_PENDING_WS_ASSIGN, workspace, new_grp);
}
static void
workspace_handle_remove(struct wl_client *client, struct wl_resource *resource)
{
struct lab_wl_resource_addon *addon = wl_resource_get_user_data(resource);
if (!addon) {
/* Workspace was destroyed from the compositor side */
return;
}
struct lab_ext_workspace *workspace = addon->data;
lab_transaction_op_add(addon->ctx, WS_PENDING_WS_REMOVE,
workspace, /*data*/ NULL);
}
static const struct ext_workspace_handle_v1_interface workspace_impl = {
.destroy = workspace_handle_destroy,
.activate = workspace_handle_activate,
.deactivate = workspace_handle_deactivate,
.assign = workspace_handle_assign,
.remove = workspace_handle_remove,
};
static void
workspace_instance_resource_destroy(struct wl_resource *resource)
{
struct lab_wl_resource_addon *addon = wl_resource_get_user_data(resource);
if (addon) {
lab_resource_addon_destroy(addon);
wl_resource_set_user_data(resource, NULL);
}
wl_list_remove(wl_resource_get_link(resource));
}
static struct wl_resource *
workspace_resource_create(struct lab_ext_workspace *workspace,
struct wl_resource *manager_resource,
struct lab_transaction_session_context *ctx)
{
struct wl_client *client = wl_resource_get_client(manager_resource);
struct wl_resource *resource = wl_resource_create(client,
&ext_workspace_handle_v1_interface,
wl_resource_get_version(manager_resource), 0);
if (!resource) {
wl_client_post_no_memory(client);
return NULL;
}
struct lab_wl_resource_addon *addon = lab_resource_addon_create(ctx);
addon->data = workspace;
wl_resource_set_implementation(resource, &workspace_impl, addon,
workspace_instance_resource_destroy);
wl_list_insert(&workspace->resources, wl_resource_get_link(resource));
return resource;
}
/* Workspace internal helpers */
static void
workspace_send_state(struct lab_ext_workspace *workspace, struct wl_resource *target)
{
if (target) {
ext_workspace_handle_v1_send_state(target, workspace->state);
} else {
struct wl_resource *resource;
wl_resource_for_each(resource, &workspace->resources) {
ext_workspace_handle_v1_send_state(resource, workspace->state);
}
}
}
static void
workspace_send_initial_state(struct lab_ext_workspace *workspace, struct wl_resource *resource)
{
ext_workspace_handle_v1_send_capabilities(resource, workspace->capabilities);
if (workspace->coordinates.size > 0) {
ext_workspace_handle_v1_send_coordinates(resource, &workspace->coordinates);
}
if (workspace->name) {
ext_workspace_handle_v1_send_name(resource, workspace->name);
}
if (workspace->id) {
ext_workspace_handle_v1_send_id(resource, workspace->id);
}
}
static void
workspace_set_state(struct lab_ext_workspace *workspace,
enum ext_workspace_handle_v1_state state, bool enabled)
{
if (!!(workspace->state_pending & state) == enabled) {
return;
}
if (enabled) {
workspace->state_pending |= state;
} else {
workspace->state_pending &= ~state;
}
ext_manager_schedule_done_event(workspace->manager);
}
/* Group */
static void
ws_create_workspace_handle_transaction_op_destroy(struct wl_listener *listener, void *data)
{
struct ws_create_workspace_event *ev =
wl_container_of(listener, ev, on.transaction_op_destroy);
wl_list_remove(&ev->on.transaction_op_destroy.link);
free(ev->name);
free(ev);
}
static void
group_handle_create_workspace(struct wl_client *client,
struct wl_resource *resource, const char *name)
{
struct lab_wl_resource_addon *addon = wl_resource_get_user_data(resource);
if (!addon) {
return;
}
struct lab_ext_workspace_group *group = addon->data;
struct ws_create_workspace_event *ev = znew(*ev);
ev->name = xstrdup(name);
struct lab_transaction_op *transaction_op = lab_transaction_op_add(
addon->ctx, WS_PENDING_WS_CREATE, group, ev);
ev->on.transaction_op_destroy.notify =
ws_create_workspace_handle_transaction_op_destroy;
wl_signal_add(&transaction_op->events.destroy, &ev->on.transaction_op_destroy);
}
static void
group_handle_destroy(struct wl_client *client, struct wl_resource *resource)
{
wl_resource_destroy(resource);
}
static const struct ext_workspace_group_handle_v1_interface group_impl = {
.create_workspace = group_handle_create_workspace,
.destroy = group_handle_destroy,
};
static void
group_instance_resource_destroy(struct wl_resource *resource)
{
struct lab_wl_resource_addon *addon = wl_resource_get_user_data(resource);
if (addon) {
lab_resource_addon_destroy(addon);
wl_resource_set_user_data(resource, NULL);
}
wl_list_remove(wl_resource_get_link(resource));
}
static struct wl_resource *
group_resource_create(struct lab_ext_workspace_group *group,
struct wl_resource *manager_resource, struct lab_transaction_session_context *ctx)
{
struct wl_client *client = wl_resource_get_client(manager_resource);
struct wl_resource *resource = wl_resource_create(client,
&ext_workspace_group_handle_v1_interface,
wl_resource_get_version(manager_resource), 0);
if (!resource) {
wl_client_post_no_memory(client);
return NULL;
}
struct lab_wl_resource_addon *addon = lab_resource_addon_create(ctx);
addon->data = group;
wl_resource_set_implementation(resource, &group_impl, addon,
group_instance_resource_destroy);
wl_list_insert(&group->resources, wl_resource_get_link(resource));
return resource;
}
/* Group internal helpers */
static void
group_send_state(struct lab_ext_workspace_group *group, struct wl_resource *resource)
{
ext_workspace_group_handle_v1_send_capabilities(
resource, group->capabilities);
ext_group_output_send_initial_state(group, resource);
}
/* Manager itself */
static void
manager_handle_commit(struct wl_client *client, struct wl_resource *resource)
{
struct lab_wl_resource_addon *addon = wl_resource_get_user_data(resource);
if (!addon) {
return;
}
struct lab_ext_workspace *workspace;
struct lab_ext_workspace_group *group;
struct lab_transaction_op *trans_op, *trans_op_tmp;
lab_transaction_for_each_safe(trans_op, trans_op_tmp, addon->ctx) {
switch (trans_op->change) {
case WS_PENDING_WS_CREATE:
group = trans_op->src;
struct ws_create_workspace_event *ev = trans_op->data;
wl_signal_emit_mutable(&group->events.create_workspace, ev->name);
break;
case WS_PENDING_WS_ACTIVATE:
workspace = trans_op->src;
wl_signal_emit_mutable(&workspace->events.activate, NULL);
break;
case WS_PENDING_WS_DEACTIVATE:
workspace = trans_op->src;
wl_signal_emit_mutable(&workspace->events.deactivate, NULL);
break;
case WS_PENDING_WS_REMOVE:
workspace = trans_op->src;
wl_signal_emit_mutable(&workspace->events.remove, NULL);
break;
case WS_PENDING_WS_ASSIGN:
workspace = trans_op->src;
wl_signal_emit_mutable(&workspace->events.assign, trans_op->data);
break;
default:
wlr_log(WLR_ERROR, "Invalid transaction state: %u", trans_op->change);
}
lab_transaction_op_destroy(trans_op);
}
}
static void
manager_handle_stop(struct wl_client *client, struct wl_resource *resource)
{
ext_workspace_manager_v1_send_finished(resource);
wl_resource_destroy(resource);
}
static const struct ext_workspace_manager_v1_interface manager_impl = {
.commit = manager_handle_commit,
.stop = manager_handle_stop,
};
static void
manager_instance_resource_destroy(struct wl_resource *resource)
{
struct lab_wl_resource_addon *addon = wl_resource_get_user_data(resource);
if (addon) {
lab_resource_addon_destroy(addon);
wl_resource_set_user_data(resource, NULL);
}
wl_list_remove(wl_resource_get_link(resource));
}
static void
manager_handle_bind(struct wl_client *client, void *data,
uint32_t version, uint32_t id)
{
struct lab_ext_workspace_manager *manager = data;
struct wl_resource *manager_resource = wl_resource_create(client,
&ext_workspace_manager_v1_interface,
version, id);
if (!manager_resource) {
wl_client_post_no_memory(client);
return;
}
struct lab_wl_resource_addon *addon =
lab_resource_addon_create(/* session context*/ NULL);
addon->data = manager;
wl_resource_set_implementation(manager_resource, &manager_impl,
addon, manager_instance_resource_destroy);
wl_list_insert(&manager->resources, wl_resource_get_link(manager_resource));
struct lab_ext_workspace *workspace;
struct lab_ext_workspace_group *group;
wl_list_for_each(group, &manager->groups, link) {
/* Create group resource */
struct wl_resource *group_resource =
group_resource_create(group, manager_resource, addon->ctx);
ext_workspace_manager_v1_send_workspace_group(manager_resource, group_resource);
group_send_state(group, group_resource);
wl_list_for_each(workspace, &manager->workspaces, link) {
if (workspace->group != group) {
continue;
}
/* Create workspace resource for the current group */
struct wl_resource *workspace_resource = workspace_resource_create(
workspace, manager_resource, addon->ctx);
ext_workspace_manager_v1_send_workspace(
manager_resource, workspace_resource);
workspace_send_initial_state(workspace, workspace_resource);
workspace_send_state(workspace, workspace_resource);
ext_workspace_group_handle_v1_send_workspace_enter(
group_resource, workspace_resource);
}
}
/* Create workspace resource for workspaces not belonging to any group */
wl_list_for_each(workspace, &manager->workspaces, link) {
if (workspace->group) {
continue;
}
struct wl_resource *workspace_resource =
workspace_resource_create(workspace, manager_resource, addon->ctx);
ext_workspace_manager_v1_send_workspace(manager_resource, workspace_resource);
workspace_send_initial_state(workspace, workspace_resource);
workspace_send_state(workspace, workspace_resource);
}
ext_workspace_manager_v1_send_done(manager_resource);
}
static void
manager_handle_display_destroy(struct wl_listener *listener, void *data)
{
struct lab_ext_workspace_manager *manager =
wl_container_of(listener, manager, on.display_destroy);
struct lab_ext_workspace_group *group, *tmp;
wl_list_for_each_safe(group, tmp, &manager->groups, link) {
lab_ext_workspace_group_destroy(group);
}
struct lab_ext_workspace *ws, *ws_tmp;
wl_list_for_each_safe(ws, ws_tmp, &manager->workspaces, link) {
lab_ext_workspace_destroy(ws);
}
if (manager->idle_source) {
wl_event_source_remove(manager->idle_source);
}
wl_list_remove(&manager->on.display_destroy.link);
wl_global_destroy(manager->global);
free(manager);
}
/* Manager internal helpers */
static void
manager_idle_send_done(void *data)
{
struct lab_ext_workspace_manager *manager = data;
struct lab_ext_workspace *workspace;
wl_list_for_each(workspace, &manager->workspaces, link) {
if (workspace->state != workspace->state_pending) {
workspace->state = workspace->state_pending;
workspace_send_state(workspace, /*target*/ NULL);
}
}
struct wl_resource *resource;
wl_resource_for_each(resource, &manager->resources) {
ext_workspace_manager_v1_send_done(resource);
}
manager->idle_source = NULL;
}
/* Internal API */
void
ext_manager_schedule_done_event(struct lab_ext_workspace_manager *manager)
{
if (manager->idle_source) {
return;
}
if (!manager->event_loop) {
return;
}
manager->idle_source = wl_event_loop_add_idle(
manager->event_loop, manager_idle_send_done, manager);
}
static void
send_group_workspace_event(struct lab_ext_workspace_group *group,
struct lab_ext_workspace *workspace,
void (*fn)(struct wl_resource *grp_res, struct wl_resource *ws_res))
{
struct lab_wl_resource_addon *workspace_addon, *group_addon;
struct wl_resource *workspace_resource, *group_resource;
wl_resource_for_each(workspace_resource, &workspace->resources) {
workspace_addon = wl_resource_get_user_data(workspace_resource);
wl_resource_for_each(group_resource, &group->resources) {
group_addon = wl_resource_get_user_data(group_resource);
if (group_addon->ctx != workspace_addon->ctx) {
continue;
}
fn(group_resource, workspace_resource);
break;
}
}
}
/* Public API */
struct lab_ext_workspace_manager *
lab_ext_workspace_manager_create(struct wl_display *display, uint32_t caps, uint32_t version)
{
assert(display);
assert(version <= EXT_WORKSPACE_V1_VERSION);
struct lab_ext_workspace_manager *manager = znew(*manager);
manager->global = wl_global_create(display,
&ext_workspace_manager_v1_interface,
version, manager, manager_handle_bind);
if (!manager->global) {
free(manager);
return NULL;
}
manager->caps = caps;
manager->event_loop = wl_display_get_event_loop(display);
manager->on.display_destroy.notify = manager_handle_display_destroy;
wl_display_add_destroy_listener(display, &manager->on.display_destroy);
wl_list_init(&manager->groups);
wl_list_init(&manager->workspaces);
wl_list_init(&manager->resources);
return manager;
}
struct lab_ext_workspace_group *
lab_ext_workspace_group_create(struct lab_ext_workspace_manager *manager)
{
assert(manager);
struct lab_ext_workspace_group *group = znew(*group);
group->manager = manager;
group->capabilities = manager->caps & WS_CAP_GRP_ALL;
wl_list_init(&group->outputs);
wl_list_init(&group->resources);
wl_signal_init(&group->events.create_workspace);
wl_signal_init(&group->events.destroy);
wl_list_append(&manager->groups, &group->link);
struct wl_resource *resource, *tmp;
wl_resource_for_each_safe(resource, tmp, &manager->resources) {
struct lab_wl_resource_addon *addon = wl_resource_get_user_data(resource);
assert(addon && addon->ctx);
struct wl_resource *group_resource =
group_resource_create(group, resource, addon->ctx);
ext_workspace_manager_v1_send_workspace_group(resource, group_resource);
group_send_state(group, group_resource);
}
ext_manager_schedule_done_event(manager);
return group;
}
void
lab_ext_workspace_group_destroy(struct lab_ext_workspace_group *group)
{
assert(group);
wl_signal_emit_mutable(&group->events.destroy, NULL);
struct lab_ext_workspace *workspace;
wl_list_for_each(workspace, &group->manager->workspaces, link) {
if (workspace->group == group) {
send_group_workspace_event(group, workspace,
ext_workspace_group_handle_v1_send_workspace_leave);
workspace->group = NULL;
}
}
struct wl_resource *resource, *res_tmp;
wl_resource_for_each_safe(resource, res_tmp, &group->resources) {
ext_workspace_group_handle_v1_send_removed(resource);
struct lab_wl_resource_addon *addon = wl_resource_get_user_data(resource);
lab_resource_addon_destroy(addon);
wl_resource_set_user_data(resource, NULL);
wl_list_remove(wl_resource_get_link(resource));
wl_list_init(wl_resource_get_link(resource));
}
/* Cancel pending transaction ops involving this group */
struct lab_transaction_op *trans_op, *trans_op_tmp;
wl_resource_for_each(resource, &group->manager->resources) {
struct lab_wl_resource_addon *addon = wl_resource_get_user_data(resource);
lab_transaction_for_each_safe(trans_op, trans_op_tmp, addon->ctx) {
if (trans_op->src == group || trans_op->data == group) {
lab_transaction_op_destroy(trans_op);
}
}
}
ext_manager_schedule_done_event(group->manager);
wl_list_remove(&group->link);
free(group);
}
struct lab_ext_workspace *
lab_ext_workspace_create(struct lab_ext_workspace_manager *manager, const char *id)
{
assert(manager);
struct lab_ext_workspace *workspace = znew(*workspace);
/*
* Ensures we are sending workspace->state_pending on the done event,
* regardless if the compositor has changed any state in between here
* and the scheduled done event or not.
*
* Without this we might have to send the state twice, first here and
* then again in the scheduled done event when there were any changes.
*/
workspace->state = WS_STATE_INVALID;
workspace->capabilities = (manager->caps & WS_CAP_WS_ALL) >> 16;
workspace->manager = manager;
if (id) {
workspace->id = xstrdup(id);
}
wl_list_init(&workspace->resources);
wl_array_init(&workspace->coordinates);
wl_signal_init(&workspace->events.activate);
wl_signal_init(&workspace->events.deactivate);
wl_signal_init(&workspace->events.remove);
wl_signal_init(&workspace->events.assign);
wl_signal_init(&workspace->events.destroy);
wl_list_append(&manager->workspaces, &workspace->link);
/* Notify clients */
struct lab_wl_resource_addon *manager_addon;
struct wl_resource *manager_resource, *workspace_resource;
wl_resource_for_each(manager_resource, &manager->resources) {
manager_addon = wl_resource_get_user_data(manager_resource);
workspace_resource = workspace_resource_create(
workspace, manager_resource, manager_addon->ctx);
ext_workspace_manager_v1_send_workspace(
manager_resource, workspace_resource);
workspace_send_initial_state(workspace, workspace_resource);
}
ext_manager_schedule_done_event(manager);
return workspace;
}
void
lab_ext_workspace_assign_to_group(struct lab_ext_workspace *workspace,
struct lab_ext_workspace_group *group)
{
assert(workspace);
if (workspace->group == group) {
return;
}
if (workspace->group) {
/* Send leave event for the old group */
send_group_workspace_event(workspace->group, workspace,
ext_workspace_group_handle_v1_send_workspace_leave);
ext_manager_schedule_done_event(workspace->manager);
}
workspace->group = group;
if (!group) {
return;
}
/* Send enter event for the new group */
send_group_workspace_event(group, workspace,
ext_workspace_group_handle_v1_send_workspace_enter);
ext_manager_schedule_done_event(workspace->manager);
}
void
lab_ext_workspace_set_name(struct lab_ext_workspace *workspace, const char *name)
{
assert(workspace);
assert(name);
if (!workspace->name || strcmp(workspace->name, name)) {
free(workspace->name);
workspace->name = xstrdup(name);
struct wl_resource *resource;
wl_resource_for_each(resource, &workspace->resources) {
ext_workspace_handle_v1_send_name(resource, workspace->name);
}
}
ext_manager_schedule_done_event(workspace->manager);
}
void
lab_ext_workspace_set_active(struct lab_ext_workspace *workspace, bool enabled)
{
assert(workspace);
workspace_set_state(workspace, EXT_WORKSPACE_HANDLE_V1_STATE_ACTIVE, enabled);
}
void
lab_ext_workspace_set_urgent(struct lab_ext_workspace *workspace, bool enabled)
{
assert(workspace);
workspace_set_state(workspace, EXT_WORKSPACE_HANDLE_V1_STATE_URGENT, enabled);
}
void
lab_ext_workspace_set_hidden(struct lab_ext_workspace *workspace, bool enabled)
{
assert(workspace);
workspace_set_state(workspace, EXT_WORKSPACE_HANDLE_V1_STATE_HIDDEN, enabled);
}
void
lab_ext_workspace_set_coordinates(struct lab_ext_workspace *workspace,
struct wl_array *coordinates)
{
assert(workspace);
assert(coordinates);
wl_array_release(&workspace->coordinates);
wl_array_init(&workspace->coordinates);
wl_array_copy(&workspace->coordinates, coordinates);
struct wl_resource *resource;
wl_resource_for_each(resource, &workspace->resources) {
ext_workspace_handle_v1_send_coordinates(resource, &workspace->coordinates);
}
ext_manager_schedule_done_event(workspace->manager);
}
void
lab_ext_workspace_destroy(struct lab_ext_workspace *workspace)
{
assert(workspace);
wl_signal_emit_mutable(&workspace->events.destroy, NULL);
if (workspace->group) {
send_group_workspace_event(workspace->group, workspace,
ext_workspace_group_handle_v1_send_workspace_leave);
}
struct wl_resource *ws_res, *ws_tmp;
wl_resource_for_each_safe(ws_res, ws_tmp, &workspace->resources) {
ext_workspace_handle_v1_send_removed(ws_res);
struct lab_wl_resource_addon *ws_addon = wl_resource_get_user_data(ws_res);
lab_resource_addon_destroy(ws_addon);
wl_resource_set_user_data(ws_res, NULL);
wl_list_remove(wl_resource_get_link(ws_res));
wl_list_init(wl_resource_get_link(ws_res));
}
ext_manager_schedule_done_event(workspace->manager);
/* Cancel pending transaction ops involving this workspace */
struct wl_resource *resource;
struct lab_transaction_op *trans_op, *trans_op_tmp;
wl_resource_for_each(resource, &workspace->manager->resources) {
struct lab_wl_resource_addon *addon = wl_resource_get_user_data(resource);
lab_transaction_for_each_safe(trans_op, trans_op_tmp, addon->ctx) {
if (trans_op->src == workspace) {
lab_transaction_op_destroy(trans_op);
}
}
}
wl_list_remove(&workspace->link);
wl_array_release(&workspace->coordinates);
zfree(workspace->id);
zfree(workspace->name);
free(workspace);
}

View file

@ -0,0 +1,4 @@
labwc_sources += files(
'ext-workspace.c',
'output.c',
)

View file

@ -0,0 +1,174 @@
// SPDX-License-Identifier: GPL-2.0-only
#include <wayland-server-core.h>
#include <wlr/types/wlr_output.h>
#include <wlr/util/log.h>
#include "common/mem.h"
#include "ext-workspace-v1-protocol.h"
#include "protocols/ext-workspace.h"
#include "protocols/ext-workspace-internal.h"
struct group_output {
struct wlr_output *wlr_output;
struct lab_ext_workspace_group *group;
struct {
struct wl_listener group_destroy;
struct wl_listener output_bind;
struct wl_listener output_destroy;
} on;
struct wl_list link;
};
/* Internal helpers */
static void
group_output_send_event(struct wl_list *group_resources, struct wl_list *output_resources,
void (*notifier)(struct wl_resource *group, struct wl_resource *output))
{
struct wl_client *client;
struct wl_resource *group_resource, *output_resource;
wl_resource_for_each(group_resource, group_resources) {
client = wl_resource_get_client(group_resource);
wl_resource_for_each(output_resource, output_resources) {
if (wl_resource_get_client(output_resource) == client) {
notifier(group_resource, output_resource);
}
}
}
}
static void
group_output_destroy(struct group_output *group_output)
{
group_output_send_event(
&group_output->group->resources,
&group_output->wlr_output->resources,
ext_workspace_group_handle_v1_send_output_leave);
ext_manager_schedule_done_event(group_output->group->manager);
wl_list_remove(&group_output->link);
wl_list_remove(&group_output->on.group_destroy.link);
wl_list_remove(&group_output->on.output_bind.link);
wl_list_remove(&group_output->on.output_destroy.link);
free(group_output);
}
/* Event handlers */
static void
handle_output_bind(struct wl_listener *listener, void *data)
{
struct group_output *group_output =
wl_container_of(listener, group_output, on.output_bind);
struct wlr_output_event_bind *event = data;
struct wl_client *client = wl_resource_get_client(event->resource);
bool sent = false;
struct wl_resource *group_resource;
wl_resource_for_each(group_resource, &group_output->group->resources) {
if (wl_resource_get_client(group_resource) == client) {
ext_workspace_group_handle_v1_send_output_enter(
group_resource, event->resource);
sent = true;
}
}
if (!sent) {
return;
}
struct wl_resource *manager_resource;
struct wl_list *manager_resources = &group_output->group->manager->resources;
wl_resource_for_each(manager_resource, manager_resources) {
if (wl_resource_get_client(manager_resource) == client) {
ext_workspace_manager_v1_send_done(manager_resource);
}
}
}
static void
handle_output_destroy(struct wl_listener *listener, void *data)
{
struct group_output *group_output =
wl_container_of(listener, group_output, on.output_destroy);
group_output_destroy(group_output);
}
static void
handle_group_destroy(struct wl_listener *listener, void *data)
{
struct group_output *group_output =
wl_container_of(listener, group_output, on.group_destroy);
group_output_destroy(group_output);
}
/* Internal API*/
void
ext_group_output_send_initial_state(struct lab_ext_workspace_group *group,
struct wl_resource *group_resource)
{
struct group_output *group_output;
struct wl_resource *output_resource;
struct wl_client *client = wl_resource_get_client(group_resource);
wl_list_for_each(group_output, &group->outputs, link) {
wl_resource_for_each(output_resource, &group_output->wlr_output->resources) {
if (wl_resource_get_client(output_resource) == client) {
ext_workspace_group_handle_v1_send_output_enter(
group_resource, output_resource);
}
}
}
}
/* Public API */
void
lab_ext_workspace_group_output_enter(struct lab_ext_workspace_group *group,
struct wlr_output *wlr_output)
{
struct group_output *group_output;
wl_list_for_each(group_output, &group->outputs, link) {
if (group_output->wlr_output == wlr_output) {
return;
}
}
group_output = znew(*group_output);
group_output->wlr_output = wlr_output;
group_output->group = group;
group_output->on.group_destroy.notify = handle_group_destroy;
wl_signal_add(&group->events.destroy, &group_output->on.group_destroy);
group_output->on.output_bind.notify = handle_output_bind;
wl_signal_add(&wlr_output->events.bind, &group_output->on.output_bind);
group_output->on.output_destroy.notify = handle_output_destroy;
wl_signal_add(&wlr_output->events.destroy, &group_output->on.output_destroy);
wl_list_insert(&group->outputs, &group_output->link);
group_output_send_event(
&group_output->group->resources,
&group_output->wlr_output->resources,
ext_workspace_group_handle_v1_send_output_enter);
ext_manager_schedule_done_event(group->manager);
}
void
lab_ext_workspace_group_output_leave(struct lab_ext_workspace_group *group,
struct wlr_output *wlr_output)
{
struct group_output *tmp;
struct group_output *group_output = NULL;
wl_list_for_each(tmp, &group->outputs, link) {
if (tmp->wlr_output == wlr_output) {
group_output = tmp;
break;
}
}
if (!group_output) {
wlr_log(WLR_ERROR, "output %s was never entered", wlr_output->name);
return;
}
group_output_destroy(group_output);
}

View file

@ -3,3 +3,4 @@ labwc_sources += files(
)
subdir('cosmic_workspaces')
subdir('ext-workspace')