From e90abbd03c6c26c4f17ad7ea63472e2c54d22e68 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?P=C5=99emysl=20Eric=20Janouch?=
Date: Thu, 30 Jul 2026 17:10:37 +0200
Subject: [PATCH] liberty-xui: import some widgets from nncmpp
- The symbol is what used to be a button, but more aptly named.
- The scrollbar is adjusted to avoid using the Full Block character.
- The gauge is adjusted to span the whole height,
and displays a ratio, using only a single attribute
which is inverted as necessary.
Widgets now contain text through a pointer, which may still point
right past the structure itself. This change makes them subclassable,
which is what all of these widgets need.
---
liberty-xui.c | 479 +++++++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 470 insertions(+), 9 deletions(-)
diff --git a/liberty-xui.c b/liberty-xui.c
index 2ad3076..875e14a 100644
--- a/liberty-xui.c
+++ b/liberty-xui.c
@@ -16,6 +16,8 @@
*
*/
+#include
+
// 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.
@@ -599,7 +601,7 @@ struct widget
int widget_id; ///< Post-layouting identification
int userdata; ///< Action ID/Tab index/...
- char text[]; ///< Any text label
+ char *text; ///< Any text label
};
static void
@@ -621,12 +623,97 @@ widget_move (struct widget *w, int dx, int dy)
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+// Use {INFINITY, INFINITY} once for stopping a subpath, twice to end icon data.
+struct xui_icon_point
+{
+ double x;
+ double y;
+};
+
+struct xui_symbol
+{
+ struct widget widget;
+ const struct xui_icon_point *icon;
+};
+
+struct xui_gauge
+{
+ struct widget widget;
+ double fraction;
+};
+
+struct xui_scrollbar
+{
+ struct widget widget;
+ long top;
+ long total;
+};
+
+// XXX: This is a rather dirty simplification.
+static struct widget *
+xui_new_symbol (struct widget *label, const struct xui_icon_point *icon)
+{
+ if (!icon)
+ return label;
+
+ size_t len = strlen (label->text) + 1;
+ struct xui_symbol *self = xcalloc (1, sizeof *self + len);
+ self->widget = *label;
+ self->widget.text = memcpy (self + 1, label->text, len);
+ self->icon = icon;
+ free (label);
+ return &self->widget;
+}
+
+struct xui_scrollbar_part
+{
+ long length;
+ long start;
+};
+
+/// Figure out scrollbar appearance. @a s is the minimal slider length as well
+/// as the scrollbar resolution per visible item.
+static struct xui_scrollbar_part
+xui_compute_scrollbar (const struct xui_scrollbar *self, long visible, long s)
+{
+ long top = s * self->top, total = s * self->total;
+ if (total < visible)
+ return (struct xui_scrollbar_part) { 0, 0 };
+ if (visible == 1)
+ return (struct xui_scrollbar_part) { s, 0 };
+ if (visible == 2)
+ return (struct xui_scrollbar_part)
+ { s, top >= total / 2 ? s : 0 };
+
+ // Only be at the top or bottom when the top or bottom item can be seen.
+ // The algorithm isn't optimal but it's a bitch to get right.
+ double available_length = visible - 2 - s + 1;
+
+ double lenf = s + available_length * visible / total, length = 0.;
+ long offset = 1 + available_length * top / total + modf (lenf, &length);
+
+ if (top == 0)
+ return (struct xui_scrollbar_part) { length, 0 };
+ if (top + visible >= total)
+ return (struct xui_scrollbar_part) { length, visible - length };
+
+ return (struct xui_scrollbar_part) { length, offset };
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
struct ui
{
struct widget *(*padding)
(chtype attrs, float width, float height);
struct widget *(*label)
(chtype attrs, unsigned extended, const char *label);
+ struct widget *(*symbol)
+ (chtype attrs, const char *label, const struct xui_icon_point *icon);
+ struct widget *(*gauge)
+ (chtype attrs, double fraction);
+ struct widget *(*scrollbar)
+ (chtype attrs, long top, long total);
void (*beep) (void);
void (*render) (void);
@@ -684,6 +771,7 @@ struct xui
struct poller_fd tty_event; ///< Terminal input event
struct poller_timer tk_timer; ///< termo timeout timer
bool locale_is_utf8; ///< The locale is Unicode
+ bool use_partial_boxes; ///< Use Unicode box drawing chars
// X11:
@@ -771,8 +859,8 @@ tui_render_padding (struct widget *self)
static struct widget *
tui_make_padding (chtype attrs, float width, float height)
{
- struct widget *w = xcalloc (1, sizeof *w + 2);
- w->text[0] = ' ';
+ struct widget *w = xcalloc (1, sizeof *w);
+ w->text = " ";
w->on_render = tui_render_padding;
w->attrs = attrs;
w->width = width * 2;
@@ -791,10 +879,9 @@ tui_render_label (struct widget *self)
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->text = (char *) (w + 1);
w->on_render = tui_render_label;
w->attrs = attrs;
w->extended_attrs = extended;
@@ -808,6 +895,127 @@ tui_make_label (chtype attrs, unsigned extended, const char *label)
return w;
}
+static struct widget *
+tui_make_symbol (chtype attrs, const char *label,
+ const struct xui_icon_point *icon)
+{
+ (void) icon;
+ return tui_make_label (attrs, 0, label);
+}
+
+static void
+tui_render_gauge (struct widget *widget)
+{
+ const struct xui_gauge *self = (const struct xui_gauge *) widget;
+ struct row_buffer buf = row_buffer_make ();
+
+ // Always compute it in exactly eight times the resolution,
+ // because sometimes Unicode is even useful
+ int len_left = self->fraction * widget->width * 8 + 0.5;
+
+ static const char *partials[] = { " ", "▏", "▎", "▍", "▌", "▋", "▊", "▉" };
+ int remainder = len_left % 8;
+ len_left /= 8;
+
+ const char *partial = NULL;
+ if (g_xui.use_partial_boxes)
+ partial = partials[remainder];
+ else
+ len_left += remainder >= 4;
+
+ int len_right = widget->width - len_left;
+ row_buffer_space (&buf, len_left, widget->attrs ^ A_REVERSE);
+ if (partial && len_right-- > 0)
+ row_buffer_append (&buf, partial, widget->attrs);
+ row_buffer_space (&buf, len_right, widget->attrs);
+ tui_flush_buffer (widget, &buf);
+}
+
+static struct widget *
+tui_make_gauge (chtype attrs, double fraction)
+{
+ struct xui_gauge *self = xcalloc (1, sizeof *self);
+ self->widget.text = "";
+ self->widget.on_render = tui_render_gauge;
+ self->widget.attrs = attrs;
+ self->widget.width = -1;
+ self->widget.height = 1;
+ self->fraction = MIN (MAX (fraction, 0.), 1.);
+ return &self->widget;
+}
+
+static void
+tui_render_scrollbar (struct widget *widget)
+{
+ // This assumes that we can write to the column, i.e.,
+ // that it's not covered by any double-wide character (and that
+ // ncurses comes to the right results when counting characters).
+ struct xui_scrollbar *self = (struct xui_scrollbar *) widget;
+ int visible = widget->height;
+
+ if (!g_xui.use_partial_boxes)
+ {
+ struct xui_scrollbar_part bar =
+ xui_compute_scrollbar (self, visible, 1);
+ for (int row = 0; row < visible; row++)
+ {
+ move (widget->y + row, widget->x);
+ chtype attrs = widget->attrs;
+ if (row >= bar.start && row < bar.start + bar.length)
+ attrs ^= A_REVERSE;
+ addch (' ' | attrs);
+ }
+ return;
+ }
+
+ struct xui_scrollbar_part bar =
+ xui_compute_scrollbar (self, visible * 8, 8);
+ bar.length += bar.start;
+
+ int start_part = bar.start % 8; bar.start /= 8;
+ int end_part = bar.length % 8; bar.length /= 8;
+
+ // Even with this, the solid part must reach full cell boundaries.
+ // It turns out trying to use '█' is plainly asking for problems,
+ // so we turn it into inversion.
+ static const char *partials[] = { " ", "▇", "▆", "▅", "▄", "▃", "▂", "▁" };
+
+ for (int row = 0; row < visible; row++)
+ {
+ chtype attrs = widget->attrs;
+ if (row >= bar.start + !!start_part
+ && row < bar.length + !!end_part)
+ attrs ^= A_REVERSE;
+
+ const char *glyph = NULL;
+ if (row == bar.start) glyph = partials[start_part];
+ if (row == bar.length) glyph = partials[end_part];
+
+ move (widget->y + row, widget->x);
+
+ struct row_buffer buf = row_buffer_make ();
+ if (glyph)
+ row_buffer_append (&buf, glyph, attrs);
+ else
+ row_buffer_append_c (&buf, ' ', attrs);
+ row_buffer_flush (&buf);
+ row_buffer_free (&buf);
+ }
+}
+
+static struct widget *
+tui_make_scrollbar (chtype attrs, long top, long total)
+{
+ struct xui_scrollbar *self = xcalloc (1, sizeof *self);
+ self->widget.text = "";
+ self->widget.on_render = tui_render_scrollbar;
+ self->widget.attrs = attrs;
+ self->widget.width = 1;
+ self->top = top;
+ self->total = total;
+ return &self->widget;
+}
+
static void
tui_render_widgets (struct widget *head)
{
@@ -876,6 +1084,9 @@ static struct ui tui_ui =
{
.padding = tui_make_padding,
.label = tui_make_label,
+ .symbol = tui_make_symbol,
+ .gauge = tui_make_gauge,
+ .scrollbar = tui_make_scrollbar,
.beep = tui_beep,
.render = tui_render,
@@ -1265,8 +1476,8 @@ x11_render_padding (struct widget *self)
static struct widget *
x11_make_padding (chtype attrs, float width, float height)
{
- struct widget *w = xcalloc (1, sizeof *w + 2);
- w->text[0] = ' ';
+ struct widget *w = xcalloc (1, sizeof *w);
+ w->text = " ";
w->on_render = x11_render_padding;
w->attrs = attrs;
w->width = g_xui.vunit * width;
@@ -1331,6 +1542,7 @@ x11_make_label (chtype attrs, unsigned extended, const char *label)
}
struct widget *w = xcalloc (1, sizeof *w + normalized_len);
+ w->text = (char *) (w + 1);
w->on_render = x11_render_label;
w->attrs = attrs;
w->extended_attrs = extended;
@@ -1343,6 +1555,131 @@ x11_make_label (chtype attrs, unsigned extended, const char *label)
return w;
}
+static void
+x11_render_symbol (struct widget *widget)
+{
+ x11_render_padding (widget);
+
+ const struct xui_icon_point *icon =
+ ((const struct xui_symbol *) widget)->icon;
+ size_t total = 0;
+ while (!isinf (icon[total].x) || !isinf (icon[total].y)
+ || !isinf (icon[total + 1].x) || !isinf (icon[total + 1].y))
+ total++;
+
+ // TODO: There might be an attribute for symbols, to handle this better.
+ XRenderColor color = *x11_fg (widget);
+ if (!(widget->attrs & A_BOLD))
+ {
+ color.alpha /= 2;
+ color.red /= 2;
+ color.green /= 2;
+ color.blue /= 2;
+ }
+
+ Picture source = XRenderCreateSolidFill (g_xui.dpy, &color);
+ const XRenderPictFormat *format
+ = XRenderFindStandardFormat (g_xui.dpy, PictStandardA8);
+
+ int x = widget->x, y = widget->y + (widget->height - widget->width) / 2;
+ XPointDouble buffer[total], *p = buffer;
+ for (size_t i = 0; i <= total; i++)
+ if (icon[i].x != INFINITY)
+ {
+ p->x = x + icon[i].x / 20.0 * widget->width;
+ p->y = y + icon[i].y / 20.0 * widget->width;
+ p++;
+ }
+ else if (p != buffer)
+ {
+ XRenderCompositeDoublePoly (g_xui.dpy, PictOpOver,
+ source, g_xui.x11_pixmap_picture, format,
+ 0, 0, 0, 0, buffer, p - buffer, EvenOddRule);
+ p = buffer;
+ }
+ XRenderFreePicture (g_xui.dpy, source);
+}
+
+static struct widget *
+x11_make_symbol (chtype attrs, const char *label,
+ const struct xui_icon_point *icon)
+{
+ struct widget *w =
+ xui_new_symbol (x11_make_label (attrs, 0, label), icon);
+ if (icon)
+ {
+ w->on_render = x11_render_symbol;
+
+ // It should be padded by the caller horizontally.
+ w->height = g_xui.vunit;
+ w->width = w->height * 3 / 4;
+ }
+ return w;
+}
+
+static void
+x11_render_gauge (struct widget *widget)
+{
+ const struct xui_gauge *self = (const struct xui_gauge *) widget;
+
+ int part = self->fraction * widget->width;
+ XRenderFillRectangle (g_xui.dpy, PictOpSrc, g_xui.x11_pixmap_picture,
+ x11_fg_attrs (widget->attrs),
+ widget->x,
+ widget->y,
+ part,
+ widget->height);
+ XRenderFillRectangle (g_xui.dpy, PictOpSrc, g_xui.x11_pixmap_picture,
+ x11_bg_attrs (widget->attrs),
+ widget->x + part,
+ widget->y,
+ widget->width - part,
+ widget->height);
+}
+
+static struct widget *
+x11_make_gauge (chtype attrs, double fraction)
+{
+ struct xui_gauge *self = xcalloc (1, sizeof *self);
+ self->widget.text = "";
+ self->widget.on_render = x11_render_gauge;
+ self->widget.attrs = attrs;
+ self->widget.width = -1;
+ self->widget.height = g_xui.vunit;
+ self->fraction = MIN (MAX (fraction, 0.), 1.);
+ return &self->widget;
+}
+
+static void
+x11_render_scrollbar (struct widget *widget)
+{
+ struct xui_scrollbar *self = (struct xui_scrollbar *) widget;
+ x11_render_padding (widget);
+
+ struct xui_scrollbar_part bar =
+ xui_compute_scrollbar (self, widget->height, g_xui.vunit);
+
+ XRenderFillRectangle (g_xui.dpy, PictOpSrc, g_xui.x11_pixmap_picture,
+ x11_fg_attrs (widget->attrs),
+ widget->x,
+ widget->y + bar.start,
+ widget->width,
+ bar.length);
+}
+
+static struct widget *
+x11_make_scrollbar (chtype attrs, long top, long total)
+{
+ struct xui_scrollbar *self = xcalloc (1, sizeof *self);
+ self->widget.text = "";
+ self->widget.on_render = x11_render_scrollbar;
+ self->widget.attrs = attrs;
+ self->widget.width = g_xui.vunit / 2;
+ self->top = top;
+ self->total = total;
+ return &self->widget;
+}
+
static void
x11_render_widget (struct widget *w, const XRectangle *clip)
{
@@ -1452,6 +1789,9 @@ static struct ui x11_ui =
{
.padding = x11_make_padding,
.label = x11_make_label,
+ .symbol = x11_make_symbol,
+ .gauge = x11_make_gauge,
+ .scrollbar = x11_make_scrollbar,
.beep = x11_beep,
.render = x11_render,
@@ -2176,8 +2516,8 @@ appkit_render_padding (struct widget *self)
static struct widget *
appkit_make_padding (chtype attrs, float width, float height)
{
- struct widget *w = xcalloc (1, sizeof *w + 2);
- w->text[0] = ' ';
+ struct widget *w = xcalloc (1, sizeof *w);
+ w->text = " ";
w->on_render = appkit_render_padding;
w->attrs = attrs;
w->width = g_xui.vunit * width;
@@ -2205,6 +2545,7 @@ 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->text = (char *) (w + 1);
w->on_render = appkit_render_label;
w->attrs = attrs;
w->extended_attrs = extended;
@@ -2216,6 +2557,118 @@ appkit_make_label (chtype attrs, unsigned extended, const char *label)
return w;
}
+static void
+appkit_render_symbol (struct widget *widget)
+{
+ appkit_render_padding (widget);
+
+ const struct xui_icon_point *icon =
+ ((const struct xui_symbol *) widget)->icon;
+
+ // TODO: There might be an attribute for symbols, to handle this better.
+ NSColor *color = appkit_fg (widget);
+ if (!(widget->attrs & A_BOLD))
+ color = [color colorWithAlphaComponent:[color alphaComponent] * 0.5];
+
+ [color setFill];
+ NSBezierPath *path = [NSBezierPath bezierPath];
+ [path setWindingRule:NSWindingRuleEvenOdd];
+
+ int x = widget->x, y = widget->y + (widget->height - widget->width) / 2;
+ bool started = false;
+ for (const struct xui_icon_point *p = icon;
+ !isinf (p[0].x) || !isinf (p[0].y)
+ || !isinf (p[1].x) || !isinf (p[1].y); p++)
+ {
+ if (isinf (p->x))
+ {
+ [path closePath];
+ started = false;
+ continue;
+ }
+
+ NSPoint point = NSMakePoint
+ (x + p->x / 20.0 * widget->width,
+ y + p->y / 20.0 * widget->width);
+ if (!started)
+ [path moveToPoint:point];
+ else
+ [path lineToPoint:point];
+ started = true;
+ }
+ [path fill];
+}
+
+static struct widget *
+appkit_make_symbol (chtype attrs, const char *label,
+ const struct xui_icon_point *icon)
+{
+ struct widget *w =
+ xui_new_symbol (appkit_make_label (attrs, 0, label), icon);
+ if (icon)
+ {
+ w->on_render = appkit_render_symbol;
+
+ // It should be padded by the caller horizontally.
+ w->height = g_xui.vunit;
+ w->width = w->height * 3 / 4;
+ }
+ return w;
+}
+
+static void
+appkit_render_gauge (struct widget *widget)
+{
+ const struct xui_gauge *self = (const struct xui_gauge *) widget;
+
+ int part = self->fraction * widget->width;
+ [appkit_fg_attrs (widget->attrs) setFill];
+ NSRectFill (NSMakeRect (widget->x, widget->y, part, widget->height));
+ [appkit_bg_attrs (widget->attrs) setFill];
+ NSRectFill (NSMakeRect (widget->x + part, widget->y,
+ widget->width - part, widget->height));
+}
+
+static struct widget *
+appkit_make_gauge (chtype attrs, double fraction)
+{
+ struct xui_gauge *self = xcalloc (1, sizeof *self);
+ self->widget.text = "";
+ self->widget.on_render = appkit_render_gauge;
+ self->widget.attrs = attrs;
+ self->widget.width = -1;
+ self->widget.height = g_xui.vunit;
+ self->fraction = MIN (MAX (fraction, 0.), 1.);
+ return &self->widget;
+}
+
+static void
+appkit_render_scrollbar (struct widget *widget)
+{
+ struct xui_scrollbar *self = (struct xui_scrollbar *) widget;
+ appkit_render_padding (widget);
+
+ struct xui_scrollbar_part bar =
+ xui_compute_scrollbar (self, widget->height, g_xui.vunit);
+
+ [appkit_fg_attrs (widget->attrs) setFill];
+ NSRectFill (NSMakeRect
+ (widget->x, widget->y + bar.start, widget->width, bar.length));
+}
+
+static struct widget *
+appkit_make_scrollbar (chtype attrs, long top, long total)
+{
+ struct xui_scrollbar *self = xcalloc (1, sizeof *self);
+ self->widget.text = "";
+ self->widget.on_render = appkit_render_scrollbar;
+ self->widget.attrs = attrs;
+ self->widget.width = g_xui.vunit / 2;
+ self->top = top;
+ self->total = total;
+ return &self->widget;
+}
+
static void
appkit_render_widget (struct widget *w, NSRect clip)
{
@@ -2297,6 +2750,9 @@ static struct ui appkit_ui =
{
.padding = appkit_make_padding,
.label = appkit_make_label,
+ .symbol = appkit_make_symbol,
+ .gauge = appkit_make_gauge,
+ .scrollbar = appkit_make_scrollbar,
.beep = appkit_beep,
.render = appkit_render,
@@ -2928,6 +3384,7 @@ static struct widget *
xui_hbox (struct widget *head)
{
struct widget *self = xcalloc (1, sizeof *self);
+ self->text = "";
self->children = head;
self->on_allocated = xui_on_hbox_allocated;
@@ -2981,6 +3438,7 @@ static struct widget *
xui_vbox (struct widget *head)
{
struct widget *self = xcalloc (1, sizeof *self);
+ self->text = "";
self->children = head;
self->on_allocated = xui_on_vbox_allocated;
@@ -3056,6 +3514,9 @@ xui_preinit (void)
// Note that non-Unicode locales are handled pretty inefficiently.
g_xui.locale_is_utf8 = !strcasecmp_ascii (locale_charset (), "UTF-8");
+ // It doesn't work 100% (e.g. incompatible with underlining in urxvt)
+ g_xui.use_partial_boxes = g_xui.locale_is_utf8;
+
// Presumably, although not necessarily; unsure if queryable at all.
g_xui.focused = true;