Merge branch 'input-method-editor'

Closes #134
This commit is contained in:
Daniel Eklöf 2020-12-08 20:06:22 +01:00
commit 0263a152d4
No known key found for this signature in database
GPG key ID: 5BBD4992C116573F
20 changed files with 1101 additions and 99 deletions

View file

@ -30,6 +30,12 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See
### Added
* IME support. This is compile-time optional, see
[INSTALL.md](INSTALL.md#user-content-options)
(https://codeberg.org/dnkl/foot/issues/134).
* `DECSET` escape to enable/disable IME: `\E[?737769h` enables IME and
`\E[?737769l` disables IME. This can be used to e.g. enable/disable
IME when entering/leaving insert mode in vim.
* Implement reverse auto-wrap (_auto\_left\_margin_, _bw_, in
terminfo). This mode can be enabled/disabled with `CSI ? 45 h` and
`CSI ? 45 l`. It is **enabled** by default

View file

@ -7,6 +7,7 @@
1. [Arch Linux](#arch-linux)
1. [Other](#other)
1. [Setup](#setup)
1. [Options](#options)
1. [Release build](#release-build)
1. [Size optimized](#size-optimized)
1. [Performance optimized, non-PGO](#performance-optimized-non-pgo)
@ -130,6 +131,15 @@ To build, first, create a build directory, and switch to it:
mkdir -p bld/release && cd bld/release
```
### Options
Available compile-time options:
| Option | Type | Default | Description | Extra dependencies |
|----------|------|---------|---------------------|--------------------|
| `-Dime` | bool | `true` | Enables IME support | None |
### Release build
Below are instructions for building foot either [size

View file

@ -16,6 +16,7 @@
#define LOG_ENABLE_DBG 0
#include "log.h"
#include "client-protocol.h"
#include "foot-features.h"
#include "version.h"
#include "xmalloc.h"
@ -27,6 +28,15 @@ sig_handler(int signo)
aborted = 1;
}
static const char *
version_and_features(void)
{
static char buf[256];
snprintf(buf, sizeof(buf), "version: %s %cime",
FOOT_VERSION, feature_ime() ? '+' : '-');
return buf;
}
static void
print_usage(const char *prog_name)
{
@ -155,7 +165,7 @@ main(int argc, char *const *argv)
break;
case 'v':
printf("footclient version %s\n", FOOT_VERSION);
printf("footclient %s\n", version_and_features());
return EXIT_SUCCESS;
case 'h':

9
csi.c
View file

@ -540,6 +540,13 @@ decset_decrst(struct terminal *term, unsigned param, bool enable)
term->modify_escape_key = enable;
break;
case 737769:
if (enable)
term_ime_enable(term);
else
term_ime_disable(term);
break;
default:
UNHANDLED();
break;
@ -588,6 +595,7 @@ xtsave(struct terminal *term, unsigned param)
case 1049: term->xtsave.alt_screen = term->grid == &term->alt; break;
case 2004: term->xtsave.bracketed_paste = term->bracketed_paste; break;
case 27127: term->xtsave.modify_escape_key = term->modify_escape_key; break;
case 737769: term->xtsave.ime = term_ime_is_enabled(term); break;
}
}
@ -622,6 +630,7 @@ xtrestore(struct terminal *term, unsigned param)
case 1049: enable = term->xtsave.alt_screen; break;
case 2004: enable = term->xtsave.bracketed_paste; break;
case 27127: enable = term->xtsave.modify_escape_key; break;
case 737769: enable = term->xtsave.ime; break;
default: return;
}

12
foot-features.h Normal file
View file

@ -0,0 +1,12 @@
#pragma once
#include <stdbool.h>
static inline bool feature_ime(void)
{
#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED
return true;
#else
return false;
#endif
}

376
ime.c Normal file
View file

@ -0,0 +1,376 @@
#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 */
/* TODO: can we receive text_input::enter() _before_ keyboard_enter()? */
struct terminal UNUSED *term = seat->kbd_focus;
assert(term != NULL);
assert(term_surface_kind(term, surface) == TERM_SURF_GRID);
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);
}
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_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_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;
if (seat->ime.serial != serial) {
LOG_DBG("IME serial mismatch: expected=0x%08x, got 0x%08x",
seat->ime.serial, serial);
return;
}
assert(seat->kbd_focus);
struct terminal *term = seat->kbd_focus;
/* 1. Delete existing pre-edit text */
if (term->ime.preedit.cells != NULL) {
term_ime_reset(term);
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->is_searching) {
search_add_chars(term, text, len);
render_refresh_search(term);
} else
term_to_slave(term, text, len);
ime_reset_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_preedit(seat);
return;
}
/* First, convert to unicode */
term->ime.preedit.text = xmalloc((wchars + 1) * sizeof(wchar_t));
mbstowcs(term->ime.preedit.text, seat->ime.preedit.pending.text, wchars);
term->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(term->ime.preedit.text[i]), 1);
widths[i] = width;
cell_count += width;
}
/* Allocate cells */
term->ime.preedit.cells = xmalloc(
cell_count * sizeof(term->ime.preedit.cells[0]));
term->ime.preedit.count = cell_count;
/* Populate cells */
for (size_t i = 0, cell_idx = 0; i < wchars; i++) {
struct cell *cell = &term->ime.preedit.cells[cell_idx];
int width = widths[i];
cell->wc = term->ime.preedit.text[i];
cell->attrs = (struct attributes){.clean = 0};
for (int j = 1; j < width; j++) {
cell = &term->ime.preedit.cells[cell_idx + j];
cell->wc = CELL_MULT_COL_SPACER;
cell->attrs = (struct attributes){.clean = 1};
}
cell_idx += width;
}
/* 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");
term->ime.preedit.cursor.hidden = true;
term->ime.preedit.cursor.start = -1;
term->ime.preedit.cursor.end = -1;
}
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.
*
* Note that this has only been tested with
*
* cursor_begin == cursor_end == 0
*
* I haven't found an IME that requests anything else
*/
const size_t byte_len = strlen(seat->ime.preedit.pending.text);
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 &&
term->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);
assert(cell_begin >= 0);
assert(cell_begin < cell_count);
assert(cell_begin <= cell_end);
assert(cell_end >= 0);
assert(cell_end <= cell_count);
term->ime.preedit.cursor.hidden = false;
term->ime.preedit.cursor.start = cell_begin;
term->ime.preedit.cursor.end = cell_end;
}
/* Underline pre-edit string that is *not* covered by the cursor */
bool hidden = term->ime.preedit.cursor.hidden;
int start = term->ime.preedit.cursor.start;
int end = term->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 = &term->ime.preedit.cells[cell_idx];
cell->attrs.underline = true;
}
}
ime_reset_preedit(seat);
if (term->is_searching)
render_refresh_search(term);
else
render_refresh(term);
}
void
ime_reset_preedit(struct seat *seat)
{
free(seat->ime.preedit.pending.text);
seat->ime.preedit.pending.text = NULL;
}
void
ime_reset_commit(struct seat *seat)
{
free(seat->ime.commit.pending.text);
seat->ime.commit.pending.text = NULL;
}
void
ime_reset(struct seat *seat)
{
ime_reset_preedit(seat);
ime_reset_commit(seat);
}
void
ime_enable(struct seat *seat)
{
struct terminal *term = seat->kbd_focus;
assert(term != NULL);
if (!term->ime.enabled)
return;
ime_reset(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_commit(seat->wl_text_input);
seat->ime.serial++;
}
void
ime_disable(struct seat *seat)
{
ime_reset(seat);
zwp_text_input_v3_disable(seat->wl_text_input);
zwp_text_input_v3_commit(seat->wl_text_input);
seat->ime.serial++;
}
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_reset_preedit(struct seat *seat) {}
void ime_reset_commit(struct seat *seat) {}
void ime_reset(struct seat *seat) {}
#endif

18
ime.h Normal file
View file

@ -0,0 +1,18 @@
#pragma once
#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED
#include "text-input-unstable-v3.h"
extern const struct zwp_text_input_v3_listener text_input_listener;
#endif /* FOOT_IME_ENABLED */
struct seat;
void ime_enable(struct seat *seat);
void ime_disable(struct seat *seat);
void ime_reset_preedit(struct seat *seat);
void ime_reset_commit(struct seat *seat);
void ime_reset(struct seat *seat);

View file

@ -752,6 +752,8 @@ keymap_lookup(struct seat *seat, struct terminal *term,
const enum keypad_keys keypad_keys_mode
= term->num_lock_modifier ? KEYPAD_NUMERICAL : term->keypad_keys_mode;
LOG_DBG("keypad mode: %d, num-lock=%d", keypad_keys_mode, seat->kbd.num);
for (size_t j = 0; j < count; j++) {
if (info[j].modifiers != MOD_ANY && info[j].modifiers != mods)
continue;

14
main.c
View file

@ -21,6 +21,7 @@
#include "log.h"
#include "config.h"
#include "foot-features.h"
#include "fdm.h"
#include "reaper.h"
#include "render.h"
@ -39,6 +40,15 @@ sig_handler(int signo)
aborted = 1;
}
static const char *
version_and_features(void)
{
static char buf[256];
snprintf(buf, sizeof(buf), "version: %s %cime",
FOOT_VERSION, feature_ime() ? '+' : '-');
return buf;
}
static void
print_usage(const char *prog_name)
{
@ -325,7 +335,7 @@ main(int argc, char *const *argv)
break;
case 'v':
printf("foot version %s\n", FOOT_VERSION);
printf("foot %s\n", version_and_features());
return EXIT_SUCCESS;
case 'h':
@ -343,7 +353,7 @@ main(int argc, char *const *argv)
argc -= optind;
argv += optind;
LOG_INFO("version: %s", FOOT_VERSION);
LOG_INFO("%s", version_and_features());
{
struct utsname name;

View file

@ -17,6 +17,9 @@ add_project_arguments(
(is_debug_build
? ['-D_DEBUG']
: [cc.get_supported_arguments('-fno-asynchronous-unwind-tables')]) +
(get_option('ime')
? ['-DFOOT_IME_ENABLED=1']
: []) +
cc.get_supported_arguments(
['-pedantic',
'-fstrict-aliasing',
@ -78,6 +81,7 @@ foreach prot : [
wayland_protocols_datadir + '/unstable/xdg-output/xdg-output-unstable-v1.xml',
wayland_protocols_datadir + '/unstable/primary-selection/primary-selection-unstable-v1.xml',
wayland_protocols_datadir + '/stable/presentation-time/presentation-time.xml',
wayland_protocols_datadir + '/unstable/text-input/text-input-unstable-v3.xml',
]
wl_proto_headers += custom_target(
@ -147,6 +151,8 @@ executable(
'commands.c', 'commands.h',
'extract.c', 'extract.h',
'fdm.c', 'fdm.h',
'foot-features.h',
'ime.c', 'ime.h',
'input.c', 'input.h',
'main.c',
'quirks.c', 'quirks.h',
@ -169,6 +175,7 @@ executable(
executable(
'footclient',
'client.c', 'client-protocol.h',
'foot-features.h',
'log.c', 'log.h',
'macros.h',
'xmalloc.c', 'xmalloc.h',
@ -196,9 +203,9 @@ subdir('completions')
subdir('doc')
subdir('icons')
# summary(
# {
# '<feature>': false,
# },
# bool_yn: true
# )
summary(
{
'IME': get_option('ime'),
},
bool_yn: true
)

1
meson_options.txt Normal file
View file

@ -0,0 +1 @@
option('ime', type: 'boolean', value: true, description: 'IME (Input Method Editor) support')

View file

@ -126,6 +126,9 @@ extract_finish(struct extraction_context *context, char **text, size_t *len)
void cmd_scrollback_up(struct terminal *term, int rows) {}
void cmd_scrollback_down(struct terminal *term, int rows) {}
void ime_enable(struct seat *seat) {}
void ime_disable(struct seat *seat) {}
int
main(int argc, const char *const *argv)
{

499
render.c
View file

@ -313,6 +313,35 @@ draw_strikeout(const struct terminal *term, pixman_image_t *pix,
cols * term->cell_width, font->strikeout.thickness});
}
static void
cursor_colors_for_cell(const struct terminal *term, const struct cell *cell,
const pixman_color_t *fg, const pixman_color_t *bg,
pixman_color_t *cursor_color, pixman_color_t *text_color)
{
bool is_selected = cell->attrs.selected;
if (term->cursor_color.cursor >> 31) {
*cursor_color = color_hex_to_pixman(term->cursor_color.cursor);
*text_color = color_hex_to_pixman(
term->cursor_color.text >> 31
? term->cursor_color.text : term->colors.bg);
if (term->reverse ^ cell->attrs.reverse ^ is_selected) {
pixman_color_t swap = *cursor_color;
*cursor_color = *text_color;
*text_color = swap;
}
if (term->is_searching && !is_selected) {
color_dim_for_search(cursor_color);
color_dim_for_search(text_color);
}
} else {
*cursor_color = *fg;
*text_color = *bg;
}
}
static void
draw_cursor(const struct terminal *term, const struct cell *cell,
const struct fcft_font *font, pixman_image_t *pix, pixman_color_t *fg,
@ -320,36 +349,14 @@ draw_cursor(const struct terminal *term, const struct cell *cell,
{
pixman_color_t cursor_color;
pixman_color_t text_color;
bool is_selected = cell->attrs.selected;
if (term->cursor_color.cursor >> 31) {
cursor_color = color_hex_to_pixman(term->cursor_color.cursor);
text_color = color_hex_to_pixman(
term->cursor_color.text >> 31
? term->cursor_color.text : term->colors.bg);
if (term->reverse ^ cell->attrs.reverse ^ is_selected) {
pixman_color_t swap = cursor_color;
cursor_color = text_color;
text_color = swap;
}
if (term->is_searching && !is_selected) {
color_dim_for_search(&cursor_color);
color_dim_for_search(&text_color);
}
} else {
cursor_color = *fg;
text_color = *bg;
}
cursor_colors_for_cell(term, cell, fg, bg, &cursor_color, &text_color);
switch (term->cursor_style) {
case CURSOR_BLOCK:
if (!term->kbd_focus)
if (unlikely(!term->kbd_focus))
draw_unfocused_block(term, pix, &cursor_color, x, y, cols);
else if (term->cursor_blink.state == CURSOR_BLINK_ON) {
else if (likely(term->cursor_blink.state == CURSOR_BLINK_ON)) {
*fg = text_color;
pixman_image_fill_rectangles(
PIXMAN_OP_SRC, pix, &cursor_color, 1,
@ -358,12 +365,17 @@ draw_cursor(const struct terminal *term, const struct cell *cell,
break;
case CURSOR_BAR:
if (term->cursor_blink.state == CURSOR_BLINK_ON || !term->kbd_focus)
if (likely(term->cursor_blink.state == CURSOR_BLINK_ON ||
!term->kbd_focus))
{
draw_bar(term, pix, font, &cursor_color, x, y);
}
break;
case CURSOR_UNDERLINE:
if (term->cursor_blink.state == CURSOR_BLINK_ON || !term->kbd_focus) {
if (likely(term->cursor_blink.state == CURSOR_BLINK_ON ||
!term->kbd_focus))
{
draw_underline(
term, pix, attrs_to_font(term, &cell->attrs), &cursor_color,
x, y, cols);
@ -1009,6 +1021,151 @@ render_sixel_images(struct terminal *term, pixman_image_t *pix)
}
}
static void
render_ime_preedit(struct terminal *term, struct buffer *buf)
{
#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED
if (likely(term->ime.preedit.cells == NULL))
return;
if (unlikely(term->is_searching))
return;
/* Adjust cursor position to viewport */
struct coord cursor;
cursor = term->grid->cursor.point;
cursor.row += term->grid->offset;
cursor.row -= term->grid->view;
cursor.row &= term->grid->num_rows - 1;
if (cursor.row < 0 || cursor.row >= term->rows)
return;
int cells_needed = term->ime.preedit.count;
int row_idx = cursor.row;
int col_idx = cursor.col;
int ime_ofs = 0; /* Offset into pre-edit string to start rendering at */
int cells_left = term->cols - cursor.col;
int cells_used = min(cells_needed, term->cols);
/* Adjust start of pre-edit text to the left if string doesn't fit on row */
if (cells_left < cells_used)
col_idx -= cells_used - cells_left;
if (cells_needed > cells_used) {
int start = term->ime.preedit.cursor.start;
int end = term->ime.preedit.cursor.end;
if (start == end) {
/* Ensure *end* of pre-edit string is visible */
ime_ofs = cells_needed - cells_used;
} else {
/* Ensure the *beginning* of the cursor-area is visible */
ime_ofs = start;
/* Display as much as possible of the pre-edit string */
if (cells_needed - ime_ofs < cells_used)
ime_ofs = cells_needed - cells_used;
}
/* Make sure we don't start in the middle of a character */
while (ime_ofs < cells_needed &&
term->ime.preedit.cells[ime_ofs].wc == CELL_MULT_COL_SPACER)
{
ime_ofs++;
}
}
assert(col_idx >= 0);
assert(col_idx < term->cols);
struct row *row = grid_row_in_view(term->grid, row_idx);
/* Don't start pre-edit text in the middle of a double-width character */
while (col_idx > 0 && row->cells[col_idx].wc == CELL_MULT_COL_SPACER) {
cells_used++;
col_idx--;
}
/*
* Copy original content (render_cell() reads cell data directly
* from grid), and mark all cells as dirty. This ensures they are
* re-rendered when the pre-edit text is modified or removed.
*/
struct cell *real_cells = malloc(cells_used * sizeof(real_cells[0]));
for (int i = 0; i < cells_used; i++) {
assert(col_idx + i < term->cols);
real_cells[i] = row->cells[col_idx + i];
real_cells[i].attrs.clean = 0;
}
row->dirty = true;
/* Render pre-edit text */
assert(term->ime.preedit.cells[ime_ofs].wc != CELL_MULT_COL_SPACER);
for (int i = 0, idx = ime_ofs; idx < term->ime.preedit.count; i++, idx++) {
const struct cell *cell = &term->ime.preedit.cells[idx];
if (cell->wc == CELL_MULT_COL_SPACER)
continue;
int width = max(1, wcwidth(cell->wc));
if (col_idx + i + width > term->cols)
break;
row->cells[col_idx + i] = *cell;
render_cell(term, buf->pix[0], row, col_idx + i, row_idx, false);
}
int start = term->ime.preedit.cursor.start - ime_ofs;
int end = term->ime.preedit.cursor.end - ime_ofs;
if (!term->ime.preedit.cursor.hidden) {
const struct cell *start_cell = &term->ime.preedit.cells[start + ime_ofs];
pixman_color_t fg = color_hex_to_pixman(term->colors.fg);
pixman_color_t bg = color_hex_to_pixman(term->colors.bg);
pixman_color_t cursor_color, text_color;
cursor_colors_for_cell(
term, start_cell, &fg, &bg, &cursor_color, &text_color);
int x = term->margins.left + (col_idx + start) * term->cell_width;
int y = term->margins.top + row_idx * term->cell_height;
if (end == start) {
/* Bar */
if (start >= 0) {
struct fcft_font *font = attrs_to_font(term, &start_cell->attrs);
draw_bar(term, buf->pix[0], font, &cursor_color, x, y);
}
}
else if (end > start) {
/* Hollow cursor */
if (start >= 0 && end <= term->cols) {
int cols = end - start;
draw_unfocused_block(term, buf->pix[0], &cursor_color, x, y, cols);
}
}
}
/* Restore original content (but do not render) */
for (int i = 0; i < cells_used; i++)
row->cells[col_idx + i] = real_cells[i];
free(real_cells);
wl_surface_damage_buffer(
term->window->surface,
term->margins.left,
term->margins.top + row_idx * term->cell_height,
term->width - term->margins.left - term->margins.right,
1 * term->cell_height);
#endif
}
static void
render_row(struct terminal *term, pixman_image_t *pix, struct row *row,
int row_no, int cursor_col)
@ -1903,6 +2060,9 @@ grid_render(struct terminal *term)
term->render.workers.buf = NULL;
}
/* Render IME pre-edit text */
render_ime_preedit(term, buf);
if (term->flash.active) {
/* Note: alpha is pre-computed in each color component */
/* TODO: dim while searching */
@ -1985,7 +2145,49 @@ render_search_box(struct terminal *term)
{
assert(term->window->search_sub_surface != NULL);
const size_t wanted_visible_chars = max(20, term->search.len);
/*
* We treat the search box pretty much like a row of cells. That
* is, a glyph is either 1 or 2 (or more) cells wide.
*
* The search length, and cursor (position) is in
* *characters*, not cells. This means we need to translate from
* character count to cell count when calculating the length of
* the search box, where in the search string we should start
* rendering etc.
*/
#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED
size_t text_len = term->search.len;
if (term->ime.preedit.text != NULL)
text_len += wcslen(term->ime.preedit.text);
wchar_t *text = xmalloc((text_len + 1) * sizeof(wchar_t));
text[0] = L'\0';
/* Copy everything up to the cursor */
wcsncpy(text, term->search.buf, term->search.cursor);
text[term->search.cursor] = L'\0';
/* Insert pre-edit text at cursor */
if (term->ime.preedit.text != NULL)
wcscat(text, term->ime.preedit.text);
/* And finally everything after the cursor */
wcsncat(text, &term->search.buf[term->search.cursor],
term->search.len - term->search.cursor);
#else
const wchar_t *text = term->search.buf;
const size_t text_len = term->search.len;
#endif
/* Calculate the width of each character */
int widths[text_len + 1];
for (size_t i = 0; i < text_len; i++)
widths[i] = max(1, wcwidth(text[i]));
widths[text_len] = 0;
const size_t total_cells = wcswidth(text, text_len);
const size_t wanted_visible_cells = max(20, total_cells);
assert(term->scale >= 1);
const int scale = term->scale;
@ -1995,12 +2197,12 @@ render_search_box(struct terminal *term)
const size_t width = term->width - 2 * margin;
const size_t visible_width = min(
term->width - 2 * margin,
2 * margin + wanted_visible_chars * term->cell_width);
2 * margin + wanted_visible_cells * term->cell_width);
const size_t height = min(
term->height - 2 * margin,
2 * margin + 1 * term->cell_height);
const size_t visible_chars = (visible_width - 2 * margin) / term->cell_width;
const size_t visible_cells = (visible_width - 2 * margin) / term->cell_width;
size_t glyph_offset = term->render.search_glyph_offset;
unsigned long cookie = shm_cookie_search(term);
@ -2008,7 +2210,7 @@ render_search_box(struct terminal *term)
/* Background - yellow on empty/match, red on mismatch */
pixman_color_t color = color_hex_to_pixman(
term->search.match_len == term->search.len
term->search.match_len == text_len
? term->colors.table[3] : term->colors.table[1]);
pixman_image_fill_rectangles(
@ -2021,49 +2223,176 @@ render_search_box(struct terminal *term)
1, &(pixman_rectangle16_t){0, 0, width - visible_width, height});
struct fcft_font *font = term->fonts[0];
int x = width - visible_width + margin;
const int x_left = width - visible_width + margin;
int x = x_left;
int y = margin;
pixman_color_t fg = color_hex_to_pixman(term->colors.table[0]);
/* Ensure cursor is visible */
if (term->search.cursor < glyph_offset)
term->render.search_glyph_offset = glyph_offset = term->search.cursor;
else if (term->search.cursor > glyph_offset + visible_chars) {
term->render.search_glyph_offset = glyph_offset =
term->search.cursor - min(term->search.cursor, visible_chars);
}
/* Move offset if there is free space available */
if (term->search.len - glyph_offset < visible_chars)
term->render.search_glyph_offset = glyph_offset =
term->search.len - min(term->search.len, visible_chars);
/* Text (what the user entered - *not* match(es)) */
for (size_t i = glyph_offset;
i < term->search.len && i - glyph_offset < visible_chars;
i++)
{
if (i == term->search.cursor)
draw_bar(term, buf->pix[0], font, &fg, x, y);
const struct fcft_glyph *glyph = fcft_glyph_rasterize(
font, term->search.buf[i], term->font_subpixel);
if (glyph == NULL)
/* Move offset we start rendering at, to ensure the cursor is visible */
for (size_t i = 0, cell_idx = 0; i <= term->search.cursor; cell_idx += widths[i], i++) {
if (i != term->search.cursor)
continue;
pixman_image_t *src = pixman_image_create_solid_fill(&fg);
pixman_image_composite32(
PIXMAN_OP_OVER, src, glyph->pix, buf->pix[0], 0, 0, 0, 0,
x + glyph->x, y + font_baseline(term) - glyph->y,
glyph->width, glyph->height);
pixman_image_unref(src);
#if (FOOT_IME_ENABLED) && FOOT_IME_ENABLED
if (term->ime.preedit.cells != NULL) {
if (term->ime.preedit.cursor.start == term->ime.preedit.cursor.end) {
/* All IME's I've seen so far keeps the cursor at
* index 0, so ensure the *end* of the pre-edit string
* is visible */
cell_idx += term->ime.preedit.count;
} else {
/* Try to predict in which direction we'll shift the text */
if (cell_idx + term->ime.preedit.cursor.start > glyph_offset)
cell_idx += term->ime.preedit.cursor.end;
else
cell_idx += term->ime.preedit.cursor.start;
}
}
#endif
x += term->cell_width;
if (cell_idx < glyph_offset) {
/* Shift to the *left*, making *this* character the
* *first* visible one */
term->render.search_glyph_offset = glyph_offset = cell_idx;
}
else if (cell_idx > glyph_offset + visible_cells) {
/* Shift to the *right*, making *this* character the
* *last* visible one */
term->render.search_glyph_offset = glyph_offset =
cell_idx - min(cell_idx, visible_cells);
}
/* Adjust offset if there is free space available */
if (total_cells - glyph_offset < visible_cells) {
term->render.search_glyph_offset = glyph_offset =
total_cells - min(total_cells, visible_cells);
}
break;
}
if (term->search.cursor >= term->search.len)
draw_bar(term, buf->pix[0], font, &fg, x, y);
/* Ensure offset is at a character boundary */
for (size_t i = 0, cell_idx = 0; i <= text_len; cell_idx += widths[i], i++) {
if (cell_idx >= glyph_offset) {
term->render.search_glyph_offset = glyph_offset = cell_idx;
break;
}
}
/*
* Render the search string, starting at glyph_offset. Note that
* glyph_offset is in cells, not characters
*/
for (size_t i = 0,
cell_idx = 0,
width = widths[i],
next_cell_idx = width;
i < text_len;
i++,
cell_idx = next_cell_idx,
width = widths[i],
next_cell_idx += width)
{
/* Render cursor */
if (i == term->search.cursor) {
#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED
bool have_preedit = term->ime.preedit.cells != NULL;
bool hidden = term->ime.preedit.cursor.hidden;
if (have_preedit && !hidden) {
/* Cursor may be outside the visible area:
* cell_idx-glyph_offset can be negative */
int cells_left = visible_cells - max(
(ssize_t)(cell_idx - glyph_offset), 0);
/* If cursor is outside the visible area, we need to
* adjust our rectangle's position */
int start = term->ime.preedit.cursor.start
+ min((ssize_t)(cell_idx - glyph_offset), 0);
int end = term->ime.preedit.cursor.end
+ min((ssize_t)(cell_idx - glyph_offset), 0);
if (start == end) {
int count = min(term->ime.preedit.count, cells_left);
/* Underline the entire (visible part of) pre-edit text */
draw_underline(term, buf->pix[0], font, &fg, x, y, count);
/* Bar-styled cursor, if in the visible area */
if (start >= 0 && start <= visible_cells)
draw_bar(term, buf->pix[0], font, &fg, x + start * term->cell_width, y);
} else {
/* Underline everything before and after the cursor */
int count1 = min(start, cells_left);
int count2 = max(
min(term->ime.preedit.count - term->ime.preedit.cursor.end,
cells_left - end),
0);
draw_underline(term, buf->pix[0], font, &fg, x, y, count1);
draw_underline(term, buf->pix[0], font, &fg, x + end * term->cell_width, y, count2);
/* TODO: how do we handle a partially hidden rectangle? */
if (start >= 0 && end <= visible_cells) {
draw_unfocused_block(
term, buf->pix[0], &fg, x + start * term->cell_width, y, end - start);
}
}
} else if (!have_preedit)
#endif
{
/* Cursor *should* be in the visible area */
assert(cell_idx >= glyph_offset);
assert(cell_idx <= glyph_offset + visible_cells);
draw_bar(term, buf->pix[0], font, &fg, x, y);
}
}
if (next_cell_idx >= glyph_offset && next_cell_idx - glyph_offset > visible_cells) {
/* We're now beyond the visible area - nothing more to render */
break;
}
if (cell_idx < glyph_offset) {
/* We haven't yet reached the visible part of the string */
cell_idx = next_cell_idx;
continue;
}
const struct fcft_glyph *glyph = fcft_glyph_rasterize(
font, text[i], term->font_subpixel);
if (glyph == NULL) {
cell_idx = next_cell_idx;
continue;
}
if (unlikely(pixman_image_get_format(glyph->pix) == PIXMAN_a8r8g8b8)) {
/* Glyph surface is a pre-rendered image (typically a color emoji...) */
pixman_image_composite32(
PIXMAN_OP_OVER, glyph->pix, NULL, buf->pix[0], 0, 0, 0, 0,
x + glyph->x, y + font_baseline(term) - glyph->y,
glyph->width, glyph->height);
} else {
pixman_image_t *src = pixman_image_create_solid_fill(&fg);
pixman_image_composite32(
PIXMAN_OP_OVER, src, glyph->pix, buf->pix[0], 0, 0, 0, 0,
x + glyph->x, y + font_baseline(term) - glyph->y,
glyph->width, glyph->height);
pixman_image_unref(src);
}
x += width * term->cell_width;
cell_idx = next_cell_idx;
}
#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED
if (term->ime.preedit.cells != NULL)
/* Already rendered */;
else
#endif
if (term->search.cursor >= term->search.len)
draw_bar(term, buf->pix[0], font, &fg, x, y);
quirk_weston_subsurface_desync_on(term->window->search_sub_surface);
@ -2086,6 +2415,10 @@ render_search_box(struct terminal *term)
wl_surface_commit(term->window->search_surface);
quirk_weston_subsurface_desync_off(term->window->search_sub_surface);
#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED
free(text);
#endif
}
static void
@ -2458,32 +2791,28 @@ fdm_hook_refresh_pending_terminals(struct fdm *fdm, void *data)
tll_foreach(renderer->wayl->terms, it) {
struct terminal *term = it->item;
if (!term->render.refresh.grid &&
!term->render.refresh.csd &&
!term->render.refresh.search)
{
if (unlikely(!term->window->is_configured))
continue;
}
if (term->render.app_sync_updates.enabled &&
!term->render.refresh.csd &&
!term->render.refresh.search)
{
continue;
}
if (term->render.refresh.csd || term->render.refresh.search) {
/* Force update of parent surface */
term->render.refresh.grid = true;
}
assert(term->window->is_configured);
bool grid = term->render.refresh.grid;
bool csd = term->render.refresh.csd;
bool search = term->render.refresh.search;
bool title = term->render.refresh.title;
if (!term->is_searching)
search = false;
if (!(grid | csd | search | title))
continue;
if (term->render.app_sync_updates.enabled && !(csd | search | title))
continue;
if (csd | search) {
/* Force update of parent surface */
grid = true;
}
term->render.refresh.grid = false;
term->render.refresh.csd = false;
term->render.refresh.search = false;

View file

@ -18,6 +18,7 @@
#include "selection.h"
#include "shm.h"
#include "util.h"
#include "xmalloc.h"
/*
* Ensures a "new" viewport doesn't contain any unallocated rows.
@ -98,6 +99,12 @@ search_cancel_keep_selection(struct terminal *term)
term->is_searching = false;
term->render.search_glyph_offset = 0;
/* Reset IME state */
if (term_ime_is_enabled(term)) {
term_ime_disable(term);
term_ime_enable(term);
}
term_xcursor_update(term);
render_refresh(term);
}
@ -110,6 +117,12 @@ search_begin(struct terminal *term)
search_cancel_keep_selection(term);
selection_cancel(term);
/* Reset IME state */
if (term_ime_is_enabled(term)) {
term_ime_disable(term);
term_ime_enable(term);
}
/* On-demand instantiate wayland surface */
struct wl_window *win = term->window;
struct wayland *wayl = term->wl;
@ -124,6 +137,11 @@ search_begin(struct terminal *term)
term->search.view_followed_offset = term->grid->view == term->grid->offset;
term->is_searching = true;
term->search.len = 0;
term->search.sz = 64;
term->search.buf = xmalloc(term->search.sz * sizeof(term->search.buf[0]));
term->search.buf[0] = L'\0';
term_xcursor_update(term);
render_refresh_search(term);
}
@ -332,8 +350,8 @@ search_find_next(struct terminal *term)
#undef ROW_DEC
}
static void
add_chars(struct terminal *term, const char *src, size_t count)
void
search_add_chars(struct terminal *term, const char *src, size_t count)
{
mbstate_t ps = {0};
size_t wchars = mbsnrtowcs(NULL, &src, count, 0, &ps);
@ -494,7 +512,7 @@ static void
from_clipboard_cb(char *text, size_t size, void *user)
{
struct terminal *term = user;
add_chars(term, text, size);
search_add_chars(term, text, size);
}
static void
@ -722,7 +740,7 @@ search_input(struct seat *seat, struct terminal *term, uint32_t key,
if (count == 0)
return;
add_chars(term, (const char *)buf, count);
search_add_chars(term, (const char *)buf, count);
update_search:
LOG_DBG("search: buffer: %ls", term->search.buf);

View file

@ -7,3 +7,4 @@ void search_begin(struct terminal *term);
void search_cancel(struct terminal *term);
void search_input(struct seat *seat, struct terminal *term, uint32_t key,
xkb_keysym_t sym, xkb_mod_mask_t mods, uint32_t serial);
void search_add_chars(struct terminal *term, const char *text, size_t len);

2
shm.c
View file

@ -655,7 +655,7 @@ shm_scroll(struct wl_shm *shm, struct buffer *buf, int rows,
: shm_scroll_reverse(shm, buf, -rows, top_margin, top_keep_rows, bottom_margin, bottom_keep_rows);
}
void
void
shm_purge(struct wl_shm *shm, unsigned long cookie)
{
LOG_DBG("cookie=%lx: purging all buffers", cookie);

View file

@ -24,6 +24,7 @@
#include "config.h"
#include "extract.h"
#include "grid.h"
#include "ime.h"
#include "quirks.h"
#include "reaper.h"
#include "render.h"
@ -1116,6 +1117,11 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper,
.shutdown_data = shutdown_data,
.foot_exe = xstrdup(foot_exe),
.cwd = xstrdup(cwd),
#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED
.ime = {
.enabled = true,
},
#endif
};
for (size_t i = 0; i < 4; i++) {
@ -1403,6 +1409,8 @@ term_destroy(struct terminal *term)
tll_free(term->alt.sixel_images);
sixel_fini(term);
term_ime_reset(term);
free(term->foot_exe);
free(term->cwd);
@ -1557,6 +1565,10 @@ term_reset(struct terminal *term, bool hard)
sixel_destroy(&it->item);
tll_free(term->alt.sixel_images);
#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED
term_ime_enable(term);
#endif
if (!hard)
return;
@ -2216,6 +2228,13 @@ term_kbd_focus_out(struct terminal *term)
if (it->item.kbd_focus == term)
return;
#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED
if (term->ime.preedit.cells != NULL) {
term_ime_reset(term);
render_refresh(term);
}
#endif
term->kbd_focus = false;
cursor_refresh(term);
@ -2744,3 +2763,67 @@ term_view_to_text(const struct terminal *term, char **text, size_t *len)
int end = grid_row_absolute_in_view(term->grid, term->rows - 1);
return rows_to_text(term, start, end, text, len);
}
bool
term_ime_is_enabled(const struct terminal *term)
{
#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED
return term->ime.enabled;
#else
return false;
#endif
}
void
term_ime_enable(struct terminal *term)
{
#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED
if (term->ime.enabled)
return;
LOG_DBG("IME enabled");
term->ime.enabled = true;
term_ime_reset(term);
/* IME is per seat - enable on all seat currently focusing us */
tll_foreach(term->wl->seats, it) {
if (it->item.kbd_focus == term)
ime_enable(&it->item);
}
#endif
}
void
term_ime_disable(struct terminal *term)
{
#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED
if (!term->ime.enabled)
return;
LOG_DBG("IME disabled");
term->ime.enabled = false;
term_ime_reset(term);
/* IME is per seat - disable on all seat currently focusing us */
tll_foreach(term->wl->seats, it) {
if (it->item.kbd_focus == term)
ime_disable(&it->item);
}
#endif
}
void
term_ime_reset(struct terminal *term)
{
#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED
if (term->ime.preedit.cells != NULL) {
free(term->ime.preedit.text);
free(term->ime.preedit.cells);
term->ime.preedit.text = NULL;
term->ime.preedit.cells = NULL;
term->ime.preedit.count = 0;
}
#endif
}

View file

@ -298,6 +298,7 @@ struct terminal {
uint32_t bell_is_urgent:1;
uint32_t alt_screen:1;
uint32_t modify_escape_key:1;
uint32_t ime:1;
} xtsave;
char *window_title;
@ -470,6 +471,23 @@ struct terminal {
unsigned max_height; /* Maximum image height, in pixels */
} sixel;
#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED
struct {
bool enabled;
struct {
wchar_t *text;
struct cell *cells;
int count;
struct {
bool hidden;
int start; /* Cell index, inclusive */
int end; /* Cell index, exclusive */
} cursor;
} preedit;
} ime;
#endif
bool quit;
bool is_shutting_down;
void (*shutdown_cb)(void *data, int exit_code);
@ -592,3 +610,8 @@ bool term_scrollback_to_text(
const struct terminal *term, char **text, size_t *len);
bool term_view_to_text(
const struct terminal *term, char **text, size_t *len);
bool term_ime_is_enabled(const struct terminal *term);
void term_ime_enable(struct terminal *term);
void term_ime_disable(struct terminal *term);
void term_ime_reset(struct terminal *term);

View file

@ -18,6 +18,7 @@
#include <tllist.h>
#include <xdg-output-unstable-v1.h>
#include <xdg-decoration-unstable-v1.h>
#include <text-input-unstable-v3.h>
#define LOG_MODULE "wayland"
#define LOG_ENABLE_DBG 0
@ -25,6 +26,7 @@
#include "config.h"
#include "terminal.h"
#include "ime.h"
#include "input.h"
#include "render.h"
#include "selection.h"
@ -111,6 +113,25 @@ seat_add_primary_selection(struct seat *seat)
primary_selection_device, &primary_selection_device_listener, seat);
}
static void
seat_add_text_input(struct seat *seat)
{
#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED
if (seat->wayl->text_input_manager == NULL)
return;
struct zwp_text_input_v3 *text_input
= zwp_text_input_manager_v3_get_text_input(
seat->wayl->text_input_manager, seat->wl_seat);
if (text_input == NULL)
return;
seat->wl_text_input = text_input;
zwp_text_input_v3_add_listener(text_input, &text_input_listener, seat);
#endif
}
static void
seat_destroy(struct seat *seat)
{
@ -165,9 +186,16 @@ seat_destroy(struct seat *seat)
wl_keyboard_release(seat->wl_keyboard);
if (seat->wl_pointer != NULL)
wl_pointer_release(seat->wl_pointer);
#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED
if (seat->wl_text_input != NULL)
zwp_text_input_v3_destroy(seat->wl_text_input);
#endif
if (seat->wl_seat != NULL)
wl_seat_release(seat->wl_seat);
ime_reset(seat);
free(seat->clipboard.text);
free(seat->primary.text);
free(seat->name);
@ -815,6 +843,7 @@ handle_global(void *data, struct wl_registry *registry,
seat_add_data_device(seat);
seat_add_primary_selection(seat);
seat_add_text_input(seat);
wl_seat_add_listener(wl_seat, &seat_listener, seat);
}
@ -895,6 +924,20 @@ handle_global(void *data, struct wl_registry *registry,
wayl->presentation, &presentation_listener, wayl);
}
}
#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED
else if (strcmp(interface, zwp_text_input_manager_v3_interface.name) == 0) {
const uint32_t required = 1;
if (!verify_iface_version(interface, version, required))
return;
wayl->text_input_manager = wl_registry_bind(
wayl->registry, name, &zwp_text_input_manager_v3_interface, required);
tll_foreach(wayl->seats, it)
seat_add_text_input(&it->item);
}
#endif
}
static void
@ -1154,6 +1197,11 @@ wayl_destroy(struct wayland *wayl)
seat_destroy(&it->item);
tll_free(wayl->seats);
#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED
if (wayl->text_input_manager != NULL)
zwp_text_input_manager_v3_destroy(wayl->text_input_manager);
#endif
if (wayl->xdg_output_manager != NULL)
zxdg_output_manager_v1_destroy(wayl->xdg_output_manager);
if (wayl->shell != NULL)

View file

@ -15,6 +15,9 @@
#include "fdm.h"
/* Forward declarations */
struct terminal;
typedef tll(xkb_keycode_t) xkb_keycode_list_t;
struct key_binding {
@ -220,6 +223,35 @@ struct seat {
struct wl_clipboard clipboard;
struct wl_primary primary;
#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED
/* Input Method Editor */
struct zwp_text_input_v3 *wl_text_input;
struct {
struct {
struct {
char *text;
int32_t cursor_begin;
int32_t cursor_end;
} pending;
} preedit;
struct {
struct {
char *text;
} pending;
} commit;
struct {
struct {
uint32_t before_length;
uint32_t after_length;
} pending;
} surrounding;
uint32_t serial;
} ime;
#endif
};
enum csd_surface {
@ -374,6 +406,10 @@ struct wayland {
struct wp_presentation *presentation;
uint32_t presentation_clock_id;
#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED
struct zwp_text_input_manager_v3 *text_input_manager;
#endif
bool have_argb8888;
tll(struct monitor) monitors; /* All available outputs */
tll(struct seat) seats;