labwc/src/workspaces.c
2025-08-21 16:55:25 +09:00

644 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_scene.h>
#include "buffer.h"
#include "common/font.h"
#include "common/graphic-helpers.h"
#include "common/list.h"
#include "common/mem.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(struct server *server)
{
struct theme *theme = 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(&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, &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, &server->workspaces.all, link) {
bool active = workspace == server->workspaces.current;
set_cairo_color(cairo, 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, 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, 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, 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 = wlr_scene_buffer_create(
&server->scene->tree, NULL);
}
/* Position the whole thing */
struct wlr_box output_box;
wlr_output_layout_get_box(output->server->output_layout,
output->wlr_output, &output_box);
int lx = output->usable_area.x
+ (output->usable_area.width - width) / 2
+ output_box.x;
int ly = output->usable_area.y
+ (output->usable_area.height - height) / 2
+ output_box.y;
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);
}
}
/* 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(struct server *server, const char *name)
{
struct workspace *workspace = znew(*workspace);
workspace->server = server;
workspace->name = xstrdup(name);
workspace->tree = wlr_scene_tree_create(server->view_tree);
wl_list_append(&server->workspaces.all, &workspace->link);
if (!server->workspaces.current) {
server->workspaces.current = workspace;
} else {
wlr_scene_node_set_enabled(&workspace->tree->node, false);
}
bool active = server->workspaces.current == workspace;
/* cosmic */
workspace->cosmic_workspace = lab_cosmic_workspace_create(server->workspaces.cosmic_group);
lab_cosmic_workspace_set_name(workspace->cosmic_workspace, name);
lab_cosmic_workspace_set_active(workspace->cosmic_workspace, active);
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(
server->workspaces.ext_manager, /*id*/ NULL);
lab_ext_workspace_assign_to_group(workspace->ext_workspace, server->workspaces.ext_group);
lab_ext_workspace_set_name(workspace->ext_workspace, name);
lab_ext_workspace_set_active(workspace->ext_workspace, active);
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 server *server)
{
struct view *view;
for_each_view(view, &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 server *server = current->server;
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, server)) {
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(struct server *server)
{
if (!rc.workspace_config.popuptime) {
return;
}
_osd_update(server);
struct output *output;
wl_list_for_each(output, &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(&server->seat)) {
/* Hidden by release of all modifiers */
server->seat.workspace_osd_shown_by_modifier = true;
} else {
/* Hidden by timer */
if (!server->seat.workspace_osd_timer) {
server->seat.workspace_osd_timer = wl_event_loop_add_timer(
server->wl_event_loop, _osd_handle_timeout, &server->seat);
}
wl_event_source_timer_update(server->seat.workspace_osd_timer,
rc.workspace_config.popuptime);
}
}
/* Public API */
void
workspaces_init(struct server *server)
{
server->workspaces.cosmic_manager = lab_cosmic_workspace_manager_create(
server->wl_display, /* capabilities */ CW_CAP_WS_ACTIVATE,
COSMIC_WORKSPACES_VERSION);
server->workspaces.ext_manager = lab_ext_workspace_manager_create(
server->wl_display, /* capabilities */ WS_CAP_WS_ACTIVATE,
EXT_WORKSPACES_VERSION);
server->workspaces.cosmic_group = lab_cosmic_workspace_group_create(
server->workspaces.cosmic_manager);
server->workspaces.ext_group = lab_ext_workspace_group_create(
server->workspaces.ext_manager);
wl_list_init(&server->workspaces.all);
struct workspace *conf;
wl_list_for_each(conf, &rc.workspace_config.workspaces, link) {
add_workspace(server, conf->name);
}
}
/*
* 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);
struct server *server = target->server;
if (target == server->workspaces.current) {
return;
}
/* Disable the old workspace */
wlr_scene_node_set_enabled(
&server->workspaces.current->tree->node, false);
lab_cosmic_workspace_set_active(
server->workspaces.current->cosmic_workspace, false);
lab_ext_workspace_set_active(
server->workspaces.current->ext_workspace, false);
/* Move Omnipresent views to new workspace */
struct view *view;
enum lab_view_criteria criteria =
LAB_VIEW_CRITERIA_CURRENT_WORKSPACE;
for_each_view_reverse(view, &server->views, criteria) {
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 */
server->workspaces.last = server->workspaces.current;
/* Make sure new views will spawn on the new workspace */
server->workspaces.current = target;
struct view *grabbed_view = server->grabbed_view;
if (grabbed_view && !view_is_always_on_top(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 or always-on-top view.
*
* TODO: Decouple always-on-top views from the omnipresent state.
* One option for that would be to create a new scene tree
* as child of every workspace tree and then reparent a-o-t
* windows to that one. Combined with adjusting the condition
* below that should take care of the issue.
*/
if (update_focus) {
struct view *active_view = server->active_view;
if (!active_view || (!active_view->visible_on_all_workspaces
&& !view_is_always_on_top(active_view))) {
desktop_focus_topmost_view(server);
}
}
/* And finally show the OSD */
_osd_show(server);
/*
* Make sure we are not carrying around a
* cursor image from the previous desktop
*/
cursor_update_focus(server);
/* Ensure that only currently visible fullscreen windows hide the top layer */
desktop_update_top_layer_visibility(server);
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;
struct server *server = seat->server;
wl_list_for_each(output, &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(server);
}
struct workspace *
workspaces_find(struct workspace *anchor, const char *name, bool wrap)
{
assert(anchor);
if (!name) {
return NULL;
}
size_t index = 0;
struct workspace *target;
size_t wants_index = parse_workspace_index(name);
struct wl_list *workspaces = &anchor->server->workspaces.all;
if (wants_index) {
wl_list_for_each(target, workspaces, link) {
if (wants_index == ++index) {
return target;
}
}
} else if (!strcasecmp(name, "current")) {
return anchor;
} else if (!strcasecmp(name, "last")) {
return anchor->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);
} else {
wl_list_for_each(target, workspaces, link) {
if (!strcasecmp(target->name, name)) {
return target;
}
}
}
wlr_log(WLR_ERROR, "Workspace '%s' not found", name);
return NULL;
}
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(struct server *server)
{
/*
* 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 *actual_workspace_link = server->workspaces.all.next;
struct workspace *configured_workspace;
wl_list_for_each(configured_workspace,
&rc.workspace_config.workspaces, link) {
struct workspace *actual_workspace = wl_container_of(
actual_workspace_link, actual_workspace, link);
if (actual_workspace_link == &server->workspaces.all) {
/* # of configured workspaces increased */
wlr_log(WLR_DEBUG, "Adding workspace \"%s\"",
configured_workspace->name);
add_workspace(server, configured_workspace->name);
continue;
}
if (strcmp(actual_workspace->name, configured_workspace->name)) {
/* Workspace is renamed */
wlr_log(WLR_DEBUG, "Renaming workspace \"%s\" to \"%s\"",
actual_workspace->name, configured_workspace->name);
free(actual_workspace->name);
actual_workspace->name = xstrdup(configured_workspace->name);
lab_cosmic_workspace_set_name(
actual_workspace->cosmic_workspace, actual_workspace->name);
lab_ext_workspace_set_name(
actual_workspace->ext_workspace, actual_workspace->name);
}
actual_workspace_link = actual_workspace_link->next;
}
if (actual_workspace_link == &server->workspaces.all) {
return;
}
/* # of configured workspaces decreased */
overlay_hide(&server->seat);
struct workspace *first_workspace =
wl_container_of(server->workspaces.all.next, first_workspace, link);
while (actual_workspace_link != &server->workspaces.all) {
struct workspace *actual_workspace = wl_container_of(
actual_workspace_link, actual_workspace, link);
wlr_log(WLR_DEBUG, "Destroying workspace \"%s\"",
actual_workspace->name);
struct view *view;
wl_list_for_each(view, &server->views, link) {
if (view->workspace == actual_workspace) {
view_move_to_workspace(view, first_workspace);
}
}
if (server->workspaces.current == actual_workspace) {
workspaces_switch_to(first_workspace,
/* update_focus */ true);
}
if (server->workspaces.last == actual_workspace) {
server->workspaces.last = first_workspace;
}
actual_workspace_link = actual_workspace_link->next;
destroy_workspace(actual_workspace);
}
}
void
workspaces_destroy(struct server *server)
{
struct workspace *workspace, *tmp;
wl_list_for_each_safe(workspace, tmp, &server->workspaces.all, link) {
destroy_workspace(workspace);
}
assert(wl_list_empty(&server->workspaces.all));
}