summaryrefslogtreecommitdiff
path: root/src/entry_backend
diff options
context:
space:
mode:
authorPhil Jones <philj56@gmail.com>2022-12-20 23:53:20 +0000
committerPhil Jones <philj56@gmail.com>2022-12-21 00:15:16 +0000
commit6c47cf7892d0f212b04e7b798e53c120f51022d7 (patch)
treec44b910e059d5bdcf991b2239de8d29cb007bed6 /src/entry_backend
parent108550fcf8d3ed8664c0e05defceaf20b4d2b49e (diff)
Add text cursor support.
This turned out to be much more complex than anticipated, and the potential for bugs is therefore quite high.
Diffstat (limited to 'src/entry_backend')
-rw-r--r--src/entry_backend/harfbuzz.c272
-rw-r--r--src/entry_backend/pango.c185
2 files changed, 435 insertions, 22 deletions
diff --git a/src/entry_backend/harfbuzz.c b/src/entry_backend/harfbuzz.c
index 59c0d11..cc5b253 100644
--- a/src/entry_backend/harfbuzz.c
+++ b/src/entry_backend/harfbuzz.c
@@ -1,5 +1,6 @@
#include <cairo/cairo.h>
#include <harfbuzz/hb-ft.h>
+#include <harfbuzz/hb-ot.h>
#include <math.h>
#include "harfbuzz.h"
#include "../entry.h"
@@ -75,6 +76,7 @@ static void setup_hb_buffer(hb_buffer_t *buffer)
hb_buffer_set_direction(buffer, HB_DIRECTION_LTR);
hb_buffer_set_script(buffer, HB_SCRIPT_LATIN);
hb_buffer_set_language(buffer, hb_language_from_string("en", -1));
+ hb_buffer_set_cluster_level(buffer, HB_BUFFER_CLUSTER_LEVEL_MONOTONE_CHARACTERS);
}
@@ -200,6 +202,199 @@ static cairo_text_extents_t render_text_themed(
return extents;
}
+/*
+ * Rendering the input is more complicated when a cursor is involved.
+ *
+ * Firstly, we need to use UTF-32 strings in order to properly position the
+ * cursor when ligatures / combining diacritics are involved.
+ *
+ * Next, we need to do some calculations on the shaped hb_buffer, to work out
+ * where to draw the cursor and how wide it needs to be. We may also need to
+ * make the background wider to account for the cursor.
+ *
+ * Finally, if we're drawing a block-style cursor, we may need to render the
+ * text again to draw the highlighted character in a different colour.
+ */
+static cairo_text_extents_t render_input(
+ cairo_t *cr,
+ struct entry_backend_harfbuzz *hb,
+ const uint32_t *text,
+ uint32_t text_length,
+ const struct text_theme *theme,
+ uint32_t cursor_position,
+ const struct cursor_theme *cursor_theme)
+{
+ cairo_font_extents_t font_extents;
+ cairo_font_extents(cr, &font_extents);
+ struct directional padding = theme->padding;
+
+ struct color color = theme->foreground_color;
+ cairo_set_source_rgba(cr, color.r, color.g, color.b, color.a);
+
+ hb_buffer_clear_contents(hb->hb_buffer);
+ setup_hb_buffer(hb->hb_buffer);
+ hb_buffer_add_utf32(hb->hb_buffer, text, -1, 0, -1);
+ hb_shape(hb->hb_font, hb->hb_buffer, hb->hb_features, hb->num_features);
+ cairo_text_extents_t extents = render_hb_buffer(cr, hb->hb_buffer);
+
+ /*
+ * If the cursor is at the end of text, we need to account for it in
+ * both the size of the background and the returned extents.x_advance.
+ */
+ double extra_cursor_advance = 0;
+ if (cursor_position == text_length && cursor_theme->show) {
+ switch (cursor_theme->style) {
+ case CURSOR_STYLE_BAR:
+ extra_cursor_advance = cursor_theme->thickness;
+ break;
+ case CURSOR_STYLE_BLOCK:
+ extra_cursor_advance = cursor_theme->em_width;
+ break;
+ case CURSOR_STYLE_UNDERSCORE:
+ extra_cursor_advance = cursor_theme->em_width;
+ break;
+ }
+ extra_cursor_advance += extents.x_advance
+ - extents.x_bearing
+ - extents.width;
+ }
+
+ /* Draw the background if required. */
+ if (theme->background_color.a != 0) {
+ 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 + extra_cursor_advance + 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_hb_buffer(cr, hb->hb_buffer);
+ }
+
+ if (!cursor_theme->show) {
+ /* No cursor to draw, we're done. */
+ return extents;
+ }
+
+ double cursor_x;
+ double cursor_width;
+
+ if (cursor_position == text_length) {
+ /* Cursor is at the end of text, no calculations to be done. */
+ cursor_x = extents.x_advance;
+ cursor_width = cursor_theme->em_width;
+ } else {
+ /*
+ * If the cursor is within the text, there's a bit more to do.
+ *
+ * We need to walk through the drawn glyphs, advancing the
+ * cursor by the appropriate amount as we go. This is
+ * complicated by the potential presence of ligatures, which
+ * mean the cursor could be located part way through a single
+ * glyph.
+ *
+ * To determine the appropriate width for the block and
+ * underscore cursors, we then do the same thing again, this
+ * time stopping at the character after the cursor. The width
+ * is then the difference between these two positions.
+ */
+ unsigned int glyph_count;
+ hb_glyph_info_t *glyph_info = hb_buffer_get_glyph_infos(hb->hb_buffer, &glyph_count);
+ hb_glyph_position_t *glyph_pos = hb_buffer_get_glyph_positions(hb->hb_buffer, &glyph_count);
+ int32_t cursor_start = 0;
+ int32_t cursor_end = 0;
+ for (size_t i = 0; i < glyph_count; i++) {
+ uint32_t cluster = glyph_info[i].cluster;
+ int32_t x_advance = glyph_pos[i].x_advance;
+ uint32_t next_cluster = text_length;
+ for (size_t j = i + 1; j < glyph_count; j++) {
+ /*
+ * This needs to be a loop to account for
+ * multiple glyphs sharing the same cluster
+ * (e.g. diacritics).
+ */
+ if (glyph_info[j].cluster > cluster) {
+ next_cluster = glyph_info[j].cluster;
+ break;
+ }
+ }
+ if (next_cluster > cursor_position) {
+ size_t glyph_clusters = next_cluster - cluster;
+ if (glyph_clusters > 1) {
+ uint32_t diff = cursor_position - cluster;
+ cursor_start += diff * x_advance / glyph_clusters;
+ }
+ break;
+ }
+ cursor_start += x_advance;
+ }
+ for (size_t i = 0; i < glyph_count; i++) {
+ uint32_t cluster = glyph_info[i].cluster;
+ int32_t x_advance = glyph_pos[i].x_advance;
+ uint32_t next_cluster = text_length;
+ for (size_t j = i + 1; j < glyph_count; j++) {
+ if (glyph_info[j].cluster > cluster) {
+ next_cluster = glyph_info[j].cluster;
+ break;
+ }
+ }
+ if (next_cluster > cursor_position + 1) {
+ size_t glyph_clusters = next_cluster - cluster;
+ if (glyph_clusters > 1) {
+ uint32_t diff = cursor_position + 1 - cluster;
+ cursor_end += diff * x_advance / glyph_clusters;
+ }
+ break;
+ }
+ cursor_end += x_advance;
+ }
+ /* Convert from HarfBuzz 26.6 fixed-point to float. */
+ cursor_x = cursor_start / 64.0;
+ cursor_width = (cursor_end - cursor_start) / 64.0;
+ }
+
+ cairo_save(cr);
+ color = cursor_theme->color;
+ cairo_set_source_rgba(cr, color.r, color.g, color.b, color.a);
+ cairo_translate(cr, cursor_x, 0);
+ switch (cursor_theme->style) {
+ case CURSOR_STYLE_BAR:
+ rounded_rectangle(cr, cursor_theme->thickness, font_extents.height, cursor_theme->corner_radius);
+ cairo_fill(cr);
+ break;
+ case CURSOR_STYLE_BLOCK:
+ rounded_rectangle(cr, cursor_width, font_extents.height, cursor_theme->corner_radius);
+ cairo_fill_preserve(cr);
+ cairo_clip(cr);
+ cairo_translate(cr, -cursor_x, 0);
+ color = cursor_theme->text_color;
+ cairo_set_source_rgba(cr, color.r, color.g, color.b, color.a);
+ render_hb_buffer(cr, hb->hb_buffer);
+ break;
+ case CURSOR_STYLE_UNDERSCORE:
+ cairo_translate(cr, 0, cursor_theme->underline_depth);
+ rounded_rectangle(cr, cursor_width, cursor_theme->thickness, cursor_theme->corner_radius);
+ cairo_fill(cr);
+ break;
+ }
+ cairo_restore(cr);
+
+ extents.x_advance += extra_cursor_advance;
+ return extents;
+}
+
static bool size_overflows(struct entry *entry, uint32_t width, uint32_t height)
{
cairo_t *cr = entry->cairo[entry->index].cr;
@@ -315,6 +510,33 @@ void entry_backend_harfbuzz_init(
feature = strtok_r(NULL, ",", &saveptr);
}
+ /* Get some font metrics used for rendering the cursor. */
+ uint32_t m_codepoint;
+ if (hb_font_get_glyph_from_name(hb->hb_font, "m", -1, &m_codepoint)) {
+ entry->cursor_theme.em_width = hb_font_get_glyph_h_advance(hb->hb_font, m_codepoint) / 64.0;
+ } else {
+ /* If we somehow fail to get an m from the font, just guess. */
+ entry->cursor_theme.em_width = font_size * 5.0 / 8.0;
+ }
+ hb_font_extents_t font_extents;
+ hb_font_get_h_extents(hb->hb_font, &font_extents);
+ int32_t underline_depth;
+ hb_ot_metrics_get_position_with_fallback(
+ hb->hb_font,
+ HB_OT_METRICS_TAG_UNDERLINE_OFFSET,
+ &underline_depth);
+ entry->cursor_theme.underline_depth = (font_extents.ascender - underline_depth) / 64.0;
+
+ if (entry->cursor_theme.style == CURSOR_STYLE_UNDERSCORE && !entry->cursor_theme.thickness_specified) {
+ int32_t thickness;
+ hb_ot_metrics_get_position_with_fallback(
+ hb->hb_font,
+ HB_OT_METRICS_TAG_UNDERLINE_SIZE,
+ &thickness);
+ entry->cursor_theme.thickness = thickness / 64.0;
+
+ }
+
log_debug("Creating Harfbuzz buffer.\n");
hb->hb_buffer = hb_buffer_create();
@@ -355,6 +577,9 @@ void entry_backend_harfbuzz_update(struct entry *entry)
cairo_t *cr = entry->cairo[entry->index].cr;
cairo_text_extents_t extents;
+ cairo_font_extents_t font_extents;
+ cairo_font_extents(cr, &font_extents);
+
cairo_save(cr);
/* Render the prompt */
@@ -364,29 +589,46 @@ void entry_backend_harfbuzz_update(struct entry *entry)
cairo_translate(cr, entry->prompt_padding, 0);
/* Render the entry text */
- if (entry->input_utf8_length == 0) {
- extents = render_text_themed(cr, &entry->harfbuzz, entry->placeholder_text, &entry->placeholder_theme);
+ if (entry->input_utf32_length == 0) {
+ uint32_t *tmp = utf8_string_to_utf32_string(entry->placeholder_text);
+ extents = render_input(
+ cr,
+ &entry->harfbuzz,
+ tmp,
+ utf32_strlen(tmp),
+ &entry->placeholder_theme,
+ 0,
+ &entry->cursor_theme);
+ free(tmp);
} else if (entry->hide_input) {
size_t nchars = entry->input_utf32_length;
- size_t char_size = entry->hidden_character_utf8_length;
- char *buf = xmalloc(1 + nchars * char_size);
+ uint32_t *buf = xcalloc(nchars + 1, sizeof(*entry->input_utf32));
+ uint32_t ch = utf8_to_utf32(entry->hidden_character_utf8);
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];
- }
+ buf[i] = ch;
}
- buf[char_size * nchars] = '\0';
-
- extents = render_text_themed(cr, &entry->harfbuzz, buf, &entry->input_theme);
+ buf[nchars] = U'\0';
+ extents = render_input(
+ cr,
+ &entry->harfbuzz,
+ buf,
+ entry->input_utf32_length,
+ &entry->input_theme,
+ entry->cursor_position,
+ &entry->cursor_theme);
free(buf);
} else {
- extents = render_text_themed(cr, &entry->harfbuzz, entry->input_utf8, &entry->input_theme);
+ extents = render_input(
+ cr,
+ &entry->harfbuzz,
+ entry->input_utf32,
+ entry->input_utf32_length,
+ &entry->input_theme,
+ entry->cursor_position,
+ &entry->cursor_theme);
}
extents.x_advance = MAX(extents.x_advance, entry->input_width);
- cairo_font_extents_t font_extents;
- cairo_font_extents(cr, &font_extents);
-
uint32_t num_results;
if (entry->num_results == 0) {
num_results = entry->results.count;
@@ -446,7 +688,6 @@ void entry_backend_harfbuzz_update(struct entry *entry)
* we don't need to re-measure it each time.
*/
if (size_overflows(entry, 0, font_extents.height)) {
- entry->num_results_drawn = i;
break;
} else {
extents = render_text_themed(cr, &entry->harfbuzz, result, theme);
@@ -465,7 +706,6 @@ void entry_backend_harfbuzz_update(struct entry *entry)
cairo_pattern_t *group = cairo_pop_group(cr);
if (size_overflows(entry, extents.x_advance, 0)) {
- entry->num_results_drawn = i;
cairo_pattern_destroy(group);
break;
} else {
diff --git a/src/entry_backend/pango.c b/src/entry_backend/pango.c
index 82c93ee..d274950 100644
--- a/src/entry_backend/pango.c
+++ b/src/entry_backend/pango.c
@@ -77,6 +77,127 @@ static void render_text_themed(
pango_cairo_show_layout(cr, layout);
}
+static void render_input(
+ cairo_t *cr,
+ PangoLayout *layout,
+ const char *text,
+ uint32_t text_length,
+ const struct text_theme *theme,
+ uint32_t cursor_position,
+ const struct cursor_theme *cursor_theme,
+ PangoRectangle *ink_rect,
+ PangoRectangle *logical_rect)
+{
+ struct directional padding = theme->padding;
+ 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);
+
+ double extra_cursor_advance = 0;
+ if (cursor_position == text_length && cursor_theme->show) {
+ switch (cursor_theme->style) {
+ case CURSOR_STYLE_BAR:
+ extra_cursor_advance = cursor_theme->thickness;
+ break;
+ case CURSOR_STYLE_BLOCK:
+ extra_cursor_advance = cursor_theme->em_width;
+ break;
+ case CURSOR_STYLE_UNDERSCORE:
+ extra_cursor_advance = cursor_theme->em_width;
+ break;
+ }
+ extra_cursor_advance += logical_rect->width
+ - logical_rect->x
+ - ink_rect->width;
+ }
+
+ if (theme->background_color.a != 0) {
+ 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 + extra_cursor_advance + 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);
+ }
+
+ if (!cursor_theme->show) {
+ /* No cursor to draw, we're done. */
+ return;
+ }
+
+ double cursor_x;
+ double cursor_width;
+
+ if (cursor_position == text_length) {
+ cursor_x = logical_rect->width + logical_rect->x;
+ cursor_width = cursor_theme->em_width;
+ } else {
+ /*
+ * Pango wants a byte index rather than a character index for
+ * the cursor position, so we have to calculate that here.
+ */
+ const char *tmp = text;
+ for (size_t i = 0; i < cursor_position; i++) {
+ tmp = utf8_next_char(tmp);
+ }
+ uint32_t start_byte_index = tmp - text;
+ uint32_t end_byte_index = utf8_next_char(tmp) - text;
+ PangoRectangle start_pos;
+ PangoRectangle end_pos;
+ pango_layout_get_cursor_pos(layout, start_byte_index, &start_pos, NULL);
+ pango_layout_get_cursor_pos(layout, end_byte_index, &end_pos, NULL);
+ cursor_x = (double)start_pos.x / PANGO_SCALE;
+ cursor_width = (double)(end_pos.x - start_pos.x) / PANGO_SCALE;;
+ }
+
+ cairo_save(cr);
+ color = cursor_theme->color;
+ cairo_set_source_rgba(cr, color.r, color.g, color.b, color.a);
+ cairo_translate(cr, cursor_x, 0);
+ switch (cursor_theme->style) {
+ case CURSOR_STYLE_BAR:
+ rounded_rectangle(cr, cursor_theme->thickness, logical_rect->height, cursor_theme->corner_radius);
+ cairo_fill(cr);
+ break;
+ case CURSOR_STYLE_BLOCK:
+ rounded_rectangle(cr, cursor_width, logical_rect->height, cursor_theme->corner_radius);
+ cairo_fill_preserve(cr);
+ cairo_clip(cr);
+ cairo_translate(cr, -cursor_x, 0);
+ color = cursor_theme->text_color;
+ cairo_set_source_rgba(cr, color.r, color.g, color.b, color.a);
+ pango_cairo_show_layout(cr, layout);
+ break;
+ case CURSOR_STYLE_UNDERSCORE:
+ cairo_translate(cr, 0, cursor_theme->underline_depth);
+ rounded_rectangle(cr, cursor_width, cursor_theme->thickness, cursor_theme->corner_radius);
+ cairo_fill(cr);
+ break;
+ }
+
+ logical_rect->width += extra_cursor_advance;
+ cairo_restore(cr);
+}
+
void entry_backend_pango_init(struct entry *entry, uint32_t *width, uint32_t *height)
{
cairo_t *cr = entry->cairo[0].cr;
@@ -97,7 +218,6 @@ void entry_backend_pango_init(struct entry *entry, uint32_t *width, uint32_t *he
entry->font_variations);
}
pango_context_set_font_description(context, font_description);
- pango_font_description_free(font_description);
entry->pango.layout = pango_layout_new(context);
@@ -109,6 +229,33 @@ void entry_backend_pango_init(struct entry *entry, uint32_t *width, uint32_t *he
pango_layout_set_attributes(entry->pango.layout, attr_list);
}
+ log_debug("Loading Pango font.\n");
+ PangoFontMap *map = pango_cairo_font_map_get_default();
+ PangoFont *font = pango_font_map_load_font(map, context, font_description);
+ PangoFontMetrics *metrics = pango_font_get_metrics(font, NULL);
+ hb_font_t *hb_font = pango_font_get_hb_font(font);
+
+ uint32_t m_codepoint;
+ if (hb_font_get_glyph_from_name(hb_font, "m", -1, &m_codepoint)) {
+ entry->cursor_theme.em_width = (double)hb_font_get_glyph_h_advance(hb_font, m_codepoint) / PANGO_SCALE;
+ } else {
+ entry->cursor_theme.em_width = (double)pango_font_metrics_get_approximate_char_width(metrics) / PANGO_SCALE;
+ }
+
+ entry->cursor_theme.underline_depth = (double)
+ (
+ pango_font_metrics_get_ascent(metrics)
+ - pango_font_metrics_get_underline_position(metrics)
+ ) / PANGO_SCALE;
+ if (entry->cursor_theme.style == CURSOR_STYLE_UNDERSCORE && !entry->cursor_theme.thickness_specified) {
+ entry->cursor_theme.thickness = pango_font_metrics_get_underline_thickness(metrics) / PANGO_SCALE;
+ }
+
+ pango_font_metrics_unref(metrics);
+ g_object_unref(font);
+ log_debug("Loaded.\n");
+
+ pango_font_description_free(font_description);
entry->pango.context = context;
}
@@ -148,7 +295,6 @@ void entry_backend_pango_update(struct entry *entry)
PangoLayout *layout = entry->pango.layout;
cairo_save(cr);
- struct color color = entry->foreground_color;
/* Render the prompt */
PangoRectangle ink_rect;
@@ -160,7 +306,16 @@ void entry_backend_pango_update(struct entry *entry)
/* Render the entry text */
if (entry->input_utf8_length == 0) {
- render_text_themed(cr, layout, entry->placeholder_text, &entry->placeholder_theme, &ink_rect, &logical_rect);
+ render_input(
+ cr,
+ layout,
+ entry->placeholder_text,
+ utf8_strlen(entry->placeholder_text),
+ &entry->placeholder_theme,
+ 0,
+ &entry->cursor_theme,
+ &ink_rect,
+ &logical_rect);
} else if (entry->hide_input) {
size_t nchars = entry->input_utf32_length;
size_t char_size = entry->hidden_character_utf8_length;
@@ -172,14 +327,32 @@ void entry_backend_pango_update(struct entry *entry)
}
buf[char_size * nchars] = '\0';
- render_text_themed(cr, layout, buf, &entry->placeholder_theme, &ink_rect, &logical_rect);
+ render_input(
+ cr,
+ layout,
+ buf,
+ entry->input_utf32_length,
+ &entry->input_theme,
+ entry->cursor_position,
+ &entry->cursor_theme,
+ &ink_rect,
+ &logical_rect);
free(buf);
} else {
- render_text_themed(cr, layout, entry->input_utf8, &entry->input_theme, &ink_rect, &logical_rect);
+ render_input(
+ cr,
+ layout,
+ entry->input_utf8,
+ entry->input_utf32_length,
+ &entry->input_theme,
+ entry->cursor_position,
+ &entry->cursor_theme,
+ &ink_rect,
+ &logical_rect);
}
logical_rect.width = MAX(logical_rect.width, (int)entry->input_width);
- color = entry->foreground_color;
+ struct color color = entry->foreground_color;
cairo_set_source_rgba(cr, color.r, color.g, color.b, color.a);
uint32_t num_results;