/* * liberty-xui.c: the ultimate C unlibrary: hybrid terminal/X11 UI * * Copyright (c) 2016 - 2024, 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. // It assumes you've already included liberty.c, and may include liberty-xdg.c. #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 #ifdef HAVE_RESIZETERM #include #endif // HAVE_RESIZETERM // 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 #define LIBERTY_XDG_WANT_X11 #define LIBERTY_XDG_WANT_ICONS #include "liberty-xdg.c" #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 struct xdg_xsettings x11_xsettings; ///< XSETTINGS int32_t x11_double_click_time; ///< Maximum delay for double clicks int32_t x11_double_click_distance; ///< Maximum distance for double clicks 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); if (self->y >= 0 && self->y < g_xui.height) { 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; #ifdef A_ITALIC if (self->attrs & A_ITALIC) style |= X11_FONT_ITALIC; #endif // A_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); xdg_xsettings_free (&g_xui.x11_xsettings); 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, MAX (g_xui.width, 1), MAX (g_xui.height, 1), 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 < (Time) g_xui.x11_double_click_time && abs (last_press_event.xbutton.x - x) < g_xui.x11_double_click_distance && abs (last_press_event.xbutton.y - y) < g_xui.x11_double_click_distance && 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); // https://www.freedesktop.org/wiki/Specifications/XSettingsRegistry/ // TODO: Try to use Xft/{Antialias,DPI,HintStyle,Hinting,RGBA} // from XSETTINGS. Sadly, Gtk/FontName is in the Pango format, // which is rather difficult to parse. g_xui.x11_xsettings = xdg_xsettings_make (); xdg_xsettings_update (&g_xui.x11_xsettings, g_xui.dpy); 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); // It should not be outlandish to expect to find a program icon, // although it should be possible to use a "DBus well-known name". const char *icon_theme_name = NULL; const struct xdg_xsettings_setting *setting = str_map_find (&g_xui.x11_xsettings.settings, "Net/IconThemeName"); if (setting != NULL && setting->type == XDG_XSETTINGS_STRING) icon_theme_name = setting->string.str; icon_theme_set_window_icon (g_xui.dpy, g_xui.x11_window, icon_theme_name, name); if ((setting = str_map_find (&g_xui.x11_xsettings.settings, "Net/DoubleClickTime")) && setting->type == XDG_XSETTINGS_INTEGER && setting->integer >= 0) g_xui.x11_double_click_time = setting->integer; if ((setting = str_map_find (&g_xui.x11_xsettings.settings, "Net/DoubleClickDistance")) && setting->type == XDG_XSETTINGS_INTEGER && setting->integer >= 0) g_xui.x11_double_click_distance = setting->integer; // 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; #ifdef LIBERTY_XUI_WANT_X11 // Note that XSETTINGS overrides some values in the init. g_xui.x11_double_click_time = 500; g_xui.x11_double_click_distance = 5; 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); }