mirror of
https://codeberg.org/dnkl/foot.git
synced 2026-02-05 04:06:08 -05:00
* Rename cursor.style value ‘bar’ to ‘beam’. ‘bar’ remains recognized, but should eventually be deprecated and then removed. * Add ‘cursor.beam-thickness’ option, a pt-or-px value specifying the thickness of the beam cursor. Defaults to 1.5pt. * Rename (and export) pt_or_px_as_pixels() to term_pt_or_px_as_pixels() * Change term_pt_or_px_as_pixels() to round point values instead of truncating them.
505 lines
14 KiB
C
505 lines
14 KiB
C
#include "ime.h"
|
|
|
|
#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED
|
|
|
|
#include <string.h>
|
|
|
|
#include "text-input-unstable-v3.h"
|
|
|
|
#define LOG_MODULE "ime"
|
|
#define LOG_ENABLE_DBG 0
|
|
#include "log.h"
|
|
#include "render.h"
|
|
#include "search.h"
|
|
#include "terminal.h"
|
|
#include "util.h"
|
|
#include "wayland.h"
|
|
#include "xmalloc.h"
|
|
|
|
static void
|
|
enter(void *data, struct zwp_text_input_v3 *zwp_text_input_v3,
|
|
struct wl_surface *surface)
|
|
{
|
|
struct seat *seat = data;
|
|
|
|
LOG_DBG("enter: seat=%s", seat->name);
|
|
|
|
/* The main grid is the *only* input-receiving surface we have */
|
|
xassert(seat->kbd_focus != NULL);
|
|
seat->ime.focused = true;
|
|
ime_enable(seat);
|
|
}
|
|
|
|
static void
|
|
leave(void *data, struct zwp_text_input_v3 *zwp_text_input_v3,
|
|
struct wl_surface *surface)
|
|
{
|
|
struct seat *seat = data;
|
|
LOG_DBG("leave: seat=%s", seat->name);
|
|
|
|
ime_disable(seat);
|
|
seat->ime.focused = false;
|
|
}
|
|
|
|
static void
|
|
preedit_string(void *data, struct zwp_text_input_v3 *zwp_text_input_v3,
|
|
const char *text, int32_t cursor_begin, int32_t cursor_end)
|
|
{
|
|
LOG_DBG("preedit-string: text=%s, begin=%d, end=%d", text, cursor_begin, cursor_end);
|
|
|
|
struct seat *seat = data;
|
|
|
|
ime_reset_pending_preedit(seat);
|
|
|
|
if (text != NULL) {
|
|
seat->ime.preedit.pending.text = xstrdup(text);
|
|
seat->ime.preedit.pending.cursor_begin = cursor_begin;
|
|
seat->ime.preedit.pending.cursor_end = cursor_end;
|
|
}
|
|
}
|
|
|
|
static void
|
|
commit_string(void *data, struct zwp_text_input_v3 *zwp_text_input_v3,
|
|
const char *text)
|
|
{
|
|
LOG_DBG("commit: text=%s", text);
|
|
|
|
struct seat *seat = data;
|
|
|
|
ime_reset_pending_commit(seat);
|
|
|
|
if (text != NULL)
|
|
seat->ime.commit.pending.text = xstrdup(text);
|
|
}
|
|
|
|
static void
|
|
delete_surrounding_text(void *data, struct zwp_text_input_v3 *zwp_text_input_v3,
|
|
uint32_t before_length, uint32_t after_length)
|
|
{
|
|
LOG_DBG("delete-surrounding: before=%d, after=%d", before_length, after_length);
|
|
|
|
struct seat *seat = data;
|
|
seat->ime.surrounding.pending.before_length = before_length;
|
|
seat->ime.surrounding.pending.after_length = after_length;
|
|
}
|
|
|
|
static void
|
|
done(void *data, struct zwp_text_input_v3 *zwp_text_input_v3,
|
|
uint32_t serial)
|
|
{
|
|
/*
|
|
* From text-input-unstable-v3.h:
|
|
*
|
|
* The application must proceed by evaluating the changes in the
|
|
* following order:
|
|
*
|
|
* 1. Replace existing preedit string with the cursor.
|
|
* 2. Delete requested surrounding text.
|
|
* 3. Insert commit string with the cursor at its end.
|
|
* 4. Calculate surrounding text to send.
|
|
* 5. Insert new preedit text in cursor position.
|
|
* 6. Place cursor inside preedit text.
|
|
*/
|
|
|
|
LOG_DBG("done: serial=%u", serial);
|
|
struct seat *seat = data;
|
|
struct terminal *term = seat->kbd_focus;
|
|
|
|
if (seat->ime.serial != serial) {
|
|
LOG_DBG("IME serial mismatch: expected=0x%08x, got 0x%08x",
|
|
seat->ime.serial, serial);
|
|
return;
|
|
}
|
|
|
|
if (term == NULL) {
|
|
static bool have_warned = false;
|
|
if (!have_warned) {
|
|
LOG_WARN(
|
|
"%s: text-input::done() received on seat that isn't "
|
|
"focusing a terminal window", seat->name);
|
|
have_warned = true;
|
|
}
|
|
}
|
|
|
|
/* 1. Delete existing pre-edit text */
|
|
if (seat->ime.preedit.cells != NULL) {
|
|
ime_reset_preedit(seat);
|
|
|
|
if (term != NULL) {
|
|
if (term->is_searching)
|
|
render_refresh_search(term);
|
|
else
|
|
render_refresh(term);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* 2. Delete requested surroundin text
|
|
*
|
|
* We don't support deleting surrounding text. But, we also never
|
|
* call set_surrounding_text() so hopefully we should never
|
|
* receive any requests to delete surrounding text.
|
|
*/
|
|
|
|
/* 3. Insert commit string */
|
|
if (seat->ime.commit.pending.text != NULL) {
|
|
const char *text = seat->ime.commit.pending.text;
|
|
size_t len = strlen(text);
|
|
|
|
if (term != NULL) {
|
|
if (term->is_searching) {
|
|
search_add_chars(term, text, len);
|
|
render_refresh_search(term);
|
|
} else
|
|
term_to_slave(term, text, len);
|
|
}
|
|
ime_reset_pending_commit(seat);
|
|
}
|
|
|
|
/* 4. Calculate surrounding text to send - not supported */
|
|
|
|
/* 5. Insert new pre-edit text */
|
|
size_t wchars = seat->ime.preedit.pending.text != NULL
|
|
? mbstowcs(NULL, seat->ime.preedit.pending.text, 0)
|
|
: 0;
|
|
|
|
if (wchars == 0 || wchars == (size_t)-1) {
|
|
ime_reset_pending_preedit(seat);
|
|
return;
|
|
}
|
|
|
|
/* First, convert to unicode */
|
|
seat->ime.preedit.text = xmalloc((wchars + 1) * sizeof(wchar_t));
|
|
mbstowcs(seat->ime.preedit.text, seat->ime.preedit.pending.text, wchars);
|
|
seat->ime.preedit.text[wchars] = L'\0';
|
|
|
|
/* Next, count number of cells needed */
|
|
size_t cell_count = 0;
|
|
size_t widths[wchars + 1];
|
|
|
|
for (size_t i = 0; i < wchars; i++) {
|
|
int width = max(wcwidth(seat->ime.preedit.text[i]), 1);
|
|
widths[i] = width;
|
|
cell_count += width;
|
|
}
|
|
|
|
/* Allocate cells */
|
|
seat->ime.preedit.cells = xmalloc(
|
|
cell_count * sizeof(seat->ime.preedit.cells[0]));
|
|
seat->ime.preedit.count = cell_count;
|
|
|
|
/* Populate cells */
|
|
for (size_t i = 0, cell_idx = 0; i < wchars; i++) {
|
|
struct cell *cell = &seat->ime.preedit.cells[cell_idx];
|
|
|
|
int width = widths[i];
|
|
|
|
cell->wc = seat->ime.preedit.text[i];
|
|
cell->attrs = (struct attributes){.clean = 0};
|
|
|
|
for (int j = 1; j < width; j++) {
|
|
cell = &seat->ime.preedit.cells[cell_idx + j];
|
|
cell->wc = CELL_MULT_COL_SPACER;
|
|
cell->attrs = (struct attributes){.clean = 1};
|
|
}
|
|
|
|
cell_idx += width;
|
|
}
|
|
|
|
const size_t byte_len = strlen(seat->ime.preedit.pending.text);
|
|
|
|
/* Pre-edit cursor - hidden */
|
|
if (seat->ime.preedit.pending.cursor_begin == -1 ||
|
|
seat->ime.preedit.pending.cursor_end == -1)
|
|
{
|
|
/* Note: docs says *both* begin and end should be -1,
|
|
* but what else can we do if only one is -1? */
|
|
LOG_DBG("pre-edit cursor is hidden");
|
|
seat->ime.preedit.cursor.hidden = true;
|
|
seat->ime.preedit.cursor.start = -1;
|
|
seat->ime.preedit.cursor.end = -1;
|
|
}
|
|
|
|
else if (seat->ime.preedit.pending.cursor_begin == byte_len &&
|
|
seat->ime.preedit.pending.cursor_end == byte_len)
|
|
{
|
|
/* Cursor is *after* the entire pre-edit string */
|
|
seat->ime.preedit.cursor.hidden = false;
|
|
seat->ime.preedit.cursor.start = cell_count;
|
|
seat->ime.preedit.cursor.end = cell_count;
|
|
}
|
|
|
|
else {
|
|
/*
|
|
* Translate cursor position to cell indices
|
|
*
|
|
* The cursor_begin and cursor_end are counted in
|
|
* *bytes*. We want to map them to *cell* indices.
|
|
*
|
|
* To do this, we use mblen() to step though the utf-8
|
|
* pre-edit string, advancing a unicode character index as
|
|
* we go, *and* advancing a *cell* index using wcwidth()
|
|
* of the unicode character.
|
|
*
|
|
* When we find the matching *byte* index, we at the same
|
|
* time know both the unicode *and* cell index.
|
|
*/
|
|
|
|
int cell_begin = -1, cell_end = -1;
|
|
for (size_t byte_idx = 0, wc_idx = 0, cell_idx = 0;
|
|
byte_idx < byte_len &&
|
|
wc_idx < wchars &&
|
|
cell_idx < cell_count &&
|
|
(cell_begin < 0 || cell_end < 0);
|
|
cell_idx += widths[wc_idx], wc_idx++)
|
|
{
|
|
if (seat->ime.preedit.pending.cursor_begin == byte_idx)
|
|
cell_begin = cell_idx;
|
|
if (seat->ime.preedit.pending.cursor_end == byte_idx)
|
|
cell_end = cell_idx;
|
|
|
|
/* Number of bytes of *next* utf-8 character */
|
|
size_t left = byte_len - byte_idx;
|
|
int wc_bytes = mblen(&seat->ime.preedit.pending.text[byte_idx], left);
|
|
|
|
if (wc_bytes <= 0)
|
|
break;
|
|
|
|
byte_idx += wc_bytes;
|
|
}
|
|
|
|
if (seat->ime.preedit.pending.cursor_end >= byte_len)
|
|
cell_end = cell_count;
|
|
|
|
/* Bounded by number of screen columns */
|
|
cell_begin = min(max(cell_begin, 0), cell_count - 1);
|
|
cell_end = min(max(cell_end, 0), cell_count);
|
|
|
|
if (cell_end < cell_begin)
|
|
cell_end = cell_begin;
|
|
|
|
/* Expand cursor end to end of glyph */
|
|
while (cell_end > cell_begin && cell_end < cell_count &&
|
|
seat->ime.preedit.cells[cell_end].wc == CELL_MULT_COL_SPACER)
|
|
{
|
|
cell_end++;
|
|
}
|
|
|
|
LOG_DBG("pre-edit cursor: begin=%d, end=%d", cell_begin, cell_end);
|
|
|
|
xassert(cell_begin >= 0);
|
|
xassert(cell_begin < cell_count);
|
|
xassert(cell_begin <= cell_end);
|
|
xassert(cell_end >= 0);
|
|
xassert(cell_end <= cell_count);
|
|
|
|
seat->ime.preedit.cursor.hidden = false;
|
|
seat->ime.preedit.cursor.start = cell_begin;
|
|
seat->ime.preedit.cursor.end = cell_end;
|
|
}
|
|
|
|
/* Underline pre-edit string that is *not* covered by the cursor */
|
|
bool hidden = seat->ime.preedit.cursor.hidden;
|
|
int start = seat->ime.preedit.cursor.start;
|
|
int end = seat->ime.preedit.cursor.end;
|
|
|
|
for (size_t i = 0, cell_idx = 0; i < wchars; cell_idx += widths[i], i++) {
|
|
if (hidden || start == end || cell_idx < start || cell_idx >= end) {
|
|
struct cell *cell = &seat->ime.preedit.cells[cell_idx];
|
|
cell->attrs.underline = true;
|
|
}
|
|
}
|
|
|
|
ime_reset_pending_preedit(seat);
|
|
|
|
if (term != NULL) {
|
|
if (term->is_searching)
|
|
render_refresh_search(term);
|
|
else
|
|
render_refresh(term);
|
|
}
|
|
}
|
|
|
|
void
|
|
ime_reset_pending_preedit(struct seat *seat)
|
|
{
|
|
free(seat->ime.preedit.pending.text);
|
|
seat->ime.preedit.pending.text = NULL;
|
|
}
|
|
|
|
void
|
|
ime_reset_pending_commit(struct seat *seat)
|
|
{
|
|
free(seat->ime.commit.pending.text);
|
|
seat->ime.commit.pending.text = NULL;
|
|
}
|
|
|
|
void
|
|
ime_reset_pending(struct seat *seat)
|
|
{
|
|
ime_reset_pending_preedit(seat);
|
|
ime_reset_pending_commit(seat);
|
|
}
|
|
|
|
void
|
|
ime_reset_preedit(struct seat *seat)
|
|
{
|
|
if (seat->ime.preedit.cells == NULL)
|
|
return;
|
|
|
|
free(seat->ime.preedit.text);
|
|
free(seat->ime.preedit.cells);
|
|
seat->ime.preedit.text = NULL;
|
|
seat->ime.preedit.cells = NULL;
|
|
seat->ime.preedit.count = 0;
|
|
}
|
|
|
|
static void
|
|
ime_send_cursor_rect(struct seat *seat)
|
|
{
|
|
if (unlikely(seat->wayl->text_input_manager == NULL))
|
|
return;
|
|
|
|
if (!seat->ime.focused)
|
|
return;
|
|
|
|
struct terminal *term = seat->kbd_focus;
|
|
|
|
if (!term->ime_enabled)
|
|
return;
|
|
|
|
if (seat->ime.cursor_rect.pending.x == seat->ime.cursor_rect.sent.x &&
|
|
seat->ime.cursor_rect.pending.y == seat->ime.cursor_rect.sent.y &&
|
|
seat->ime.cursor_rect.pending.width == seat->ime.cursor_rect.sent.width &&
|
|
seat->ime.cursor_rect.pending.height == seat->ime.cursor_rect.sent.height)
|
|
{
|
|
return;
|
|
}
|
|
|
|
zwp_text_input_v3_set_cursor_rectangle(
|
|
seat->wl_text_input,
|
|
seat->ime.cursor_rect.pending.x / term->scale,
|
|
seat->ime.cursor_rect.pending.y / term->scale,
|
|
seat->ime.cursor_rect.pending.width / term->scale,
|
|
seat->ime.cursor_rect.pending.height / term->scale);
|
|
|
|
zwp_text_input_v3_commit(seat->wl_text_input);
|
|
seat->ime.serial++;
|
|
|
|
seat->ime.cursor_rect.sent = seat->ime.cursor_rect.pending;
|
|
}
|
|
|
|
void
|
|
ime_enable(struct seat *seat)
|
|
{
|
|
if (unlikely(seat->wayl->text_input_manager == NULL))
|
|
return;
|
|
|
|
if (!seat->ime.focused)
|
|
return;
|
|
|
|
struct terminal *term = seat->kbd_focus;
|
|
if (term == NULL)
|
|
return;
|
|
|
|
if (!term->ime_enabled)
|
|
return;
|
|
|
|
ime_reset_pending(seat);
|
|
ime_reset_preedit(seat);
|
|
|
|
zwp_text_input_v3_enable(seat->wl_text_input);
|
|
zwp_text_input_v3_set_content_type(
|
|
seat->wl_text_input,
|
|
ZWP_TEXT_INPUT_V3_CONTENT_HINT_NONE,
|
|
ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_TERMINAL);
|
|
|
|
zwp_text_input_v3_set_cursor_rectangle(
|
|
seat->wl_text_input,
|
|
seat->ime.cursor_rect.pending.x / term->scale,
|
|
seat->ime.cursor_rect.pending.y / term->scale,
|
|
seat->ime.cursor_rect.pending.width / term->scale,
|
|
seat->ime.cursor_rect.pending.height / term->scale);
|
|
|
|
seat->ime.cursor_rect.sent = seat->ime.cursor_rect.pending;
|
|
|
|
zwp_text_input_v3_commit(seat->wl_text_input);
|
|
seat->ime.serial++;
|
|
}
|
|
|
|
void
|
|
ime_disable(struct seat *seat)
|
|
{
|
|
if (unlikely(seat->wayl->text_input_manager == NULL))
|
|
return;
|
|
|
|
if (!seat->ime.focused)
|
|
return;
|
|
|
|
ime_reset_pending(seat);
|
|
ime_reset_preedit(seat);
|
|
|
|
zwp_text_input_v3_disable(seat->wl_text_input);
|
|
zwp_text_input_v3_commit(seat->wl_text_input);
|
|
seat->ime.serial++;
|
|
}
|
|
|
|
void
|
|
ime_update_cursor_rect(struct seat *seat)
|
|
{
|
|
struct terminal *term = seat->kbd_focus;
|
|
|
|
/* Set in render_ime_preedit() */
|
|
if (seat->ime.preedit.cells != NULL)
|
|
goto update;
|
|
|
|
/* Set in render_search_box() */
|
|
if (term->is_searching)
|
|
goto update;
|
|
|
|
int x, y, width, height;
|
|
int col = term->grid->cursor.point.col;
|
|
int row = term->grid->cursor.point.row;
|
|
row += term->grid->offset;
|
|
row -= term->grid->view;
|
|
row &= term->grid->num_rows - 1;
|
|
x = term->margins.left + col * term->cell_width;
|
|
y = term->margins.top + row * term->cell_height;
|
|
|
|
if (term->cursor_style == CURSOR_BEAM)
|
|
width = 1;
|
|
else
|
|
width = term->cell_width;
|
|
|
|
height = term->cell_height;
|
|
|
|
seat->ime.cursor_rect.pending.x = x;
|
|
seat->ime.cursor_rect.pending.y = y;
|
|
seat->ime.cursor_rect.pending.width = width;
|
|
seat->ime.cursor_rect.pending.height = height;
|
|
|
|
update:
|
|
ime_send_cursor_rect(seat);
|
|
}
|
|
|
|
const struct zwp_text_input_v3_listener text_input_listener = {
|
|
.enter = &enter,
|
|
.leave = &leave,
|
|
.preedit_string = &preedit_string,
|
|
.commit_string = &commit_string,
|
|
.delete_surrounding_text = &delete_surrounding_text,
|
|
.done = &done,
|
|
};
|
|
|
|
#else /* !FOOT_IME_ENABLED */
|
|
|
|
void ime_enable(struct seat *seat) {}
|
|
void ime_disable(struct seat *seat) {}
|
|
void ime_update_cursor_rect(struct seat *seat) {}
|
|
|
|
void ime_reset_pending_preedit(struct seat *seat) {}
|
|
void ime_reset_pending_commit(struct seat *seat) {}
|
|
void ime_reset_pending(struct seat *seat) {}
|
|
void ime_reset_preedit(struct seat *seat) {}
|
|
|
|
#endif
|