swaylock/render.c
Manuel Stoeckl 10ab8b2740 Estimate buffer size in advance for render_frame
The size of the surface used to draw the indicator depends on
the extents of the text being drawn on and under the indicator.

This commit refactors the `render_frame` function so that the surface
size is computed before drawing; before, `render_frame` drew onto a
buffer, estimated the size the buffer should have had, and recursively
called itself to try again with the estimated size, if necessary. This
was done because Cairo's methods to estimate font and text size
require that a cairo_t context object and an associated cairo_surface
already have been set up. Since the surface size depends on the text
size, the natural way to use Cairo would have a circular dependency.

In order to compute sizes _before_ the buffer is created, this commit
adds a 1x1 surface and a matching `test_cairo` context which is set to
the same font and drawing parameters as the buffer that will be
created. Font/text extent measurements should give the same results as
for the final buffer.
2023-01-27 12:19:32 +01:00

350 lines
12 KiB
C

#include <math.h>
#include <stdlib.h>
#include <wayland-client.h>
#include "cairo.h"
#include "background-image.h"
#include "swaylock.h"
#define M_PI 3.14159265358979323846
const float TYPE_INDICATOR_RANGE = M_PI / 3.0f;
const float TYPE_INDICATOR_BORDER_THICKNESS = M_PI / 128.0f;
static void set_color_for_state(cairo_t *cairo, struct swaylock_state *state,
struct swaylock_colorset *colorset) {
if (state->auth_state == AUTH_STATE_VALIDATING) {
cairo_set_source_u32(cairo, colorset->verifying);
} else if (state->auth_state == AUTH_STATE_INVALID) {
cairo_set_source_u32(cairo, colorset->wrong);
} else if (state->auth_state == AUTH_STATE_CLEAR) {
cairo_set_source_u32(cairo, colorset->cleared);
} else {
if (state->xkb.caps_lock && state->args.show_caps_lock_indicator) {
cairo_set_source_u32(cairo, colorset->caps_lock);
} else if (state->xkb.caps_lock && !state->args.show_caps_lock_indicator &&
state->args.show_caps_lock_text) {
uint32_t inputtextcolor = state->args.colors.text.input;
state->args.colors.text.input = state->args.colors.text.caps_lock;
cairo_set_source_u32(cairo, colorset->input);
state->args.colors.text.input = inputtextcolor;
} else {
cairo_set_source_u32(cairo, colorset->input);
}
}
}
void render_frame_background(struct swaylock_surface *surface) {
struct swaylock_state *state = surface->state;
int buffer_width = surface->width * surface->scale;
int buffer_height = surface->height * surface->scale;
if (buffer_width == 0 || buffer_height == 0) {
return; // not yet configured
}
struct pool_buffer *buffer = get_next_buffer(state->shm,
surface->buffers, buffer_width, buffer_height);
if (buffer == NULL) {
return;
}
cairo_t *cairo = buffer->cairo;
cairo_set_antialias(cairo, CAIRO_ANTIALIAS_BEST);
cairo_save(cairo);
cairo_set_operator(cairo, CAIRO_OPERATOR_SOURCE);
cairo_set_source_u32(cairo, state->args.colors.background);
cairo_paint(cairo);
if (surface->image && state->args.mode != BACKGROUND_MODE_SOLID_COLOR) {
cairo_set_operator(cairo, CAIRO_OPERATOR_OVER);
render_background_image(cairo, surface->image,
state->args.mode, buffer_width, buffer_height);
}
cairo_restore(cairo);
cairo_identity_matrix(cairo);
wl_surface_set_buffer_scale(surface->surface, surface->scale);
wl_surface_attach(surface->surface, buffer->buffer, 0, 0);
wl_surface_damage_buffer(surface->surface, 0, 0, INT32_MAX, INT32_MAX);
wl_surface_commit(surface->surface);
}
static void configure_font_drawing(cairo_t *cairo, struct swaylock_state *state,
enum wl_output_subpixel subpixel, int arc_radius) {
cairo_font_options_t *fo = cairo_font_options_create();
cairo_font_options_set_hint_style(fo, CAIRO_HINT_STYLE_FULL);
cairo_font_options_set_antialias(fo, CAIRO_ANTIALIAS_SUBPIXEL);
cairo_font_options_set_subpixel_order(fo, to_cairo_subpixel_order(subpixel));
cairo_set_font_options(cairo, fo);
cairo_select_font_face(cairo, state->args.font,
CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL);
if (state->args.font_size > 0) {
cairo_set_font_size(cairo, state->args.font_size);
} else {
cairo_set_font_size(cairo, arc_radius / 3.0f);
}
cairo_font_options_destroy(fo);
}
void render_frame(struct swaylock_surface *surface) {
struct swaylock_state *state = surface->state;
// First, compute the text that will be drawn, if any, since this
// determines the size/positioning of the surface
char attempts[4]; // like i3lock: count no more than 999
char *text = NULL;
const char *layout_text = NULL;
if (state->args.show_indicator) {
switch (state->auth_state) {
case AUTH_STATE_VALIDATING:
text = "Verifying";
break;
case AUTH_STATE_INVALID:
text = "Wrong";
break;
case AUTH_STATE_CLEAR:
text = "Cleared";
break;
case AUTH_STATE_INPUT:
case AUTH_STATE_INPUT_NOP:
case AUTH_STATE_BACKSPACE:
// Caps Lock has higher priority
if (state->xkb.caps_lock && state->args.show_caps_lock_text) {
text = "Caps Lock";
} else if (state->args.show_failed_attempts &&
state->failed_attempts > 0) {
if (state->failed_attempts > 999) {
text = "999+";
} else {
snprintf(attempts, sizeof(attempts), "%d", state->failed_attempts);
text = attempts;
}
}
xkb_layout_index_t num_layout = xkb_keymap_num_layouts(state->xkb.keymap);
if (!state->args.hide_keyboard_layout &&
(state->args.show_keyboard_layout || num_layout > 1)) {
xkb_layout_index_t curr_layout = 0;
// advance to the first active layout (if any)
while (curr_layout < num_layout &&
xkb_state_layout_index_is_active(state->xkb.state,
curr_layout, XKB_STATE_LAYOUT_EFFECTIVE) != 1) {
++curr_layout;
}
// will handle invalid index if none are active
layout_text = xkb_keymap_layout_get_name(state->xkb.keymap, curr_layout);
}
break;
default:
break;
}
}
// Compute the size of the buffer needed
int arc_radius = state->args.radius * surface->scale;
int arc_thickness = state->args.thickness * surface->scale;
int buffer_diameter = (arc_radius + arc_thickness) * 2;
int buffer_width = buffer_diameter;
int buffer_height = buffer_diameter;
if (text || layout_text) {
cairo_set_antialias(state->test_cairo, CAIRO_ANTIALIAS_BEST);
configure_font_drawing(state->test_cairo, state, surface->subpixel, arc_radius);
if (text) {
cairo_text_extents_t extents;
cairo_text_extents(state->test_cairo, text, &extents);
if (buffer_width < extents.width) {
buffer_width = extents.width;
}
}
if (layout_text) {
cairo_text_extents_t extents;
cairo_font_extents_t fe;
double box_padding = 4.0 * surface->scale;
cairo_text_extents(state->test_cairo, layout_text, &extents);
cairo_font_extents(state->test_cairo, &fe);
buffer_height += fe.height + 2 * box_padding;
if (buffer_width < extents.width + 2 * box_padding) {
buffer_width = extents.width + 2 * box_padding;
}
}
}
// Ensure buffer size is multiple of buffer scale - required by protocol
buffer_height += surface->scale - (buffer_height % surface->scale);
buffer_width += surface->scale - (buffer_width % surface->scale);
int subsurf_xpos;
int subsurf_ypos;
// Center the indicator unless overridden by the user
if (state->args.override_indicator_x_position) {
subsurf_xpos = state->args.indicator_x_position -
buffer_width / (2 * surface->scale) + 2 / surface->scale;
} else {
subsurf_xpos = surface->width / 2 -
buffer_width / (2 * surface->scale) + 2 / surface->scale;
}
if (state->args.override_indicator_y_position) {
subsurf_ypos = state->args.indicator_y_position -
(state->args.radius + state->args.thickness);
} else {
subsurf_ypos = surface->height / 2 -
(state->args.radius + state->args.thickness);
}
struct pool_buffer *buffer = get_next_buffer(state->shm,
surface->indicator_buffers, buffer_width, buffer_height);
if (buffer == NULL) {
return;
}
// Render the buffer
cairo_t *cairo = buffer->cairo;
cairo_set_antialias(cairo, CAIRO_ANTIALIAS_BEST);
cairo_identity_matrix(cairo);
// Clear
cairo_save(cairo);
cairo_set_source_rgba(cairo, 0, 0, 0, 0);
cairo_set_operator(cairo, CAIRO_OPERATOR_SOURCE);
cairo_paint(cairo);
cairo_restore(cairo);
float type_indicator_border_thickness =
TYPE_INDICATOR_BORDER_THICKNESS * surface->scale;
if (state->args.show_indicator && (state->auth_state != AUTH_STATE_IDLE ||
state->args.indicator_idle_visible)) {
// Fill inner circle
cairo_set_line_width(cairo, 0);
cairo_arc(cairo, buffer_width / 2, buffer_diameter / 2,
arc_radius - arc_thickness / 2, 0, 2 * M_PI);
set_color_for_state(cairo, state, &state->args.colors.inside);
cairo_fill_preserve(cairo);
cairo_stroke(cairo);
// Draw ring
cairo_set_line_width(cairo, arc_thickness);
cairo_arc(cairo, buffer_width / 2, buffer_diameter / 2, arc_radius,
0, 2 * M_PI);
set_color_for_state(cairo, state, &state->args.colors.ring);
cairo_stroke(cairo);
// Draw a message
configure_font_drawing(cairo, state, surface->subpixel, arc_radius);
set_color_for_state(cairo, state, &state->args.colors.text);
if (text) {
cairo_text_extents_t extents;
cairo_font_extents_t fe;
double x, y;
cairo_text_extents(cairo, text, &extents);
cairo_font_extents(cairo, &fe);
x = (buffer_width / 2) -
(extents.width / 2 + extents.x_bearing);
y = (buffer_diameter / 2) +
(fe.height / 2 - fe.descent);
cairo_move_to(cairo, x, y);
cairo_show_text(cairo, text);
cairo_close_path(cairo);
cairo_new_sub_path(cairo);
}
// Typing indicator: Highlight random part on keypress
if (state->auth_state == AUTH_STATE_INPUT
|| state->auth_state == AUTH_STATE_BACKSPACE) {
static double highlight_start = 0;
highlight_start +=
(rand() % (int)(M_PI * 100)) / 100.0 + M_PI * 0.5;
cairo_arc(cairo, buffer_width / 2, buffer_diameter / 2,
arc_radius, highlight_start,
highlight_start + TYPE_INDICATOR_RANGE);
if (state->auth_state == AUTH_STATE_INPUT) {
if (state->xkb.caps_lock && state->args.show_caps_lock_indicator) {
cairo_set_source_u32(cairo, state->args.colors.caps_lock_key_highlight);
} else {
cairo_set_source_u32(cairo, state->args.colors.key_highlight);
}
} else {
if (state->xkb.caps_lock && state->args.show_caps_lock_indicator) {
cairo_set_source_u32(cairo, state->args.colors.caps_lock_bs_highlight);
} else {
cairo_set_source_u32(cairo, state->args.colors.bs_highlight);
}
}
cairo_stroke(cairo);
// Draw borders
cairo_set_source_u32(cairo, state->args.colors.separator);
cairo_arc(cairo, buffer_width / 2, buffer_diameter / 2,
arc_radius, highlight_start,
highlight_start + type_indicator_border_thickness);
cairo_stroke(cairo);
cairo_arc(cairo, buffer_width / 2, buffer_diameter / 2,
arc_radius, highlight_start + TYPE_INDICATOR_RANGE,
highlight_start + TYPE_INDICATOR_RANGE +
type_indicator_border_thickness);
cairo_stroke(cairo);
}
// Draw inner + outer border of the circle
set_color_for_state(cairo, state, &state->args.colors.line);
cairo_set_line_width(cairo, 2.0 * surface->scale);
cairo_arc(cairo, buffer_width / 2, buffer_diameter / 2,
arc_radius - arc_thickness / 2, 0, 2 * M_PI);
cairo_stroke(cairo);
cairo_arc(cairo, buffer_width / 2, buffer_diameter / 2,
arc_radius + arc_thickness / 2, 0, 2 * M_PI);
cairo_stroke(cairo);
// display layout text separately
if (layout_text) {
cairo_text_extents_t extents;
cairo_font_extents_t fe;
double x, y;
double box_padding = 4.0 * surface->scale;
cairo_text_extents(cairo, layout_text, &extents);
cairo_font_extents(cairo, &fe);
// upper left coordinates for box
x = (buffer_width / 2) - (extents.width / 2) - box_padding;
y = buffer_diameter;
// background box
cairo_rectangle(cairo, x, y,
extents.width + 2.0 * box_padding,
fe.height + 2.0 * box_padding);
cairo_set_source_u32(cairo, state->args.colors.layout_background);
cairo_fill_preserve(cairo);
// border
cairo_set_source_u32(cairo, state->args.colors.layout_border);
cairo_stroke(cairo);
// take font extents and padding into account
cairo_move_to(cairo,
x - extents.x_bearing + box_padding,
y + (fe.height - fe.descent) + box_padding);
cairo_set_source_u32(cairo, state->args.colors.layout_text);
cairo_show_text(cairo, layout_text);
cairo_new_sub_path(cairo);
}
}
// Send Wayland requests
wl_subsurface_set_position(surface->subsurface, subsurf_xpos, subsurf_ypos);
wl_surface_set_buffer_scale(surface->child, surface->scale);
wl_surface_attach(surface->child, buffer->buffer, 0, 0);
wl_surface_damage_buffer(surface->child, 0, 0, INT32_MAX, INT32_MAX);
wl_surface_commit(surface->child);
wl_surface_commit(surface->surface);
}