Index: uspace/lib/ui/src/paint.c
===================================================================
--- uspace/lib/ui/src/paint.c	(revision 81ec7e1867942dce648bbef472b80d7b8a71f294)
+++ uspace/lib/ui/src/paint.c	(revision 1eaead40ef1e81dc0e203498597d60b2e71fcd27)
@@ -1,4 +1,4 @@
 /*
- * Copyright (c) 2021 Jiri Svoboda
+ * Copyright (c) 2023 Jiri Svoboda
  * All rights reserved.
  *
@@ -39,4 +39,5 @@
 #include <gfx/render.h>
 #include <gfx/text.h>
+#include <ui/accel.h>
 #include <ui/paint.h>
 #include <stdlib.h>
@@ -359,14 +360,289 @@
 }
 
-/** Paint a text box.
+/** Paint upward pointing triangle.
+ *
+ * @param gc Graphic context
+ * @param pos Center position
+ * @param n Length of triangle side
+ * @return EOK on success or an error code
+ */
+errno_t ui_paint_up_triangle(gfx_context_t *gc, gfx_coord2_t *pos,
+    gfx_coord_t n)
+{
+	gfx_coord_t i;
+	errno_t rc;
+	gfx_rect_t rect;
+
+	for (i = 0; i < n; i++) {
+		rect.p0.x = pos->x - i;
+		rect.p0.y = pos->y - n / 2 + i;
+		rect.p1.x = pos->x + i + 1;
+		rect.p1.y = pos->y - n / 2 + i + 1;
+		rc = gfx_fill_rect(gc, &rect);
+		if (rc != EOK)
+			return rc;
+	}
+
+	return EOK;
+}
+
+/** Paint downward pointing triangle.
+ *
+ * @param gc Graphic context
+ * @param pos Center position
+ * @param n Length of triangle side
+ * @return EOK on success or an error code
+ */
+errno_t ui_paint_down_triangle(gfx_context_t *gc, gfx_coord2_t *pos,
+    gfx_coord_t n)
+{
+	gfx_coord_t i;
+	errno_t rc;
+	gfx_rect_t rect;
+
+	for (i = 0; i < n; i++) {
+		rect.p0.x = pos->x - i;
+		rect.p0.y = pos->y + n / 2 - i;
+		rect.p1.x = pos->x + i + 1;
+		rect.p1.y = pos->y + n / 2 - i + 1;
+		rc = gfx_fill_rect(gc, &rect);
+		if (rc != EOK)
+			return rc;
+	}
+
+	return EOK;
+}
+
+/** Paint left pointing triangle.
+ *
+ * @param gc Graphic context
+ * @param pos Center position
+ * @param n Length of triangle side
+ * @return EOK on success or an error code
+ */
+errno_t ui_paint_left_triangle(gfx_context_t *gc, gfx_coord2_t *pos,
+    gfx_coord_t n)
+{
+	gfx_coord_t i;
+	errno_t rc;
+	gfx_rect_t rect;
+
+	for (i = 0; i < n; i++) {
+		rect.p0.x = pos->x - n / 2 + i;
+		rect.p0.y = pos->y - i;
+		rect.p1.x = pos->x - n / 2 + i + 1;
+		rect.p1.y = pos->y + i + 1;
+		rc = gfx_fill_rect(gc, &rect);
+		if (rc != EOK)
+			return rc;
+	}
+
+	return EOK;
+}
+
+/** Paint right pointing triangle.
+ *
+ * @param gc Graphic context
+ * @param pos Center position
+ * @param n Length of triangle side
+ * @return EOK on success or an error code
+ */
+errno_t ui_paint_right_triangle(gfx_context_t *gc, gfx_coord2_t *pos,
+    gfx_coord_t n)
+{
+	gfx_coord_t i;
+	errno_t rc;
+	gfx_rect_t rect;
+
+	for (i = 0; i < n; i++) {
+		rect.p0.x = pos->x + n / 2 - i;
+		rect.p0.y = pos->y - i;
+		rect.p1.x = pos->x + n / 2 - i + 1;
+		rect.p1.y = pos->y + i + 1;
+		rc = gfx_fill_rect(gc, &rect);
+		if (rc != EOK)
+			return rc;
+	}
+
+	return EOK;
+}
+
+/** Paint diagonal cross (X).
+ *
+ * @param gc Graphic context
+ * @param pos Center position
+ * @param n Length of each leg
+ * @param w Pen width
+ * @param h Pen height
+ * @return EOK on success or an error code
+ */
+errno_t ui_paint_cross(gfx_context_t *gc, gfx_coord2_t *pos,
+    gfx_coord_t n, gfx_coord_t w, gfx_coord_t h)
+{
+	gfx_coord_t i;
+	gfx_rect_t rect;
+	errno_t rc;
+
+	rect.p0.x = pos->x;
+	rect.p0.y = pos->y;
+	rect.p1.x = pos->x + w;
+	rect.p1.y = pos->y + h;
+	rc = gfx_fill_rect(gc, &rect);
+	if (rc != EOK)
+		return rc;
+
+	for (i = 1; i < n; i++) {
+		rect.p0.x = pos->x - i;
+		rect.p0.y = pos->y - i;
+		rect.p1.x = pos->x - i + w;
+		rect.p1.y = pos->y - i + h;
+		rc = gfx_fill_rect(gc, &rect);
+		if (rc != EOK)
+			return rc;
+
+		rect.p0.x = pos->x - i;
+		rect.p0.y = pos->y + i;
+		rect.p1.x = pos->x - i + w;
+		rect.p1.y = pos->y + i + h;
+		rc = gfx_fill_rect(gc, &rect);
+		if (rc != EOK)
+			return rc;
+
+		rect.p0.x = pos->x + i;
+		rect.p0.y = pos->y - i;
+		rect.p1.x = pos->x + i + w;
+		rect.p1.y = pos->y - i + h;
+		rc = gfx_fill_rect(gc, &rect);
+		if (rc != EOK)
+			return rc;
+
+		rect.p0.x = pos->x + i;
+		rect.p0.y = pos->y + i;
+		rect.p1.x = pos->x + i + w;
+		rect.p1.y = pos->y + i + h;
+		rc = gfx_fill_rect(gc, &rect);
+		if (rc != EOK)
+			return rc;
+	}
+
+	return EOK;
+}
+
+/** Paint minimize icon.
+ *
+ * @param resource UI resource
+ * @param pos Center position
+ * @param w Icon width
+ * @param h Icon height
+ * @return EOK on success or an error code
+ */
+errno_t ui_paint_minicon(ui_resource_t *resource, gfx_coord2_t *pos,
+    gfx_coord_t w, gfx_coord_t h)
+{
+	gfx_rect_t rect;
+	errno_t rc;
+
+	rc = gfx_set_color(resource->gc, resource->btn_text_color);
+	if (rc != EOK)
+		return rc;
+
+	rect.p0.x = pos->x - w / 2;
+	rect.p0.y = pos->y + h / 2 - 2;
+	rect.p1.x = rect.p0.x + w;
+	rect.p1.y = rect.p0.y + 2;
+	rc = gfx_fill_rect(resource->gc, &rect);
+	if (rc != EOK)
+		return rc;
+
+	return EOK;
+}
+
+/** Paint maximize icon.
+ *
+ * @param resource UI resource
+ * @param pos Center position
+ * @param w Icon width
+ * @param h Icon height
+ * @return EOK on success or an error code
+ */
+errno_t ui_paint_maxicon(ui_resource_t *resource, gfx_coord2_t *pos,
+    gfx_coord_t w, gfx_coord_t h)
+{
+	gfx_rect_t rect;
+	errno_t rc;
+
+	rc = gfx_set_color(resource->gc, resource->btn_text_color);
+	if (rc != EOK)
+		return rc;
+
+	rect.p0.x = pos->x - w / 2;
+	rect.p0.y = pos->y - h / 2;
+	rect.p1.x = rect.p0.x + w;
+	rect.p1.y = rect.p0.y + h;
+	rc = gfx_fill_rect(resource->gc, &rect);
+	if (rc != EOK)
+		return rc;
+
+	rc = gfx_set_color(resource->gc, resource->btn_face_color);
+	if (rc != EOK)
+		return rc;
+
+	rect.p0.x += 1;
+	rect.p0.y += 2;
+	rect.p1.x -= 1;
+	rect.p1.y -= 1;
+	rc = gfx_fill_rect(resource->gc, &rect);
+	if (rc != EOK)
+		return rc;
+
+	return EOK;
+}
+
+/** Paint unmaximize icon.
+ *
+ * Unmaximize icon consists of two overlapping window icons.
+ *
+ * @param resource UI resource
+ * @param pos Center position
+ * @param w Window icon width
+ * @param h Window icon height
+ * @param dw Horizontal distance between window icon centers
+ * @param dh Vertical distance between window icon centers
+ * @return EOK on success or an error code
+ */
+errno_t ui_paint_unmaxicon(ui_resource_t *resource, gfx_coord2_t *pos,
+    gfx_coord_t w, gfx_coord_t h, gfx_coord_t dw, gfx_coord_t dh)
+{
+	gfx_coord2_t p;
+	errno_t rc;
+
+	p.x = pos->x + dw / 2;
+	p.y = pos->y - dh / 2;
+	rc = ui_paint_maxicon(resource, &p, w, h);
+	if (rc != EOK)
+		return rc;
+
+	p.x = pos->x - dw / 2;
+	p.y = pos->y + dh / 2;
+	rc = ui_paint_maxicon(resource, &p, w, h);
+	if (rc != EOK)
+		return rc;
+
+	return EOK;
+}
+
+/** Paint a custom text box.
+ *
+ * Paint a text box using user-provided box chars.
  *
  * @param resource UI resource
  * @param rect Rectangle inside which to paint the box
  * @param style Box style
+ * @param boxc Box characters
  * @param color Color
  * @return EOK on success or an error code
  */
-errno_t ui_paint_text_box(ui_resource_t *resource, gfx_rect_t *rect,
-    ui_box_style_t style, gfx_color_t *color)
+errno_t ui_paint_text_box_custom(ui_resource_t *resource, gfx_rect_t *rect,
+    ui_box_chars_t *boxc, gfx_color_t *color)
 {
 	errno_t rc;
@@ -380,5 +656,4 @@
 	gfx_coord_t y;
 	char *str = NULL;
-	ui_box_chars_t *boxc = NULL;
 
 	gfx_rect_points_sort(rect, &srect);
@@ -388,4 +663,93 @@
 	if (dim.x < 2 || dim.y < 2)
 		return EOK;
+
+	gfx_text_fmt_init(&fmt);
+	fmt.font = resource->font;
+	fmt.color = color;
+
+	bufsz = str_size(boxc->c[0][0]) +
+	    str_size(boxc->c[0][1]) * (dim.x - 2) +
+	    str_size(boxc->c[0][2]) + 1;
+
+	str = malloc(bufsz);
+	if (str == NULL)
+		return ENOMEM;
+
+	/* Top edge and corners */
+
+	str_cpy(str, bufsz, boxc->c[0][0]);
+	off = str_size(boxc->c[0][0]);
+
+	for (i = 1; i < dim.x - 1; i++) {
+		str_cpy(str + off, bufsz - off, boxc->c[0][1]);
+		off += str_size(boxc->c[0][1]);
+	}
+
+	str_cpy(str + off, bufsz - off, boxc->c[0][2]);
+	off += str_size(boxc->c[0][2]);
+	str[off] = '\0';
+
+	pos = rect->p0;
+	rc = gfx_puttext(&pos, &fmt, str);
+	if (rc != EOK)
+		goto error;
+
+	/* Vertical edges */
+	for (y = rect->p0.y + 1; y < rect->p1.y - 1; y++) {
+		pos.y = y;
+
+		pos.x = rect->p0.x;
+		rc = gfx_puttext(&pos, &fmt, boxc->c[1][0]);
+		if (rc != EOK)
+			goto error;
+
+		pos.x = rect->p1.x - 1;
+		rc = gfx_puttext(&pos, &fmt, boxc->c[1][2]);
+		if (rc != EOK)
+			goto error;
+	}
+
+	/* Bottom edge and corners */
+
+	str_cpy(str, bufsz, boxc->c[2][0]);
+	off = str_size(boxc->c[2][0]);
+
+	for (i = 1; i < dim.x - 1; i++) {
+		str_cpy(str + off, bufsz - off, boxc->c[2][1]);
+		off += str_size(boxc->c[2][1]);
+	}
+
+	str_cpy(str + off, bufsz - off, boxc->c[2][2]);
+	off += str_size(boxc->c[2][2]);
+	str[off] = '\0';
+
+	pos.x = rect->p0.x;
+	pos.y = rect->p1.y - 1;
+	rc = gfx_puttext(&pos, &fmt, str);
+	if (rc != EOK)
+		goto error;
+
+	free(str);
+	return EOK;
+error:
+	if (str != NULL)
+		free(str);
+	return rc;
+}
+
+/** Paint a text box.
+ *
+ * Paint a text box with the specified style.
+ *
+ * @param resource UI resource
+ * @param rect Rectangle inside which to paint the box
+ * @param style Box style
+ * @param color Color
+ * @return EOK on success or an error code
+ */
+errno_t ui_paint_text_box(ui_resource_t *resource, gfx_rect_t *rect,
+    ui_box_style_t style, gfx_color_t *color)
+{
+	ui_box_chars_t *boxc = NULL;
 
 	switch (style) {
@@ -401,77 +765,5 @@
 		return EINVAL;
 
-	gfx_text_fmt_init(&fmt);
-	fmt.color = color;
-
-	bufsz = str_size(boxc->c[0][0]) +
-	    str_size(boxc->c[0][1]) * (dim.x - 2) +
-	    str_size(boxc->c[0][2]) + 1;
-
-	str = malloc(bufsz);
-	if (str == NULL)
-		return ENOMEM;
-
-	/* Top edge and corners */
-
-	str_cpy(str, bufsz, boxc->c[0][0]);
-	off = str_size(boxc->c[0][0]);
-
-	for (i = 1; i < dim.x - 1; i++) {
-		str_cpy(str + off, bufsz - off, boxc->c[0][1]);
-		off += str_size(boxc->c[0][1]);
-	}
-
-	str_cpy(str + off, bufsz - off, boxc->c[0][2]);
-	off += str_size(boxc->c[0][2]);
-	str[off] = '\0';
-
-	pos = rect->p0;
-	rc = gfx_puttext(resource->font, &pos, &fmt, str);
-	if (rc != EOK)
-		goto error;
-
-	/* Vertical edges */
-	for (y = rect->p0.y + 1; y < rect->p1.y - 1; y++) {
-		pos.y = y;
-
-		pos.x = rect->p0.x;
-		rc = gfx_puttext(resource->font, &pos, &fmt,
-		    boxc->c[1][0]);
-		if (rc != EOK)
-			goto error;
-
-		pos.x = rect->p1.x - 1;
-		rc = gfx_puttext(resource->font, &pos, &fmt,
-		    boxc->c[1][2]);
-		if (rc != EOK)
-			goto error;
-	}
-
-	/* Bottom edge and corners */
-
-	str_cpy(str, bufsz, boxc->c[2][0]);
-	off = str_size(boxc->c[2][0]);
-
-	for (i = 1; i < dim.x - 1; i++) {
-		str_cpy(str + off, bufsz - off, boxc->c[2][1]);
-		off += str_size(boxc->c[2][1]);
-	}
-
-	str_cpy(str + off, bufsz - off, boxc->c[2][2]);
-	off += str_size(boxc->c[2][2]);
-	str[off] = '\0';
-
-	pos.x = rect->p0.x;
-	pos.y = rect->p1.y - 1;
-	rc = gfx_puttext(resource->font, &pos, &fmt, str);
-	if (rc != EOK)
-		goto error;
-
-	free(str);
-	return EOK;
-error:
-	if (str != NULL)
-		free(str);
-	return rc;
+	return ui_paint_text_box_custom(resource, rect, boxc, color);
 }
 
@@ -519,4 +811,5 @@
 
 	gfx_text_fmt_init(&fmt);
+	fmt.font = resource->font;
 	fmt.color = color;
 
@@ -542,5 +835,5 @@
 
 	pos = rect->p0;
-	rc = gfx_puttext(resource->font, &pos, &fmt, str);
+	rc = gfx_puttext(&pos, &fmt, str);
 	if (rc != EOK)
 		goto error;
@@ -554,4 +847,157 @@
 }
 
+/** Fill rectangle with text character.
+ *
+ * @param resource UI resource
+ * @param rect Rectangle
+ * @param color Color
+ * @param gchar Character to fill with
+ * @return EOK on success or an error code
+ */
+errno_t ui_paint_text_rect(ui_resource_t *resource, gfx_rect_t *rect,
+    gfx_color_t *color, const char *gchar)
+{
+	gfx_coord2_t pos;
+	gfx_text_fmt_t fmt;
+	gfx_rect_t srect;
+	gfx_coord_t w, i;
+	char *buf;
+	size_t gcharsz;
+	errno_t rc;
+
+	gfx_rect_points_sort(rect, &srect);
+
+	gfx_text_fmt_init(&fmt);
+	fmt.font = resource->font;
+	fmt.color = color;
+	fmt.halign = gfx_halign_left;
+	fmt.valign = gfx_valign_top;
+
+	w = srect.p1.x - srect.p0.x;
+	if (w == 0)
+		return EOK;
+
+	gcharsz = str_size(gchar);
+
+	buf = malloc(w * gcharsz + 1);
+	if (buf == NULL)
+		return ENOMEM;
+
+	for (i = 0; i < w; i++)
+		str_cpy(buf + i * gcharsz, (w - i) * gcharsz + 1, gchar);
+	buf[w * gcharsz] = '\0';
+
+	pos.x = srect.p0.x;
+	for (pos.y = srect.p0.y; pos.y < srect.p1.y; pos.y++) {
+		rc = gfx_puttext(&pos, &fmt, buf);
+		if (rc != EOK)
+			goto error;
+	}
+
+	free(buf);
+	return EOK;
+error:
+	free(buf);
+	return rc;
+}
+
+/** Initialize UI text formatting structure.
+ *
+ * UI text formatting structure must always be initialized using this function
+ * first.
+ *
+ * @param fmt UI text formatting structure
+ */
+void ui_text_fmt_init(ui_text_fmt_t *fmt)
+{
+	memset(fmt, 0, sizeof(ui_text_fmt_t));
+}
+
+/** Compute UI text width.
+ *
+ * @param font Font
+ * @param str String
+ * @return Text width
+ */
+gfx_coord_t ui_text_width(gfx_font_t *font, const char *str)
+{
+	gfx_coord_t w;
+	gfx_coord_t tw;
+	const char *sp;
+
+	/* Text including tildes */
+	w = gfx_text_width(font, str);
+	tw = gfx_text_width(font, "~");
+
+	/* Subtract width of singular tildes */
+	sp = str;
+	while (*sp != '\0') {
+		if (*sp == '~' && sp[1] != '~')
+			w -= tw;
+		++sp;
+	}
+
+	return w;
+}
+
+/** Paint text (with highlighting markup).
+ *
+ * Paint some text with highlighted sections enclosed with tilde characters
+ * ('~'). A literal tilde can be printed with "~~". Text can be highlighted
+ * with an underline or different color, based on display capabilities.
+ *
+ * @param pos Anchor position
+ * @param fmt Text formatting
+ * @param str String containing '~' markup
+ *
+ * @return EOK on success or an error code
+ */
+errno_t ui_paint_text(gfx_coord2_t *pos, ui_text_fmt_t *fmt, const char *str)
+{
+	gfx_coord2_t tpos;
+	gfx_text_fmt_t tfmt;
+	char *buf;
+	char *endptr;
+	const char *sp;
+	errno_t rc;
+
+	/* Break down string into list of (non)highlighted parts */
+	rc = ui_accel_process(str, &buf, &endptr);
+	if (rc != EOK)
+		return rc;
+
+	gfx_text_fmt_init(&tfmt);
+	tfmt.font = fmt->font;
+	tfmt.color = fmt->color;
+	tfmt.halign = fmt->halign;
+	tfmt.width = fmt->width;
+	tfmt.valign = fmt->valign;
+	tfmt.underline = false;
+
+	tpos = *pos;
+	sp = buf;
+	while (sp != endptr) {
+		/* Print the current string */
+		rc = gfx_puttext(&tpos, &tfmt, sp);
+		if (rc != EOK)
+			goto error;
+
+		gfx_text_cont(&tpos, &tfmt, sp, &tpos, &tfmt);
+		tfmt.underline = !tfmt.underline;
+		tfmt.color = tfmt.underline ? fmt->hgl_color : fmt->color;
+
+		/* Skip to the next string */
+		while (*sp != '\0')
+			++sp;
+		++sp;
+	}
+
+	free(buf);
+	return EOK;
+error:
+	free(buf);
+	return rc;
+}
+
 /** @}
  */
