From d01a1ff0348174f91bb2d3ba53145cc2c9f50a7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=99emysl=20Eric=20Janouch?= Date: Mon, 19 Jun 2023 13:02:44 +0200 Subject: [PATCH] Turn liberty-tui into a terminal/X11 hybrid Importing code from nncmpp, adjusting it to work with hex as well. --- LICENSE | 2 +- liberty-tui.c | 270 ------ liberty-xui.c | 2197 +++++++++++++++++++++++++++++++++++++++++++++++++ tests/fuzz.c | 21 - 4 files changed, 2198 insertions(+), 292 deletions(-) delete mode 100644 liberty-tui.c create mode 100644 liberty-xui.c diff --git a/LICENSE b/LICENSE index 4b31682..0610bbd 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2014 - 2022, Přemysl Eric Janouch +Copyright (c) 2014 - 2023, Přemysl Eric Janouch Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. diff --git a/liberty-tui.c b/liberty-tui.c deleted file mode 100644 index 2f54f6d..0000000 --- a/liberty-tui.c +++ /dev/null @@ -1,270 +0,0 @@ -/* - * liberty-tui.c: the ultimate C unlibrary: TUI - * - * Copyright (c) 2016 - 2017, Přemysl Eric Janouch - * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY - * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION - * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN - * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - * - */ - -// This file includes some common stuff to build TUI applications with - -#include - -// It is surprisingly hard to find a good library to handle Unicode shenanigans, -// and there's enough of those for it to be impractical to reimplement them. -// -// GLib ICU libunistring utf8proc -// Decently sized . . x x -// Grapheme breaks . x . x -// Character width x . x x -// Locale handling . . x . -// Liberal license . x . x -// -// Also note that the ICU API is icky and uses UTF-16 for its primary encoding. -// -// Currently we're chugging along with libunistring but utf8proc seems viable. -// Non-Unicode locales can mostly be handled with simple iconv like in sdtui. -// Similarly grapheme breaks can be guessed at using character width (a basic -// test here is Zalgo text). -// -// None of this is ever going to work too reliably anyway because terminals -// and Unicode don't go awfully well together. In particular, character cell -// devices have some problems with double-wide characters. - -#include -#include -#include -#include - -// --- Configurable display attributes ----------------------------------------- - -struct attrs -{ - short fg; ///< Foreground colour index - short bg; ///< Background colour index - chtype attrs; ///< Other attributes -}; - -/// Decode attributes in the value using a subset of the git config format, -/// ignoring all errors since it doesn't affect functionality -static struct attrs -attrs_decode (const char *value) -{ - struct strv v = strv_make (); - cstr_split (value, " ", true, &v); - - int colors = 0; - struct attrs attrs = { -1, -1, 0 }; - for (char **it = v.vector; *it; it++) - { - char *end = NULL; - long n = strtol (*it, &end, 10); - if (*it != end && !*end && n >= SHRT_MIN && n <= SHRT_MAX) - { - if (colors == 0) attrs.fg = n; - if (colors == 1) attrs.bg = n; - colors++; - } - else if (!strcmp (*it, "bold")) attrs.attrs |= A_BOLD; - else if (!strcmp (*it, "dim")) attrs.attrs |= A_DIM; - else if (!strcmp (*it, "ul")) attrs.attrs |= A_UNDERLINE; - else if (!strcmp (*it, "blink")) attrs.attrs |= A_BLINK; - else if (!strcmp (*it, "reverse")) attrs.attrs |= A_REVERSE; -#ifdef A_ITALIC - else if (!strcmp (*it, "italic")) attrs.attrs |= A_ITALIC; -#endif // A_ITALIC - } - strv_free (&v); - return attrs; -} - -// --- Terminal output --------------------------------------------------------- - -// Necessary abstraction to simplify aligned, formatted character output - -// This callback you need to implement in the application -static bool app_is_character_in_locale (ucs4_t ch); - -struct row_char -{ - ucs4_t c; ///< Unicode codepoint - chtype attrs; ///< Special attributes - int width; ///< How many cells this takes -}; - -struct row_buffer -{ - ARRAY (struct row_char, chars) ///< Characters - int total_width; ///< Total width of all characters -}; - -static struct row_buffer -row_buffer_make (void) -{ - struct row_buffer self = {}; - ARRAY_INIT_SIZED (self.chars, 256); - return self; -} - -static void -row_buffer_free (struct row_buffer *self) -{ - free (self->chars); -} - -/// Replace invalid chars and push all codepoints to the array w/ attributes. -static void -row_buffer_append (struct row_buffer *self, const char *str, chtype attrs) -{ - // The encoding is only really used internally for some corner cases - const char *encoding = locale_charset (); - - // Note that this function is a hotspot, try to keep it decently fast - struct row_char current = { .attrs = attrs }; - struct row_char invalid = { .attrs = attrs, .c = '?', .width = 1 }; - const uint8_t *next = (const uint8_t *) str; - while ((next = u8_next (¤t.c, next))) - { - current.width = uc_width (current.c, encoding); - if (current.width < 0 || !app_is_character_in_locale (current.c)) - current = invalid; - - ARRAY_RESERVE (self->chars, 1); - self->chars[self->chars_len++] = current; - self->total_width += current.width; - } -} - -static void -row_buffer_append_args (struct row_buffer *self, const char *s, ...) - ATTRIBUTE_SENTINEL; - -static void -row_buffer_append_args (struct row_buffer *self, const char *s, ...) -{ - va_list ap; - va_start (ap, s); - - while (s) - { - row_buffer_append (self, s, va_arg (ap, chtype)); - s = va_arg (ap, const char *); - } - va_end (ap); -} - -static void -row_buffer_append_buffer (struct row_buffer *self, const struct row_buffer *rb) -{ - ARRAY_RESERVE (self->chars, rb->chars_len); - memcpy (self->chars + self->chars_len, rb->chars, - rb->chars_len * sizeof *rb->chars); - - self->chars_len += rb->chars_len; - self->total_width += rb->total_width; -} - -/// Pop as many codepoints as needed to free up "space" character cells. -/// Given the suffix nature of combining marks, this should work pretty fine. -static int -row_buffer_pop_cells (struct row_buffer *self, int space) -{ - int made = 0; - while (self->chars_len && made < space) - made += self->chars[--self->chars_len].width; - self->total_width -= made; - return made; -} - -static void -row_buffer_space (struct row_buffer *self, int width, chtype attrs) -{ - if (width < 0) - return; - - ARRAY_RESERVE (self->chars, (size_t) width); - - struct row_char space = { .attrs = attrs, .c = ' ', .width = 1 }; - self->total_width += width; - while (width-- > 0) - self->chars[self->chars_len++] = space; -} - -static void -row_buffer_ellipsis (struct row_buffer *self, int target) -{ - if (self->total_width <= target - || !row_buffer_pop_cells (self, self->total_width - target)) - return; - - // We use attributes from the last character we've removed, - // assuming that we don't shrink the array (and there's no real need) - ucs4_t ellipsis = 0x2026; // … - if (app_is_character_in_locale (ellipsis)) - { - if (self->total_width >= target) - row_buffer_pop_cells (self, 1); - if (self->total_width + 1 <= target) - row_buffer_append (self, "…", self->chars[self->chars_len].attrs); - } - else if (target >= 3) - { - if (self->total_width >= target) - row_buffer_pop_cells (self, 3); - if (self->total_width + 3 <= target) - row_buffer_append (self, "...", self->chars[self->chars_len].attrs); - } -} - -static void -row_buffer_align (struct row_buffer *self, int target, chtype attrs) -{ - row_buffer_ellipsis (self, target); - row_buffer_space (self, target - self->total_width, attrs); -} - -static void -row_buffer_print (uint32_t *ucs4, chtype attrs) -{ - // This assumes that we can reset the attribute set without consequences - char *str = u32_strconv_to_locale (ucs4); - if (str) - { - attrset (attrs); - addstr (str); - attrset (0); - free (str); - } -} - -static void -row_buffer_flush (struct row_buffer *self) -{ - if (!self->chars_len) - return; - - // We only NUL-terminate the chunks because of the libunistring API - uint32_t chunk[self->chars_len + 1], *insertion_point = chunk; - for (size_t i = 0; i < self->chars_len; i++) - { - struct row_char *iter = self->chars + i; - if (i && iter[0].attrs != iter[-1].attrs) - { - row_buffer_print (chunk, iter[-1].attrs); - insertion_point = chunk; - } - *insertion_point++ = iter->c; - *insertion_point = 0; - } - row_buffer_print (chunk, self->chars[self->chars_len - 1].attrs); -} diff --git a/liberty-xui.c b/liberty-xui.c new file mode 100644 index 0000000..b6b5e24 --- /dev/null +++ b/liberty-xui.c @@ -0,0 +1,2197 @@ +/* + * liberty-xui.c: the ultimate C unlibrary: hybrid terminal/X11 UI + * + * Copyright (c) 2016 - 2023, Přemysl Eric Janouch + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION + * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN + * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + */ + +// This file includes some common stuff to build terminal/X11 applications with. + +#include + +// It is surprisingly hard to find a good library to handle Unicode shenanigans, +// and there's enough of those for it to be impractical to reimplement them. +// +// GLib ICU libunistring utf8proc +// Decently sized . . x x +// Grapheme breaks . x . x +// Character width x . x x +// Locale handling . . x . +// Liberal license . x . x +// +// Also note that the ICU API is icky and uses UTF-16 for its primary encoding. +// +// Currently we're chugging along with libunistring but utf8proc seems viable. +// Non-Unicode locales can mostly be handled with simple iconv like in tdv. +// Similarly grapheme breaks can be guessed at using character width (a basic +// test here is Zalgo text). +// +// None of this is ever going to work too reliably anyway because terminals +// and Unicode don't go awfully well together. In particular, character cell +// devices have some problems with double-wide characters. + +#include +#include +#include +#include + +#include +#ifndef TIOCGWINSZ +#include +#endif // ! TIOCGWINSZ + +// ncurses is notoriously retarded for input handling, and in past versions +// used to process mouse events unreliably. Moreover, rxvt-unicode only +// supports the 1006 mode that ncurses also supports mode starting with 9.25. +#include "termo.h" + +// Carefully chosen to limit the possibility of ever hitting termo keymods. +enum { XUI_KEYMOD_DOUBLE_CLICK = 1 << 15 }; + +// Elementary port of TUI facilities to X11. +#ifdef LIBERTY_XUI_WANT_X11 +#include +#include +#include +#include +#include +#endif // LIBERTY_XUI_WANT_X11 + +// The application needs to implement these. +static void app_quit (void); +static void app_layout (void); +static bool app_process_termo_event (termo_key_t *event); +static bool app_process_mouse (termo_mouse_event_t type, + int x, int y, int button, int modifiers); +static bool app_on_insufficient_color (void); +static void app_on_clipboard_copy (const char *text); + +// This could be overridable, however thus far row_buffer and line_editor both +// depend on XUI being initialized. +static bool xui_is_character_in_locale (ucs4_t ch); + +// --- Utilities --------------------------------------------------------------- + +// Unlike poller_timers_get_current_time(), this has a hard dependency +// on _POSIX_TIMERS, and can be used with both realtime nad monotonic clocks. +static int64_t +clock_msec (clockid_t clock) +{ + struct timespec tp; + hard_assert (clock_gettime (clock, &tp) != -1); + return (int64_t) tp.tv_sec * 1000 + (int64_t) tp.tv_nsec / 1000000; +} + +// --- Configurable display attributes ----------------------------------------- + +struct attrs +{ + short fg; ///< Foreground colour index + short bg; ///< Background colour index + chtype attrs; ///< Other attributes +}; + +/// Decode attributes in the value using a subset of the git config format, +/// ignoring all errors since it doesn't affect functionality +static struct attrs +attrs_decode (const char *value) +{ + struct strv v = strv_make (); + cstr_split (value, " ", true, &v); + + int colors = 0; + struct attrs attrs = { -1, -1, 0 }; + for (char **it = v.vector; *it; it++) + { + char *end = NULL; + long n = strtol (*it, &end, 10); + if (*it != end && !*end && n >= SHRT_MIN && n <= SHRT_MAX) + { + if (colors == 0) attrs.fg = n; + if (colors == 1) attrs.bg = n; + colors++; + } + else if (!strcmp (*it, "bold")) attrs.attrs |= A_BOLD; + else if (!strcmp (*it, "dim")) attrs.attrs |= A_DIM; + else if (!strcmp (*it, "ul")) attrs.attrs |= A_UNDERLINE; + else if (!strcmp (*it, "blink")) attrs.attrs |= A_BLINK; + else if (!strcmp (*it, "reverse")) attrs.attrs |= A_REVERSE; +#ifdef A_ITALIC + else if (!strcmp (*it, "italic")) attrs.attrs |= A_ITALIC; +#endif // A_ITALIC + } + strv_free (&v); + return attrs; +} + +// --- Line editor ------------------------------------------------------------- + +enum line_editor_action +{ + LINE_EDITOR_B_CHAR, ///< Go back a character + LINE_EDITOR_F_CHAR, ///< Go forward a character + LINE_EDITOR_B_WORD, ///< Go back a word + LINE_EDITOR_F_WORD, ///< Go forward a word + LINE_EDITOR_HOME, ///< Go to start of line + LINE_EDITOR_END, ///< Go to end of line + + LINE_EDITOR_UPCASE_WORD, ///< Convert word to uppercase + LINE_EDITOR_DOWNCASE_WORD, ///< Convert word to lowercase + LINE_EDITOR_CAPITALIZE_WORD, ///< Capitalize word + + LINE_EDITOR_B_DELETE, ///< Delete last character + LINE_EDITOR_F_DELETE, ///< Delete next character + LINE_EDITOR_B_KILL_WORD, ///< Delete last word + LINE_EDITOR_B_KILL_LINE, ///< Delete everything up to BOL + LINE_EDITOR_F_KILL_LINE, ///< Delete everything up to EOL +}; + +struct line_editor +{ + int point; ///< Caret index into line data + ucs4_t *line; ///< Line data, 0-terminated + int *w; ///< Codepoint widths, 0-terminated + size_t len; ///< Editor length + size_t alloc; ///< Editor allocated + char prompt; ///< Prompt character + + void (*on_changed) (void); ///< Callback on text change + void (*on_end) (bool); ///< Callback on abort +}; + +static void +line_editor_free (struct line_editor *self) +{ + free (self->line); + free (self->w); +} + +/// Notify whomever invoked the editor that it's been either confirmed or +/// cancelled and clean up editor state +static void +line_editor_abort (struct line_editor *self, bool status) +{ + self->on_end (status); + self->on_changed = NULL; + + free (self->line); + self->line = NULL; + free (self->w); + self->w = NULL; + self->alloc = 0; + self->len = 0; + self->point = 0; + self->prompt = 0; +} + +/// Start the line editor; remember to fill in "change" and "end" callbacks +static void +line_editor_start (struct line_editor *self, char prompt) +{ + self->alloc = 16; + self->line = xcalloc (sizeof *self->line, self->alloc); + self->w = xcalloc (sizeof *self->w, self->alloc); + self->len = 0; + self->point = 0; + self->prompt = prompt; +} + +static void +line_editor_changed (struct line_editor *self) +{ + self->line[self->len] = 0; + self->w[self->len] = 0; + + if (self->on_changed) + self->on_changed (); +} + +static void +line_editor_move (struct line_editor *self, int to, int from, int len) +{ + memmove (self->line + to, self->line + from, + sizeof *self->line * len); + memmove (self->w + to, self->w + from, + sizeof *self->w * len); +} + +static void +line_editor_insert (struct line_editor *self, ucs4_t codepoint) +{ + while (self->alloc - self->len < 2 /* inserted + sentinel */) + { + self->alloc <<= 1; + self->line = xreallocarray + (self->line, sizeof *self->line, self->alloc); + self->w = xreallocarray + (self->w, sizeof *self->w, self->alloc); + } + + line_editor_move (self, self->point + 1, self->point, + self->len - self->point); + self->line[self->point] = codepoint; + self->w[self->point] = xui_is_character_in_locale (codepoint) + ? uc_width (codepoint, locale_charset ()) + : 1 /* the replacement question mark */; + + self->point++; + self->len++; + line_editor_changed (self); +} + +static bool +line_editor_action (struct line_editor *self, enum line_editor_action action) +{ + switch (action) + { + default: + return soft_assert (!"unknown line editor action"); + + case LINE_EDITOR_B_CHAR: + if (self->point < 1) + return false; + do self->point--; + while (self->point > 0 + && !self->w[self->point]); + return true; + case LINE_EDITOR_F_CHAR: + if (self->point + 1 > (int) self->len) + return false; + do self->point++; + while (self->point < (int) self->len + && !self->w[self->point]); + return true; + case LINE_EDITOR_B_WORD: + { + if (self->point < 1) + return false; + int i = self->point; + while (i && self->line[--i] == ' '); + while (i-- && self->line[i] != ' '); + self->point = ++i; + return true; + } + case LINE_EDITOR_F_WORD: + { + if (self->point + 1 > (int) self->len) + return false; + int i = self->point; + while (i < (int) self->len && self->line[i] == ' ') i++; + while (i < (int) self->len && self->line[i] != ' ') i++; + self->point = i; + return true; + } + case LINE_EDITOR_HOME: + self->point = 0; + return true; + case LINE_EDITOR_END: + self->point = self->len; + return true; + + case LINE_EDITOR_UPCASE_WORD: + { + int i = self->point; + for (; i < (int) self->len && self->line[i] == ' '; i++); + for (; i < (int) self->len && self->line[i] != ' '; i++) + self->line[i] = uc_toupper (self->line[i]); + self->point = i; + line_editor_changed (self); + return true; + } + case LINE_EDITOR_DOWNCASE_WORD: + { + int i = self->point; + for (; i < (int) self->len && self->line[i] == ' '; i++); + for (; i < (int) self->len && self->line[i] != ' '; i++) + self->line[i] = uc_tolower (self->line[i]); + self->point = i; + line_editor_changed (self); + return true; + } + case LINE_EDITOR_CAPITALIZE_WORD: + { + int i = self->point; + ucs4_t (*converter) (ucs4_t) = uc_totitle; + for (; i < (int) self->len && self->line[i] == ' '; i++); + for (; i < (int) self->len && self->line[i] != ' '; i++) + { + self->line[i] = converter (self->line[i]); + converter = uc_tolower; + } + self->point = i; + line_editor_changed (self); + return true; + } + + case LINE_EDITOR_B_DELETE: + { + if (self->point < 1) + return false; + int len = 1; + while (self->point - len > 0 + && !self->w[self->point - len]) + len++; + line_editor_move (self, self->point - len, self->point, + self->len - self->point); + self->len -= len; + self->point -= len; + line_editor_changed (self); + return true; + } + case LINE_EDITOR_F_DELETE: + { + if (self->point + 1 > (int) self->len) + return false; + int len = 1; + while (self->point + len < (int) self->len + && !self->w[self->point + len]) + len++; + self->len -= len; + line_editor_move (self, self->point, self->point + len, + self->len - self->point); + line_editor_changed (self); + return true; + } + case LINE_EDITOR_B_KILL_WORD: + { + if (self->point < 1) + return false; + + int i = self->point; + while (i && self->line[--i] == ' '); + while (i-- && self->line[i] != ' '); + i++; + + line_editor_move (self, i, self->point, (self->len - self->point)); + self->len -= self->point - i; + self->point = i; + line_editor_changed (self); + return true; + } + case LINE_EDITOR_B_KILL_LINE: + self->len -= self->point; + line_editor_move (self, 0, self->point, self->len); + self->point = 0; + line_editor_changed (self); + return true; + case LINE_EDITOR_F_KILL_LINE: + self->len = self->point; + line_editor_changed (self); + return true; + } +} + +// --- Terminal output --------------------------------------------------------- + +// Necessary abstraction to simplify aligned, formatted character output + +struct row_char +{ + ucs4_t c; ///< Unicode codepoint + chtype attrs; ///< Special attributes + int width; ///< How many cells this takes +}; + +struct row_buffer +{ + ARRAY (struct row_char, chars) ///< Characters + int total_width; ///< Total width of all characters +}; + +static struct row_buffer +row_buffer_make (void) +{ + struct row_buffer self = {}; + ARRAY_INIT_SIZED (self.chars, 256); + return self; +} + +static void +row_buffer_free (struct row_buffer *self) +{ + free (self->chars); +} + +static void +row_buffer_append_c (struct row_buffer *self, ucs4_t c, chtype attrs) +{ + struct row_char current = { .attrs = attrs, .c = c }; + struct row_char invalid = { .attrs = attrs, .c = '?', .width = 1 }; + + current.width = uc_width (current.c, locale_charset ()); + if (current.width < 0 || !xui_is_character_in_locale (current.c)) + current = invalid; + + ARRAY_RESERVE (self->chars, 1); + self->chars[self->chars_len++] = current; + self->total_width += current.width; +} + +/// Replace invalid chars and push all codepoints to the array w/ attributes. +static void +row_buffer_append (struct row_buffer *self, const char *str, chtype attrs) +{ + // The encoding is only really used internally for some corner cases + const char *encoding = locale_charset (); + + // Note that this function is a hotspot, try to keep it decently fast + struct row_char current = { .attrs = attrs }; + struct row_char invalid = { .attrs = attrs, .c = '?', .width = 1 }; + const uint8_t *next = (const uint8_t *) str; + while ((next = u8_next (¤t.c, next))) + { + current.width = uc_width (current.c, encoding); + if (current.width < 0 || !xui_is_character_in_locale (current.c)) + current = invalid; + + ARRAY_RESERVE (self->chars, 1); + self->chars[self->chars_len++] = current; + self->total_width += current.width; + } +} + +/// Pop as many codepoints as needed to free up "space" character cells. +/// Given the suffix nature of combining marks, this should work pretty fine. +static int +row_buffer_pop_cells (struct row_buffer *self, int space) +{ + int made = 0; + while (self->chars_len && made < space) + made += self->chars[--self->chars_len].width; + self->total_width -= made; + return made; +} + +static void +row_buffer_space (struct row_buffer *self, int width, chtype attrs) +{ + if (width < 0) + return; + + ARRAY_RESERVE (self->chars, (size_t) width); + + struct row_char space = { .attrs = attrs, .c = ' ', .width = 1 }; + self->total_width += width; + while (width-- > 0) + self->chars[self->chars_len++] = space; +} + +static void +row_buffer_ellipsis (struct row_buffer *self, int target) +{ + if (self->total_width <= target + || !row_buffer_pop_cells (self, self->total_width - target)) + return; + + // We use attributes from the last character we've removed, + // assuming that we don't shrink the array (and there's no real need) + ucs4_t ellipsis = 0x2026; // … + if (xui_is_character_in_locale (ellipsis)) + { + if (self->total_width >= target) + row_buffer_pop_cells (self, 1); + if (self->total_width + 1 <= target) + row_buffer_append (self, "…", self->chars[self->chars_len].attrs); + } + else if (target >= 3) + { + if (self->total_width >= target) + row_buffer_pop_cells (self, 3); + if (self->total_width + 3 <= target) + row_buffer_append (self, "...", self->chars[self->chars_len].attrs); + } +} + +static void +row_buffer_align (struct row_buffer *self, int target, chtype attrs) +{ + row_buffer_ellipsis (self, target); + row_buffer_space (self, target - self->total_width, attrs); +} + +static void +row_buffer_print (uint32_t *ucs4, chtype attrs) +{ + // This assumes that we can reset the attribute set without consequences + char *str = u32_strconv_to_locale (ucs4); + if (str) + { + attrset (attrs); + addstr (str); + attrset (0); + free (str); + } +} + +static void +row_buffer_flush (struct row_buffer *self) +{ + if (!self->chars_len) + return; + + // We only NUL-terminate the chunks because of the libunistring API + uint32_t chunk[self->chars_len + 1], *insertion_point = chunk; + for (size_t i = 0; i < self->chars_len; i++) + { + struct row_char *iter = self->chars + i; + if (i && iter[0].attrs != iter[-1].attrs) + { + row_buffer_print (chunk, iter[-1].attrs); + insertion_point = chunk; + } + *insertion_point++ = iter->c; + *insertion_point = 0; + } + row_buffer_print (chunk, self->chars[self->chars_len - 1].attrs); +} + +// --- XUI --------------------------------------------------------------------- + +struct widget; + +/// Draw a widget on the window +typedef void (*widget_render_fn) (struct widget *self); + +/// The widget has been placed +typedef void (*widget_allocated_fn) (struct widget *self); + +/// Extended attributes +enum { XUI_ATTR_MONOSPACE = 1 << 0 }; + +/// A minimal abstraction appropriate for both TUI and GUI widgets. +/// Units for the widget's region are frontend-specific. +/// Having this as a linked list simplifies layouting and memory management. +struct widget +{ + LIST_HEADER (struct widget) + + int x; ///< X coordinate + int y; ///< Y coordinate + int width; ///< Width, initialized by UI methods + int height; ///< Height, initialized by UI methods + + widget_render_fn on_render; ///< Render callback + widget_allocated_fn on_allocated; ///< Allocation callback + struct widget *children; ///< Child widgets of containers + chtype attrs; ///< Rendition, in Curses terms + unsigned extended_attrs; ///< XUI-specific attributes + + int id; ///< Post-layouting identification + int userdata; ///< Action ID/Tab index/... + char text[]; ///< Any text label +}; + +static void +widget_destroy (struct widget *self) +{ + LIST_FOR_EACH (struct widget, w, self->children) + widget_destroy (w); + free (self); +} + +static void +widget_move (struct widget *w, int dx, int dy) +{ + w->x += dx; + w->y += dy; + LIST_FOR_EACH (struct widget, child, w->children) + widget_move (child, dx, dy); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +struct ui +{ + struct widget *(*padding) + (chtype attrs, float width, float height); + struct widget *(*label) + (chtype attrs, unsigned extended, const char *label); + + void (*render) (void); + void (*flip) (void); + void (*winch) (void); + void (*destroy) (void); +}; + +#ifdef LIBERTY_XUI_WANT_X11 + +/// Wraps Xft fonts into a linked list with fallbacks. +struct x11_font_link +{ + struct x11_font_link *next; + XftFont *font; +}; + +enum +{ + X11_FONT_BOLD = 1 << 0, + X11_FONT_ITALIC = 1 << 1, + X11_FONT_MONOSPACE = 1 << 2, +}; + +struct x11_font +{ + struct x11_font *next; ///< Next in a linked list + + struct x11_font_link *list; ///< Fonts of varying Unicode coverage + unsigned style; ///< X11_FONT_* flags + FcPattern *pattern; ///< Original unsubstituted pattern + FcCharSet *unavailable; ///< Couldn't find a font for these +}; + +#endif // LIBERTY_XUI_WANT_X11 + +struct xui +{ + struct poller_idle refresh_event; ///< Refresh the window's contents + struct poller_idle flip_event; ///< Draw rendered widgets on screen + + // User interface: + + struct ui *ui; ///< User interface interface + struct widget *widgets; ///< Layouted widgets + int width; ///< Window width + int height; ///< Window height + int hunit; ///< Horizontal unit + int vunit; ///< Vertical unit + bool focused; ///< Whether the window has focus + + // Terminal: + + termo_t *tk; ///< termo handle (TUI/X11) + struct poller_fd tty_event; ///< Terminal input event + struct poller_timer tk_timer; ///< termo timeout timer + bool locale_is_utf8; ///< The locale is Unicode + + // X11: + +#ifdef LIBERTY_XUI_WANT_X11 + XIM x11_im; ///< Input method + XIC x11_ic; ///< Input method context + Display *dpy; ///< X display handle + struct poller_fd x11_event; ///< X11 events on wire + struct poller_idle xpending_event; ///< X11 events possibly in I/O queues + int xkb_base_event_code; ///< Xkb base event code + Window x11_window; ///< Application window + Pixmap x11_pixmap; ///< Off-screen bitmap + Region x11_clip; ///< Invalidated region + Picture x11_pixmap_picture; ///< XRender wrap for x11_pixmap + XftDraw *xft_draw; ///< Xft rendering context + struct x11_font *xft_fonts; ///< Font collection + char *x11_selection; ///< CLIPBOARD selection + + const char *x11_fontname; ///< Fontconfig font name + const char *x11_fontname_monospace; ///< Fontconfig monospace font name + XRenderColor *x_fg; ///< Foreground per attribute + XRenderColor *x_bg; ///< Background per attribute +#endif // LIBERTY_XUI_WANT_X11 +} +g_xui; + +static void +xui_invalidate (void) +{ + poller_idle_set (&g_xui.refresh_event); +} + +static bool +xui_process_termo_event (termo_key_t *event) +{ + if (event->type == TERMO_TYPE_FOCUS) + g_xui.focused = !!event->code.focused; + return app_process_termo_event (event); +} + +// --- TUI --------------------------------------------------------------------- + +static void +tui_flush_buffer (struct widget *self, struct row_buffer *buf) +{ + move (self->y, self->x); + + int space = MIN (self->width, g_xui.width - self->x); + row_buffer_align (buf, space, self->attrs); + row_buffer_flush (buf); + row_buffer_free (buf); +} + +static void +tui_render_padding (struct widget *self) +{ + // TODO: This should work even for heights != 1. + struct row_buffer buf = row_buffer_make (); + tui_flush_buffer (self, &buf); +} + +static struct widget * +tui_make_padding (chtype attrs, float width, float height) +{ + struct widget *w = xcalloc (1, sizeof *w + 2); + w->text[0] = ' '; + w->on_render = tui_render_padding; + w->attrs = attrs; + w->width = width * 2; + w->height = height; + return w; +} + +static void +tui_render_label (struct widget *self) +{ + struct row_buffer buf = row_buffer_make (); + row_buffer_append (&buf, self->text, self->attrs); + tui_flush_buffer (self, &buf); +} + +static struct widget * +tui_make_label (chtype attrs, unsigned extended, const char *label) +{ + (void) extended; + + size_t len = strlen (label); + struct widget *w = xcalloc (1, sizeof *w + len + 1); + w->on_render = tui_render_label; + w->attrs = attrs; + w->extended_attrs = extended; + memcpy (w->text, label, len); + + struct row_buffer buf = row_buffer_make (); + row_buffer_append (&buf, w->text, w->attrs); + w->width = buf.total_width; + w->height = 1; + row_buffer_free (&buf); + return w; +} + +static void +tui_render_widgets (struct widget *head) +{ + LIST_FOR_EACH (struct widget, w, head) + { + if (w->width < 0 || w->height < 0) + continue; + if (w->on_render) + w->on_render (w); + tui_render_widgets (w->children); + } +} + +static void +tui_render (void) +{ + erase (); + tui_render_widgets (g_xui.widgets); +} + +static void +tui_flip (void) +{ + // Curses handles double-buffering for us automatically. + refresh (); +} + +static void +tui_winch (void) +{ + // The standard endwin/refresh sequence makes the terminal flicker +#if defined HAVE_RESIZETERM && defined TIOCGWINSZ + struct winsize size; + if (!ioctl (STDOUT_FILENO, TIOCGWINSZ, (char *) &size)) + { + char *row = getenv ("LINES"); + char *col = getenv ("COLUMNS"); + unsigned long tmp; + resizeterm ( + (row && xstrtoul (&tmp, row, 10)) ? tmp : size.ws_row, + (col && xstrtoul (&tmp, col, 10)) ? tmp : size.ws_col); + } +#else // HAVE_RESIZETERM && TIOCGWINSZ + endwin (); + refresh (); +#endif // HAVE_RESIZETERM && TIOCGWINSZ + + g_xui.width = COLS; + g_xui.height = LINES; + xui_invalidate (); +} + +static void +tui_destroy (void) +{ + endwin (); +} + +static struct ui tui_ui = +{ + .padding = tui_make_padding, + .label = tui_make_label, + + .render = tui_render, + .flip = tui_flip, + .winch = tui_winch, + .destroy = tui_destroy, +}; + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static void +tui_on_tty_event (termo_key_t *event, int64_t event_ts) +{ + // Simple double click detection via release--press delay, only a bit + // complicated by the fact that we don't know what's being released + static termo_key_t last_event; + static int64_t last_event_ts; + static int last_button; + + int y, x, button, y_last, x_last, modifiers = 0; + termo_mouse_event_t type, type_last; + if (termo_interpret_mouse (g_xui.tk, event, &type, &button, &y, &x)) + { + if (termo_interpret_mouse + (g_xui.tk, &last_event, &type_last, NULL, &y_last, &x_last) + && event_ts - last_event_ts < 500 + && type_last == TERMO_MOUSE_RELEASE && type == TERMO_MOUSE_PRESS + && y_last == y && x_last == x && last_button == button) + { + modifiers |= XUI_KEYMOD_DOUBLE_CLICK; + // Prevent interpreting triple clicks as two double clicks. + last_button = 0; + } + else if (type == TERMO_MOUSE_PRESS) + last_button = button; + + if (!app_process_mouse (type, x, y, button, modifiers)) + beep (); + } + else if (!xui_process_termo_event (event)) + beep (); + + last_event = *event; + last_event_ts = event_ts; +} + +static void +tui_on_tty_readable (const struct pollfd *fd, void *user_data) +{ + (void) user_data; + if (fd->revents & ~(POLLIN | POLLHUP | POLLERR)) + print_debug ("fd %d: unexpected revents: %d", fd->fd, fd->revents); + + poller_timer_reset (&g_xui.tk_timer); + termo_advisereadable (g_xui.tk); + + termo_key_t event = {}; + int64_t event_ts = clock_msec (CLOCK_BEST); + termo_result_t res; + while ((res = termo_getkey (g_xui.tk, &event)) == TERMO_RES_KEY) + tui_on_tty_event (&event, event_ts); + + if (res == TERMO_RES_AGAIN) + poller_timer_set (&g_xui.tk_timer, termo_get_waittime (g_xui.tk)); + else if (res == TERMO_RES_ERROR || res == TERMO_RES_EOF) + app_quit (); +} + +static void +tui_on_key_timer (void *user_data) +{ + (void) user_data; + + termo_key_t event; + if (termo_getkey_force (g_xui.tk, &event) == TERMO_RES_KEY) + if (!xui_process_termo_event (&event)) + beep (); +} + +static void +tui_init (struct poller *poller, struct attrs *attrs, size_t attrs_len) +{ + (void) poller; + + poller_fd_set (&g_xui.tty_event, POLLIN); + if (!termo_start (g_xui.tk) || !initscr () || nonl () == ERR) + exit_fatal ("failed to set up the terminal"); + + termo_set_mouse_tracking_mode (g_xui.tk, TERMO_MOUSE_TRACKING_DRAG); + + curs_set (0); + + g_xui.ui = &tui_ui; + g_xui.width = COLS; + g_xui.height = LINES; + g_xui.vunit = 1; + g_xui.hunit = 1; + + // The application should fall back to something at least nearly colourless + if (start_color () == ERR + || use_default_colors () == ERR + || COLOR_PAIRS <= (int) attrs_len) + { + app_on_insufficient_color (); + return; + } + + for (size_t a = 0; a < attrs_len; a++) + { + if (attrs[a].fg >= -1 && attrs[a].bg >= -1 + && init_pair (a + 1, attrs[a].fg, attrs[a].bg) == OK) + attrs[a].attrs |= COLOR_PAIR (a + 1); + else if (app_on_insufficient_color ()) + return; + } +} + +// --- X11 --------------------------------------------------------------------- + +#ifdef LIBERTY_XUI_WANT_X11 + +static XRenderColor x11_default_fg = { .alpha = 0xffff }; +static XRenderColor x11_default_bg = { 0xffff, 0xffff, 0xffff, 0xffff }; +static XErrorHandler x11_default_error_handler; + +static struct x11_font_link * +x11_font_link_new (XftFont *font) +{ + struct x11_font_link *self = xcalloc (1, sizeof *self); + self->font = font; + return self; +} + +static void +x11_font_link_destroy (struct x11_font_link *self) +{ + XftFontClose (g_xui.dpy, self->font); + free (self); +} + +static struct x11_font_link * +x11_font_link_open (FcPattern *pattern) +{ + XftFont *font = XftFontOpenPattern (g_xui.dpy, pattern); + if (!font) + { + FcPatternDestroy (pattern); + return NULL; + } + return x11_font_link_new (font); +} + +static struct x11_font * +x11_font_open (unsigned style) +{ + FcPattern *pattern = (style & X11_FONT_MONOSPACE) + ? FcNameParse ((const FcChar8 *) g_xui.x11_fontname_monospace) + : FcNameParse ((const FcChar8 *) g_xui.x11_fontname); + if (style & X11_FONT_BOLD) + FcPatternAdd (pattern, FC_STYLE, (FcValue) { + .type = FcTypeString, .u.s = (FcChar8 *) "Bold" }, FcFalse); + if (style & X11_FONT_ITALIC) + FcPatternAdd (pattern, FC_STYLE, (FcValue) { + .type = FcTypeString, .u.s = (FcChar8 *) "Italic" }, FcFalse); + + FcPattern *substituted = FcPatternDuplicate (pattern); + FcConfigSubstitute (NULL, substituted, FcMatchPattern); + + FcResult result = 0; + FcPattern *match = XftFontMatch (g_xui.dpy, + DefaultScreen (g_xui.dpy), substituted, &result); + FcPatternDestroy (substituted); + struct x11_font_link *link = NULL; + if (!match || !(link = x11_font_link_open (match))) + { + FcPatternDestroy (pattern); + return NULL; + } + + struct x11_font *self = xcalloc (1, sizeof *self); + self->list = link; + self->style = style; + self->pattern = pattern; + self->unavailable = FcCharSetCreate (); + return self; +} + +static void +x11_font_destroy (struct x11_font *self) +{ + FcPatternDestroy (self->pattern); + FcCharSetDestroy (self->unavailable); + LIST_FOR_EACH (struct x11_font_link, iter, self->list) + x11_font_link_destroy (iter); + free (self); + +} + +/// Find or instantiate a font that can render the character given by cp. +static XftFont * +x11_font_cover_codepoint (struct x11_font *self, ucs4_t cp) +{ + if (FcCharSetHasChar (self->unavailable, cp)) + return self->list->font; + + struct x11_font_link **used = &self->list; + for (; *used; used = &(*used)->next) + if (XftCharExists (g_xui.dpy, (*used)->font, cp)) + return (*used)->font; + + FcCharSet *set = FcCharSetCreate (); + FcCharSetAddChar (set, cp); + FcPattern *needle = FcPatternDuplicate (self->pattern); + FcPatternAddCharSet (needle, FC_CHARSET, set); + FcConfigSubstitute (NULL, needle, FcMatchPattern); + + FcResult result = 0; + FcPattern *match + = XftFontMatch (g_xui.dpy, DefaultScreen (g_xui.dpy), needle, &result); + FcCharSetDestroy (set); + FcPatternDestroy (needle); + if (!match) + goto fail; + + struct x11_font_link *new = x11_font_link_open (match); + if (!new) + goto fail; + + // The reverse may happen simply due to race conditions. + if (XftCharExists (g_xui.dpy, new->font, cp)) + return (*used = new)->font; + + x11_font_link_destroy (new); +fail: + FcCharSetAddChar (self->unavailable, cp); + return self->list->font; +} + +// TODO: Perhaps produce an array of FT_UInt glyph indexes, mainly so that +// x11_font_{hadvance,draw,render}() can use the same data, through the use +// of a new function that collects the spans in a data structure. +static size_t +x11_font_span (struct x11_font *self, const uint8_t *text, XftFont **font) +{ + hard_assert (self->list != NULL); + + // Xft similarly just stops on invalid UTF-8. + ucs4_t cp = 0; + const uint8_t *p = text; + if (!(p = u8_next (&cp, p))) + return 0; + + *font = x11_font_cover_codepoint (self, cp); + for (const uint8_t *end = NULL; (end = u8_next (&cp, p)); p = end) + { + if (x11_font_cover_codepoint (self, cp) != *font) + break; + } + return p - text; +} + +static int +x11_font_draw (struct x11_font *self, XftColor *color, int x, int y, + const char *text) +{ + int advance = 0; + size_t len = 0; + XftFont *font = NULL; + while ((len = x11_font_span (self, (const uint8_t *) text, &font))) + { + if (color) + { + XftDrawStringUtf8 (g_xui.xft_draw, color, font, + x + advance, y + self->list->font->ascent, + (const FcChar8 *) text, len); + } + + XGlyphInfo extents = {}; + XftTextExtentsUtf8 (g_xui.dpy, + font, (const FcChar8 *) text, len, &extents); + text += len; + advance += extents.xOff; + } + return advance; +} + +static int +x11_font_hadvance (struct x11_font *self, const char *text) +{ + return x11_font_draw (self, NULL, 0, 0, text); +} + +static int +x11_font_render (struct x11_font *self, int op, Picture src, int srcx, int srcy, + int x, int y, const char *text) +{ + int advance = 0; + size_t len = 0; + XftFont *font = NULL; + while ((len = x11_font_span (self, (const uint8_t *) text, &font))) + { + if (src) + { + XftTextRenderUtf8 (g_xui.dpy, + op, src, font, g_xui.x11_pixmap_picture, + srcx, srcy, x + advance, y + self->list->font->ascent, + (const FcChar8 *) text, len); + } + + XGlyphInfo extents = {}; + XftTextExtentsUtf8 (g_xui.dpy, + font, (const FcChar8 *) text, len, &extents); + text += len; + advance += extents.xOff; + } + return advance; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static struct x11_font * +x11_widget_font (struct widget *self) +{ + unsigned style = 0; + if (self->attrs & A_BOLD) + style |= X11_FONT_BOLD; + if (self->attrs & A_ITALIC) + style |= X11_FONT_ITALIC; + if (self->extended_attrs & XUI_ATTR_MONOSPACE) + style |= X11_FONT_MONOSPACE; + + struct x11_font **font = &g_xui.xft_fonts; + for (; *font; font = &(*font)->next) + if ((*font)->style == style) + return *font; + if ((*font = x11_font_open (style))) + return *font; + + // But FontConfig has a tendency to always return something. + return g_xui.xft_fonts; +} + +static XRenderColor * +x11_fg_attrs (chtype attrs) +{ + int pair = PAIR_NUMBER (attrs); + if (!pair--) + return &x11_default_fg; + return (attrs & A_REVERSE) ? &g_xui.x_bg[pair] : &g_xui.x_fg[pair]; +} + +static XRenderColor * +x11_fg (struct widget *self) +{ + return x11_fg_attrs (self->attrs); +} + +static XRenderColor * +x11_bg_attrs (chtype attrs) +{ + int pair = PAIR_NUMBER (attrs); + if (!pair--) + return &x11_default_bg; + return (attrs & A_REVERSE) ? &g_xui.x_fg[pair] : &g_xui.x_bg[pair]; +} + +static XRenderColor * +x11_bg (struct widget *self) +{ + return x11_bg_attrs (self->attrs); +} + +static void +x11_render_padding (struct widget *self) +{ + if (PAIR_NUMBER (self->attrs)) + { + XRenderFillRectangle (g_xui.dpy, PictOpSrc, g_xui.x11_pixmap_picture, + x11_bg (self), self->x, self->y, self->width, self->height); + } + if (self->attrs & A_UNDERLINE) + { + XRenderFillRectangle (g_xui.dpy, PictOpSrc, g_xui.x11_pixmap_picture, + x11_fg (self), self->x, self->y + self->height - 1, self->width, 1); + } +} + +static struct widget * +x11_make_padding (chtype attrs, float width, float height) +{ + struct widget *w = xcalloc (1, sizeof *w + 2); + w->text[0] = ' '; + w->on_render = x11_render_padding; + w->attrs = attrs; + w->width = g_xui.vunit * width; + w->height = g_xui.vunit * height; + return w; +} + +static void +x11_render_label (struct widget *self) +{ + x11_render_padding (self); + + int space = MIN (self->width, g_xui.width - self->x); + if (space <= 0) + return; + + // TODO: Try to avoid re-measuring on each render. + struct x11_font *font = x11_widget_font (self); + int advance = x11_font_hadvance (font, self->text); + if (advance <= space) + { + XftColor color = { .color = *x11_fg (self) }; + x11_font_draw (font, &color, self->x, self->y, self->text); + return; + } + + // XRender doesn't extend gradients beyond their end stops. + XRenderColor solid = *x11_fg (self), colors[3] = { solid, solid, solid }; + colors[2].alpha = 0; + + double portion = MIN (1, 2.0 * font->list->font->height / space); + XFixed stops[3] = { 0, XDoubleToFixed (1 - portion), XDoubleToFixed (1) }; + XLinearGradient gradient = { {}, { XDoubleToFixed (space), 0 } }; + + // Note that this masking is a very expensive operation. + Picture source = + XRenderCreateLinearGradient (g_xui.dpy, &gradient, stops, colors, 3); + x11_font_render (font, PictOpOver, source, -self->x, 0, self->x, self->y, + self->text); + XRenderFreePicture (g_xui.dpy, source); +} + +static struct widget * +x11_make_label (chtype attrs, unsigned extended, const char *label) +{ + // Xft renders combining marks by themselves, NFC improves it a bit. + // We'd have to use HarfBuzz to do this correctly. + size_t label_len = strlen (label) + 1, normalized_len = 0; + uint8_t *normalized = u8_normalize (UNINORM_NFC, + (const uint8_t *) label, label_len, NULL, &normalized_len); + if (!normalized) + { + normalized = memcpy (xmalloc (label_len), label, label_len); + normalized_len = label_len; + } + + struct widget *w = xcalloc (1, sizeof *w + normalized_len); + w->on_render = x11_render_label; + w->attrs = attrs; + w->extended_attrs = extended; + memcpy (w->text, normalized, normalized_len); + free (normalized); + + struct x11_font *font = x11_widget_font (w); + w->width = x11_font_hadvance (font, w->text); + w->height = font->list->font->height; + return w; +} + +static void +x11_render_widget (struct widget *w, const XRectangle *clip) +{ + if (w->width < 0 || w->height < 0) + return; + + // Children may set their own clips, so reset before each sibling. + // We need to go through Xft, or XftTextRenderUtf8() might skip glyphs. + if (clip) + XftDrawSetClipRectangles (g_xui.xft_draw, 0, 0, clip, 1); + else + XftDrawSetClip (g_xui.xft_draw, None); + + if (w->on_render) + w->on_render (w); + + // We set clips on containers, not on individual widgets. + XRectangle subclip = { w->x, w->y, w->width, w->height }; + if (clip) + { + int x1 = MAX (clip->x, w->x); + int y1 = MAX (clip->y, w->y); + int x2 = MIN (clip->x + clip->width, w->x + w->width); + int y2 = MIN (clip->y + clip->height, w->y + w->height); + if (x1 >= x2 || y1 >= y2) + return; + + subclip.x = x1; + subclip.y = y1; + subclip.width = x2 - x1; + subclip.height = y2 - y1; + } + + LIST_FOR_EACH (struct widget, child, w->children) + x11_render_widget (child, &subclip); +} + +static void +x11_render (void) +{ + XRenderFillRectangle (g_xui.dpy, PictOpSrc, g_xui.x11_pixmap_picture, + &x11_default_bg, 0, 0, g_xui.width, g_xui.height); + + LIST_FOR_EACH (struct widget, w, g_xui.widgets) + x11_render_widget (w, NULL); + + XRectangle r = { 0, 0, g_xui.width, g_xui.height }; + XUnionRectWithRegion (&r, g_xui.x11_clip, g_xui.x11_clip); + poller_idle_set (&g_xui.xpending_event); +} + +static void +x11_flip (void) +{ + // This exercise in futility doesn't seem to affect CPU usage much. + XRectangle r = {}; + XClipBox (g_xui.x11_clip, &r); + XCopyArea (g_xui.dpy, g_xui.x11_pixmap, g_xui.x11_window, + DefaultGC (g_xui.dpy, DefaultScreen (g_xui.dpy)), + r.x, r.y, r.width, r.height, r.x, r.y); + + XSubtractRegion (g_xui.x11_clip, g_xui.x11_clip, g_xui.x11_clip); + poller_idle_set (&g_xui.xpending_event); +} + +static void +x11_destroy (void) +{ + XDestroyIC (g_xui.x11_ic); + XCloseIM (g_xui.x11_im); + XDestroyRegion (g_xui.x11_clip); + XDestroyWindow (g_xui.dpy, g_xui.x11_window); + XRenderFreePicture (g_xui.dpy, g_xui.x11_pixmap_picture); + XFreePixmap (g_xui.dpy, g_xui.x11_pixmap); + XftDrawDestroy (g_xui.xft_draw); + LIST_FOR_EACH (struct x11_font, font, g_xui.xft_fonts) + x11_font_destroy (font); + cstr_set (&g_xui.x11_selection, NULL); + + free (g_xui.x_fg); + free (g_xui.x_bg); + + poller_fd_reset (&g_xui.x11_event); + XCloseDisplay (g_xui.dpy); + + // Xft hooks called in XCloseDisplay() don't clean up everything. + FcFini (); +} + +static struct ui x11_ui = +{ + .padding = x11_make_padding, + .label = x11_make_label, + + .render = x11_render, + .flip = x11_flip, + .destroy = x11_destroy, +}; + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static termo_sym_t +x11_convert_keysym (KeySym keysym) +{ + // Leaving out TERMO_TYPE_FUNCTION, TERMO_SYM_DEL (N/A), + // and TERMO_SYM_SPACE (governed by TERMO_FLAG_SPACESYMBOL, not in use). + switch (keysym) + { + case XK_BackSpace: return TERMO_SYM_BACKSPACE; + case XK_Tab: return TERMO_SYM_TAB; + case XK_ISO_Left_Tab: return TERMO_SYM_TAB; + case XK_Return: return TERMO_SYM_ENTER; + case XK_Escape: return TERMO_SYM_ESCAPE; + + case XK_Up: return TERMO_SYM_UP; + case XK_Down: return TERMO_SYM_DOWN; + case XK_Left: return TERMO_SYM_LEFT; + case XK_Right: return TERMO_SYM_RIGHT; + case XK_Begin: return TERMO_SYM_BEGIN; + case XK_Find: return TERMO_SYM_FIND; + case XK_Insert: return TERMO_SYM_INSERT; + case XK_Delete: return TERMO_SYM_DELETE; + case XK_Select: return TERMO_SYM_SELECT; + case XK_Page_Up: return TERMO_SYM_PAGEUP; + case XK_Page_Down: return TERMO_SYM_PAGEDOWN; + case XK_Home: return TERMO_SYM_HOME; + case XK_End: return TERMO_SYM_END; + + case XK_Cancel: return TERMO_SYM_CANCEL; + case XK_Clear: return TERMO_SYM_CLEAR; + // TERMO_SYM_CLOSE + // TERMO_SYM_COMMAND + // TERMO_SYM_COPY + // TERMO_SYM_EXIT + case XK_Help: return TERMO_SYM_HELP; + // TERMO_SYM_MARK + // TERMO_SYM_MESSAGE + // TERMO_SYM_MOVE + // TERMO_SYM_OPEN + // TERMO_SYM_OPTIONS + case XK_Print: return TERMO_SYM_PRINT; + case XK_Redo: return TERMO_SYM_REDO; + // TERMO_SYM_REFERENCE + // TERMO_SYM_REFRESH + // TERMO_SYM_REPLACE + // TERMO_SYM_RESTART + // TERMO_SYM_RESUME + // TERMO_SYM_SAVE + // TERMO_SYM_SUSPEND + case XK_Undo: return TERMO_SYM_UNDO; + + case XK_KP_0: return TERMO_SYM_KP0; + case XK_KP_1: return TERMO_SYM_KP1; + case XK_KP_2: return TERMO_SYM_KP2; + case XK_KP_3: return TERMO_SYM_KP3; + case XK_KP_4: return TERMO_SYM_KP4; + case XK_KP_5: return TERMO_SYM_KP5; + case XK_KP_6: return TERMO_SYM_KP6; + case XK_KP_7: return TERMO_SYM_KP7; + case XK_KP_8: return TERMO_SYM_KP8; + case XK_KP_9: return TERMO_SYM_KP9; + case XK_KP_Enter: return TERMO_SYM_KPENTER; + case XK_KP_Add: return TERMO_SYM_KPPLUS; + case XK_KP_Subtract: return TERMO_SYM_KPMINUS; + case XK_KP_Multiply: return TERMO_SYM_KPMULT; + case XK_KP_Divide: return TERMO_SYM_KPDIV; + case XK_KP_Separator: return TERMO_SYM_KPCOMMA; + case XK_KP_Decimal: return TERMO_SYM_KPPERIOD; + case XK_KP_Equal: return TERMO_SYM_KPEQUALS; + } + return TERMO_SYM_UNKNOWN; +} + +static bool +on_x11_keypress (XEvent *e) +{ + // A kibibyte long buffer will have to suffice for anyone. + XKeyEvent *ev = &e->xkey; + char buf[1 << 10] = {}, *p = buf; + KeySym keysym = None; + Status status = 0; + int len = Xutf8LookupString + (g_xui.x11_ic, ev, buf, sizeof buf, &keysym, &status); + if (status == XBufferOverflow) + print_warning ("input method overflow"); + + termo_key_t key = {}; + if (ev->state & ShiftMask) + key.modifiers |= TERMO_KEYMOD_SHIFT; + if (ev->state & ControlMask) + key.modifiers |= TERMO_KEYMOD_CTRL; + if (ev->state & Mod1Mask) + key.modifiers |= TERMO_KEYMOD_ALT; + + if (keysym >= XK_F1 && keysym <= XK_F35) + { + key.type = TERMO_TYPE_FUNCTION; + key.code.number = 1 + keysym - XK_F1; + return xui_process_termo_event (&key); + } + if ((key.code.sym = x11_convert_keysym (keysym)) != TERMO_SYM_UNKNOWN) + { + key.type = TERMO_TYPE_KEYSYM; + return xui_process_termo_event (&key); + } + + bool result = true; + if (len) + { + key.type = TERMO_TYPE_KEY; + key.modifiers &= ~TERMO_KEYMOD_SHIFT; + + int32_t cp = 0; + struct utf8_iter iter = { .s = buf, .len = len }; + size_t cp_len = 0; + while ((cp = utf8_iter_next (&iter, &cp_len)) >= 0) + { + termo_key_t k = key; + memcpy (k.multibyte, p, MIN (cp_len, sizeof k.multibyte - 1)); + p += cp_len; + + // This is all unfortunate, but probably in the right place. + if (!cp) + { + k.code.codepoint = ' '; + if (ev->state & ShiftMask) + k.modifiers |= TERMO_KEYMOD_SHIFT; + } + else if (cp >= 32) + k.code.codepoint = cp; + else if (ev->state & ShiftMask) + k.code.codepoint = cp + 64; + else + k.code.codepoint = cp + 96; + if (!xui_process_termo_event (&k)) + result = false; + } + } + return result; +} + +static void +x11_init_pixmap (void) +{ + int screen = DefaultScreen (g_xui.dpy); + g_xui.x11_pixmap = XCreatePixmap (g_xui.dpy, g_xui.x11_window, + g_xui.width, g_xui.height, DefaultDepth (g_xui.dpy, screen)); + + Visual *visual = DefaultVisual (g_xui.dpy, screen); + XRenderPictFormat *format = XRenderFindVisualFormat (g_xui.dpy, visual); + g_xui.x11_pixmap_picture + = XRenderCreatePicture (g_xui.dpy, g_xui.x11_pixmap, format, 0, NULL); +} + +static char * +x11_find_text (struct widget *list, int x, int y) +{ + struct widget *target = NULL; + LIST_FOR_EACH (struct widget, w, list) + if (x >= w->x && x < w->x + w->width + && y >= w->y && y < w->y + w->height) + target = w; + if (!target) + return NULL; + + char *result = x11_find_text (target->children, x, y); + if (result) + return result; + return xstrdup (target->text); +} + +// TODO: OSC 52 exists for terminals, so make it possible to enable that there. +static bool +x11_process_press (int x, int y, int button, int modifiers) +{ + if (button != Button3) + goto out; + + char *text = x11_find_text (g_xui.widgets, x, y); + if (!text || !*(cstr_strip_in_place (text, " \t"))) + { + free (text); + goto out; + } + + cstr_set (&g_xui.x11_selection, text); + XSetSelectionOwner (g_xui.dpy, XInternAtom (g_xui.dpy, "CLIPBOARD", False), + g_xui.x11_window, CurrentTime); + app_on_clipboard_copy (g_xui.x11_selection); + return true; + +out: + return app_process_mouse (TERMO_MOUSE_PRESS, x, y, button, modifiers); +} + +static int +x11_state_to_modifiers (unsigned int state) +{ + int modifiers = 0; + if (state & ShiftMask) modifiers |= TERMO_KEYMOD_SHIFT; + if (state & ControlMask) modifiers |= TERMO_KEYMOD_CTRL; + if (state & Mod1Mask) modifiers |= TERMO_KEYMOD_ALT; + return modifiers; +} + +static bool +on_x11_input_event (XEvent *ev) +{ + static XEvent last_press_event; + if (ev->type == KeyPress) + { + last_press_event = (XEvent) {}; + return on_x11_keypress (ev); + } + if (ev->type == MotionNotify) + { + return app_process_mouse (TERMO_MOUSE_DRAG, + ev->xmotion.x, ev->xmotion.y, 1 /* Button1MotionMask */, + x11_state_to_modifiers (ev->xmotion.state)); + } + + // This is nearly the same as tui_on_tty_event(). + int x = ev->xbutton.x, y = ev->xbutton.y; + unsigned int button = ev->xbutton.button; + int modifiers = x11_state_to_modifiers (ev->xbutton.state); + if (ev->type == ButtonPress + && ev->xbutton.time - last_press_event.xbutton.time < 500 + && abs (last_press_event.xbutton.x - x) < 5 + && abs (last_press_event.xbutton.y - y) < 5 + && last_press_event.xbutton.button == button) + { + modifiers |= XUI_KEYMOD_DOUBLE_CLICK; + // Prevent interpreting triple clicks as two double clicks. + last_press_event = (XEvent) {}; + } + else if (ev->type == ButtonPress) + last_press_event = *ev; + + if (ev->type == ButtonPress) + return x11_process_press (x, y, button, modifiers); + if (ev->type == ButtonRelease) + return app_process_mouse + (TERMO_MOUSE_RELEASE, x, y, button, modifiers); + return false; +} + +static void +on_x11_selection_request (XSelectionRequestEvent *ev) +{ + Atom xa_targets = XInternAtom (g_xui.dpy, "TARGETS", False); + Atom xa_compound_text = XInternAtom (g_xui.dpy, "COMPOUND_TEXT", False); + Atom xa_utf8 = XInternAtom (g_xui.dpy, "UTF8_STRING", False); + Atom targets[] = { xa_targets, XA_STRING, xa_compound_text, xa_utf8 }; + + XEvent response = {}; + bool ok = false; + Atom property = ev->property ? ev->property : ev->target; + if (!g_xui.x11_selection) + goto out; + + XICCEncodingStyle style = 0; + if ((ok = ev->target == xa_targets)) + { + XChangeProperty (g_xui.dpy, ev->requestor, property, + XA_ATOM, 32, PropModeReplace, + (const unsigned char *) targets, N_ELEMENTS (targets)); + goto out; + } + else if (ev->target == XA_STRING) + style = XStringStyle; + else if (ev->target == xa_compound_text) + style = XCompoundTextStyle; + else if (ev->target == xa_utf8) + style = XUTF8StringStyle; + else + goto out; + + // XXX: We let it crash us with BadLength, but we may, e.g., use INCR. + XTextProperty text = {}; + if ((ok = !Xutf8TextListToTextProperty + (g_xui.dpy, &g_xui.x11_selection, 1, style, &text))) + { + XChangeProperty (g_xui.dpy, ev->requestor, property, + text.encoding, text.format, PropModeReplace, + text.value, text.nitems); + } + XFree (text.value); + +out: + response.xselection.type = SelectionNotify; + // XXX: We should check it against the event causing XSetSelectionOwner(). + response.xselection.time = ev->time; + response.xselection.requestor = ev->requestor; + response.xselection.selection = ev->selection; + response.xselection.target = ev->target; + response.xselection.property = ok ? property : None; + XSendEvent (g_xui.dpy, ev->requestor, False, 0, &response); +} + +static void +on_x11_event (XEvent *ev) +{ + termo_key_t key = {}; + switch (ev->type) + { + case Expose: + { + XRectangle r = { ev->xexpose.x, ev->xexpose.y, + ev->xexpose.width, ev->xexpose.height }; + XUnionRectWithRegion (&r, g_xui.x11_clip, g_xui.x11_clip); + poller_idle_set (&g_xui.flip_event); + break; + } + case ConfigureNotify: + if (g_xui.width == ev->xconfigure.width + && g_xui.height == ev->xconfigure.height) + break; + + g_xui.width = ev->xconfigure.width; + g_xui.height = ev->xconfigure.height; + + XRenderFreePicture (g_xui.dpy, g_xui.x11_pixmap_picture); + XFreePixmap (g_xui.dpy, g_xui.x11_pixmap); + x11_init_pixmap (); + XftDrawChange (g_xui.xft_draw, g_xui.x11_pixmap); + xui_invalidate (); + break; + case SelectionRequest: + on_x11_selection_request (&ev->xselectionrequest); + break; + case SelectionClear: + cstr_set (&g_xui.x11_selection, NULL); + break; + // UnmapNotify can be received when restarting the window manager. + // Should this turn out to be unreliable (window not destroyed by WM + // upon closing), opt for the WM_DELETE_WINDOW protocol as well. + case DestroyNotify: + app_quit (); + break; + case FocusIn: + key.type = TERMO_TYPE_FOCUS; + key.code.focused = true; + xui_process_termo_event (&key); + break; + case FocusOut: + key.type = TERMO_TYPE_FOCUS; + key.code.focused = false; + xui_process_termo_event (&key); + break; + case KeyPress: + case ButtonPress: + case ButtonRelease: + case MotionNotify: + if (!on_x11_input_event (ev)) + XkbBell (g_xui.dpy, ev->xany.window, 0, None); + } +} + +static void +on_x11_pending (void *user_data) +{ + (void) user_data; + + XkbEvent ev; + while (XPending (g_xui.dpy)) + { + if (XNextEvent (g_xui.dpy, &ev.core)) + exit_fatal ("XNextEvent returned non-zero"); + if (XFilterEvent (&ev.core, None)) + continue; + + on_x11_event (&ev.core); + } + + poller_idle_reset (&g_xui.xpending_event); +} + +static void +on_x11_ready (const struct pollfd *pfd, void *user_data) +{ + (void) pfd; + on_x11_pending (user_data); +} + +static int +on_x11_error (Display *dpy, XErrorEvent *event) +{ + // Without opting for WM_DELETE_WINDOW, this window can become destroyed + // and hence invalid at any time. We don't use the Window much, + // so we should be fine ignoring these errors. + if ((event->error_code == BadWindow + && event->resourceid == g_xui.x11_window) + || (event->error_code == BadDrawable + && event->resourceid == g_xui.x11_window)) + return app_quit (), 0; + + // XXX: The simplest possible way of discarding selection management errors. + // XCB would be a small win here, but it is a curse at the same time. + if (event->error_code == BadWindow && event->resourceid != g_xui.x11_window) + return 0; + + return x11_default_error_handler (dpy, event); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static XRenderColor +x11_convert_color (int color) +{ + hard_assert (color >= 0 && color <= 255); + + static const uint16_t base[16] = + { + 0x000, 0x800, 0x080, 0x880, 0x008, 0x808, 0x088, 0xccc, + 0x888, 0xf00, 0x0f0, 0xff0, 0x00f, 0xf0f, 0x0ff, 0xfff, + }; + + XRenderColor c = { .alpha = 0xffff }; + if (color < 16) + { + c.red = 0x1111 * (base[color] >> 8); + c.green = 0x1111 * (0xf & (base[color] >> 4)); + c.blue = 0x1111 * (0xf & (base[color])); + } + else if (color >= 232) + c.red = c.green = c.blue = 0x0101 * (8 + (color - 232) * 10); + else + { + color -= 16; + + int r = color / 36; + int g = (color / 6) % 6; + int b = (color % 6); + c.red = 0x0101 * !!r * (55 + 40 * r); + c.green = 0x0101 * !!g * (55 + 40 * g); + c.blue = 0x0101 * !!b * (55 + 40 * b); + } + return c; +} + +static void +x11_init_attributes (struct attrs *attrs, size_t attrs_len) +{ + g_xui.x_fg = xcalloc (attrs_len, sizeof g_xui.x_fg[0]); + g_xui.x_bg = xcalloc (attrs_len, sizeof g_xui.x_bg[0]); + for (size_t a = 0; a < attrs_len; a++) + { + g_xui.x_fg[a] = x11_default_fg; + g_xui.x_bg[a] = x11_default_bg; + if (attrs[a].fg >= 256 || attrs[a].fg < -1 + || attrs[a].bg >= 256 || attrs[a].bg < -1) + continue; + + if (attrs[a].fg != -1) + g_xui.x_fg[a] = x11_convert_color (attrs[a].fg); + if (attrs[a].bg != -1) + g_xui.x_bg[a] = x11_convert_color (attrs[a].bg); + + attrs[a].attrs |= COLOR_PAIR (a + 1); + } +} + +static void +x11_init (struct poller *poller, struct attrs *app_attrs, size_t app_attrs_len) +{ + // https://tedyin.com/posts/a-brief-intro-to-linux-input-method-framework/ + if (!XSupportsLocale ()) + print_warning ("locale not supported by Xlib"); + XSetLocaleModifiers (""); + + if (!(g_xui.dpy = XkbOpenDisplay + (NULL, &g_xui.xkb_base_event_code, NULL, NULL, NULL, NULL))) + exit_fatal ("cannot open display"); + if (!XftDefaultHasRender (g_xui.dpy)) + exit_fatal ("XRender is not supported"); + if (!(g_xui.x11_im = XOpenIM (g_xui.dpy, NULL, NULL, NULL))) + exit_fatal ("failed to open an input method"); + + x11_default_error_handler = XSetErrorHandler (on_x11_error); + + set_cloexec (ConnectionNumber (g_xui.dpy)); + g_xui.x11_event = poller_fd_make (poller, ConnectionNumber (g_xui.dpy)); + g_xui.x11_event.dispatcher = on_x11_ready; + poller_fd_set (&g_xui.x11_event, POLLIN); + + // Whenever something causes Xlib to read its socket, it can make + // the I/O event above fail to trigger for whatever might have ended up + // in its queue. So always use this instead of XSync: + g_xui.xpending_event = poller_idle_make (poller); + g_xui.xpending_event.dispatcher = on_x11_pending; + poller_idle_set (&g_xui.xpending_event); + + x11_init_attributes (app_attrs, app_attrs_len); + + if (!FcInit ()) + print_warning ("Fontconfig initialization failed"); + if (!(g_xui.xft_fonts = x11_font_open (0))) + exit_fatal ("cannot open a font"); + + int screen = DefaultScreen (g_xui.dpy); + Colormap cmap = DefaultColormap (g_xui.dpy, screen); + XColor default_bg = + { + .red = x11_default_bg.red, + .green = x11_default_bg.green, + .blue = x11_default_bg.blue, + }; + if (!XAllocColor (g_xui.dpy, cmap, &default_bg)) + exit_fatal ("X11 setup failed"); + + XSetWindowAttributes attrs = + { + .event_mask = StructureNotifyMask | ExposureMask | FocusChangeMask + | KeyPressMask | ButtonPressMask | ButtonReleaseMask + | Button1MotionMask, + .bit_gravity = NorthWestGravity, + .background_pixel = default_bg.pixel, + }; + + // Approximate the average width of a character to half of the em unit. + g_xui.vunit = g_xui.xft_fonts->list->font->height; + g_xui.hunit = g_xui.vunit / 2; + // Base the window's size on the regular font size. + // Roughly trying to match the 80x24 default dimensions of terminals. + g_xui.height = 24 * g_xui.vunit; + g_xui.width = g_xui.height * 4 / 3; + + long im_event_mask = 0; + if (!XGetIMValues (g_xui.x11_im, XNFilterEvents, &im_event_mask, NULL)) + attrs.event_mask |= im_event_mask; + + Visual *visual = DefaultVisual (g_xui.dpy, screen); + g_xui.x11_window = XCreateWindow (g_xui.dpy, + RootWindow (g_xui.dpy, screen), 100, 100, + g_xui.width, g_xui.height, 0, CopyFromParent, InputOutput, visual, + CWEventMask | CWBackPixel | CWBitGravity, &attrs); + g_xui.x11_clip = XCreateRegion (); + + XTextProperty prop = {}; + char *name = PROGRAM_NAME; + if (!Xutf8TextListToTextProperty (g_xui.dpy, + &name, 1, XUTF8StringStyle, &prop)) + XSetWMName (g_xui.dpy, g_xui.x11_window, &prop); + XFree (prop.value); + + // TODO: It is possible to do, e.g., on-the-spot. + XIMStyle im_style = XIMPreeditNothing | XIMStatusNothing; + XIMStyles *im_styles = NULL; + bool im_style_found = false; + if (!XGetIMValues (g_xui.x11_im, XNQueryInputStyle, &im_styles, NULL) + && im_styles) + { + for (unsigned i = 0; i < im_styles->count_styles; i++) + im_style_found |= im_styles->supported_styles[i] == im_style; + XFree (im_styles); + } + if (!im_style_found) + print_warning ("failed to find the desired input method style"); + if (!(g_xui.x11_ic = XCreateIC (g_xui.x11_im, + XNInputStyle, im_style, + XNClientWindow, g_xui.x11_window, + NULL))) + exit_fatal ("failed to open an input context"); + + XSetICFocus (g_xui.x11_ic); + + x11_init_pixmap (); + g_xui.xft_draw = XftDrawCreate (g_xui.dpy, g_xui.x11_pixmap, visual, cmap); + g_xui.ui = &x11_ui; + + XMapWindow (g_xui.dpy, g_xui.x11_window); +} + +#endif // LIBERTY_XUI_WANT_X11 + +// --- Containers -------------------------------------------------------------- + +static void +xui_on_hbox_allocated (struct widget *self) +{ + int parts = 0, width = self->width; + LIST_FOR_EACH (struct widget, w, self->children) + { + if (w->width < 0) + parts -= w->width; + else + width -= w->width; + } + + int remaining = MAX (width, 0), + part_width = parts ? remaining / parts : 0; + struct widget *last = NULL; + LIST_FOR_EACH (struct widget, w, self->children) + { + w->height = self->height; + if (w->width < 0) + { + remaining -= (w->width *= -part_width); + last = w; + } + } + if (last) + last->width += remaining; + + int x = self->x, y = self->y; + LIST_FOR_EACH (struct widget, w, self->children) + { + widget_move (w, x - w->x, y - w->y); + x += w->width; + + if (w->on_allocated) + w->on_allocated (w); + } +} + +static struct widget * +xui_hbox (struct widget *head) +{ + struct widget *self = xcalloc (1, sizeof *self); + self->children = head; + self->on_allocated = xui_on_hbox_allocated; + + LIST_FOR_EACH (struct widget, w, head) + { + self->height = MAX (self->height, w->height); + self->width += MAX (0, w->width); + } + return self; +} + +static void +xui_on_vbox_allocated (struct widget *self) +{ + int parts = 0, height = self->height; + LIST_FOR_EACH (struct widget, w, self->children) + { + if (w->height < 0) + parts -= w->height; + else + height -= w->height; + } + + int remaining = MAX (height, 0), + part_height = parts ? remaining / parts : 0; + struct widget *last = NULL; + LIST_FOR_EACH (struct widget, w, self->children) + { + w->width = self->width; + if (w->height < 0) + { + remaining -= (w->height *= -part_height); + last = w; + } + } + if (last) + last->height += remaining; + + int x = self->x, y = self->y; + LIST_FOR_EACH (struct widget, w, self->children) + { + widget_move (w, x - w->x, y - w->y); + y += w->height; + + if (w->on_allocated) + w->on_allocated (w); + } +} + +static struct widget * +xui_vbox (struct widget *head) +{ + struct widget *self = xcalloc (1, sizeof *self); + self->children = head; + self->on_allocated = xui_on_vbox_allocated; + + LIST_FOR_EACH (struct widget, w, head) + { + self->width = MAX (self->width, w->width); + self->height += MAX (0, w->height); + } + return self; +} + +// --- XUI --------------------------------------------------------------------- + +static bool +xui_is_character_in_locale (ucs4_t ch) +{ + // Avoid the overhead joined with calling iconv() for all characters. + if (g_xui.locale_is_utf8) + return true; + + // The library really creates a new conversion object every single time + // and doesn't provide any smarter APIs. Luckily, most users use UTF-8. + size_t len; + char *tmp = u32_conv_to_encoding (locale_charset (), iconveh_error, + &ch, 1, NULL, NULL, &len); + if (!tmp) + return false; + free (tmp); + return true; +} + +static void +xui_on_flip (void *user_data) +{ + (void) user_data; + poller_idle_reset (&g_xui.flip_event); + + // Waste of time, and may cause X11 to render uninitialised pixmaps. + if (/*g.polling &&*/ !g_xui.refresh_event.active) + g_xui.ui->flip (); +} + +static void +xui_on_refresh (void *user_data) +{ + (void) user_data; + poller_idle_reset (&g_xui.refresh_event); + + LIST_FOR_EACH (struct widget, w, g_xui.widgets) + widget_destroy (w); + + g_xui.widgets = NULL; + app_layout (); + + // Keep whatever the application gave them, for flexibility. + LIST_FOR_EACH (struct widget, w, g_xui.widgets) + if (w->on_allocated) + w->on_allocated (w); + + g_xui.ui->render (); + poller_idle_set (&g_xui.flip_event); +} + +static void +xui_preinit (void) +{ + TERMO_CHECK_VERSION; + if (!(g_xui.tk = termo_new (STDIN_FILENO, NULL, TERMO_FLAG_NOSTART))) + exit_fatal ("failed to initialize termo"); + + // This is also approximately what libunistring does internally, + // since the locale name is canonicalized by locale_charset(). + // Note that non-Unicode locales are handled pretty inefficiently. + g_xui.locale_is_utf8 = !strcasecmp_ascii (locale_charset (), "UTF-8"); + + // Presumably, although not necessarily; unsure if queryable at all. + g_xui.focused = true; + + // TODO: Try to use Gtk/FontName from the _XSETTINGS_S%d selection, + // as well as Net/DoubleClick*. See the XSETTINGS proposal for details. + // https://www.freedesktop.org/wiki/Specifications/XSettingsRegistry/ + // Note that this needs an X11 connection already. +#ifdef LIBERTY_XUI_WANT_X11 + g_xui.x11_fontname = "sans\\-serif-11"; + g_xui.x11_fontname_monospace = "monospace-11"; +#endif // LIBERTY_XUI_WANT_X11 +} + +static void +xui_start (struct poller *poller, + bool force_x11, struct attrs *attrs, size_t attrs_len) +{ + (void) force_x11; + + g_xui.refresh_event = poller_idle_make (poller); + g_xui.refresh_event.dispatcher = xui_on_refresh; + g_xui.flip_event = poller_idle_make (poller); + g_xui.flip_event.dispatcher = xui_on_flip; + + // Always initialized, but only activated with the TUI. + g_xui.tty_event = poller_fd_make (poller, STDIN_FILENO); + g_xui.tty_event.dispatcher = tui_on_tty_readable; + g_xui.tk_timer = poller_timer_make (poller); + g_xui.tk_timer.dispatcher = tui_on_key_timer; + +#ifdef LIBERTY_XUI_WANT_X11 + if (force_x11 || (!isatty (STDIN_FILENO) && getenv ("DISPLAY"))) + x11_init (poller, attrs, attrs_len); + else +#endif // LIBERTY_XUI_WANT_X11 + tui_init (poller, attrs, attrs_len); +} + +static void +xui_stop (void) +{ + poller_idle_reset (&g_xui.refresh_event); + poller_idle_reset (&g_xui.flip_event); + poller_fd_reset (&g_xui.tty_event); + poller_timer_reset (&g_xui.tk_timer); + + g_xui.ui->destroy (); + LIST_FOR_EACH (struct widget, w, g_xui.widgets) + widget_destroy (w); + + termo_destroy (g_xui.tk); +} diff --git a/tests/fuzz.c b/tests/fuzz.c index 74db020..1e3c995 100644 --- a/tests/fuzz.c +++ b/tests/fuzz.c @@ -32,13 +32,6 @@ #define LIBERTY_WANT_PROTO_MPD #include "../liberty.c" -#include "../liberty-tui.c" - -static bool -app_is_character_in_locale (ucs4_t ch) -{ - return ch < 128; -} // --- UTF-8 ------------------------------------------------------------------- @@ -211,19 +204,6 @@ test_config_item_parse (const uint8_t *data, size_t size) config_item_destroy (item); } -// --- TUI --------------------------------------------------------------------- - -static void -test_attrs_decode (const uint8_t *data, size_t size) -{ - struct str wrap = str_make (); - str_append_data (&wrap, data, size); - - attrs_decode (wrap.str); - - str_free (&wrap); -} - // --- MPD --------------------------------------------------------------------- static void @@ -266,7 +246,6 @@ LLVMFuzzerInitialize (int *argcp, char ***argvp) REGISTER (fcgi_parser_push) REGISTER (fcgi_nv_parser_push) REGISTER (config_item_parse) - REGISTER (attrs_decode) REGISTER (mpd_client_process_input) char **argv = *argvp, *option = "-test=", *name = NULL;