From 3e11dc2327d70e860b3ad32db386aadd549393b3 Mon Sep 17 00:00:00 2001 From: Phil Jones Date: Mon, 21 Nov 2022 23:08:51 +0000 Subject: Overhaul text theming. Each piece of text is now individually themable, with foreground and background colours and optionally rounded background corners. --- src/config.c | 148 ++++++++++++++++++++- src/entry.c | 57 +++++++++ src/entry.h | 29 ++++- src/entry_backend/harfbuzz.c | 298 +++++++++++++++++++++++++++---------------- src/entry_backend/pango.c | 231 +++++++++++++++++++++------------ src/main.c | 27 +++- 6 files changed, 592 insertions(+), 198 deletions(-) (limited to 'src') diff --git a/src/config.c b/src/config.c index 3a65d3d..3f2873d 100644 --- a/src/config.c +++ b/src/config.c @@ -83,6 +83,7 @@ static struct color parse_color(const char *filename, size_t lineno, const char static uint32_t parse_uint32(const char *filename, size_t lineno, const char *str, bool *err); static int32_t parse_int32(const char *filename, size_t lineno, const char *str, bool *err); static struct uint32_percent parse_uint32_percent(const char *filename, size_t lineno, const char *str, bool *err); +static struct directional parse_directional(const char *filename, size_t lineno, const char *str, bool *err); /* * Function-like macro. Yuck. @@ -339,10 +340,68 @@ bool parse_option(struct tofi *tofi, const char *filename, size_t lineno, const snprintf(tofi->window.entry.prompt_text, N_ELEM(tofi->window.entry.prompt_text), "%s", value); } else if (strcasecmp(option, "prompt-padding") == 0) { tofi->window.entry.prompt_padding = parse_uint32(filename, lineno, value, &err); + } else if (strcasecmp(option, "prompt-color") == 0) { + tofi->window.entry.prompt_theme.foreground_color = parse_color(filename, lineno, value, &err); + tofi->window.entry.prompt_theme.foreground_specified = true; + } else if (strcasecmp(option, "prompt-background") == 0) { + tofi->window.entry.prompt_theme.background_color = parse_color(filename, lineno, value, &err); + tofi->window.entry.prompt_theme.background_specified = true; + } else if (strcasecmp(option, "prompt-background-padding") == 0) { + tofi->window.entry.prompt_theme.padding = parse_directional(filename, lineno, value, &err); + tofi->window.entry.prompt_theme.padding_specified = true; + } else if (strcasecmp(option, "prompt-background-corner-radius") == 0) { + tofi->window.entry.prompt_theme.background_corner_radius = parse_uint32(filename, lineno, value, &err); + tofi->window.entry.prompt_theme.radius_specified = true; } else if (strcasecmp(option, "placeholder-text") == 0) { snprintf(tofi->window.entry.placeholder_text, N_ELEM(tofi->window.entry.placeholder_text), "%s", value); } else if (strcasecmp(option, "placeholder-color") == 0) { - tofi->window.entry.placeholder_color = parse_color(filename, lineno, value, &err); + tofi->window.entry.placeholder_theme.foreground_color = parse_color(filename, lineno, value, &err); + tofi->window.entry.placeholder_theme.foreground_specified = true; + } else if (strcasecmp(option, "placeholder-background") == 0) { + tofi->window.entry.placeholder_theme.background_color = parse_color(filename, lineno, value, &err); + tofi->window.entry.placeholder_theme.background_specified = true; + } else if (strcasecmp(option, "placeholder-background-padding") == 0) { + tofi->window.entry.placeholder_theme.padding = parse_directional(filename, lineno, value, &err); + tofi->window.entry.placeholder_theme.padding_specified = true; + } else if (strcasecmp(option, "placeholder-background-corner-radius") == 0) { + tofi->window.entry.placeholder_theme.background_corner_radius = parse_uint32(filename, lineno, value, &err); + tofi->window.entry.placeholder_theme.radius_specified = true; + } else if (strcasecmp(option, "input-color") == 0) { + tofi->window.entry.input_theme.foreground_color = parse_color(filename, lineno, value, &err); + tofi->window.entry.input_theme.foreground_specified = true; + } else if (strcasecmp(option, "input-background") == 0) { + tofi->window.entry.input_theme.background_color = parse_color(filename, lineno, value, &err); + tofi->window.entry.input_theme.background_specified = true; + } else if (strcasecmp(option, "input-background-padding") == 0) { + tofi->window.entry.input_theme.padding = parse_directional(filename, lineno, value, &err); + tofi->window.entry.input_theme.padding_specified = true; + } else if (strcasecmp(option, "input-background-corner-radius") == 0) { + tofi->window.entry.input_theme.background_corner_radius = parse_uint32(filename, lineno, value, &err); + tofi->window.entry.input_theme.radius_specified = true; + } else if (strcasecmp(option, "default-result-color") == 0) { + tofi->window.entry.default_result_theme.foreground_color = parse_color(filename, lineno, value, &err); + tofi->window.entry.default_result_theme.foreground_specified = true; + } else if (strcasecmp(option, "default-result-background") == 0) { + tofi->window.entry.default_result_theme.background_color = parse_color(filename, lineno, value, &err); + tofi->window.entry.default_result_theme.background_specified = true; + } else if (strcasecmp(option, "default-result-background-padding") == 0) { + tofi->window.entry.default_result_theme.padding = parse_directional(filename, lineno, value, &err); + tofi->window.entry.default_result_theme.padding_specified = true; + } else if (strcasecmp(option, "default-result-background-corner-radius") == 0) { + tofi->window.entry.default_result_theme.background_corner_radius = parse_uint32(filename, lineno, value, &err); + tofi->window.entry.default_result_theme.radius_specified = true; + } else if (strcasecmp(option, "alternate-result-color") == 0) { + tofi->window.entry.alternate_result_theme.foreground_color = parse_color(filename, lineno, value, &err); + tofi->window.entry.alternate_result_theme.foreground_specified = true; + } else if (strcasecmp(option, "alternate-result-background") == 0) { + tofi->window.entry.alternate_result_theme.background_color = parse_color(filename, lineno, value, &err); + tofi->window.entry.alternate_result_theme.background_specified = true; + } else if (strcasecmp(option, "alternate-result-background-padding") == 0) { + tofi->window.entry.alternate_result_theme.padding = parse_directional(filename, lineno, value, &err); + tofi->window.entry.alternate_result_theme.padding_specified = true; + } else if (strcasecmp(option, "alternate-result-background-corner-radius") == 0) { + tofi->window.entry.alternate_result_theme.background_corner_radius = parse_uint32(filename, lineno, value, &err); + tofi->window.entry.alternate_result_theme.radius_specified = true; } else if (strcasecmp(option, "min-input-width") == 0) { tofi->window.entry.input_width = parse_uint32(filename, lineno, value, &err); } else if (strcasecmp(option, "result-spacing") == 0) { @@ -354,13 +413,25 @@ bool parse_option(struct tofi *tofi, const char *filename, size_t lineno, const } else if (strcasecmp(option, "text-color") == 0) { tofi->window.entry.foreground_color = parse_color(filename, lineno, value, &err); } else if (strcasecmp(option, "selection-color") == 0) { - tofi->window.entry.selection_foreground_color = parse_color(filename, lineno, value, &err); + tofi->window.entry.selection_theme.foreground_color = parse_color(filename, lineno, value, &err); + tofi->window.entry.selection_theme.foreground_specified = true; } else if (strcasecmp(option, "selection-match-color") == 0) { tofi->window.entry.selection_highlight_color = parse_color(filename, lineno, value, &err); } else if (strcasecmp(option, "selection-padding") == 0) { - tofi->window.entry.selection_background_padding = parse_int32(filename, lineno, value, &err); + log_warning("The \"selection-padding\" option is deprecated, and will be removed in future. Please switch to \"selection-background-padding\".\n"); + int32_t val = parse_int32(filename, lineno, value, &err); + tofi->window.entry.selection_theme.padding.left = val; + tofi->window.entry.selection_theme.padding.right = val; + tofi->window.entry.selection_theme.padding_specified = true; } else if (strcasecmp(option, "selection-background") == 0) { - tofi->window.entry.selection_background_color = parse_color(filename, lineno, value, &err); + tofi->window.entry.selection_theme.background_color = parse_color(filename, lineno, value, &err); + tofi->window.entry.selection_theme.background_specified = true; + } else if (strcasecmp(option, "selection-background-padding") == 0) { + tofi->window.entry.selection_theme.padding = parse_directional(filename, lineno, value, &err); + tofi->window.entry.selection_theme.padding_specified = true; + } else if (strcasecmp(option, "selection-background-corner-radius") == 0) { + tofi->window.entry.selection_theme.background_corner_radius = parse_uint32(filename, lineno, value, &err); + tofi->window.entry.selection_theme.radius_specified = true; } else if (strcasecmp(option, "exclusive-zone") == 0) { if (strcmp(value, "-1") == 0) { tofi->window.exclusive_zone = -1; @@ -735,3 +806,72 @@ struct uint32_percent parse_uint32_percent(const char *filename, size_t lineno, } return (struct uint32_percent){ val, percent }; } + +struct directional parse_directional(const char *filename, size_t lineno, const char *str, bool *err) +{ + int32_t values[4]; + char *saveptr = NULL; + char *tmp = xstrdup(str); + char *val = strtok_r(tmp, ",", &saveptr); + + size_t n; + + for (n = 0; n < N_ELEM(values) && val != NULL; n++) { + values[n] = parse_int32(filename, lineno, val, err); + if (err && *err) { + break; + } + val = strtok_r(NULL, ",", &saveptr); + } + free(tmp); + + struct directional ret = {0}; + if (err && *err) { + return ret; + } + + switch (n) { + case 0: + break; + case 1: + ret = (struct directional) { + .top = values[0], + .right = values[0], + .bottom = values[0], + .left = values[0], + }; + break; + case 2: + ret = (struct directional) { + .top = values[0], + .right = values[1], + .bottom = values[0], + .left = values[1], + }; + break; + case 3: + ret = (struct directional) { + .top = values[0], + .right = values[1], + .bottom = values[2], + .left = values[1], + }; + break; + case 4: + ret = (struct directional) { + .top = values[0], + .right = values[1], + .bottom = values[2], + .left = values[3], + }; + break; + default: + PARSE_ERROR(filename, lineno, "Too many values in \"%s\" for directional.\n", str); + if (err) { + *err = true; + } + break; + }; + + return ret; +} diff --git a/src/entry.c b/src/entry.c index 70f4525..9e05f57 100644 --- a/src/entry.c +++ b/src/entry.c @@ -27,6 +27,38 @@ static void rounded_rectangle(cairo_t *cr, uint32_t width, uint32_t height, uint cairo_close_path(cr); } +static void apply_text_theme_fallback(struct text_theme *theme, const struct text_theme *fallback) +{ + if (!theme->foreground_specified) { + theme->foreground_color = fallback->foreground_color; + } + if (!theme->background_specified) { + theme->background_color = fallback->background_color; + } + if (!theme->padding_specified) { + theme->padding = fallback->padding; + } + if (!theme->radius_specified) { + theme->background_corner_radius = fallback->background_corner_radius; + } +} + +static void fixup_padding_sizes(struct directional *padding, uint32_t clip_width, uint32_t clip_height) +{ + if (padding->top == -1) { + padding->top = clip_height; + } + if (padding->bottom == -1) { + padding->bottom = clip_height; + } + if (padding->left == -1) { + padding->left = clip_width; + } + if (padding->right == -1) { + padding->right = clip_width; + } +} + void entry_init(struct entry *entry, uint8_t *restrict buffer, uint32_t width, uint32_t height) { entry->image.width = width; @@ -139,6 +171,31 @@ void entry_init(struct entry *entry, uint8_t *restrict buffer, uint32_t width, u entry->clip_width = width; entry->clip_height = height; + /* + * Before we render any text, ensure all text themes are fully + * specified. + */ + const struct text_theme default_theme = { + .foreground_color = entry->foreground_color, + .background_color = (struct color) { .a = 0 }, + .padding = (struct directional) {0}, + .background_corner_radius = 0 + }; + + apply_text_theme_fallback(&entry->prompt_theme, &default_theme); + apply_text_theme_fallback(&entry->input_theme, &default_theme); + apply_text_theme_fallback(&entry->placeholder_theme, &default_theme); + apply_text_theme_fallback(&entry->default_result_theme, &default_theme); + apply_text_theme_fallback(&entry->alternate_result_theme, &entry->default_result_theme); + apply_text_theme_fallback(&entry->selection_theme, &default_theme); + + fixup_padding_sizes(&entry->prompt_theme.padding, width, height); + fixup_padding_sizes(&entry->input_theme.padding, width, height); + fixup_padding_sizes(&entry->placeholder_theme.padding, width, height); + fixup_padding_sizes(&entry->default_result_theme.padding, width, height); + fixup_padding_sizes(&entry->alternate_result_theme.padding, width, height); + fixup_padding_sizes(&entry->selection_theme.padding, width, height); + /* * Perform an initial render of the text. * This is done here rather than by calling entry_update to avoid the diff --git a/src/entry.h b/src/entry.h index 8e9d2b4..05e73ab 100644 --- a/src/entry.h +++ b/src/entry.h @@ -19,6 +19,25 @@ #define MAX_FONT_FEATURES_LENGTH 128 #define MAX_FONT_VARIATIONS_LENGTH 128 +struct directional { + int32_t top; + int32_t right; + int32_t bottom; + int32_t left; +}; + +struct text_theme { + struct color foreground_color; + struct color background_color; + struct directional padding; + uint32_t background_corner_radius; + + bool foreground_specified; + bool background_specified; + bool padding_specified; + bool radius_specified; +}; + struct entry { struct image image; struct entry_backend_harfbuzz harfbuzz; @@ -79,12 +98,16 @@ struct entry { uint32_t outline_width; struct color foreground_color; struct color background_color; - struct color placeholder_color; struct color selection_highlight_color; - struct color selection_foreground_color; - struct color selection_background_color; struct color border_color; struct color outline_color; + + struct text_theme prompt_theme; + struct text_theme input_theme; + struct text_theme placeholder_theme; + struct text_theme default_result_theme; + struct text_theme alternate_result_theme; + struct text_theme selection_theme; }; void entry_init(struct entry *entry, uint8_t *restrict buffer, uint32_t width, uint32_t height); diff --git a/src/entry_backend/harfbuzz.c b/src/entry_backend/harfbuzz.c index f038578..59c0d11 100644 --- a/src/entry_backend/harfbuzz.c +++ b/src/entry_backend/harfbuzz.c @@ -30,6 +30,25 @@ const struct { #undef MIN #define MIN(a, b) ((a) < (b) ? (a) : (b)) +static void rounded_rectangle(cairo_t *cr, uint32_t width, uint32_t height, uint32_t r) +{ + cairo_new_path(cr); + + /* Top-left */ + cairo_arc(cr, r, r, r, -M_PI, -M_PI_2); + + /* Top-right */ + cairo_arc(cr, width - r, r, r, -M_PI_2, 0); + + /* Bottom-right */ + cairo_arc(cr, width - r, height - r, r, 0, M_PI_2); + + /* Bottom-left */ + cairo_arc(cr, r, height - r, r, M_PI_2, M_PI); + + cairo_close_path(cr); +} + static const char *get_ft_error_string(int err_code) { for (size_t i = 0; i < N_ELEM(ft_errors); i++) { @@ -60,8 +79,8 @@ static void setup_hb_buffer(hb_buffer_t *buffer) /* - * Render a hb_buffer with Cairo, and return the width of the rendered area in - * Cairo units. + * Render a hb_buffer with Cairo, and return the extents of the rendered text + * in Cairo units. */ static cairo_text_extents_t render_hb_buffer(cairo_t *cr, hb_buffer_t *buffer) { @@ -109,6 +128,10 @@ static cairo_text_extents_t render_hb_buffer(cairo_t *cr, hb_buffer_t *buffer) return extents; } +/* + * Clear the harfbuzz buffer, shape some text and render it with Cairo, + * returning the extents of the rendered text in Cairo units. + */ static cairo_text_extents_t render_text( cairo_t *cr, struct entry_backend_harfbuzz *hb, @@ -121,6 +144,62 @@ static cairo_text_extents_t render_text( return render_hb_buffer(cr, hb->hb_buffer); } + +/* + * Render some text with an optional background box, using settings from the + * given theme. + */ +static cairo_text_extents_t render_text_themed( + cairo_t *cr, + struct entry_backend_harfbuzz *hb, + const char *text, + const struct text_theme *theme) +{ + cairo_font_extents_t font_extents; + cairo_font_extents(cr, &font_extents); + struct directional padding = theme->padding; + + /* + * I previously thought rendering the text to a group, measuring it, + * drawing the box on the main canvas and then drawing the group would + * be the most efficient way of doing this. I was wrong. + * + * It turns out to be much quicker to just draw the text to the canvas, + * paint over it with the box, and then draw the text again. This is + * fine, as long as the box is always bigger than the text (which it is + * unless the user sets some extreme values for the corner radius). + */ + struct color color = theme->foreground_color; + cairo_set_source_rgba(cr, color.r, color.g, color.b, color.a); + cairo_text_extents_t extents = render_text(cr, hb, text); + + if (theme->background_color.a == 0) { + /* No background to draw, we're done. */ + return extents; + } + + cairo_save(cr); + color = theme->background_color; + cairo_set_source_rgba(cr, color.r, color.g, color.b, color.a); + cairo_translate( + cr, + floor(-padding.left + extents.x_bearing), + -padding.top); + rounded_rectangle( + cr, + ceil(extents.width + padding.left + padding.right), + ceil(font_extents.height + padding.top + padding.bottom), + theme->background_corner_radius + ); + cairo_fill(cr); + cairo_restore(cr); + + color = theme->foreground_color; + cairo_set_source_rgba(cr, color.r, color.g, color.b, color.a); + render_text(cr, hb, text); + return extents; +} + static bool size_overflows(struct entry *entry, uint32_t width, uint32_t height) { cairo_t *cr = entry->cairo[entry->index].cr; @@ -274,44 +353,40 @@ void entry_backend_harfbuzz_destroy(struct entry *entry) void entry_backend_harfbuzz_update(struct entry *entry) { cairo_t *cr = entry->cairo[entry->index].cr; - hb_buffer_t *buffer = entry->harfbuzz.hb_buffer; cairo_text_extents_t extents; cairo_save(cr); - struct color color = entry->foreground_color; - cairo_set_source_rgba(cr, color.r, color.g, color.b, color.a); /* Render the prompt */ - extents = render_text(cr, &entry->harfbuzz, entry->prompt_text); + extents = render_text_themed(cr, &entry->harfbuzz, entry->prompt_text, &entry->prompt_theme); cairo_translate(cr, extents.x_advance, 0); cairo_translate(cr, entry->prompt_padding, 0); /* Render the entry text */ if (entry->input_utf8_length == 0) { - color = entry->placeholder_color; - cairo_set_source_rgba(cr, color.r, color.g, color.b, color.a); - extents = render_text(cr, &entry->harfbuzz, entry->placeholder_text); + extents = render_text_themed(cr, &entry->harfbuzz, entry->placeholder_text, &entry->placeholder_theme); } else if (entry->hide_input) { - hb_buffer_clear_contents(buffer); - setup_hb_buffer(buffer); - size_t char_len = N_ELEM(entry->hidden_character_utf8); - for (size_t i = 0; i < entry->input_utf32_length; i++) { - hb_buffer_add_utf8(buffer, entry->hidden_character_utf8, char_len, 0, char_len); + size_t nchars = entry->input_utf32_length; + size_t char_size = entry->hidden_character_utf8_length; + char *buf = xmalloc(1 + nchars * char_size); + for (size_t i = 0; i < nchars; i++) { + for (size_t j = 0; j < char_size; j++) { + buf[i * char_size + j] = entry->hidden_character_utf8[j]; + } } - hb_shape(entry->harfbuzz.hb_font, buffer, entry->harfbuzz.hb_features, entry->harfbuzz.num_features); - extents = render_hb_buffer(cr, buffer); + buf[char_size * nchars] = '\0'; + + extents = render_text_themed(cr, &entry->harfbuzz, buf, &entry->input_theme); + free(buf); } else { - extents = render_text(cr, &entry->harfbuzz, entry->input_utf8); + extents = render_text_themed(cr, &entry->harfbuzz, entry->input_utf8, &entry->input_theme); } extents.x_advance = MAX(extents.x_advance, entry->input_width); cairo_font_extents_t font_extents; cairo_font_extents(cr, &font_extents); - color = entry->foreground_color; - cairo_set_source_rgba(cr, color.r, color.g, color.b, color.a); - uint32_t num_results; if (entry->num_results == 0) { num_results = entry->results.count; @@ -347,25 +422,24 @@ void entry_backend_harfbuzz_update(struct entry *entry) const char *result = entry->results.buf[index].string; /* * If this isn't the selected result, or it is but we're not - * doing any fancy match-highlighting or backgrounds, just - * print as normal. + * doing any fancy match-highlighting, just print as normal. */ - if (i != entry->selection - || (entry->selection_highlight_color.a == 0 - && entry->selection_background_color.a == 0)) { + if (i != entry->selection || (entry->selection_highlight_color.a == 0)) { + const struct text_theme *theme; if (i == entry->selection) { - color = entry->selection_foreground_color; + theme = &entry->selection_theme; + } else if (index % 2) { + theme = &entry->alternate_result_theme;; } else { - color = entry->foreground_color; + theme = &entry->default_result_theme;; } - cairo_set_source_rgba(cr, color.r, color.g, color.b, color.a); if (entry->num_results > 0) { /* * We're not auto-detecting how many results we * can fit, so just render the text. */ - extents = render_text(cr, &entry->harfbuzz, result); + extents = render_text_themed(cr, &entry->harfbuzz, result, theme); } else if (!entry->horizontal) { /* * The height of the text doesn't change, so @@ -375,19 +449,19 @@ void entry_backend_harfbuzz_update(struct entry *entry) entry->num_results_drawn = i; break; } else { - extents = render_text(cr, &entry->harfbuzz, result); + extents = render_text_themed(cr, &entry->harfbuzz, result, theme); } } else { /* * The difficult case: we're auto-detecting how * many results to draw, but we can't know - * whether this results will fit without + * whether this result will fit without * drawing it! To solve this, draw to a * temporary group, measure that, then copy it * to the main canvas only if it will fit. */ cairo_push_group(cr); - extents = render_text(cr, &entry->harfbuzz, result); + extents = render_text_themed(cr, &entry->harfbuzz, result, theme); cairo_pattern_t *group = cairo_pop_group(cr); if (size_overflows(entry, extents.x_advance, 0)) { @@ -404,21 +478,18 @@ void entry_backend_harfbuzz_update(struct entry *entry) } } else { /* - * For the selected result, there's a bit more to do. - * - * First, we need to use a different foreground color - - * simple enough. + * For match highlighting, there's a bit more to do. * - * Next, we may need to draw a background box - this - * involves rendering to a cairo group, measuring the - * size of the text, drawing the background on the main - * canvas, then finally drawing the group on top of - * that. + * We need to split the text into prematch, match and + * postmatch chunks, and draw each separately. * - * Finally, we may need to highlight the matching - * portion of text - this is achieved simply by - * splitting the text into prematch, match and - * postmatch chunks, and drawing each separately. + * However, we only want one background box around them + * all (if we're drawing one). To do this, we have to + * do the rendering part of render_text_themed() + * manually, with the same method of: + * - Draw the text and measure it + * - Draw the box + * - Draw the text again * * N.B. The size_overflows check isn't necessary here, * as it's currently not possible for the selection to @@ -429,7 +500,6 @@ void entry_backend_harfbuzz_update(struct entry *entry) char *prematch = xstrdup(result); char *match = NULL; char *postmatch = NULL; - cairo_text_extents_t subextents; if (entry->input_utf8_length > 0 && entry->selection_highlight_color.a != 0) { char *match_pos = utf8_strcasestr(prematch, entry->input_utf8); if (match_pos != NULL) { @@ -444,38 +514,53 @@ void entry_backend_harfbuzz_update(struct entry *entry) } } - cairo_push_group(cr); - color = entry->selection_foreground_color; - cairo_set_source_rgba(cr, color.r, color.g, color.b, color.a); - - subextents = render_text(cr, &entry->harfbuzz, prematch); - extents = subextents; - - free(prematch); - prematch = NULL; - - if (match != NULL) { - cairo_translate(cr, subextents.x_advance, 0); - color = entry->selection_highlight_color; + for (int pass = 0; pass < 2; pass++) { + cairo_save(cr); + struct color color = entry->selection_theme.foreground_color; cairo_set_source_rgba(cr, color.r, color.g, color.b, color.a); - subextents = render_text(cr, &entry->harfbuzz, &match[prematch_len]); + cairo_text_extents_t subextents = render_text(cr, &entry->harfbuzz, prematch); + extents = subextents; + + if (match != NULL) { + cairo_translate(cr, subextents.x_advance, 0); + color = entry->selection_highlight_color; + cairo_set_source_rgba(cr, color.r, color.g, color.b, color.a); + + subextents = render_text(cr, &entry->harfbuzz, &match[prematch_len]); + + if (prematch_len == 0) { + extents = subextents; + } else { + /* + * This calculation is a little + * complex, but it's basically: + * + * (distance from leftmost pixel of + * prematch to logical end of prematch) + * + * + + * + * (distance from logical start of match + * to rightmost pixel of match). + */ + extents.width = extents.x_advance + - extents.x_bearing + + subextents.x_bearing + + subextents.width; + extents.x_advance += subextents.x_advance; + } + } + + if (postmatch != NULL) { + cairo_translate(cr, subextents.x_advance, 0); + color = entry->selection_theme.foreground_color; + cairo_set_source_rgba(cr, color.r, color.g, color.b, color.a); + subextents = render_text( + cr, + &entry->harfbuzz, + &postmatch[entry->input_utf8_length + prematch_len]); - if (prematch_len == 0) { - extents = subextents; - } else { - /* - * This calculation is a little - * complex, but it's basically: - * - * (distance from leftmost pixel of - * prematch to logical end of prematch) - * - * + - * - * (distance from logical start of match - * to rightmost pixel of match). - */ extents.width = extents.x_advance - extents.x_bearing + subextents.x_bearing @@ -483,43 +568,42 @@ void entry_backend_harfbuzz_update(struct entry *entry) extents.x_advance += subextents.x_advance; } - free(match); - match = NULL; + cairo_restore(cr); + + if (entry->selection_theme.background_color.a == 0) { + /* No background box, we're done. */ + break; + } else if (pass == 0) { + /* + * First pass, paint over the text with + * our background box. + */ + struct directional padding = entry->selection_theme.padding; + cairo_save(cr); + color = entry->selection_theme.background_color; + cairo_set_source_rgba(cr, color.r, color.g, color.b, color.a); + cairo_translate( + cr, + floor(-padding.left + extents.x_bearing), + -padding.top); + rounded_rectangle( + cr, + ceil(extents.width + padding.left + padding.right), + ceil(font_extents.height + padding.top + padding.bottom), + entry->selection_theme.background_corner_radius + ); + cairo_fill(cr); + cairo_restore(cr); + } } + free(prematch); + if (match != NULL) { + free(match); + } if (postmatch != NULL) { - cairo_translate(cr, subextents.x_advance, 0); - color = entry->selection_foreground_color; - cairo_set_source_rgba(cr, color.r, color.g, color.b, color.a); - subextents = render_text( - cr, - &entry->harfbuzz, - &postmatch[entry->input_utf8_length + prematch_len]); - - extents.width = extents.x_advance - - extents.x_bearing - + subextents.x_bearing - + subextents.width; - extents.x_advance += subextents.x_advance; - - free(postmatch); - postmatch = NULL; - } - - cairo_pop_group_to_source(cr); - cairo_save(cr); - color = entry->selection_background_color; - cairo_set_source_rgba(cr, color.r, color.g, color.b, color.a); - int32_t pad = entry->selection_background_padding; - if (pad < 0) { - pad = entry->clip_width; } - cairo_translate(cr, floor(-pad + extents.x_bearing), 0); - cairo_rectangle(cr, 0, 0, ceil(extents.width + pad * 2), ceil(font_extents.height)); - cairo_fill(cr); - cairo_restore(cr); - cairo_paint(cr); } } entry->num_results_drawn = i; diff --git a/src/entry_backend/pango.c b/src/entry_backend/pango.c index 7a63d1a..82c93ee 100644 --- a/src/entry_backend/pango.c +++ b/src/entry_backend/pango.c @@ -13,6 +13,70 @@ #undef MIN #define MIN(a, b) ((a) < (b) ? (a) : (b)) +static void rounded_rectangle(cairo_t *cr, uint32_t width, uint32_t height, uint32_t r) +{ + cairo_new_path(cr); + + /* Top-left */ + cairo_arc(cr, r, r, r, -M_PI, -M_PI_2); + + /* Top-right */ + cairo_arc(cr, width - r, r, r, -M_PI_2, 0); + + /* Bottom-right */ + cairo_arc(cr, width - r, height - r, r, 0, M_PI_2); + + /* Bottom-left */ + cairo_arc(cr, r, height - r, r, M_PI_2, M_PI); + + cairo_close_path(cr); +} + +static void render_text_themed( + cairo_t *cr, + PangoLayout *layout, + const char *text, + const struct text_theme *theme, + PangoRectangle *ink_rect, + PangoRectangle *logical_rect) +{ + struct color color = theme->foreground_color; + cairo_set_source_rgba(cr, color.r, color.g, color.b, color.a); + + pango_layout_set_text(layout, text, -1); + pango_cairo_update_layout(cr, layout); + pango_cairo_show_layout(cr, layout); + + pango_layout_get_pixel_extents(layout, ink_rect, logical_rect); + + if (theme->background_color.a == 0) { + /* No background to draw, we're done. */ + return; + } + + struct directional padding = theme->padding; + + cairo_save(cr); + color = theme->background_color; + cairo_set_source_rgba(cr, color.r, color.g, color.b, color.a); + cairo_translate( + cr, + floor(-padding.left + ink_rect->x), + -padding.top); + rounded_rectangle( + cr, + ceil(ink_rect->width + padding.left + padding.right), + ceil(logical_rect->height + padding.top + padding.bottom), + theme->background_corner_radius + ); + cairo_fill(cr); + cairo_restore(cr); + + color = theme->foreground_color; + cairo_set_source_rgba(cr, color.r, color.g, color.b, color.a); + pango_cairo_show_layout(cr, layout); +} + void entry_backend_pango_init(struct entry *entry, uint32_t *width, uint32_t *height) { cairo_t *cr = entry->cairo[0].cr; @@ -85,44 +149,34 @@ void entry_backend_pango_update(struct entry *entry) cairo_save(cr); struct color color = entry->foreground_color; - cairo_set_source_rgba(cr, color.r, color.g, color.b, color.a); /* Render the prompt */ - pango_layout_set_text(layout, entry->prompt_text, -1); - pango_cairo_update_layout(cr, layout); - pango_cairo_show_layout(cr, layout); - PangoRectangle ink_rect; PangoRectangle logical_rect; - pango_layout_get_pixel_extents(entry->pango.layout, &ink_rect, &logical_rect); - cairo_translate(cr, logical_rect.width + logical_rect.x, 0); + render_text_themed(cr, layout, entry->prompt_text, &entry->prompt_theme, &ink_rect, &logical_rect); + cairo_translate(cr, logical_rect.width + logical_rect.x, 0); cairo_translate(cr, entry->prompt_padding, 0); /* Render the entry text */ if (entry->input_utf8_length == 0) { - color = entry->placeholder_color; - cairo_set_source_rgba(cr, color.r, color.g, color.b, color.a); - pango_layout_set_text(layout, entry->placeholder_text, -1); + render_text_themed(cr, layout, entry->placeholder_text, &entry->placeholder_theme, &ink_rect, &logical_rect); } else if (entry->hide_input) { - /* - * Pango needs to be passed the whole text at once, so we need - * to manually replicate the replacement character in a buffer. - */ - static char buf[sizeof(entry->input_utf8)]; - uint32_t char_len = entry->hidden_character_utf8_length; - for (size_t i = 0; i < entry->input_utf32_length; i++) { - for (size_t j = 0; j < char_len; j++) { - buf[i * char_len + j] = entry->hidden_character_utf8[j]; + size_t nchars = entry->input_utf32_length; + size_t char_size = entry->hidden_character_utf8_length; + char *buf = xmalloc(1 + nchars * char_size); + for (size_t i = 0; i < nchars; i++) { + for (size_t j = 0; j < char_size; j++) { + buf[i * char_size + j] = entry->hidden_character_utf8[j]; } } - pango_layout_set_text(layout, buf, char_len * entry->input_utf32_length); + buf[char_size * nchars] = '\0'; + + render_text_themed(cr, layout, buf, &entry->placeholder_theme, &ink_rect, &logical_rect); + free(buf); } else { - pango_layout_set_text(layout, entry->input_utf8, -1); + render_text_themed(cr, layout, entry->input_utf8, &entry->input_theme, &ink_rect, &logical_rect); } - pango_cairo_update_layout(cr, layout); - pango_cairo_show_layout(cr, layout); - pango_layout_get_pixel_extents(entry->pango.layout, &ink_rect, &logical_rect); logical_rect.width = MAX(logical_rect.width, (int)entry->input_width); color = entry->foreground_color; @@ -164,25 +218,29 @@ void entry_backend_pango_update(struct entry *entry) } else { str = ""; } - if (i != entry->selection) { - pango_layout_set_text(layout, str, -1); - pango_cairo_update_layout(cr, layout); + if (i != entry->selection || (entry->selection_highlight_color.a == 0)) { + const struct text_theme *theme; + if (i == entry->selection) { + theme = &entry->selection_theme; + } else if (index % 2) { + theme = &entry->alternate_result_theme;; + } else { + theme = &entry->default_result_theme;; + } if (entry->num_results > 0) { - pango_cairo_show_layout(cr, layout); - pango_layout_get_pixel_extents(entry->pango.layout, &ink_rect, &logical_rect); + render_text_themed(cr, layout, str, theme, &ink_rect, &logical_rect); } else if (!entry->horizontal) { if (size_overflows(entry, 0, logical_rect.height)) { entry->num_results_drawn = i; break; } else { - pango_cairo_show_layout(cr, layout); - pango_layout_get_pixel_extents(entry->pango.layout, &ink_rect, &logical_rect); + render_text_themed(cr, layout, str, theme, &ink_rect, &logical_rect); } } else { cairo_push_group(cr); - pango_cairo_show_layout(cr, layout); - pango_layout_get_pixel_extents(entry->pango.layout, &ink_rect, &logical_rect); + render_text_themed(cr, layout, str, theme, &ink_rect, &logical_rect); + cairo_pattern_t *group = cairo_pop_group(cr); if (size_overflows(entry, logical_rect.width, 0)) { entry->num_results_drawn = i; @@ -213,68 +271,77 @@ void entry_backend_pango_update(struct entry *entry) } } - cairo_push_group(cr); - color = entry->selection_foreground_color; - cairo_set_source_rgba(cr, color.r, color.g, color.b, color.a); - - pango_layout_set_text(layout, str, prematch_len); - pango_cairo_update_layout(cr, layout); - pango_cairo_show_layout(cr, layout); - pango_layout_get_pixel_extents(entry->pango.layout, &ink_subrect, &logical_subrect); - ink_rect = ink_subrect; - logical_rect = logical_subrect; - - if (prematch_len != -1) { - cairo_translate(cr, logical_subrect.x + logical_subrect.width, 0); - color = entry->selection_highlight_color; + for (int pass = 0; pass < 2; pass++) { + cairo_save(cr); + color = entry->selection_theme.foreground_color; cairo_set_source_rgba(cr, color.r, color.g, color.b, color.a); - pango_layout_set_text(layout, &str[prematch_len], match_len); + + pango_layout_set_text(layout, str, prematch_len); pango_cairo_update_layout(cr, layout); pango_cairo_show_layout(cr, layout); pango_layout_get_pixel_extents(entry->pango.layout, &ink_subrect, &logical_subrect); - if (prematch_len == 0) { - ink_rect = ink_subrect; - logical_rect = logical_subrect; - } else { + ink_rect = ink_subrect; + logical_rect = logical_subrect; + + if (prematch_len != -1) { + cairo_translate(cr, logical_subrect.x + logical_subrect.width, 0); + color = entry->selection_highlight_color; + cairo_set_source_rgba(cr, color.r, color.g, color.b, color.a); + pango_layout_set_text(layout, &str[prematch_len], match_len); + pango_cairo_update_layout(cr, layout); + pango_cairo_show_layout(cr, layout); + pango_layout_get_pixel_extents(entry->pango.layout, &ink_subrect, &logical_subrect); + if (prematch_len == 0) { + ink_rect = ink_subrect; + logical_rect = logical_subrect; + } else { + ink_rect.width = logical_rect.width + - ink_rect.x + + ink_subrect.x + + ink_subrect.width; + logical_rect.width += logical_subrect.x + logical_subrect.width; + } + } + + if (postmatch_len != -1) { + cairo_translate(cr, logical_subrect.x + logical_subrect.width, 0); + color = entry->selection_theme.foreground_color; + cairo_set_source_rgba(cr, color.r, color.g, color.b, color.a); + pango_layout_set_text(layout, &str[prematch_len + match_len], -1); + pango_cairo_update_layout(cr, layout); + pango_cairo_show_layout(cr, layout); + pango_layout_get_pixel_extents(entry->pango.layout, &ink_subrect, &logical_subrect); ink_rect.width = logical_rect.width - ink_rect.x + ink_subrect.x + ink_subrect.width; logical_rect.width += logical_subrect.x + logical_subrect.width; - } - } - if (postmatch_len != -1) { - cairo_translate(cr, logical_subrect.x + logical_subrect.width, 0); - color = entry->selection_foreground_color; - cairo_set_source_rgba(cr, color.r, color.g, color.b, color.a); - pango_layout_set_text(layout, &str[prematch_len + match_len], -1); - pango_cairo_update_layout(cr, layout); - pango_cairo_show_layout(cr, layout); - pango_layout_get_pixel_extents(entry->pango.layout, &ink_subrect, &logical_subrect); - ink_rect.width = logical_rect.width - - ink_rect.x - + ink_subrect.x - + ink_subrect.width; - logical_rect.width += logical_subrect.x + logical_subrect.width; + } - } + cairo_restore(cr); - cairo_pop_group_to_source(cr); - cairo_save(cr); - color = entry->selection_background_color; - cairo_set_source_rgba(cr, color.r, color.g, color.b, color.a); - int32_t pad = entry->selection_background_padding; - if (pad < 0) { - pad = entry->clip_width; + if (entry->selection_theme.background_color.a == 0) { + break; + } else if (pass == 0) { + struct directional padding = entry->selection_theme.padding; + cairo_save(cr); + color = entry->selection_theme.background_color; + cairo_set_source_rgba(cr, color.r, color.g, color.b, color.a); + cairo_translate( + cr, + floor(-padding.left + ink_rect.x), + -padding.top); + rounded_rectangle( + cr, + ceil(ink_rect.width + padding.left + padding.right), + ceil(logical_rect.height + padding.top + padding.bottom), + entry->selection_theme.background_corner_radius + ); + cairo_fill(cr); + cairo_restore(cr); + } } - cairo_translate(cr, floor(-pad + ink_rect.x), 0); - cairo_rectangle(cr, 0, 0, ceil(ink_rect.width + pad * 2), ceil(logical_rect.height)); - cairo_fill(cr); - cairo_restore(cr); - cairo_paint(cr); - color = entry->foreground_color; - cairo_set_source_rgba(cr, color.r, color.g, color.b, color.a); } } entry->num_results_drawn = i; diff --git a/src/main.c b/src/main.c index 500edfb..f9291ee 100644 --- a/src/main.c +++ b/src/main.c @@ -790,12 +790,33 @@ const struct option long_options[] = { {"selection-match-color", required_argument, NULL, 0}, {"selection-padding", required_argument, NULL, 0}, {"selection-background", required_argument, NULL, 0}, + {"selection-background-padding", required_argument, NULL, 0}, + {"selection-background-corner-radius", required_argument, NULL, 0}, {"outline-width", required_argument, NULL, 0}, {"outline-color", required_argument, NULL, 0}, {"prompt-text", required_argument, NULL, 0}, {"prompt-padding", required_argument, NULL, 0}, + {"prompt-color", required_argument, NULL, 0}, + {"prompt-background", required_argument, NULL, 0}, + {"prompt-background-padding", required_argument, NULL, 0}, + {"prompt-background-corner-radius", required_argument, NULL, 0}, {"placeholder-text", required_argument, NULL, 0}, {"placeholder-color", required_argument, NULL, 0}, + {"placeholder-background", required_argument, NULL, 0}, + {"placeholder-background-padding", required_argument, NULL, 0}, + {"placeholder-background-corner-radius", required_argument, NULL, 0}, + {"input-color", required_argument, NULL, 0}, + {"input-background", required_argument, NULL, 0}, + {"input-background-padding", required_argument, NULL, 0}, + {"input-background-corner-radius", required_argument, NULL, 0}, + {"default-result-color", required_argument, NULL, 0}, + {"default-result-background", required_argument, NULL, 0}, + {"default-result-background-padding", required_argument, NULL, 0}, + {"default-result-background-corner-radius", required_argument, NULL, 0}, + {"alternate-result-color", required_argument, NULL, 0}, + {"alternate-result-background", required_argument, NULL, 0}, + {"alternate-result-background-padding", required_argument, NULL, 0}, + {"alternate-result-background-corner-radius", required_argument, NULL, 0}, {"result-spacing", required_argument, NULL, 0}, {"min-input-width", required_argument, NULL, 0}, {"border-width", required_argument, NULL, 0}, @@ -1046,10 +1067,12 @@ int main(int argc, char *argv[]) .outline_width = 4, .background_color = {0.106f, 0.114f, 0.118f, 1.0f}, .foreground_color = {1.0f, 1.0f, 1.0f, 1.0f}, - .placeholder_color = {1.0f, 1.0f, 1.0f, 0.66f}, - .selection_foreground_color = {0.976f, 0.149f, 0.447f, 1.0f}, .border_color = {0.976f, 0.149f, 0.447f, 1.0f}, .outline_color = {0.031f, 0.031f, 0.0f, 1.0f}, + .placeholder_theme.foreground_color = {1.0f, 1.0f, 1.0f, 0.66f}, + .placeholder_theme.foreground_specified = true, + .selection_theme.foreground_color = {0.976f, 0.149f, 0.447f, 1.0f}, + .selection_theme.foreground_specified = true } }, .anchor = ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP -- cgit v1.2.3