labwc/src/workspaces.c
John Lindgren cb49bddf63 tree-wide: auto-replace of (struct server *)
#!/bin/bash
    read -r -d '' EXPRS << EOF
    s/xwayland->server/xwayland->svr/g;

    s/\t*struct server \*server;\n//g;
    s/\t*struct server \*server =.*?;\n//gs;
    s/\t*.* = ([a-z_]*->)*server[;,]\n//g;
    s/\{\n\n/\{\n/g;
    s/\n\n+/\n\n/g;

    s/\(\s*struct server \*server\)/(void)/g;
    s/\(\s*struct server \*server,\s*/(/g;
    s/,\s*struct server \*server\)/)/g;
    s/,\s*struct server \*server,\s*/, /g;

    s/\(\s*([a-z_]*->)*server\)/()/g;
    s/\(\s*([a-z_]*->)*server,\s*/(/g;
    s/,\s*([a-z_]*->)*server\)/)/g;
    s/,\s*([a-z_]*->)*server,\s*/, /g;

    s/([a-z_]*->)*server->/g_server./g;

    s/xwayland->svr/xwayland->server/g;
    EOF

    find src include \( -name \*.c -o -name \*.h \) -exec \
        perl -0777 -i -pe "$EXPRS" \{\} \;
2026-03-21 21:35:33 +00:00

661 lines
19 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// SPDX-License-Identifier: GPL-2.0-only
#define _POSIX_C_SOURCE 200809L
#include "workspaces.h"
#include <assert.h>
#include <cairo.h>
#include <pango/pangocairo.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <wlr/types/wlr_output_layout.h>
#include <wlr/types/wlr_scene.h>
#include "buffer.h"
#include "common/font.h"
#include "common/graphic-helpers.h"
#include "common/list.h"
#include "common/mem.h"
#include "common/scene-helpers.h"
#include "config/rcxml.h"
#include "input/keyboard.h"
#include "labwc.h"
#include "output.h"
#include "protocols/cosmic-workspaces.h"
#include "protocols/ext-workspace.h"
#include "theme.h"
#include "view.h"
#define COSMIC_WORKSPACES_VERSION 1
#define EXT_WORKSPACES_VERSION 1
/* Internal helpers */
static size_t
parse_workspace_index(const char *name)
{
/*
* We only want to get positive numbers which span the whole string.
*
* More detailed requirement:
* .---------------.--------------.
* | Input | Return value |
* |---------------+--------------|
* | "2nd desktop" | 0 |
* | "-50" | 0 |
* | "0" | 0 |
* | "124" | 124 |
* | "1.24" | 0 |
* `------------------------------´
*
* As atoi() happily parses any numbers until it hits a non-number we
* can't really use it for this case. Instead, we use strtol() combined
* with further checks for the endptr (remaining non-number characters)
* and returned negative numbers.
*/
long index;
char *endptr;
errno = 0;
index = strtol(name, &endptr, 10);
if (errno || *endptr != '\0' || index < 0) {
return 0;
}
return index;
}
static void
_osd_update(void)
{
struct theme *theme = g_server.theme;
/* Settings */
uint16_t margin = 10;
uint16_t padding = 2;
uint16_t rect_height = theme->osd_workspace_switcher_boxes_height;
uint16_t rect_width = theme->osd_workspace_switcher_boxes_width;
bool hide_boxes = theme->osd_workspace_switcher_boxes_width == 0 ||
theme->osd_workspace_switcher_boxes_height == 0;
/* Dimensions */
size_t workspace_count = wl_list_length(&g_server.workspaces.all);
uint16_t marker_width = workspace_count * (rect_width + padding) - padding;
uint16_t width = margin * 2 + (marker_width < 200 ? 200 : marker_width);
uint16_t height = margin * (hide_boxes ? 2 : 3) + rect_height + font_height(&rc.font_osd);
cairo_t *cairo;
cairo_surface_t *surface;
struct workspace *workspace;
struct output *output;
wl_list_for_each(output, &g_server.outputs, link) {
if (!output_is_usable(output)) {
continue;
}
struct lab_data_buffer *buffer = buffer_create_cairo(width, height,
output->wlr_output->scale);
if (!buffer) {
wlr_log(WLR_ERROR, "Failed to allocate buffer for workspace OSD");
continue;
}
cairo = cairo_create(buffer->surface);
/* Background */
set_cairo_color(cairo, theme->osd_bg_color);
cairo_rectangle(cairo, 0, 0, width, height);
cairo_fill(cairo);
/* Border */
set_cairo_color(cairo, theme->osd_border_color);
struct wlr_fbox border_fbox = {
.width = width,
.height = height,
};
draw_cairo_border(cairo, border_fbox, theme->osd_border_width);
/* Boxes */
uint16_t x;
if (!hide_boxes) {
x = (width - marker_width) / 2;
wl_list_for_each(workspace, &g_server.workspaces.all, link) {
bool active = workspace == g_server.workspaces.current;
set_cairo_color(cairo, g_server.theme->osd_label_text_color);
struct wlr_fbox fbox = {
.x = x,
.y = margin,
.width = rect_width,
.height = rect_height,
};
draw_cairo_border(cairo, fbox,
theme->osd_workspace_switcher_boxes_border_width);
if (active) {
cairo_rectangle(cairo, x, margin,
rect_width, rect_height);
cairo_fill(cairo);
}
x += rect_width + padding;
}
}
/* Text */
set_cairo_color(cairo, g_server.theme->osd_label_text_color);
PangoLayout *layout = pango_cairo_create_layout(cairo);
pango_context_set_round_glyph_positions(pango_layout_get_context(layout), false);
pango_layout_set_ellipsize(layout, PANGO_ELLIPSIZE_END);
/* Center workspace indicator on the x axis */
int req_width = font_width(&rc.font_osd, g_server.workspaces.current->name);
req_width = MIN(req_width, width - 2 * margin);
x = (width - req_width) / 2;
if (!hide_boxes) {
cairo_move_to(cairo, x, margin * 2 + rect_height);
} else {
cairo_move_to(cairo, x, (height - font_height(&rc.font_osd)) / 2.0);
}
PangoFontDescription *desc = font_to_pango_desc(&rc.font_osd);
//pango_font_description_set_weight(desc, PANGO_WEIGHT_BOLD);
pango_layout_set_font_description(layout, desc);
pango_layout_set_width(layout, req_width * PANGO_SCALE);
pango_font_description_free(desc);
pango_layout_set_text(layout, g_server.workspaces.current->name, -1);
pango_cairo_show_layout(cairo, layout);
g_object_unref(layout);
surface = cairo_get_target(cairo);
cairo_surface_flush(surface);
cairo_destroy(cairo);
if (!output->workspace_osd) {
output->workspace_osd = lab_wlr_scene_buffer_create(
&g_server.scene->tree, NULL);
}
/* Position the whole thing */
struct wlr_box output_box;
wlr_output_layout_get_box(g_server.output_layout,
output->wlr_output, &output_box);
int lx = output_box.x + (output_box.width - width) / 2;
int ly = output_box.y + (output_box.height - height) / 2;
wlr_scene_node_set_position(&output->workspace_osd->node, lx, ly);
wlr_scene_buffer_set_buffer(output->workspace_osd, &buffer->base);
wlr_scene_buffer_set_dest_size(output->workspace_osd,
buffer->logical_width, buffer->logical_height);
/* And finally drop the buffer so it will get destroyed on OSD hide */
wlr_buffer_drop(&buffer->base);
}
}
static struct workspace *
workspace_find_by_name(const char *name)
{
struct workspace *workspace;
/* by index */
size_t parsed_index = parse_workspace_index(name);
if (parsed_index) {
size_t index = 0;
wl_list_for_each(workspace, &g_server.workspaces.all, link) {
if (parsed_index == ++index) {
return workspace;
}
}
}
/* by name */
wl_list_for_each(workspace, &g_server.workspaces.all, link) {
if (!strcmp(workspace->name, name)) {
return workspace;
}
}
wlr_log(WLR_ERROR, "Workspace '%s' not found", name);
return NULL;
}
/* cosmic workspace handlers */
static void
handle_cosmic_workspace_activate(struct wl_listener *listener, void *data)
{
struct workspace *workspace = wl_container_of(listener, workspace, on_cosmic.activate);
workspaces_switch_to(workspace, /* update_focus */ true);
wlr_log(WLR_INFO, "cosmic activating workspace %s", workspace->name);
}
/* ext workspace handlers */
static void
handle_ext_workspace_activate(struct wl_listener *listener, void *data)
{
struct workspace *workspace = wl_container_of(listener, workspace, on_ext.activate);
workspaces_switch_to(workspace, /* update_focus */ true);
wlr_log(WLR_INFO, "ext activating workspace %s", workspace->name);
}
/* Internal API */
static void
add_workspace(const char *name)
{
struct workspace *workspace = znew(*workspace);
workspace->name = xstrdup(name);
workspace->tree = lab_wlr_scene_tree_create(g_server.workspace_tree);
workspace->view_trees[VIEW_LAYER_ALWAYS_ON_BOTTOM] =
lab_wlr_scene_tree_create(workspace->tree);
workspace->view_trees[VIEW_LAYER_NORMAL] =
lab_wlr_scene_tree_create(workspace->tree);
workspace->view_trees[VIEW_LAYER_ALWAYS_ON_TOP] =
lab_wlr_scene_tree_create(workspace->tree);
wl_list_append(&g_server.workspaces.all, &workspace->link);
wlr_scene_node_set_enabled(&workspace->tree->node, false);
/* cosmic */
workspace->cosmic_workspace = lab_cosmic_workspace_create(g_server.workspaces.cosmic_group);
lab_cosmic_workspace_set_name(workspace->cosmic_workspace, name);
workspace->on_cosmic.activate.notify = handle_cosmic_workspace_activate;
wl_signal_add(&workspace->cosmic_workspace->events.activate,
&workspace->on_cosmic.activate);
/* ext */
workspace->ext_workspace = lab_ext_workspace_create(
g_server.workspaces.ext_manager, /*id*/ NULL);
lab_ext_workspace_assign_to_group(workspace->ext_workspace, g_server.workspaces.ext_group);
lab_ext_workspace_set_name(workspace->ext_workspace, name);
workspace->on_ext.activate.notify = handle_ext_workspace_activate;
wl_signal_add(&workspace->ext_workspace->events.activate,
&workspace->on_ext.activate);
}
static struct workspace *
get_prev(struct workspace *current, struct wl_list *workspaces, bool wrap)
{
struct wl_list *target_link = current->link.prev;
if (target_link == workspaces) {
/* Current workspace is the first one */
if (!wrap) {
return NULL;
}
/* Roll over */
target_link = target_link->prev;
}
return wl_container_of(target_link, current, link);
}
static struct workspace *
get_next(struct workspace *current, struct wl_list *workspaces, bool wrap)
{
struct wl_list *target_link = current->link.next;
if (target_link == workspaces) {
/* Current workspace is the last one */
if (!wrap) {
return NULL;
}
/* Roll over */
target_link = target_link->next;
}
return wl_container_of(target_link, current, link);
}
static bool
workspace_has_views(struct workspace *workspace)
{
struct view *view;
for_each_view(view, &g_server.views, LAB_VIEW_CRITERIA_NO_OMNIPRESENT) {
if (view->workspace == workspace) {
return true;
}
}
return false;
}
static struct workspace *
get_adjacent_occupied(struct workspace *current, struct wl_list *workspaces,
bool wrap, bool reverse)
{
struct wl_list *start = &current->link;
struct wl_list *link = reverse ? start->prev : start->next;
bool has_wrapped = false;
while (true) {
/* Handle list boundaries */
if (link == workspaces) {
if (!wrap) {
break; /* No wrapping allowed - stop searching */
}
if (has_wrapped) {
break; /* Already wrapped once - stop to prevent infinite loop */
}
/* Wrap around */
link = reverse ? workspaces->prev : workspaces->next;
has_wrapped = true;
continue;
}
/* Get the workspace */
struct workspace *target = wl_container_of(link, target, link);
/* Check if we've come full circle */
if (link == start) {
break;
}
/* Check if it's occupied (and not current) */
if (target != current && workspace_has_views(target)) {
return target;
}
/* Move to next/prev */
link = reverse ? link->prev : link->next;
}
return NULL; /* No occupied workspace found */
}
static struct workspace *
get_prev_occupied(struct workspace *current, struct wl_list *workspaces, bool wrap)
{
return get_adjacent_occupied(current, workspaces, wrap, true);
}
static struct workspace *
get_next_occupied(struct workspace *current, struct wl_list *workspaces, bool wrap)
{
return get_adjacent_occupied(current, workspaces, wrap, false);
}
static int
_osd_handle_timeout(void *data)
{
struct seat *seat = data;
workspaces_osd_hide(seat);
/* Don't re-check */
return 0;
}
static void
_osd_show(void)
{
if (!rc.workspace_config.popuptime) {
return;
}
_osd_update();
struct output *output;
wl_list_for_each(output, &g_server.outputs, link) {
if (output_is_usable(output) && output->workspace_osd) {
wlr_scene_node_set_enabled(&output->workspace_osd->node, true);
}
}
if (keyboard_get_all_modifiers(&g_server.seat)) {
/* Hidden by release of all modifiers */
g_server.seat.workspace_osd_shown_by_modifier = true;
} else {
/* Hidden by timer */
if (!g_server.seat.workspace_osd_timer) {
g_server.seat.workspace_osd_timer = wl_event_loop_add_timer(
g_server.wl_event_loop, _osd_handle_timeout, &g_server.seat);
}
wl_event_source_timer_update(g_server.seat.workspace_osd_timer,
rc.workspace_config.popuptime);
}
}
/* Public API */
void
workspaces_init(void)
{
g_server.workspaces.cosmic_manager = lab_cosmic_workspace_manager_create(
g_server.wl_display, /* capabilities */ CW_CAP_WS_ACTIVATE,
COSMIC_WORKSPACES_VERSION);
g_server.workspaces.ext_manager = lab_ext_workspace_manager_create(
g_server.wl_display, /* capabilities */ WS_CAP_WS_ACTIVATE,
EXT_WORKSPACES_VERSION);
g_server.workspaces.cosmic_group = lab_cosmic_workspace_group_create(
g_server.workspaces.cosmic_manager);
g_server.workspaces.ext_group = lab_ext_workspace_group_create(
g_server.workspaces.ext_manager);
wl_list_init(&g_server.workspaces.all);
struct workspace_config *conf;
wl_list_for_each(conf, &rc.workspace_config.workspaces, link) {
add_workspace(conf->name);
}
/*
* After adding workspaces, check if there is an initial workspace
* selected and set that as the initial workspace.
*/
char *initial_name = rc.workspace_config.initial_workspace_name;
struct workspace *initial = NULL;
struct workspace *first = wl_container_of(
g_server.workspaces.all.next, first, link);
if (initial_name) {
initial = workspace_find_by_name(initial_name);
}
if (!initial) {
initial = first;
}
g_server.workspaces.current = initial;
wlr_scene_node_set_enabled(&initial->tree->node, true);
lab_cosmic_workspace_set_active(initial->cosmic_workspace, true);
lab_ext_workspace_set_active(initial->ext_workspace, true);
}
/*
* update_focus should normally be set to true. It is set to false only
* when this function is called from desktop_focus_view(), in order to
* avoid unnecessary extra focus changes and possible recursion.
*/
void
workspaces_switch_to(struct workspace *target, bool update_focus)
{
assert(target);
if (target == g_server.workspaces.current) {
return;
}
/* Disable the old workspace */
wlr_scene_node_set_enabled(
&g_server.workspaces.current->tree->node, false);
lab_cosmic_workspace_set_active(
g_server.workspaces.current->cosmic_workspace, false);
lab_ext_workspace_set_active(
g_server.workspaces.current->ext_workspace, false);
/*
* Move Omnipresent views to new workspace.
* Not using for_each_view() since it skips views that
* view_is_focusable() returns false (e.g. Conky).
*/
struct view *view;
wl_list_for_each_reverse(view, &g_server.views, link) {
if (view->visible_on_all_workspaces) {
view_move_to_workspace(view, target);
}
}
/* Enable the new workspace */
wlr_scene_node_set_enabled(&target->tree->node, true);
/* Save the last visited workspace */
g_server.workspaces.last = g_server.workspaces.current;
/* Make sure new views will spawn on the new workspace */
g_server.workspaces.current = target;
struct view *grabbed_view = g_server.grabbed_view;
if (grabbed_view) {
view_move_to_workspace(grabbed_view, target);
}
/*
* Make sure we are focusing what the user sees. Only refocus if
* the focus is not already on an omnipresent view.
*/
if (update_focus) {
struct view *active_view = g_server.active_view;
if (!(active_view && active_view->visible_on_all_workspaces)) {
desktop_focus_topmost_view();
}
}
/* And finally show the OSD */
_osd_show();
/*
* Make sure we are not carrying around a
* cursor image from the previous desktop
*/
cursor_update_focus();
/* Ensure that only currently visible fullscreen windows hide the top layer */
desktop_update_top_layer_visibility();
lab_cosmic_workspace_set_active(target->cosmic_workspace, true);
lab_ext_workspace_set_active(target->ext_workspace, true);
}
void
workspaces_osd_hide(struct seat *seat)
{
assert(seat);
struct output *output;
wl_list_for_each(output, &g_server.outputs, link) {
if (!output->workspace_osd) {
continue;
}
wlr_scene_node_set_enabled(&output->workspace_osd->node, false);
wlr_scene_buffer_set_buffer(output->workspace_osd, NULL);
}
seat->workspace_osd_shown_by_modifier = false;
/* Update the cursor focus in case it was on top of the OSD before */
cursor_update_focus();
}
struct workspace *
workspaces_find(struct workspace *anchor, const char *name, bool wrap)
{
assert(anchor);
if (!name) {
return NULL;
}
struct wl_list *workspaces = &g_server.workspaces.all;
if (!strcasecmp(name, "current")) {
return anchor;
} else if (!strcasecmp(name, "last")) {
return g_server.workspaces.last;
} else if (!strcasecmp(name, "left")) {
return get_prev(anchor, workspaces, wrap);
} else if (!strcasecmp(name, "right")) {
return get_next(anchor, workspaces, wrap);
} else if (!strcasecmp(name, "left-occupied")) {
return get_prev_occupied(anchor, workspaces, wrap);
} else if (!strcasecmp(name, "right-occupied")) {
return get_next_occupied(anchor, workspaces, wrap);
}
return workspace_find_by_name(name);
}
static void
destroy_workspace(struct workspace *workspace)
{
wlr_scene_node_destroy(&workspace->tree->node);
zfree(workspace->name);
wl_list_remove(&workspace->link);
wl_list_remove(&workspace->on_cosmic.activate.link);
wl_list_remove(&workspace->on_ext.activate.link);
lab_cosmic_workspace_destroy(workspace->cosmic_workspace);
lab_ext_workspace_destroy(workspace->ext_workspace);
free(workspace);
}
void
workspaces_reconfigure(void)
{
/*
* Compare actual workspace list with the new desired configuration to:
* - Update names
* - Add workspaces if more workspaces are desired
* - Destroy workspaces if fewer workspace are desired
*/
struct wl_list *workspace_link = g_server.workspaces.all.next;
struct workspace_config *conf;
wl_list_for_each(conf, &rc.workspace_config.workspaces, link) {
struct workspace *workspace = wl_container_of(
workspace_link, workspace, link);
if (workspace_link == &g_server.workspaces.all) {
/* # of configured workspaces increased */
wlr_log(WLR_DEBUG, "Adding workspace \"%s\"",
conf->name);
add_workspace(conf->name);
continue;
}
if (strcmp(workspace->name, conf->name)) {
/* Workspace is renamed */
wlr_log(WLR_DEBUG, "Renaming workspace \"%s\" to \"%s\"",
workspace->name, conf->name);
xstrdup_replace(workspace->name, conf->name);
lab_cosmic_workspace_set_name(
workspace->cosmic_workspace, workspace->name);
lab_ext_workspace_set_name(
workspace->ext_workspace, workspace->name);
}
workspace_link = workspace_link->next;
}
if (workspace_link == &g_server.workspaces.all) {
return;
}
/* # of configured workspaces decreased */
overlay_finish(&g_server.seat);
struct workspace *first_workspace =
wl_container_of(g_server.workspaces.all.next, first_workspace, link);
while (workspace_link != &g_server.workspaces.all) {
struct workspace *workspace = wl_container_of(
workspace_link, workspace, link);
wlr_log(WLR_DEBUG, "Destroying workspace \"%s\"",
workspace->name);
struct view *view;
wl_list_for_each(view, &g_server.views, link) {
if (view->workspace == workspace) {
view_move_to_workspace(view, first_workspace);
}
}
if (g_server.workspaces.current == workspace) {
workspaces_switch_to(first_workspace,
/* update_focus */ true);
}
if (g_server.workspaces.last == workspace) {
g_server.workspaces.last = first_workspace;
}
workspace_link = workspace_link->next;
destroy_workspace(workspace);
}
}
void
workspaces_destroy(void)
{
struct workspace *workspace, *tmp;
wl_list_for_each_safe(workspace, tmp, &g_server.workspaces.all, link) {
destroy_workspace(workspace);
}
assert(wl_list_empty(&g_server.workspaces.all));
}