From 8ba066a1a5c7fc60a2df146ab1753b4027ee0bee Mon Sep 17 00:00:00 2001 From: Simon Long Date: Wed, 15 May 2024 23:07:23 +0100 Subject: [PATCH] Add screen magnifier This adds a screen magnifier which can be controlled with the `ZoomIn` / `ZoomOut` and `ToggleMagnify` actions. It scales up part of the rendered framebuffer so the magnification may end up looking blurry depending on the magnification scale. PR #1774 --- docs/labwc-actions.5.scd | 12 ++ docs/labwc-config.5.scd | 33 ++++ docs/labwc-theme.5.scd | 6 + docs/rc.xml.all | 19 +++ include/config/rcxml.h | 7 + include/magnifier.h | 23 +++ include/theme.h | 4 + src/action.c | 16 ++ src/common/scene-helpers.c | 20 ++- src/config/rcxml.c | 20 +++ src/magnifier.c | 302 +++++++++++++++++++++++++++++++++++++ src/meson.build | 1 + src/theme.c | 11 ++ 13 files changed, 473 insertions(+), 1 deletion(-) create mode 100644 include/magnifier.h create mode 100644 src/magnifier.c diff --git a/docs/labwc-actions.5.scd b/docs/labwc-actions.5.scd index b7e7db68..4f0492b6 100644 --- a/docs/labwc-actions.5.scd +++ b/docs/labwc-actions.5.scd @@ -264,6 +264,18 @@ Actions are used in menus and keyboard/mouse bindings. decorations (including those for which the server-side titlebar has been hidden) are not eligible for shading. +** + Toggle the screen magnifier on or off at the last magnification level + used. + +**++ +** + Increase or decrease the magnification level for the screen magnifier. + If the magnifier is currently off, ZoomIn will enable it at the lowest + magnification, equal to (1 + the magnifier increment set in the theme). + If the magnifier is on and at the lowest magnification, ZoomOut will + turn it off. + ** If used as the only action for a binding: clear an earlier defined binding. diff --git a/docs/labwc-config.5.scd b/docs/labwc-config.5.scd index d86634a8..6b92c6ee 100644 --- a/docs/labwc-config.5.scd +++ b/docs/labwc-config.5.scd @@ -909,6 +909,39 @@ situation. option has been exposed for unusual use-cases. It is equivalent to Openbox's ``. Default is 250 ms. +## MAGNIFIER + +``` + + 400 + 400 + 2 + 0.2 + true + +``` + +** + Width of magnifier window in pixels. Default is 400. + Set to -1 to use fullscreen magnifier. + +** + Height of magnifier window in pixels. Default is 400. + Set to -1 to use fullscreen magnifier. + +** + Initial number of times by which magnified image is scaled. Value + is the default at boot; can be modified at run-time in a keyboard + or mouse binding by calling 'ZoomIn' or 'ZoomOut'. Default is x2.0. + +** + Step by which magnification changes on each call to 'ZoomIn' or + 'ZoomOut'. Default is 0.2. + +** [yes|no|default] + Whether to apply a bilinear filter to the magnified image, or + just to use nearest-neighbour. Default is true - bilinear filtered. + ## ENVIRONMENT VARIABLES *XCURSOR_THEME* and *XCURSOR_SIZE* are supported to set cursor theme diff --git a/docs/labwc-theme.5.scd b/docs/labwc-theme.5.scd index 51bdc5c3..7611d479 100644 --- a/docs/labwc-theme.5.scd +++ b/docs/labwc-theme.5.scd @@ -277,6 +277,12 @@ elements are not listed here, but are supported. *window.inactive.border.color*. This is obsolete, but supported for backward compatibility as some themes still contain it. +*magnifier.border.width* + Width of magnifier window border in pixels. Default is 1. + +*magnifier.border.color* + Color of the magnfier window border. Default is #ff0000 (red). + # BUTTONS The images used for the titlebar icons are referred to as buttons. diff --git a/docs/rc.xml.all b/docs/rc.xml.all index 8b3d8e7c..2cccecd6 100644 --- a/docs/rc.xml.all +++ b/docs/rc.xml.all @@ -589,4 +589,23 @@ 250 + + + + 400 + 400 + 2.0 + 0.2 + true + + diff --git a/include/config/rcxml.h b/include/config/rcxml.h index 210d92f2..80916ea1 100644 --- a/include/config/rcxml.h +++ b/include/config/rcxml.h @@ -140,6 +140,13 @@ struct rcxml { /* Menu */ unsigned int menu_ignore_button_release_period; + + /* Magnifier */ + int mag_width; + int mag_height; + float mag_scale; + float mag_increment; + bool mag_filter; }; extern struct rcxml rc; diff --git a/include/magnifier.h b/include/magnifier.h new file mode 100644 index 00000000..407fb04d --- /dev/null +++ b/include/magnifier.h @@ -0,0 +1,23 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +#ifndef LABWC_MAGNIFIER_H +#define LABWC_MAGNIFIER_H + +#include +#include + +struct server; +struct output; + +enum magnify_dir { + MAGNIFY_INCREASE, + MAGNIFY_DECREASE +}; + +void magnify_toggle(struct server *server); +void magnify_set_scale(struct server *server, enum magnify_dir dir); +bool output_wants_magnification(struct output *output); +void magnify(struct output *output, struct wlr_buffer *output_buffer, + struct wlr_box *damage); +bool is_magnify_on(void); + +#endif /* LABWC_MAGNIFIER_H */ diff --git a/include/theme.h b/include/theme.h index 204a978e..50a69f6f 100644 --- a/include/theme.h +++ b/include/theme.h @@ -139,6 +139,10 @@ struct theme { /* not set in rc.xml/themerc, but derived from font & padding_height */ int osd_window_switcher_item_height; + + /* magnifier */ + float mag_border_color[4]; + int mag_border_width; }; struct server; diff --git a/src/action.c b/src/action.c index bf94e0a0..68086bf9 100644 --- a/src/action.c +++ b/src/action.c @@ -15,6 +15,7 @@ #include "common/string-helpers.h" #include "debug.h" #include "labwc.h" +#include "magnifier.h" #include "menu/menu.h" #include "osd.h" #include "output-virtual.h" @@ -110,6 +111,9 @@ enum action_type { ACTION_TYPE_SHADE, ACTION_TYPE_UNSHADE, ACTION_TYPE_TOGGLE_SHADE, + ACTION_TYPE_TOGGLE_MAGNIFY, + ACTION_TYPE_ZOOM_IN, + ACTION_TYPE_ZOOM_OUT }; const char *action_names[] = { @@ -163,6 +167,9 @@ const char *action_names[] = { "Shade", "Unshade", "ToggleShade", + "ToggleMagnify", + "ZoomIn", + "ZoomOut", NULL }; @@ -1046,6 +1053,15 @@ actions_run(struct view *activator, struct server *server, view_set_shade(view, false); } break; + case ACTION_TYPE_TOGGLE_MAGNIFY: + magnify_toggle(server); + break; + case ACTION_TYPE_ZOOM_IN: + magnify_set_scale(server, MAGNIFY_INCREASE); + break; + case ACTION_TYPE_ZOOM_OUT: + magnify_set_scale(server, MAGNIFY_DECREASE); + break; case ACTION_TYPE_INVALID: wlr_log(WLR_ERROR, "Not executing unknown action"); break; diff --git a/src/common/scene-helpers.c b/src/common/scene-helpers.c index 6c7ed9ea..f09cd9c0 100644 --- a/src/common/scene-helpers.c +++ b/src/common/scene-helpers.c @@ -5,6 +5,7 @@ #include #include #include "common/scene-helpers.h" +#include "magnifier.h" struct wlr_surface * lab_wlr_surface_from_node(struct wlr_scene_node *node) @@ -44,16 +45,29 @@ lab_wlr_scene_output_commit(struct wlr_scene_output *scene_output) assert(scene_output); struct wlr_output *wlr_output = scene_output->output; struct wlr_output_state *state = &wlr_output->pending; + struct output *output = wlr_output->data; + bool wants_magnification = output_wants_magnification(output); + static bool last_mag = false; if (!wlr_output->needs_frame && !pixman_region32_not_empty( - &scene_output->damage_ring.current)) { + &scene_output->damage_ring.current) && !wants_magnification + && last_mag != is_magnify_on()) { return false; } + + last_mag = is_magnify_on(); + if (!wlr_scene_output_build_state(scene_output, state, NULL)) { wlr_log(WLR_ERROR, "Failed to build output state for %s", wlr_output->name); return false; } + + struct wlr_box additional_damage = {0}; + if (state->buffer && is_magnify_on()) { + magnify(output, state->buffer, &additional_damage); + } + if (!wlr_output_commit(wlr_output)) { wlr_log(WLR_INFO, "Failed to commit output %s", wlr_output->name); @@ -66,5 +80,9 @@ lab_wlr_scene_output_commit(struct wlr_scene_output *scene_output) * again. */ wlr_damage_ring_rotate(&scene_output->damage_ring); + + if (!wlr_box_empty(&additional_damage)) { + wlr_damage_ring_add_box(&scene_output->damage_ring, &additional_damage); + } return true; } diff --git a/src/config/rcxml.c b/src/config/rcxml.c index 1e268f47..c2863819 100644 --- a/src/config/rcxml.c +++ b/src/config/rcxml.c @@ -1036,6 +1036,16 @@ entry(xmlNode *node, char *nodename, char *content) } } else if (!strcasecmp(nodename, "ignoreButtonReleasePeriod.menu")) { rc.menu_ignore_button_release_period = atoi(content); + } else if (!strcasecmp(nodename, "width.magnifier")) { + rc.mag_width = atoi(content); + } else if (!strcasecmp(nodename, "height.magnifier")) { + rc.mag_height = atoi(content); + } else if (!strcasecmp(nodename, "initScale.magnifier")) { + set_float(content, &rc.mag_scale); + } else if (!strcasecmp(nodename, "increment.magnifier")) { + set_float(content, &rc.mag_increment); + } else if (!strcasecmp(nodename, "useFilter.magnifier")) { + set_bool(content, &rc.mag_filter); } } @@ -1242,6 +1252,12 @@ rcxml_init(void) rc.workspace_config.min_nr_workspaces = 1; rc.menu_ignore_button_release_period = 250; + + rc.mag_width = 400; + rc.mag_height = 400; + rc.mag_scale = 2.0; + rc.mag_increment = 0.2; + rc.mag_filter = true; } static void @@ -1468,6 +1484,10 @@ post_processing(void) wlr_log(WLR_INFO, "load default window switcher fields"); load_default_window_switcher_fields(); } + + if (rc.mag_scale <= 0.0) { + rc.mag_scale = 1.0; + } } static void diff --git a/src/magnifier.c b/src/magnifier.c new file mode 100644 index 00000000..4c2097b7 --- /dev/null +++ b/src/magnifier.c @@ -0,0 +1,302 @@ +// SPDX-License-Identifier: GPL-2.0-only + +#include +#include +#include "magnifier.h" +#include "labwc.h" +#include "theme.h" +#include "common/macros.h" + +bool magnify_on; +double mag_scale = 0.0; + +#define CLAMP(in, lower, upper) MAX(MIN(in, upper), lower) + +void +magnify(struct output *output, struct wlr_buffer *output_buffer, struct wlr_box *damage) +{ + int width, height; + double x, y; + struct wlr_box border_box, dst_box; + struct wlr_fbox src_box; + bool fullscreen = false; + + /* Reuse a single scratch buffer */ + static struct wlr_buffer *tmp_buffer = NULL; + static struct wlr_texture *tmp_texture = NULL; + + /* TODO: This looks way too complicated to just get the used format */ + struct wlr_drm_format wlr_drm_format = {0}; + struct wlr_shm_attributes shm_attribs = {0}; + struct wlr_dmabuf_attributes dma_attribs = {0}; + if (wlr_buffer_get_dmabuf(output_buffer, &dma_attribs)) { + wlr_drm_format.format = dma_attribs.format; + wlr_drm_format.len = 1; + wlr_drm_format.modifiers = &dma_attribs.modifier; + } else if (wlr_buffer_get_shm(output_buffer, &shm_attribs)) { + wlr_drm_format.format = shm_attribs.format; + } else { + wlr_log(WLR_ERROR, "Failed to read buffer format"); + return; + } + + /* Fetch scale-adjusted cursor coordinates */ + struct server *server = output->server; + struct theme *theme = server->theme; + struct wlr_cursor *cursor = server->seat.cursor; + double ox = cursor->x; + double oy = cursor->y; + wlr_output_layout_output_coords(server->output_layout, output->wlr_output, &ox, &oy); + ox *= output->wlr_output->scale; + oy *= output->wlr_output->scale; + if (rc.mag_width == -1 || rc.mag_height == -1) { + fullscreen = true; + } + if ((ox < 0 || oy < 0 || ox >= output_buffer->width || oy >= output_buffer->height) + && fullscreen) { + return; + } + + if (mag_scale == 0.0) { + mag_scale = rc.mag_scale; + } + if (mag_scale == 0.0) { + mag_scale = 1.0; + } + + if (fullscreen) { + width = output_buffer->width; + height = output_buffer->height; + x = 0; + y = 0; + } else { + width = rc.mag_width + 1; + height = rc.mag_height + 1; + x = ox - (rc.mag_width / 2.0); + y = oy - (rc.mag_height / 2.0); + } + double cropped_width = width; + double cropped_height = height; + double dst_x = 0; + double dst_y = 0; + + /* Ensure everything is kept within output boundaries */ + if (x < 0) { + cropped_width += x; + dst_x = x * -1; + x = 0; + } + if (y < 0) { + cropped_height += y; + dst_y = y * -1; + y = 0; + } + cropped_width = MIN(cropped_width, (double)output_buffer->width - x); + cropped_height = MIN(cropped_height, (double)output_buffer->height - y); + + /* (Re)create the temporary buffer if required */ + if (tmp_buffer && (tmp_buffer->width != width || tmp_buffer->height != height)) { + wlr_log(WLR_DEBUG, "tmp buffer size changed, dropping"); + assert(tmp_texture); + wlr_texture_destroy(tmp_texture); + wlr_buffer_drop(tmp_buffer); + tmp_buffer = NULL; + tmp_texture = NULL; + } + if (!tmp_buffer) { + tmp_buffer = wlr_allocator_create_buffer( + server->allocator, width, height, &wlr_drm_format); + } + if (!tmp_buffer) { + wlr_log(WLR_ERROR, "Failed to allocate temporary magnifier buffer"); + return; + } + + /* Paste the magnified result back into the output buffer */ + if (!tmp_texture) { + tmp_texture = wlr_texture_from_buffer(server->renderer, tmp_buffer); + } + if (!tmp_texture) { + wlr_log(WLR_ERROR, "Failed to allocate temporary texture"); + wlr_buffer_drop(tmp_buffer); + tmp_buffer = NULL; + return; + } + + /* Extract source region into temporary buffer */ + + struct wlr_render_pass *tmp_render_pass = wlr_renderer_begin_buffer_pass( + server->renderer, tmp_buffer, NULL); + + wlr_buffer_lock(output_buffer); + struct wlr_texture *output_texture = wlr_texture_from_buffer( + server->renderer, output_buffer); + if (!output_texture) { + goto cleanup; + } + + struct wlr_render_texture_options opts = { + .texture = output_texture, + .src_box = (struct wlr_fbox) { + x, y, cropped_width, cropped_height }, + .dst_box = (struct wlr_box) { + dst_x, dst_y, cropped_width, cropped_height }, + .alpha = NULL, + }; + wlr_render_pass_add_texture(tmp_render_pass, &opts); + if (!wlr_render_pass_submit(tmp_render_pass)) { + wlr_log(WLR_ERROR, "Failed to extract magnifier source region"); + wlr_texture_destroy(output_texture); + goto cleanup; + } + wlr_texture_destroy(output_texture); + + /* Render to the output buffer itself */ + tmp_render_pass = wlr_renderer_begin_buffer_pass( + server->renderer, output_buffer, NULL); + + /* Borders */ + if (fullscreen) { + border_box.x = 0; + border_box.y = 0; + border_box.width = width; + border_box.height = height; + } else { + border_box.x = ox - (width / 2 + theme->mag_border_width); + border_box.y = oy - (height / 2 + theme->mag_border_width); + border_box.width = (width + theme->mag_border_width * 2); + border_box.height = (height + theme->mag_border_width * 2); + struct wlr_render_rect_options bg_opts = { + .box = border_box, + .color = (struct wlr_render_color) { + .r = theme->mag_border_color[0], + .g = theme->mag_border_color[1], + .b = theme->mag_border_color[2], + .a = theme->mag_border_color[3] + }, + .clip = NULL, + }; + wlr_render_pass_add_rect(tmp_render_pass, &bg_opts); + } + + src_box.width = width / mag_scale; + src_box.height = height / mag_scale; + dst_box.width = width; + dst_box.height = height; + + if (fullscreen) { + src_box.x = CLAMP(ox - (ox / mag_scale), 0.0, + width * (mag_scale - 1.0) / mag_scale); + src_box.y = CLAMP(oy - (oy / mag_scale), 0.0, + height * (mag_scale - 1.0) / mag_scale); + dst_box.x = 0; + dst_box.y = 0; + } else { + src_box.x = width * (mag_scale - 1.0) / (2.0 * mag_scale); + src_box.y = height * (mag_scale - 1.0) / (2.0 * mag_scale); + dst_box.x = ox - (width / 2); + dst_box.y = oy - (height / 2); + } + + opts = (struct wlr_render_texture_options) { + .texture = tmp_texture, + .src_box = src_box, + .dst_box = dst_box, + .alpha = NULL, + .clip = NULL, + .filter_mode = rc.mag_filter ? WLR_SCALE_FILTER_BILINEAR + : WLR_SCALE_FILTER_NEAREST, + }; + wlr_render_pass_add_texture(tmp_render_pass, &opts); + if (!wlr_render_pass_submit(tmp_render_pass)) { + wlr_log(WLR_ERROR, "Failed to submit render pass"); + goto cleanup; + } + + /* And finally mark the extra damage */ + *damage = border_box; + damage->width += 1; + damage->height += 1; + +cleanup: + wlr_buffer_unlock(output_buffer); +} + +bool +output_wants_magnification(struct output *output) +{ + static double x = -1; + static double y = -1; + struct wlr_cursor *cursor = output->server->seat.cursor; + if (!magnify_on) { + x = -1; + y = -1; + return false; + } + if (cursor->x == x && cursor->y == y) { + return false; + } + x = cursor->x; + y = cursor->y; + return output_nearest_to_cursor(output->server) == output; +} + +/* + * Toggles magnification on and off + */ + +void +magnify_toggle(struct server *server) +{ + struct output *output = output_nearest_to_cursor(server); + + if (magnify_on) { + magnify_on = false; + } else { + magnify_on = true; + } + + if (output) { + wlr_output_schedule_frame(output->wlr_output); + } +} + +/* + * Increases and decreases magnification scale + */ + +void +magnify_set_scale(struct server *server, enum magnify_dir dir) +{ + struct output *output = output_nearest_to_cursor(server); + + if (dir == MAGNIFY_INCREASE) { + if (magnify_on) { + mag_scale += rc.mag_increment; + } else { + magnify_on = true; + mag_scale = 1.0 + rc.mag_increment; + } + } else { + if (magnify_on && mag_scale > 1.0 + rc.mag_increment) { + mag_scale -= rc.mag_increment; + } else { + magnify_on = false; + } + } + + if (output) { + wlr_output_schedule_frame(output->wlr_output); + } +} + +/* + * Report whether magnification is enabled + */ + +bool +is_magnify_on(void) +{ + return magnify_on; +} + diff --git a/src/meson.build b/src/meson.build index a7450d91..fab82b2a 100644 --- a/src/meson.build +++ b/src/meson.build @@ -9,6 +9,7 @@ labwc_sources = files( 'idle.c', 'interactive.c', 'layers.c', + 'magnifier.c', 'main.c', 'node.c', 'osd.c', diff --git a/src/theme.c b/src/theme.c index 2b10191f..d3d2b20f 100644 --- a/src/theme.c +++ b/src/theme.c @@ -571,6 +571,10 @@ theme_builtin(struct theme *theme, struct server *server) memset(theme->snapping_overlay_edge.border_color, 0, sizeof(theme->snapping_overlay_edge.border_color)); theme->snapping_overlay_edge.border_color[0][0] = FLT_MIN; + + /* magnifier */ + parse_hexstr("#ff0000", theme->mag_border_color); + theme->mag_border_width = 1; } static void @@ -826,6 +830,13 @@ entry(struct theme *theme, const char *key, const char *value) if (match_glob(key, "snapping.overlay.edge.border.color")) { parse_hexstrs(value, theme->snapping_overlay_edge.border_color); } + + if (match_glob(key, "magnifier.border.width")) { + theme->mag_border_width = atoi(value); + } + if (match_glob(key, "magnifier.border.color")) { + parse_hexstr(value, theme->mag_border_color); + } } static void -- 2.52.0