mirror of
https://codeberg.org/dnkl/foot.git
synced 2026-02-10 04:27:45 -05:00
commit
0263a152d4
20 changed files with 1101 additions and 99 deletions
|
|
@ -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
|
||||
|
|
|
|||
10
INSTALL.md
10
INSTALL.md
|
|
@ -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
|
||||
|
|
|
|||
12
client.c
12
client.c
|
|
@ -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
9
csi.c
|
|
@ -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
12
foot-features.h
Normal 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
376
ime.c
Normal 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
18
ime.h
Normal 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);
|
||||
2
input.c
2
input.c
|
|
@ -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
14
main.c
|
|
@ -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;
|
||||
|
|
|
|||
19
meson.build
19
meson.build
|
|
@ -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
1
meson_options.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
option('ime', type: 'boolean', value: true, description: 'IME (Input Method Editor) support')
|
||||
|
|
@ -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
499
render.c
|
|
@ -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;
|
||||
|
|
|
|||
26
search.c
26
search.c
|
|
@ -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);
|
||||
|
|
|
|||
1
search.h
1
search.h
|
|
@ -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
2
shm.c
|
|
@ -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);
|
||||
|
|
|
|||
83
terminal.c
83
terminal.c
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
23
terminal.h
23
terminal.h
|
|
@ -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);
|
||||
|
|
|
|||
48
wayland.c
48
wayland.c
|
|
@ -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)
|
||||
|
|
|
|||
36
wayland.h
36
wayland.h
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue