]> git.mdlowis.com Git - proto/labwc.git/commitdiff
Add screen magnifier
authorSimon Long <simon@raspberrypi.com>
Wed, 15 May 2024 22:07:23 +0000 (23:07 +0100)
committerGitHub <noreply@github.com>
Wed, 15 May 2024 22:07:23 +0000 (00:07 +0200)
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

13 files changed:
docs/labwc-actions.5.scd
docs/labwc-config.5.scd
docs/labwc-theme.5.scd
docs/rc.xml.all
include/config/rcxml.h
include/magnifier.h [new file with mode: 0644]
include/theme.h
src/action.c
src/common/scene-helpers.c
src/config/rcxml.c
src/magnifier.c [new file with mode: 0644]
src/meson.build
src/theme.c

index b7e7db683eb84dedf34100a1afe54665b4a55a2f..4f0492b627a99258544fe2424749b449537ea189 100644 (file)
@@ -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.
 
+*<action name="ToggleMagnify">*
+       Toggle the screen magnifier on or off at the last magnification level
+       used.
+
+*<action name="ZoomIn">*++
+*<action name="ZoomOut">*
+       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.
+
 *<action name="None" />*
        If used as the only action for a binding: clear an earlier defined
        binding.
index d86634a879ad2c80173e7d5b8a61027b2c07000f..6b92c6ee841268ee74439afb5e70a37eef09ad78 100644 (file)
@@ -909,6 +909,39 @@ situation.
        option has been exposed for unusual use-cases. It is equivalent to
        Openbox's `<hideDelay>`. Default is 250 ms.
 
+## MAGNIFIER
+
+```
+<magnifier>
+  <width>400</width>
+  <height>400</height>
+  <initScale>2</initScale>
+  <increment>0.2</increment>
+  <useFilter>true</useFilter>
+</magnifier>
+```
+
+*<magnifier><width>*
+       Width of magnifier window in pixels. Default is 400.
+       Set to -1 to use fullscreen magnifier.
+
+*<magnifier><height>*
+       Height of magnifier window in pixels. Default is 400.
+       Set to -1 to use fullscreen magnifier.
+
+*<magnifier><initScale>*
+       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.
+
+*<magnifier><increment>*
+       Step by which magnification changes on each call to 'ZoomIn' or
+       'ZoomOut'. Default is 0.2.
+
+*<magnifier><useFilter>* [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
index 51bdc5c38bcb6fff54939c8691deaa127606947a..7611d4794beec75013119bcf0d6e4be7d594dcba 100644 (file)
@@ -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.
index 8b3d8e7c4468bad10ebe48c5fbbeb0393bb3d229..2cccecd60f054434521c60a6c59a5dd3d0fea90c 100644 (file)
   <menu>
     <ignoreButtonReleasePeriod>250</ignoreButtonReleasePeriod>
   </menu>
+
+  <!--
+    Magnifier settings
+    'width' sets the width in pixels of the magnifier window.
+    'height' sets the height in pixels of the magnifier window.
+    'initScale' sets the initial magnification factor at boot.
+    'increment' sets the amount by which the magnification factor
+      changes when 'ZoomIn' or 'ZoomOut' are called.
+    'useFilter' sets whether to use a bilinear filter on the magnified
+      output or simply to take nearest pixel.
+  -->
+  <magnifier>
+    <width>400</width>
+    <height>400</height>
+    <initScale>2.0</initScale>
+    <increment>0.2</increment>
+    <useFilter>true</useFilter>
+  </magnifier>
+
 </labwc_config>
index 210d92f2c75c183c9048f40c16dd8c163eecc23a..80916ea1909181c5eb037616cca93ba8a6db9d8a 100644 (file)
@@ -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 (file)
index 0000000..407fb04
--- /dev/null
@@ -0,0 +1,23 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef LABWC_MAGNIFIER_H
+#define LABWC_MAGNIFIER_H
+
+#include <stdbool.h>
+#include <wayland-server-core.h>
+
+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 */
index 204a978ee4ecf127889d440598d02b2b9cf6028f..50a69f6f066bcf99ab58ecf1ed527febd0ec0c0b 100644 (file)
@@ -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;
index bf94e0a0baba65ac31aca9d758adcd02f773ba09..68086bf9eaf8aa68e0b28550d0a7b339d109a2b0 100644 (file)
@@ -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;
index 6c7ed9ea364dbb2e29e30292891797d7546bb828..f09cd9c0fda0b39527b58790b817e2fca4f0951a 100644 (file)
@@ -5,6 +5,7 @@
 #include <wlr/types/wlr_scene.h>
 #include <wlr/util/log.h>
 #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;
 }
index 1e268f474a2421148ab34a61c49d8ff91407bfe8..c2863819a7fb043d29f95694da4a843d0ec5a950 100644 (file)
@@ -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 (file)
index 0000000..4c2097b
--- /dev/null
@@ -0,0 +1,302 @@
+// SPDX-License-Identifier: GPL-2.0-only
+
+#include <assert.h>
+#include <wlr/types/wlr_output.h>
+#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;
+}
+
index a7450d9169c691b67038a77639a2e39d285dc50e..fab82b2af5df85833d67eba2a62a020b6b59d974 100644 (file)
@@ -9,6 +9,7 @@ labwc_sources = files(
   'idle.c',
   'interactive.c',
   'layers.c',
+  'magnifier.c',
   'main.c',
   'node.c',
   'osd.c',
index 2b10191f3f9ef0be34d7460fe835e2c7835dc496..d3d2b20fe81e4c7161e4dfff6e79936e6eaf94df 100644 (file)
@@ -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