/*
 * liberty-xui.c: the ultimate C unlibrary: hybrid terminal/X11 UI
 *
 * Copyright (c) 2016 - 2026, Přemysl Eric Janouch <p@janouch.name>
 *
 * 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 <ncurses.h>

// 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 libgrapheme
// Decently sized           .          .          x          x          x
// Grapheme breaks          .          x          .          x          x
// Character width          x          .          x          x          .
// Locale handling          .          .          x          .          .
// Liberal license          .          x          .          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 <unistr.h>
#include <uniwidth.h>
#include <uniconv.h>
#include <unicase.h>

#include <termios.h>
#ifdef HAVE_RESIZETERM
#include <sys/ioctl.h>
#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 <X11/Xatom.h>
#include <X11/Xlib.h>
#include <X11/keysym.h>
#include <X11/XKBlib.h>
#include <X11/Xft/Xft.h>

#define LIBERTY_XDG_WANT_X11
#define LIBERTY_XDG_WANT_ICONS
#include "liberty-xdg.c"
#endif  // LIBERTY_XUI_WANT_X11

#ifdef LIBERTY_XUI_WANT_APPKIT
#import <AppKit/AppKit.h>
#import <CoreFoundation/CoreFoundation.h>
#endif  // LIBERTY_XUI_WANT_APPKIT

// 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 (self->alloc, sizeof *self->line);
	self->w = xcalloc (self->alloc, sizeof *self->w);
	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 (&current.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 widget_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

	// AppKit:

#ifdef LIBERTY_XUI_WANT_APPKIT
	NSWindow *appkit_window;            ///< Application window
	NSView *appkit_view;                ///< Application view
	id appkit_delegate;                 ///< Application delegate

	struct poller_timer appkit_event;   ///< Poll for AppKit events

	NSFont *appkit_font_regular;        ///< Regular proportional font
	NSFont *appkit_font_monospace;      ///< Regular monospace font
	NSColor *appkit_colors[256];        ///< Colour cube

	NSColor **appkit_fg;                ///< Foreground per attribute (weak ref)
	NSColor **appkit_bg;                ///< Background per attribute (weak ref)
#endif  // LIBERTY_XUI_WANT_APPKIT
}
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;

	// Nvidia's XRender appears to not add (x, y) to (srcx, srcy),
	// and works correctly if we pass self->x as srcx further on.
	//
	// Instead of special-casing nvidia_drv.so, let's have the gradient
	// also extend to the left, in order to make text legible at all.
	//
	// We could also detect the behaviour experimentally in run-time.
	double portion = MIN (1, 2. * font->list->font->height / (self->x + space));
	XFixed stops[3] = { 0, XDoubleToFixed (1 - portion), XDoubleToFixed (1) };
	XLinearGradient gradient = { {}, { XDoubleToFixed (self->x + 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, 0, 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;
	key.type = TERMO_TYPE_KEY;
	key.modifiers &= ~TERMO_KEYMOD_SHIFT;

	if (len)
	{
		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 (!XftInit (NULL))
		print_warning ("Fontconfig initialization failed");
	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 (!(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

// --- AppKit ------------------------------------------------------------------

#ifdef LIBERTY_XUI_WANT_APPKIT

static const int appkit_default_fg = 16;
static const int appkit_default_bg = 231;

static NSString *
appkit_string (const char *text)
{
	NSString *string = [NSString stringWithUTF8String:text];
	if (!string)
		string = @"?";
	return string;
}

static int
appkit_font_hadvance (NSFont *font, const char *text)
{
	NSDictionary *attrs
		= [NSDictionary dictionaryWithObject:font forKey:NSFontAttributeName];
	return ceil ([appkit_string (text) sizeWithAttributes:attrs].width);
}

static void
appkit_font_draw (NSFont *font, NSColor *color, int x, int y,
	const char *text, int max_width)
{
	NSMutableParagraphStyle *style = [NSMutableParagraphStyle new];
	[style setLineBreakMode:NSLineBreakByTruncatingTail];

	NSDictionary *attrs = [NSDictionary dictionaryWithObjectsAndKeys:
		font, NSFontAttributeName,
		color, NSForegroundColorAttributeName,
		style, NSParagraphStyleAttributeName,
		nil];
	[style release];

	NSRect rect = NSMakeRect (x, y, max_width, g_xui.vunit);
	[appkit_string (text) drawInRect:rect withAttributes:attrs];
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

static NSFont *
appkit_widget_font (struct widget *self)
{
	NSFont *base = (self->extended_attrs & XUI_ATTR_MONOSPACE)
		? g_xui.appkit_font_monospace
		: g_xui.appkit_font_regular;

	NSFontTraitMask traits = 0;
	if (self->attrs & A_BOLD)
		traits |= NSBoldFontMask;
#ifdef A_ITALIC
	if (self->attrs & A_ITALIC)
		traits |= NSItalicFontMask;
#endif  // A_ITALIC

	NSFont *font = [[NSFontManager sharedFontManager]
		convertFont:base toHaveTrait:traits];
	return font ? font : base;
}

static NSColor *
appkit_fg_attrs (chtype attrs)
{
	int pair = PAIR_NUMBER (attrs);
	if (!pair--)
		return g_xui.appkit_colors[appkit_default_fg];
	return ((attrs & A_REVERSE) ? g_xui.appkit_bg : g_xui.appkit_fg)[pair];
}

static NSColor *
appkit_fg (struct widget *self)
{
	return appkit_fg_attrs (self->attrs);
}

static NSColor *
appkit_bg_attrs (chtype attrs)
{
	int pair = PAIR_NUMBER (attrs);
	if (!pair--)
		return g_xui.appkit_colors[appkit_default_bg];
	return ((attrs & A_REVERSE) ? g_xui.appkit_fg : g_xui.appkit_bg)[pair];
}

static NSColor *
appkit_bg (struct widget *self)
{
	return appkit_bg_attrs (self->attrs);
}

static void
appkit_render_padding (struct widget *self)
{
	if (PAIR_NUMBER (self->attrs))
	{
		[appkit_bg (self) setFill];
		NSRectFill (NSMakeRect (self->x, self->y, self->width, self->height));
	}
	if (self->attrs & A_UNDERLINE)
	{
		[appkit_fg (self) setFill];
		NSRectFill (NSMakeRect
			(self->x, self->y + self->height - 1, self->width, 1));
	}
}

static struct widget *
appkit_make_padding (chtype attrs, float width, float height)
{
	struct widget *w = xcalloc (1, sizeof *w + 2);
	w->text[0] = ' ';
	w->on_render = appkit_render_padding;
	w->attrs = attrs;
	w->width = g_xui.vunit * width;
	w->height = g_xui.vunit * height;
	return w;
}

static void
appkit_render_label (struct widget *self)
{
	appkit_render_padding (self);

	int space = MIN (self->width, g_xui.width - self->x);
	if (space <= 0)
		return;

	NSFont *font = appkit_widget_font (self);
	int y = self->y + (self->height - g_xui.vunit) / 2;
	appkit_font_draw (font, appkit_fg (self), self->x, y, self->text, space);
}

static struct widget *
appkit_make_label (chtype attrs, unsigned extended, const char *label)
{
	size_t label_len = strlen (label) + 1;

	struct widget *w = xcalloc (1, sizeof *w + label_len);
	w->on_render = appkit_render_label;
	w->attrs = attrs;
	w->extended_attrs = extended;
	memcpy (w->text, label, label_len);

	NSFont *font = appkit_widget_font (w);
	w->width = appkit_font_hadvance (font, w->text);
	w->height = g_xui.vunit;
	return w;
}

static void
appkit_render_widget (struct widget *w, NSRect clip)
{
	if (w->width < 0 || w->height < 0)
		return;

	NSRect rect = NSMakeRect (w->x, w->y, w->width, w->height);
	NSRect subclip = NSIntersectionRect (rect, clip);
	if (NSIsEmptyRect (subclip))
		return;

	[NSGraphicsContext saveGraphicsState];
	NSRectClip (subclip);
	if (w->on_render)
		w->on_render (w);
	[NSGraphicsContext restoreGraphicsState];

	LIST_FOR_EACH (struct widget, child, w->children)
		appkit_render_widget (child, subclip);
}

static void
appkit_render (void)
{
	@autoreleasepool
	{
		[g_xui.appkit_view setNeedsDisplay:YES];
		[g_xui.appkit_view displayIfNeeded];
		[NSApp updateWindows];
	}
}

static void
appkit_flip (void)
{
	// AppKit does its own double-buffering, flipping is done in render(),
	// or more specifically in the drawRect: method.
}

static void
appkit_destroy (void)
{
	[g_xui.appkit_window close];
	g_xui.appkit_event.dispatcher (NULL);
	poller_timer_reset (&g_xui.appkit_event);

	[g_xui.appkit_window setDelegate:nil];
	[g_xui.appkit_delegate release];
	g_xui.appkit_delegate = nil;

	[g_xui.appkit_view release];
	g_xui.appkit_view = nil;

	[g_xui.appkit_window release];
	g_xui.appkit_window = nil;

	[g_xui.appkit_font_regular release];
	g_xui.appkit_font_regular = nil;
	[g_xui.appkit_font_monospace release];
	g_xui.appkit_font_monospace = nil;

	for (size_t i = 0; i < N_ELEMENTS (g_xui.appkit_colors); i++)
		[g_xui.appkit_colors[i] release];
	memset (g_xui.appkit_colors, 0, sizeof g_xui.appkit_colors);

	free (g_xui.appkit_fg);
	free (g_xui.appkit_bg);
	g_xui.appkit_fg = NULL;
	g_xui.appkit_bg = NULL;
}

static struct ui appkit_ui =
{
	.padding     = appkit_make_padding,
	.label       = appkit_make_label,

	.render      = appkit_render,
	.flip        = appkit_flip,
	.destroy     = appkit_destroy,
};

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

static termo_sym_t
appkit_convert_special (unichar c)
{
	// Leaving out TERMO_TYPE_FUNCTION, TERMO_SYM_DEL (N/A),
	// and TERMO_SYM_SPACE (governed by TERMO_FLAG_SPACESYMBOL, not in use).
	switch (c)
	{
	case NSBackspaceCharacter:       return TERMO_SYM_BACKSPACE;
	case NSDeleteCharacter:          return TERMO_SYM_BACKSPACE;
	case NSTabCharacter:             return TERMO_SYM_TAB;
	case NSBackTabCharacter:         return TERMO_SYM_TAB;
	case NSCarriageReturnCharacter:  return TERMO_SYM_ENTER;
	case NSEnterCharacter:           return TERMO_SYM_ENTER;
	case 0x1b:                       return TERMO_SYM_ESCAPE;

	case NSUpArrowFunctionKey:       return TERMO_SYM_UP;
	case NSDownArrowFunctionKey:     return TERMO_SYM_DOWN;
	case NSLeftArrowFunctionKey:     return TERMO_SYM_LEFT;
	case NSRightArrowFunctionKey:    return TERMO_SYM_RIGHT;
	case NSBeginFunctionKey:         return TERMO_SYM_BEGIN;
	case NSFindFunctionKey:          return TERMO_SYM_FIND;
	case NSInsertFunctionKey:        return TERMO_SYM_INSERT;
	case NSDeleteFunctionKey:        return TERMO_SYM_DELETE;
	case NSSelectFunctionKey:        return TERMO_SYM_SELECT;
	case NSPageUpFunctionKey:        return TERMO_SYM_PAGEUP;
	case NSPageDownFunctionKey:      return TERMO_SYM_PAGEDOWN;
	case NSHomeFunctionKey:          return TERMO_SYM_HOME;
	case NSEndFunctionKey:           return TERMO_SYM_END;

	// TERMO_SYM_CANCEL
	// TERMO_SYM_CLEAR
	// TERMO_SYM_CLOSE
	// TERMO_SYM_COMMAND
	// TERMO_SYM_COPY
	// TERMO_SYM_EXIT
	case NSHelpFunctionKey:          return TERMO_SYM_HELP;
	// TERMO_SYM_MARK
	// TERMO_SYM_MESSAGE
	// TERMO_SYM_MOVE
	// TERMO_SYM_OPEN
	// TERMO_SYM_OPTIONS
	case NSPrintFunctionKey:         return TERMO_SYM_PRINT;
	case NSRedoFunctionKey:          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 NSUndoFunctionKey:          return TERMO_SYM_UNDO;

	// TERMO_SYM_KP*
	}
	return TERMO_SYM_UNKNOWN;
}

static int
appkit_modifiers (NSEventModifierFlags flags)
{
	int modifiers = 0;
	if (flags & NSEventModifierFlagShift)   modifiers |= TERMO_KEYMOD_SHIFT;
	if (flags & NSEventModifierFlagControl) modifiers |= TERMO_KEYMOD_CTRL;
	if (flags & NSEventModifierFlagOption)  modifiers |= TERMO_KEYMOD_ALT;
	return modifiers;
}

static bool
on_appkit_key_down (NSEvent *event)
{
	if ([event modifierFlags] & NSEventModifierFlagCommand)
		return true;

	termo_key_t key = {};
	key.modifiers = appkit_modifiers ([event modifierFlags]);

	NSString *ignoring_modifiers = [event charactersIgnoringModifiers];
	if ([ignoring_modifiers length] == 1)
	{
		unichar c = [ignoring_modifiers characterAtIndex:0];
		if (c >= NSF1FunctionKey && c <= NSF35FunctionKey)
		{
			key.type = TERMO_TYPE_FUNCTION;
			key.code.number = 1 + c - NSF1FunctionKey;
			return xui_process_termo_event (&key);
		}
		if ((key.code.sym = appkit_convert_special (c)) != TERMO_SYM_UNKNOWN)
		{
			key.type = TERMO_TYPE_KEYSYM;
			return xui_process_termo_event (&key);
		}
	}

	// Let's make the right Option key act as AltGr, if possible.
	CGEventRef e = [event CGEvent];
	if (e && (CGEventGetFlags (e) & NX_DEVICERALTKEYMASK))
		key.modifiers &= ~TERMO_KEYMOD_ALT;

	bool result = true;
	key.type = TERMO_TYPE_KEY;
	key.modifiers &= ~TERMO_KEYMOD_SHIFT;

	NSData *utf8 = [[event characters] dataUsingEncoding:NSUTF8StringEncoding];
	if (utf8 && [utf8 length] > 0)
	{
		int32_t cp = 0;
		struct utf8_iter iter = { .s = [utf8 bytes], .len = [utf8 length] };
		const char *p = iter.s;

		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 ([event modifierFlags] & NSEventModifierFlagShift)
					k.modifiers |= TERMO_KEYMOD_SHIFT;
			}
			else if (cp >= 32)
				k.code.codepoint = cp;
			else if ([event modifierFlags] & NSEventModifierFlagShift)
				k.code.codepoint = cp + 64;
			else
				k.code.codepoint = cp + 96;
			if (!xui_process_termo_event (&k))
				result = false;
		}
	}
	return result;
}

static char *
appkit_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 = appkit_find_text (target->children, x, y);
	if (result)
		return result;
	return xstrdup (target->text);
}

static void
appkit_update_dimensions (void)
{
	NSRect bounds = [g_xui.appkit_view bounds];
	g_xui.width = bounds.size.width;
	g_xui.height = bounds.size.height;
}

static bool
appkit_event_position (NSEvent *event, int *x, int *y)
{
	NSPoint p = [g_xui.appkit_view
		convertPoint:[event locationInWindow] fromView:nil];
	*x = p.x;
	*y = p.y;
	return true;
}

static int
appkit_mouse_button (NSEvent *event)
{
	NSUInteger button = [event buttonNumber];
	switch (button)
	{
	case 0:
		return 1;
	case 1:
		return 3;
	case 2:
		return 2;
	default:
		// Skip all scroll wheel directions, so that we match X11.
		return button + 1 + 4;
	}
}

static bool
appkit_process_press (int x, int y, int button, int modifiers)
{
	if (button != 3)
		goto out;

	char *text = appkit_find_text (g_xui.widgets, x, y);
	if (!text || !*(cstr_strip_in_place (text, " \t")))
	{
		free (text);
		goto out;
	}

	NSPasteboard *pasteboard = [NSPasteboard generalPasteboard];
	[pasteboard clearContents];
	[pasteboard setString:appkit_string (text) forType:NSPasteboardTypeString];
	app_on_clipboard_copy (text);
	free (text);
	return true;

out:
	return app_process_mouse (TERMO_MOUSE_PRESS, x, y, button, modifiers);
}

static bool
on_appkit_mouse_event (NSEvent *event, termo_mouse_event_t type, int button)
{
	int x = 0, y = 0;
	if (!appkit_event_position (event, &x, &y))
		return false;

	int modifiers = appkit_modifiers ([event modifierFlags]);
	if (type == TERMO_MOUSE_PRESS && [event clickCount] > 1)
		modifiers |= XUI_KEYMOD_DOUBLE_CLICK;

	if (type == TERMO_MOUSE_PRESS)
		return appkit_process_press (x, y, button, modifiers);

	return app_process_mouse (type, x, y, button, modifiers);
}

static bool
on_appkit_scroll_event (NSEvent *event)
{
	int x = 0, y = 0;
	if (!appkit_event_position (event, &x, &y))
		return false;

	double delta_y = [event scrollingDeltaY];
	if (!delta_y)
		return true;

	int modifiers = appkit_modifiers ([event modifierFlags]);
	int button = delta_y > 0 ? 4 : 5;
	int repeats = MAX (1, (int) round (fabs (delta_y)));
	bool result = true;
	while (result && repeats-- > 0)
	{
		result &= app_process_mouse (TERMO_MOUSE_PRESS,
			x, y, button, modifiers);

		// XXX: Terminals do not send it, but X11 does.  We should either
		// suppress the releases, or synthesize them for terminals as well.
		result &= app_process_mouse (TERMO_MOUSE_RELEASE,
			x, y, button, modifiers);
	}
	return result;
}

static void
appkit_send_focus_event (bool focused)
{
	termo_key_t key = { .type = TERMO_TYPE_FOCUS };
	key.code.focused = focused;
	xui_process_termo_event (&key);
}

@interface XUIAppKitView : NSView
@end

@implementation XUIAppKitView

- (BOOL) isFlipped { return YES; }
- (BOOL) acceptsFirstResponder { return YES; }

- (void) drawRect:(NSRect)dirty_rect
{
	[g_xui.appkit_colors[appkit_default_bg] setFill];
	NSRectFill (dirty_rect);

	LIST_FOR_EACH (struct widget, w, g_xui.widgets)
		appkit_render_widget (w, dirty_rect);
}

- (void) keyDown:(NSEvent *)event
{
	if (!on_appkit_key_down (event))
		NSBeep ();
}

- (void) mouseDown:(NSEvent *)event
{
	if (!on_appkit_mouse_event (event, TERMO_MOUSE_PRESS, 1))
		NSBeep ();
}

- (void) mouseUp:(NSEvent *)event
{
	if (!on_appkit_mouse_event (event, TERMO_MOUSE_RELEASE, 1))
		NSBeep ();
}

- (void) rightMouseDown:(NSEvent *)event
{
	if (!on_appkit_mouse_event (event, TERMO_MOUSE_PRESS, 3))
		NSBeep ();
}

- (void) rightMouseUp:(NSEvent *)event
{
	if (!on_appkit_mouse_event (event, TERMO_MOUSE_RELEASE, 3))
		NSBeep ();
}

- (void) otherMouseDown:(NSEvent *)event
{
	if (!on_appkit_mouse_event (event,
		TERMO_MOUSE_PRESS, appkit_mouse_button (event)))
		NSBeep ();
}

- (void) otherMouseUp:(NSEvent *)event
{
	if (!on_appkit_mouse_event (event,
		TERMO_MOUSE_RELEASE, appkit_mouse_button (event)))
		NSBeep ();
}

- (void) mouseDragged:(NSEvent *)event
{
	if (!on_appkit_mouse_event (event, TERMO_MOUSE_DRAG, 1))
		NSBeep ();
}

- (void) rightMouseDragged:(NSEvent *)event
{
	if (!on_appkit_mouse_event (event, TERMO_MOUSE_DRAG, 3))
		NSBeep ();
}

- (void) otherMouseDragged:(NSEvent *)event
{
	if (!on_appkit_mouse_event (event,
		TERMO_MOUSE_DRAG, appkit_mouse_button (event)))
		NSBeep ();
}

- (void) scrollWheel:(NSEvent *)event
{
	if (!on_appkit_scroll_event (event))
		NSBeep ();
}

@end

@interface XUIAppKitDelegate : NSObject <NSWindowDelegate>
@end

@implementation XUIAppKitDelegate

- (void) windowWillClose:(NSNotification *)notification
{
	(void) notification;
	app_quit ();
}

- (void) windowDidResize:(NSNotification *)notification
{
	(void) notification;
	appkit_update_dimensions ();

	// AppKit seems to run an event subloop, so relayout immediately.
	g_xui.refresh_event.dispatcher (NULL);
	poller_idle_reset (&g_xui.refresh_event);
}

- (void) windowDidBecomeKey:(NSNotification *)notification
{
	(void) notification;
	appkit_send_focus_event (true);
}

- (void) windowDidResignKey:(NSNotification *)notification
{
	(void) notification;
	appkit_send_focus_event (false);
}

@end

static NSEvent *
appkit_next_event (NSDate *limit)
{
	// Probably not needing NSEventTrackingRunLoopMode
	// or NSModalPanelRunLoopMode.
	return [NSApp nextEventMatchingMask:NSEventMaskAny
		untilDate:limit inMode:NSDefaultRunLoopMode dequeue:true];
}

static void
on_appkit_ready (void *user_data)
{
	(void) user_data;

	// AppKit likes to own the event loop; we give up and poll.
	poller_timer_set (&g_xui.appkit_event, 1000 / 60 /* FPS */);

	@autoreleasepool
	{
		NSDate *limit = [NSDate distantPast];
		NSEvent *event = nil;
		while ((event = appkit_next_event (limit)))
		{
			[NSApp sendEvent:event];
			[NSApp updateWindows];
		}
	}
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

static NSColor *
appkit_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,
	};

	CGFloat red = 1, green = 1, blue = 1, alpha = 1;
	if (color < 16)
	{
		red   =        (base[color] >> 8)  / 15.0;
		green = (0xf & (base[color] >> 4)) / 15.0;
		blue  = (0xf & (base[color]))      / 15.0;
	}
	else if (color >= 232)
		red = green = blue = (8 + (color - 232) * 10) / 255.0;
	else
	{
		color -= 16;

		int r =  color / 36;
		int g = (color / 6) % 6;
		int b = (color % 6);
		red   = !!r * (55 + 40 * r) / 255.0;
		green = !!g * (55 + 40 * g) / 255.0;
		blue  = !!b * (55 + 40 * b) / 255.0;
	}
	return [NSColor colorWithDeviceRed:red green:green blue:blue alpha:alpha];
}

static void
appkit_init_attributes (struct attrs *attrs, size_t attrs_len)
{
	for (size_t i = 0; i < N_ELEMENTS (g_xui.appkit_colors); i++)
		g_xui.appkit_colors[i] = [appkit_convert_color (i) retain];

	g_xui.appkit_fg = xcalloc (attrs_len, sizeof *g_xui.appkit_fg);
	g_xui.appkit_bg = xcalloc (attrs_len, sizeof *g_xui.appkit_bg);
	for (size_t a = 0; a < attrs_len; a++)
	{
		g_xui.appkit_fg[a] = g_xui.appkit_colors[appkit_default_fg];
		g_xui.appkit_bg[a] = g_xui.appkit_colors[appkit_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.appkit_fg[a] = g_xui.appkit_colors[attrs[a].fg];
		if (attrs[a].bg != -1)
			g_xui.appkit_bg[a] = g_xui.appkit_colors[attrs[a].bg];

		attrs[a].attrs |= COLOR_PAIR (a + 1);
	}
}

static void
appkit_init_menu (void)
{
	NSMenu *app_menu = [[NSMenu new] autorelease];

	NSMenuItem *services_item = [app_menu
		addItemWithTitle:@"Services"
		action:nil
		keyEquivalent:@""];
	NSMenu *services_menu = [[NSMenu new] autorelease];
	[services_item setSubmenu:services_menu];
	[NSApp setServicesMenu:services_menu];

	[app_menu addItem:[NSMenuItem separatorItem]];
	[app_menu addItemWithTitle:@"Hide " PROGRAM_NAME
		action:@selector (hide:)
		keyEquivalent:@"h"];
	[app_menu addItem:[NSMenuItem separatorItem]];
	[app_menu addItemWithTitle:@"Quit " PROGRAM_NAME
		action:@selector (terminate:)
		keyEquivalent:@"q"];

	NSMenuItem *app_menu_item = [[NSMenuItem new] autorelease];
	[app_menu_item setSubmenu:app_menu];
	NSMenu *menubar = [[NSMenu new] autorelease];
	[menubar addItem:app_menu_item];
	[NSApp setMainMenu:menubar];
}

static void
appkit_init (struct poller *poller, struct attrs *attrs, size_t attrs_len)
{
	(void) [NSApplication sharedApplication];
	// Apparently this won't succeed when run from within a bundle.
	(void) [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
	appkit_init_menu ();
	[NSApp finishLaunching];

	NSFont *system =
		[NSFont systemFontOfSize:[NSFont systemFontSize]];
	NSFont *mono =
		[NSFont userFixedPitchFontOfSize:[NSFont systemFontSize]];
	if (!mono)
		mono = system;
	g_xui.appkit_font_regular = [system retain];
	g_xui.appkit_font_monospace = [mono retain];
	if (!g_xui.appkit_font_regular || !g_xui.appkit_font_monospace)
		[NSException raise:@"failed" format:@"failed to load fonts"];

	g_xui.vunit = ceil ([system ascender] - [system descender]
		+ [system leading]);
	g_xui.vunit = MAX (1, g_xui.vunit);
	g_xui.hunit = MAX (1, g_xui.vunit / 2);
	g_xui.height = 24 * g_xui.vunit;
	g_xui.width = g_xui.height * 4 / 3;

	NSRect frame = NSMakeRect (0, 0, g_xui.width, g_xui.height);
	NSWindowStyleMask style = NSWindowStyleMaskTitled
		| NSWindowStyleMaskClosable
		| NSWindowStyleMaskMiniaturizable
		| NSWindowStyleMaskResizable;
	g_xui.appkit_window = [[NSWindow alloc]
		initWithContentRect:frame
		styleMask:style
		backing:NSBackingStoreBuffered
		defer:NO];
	if (!g_xui.appkit_window)
		[NSException raise:@"failed" format:@"failed to create window"];
	g_xui.appkit_window.releasedWhenClosed = false;
	[g_xui.appkit_window center];

	g_xui.appkit_view = [[XUIAppKitView alloc] initWithFrame:frame];
	if (!g_xui.appkit_view)
		[NSException raise:@"failed" format:@"failed to create view"];
	[g_xui.appkit_window setContentView:g_xui.appkit_view];

	g_xui.appkit_delegate = [XUIAppKitDelegate new];
	if (!g_xui.appkit_delegate)
		[NSException raise:@"failed" format:@"failed to create delegate"];
	[g_xui.appkit_window setDelegate:g_xui.appkit_delegate];

	[g_xui.appkit_window setAcceptsMouseMovedEvents:YES];
	[g_xui.appkit_window setTitle:appkit_string (PROGRAM_NAME)];
	[g_xui.appkit_window makeFirstResponder:g_xui.appkit_view];
	[g_xui.appkit_window makeKeyAndOrderFront:nil];
	[NSApp activateIgnoringOtherApps:YES];

	appkit_update_dimensions ();
	appkit_init_attributes (attrs, attrs_len);

	g_xui.appkit_event = poller_timer_make (poller);
	g_xui.appkit_event.dispatcher = on_appkit_ready;
	poller_timer_set (&g_xui.appkit_event, 0);

	g_xui.ui = &appkit_ui;
}

#endif  // LIBERTY_XUI_WANT_APPKIT

// --- 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)
{
#if !defined LIBERTY_XUI_WANT_X11 && !defined LIBERTY_XUI_WANT_APPKIT
	(void) force_x11;
#endif  // !LIBERTY_XUI_WANT_X11 && !LIBERTY_XUI_WANT_APPKIT

	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 (getenv ("DISPLAY")
	 && (force_x11 || !isatty (STDIN_FILENO)))
	{
		x11_init (poller, attrs, attrs_len);
		return;
	}
#endif  // LIBERTY_XUI_WANT_X11

#ifdef LIBERTY_XUI_WANT_APPKIT
	if (force_x11 || !isatty (STDIN_FILENO))
	{
		@autoreleasepool
		{
			@try
			{
				appkit_init (poller, attrs, attrs_len);
			}
			@catch (NSException *e)
			{
				exit_fatal ("AppKit setup failed: %s", [[e reason] UTF8String]);
			}
		}
		return;
	}
#endif  // LIBERTY_XUI_WANT_APPKIT

	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);
}
