diff options
author | Phil Jones <philj56@gmail.com> | 2022-12-20 23:53:20 +0000 |
---|---|---|
committer | Phil Jones <philj56@gmail.com> | 2022-12-21 00:15:16 +0000 |
commit | 6c47cf7892d0f212b04e7b798e53c120f51022d7 (patch) | |
tree | c44b910e059d5bdcf991b2239de8d29cb007bed6 /src | |
parent | 108550fcf8d3ed8664c0e05defceaf20b4d2b49e (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')
-rw-r--r-- | src/config.c | 59 | ||||
-rw-r--r-- | src/entry.c | 21 | ||||
-rw-r--r-- | src/entry.h | 27 | ||||
-rw-r--r-- | src/entry_backend/harfbuzz.c | 272 | ||||
-rw-r--r-- | src/entry_backend/pango.c | 185 | ||||
-rw-r--r-- | src/input.c | 109 | ||||
-rw-r--r-- | src/main.c | 41 | ||||
-rw-r--r-- | src/unicode.c | 14 | ||||
-rw-r--r-- | src/unicode.h | 2 |
9 files changed, 678 insertions, 52 deletions
diff --git a/src/config.c b/src/config.c index 3f61f8b..ab1e4e3 100644 --- a/src/config.c +++ b/src/config.c @@ -78,6 +78,7 @@ static uint32_t fixup_percentage(uint32_t value, uint32_t base, bool is_percent, static void fixup_text_theme(struct text_theme *theme, uint32_t scale); static uint32_t parse_anchor(const char *filename, size_t lineno, const char *str, bool *err); +static enum cursor_style parse_cursor_style(const char *filename, size_t lineno, const char *str, bool *err); static bool parse_bool(const char *filename, size_t lineno, const char *str, bool *err); static uint32_t parse_char(const char *filename, size_t lineno, const char *str, bool *err); static struct color parse_color(const char *filename, size_t lineno, const char *str, bool *err); @@ -366,6 +367,39 @@ bool parse_option(struct tofi *tofi, const char *filename, size_t lineno, const if (!err) { tofi->window.entry.outline_color = val; } + } else if (strcasecmp(option, "text-cursor") == 0) { + bool val = parse_bool(filename, lineno, value, &err); + if (!err) { + tofi->window.entry.cursor_theme.show = val; + } + } else if (strcasecmp(option, "text-cursor-style") == 0) { + enum cursor_style val = parse_cursor_style(filename, lineno, value, &err); + if (!err) { + tofi->window.entry.cursor_theme.style = val; + } + } else if (strcasecmp(option, "text-cursor-color") == 0) { + struct color val = parse_color(filename, lineno, value, &err); + if (!err) { + tofi->window.entry.cursor_theme.color = val; + tofi->window.entry.cursor_theme.color_specified = true; + } + } else if (strcasecmp(option, "text-cursor-background") == 0) { + struct color val = parse_color(filename, lineno, value, &err); + if (!err) { + tofi->window.entry.cursor_theme.text_color = val; + tofi->window.entry.cursor_theme.text_color_specified = true; + } + } else if (strcasecmp(option, "text-cursor-corner-radius") == 0) { + uint32_t val = parse_uint32(filename, lineno, value, &err); + if (!err) { + tofi->window.entry.cursor_theme.corner_radius = val; + } + } else if (strcasecmp(option, "text-cursor-thickness") == 0) { + uint32_t val = parse_uint32(filename, lineno, value, &err); + if (!err) { + tofi->window.entry.cursor_theme.thickness = val; + tofi->window.entry.cursor_theme.thickness_specified = true; + } } else if (strcasecmp(option, "prompt-text") == 0) { snprintf(tofi->window.entry.prompt_text, N_ELEM(tofi->window.entry.prompt_text), "%s", value); } else if (strcasecmp(option, "prompt-padding") == 0) { @@ -753,6 +787,9 @@ void config_fixup_values(struct tofi *tofi) entry->outline_width *= scale; entry->border_width *= scale; + entry->cursor_theme.corner_radius *= scale; + entry->cursor_theme.thickness *= scale; + fixup_text_theme(&entry->prompt_theme, scale); fixup_text_theme(&entry->placeholder_theme, scale); fixup_text_theme(&entry->input_theme, scale); @@ -946,7 +983,27 @@ uint32_t parse_anchor(const char *filename, size_t lineno, const char *str, bool return ANCHOR_CENTER; } PARSE_ERROR(filename, lineno, "Invalid anchor \"%s\".\n", str); - *err = true; + if (err) { + *err = true; + } + return 0; +} + +enum cursor_style parse_cursor_style(const char *filename, size_t lineno, const char *str, bool *err) +{ + if(strcasecmp(str, "bar") == 0) { + return CURSOR_STYLE_BAR; + } + if(strcasecmp(str, "block") == 0) { + return CURSOR_STYLE_BLOCK; + } + if(strcasecmp(str, "underscore") == 0) { + return CURSOR_STYLE_UNDERSCORE; + } + PARSE_ERROR(filename, lineno, "Invalid cursor style \"%s\".\n", str); + if (err) { + *err = true; + } return 0; } diff --git a/src/entry.c b/src/entry.c index 2ea14ac..5ccdb72 100644 --- a/src/entry.c +++ b/src/entry.c @@ -59,7 +59,7 @@ static void fixup_padding_sizes(struct directional *padding, uint32_t clip_width } } -void entry_init(struct entry *entry, uint8_t *restrict buffer, uint32_t width, uint32_t height) +void entry_init(struct entry *entry, uint8_t *restrict buffer, uint32_t width, uint32_t height, uint32_t scale) { entry->image.width = width; entry->image.height = height; @@ -211,6 +211,25 @@ void entry_init(struct entry *entry, uint8_t *restrict buffer, uint32_t width, u fixup_padding_sizes(&entry->alternate_result_theme.padding, width, height); fixup_padding_sizes(&entry->selection_theme.padding, width, height); + /* The cursor is a special case, as it just needs the input colours. */ + if (!entry->cursor_theme.color_specified) { + entry->cursor_theme.color = entry->input_theme.foreground_color; + } + if (!entry->cursor_theme.text_color_specified) { + entry->cursor_theme.text_color = entry->background_color; + } + + /* + * TODO: + * This is a dirty hack. The proper thing to do is probably just to + * stop scaling everything manually, set cairo_scale and have done + * with. The reason that isn't how things are currently done is due to + * historic scaling behaviour of tofi. + */ + if (!entry->cursor_theme.thickness_specified) { + entry->cursor_theme.thickness *= scale; + } + /* * 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 4ee6316..e6b20fa 100644 --- a/src/entry.h +++ b/src/entry.h @@ -19,6 +19,12 @@ #define MAX_FONT_FEATURES_LENGTH 128 #define MAX_FONT_VARIATIONS_LENGTH 128 +enum cursor_style { + CURSOR_STYLE_BAR, + CURSOR_STYLE_BLOCK, + CURSOR_STYLE_UNDERSCORE +}; + struct directional { int32_t top; int32_t right; @@ -38,6 +44,23 @@ struct text_theme { bool radius_specified; }; +struct cursor_theme { + struct color color; + struct color text_color; + enum cursor_style style; + uint32_t corner_radius; + uint32_t thickness; + + double underline_depth; + double em_width; + + bool color_specified; + bool text_color_specified; + bool thickness_specified; + + bool show; +}; + struct entry { struct image image; struct entry_backend_harfbuzz harfbuzz; @@ -52,6 +75,7 @@ struct entry { char input_utf8[4*MAX_INPUT_LENGTH]; uint32_t input_utf32_length; uint32_t input_utf8_length; + uint32_t cursor_position; uint32_t selection; uint32_t first_result; @@ -103,6 +127,7 @@ struct entry { struct color border_color; struct color outline_color; + struct cursor_theme cursor_theme; struct text_theme prompt_theme; struct text_theme input_theme; struct text_theme placeholder_theme; @@ -111,7 +136,7 @@ struct entry { struct text_theme selection_theme; }; -void entry_init(struct entry *entry, uint8_t *restrict buffer, uint32_t width, uint32_t height); +void entry_init(struct entry *entry, uint8_t *restrict buffer, uint32_t width, uint32_t height, uint32_t scale); void entry_destroy(struct entry *entry); void entry_update(struct entry *entry); 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; diff --git a/src/input.c b/src/input.c index 9be16e9..be65e24 100644 --- a/src/input.c +++ b/src/input.c @@ -16,6 +16,8 @@ static void clear_input(struct tofi *tofi); static void paste(struct tofi *tofi); static void select_previous_result(struct tofi *tofi); static void select_next_result(struct tofi *tofi); +static void next_cursor_or_result(struct tofi *tofi); +static void previous_cursor_or_result(struct tofi *tofi); static void reset_selection(struct tofi *tofi); void input_handle_keypress(struct tofi *tofi, xkb_keycode_t keycode) @@ -62,6 +64,10 @@ void input_handle_keypress(struct tofi *tofi, xkb_keycode_t keycode) ) { paste(tofi); + } else if (sym == XKB_KEY_Left) { + previous_cursor_or_result(tofi); + } else if (sym == XKB_KEY_Right) { + next_cursor_or_result(tofi); } else if (sym == XKB_KEY_Up || sym == XKB_KEY_Left || sym == XKB_KEY_ISO_Left_Tab || (key == KEY_K && xkb_state_mod_name_is_active( @@ -122,24 +128,38 @@ void add_character(struct tofi *tofi, xkb_keycode_t keycode) keycode, buf, sizeof(buf)); - entry->input_utf32[entry->input_utf32_length] = utf8_to_utf32(buf); - entry->input_utf32_length++; - entry->input_utf32[entry->input_utf32_length] = U'\0'; - memcpy(&entry->input_utf8[entry->input_utf8_length], - buf, - N_ELEM(buf)); - entry->input_utf8_length += len; - if (entry->drun) { - struct string_ref_vec results = desktop_vec_filter(&entry->apps, entry->input_utf8, tofi->fuzzy_match); - string_ref_vec_destroy(&entry->results); - entry->results = results; + if (entry->cursor_position == entry->input_utf32_length) { + entry->input_utf32[entry->input_utf32_length] = utf8_to_utf32(buf); + entry->input_utf32_length++; + entry->input_utf32[entry->input_utf32_length] = U'\0'; + memcpy(&entry->input_utf8[entry->input_utf8_length], + buf, + N_ELEM(buf)); + entry->input_utf8_length += len; + + if (entry->drun) { + struct string_ref_vec results = desktop_vec_filter(&entry->apps, entry->input_utf8, tofi->fuzzy_match); + string_ref_vec_destroy(&entry->results); + entry->results = results; + } else { + struct string_ref_vec tmp = entry->results; + entry->results = string_ref_vec_filter(&entry->results, entry->input_utf8, tofi->fuzzy_match); + string_ref_vec_destroy(&tmp); + } + + reset_selection(tofi); } else { - struct string_ref_vec tmp = entry->results; - entry->results = string_ref_vec_filter(&entry->results, entry->input_utf8, tofi->fuzzy_match); - string_ref_vec_destroy(&tmp); + for (size_t i = entry->input_utf32_length; i > entry->cursor_position; i--) { + entry->input_utf32[i] = entry->input_utf32[i - 1]; + } + entry->input_utf32[entry->cursor_position] = utf8_to_utf32(buf); + entry->input_utf32_length++; + entry->input_utf32[entry->input_utf32_length] = U'\0'; + + input_refresh_results(tofi); } - reset_selection(tofi); + entry->cursor_position++; } void input_refresh_results(struct tofi *tofi) @@ -173,8 +193,20 @@ void delete_character(struct tofi *tofi) return; } - entry->input_utf32_length--; - entry->input_utf32[entry->input_utf32_length] = U'\0'; + if (entry->cursor_position == 0) { + return; + } else if (entry->cursor_position == entry->input_utf32_length) { + entry->cursor_position--; + entry->input_utf32_length--; + entry->input_utf32[entry->input_utf32_length] = U'\0'; + } else { + for (size_t i = entry->cursor_position - 1; i < entry->input_utf32_length - 1; i++) { + entry->input_utf32[i] = entry->input_utf32[i + 1]; + } + entry->cursor_position--; + entry->input_utf32_length--; + entry->input_utf32[entry->input_utf32_length] = U'\0'; + } input_refresh_results(tofi); } @@ -183,19 +215,26 @@ void delete_word(struct tofi *tofi) { struct entry *entry = &tofi->window.entry; - if (entry->input_utf32_length == 0) { + if (entry->cursor_position == 0) { /* No input to delete. */ return; } - while (entry->input_utf32_length > 0 && utf32_isspace(entry->input_utf32[entry->input_utf32_length - 1])) { - entry->input_utf32_length--; + uint32_t new_cursor_pos = entry->cursor_position; + while (new_cursor_pos > 0 && utf32_isspace(entry->input_utf32[new_cursor_pos - 1])) { + new_cursor_pos--; } - while (entry->input_utf32_length > 0 && !utf32_isspace(entry->input_utf32[entry->input_utf32_length - 1])) { - entry->input_utf32_length--; + while (new_cursor_pos > 0 && !utf32_isspace(entry->input_utf32[new_cursor_pos - 1])) { + new_cursor_pos--; + } + uint32_t new_length = entry->input_utf32_length - (entry->cursor_position - new_cursor_pos); + for (size_t i = 0; i < new_length; i++) { + entry->input_utf32[new_cursor_pos + i] = entry->input_utf32[entry->cursor_position + i]; } + entry->input_utf32_length = new_length; entry->input_utf32[entry->input_utf32_length] = U'\0'; + entry->cursor_position = new_cursor_pos; input_refresh_results(tofi); } @@ -203,6 +242,7 @@ void clear_input(struct tofi *tofi) { struct entry *entry = &tofi->window.entry; + entry->cursor_position = 0; entry->input_utf32_length = 0; entry->input_utf32[0] = U'\0'; @@ -270,3 +310,28 @@ void select_next_result(struct tofi *tofi) entry->last_num_results_drawn = entry->num_results_drawn; } } + +void previous_cursor_or_result(struct tofi *tofi) +{ + struct entry *entry = &tofi->window.entry; + + if (entry->cursor_theme.show + && entry->selection == 0 + && entry->cursor_position > 0) { + entry->cursor_position--; + } else { + select_previous_result(tofi); + } +} + +void next_cursor_or_result(struct tofi *tofi) +{ + struct entry *entry = &tofi->window.entry; + + if (entry->cursor_theme.show + && entry->cursor_position < entry->input_utf32_length) { + entry->cursor_position++; + } else { + select_next_result(tofi); + } +} @@ -832,6 +832,12 @@ const struct option long_options[] = { {"selection-background-corner-radius", required_argument, NULL, 0}, {"outline-width", required_argument, NULL, 0}, {"outline-color", required_argument, NULL, 0}, + {"text-cursor", required_argument, NULL, 0}, + {"text-cursor-style", required_argument, NULL, 0}, + {"text-cursor-color", required_argument, NULL, 0}, + {"text-cursor-background", required_argument, NULL, 0}, + {"text-cursor-corner-radius", required_argument, NULL, 0}, + {"text-cursor-thickness", required_argument, NULL, 0}, {"prompt-text", required_argument, NULL, 0}, {"prompt-padding", required_argument, NULL, 0}, {"prompt-color", required_argument, NULL, 0}, @@ -1019,12 +1025,22 @@ static bool do_submit(struct tofi *tofi) static void read_clipboard(struct tofi *tofi) { struct entry *entry = &tofi->window.entry; + + /* Make a copy of any text after the cursor. */ + uint32_t *end_text = NULL; + size_t end_text_length = entry->input_utf32_length - entry->cursor_position; + if (end_text_length > 0) { + end_text = xcalloc(end_text_length, sizeof(*entry->input_utf32)); + memcpy(end_text, + &entry->input_utf32[entry->cursor_position], + end_text_length * sizeof(*entry->input_utf32)); + } /* Buffer for 4 UTF-8 bytes plus a null terminator. */ char buffer[5]; memset(buffer, 0, N_ELEM(buffer)); errno = 0; bool eof = false; - while (entry->input_utf32_length < N_ELEM(entry->input_utf32)) { + while (entry->cursor_position < N_ELEM(entry->input_utf32)) { for (size_t i = 0; i < 4; i++) { /* * Read input 1 byte at a time. This is slow, but easy, @@ -1062,8 +1078,8 @@ static void read_clipboard(struct tofi *tofi) log_error("Invalid UTF-8 character in clipboard: %s\n", buffer); break; } else { - entry->input_utf32[entry->input_utf32_length] = unichar; - entry->input_utf32_length++; + entry->input_utf32[entry->cursor_position] = unichar; + entry->cursor_position++; break; } } @@ -1072,6 +1088,19 @@ static void read_clipboard(struct tofi *tofi) break; } } + entry->input_utf32_length = entry->cursor_position; + + /* If there was any text after the cursor, re-insert it now. */ + if (end_text != NULL) { + for (size_t i = 0; i < end_text_length; i++) { + if (entry->input_utf32_length == N_ELEM(entry->input_utf32)) { + break; + } + entry->input_utf32[entry->input_utf32_length] = end_text[i]; + entry->input_utf32_length++; + } + free(end_text); + } entry->input_utf32[MIN(entry->input_utf32_length, N_ELEM(entry->input_utf32) - 1)] = U'\0'; clipboard_finish_paste(&tofi->clipboard); @@ -1117,7 +1146,8 @@ int main(int argc, char *argv[]) .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 + .selection_theme.foreground_specified = true, + .cursor_theme.thickness = 2 } }, .anchor = ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP @@ -1495,7 +1525,8 @@ int main(int argc, char *argv[]) &tofi.window.entry, tofi.window.surface.shm_pool_data, tofi.window.width, - tofi.window.height); + tofi.window.height, + tofi.use_scale ? tofi.window.scale : 1); log_unindent(); log_debug("Renderer initialised.\n"); diff --git a/src/unicode.c b/src/unicode.c index 7ddc0d5..4560d50 100644 --- a/src/unicode.c +++ b/src/unicode.c @@ -18,6 +18,11 @@ uint32_t utf8_to_utf32_validate(const char *s) return g_utf8_get_char_validated(s, -1); } +uint32_t *utf8_string_to_utf32_string(const char *s) +{ + return g_utf8_to_ucs4_fast(s, -1, NULL); +} + uint32_t utf32_isprint(uint32_t c) { return g_unichar_isprint(c); @@ -53,6 +58,15 @@ uint32_t utf32_tolower(uint32_t c) return g_unichar_tolower(c); } +size_t utf32_strlen(const uint32_t *s) +{ + size_t len = 0; + while (s[len] != U'\0') { + len++; + } + return len; +} + char *utf8_next_char(const char *s) { return g_utf8_next_char(s); diff --git a/src/unicode.h b/src/unicode.h index d32303d..5456db0 100644 --- a/src/unicode.h +++ b/src/unicode.h @@ -8,6 +8,7 @@ uint8_t utf32_to_utf8(uint32_t c, char *buf); uint32_t utf8_to_utf32(const char *s); uint32_t utf8_to_utf32_validate(const char *s); +uint32_t *utf8_string_to_utf32_string(const char *s); uint32_t utf32_isprint(uint32_t c); uint32_t utf32_isspace(uint32_t c); @@ -16,6 +17,7 @@ uint32_t utf32_islower(uint32_t c); uint32_t utf32_isalnum(uint32_t c); uint32_t utf32_toupper(uint32_t c); uint32_t utf32_tolower(uint32_t c); +size_t utf32_strlen(const uint32_t *s); char *utf8_next_char(const char *s); char *utf8_prev_char(const char *s); |