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
This commit is contained in:
Simon Long 2024-05-15 23:07:23 +01:00 committed by GitHub
parent ad15c0474d
commit 8ba066a1a5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 473 additions and 1 deletions

View 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.

View 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

View 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.

View file

@ -589,4 +589,23 @@
<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>

View 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;

23
include/magnifier.h Normal file
View file

@ -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 */

View 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;

View 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;

View 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;
}

View 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

302
src/magnifier.c Normal file
View file

@ -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;
}

View file

@ -9,6 +9,7 @@ labwc_sources = files(
'idle.c',
'interactive.c',
'layers.c',
'magnifier.c',
'main.c',
'node.c',
'osd.c',

View 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