summaryrefslogtreecommitdiff
path: root/src
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
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')
-rw-r--r--src/config.c59
-rw-r--r--src/entry.c21
-rw-r--r--src/entry.h27
-rw-r--r--src/entry_backend/harfbuzz.c272
-rw-r--r--src/entry_backend/pango.c185
-rw-r--r--src/input.c109
-rw-r--r--src/main.c41
-rw-r--r--src/unicode.c14
-rw-r--r--src/unicode.h2
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);
+ }
+}
diff --git a/src/main.c b/src/main.c
index 0c5c6cb..d874745 100644
--- a/src/main.c
+++ b/src/main.c
@@ -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);