From d557623c348c882582d167cb9f45ab9ec8008214 Mon Sep 17 00:00:00 2001 From: Consolatis <35009135+Consolatis@users.noreply.github.com> Date: Wed, 15 Jun 2022 01:07:45 +0200 Subject: [PATCH 1/7] workspaces: Add workspaces.{c,h} --- include/config/rcxml.h | 5 + include/labwc.h | 12 ++ include/workspaces.h | 28 ++++ src/meson.build | 1 + src/workspaces.c | 371 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 417 insertions(+) create mode 100644 include/workspaces.h create mode 100644 src/workspaces.c diff --git a/include/config/rcxml.h b/include/config/rcxml.h index 99a2be46..41382de2 100644 --- a/include/config/rcxml.h +++ b/include/config/rcxml.h @@ -55,6 +55,11 @@ struct rcxml { /* cycle view (alt+tab) */ bool cycle_preview_contents; + + struct { + int popuptime; + struct wl_list workspaces; /* struct workspace.link */ + } workspace_config; }; extern struct rcxml rc; diff --git a/include/labwc.h b/include/labwc.h index 9e083a7e..83a30db0 100644 --- a/include/labwc.h +++ b/include/labwc.h @@ -87,6 +87,10 @@ struct seat { struct wlr_idle *wlr_idle; struct wlr_idle_inhibit_manager_v1 *wlr_idle_inhibit_manager; + /* Used to hide the workspace OSD after switching workspaces */ + struct wl_event_source *workspace_osd_timer; + bool workspace_osd_shown_by_modifier; + /* if set, views cannot receive focus */ struct wlr_layer_surface_v1 *focused_layer; @@ -143,9 +147,11 @@ struct seat { }; struct lab_data_buffer; +struct workspace; struct server { struct wl_display *wl_display; + struct wl_event_loop *wl_event_loop; /* Can be used for timer events */ struct wlr_renderer *renderer; struct wlr_allocator *allocator; struct wlr_backend *backend; @@ -194,6 +200,10 @@ struct server { /* Tree for built in menu */ struct wlr_scene_tree *menu_tree; + /* Workspaces */ + struct wl_list workspaces; /* struct workspace.link */ + struct workspace *workspace_current; + struct wl_list outputs; struct wl_listener new_output; struct wlr_output_layout *output_layout; @@ -234,6 +244,7 @@ struct output { struct wlr_scene_tree *layer_tree[LAB_NR_LAYERS]; struct wlr_scene_tree *layer_popup_tree; struct wlr_scene_tree *osd_tree; + struct wlr_scene_buffer *workspace_osd; struct wlr_box usable_area; struct lab_data_buffer *osd_buffer; @@ -276,6 +287,7 @@ struct view { const struct view_impl *impl; struct wl_list link; struct output *output; + struct workspace *workspace; union { struct wlr_xdg_surface *xdg_surface; diff --git a/include/workspaces.h b/include/workspaces.h new file mode 100644 index 00000000..8b16bd05 --- /dev/null +++ b/include/workspaces.h @@ -0,0 +1,28 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +#ifndef __LABWC_WORKSPACES_H +#define __LABWC_WORKSPACES_H + +struct seat; +struct view; +struct server; +struct wl_list; + +/* Double use: as config in config/rcxml.c and as instance in workspaces.c */ +struct workspace { + struct wl_list link; /* struct server.workspaces + struct rcxml.workspace_config.workspaces */ + struct server *server; + + char *name; + struct wlr_scene_tree *tree; +}; + + +void workspaces_init(struct server *server); +void workspaces_switch_to(struct workspace *target); +void workspaces_send_to(struct view *view, struct workspace *target); +void workspaces_destroy(struct server *server); +void workspaces_osd_hide(struct seat *seat); +struct workspace * workspaces_find(struct workspace *anchor, const char *name); + +#endif /* __LABWC_WORKSPACES_H */ diff --git a/src/meson.build b/src/meson.build index aedda90f..7771fcab 100644 --- a/src/meson.build +++ b/src/meson.build @@ -20,6 +20,7 @@ labwc_sources = files( 'theme.c', 'view.c', 'view-impl.c', + 'workspaces.c', 'xdg.c', 'xdg-deco.c', 'xdg-popup.c', diff --git a/src/workspaces.c b/src/workspaces.c new file mode 100644 index 00000000..d481c63d --- /dev/null +++ b/src/workspaces.c @@ -0,0 +1,371 @@ +// SPDX-License-Identifier: GPL-2.0-only +#define _POSIX_C_SOURCE 200809L +#include +#include +#include +#include +#include +#include +#include +#include "labwc.h" +#include "common/font.h" +#include "common/zfree.h" +#include "workspaces.h" + +/* 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; +} + +/* + * TODO: set_source and draw_border are straight up copies from src/osd.c + * find some proper place for them instead of duplicating stuff. + */ +static void +set_source(cairo_t *cairo, float *c) +{ + cairo_set_source_rgba(cairo, c[0], c[1], c[2], c[3]); +} + +static void +draw_border(cairo_t *cairo, double width, double height, double line_width) +{ + cairo_save(cairo); + + double x, y, w, h; + /* The anchor point of a line is in the center */ + x = y = line_width / 2; + w = width - line_width; + h = height - line_width; + cairo_set_line_width(cairo, line_width); + cairo_rectangle(cairo, x, y, w, h); + cairo_stroke(cairo); + + cairo_restore(cairo); +} + +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 = 20; + uint16_t rect_width = 20; + struct font font = { + .name = rc.font_name_osd, + .size = rc.font_size_osd, + }; + + /* Dimensions */ + size_t workspace_count = wl_list_length(&server->workspaces); + 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 * 3 + rect_height + font_height(&font); + + cairo_t *cairo; + cairo_surface_t *surface; + struct workspace *workspace; + + struct output *output; + wl_list_for_each(output, &server->outputs, link) { + struct lab_data_buffer *buffer = buffer_create_cairo(width, height, + output->wlr_output->scale, true); + if (!buffer) { + wlr_log(WLR_ERROR, "Failed to allocate buffer for workspace OSD"); + continue; + } + + cairo = buffer->cairo; + + /* Background */ + set_source(cairo, theme->osd_bg_color); + cairo_rectangle(cairo, 0, 0, width, height); + cairo_fill(cairo); + + /* Border */ + set_source(cairo, theme->osd_border_color); + draw_border(cairo, width, height, theme->osd_border_width); + + uint16_t x = (width - marker_width) / 2; + wl_list_for_each(workspace, &server->workspaces, link) { + bool active = workspace == server->workspace_current; + set_source(cairo, server->theme->osd_label_text_color); + cairo_rectangle(cairo, x, margin, + rect_width - padding, rect_height); + cairo_stroke(cairo); + if (active) { + cairo_rectangle(cairo, x, margin, + rect_width - padding, rect_height); + cairo_fill(cairo); + } + x += rect_width + padding; + } + + /* Text */ + set_source(cairo, server->theme->osd_label_text_color); + PangoLayout *layout = pango_cairo_create_layout(cairo); + pango_layout_set_width(layout, (width - 2 * margin) * PANGO_SCALE); + pango_layout_set_ellipsize(layout, PANGO_ELLIPSIZE_END); + + PangoFontDescription *desc = pango_font_description_new(); + pango_font_description_set_family(desc, font.name); + pango_font_description_set_size(desc, font.size * PANGO_SCALE); + pango_layout_set_font_description(layout, desc); + + /* Center workspace indicator on the x axis */ + x = font_width(&font, server->workspace_current->name); + x = (width - x) / 2; + cairo_move_to(cairo, x, margin * 2 + rect_height); + //pango_font_description_set_weight(desc, PANGO_WEIGHT_BOLD); + pango_layout_set_font_description(layout, desc); + pango_font_description_free(desc); + pango_layout_set_text(layout, server->workspace_current->name, -1); + pango_cairo_show_layout(cairo, layout); + + g_object_unref(layout); + surface = cairo_get_target(cairo); + cairo_surface_flush(surface); + + 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->unscaled_width, buffer->unscaled_height); + + /* And finally drop the buffer so it will get destroyed on OSD hide */ + wlr_buffer_drop(&buffer->base); + } +} + +/* Internal API */ +static void +add_workspace(struct server *server, const char *name) +{ + struct workspace *workspace = calloc(1, sizeof(struct workspace)); + workspace->server = server; + workspace->name = strdup(name); + workspace->tree = wlr_scene_tree_create(server->view_tree); + wl_list_insert(server->workspaces.prev, &workspace->link); + if (!server->workspace_current) { + server->workspace_current = workspace; + } else { + wlr_scene_node_set_enabled(&workspace->tree->node, false); + } +} + +static struct workspace * +get_prev(struct workspace *current, struct wl_list *workspaces) +{ + struct wl_list *target_link = current->link.prev; + if (target_link == workspaces) { + /* Current workspace is the first one, 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) +{ + struct wl_list *target_link = current->link.next; + if (target_link == workspaces) { + /* Current workspace is the last one, roll over */ + target_link = target_link->next; + } + return wl_container_of(target_link, current, link); +} + +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) { + wlr_scene_node_set_enabled(&output->workspace_osd->node, true); + } + struct wlr_keyboard *keyboard = &server->seat.keyboard_group->keyboard; + if (keyboard_any_modifiers_pressed(keyboard)) { + /* 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) +{ + wl_list_init(&server->workspaces); + + struct workspace *conf; + wl_list_for_each(conf, &rc.workspace_config.workspaces, link) { + add_workspace(server, conf->name); + } +} + +void +workspaces_switch_to(struct workspace *target) +{ + assert(target); + if (target == target->server->workspace_current) { + return; + } + + /* Disable the old workspace */ + wlr_scene_node_set_enabled( + &target->server->workspace_current->tree->node, false); + + /* Enable the new workspace */ + wlr_scene_node_set_enabled(&target->tree->node, true); + + /* Make sure new views will spawn on the new workspace */ + target->server->workspace_current = target; + + /** + * Make sure we are focusing what the user sees. + * + * TODO: This is an issue for always-on-top views as they will + * loose keyboard focus once switching to another workspace. + */ + desktop_focus_topmost_mapped_view(target->server); + + /* And finally show the OSD */ + _osd_show(target->server); +} + +void +workspaces_send_to(struct view *view, struct workspace *target) +{ + assert(view); + assert(target); + if (view->workspace == target) { + return; + } + wlr_scene_node_reparent(&view->scene_tree->node, target->tree); + view->workspace = target; +} + + +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) { + 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; +} + +struct workspace * +workspaces_find(struct workspace *anchor, const char *name) +{ + 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; + + if (wants_index) { + wl_list_for_each(target, workspaces, link) { + if (wants_index == ++index) { + return target; + } + } + } else if (!strcasecmp(name, "left")) { + return get_prev(anchor, workspaces); + } else if (!strcasecmp(name, "right")) { + return get_next(anchor, workspaces); + } 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; +} + +void +workspaces_destroy(struct server *server) +{ + struct workspace *workspace, *tmp; + wl_list_for_each_safe(workspace, tmp, &server->workspaces, link) { + wlr_scene_node_destroy(&workspace->tree->node); + zfree(workspace->name); + wl_list_remove(&workspace->link); + free(workspace); + } + assert(wl_list_empty(&server->workspaces)); +} From ae2fa1571b3fcfe8f7f0daa14394813b9bd72292 Mon Sep 17 00:00:00 2001 From: Consolatis <35009135+Consolatis@users.noreply.github.com> Date: Wed, 15 Jun 2022 02:07:22 +0200 Subject: [PATCH 2/7] workspaces: Implement config parsing --- docs/labwc-config.5.scd | 13 +++++++++++++ docs/rc.xml.all | 26 ++++++++++++++++++++++++++ src/config/rcxml.c | 24 ++++++++++++++++++++++++ 3 files changed, 63 insertions(+) diff --git a/docs/labwc-config.5.scd b/docs/labwc-config.5.scd index 73c74bfe..034fb324 100644 --- a/docs/labwc-config.5.scd +++ b/docs/labwc-config.5.scd @@ -88,6 +88,7 @@ The rest of this man page describes configuration options. Raise window to top when focused. Default is no. ## WINDOW SNAPPING + ** The distance in pixels from the edge of an ouput for window Move operations to trigger SnapToEdge. A range of 0 disables window snapping. @@ -96,6 +97,18 @@ The rest of this man page describes configuration options. ** [yes|no] Maximize window if Move operation ends on the top edge. Default is yes. +## WORKSPACES + +** + Define workspaces. A workspace covers all outputs. The OSD only shows + windows on the current workspace. Workspaces can be switched to with + GoToDesktop and windows can be moved with SendToDesktop. See + labwc-actions(5) for more information about their arguments. + +** + Define the timeout after which to hide the workspace OSD. + A setting of 0 disables the OSD. Default is 1000 ms. + ## THEME ** diff --git a/docs/rc.xml.all b/docs/rc.xml.all index 5acc5bcc..1468b9f7 100644 --- a/docs/rc.xml.all +++ b/docs/rc.xml.all @@ -39,6 +39,32 @@ yes + + + + 1000 + + Default + + +