Index: uspace/lib/ui/src/list.c
===================================================================
--- uspace/lib/ui/src/list.c	(revision 7cf5ddbad0929896f7de7dcfdf23d42fe47929b3)
+++ uspace/lib/ui/src/list.c	(revision 7cf5ddbad0929896f7de7dcfdf23d42fe47929b3)
@@ -0,0 +1,1462 @@
+/*
+ * Copyright (c) 2023 Jiri Svoboda
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ * - The name of the author may not be used to endorse or promote products
+ *   derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/** @addtogroup libui
+ * @{
+ */
+/** @file List.
+ *
+ * Simple list control.
+ */
+
+#include <errno.h>
+#include <gfx/render.h>
+#include <gfx/text.h>
+#include <stdlib.h>
+#include <str.h>
+#include <ui/control.h>
+#include <ui/list.h>
+#include <ui/paint.h>
+#include <ui/resource.h>
+#include <ui/scrollbar.h>
+#include "../private/list.h"
+#include "../private/resource.h"
+
+static void ui_list_ctl_destroy(void *);
+static errno_t ui_list_ctl_paint(void *);
+static ui_evclaim_t ui_list_ctl_kbd_event(void *, kbd_event_t *);
+static ui_evclaim_t ui_list_ctl_pos_event(void *, pos_event_t *);
+
+/** List control ops */
+ui_control_ops_t ui_list_ctl_ops = {
+	.destroy = ui_list_ctl_destroy,
+	.paint = ui_list_ctl_paint,
+	.kbd_event = ui_list_ctl_kbd_event,
+	.pos_event = ui_list_ctl_pos_event
+};
+
+enum {
+	list_entry_hpad = 2,
+	list_entry_vpad = 2,
+	list_entry_hpad_text = 1,
+	list_entry_vpad_text = 0,
+};
+
+static void ui_list_scrollbar_up(ui_scrollbar_t *, void *);
+static void ui_list_scrollbar_down(ui_scrollbar_t *, void *);
+static void ui_list_scrollbar_page_up(ui_scrollbar_t *, void *);
+static void ui_list_scrollbar_page_down(ui_scrollbar_t *, void *);
+static void ui_list_scrollbar_moved(ui_scrollbar_t *, void *, gfx_coord_t);
+
+/** List scrollbar callbacks */
+static ui_scrollbar_cb_t ui_list_scrollbar_cb = {
+	.up = ui_list_scrollbar_up,
+	.down = ui_list_scrollbar_down,
+	.page_up = ui_list_scrollbar_page_up,
+	.page_down = ui_list_scrollbar_page_down,
+	.moved = ui_list_scrollbar_moved
+};
+
+/** Create UI list.
+ *
+ * @param window Containing window
+ * @param active @c true iff list should be active
+ * @param rlist Place to store pointer to new list
+ * @return EOK on success or an error code
+ */
+errno_t ui_list_create(ui_window_t *window, bool active,
+    ui_list_t **rlist)
+{
+	ui_list_t *list;
+	errno_t rc;
+
+	list = calloc(1, sizeof(ui_list_t));
+	if (list == NULL)
+		return ENOMEM;
+
+	rc = ui_control_new(&ui_list_ctl_ops, (void *)list,
+	    &list->control);
+	if (rc != EOK) {
+		free(list);
+		return rc;
+	}
+
+	rc = ui_scrollbar_create(ui_window_get_ui(window), window,
+	    ui_sbd_vert, &list->scrollbar);
+	if (rc != EOK)
+		goto error;
+
+	ui_scrollbar_set_cb(list->scrollbar, &ui_list_scrollbar_cb,
+	    (void *) list);
+
+	list->window = window;
+	list_initialize(&list->entries);
+	list->entries_cnt = 0;
+	list->active = active;
+
+	*rlist = list;
+	return EOK;
+error:
+	ui_control_delete(list->control);
+	free(list);
+	return rc;
+}
+
+/** Destroy UI list.
+ *
+ * @param list UI list
+ */
+void ui_list_destroy(ui_list_t *list)
+{
+	ui_list_clear_entries(list);
+	ui_control_delete(list->control);
+	free(list);
+}
+
+/** Set UI list callbacks.
+ *
+ * @param list UI list
+ * @param cb Callbacks
+ * @param arg Argument to callback functions
+ */
+void ui_list_set_cb(ui_list_t *list, ui_list_cb_t *cb, void *arg)
+{
+	list->cb = cb;
+	list->cb_arg = arg;
+}
+
+/** Get height of list entry.
+ *
+ * @param list UI list
+ * @return Entry height in pixels
+ */
+gfx_coord_t ui_list_entry_height(ui_list_t *list)
+{
+	ui_resource_t *res;
+	gfx_font_metrics_t metrics;
+	gfx_coord_t height;
+	gfx_coord_t vpad;
+
+	res = ui_window_get_res(list->window);
+
+	if (res->textmode) {
+		vpad = list_entry_vpad_text;
+	} else {
+		vpad = list_entry_vpad;
+	}
+
+	/* Normal menu entry */
+	gfx_font_get_metrics(res->font, &metrics);
+	height = metrics.ascent + metrics.descent + 1;
+
+	return height + 2 * vpad;
+}
+
+/** Paint list entry.
+ *
+ * @param entry List entry
+ * @param entry_idx Entry index (within list of entries)
+ * @return EOK on success or an error code
+ */
+errno_t ui_list_entry_paint(ui_list_entry_t *entry, size_t entry_idx)
+{
+	ui_list_t *list = entry->list;
+	gfx_context_t *gc = ui_window_get_gc(list->window);
+	ui_resource_t *res = ui_window_get_res(list->window);
+	gfx_font_t *font = ui_resource_get_font(res);
+	gfx_text_fmt_t fmt;
+	gfx_coord2_t pos;
+	gfx_rect_t rect;
+	gfx_rect_t lrect;
+	gfx_rect_t crect;
+	gfx_color_t *bgcolor;
+	gfx_coord_t hpad, vpad;
+	gfx_coord_t line_height;
+	size_t rows;
+	errno_t rc;
+
+	line_height = ui_list_entry_height(list);
+	ui_list_inside_rect(entry->list, &lrect);
+
+	gfx_text_fmt_init(&fmt);
+	fmt.font = font;
+	rows = ui_list_page_size(list) + 1;
+
+	/* Do not display entry outside of current page */
+	if (entry_idx < list->page_idx ||
+	    entry_idx >= list->page_idx + rows)
+		return EOK;
+
+	if (res->textmode) {
+		hpad = list_entry_hpad_text;
+		vpad = list_entry_vpad_text;
+	} else {
+		hpad = list_entry_hpad;
+		vpad = list_entry_vpad;
+	}
+
+	pos.x = lrect.p0.x;
+	pos.y = lrect.p0.y + line_height * (entry_idx - list->page_idx);
+
+	if (entry == list->cursor && list->active) {
+		fmt.color = res->entry_sel_text_fg_color;
+		bgcolor = res->entry_sel_text_bg_color;
+	} else {
+		if (entry->color != NULL)
+			fmt.color = entry->color;
+		else
+			fmt.color = res->entry_fg_color;
+
+		if (entry->bgcolor != NULL)
+			bgcolor = entry->bgcolor;
+		else
+			bgcolor = res->entry_bg_color;
+	}
+
+	/* Draw entry background */
+	rect.p0 = pos;
+	rect.p1.x = lrect.p1.x;
+	rect.p1.y = rect.p0.y + line_height;
+
+	/* Clip to list interior */
+	gfx_rect_clip(&rect, &lrect, &crect);
+
+	rc = gfx_set_color(gc, bgcolor);
+	if (rc != EOK)
+		return rc;
+
+	rc = gfx_fill_rect(gc, &crect);
+	if (rc != EOK)
+		return rc;
+
+	/*
+	 * Make sure caption does not overflow the entry rectangle.
+	 *
+	 * XXX We probably want to measure the text width, and,
+	 * if it's too long, use gfx_text_find_pos() to find where
+	 * it should be cut off (and append some sort of overflow
+	 * marker.
+	 */
+	rc = gfx_set_clip_rect(gc, &crect);
+	if (rc != EOK)
+		return rc;
+
+	pos.x += hpad;
+	pos.y += vpad;
+
+	rc = gfx_puttext(&pos, &fmt, entry->caption);
+	if (rc != EOK) {
+		(void) gfx_set_clip_rect(gc, NULL);
+		return rc;
+	}
+
+	return gfx_set_clip_rect(gc, NULL);
+}
+
+/** Paint UI list.
+ *
+ * @param list UI list
+ */
+errno_t ui_list_paint(ui_list_t *list)
+{
+	gfx_context_t *gc = ui_window_get_gc(list->window);
+	ui_resource_t *res = ui_window_get_res(list->window);
+	ui_list_entry_t *entry;
+	int i, lines;
+	errno_t rc;
+
+	rc = gfx_set_color(gc, res->entry_bg_color);
+	if (rc != EOK)
+		return rc;
+
+	rc = gfx_fill_rect(gc, &list->rect);
+	if (rc != EOK)
+		return rc;
+
+	if (!res->textmode) {
+		rc = ui_paint_inset_frame(res, &list->rect, NULL);
+		if (rc != EOK)
+			return rc;
+	}
+
+	lines = ui_list_page_size(list) + 1;
+	i = 0;
+
+	entry = list->page;
+	while (entry != NULL && i < lines) {
+		rc = ui_list_entry_paint(entry, list->page_idx + i);
+		if (rc != EOK)
+			return rc;
+
+		++i;
+		entry = ui_list_next(entry);
+	}
+
+	rc = ui_scrollbar_paint(list->scrollbar);
+	if (rc != EOK)
+		return rc;
+
+	rc = gfx_update(gc);
+	if (rc != EOK)
+		return rc;
+
+	return EOK;
+}
+
+/** Handle list keyboard event.
+ *
+ * @param list UI list
+ * @param event Keyboard event
+ * @return ui_claimed iff event was claimed
+ */
+ui_evclaim_t ui_list_kbd_event(ui_list_t *list, kbd_event_t *event)
+{
+	if (!list->active)
+		return ui_unclaimed;
+
+	if (event->type == KEY_PRESS) {
+		if ((event->mods & (KM_CTRL | KM_ALT | KM_SHIFT)) == 0) {
+			switch (event->key) {
+			case KC_UP:
+				ui_list_cursor_up(list);
+				break;
+			case KC_DOWN:
+				ui_list_cursor_down(list);
+				break;
+			case KC_HOME:
+				ui_list_cursor_top(list);
+				break;
+			case KC_END:
+				ui_list_cursor_bottom(list);
+				break;
+			case KC_PAGE_UP:
+				ui_list_page_up(list);
+				break;
+			case KC_PAGE_DOWN:
+				ui_list_page_down(list);
+				break;
+			case KC_ENTER:
+				ui_list_selected(list->cursor);
+				break;
+			default:
+				break;
+			}
+		}
+	}
+
+	return ui_claimed;
+}
+
+/** Handle UI list position event.
+ *
+ * @param list UI list
+ * @param event Position event
+ * @return ui_claimed iff event was claimed
+ */
+ui_evclaim_t ui_list_pos_event(ui_list_t *list, pos_event_t *event)
+{
+	gfx_coord2_t pos;
+	gfx_rect_t irect;
+	ui_list_entry_t *entry;
+	gfx_coord_t line_height;
+	size_t entry_idx;
+	ui_evclaim_t claim;
+	int n;
+
+	claim = ui_scrollbar_pos_event(list->scrollbar, event);
+	if (claim == ui_claimed)
+		return ui_claimed;
+
+	line_height = ui_list_entry_height(list);
+
+	pos.x = event->hpos;
+	pos.y = event->vpos;
+	if (!gfx_pix_inside_rect(&pos, &list->rect))
+		return ui_unclaimed;
+
+	if (event->type == POS_PRESS || event->type == POS_DCLICK) {
+		ui_list_inside_rect(list, &irect);
+
+		/* Did we click on one of the entries? */
+		if (gfx_pix_inside_rect(&pos, &irect)) {
+			/* Index within page */
+			n = (pos.y - irect.p0.y) / line_height;
+
+			/* Entry and its index within entire listing */
+			entry = ui_list_page_nth_entry(list, n, &entry_idx);
+			if (entry == NULL)
+				return ui_claimed;
+
+			if (event->type == POS_PRESS) {
+				/* Move to the entry found */
+				ui_list_cursor_move(list, entry, entry_idx);
+			} else {
+				/* event->type == POS_DCLICK */
+				ui_list_selected(entry);
+			}
+		} else {
+			/* It's in the border. */
+			if (event->type == POS_PRESS) {
+				/* Top or bottom half? */
+				if (pos.y >= (irect.p0.y + irect.p1.y) / 2)
+					ui_list_page_down(list);
+				else
+					ui_list_page_up(list);
+			}
+		}
+	}
+
+	if (!list->active && event->type == POS_PRESS)
+		ui_list_activate_req(list);
+
+	return ui_claimed;
+}
+
+/** Get base control for UI list.
+ *
+ * @param list UI list
+ * @return Base UI control
+ */
+ui_control_t *ui_list_ctl(ui_list_t *list)
+{
+	return list->control;
+}
+
+/** Set UI list rectangle.
+ *
+ * @param list UI list
+ * @param rect Rectangle
+ */
+void ui_list_set_rect(ui_list_t *list, gfx_rect_t *rect)
+{
+	gfx_rect_t srect;
+
+	list->rect = *rect;
+
+	ui_list_scrollbar_rect(list, &srect);
+	ui_scrollbar_set_rect(list->scrollbar, &srect);
+}
+
+/** Get UI list page size.
+ *
+ * @param list UI list
+ * @return Number of entries that fit in list at the same time.
+ */
+unsigned ui_list_page_size(ui_list_t *list)
+{
+	gfx_coord_t line_height;
+	gfx_rect_t irect;
+
+	line_height = ui_list_entry_height(list);
+	ui_list_inside_rect(list, &irect);
+	return (irect.p1.y - irect.p0.y) / line_height;
+}
+
+/** Get UI list interior rectangle.
+ *
+ * @param list UI list
+ * @param irect Place to store interior rectangle
+ */
+void ui_list_inside_rect(ui_list_t *list, gfx_rect_t *irect)
+{
+	ui_resource_t *res = ui_window_get_res(list->window);
+	gfx_rect_t rect;
+	gfx_coord_t width;
+
+	if (res->textmode) {
+		rect = list->rect;
+	} else {
+		ui_paint_get_inset_frame_inside(res, &list->rect, &rect);
+	}
+
+	if (res->textmode) {
+		width = 1;
+	} else {
+		width = 23;
+	}
+
+	irect->p0 = rect.p0;
+	irect->p1.x = rect.p1.x - width;
+	irect->p1.y = rect.p1.y;
+}
+
+/** Get UI list scrollbar rectangle.
+ *
+ * @param list UI list
+ * @param irect Place to store interior rectangle
+ */
+void ui_list_scrollbar_rect(ui_list_t *list, gfx_rect_t *srect)
+{
+	ui_resource_t *res = ui_window_get_res(list->window);
+	gfx_rect_t rect;
+	gfx_coord_t width;
+
+	if (res->textmode) {
+		rect = list->rect;
+	} else {
+		ui_paint_get_inset_frame_inside(res, &list->rect, &rect);
+	}
+
+	if (res->textmode) {
+		width = 1;
+	} else {
+		width = 23;
+	}
+
+	srect->p0.x = rect.p1.x - width;
+	srect->p0.y = rect.p0.y;
+	srect->p1 = rect.p1;
+}
+
+/** Compute new position for UI list scrollbar thumb.
+ *
+ * @param list UI list
+ * @return New position
+ */
+gfx_coord_t ui_list_scrollbar_pos(ui_list_t *list)
+{
+	size_t entries;
+	size_t pglen;
+	size_t sbar_len;
+
+	entries = list_count(&list->entries);
+	pglen = ui_list_page_size(list);
+	sbar_len = ui_scrollbar_move_length(list->scrollbar);
+
+	if (entries > pglen)
+		return sbar_len * list->page_idx / (entries - pglen);
+	else
+		return 0;
+}
+
+/** Update UI list scrollbar position.
+ *
+ * @param list UI list
+ */
+void ui_list_scrollbar_update(ui_list_t *list)
+{
+	ui_scrollbar_set_pos(list->scrollbar,
+	    ui_list_scrollbar_pos(list));
+}
+
+/** Determine if UI list is active.
+ *
+ * @param list UI list
+ * @return @c true iff UI list is active
+ */
+bool ui_list_is_active(ui_list_t *list)
+{
+	return list->active;
+}
+
+/** Activate UI list.
+ *
+ * @param list UI list
+ *
+ * @return EOK on success or an error code
+ */
+errno_t ui_list_activate(ui_list_t *list)
+{
+	list->active = true;
+	(void) ui_list_paint(list);
+	return EOK;
+}
+
+/** Deactivate UI list.
+ *
+ * @param list UI list
+ */
+void ui_list_deactivate(ui_list_t *list)
+{
+	list->active = false;
+	(void) ui_list_paint(list);
+}
+
+/** Initialize UI list entry attributes.
+ *
+ * @param attr Attributes
+ */
+void ui_list_entry_attr_init(ui_list_entry_attr_t *attr)
+{
+	memset(attr, 0, sizeof(*attr));
+}
+
+/** Destroy UI list control.
+ *
+ * @param arg Argument (ui_list_t *)
+ */
+void ui_list_ctl_destroy(void *arg)
+{
+	ui_list_t *list = (ui_list_t *) arg;
+
+	ui_list_destroy(list);
+}
+
+/** Paint UI list control.
+ *
+ * @param arg Argument (ui_list_t *)
+ * @return EOK on success or an error code
+ */
+errno_t ui_list_ctl_paint(void *arg)
+{
+	ui_list_t *list = (ui_list_t *) arg;
+
+	return ui_list_paint(list);
+}
+
+/** Handle UI list control keyboard event.
+ *
+ * @param arg Argument (ui_list_t *)
+ * @param kbd_event Keyboard event
+ * @return @c ui_claimed iff the event is claimed
+ */
+ui_evclaim_t ui_list_ctl_kbd_event(void *arg, kbd_event_t *event)
+{
+	ui_list_t *list = (ui_list_t *) arg;
+
+	return ui_list_kbd_event(list, event);
+}
+
+/** Handle UI list control position event.
+ *
+ * @param arg Argument (ui_list_t *)
+ * @param pos_event Position event
+ * @return @c ui_claimed iff the event is claimed
+ */
+ui_evclaim_t ui_list_ctl_pos_event(void *arg, pos_event_t *event)
+{
+	ui_list_t *list = (ui_list_t *) arg;
+
+	return ui_list_pos_event(list, event);
+}
+
+/** Append new UI list entry.
+ *
+ * @param list UI list
+ * @param attr Entry attributes
+ * @param rentry Place to store pointer to new entry or @c NULL if not
+ *               interested
+ * @return EOK on success or an error code
+ */
+errno_t ui_list_entry_append(ui_list_t *list, ui_list_entry_attr_t *attr,
+    ui_list_entry_t **rentry)
+{
+	ui_list_entry_t *entry;
+
+	entry = calloc(1, sizeof(ui_list_entry_t));
+	if (entry == NULL)
+		return ENOMEM;
+
+	entry->list = list;
+	entry->caption = str_dup(attr->caption);
+	if (entry->caption == NULL) {
+		free(entry);
+		return ENOMEM;
+	}
+
+	entry->arg = attr->arg;
+	entry->color = attr->color;
+	entry->bgcolor = attr->bgcolor;
+	link_initialize(&entry->lentries);
+	list_append(&entry->lentries, &list->entries);
+
+	if (list->entries_cnt == 0) {
+		/* Adding first entry - need to set the cursor */
+		list->cursor = entry;
+		list->cursor_idx = 0;
+		list->page = entry;
+		list->page_idx = 0;
+	}
+
+	++list->entries_cnt;
+
+	if (rentry != NULL)
+		*rentry = entry;
+	return EOK;
+}
+
+/** Delete UI list entry.
+ *
+ * @param entry UI list entry
+ */
+void ui_list_entry_delete(ui_list_entry_t *entry)
+{
+	if (entry->list->cursor == entry)
+		entry->list->cursor = NULL;
+	if (entry->list->page == entry)
+		entry->list->page = NULL;
+
+	list_remove(&entry->lentries);
+	--entry->list->entries_cnt;
+	free((char *) entry->caption);
+	free(entry);
+}
+
+/** Get entry argument.
+ *
+ * @param entry UI list entry
+ * @return User argument
+ */
+void *ui_list_entry_get_arg(ui_list_entry_t *entry)
+{
+	return entry->arg;
+}
+
+/** Clear UI list entry list.
+ *
+ * @param list UI list
+ */
+void ui_list_clear_entries(ui_list_t *list)
+{
+	ui_list_entry_t *entry;
+
+	entry = ui_list_first(list);
+	while (entry != NULL) {
+		ui_list_entry_delete(entry);
+		entry = ui_list_first(list);
+	}
+}
+
+/** Get number of UI list entries.
+ *
+ * @param list UI list
+ * @return Number of entries
+ */
+size_t ui_list_entries_cnt(ui_list_t *list)
+{
+	return list->entries_cnt;
+}
+
+/** Return first UI list entry.
+ *
+ * @param list UI list
+ * @return First UI list entry or @c NULL if there are no entries
+ */
+ui_list_entry_t *ui_list_first(ui_list_t *list)
+{
+	link_t *link;
+
+	link = list_first(&list->entries);
+	if (link == NULL)
+		return NULL;
+
+	return list_get_instance(link, ui_list_entry_t, lentries);
+}
+
+/** Return last UI list entry.
+ *
+ * @param list UI list
+ * @return Last UI list entry or @c NULL if there are no entries
+ */
+ui_list_entry_t *ui_list_last(ui_list_t *list)
+{
+	link_t *link;
+
+	link = list_last(&list->entries);
+	if (link == NULL)
+		return NULL;
+
+	return list_get_instance(link, ui_list_entry_t, lentries);
+}
+
+/** Return next UI list entry.
+ *
+ * @param cur Current entry
+ * @return Next entry or @c NULL if @a cur is the last entry
+ */
+ui_list_entry_t *ui_list_next(ui_list_entry_t *cur)
+{
+	link_t *link;
+
+	link = list_next(&cur->lentries, &cur->list->entries);
+	if (link == NULL)
+		return NULL;
+
+	return list_get_instance(link, ui_list_entry_t, lentries);
+}
+
+/** Return previous UI list entry.
+ *
+ * @param cur Current entry
+ * @return Previous entry or @c NULL if @a cur is the first entry
+ */
+ui_list_entry_t *ui_list_prev(ui_list_entry_t *cur)
+{
+	link_t *link;
+
+	link = list_prev(&cur->lentries, &cur->list->entries);
+	if (link == NULL)
+		return NULL;
+
+	return list_get_instance(link, ui_list_entry_t, lentries);
+}
+
+/** Find the n-th entry of the current UI list page.
+ *
+ * @param list UI list
+ * @param n Which entry to get (starting from 0)
+ * @param ridx Place to store index (within listing) of the entry
+ * @return n-th entry of the page
+ */
+ui_list_entry_t *ui_list_page_nth_entry(ui_list_t *list,
+    size_t n, size_t *ridx)
+{
+	ui_list_entry_t *entry;
+	size_t i;
+	size_t idx;
+
+	assert(n <= ui_list_page_size(list));
+
+	entry = list->page;
+	if (entry == NULL)
+		return NULL;
+
+	idx = list->page_idx;
+	for (i = 0; i < n; i++) {
+		entry = ui_list_next(entry);
+		if (entry == NULL)
+			return NULL;
+
+		++idx;
+	}
+
+	*ridx = idx;
+	return entry;
+}
+
+/** Get entry under cursor.
+ *
+ * @param list UI list
+ * @return Current cursor
+ */
+ui_list_entry_t *ui_list_get_cursor(ui_list_t *list)
+{
+	return list->cursor;
+}
+
+/** Move cursor to a new position, possibly scrolling.
+ *
+ * @param list UI list
+ * @param entry New entry under cursor
+ * @param entry_idx Index of new entry under cursor
+ */
+void ui_list_cursor_move(ui_list_t *list,
+    ui_list_entry_t *entry, size_t entry_idx)
+{
+	gfx_context_t *gc = ui_window_get_gc(list->window);
+	ui_list_entry_t *old_cursor;
+	size_t old_idx;
+	size_t rows;
+	ui_list_entry_t *e;
+	size_t i;
+
+	rows = ui_list_page_size(list);
+
+	old_cursor = list->cursor;
+	old_idx = list->cursor_idx;
+
+	list->cursor = entry;
+	list->cursor_idx = entry_idx;
+
+	if (entry_idx >= list->page_idx &&
+	    entry_idx < list->page_idx + rows) {
+		/*
+		 * If cursor is still on the current page, we're not
+		 * scrolling. Just unpaint old cursor and paint new
+		 * cursor.
+		 */
+		ui_list_entry_paint(old_cursor, old_idx);
+		ui_list_entry_paint(list->cursor, list->cursor_idx);
+
+		(void) gfx_update(gc);
+	} else {
+		/*
+		 * Need to scroll and update all rows.
+		 */
+
+		/* Scrolling up */
+		if (entry_idx < list->page_idx) {
+			list->page = entry;
+			list->page_idx = entry_idx;
+		}
+
+		/* Scrolling down */
+		if (entry_idx >= list->page_idx + rows) {
+			if (entry_idx >= rows) {
+				list->page_idx = entry_idx - rows + 1;
+				/* Find first page entry (go back rows - 1) */
+				e = entry;
+				for (i = 0; i < rows - 1; i++) {
+					e = ui_list_prev(e);
+				}
+
+				/* Should be valid */
+				assert(e != NULL);
+				list->page = e;
+			} else {
+				list->page = ui_list_first(list);
+				list->page_idx = 0;
+			}
+		}
+
+		ui_list_scrollbar_update(list);
+		(void) ui_list_paint(list);
+	}
+}
+
+/** Move cursor one entry up.
+ *
+ * @param list UI list
+ */
+void ui_list_cursor_up(ui_list_t *list)
+{
+	ui_list_entry_t *prev;
+	size_t prev_idx;
+
+	prev = ui_list_prev(list->cursor);
+	prev_idx = list->cursor_idx - 1;
+	if (prev != NULL)
+		ui_list_cursor_move(list, prev, prev_idx);
+}
+
+/** Move cursor one entry down.
+ *
+ * @param list UI list
+ */
+void ui_list_cursor_down(ui_list_t *list)
+{
+	ui_list_entry_t *next;
+	size_t next_idx;
+
+	next = ui_list_next(list->cursor);
+	next_idx = list->cursor_idx + 1;
+	if (next != NULL)
+		ui_list_cursor_move(list, next, next_idx);
+}
+
+/** Move cursor to top.
+ *
+ * @param list UI list
+ */
+void ui_list_cursor_top(ui_list_t *list)
+{
+	ui_list_cursor_move(list, ui_list_first(list), 0);
+}
+
+/** Move cursor to bottom.
+ *
+ * @param list UI list
+ */
+void ui_list_cursor_bottom(ui_list_t *list)
+{
+	ui_list_cursor_move(list, ui_list_last(list),
+	    list->entries_cnt - 1);
+}
+
+/** Move cursor one page up.
+ *
+ * @param list UI list
+ */
+void ui_list_page_up(ui_list_t *list)
+{
+	gfx_context_t *gc = ui_window_get_gc(list->window);
+	ui_list_entry_t *old_page;
+	ui_list_entry_t *old_cursor;
+	size_t old_idx;
+	size_t rows;
+	ui_list_entry_t *entry;
+	size_t i;
+
+	rows = ui_list_page_size(list);
+
+	old_page = list->page;
+	old_cursor = list->cursor;
+	old_idx = list->cursor_idx;
+
+	/* Move page by rows entries up (if possible) */
+	for (i = 0; i < rows; i++) {
+		entry = ui_list_prev(list->page);
+		if (entry != NULL) {
+			list->page = entry;
+			--list->page_idx;
+		}
+	}
+
+	/* Move cursor by rows entries up (if possible) */
+
+	for (i = 0; i < rows; i++) {
+		entry = ui_list_prev(list->cursor);
+		if (entry != NULL) {
+			list->cursor = entry;
+			--list->cursor_idx;
+		}
+	}
+
+	if (list->page != old_page) {
+		/* We have scrolled. Need to repaint all entries */
+		ui_list_scrollbar_update(list);
+		(void) ui_list_paint(list);
+	} else if (list->cursor != old_cursor) {
+		/* No scrolling, but cursor has moved */
+		ui_list_entry_paint(old_cursor, old_idx);
+		ui_list_entry_paint(list->cursor, list->cursor_idx);
+
+		(void) gfx_update(gc);
+	}
+}
+
+/** Move cursor one page down.
+ *
+ * @param list UI list
+ */
+void ui_list_page_down(ui_list_t *list)
+{
+	gfx_context_t *gc = ui_window_get_gc(list->window);
+	ui_list_entry_t *old_page;
+	ui_list_entry_t *old_cursor;
+	size_t old_idx;
+	size_t max_idx;
+	size_t rows;
+	ui_list_entry_t *entry;
+	size_t i;
+
+	rows = ui_list_page_size(list);
+
+	old_page = list->page;
+	old_cursor = list->cursor;
+	old_idx = list->cursor_idx;
+
+	if (list->entries_cnt > rows)
+		max_idx = list->entries_cnt - rows;
+	else
+		max_idx = 0;
+
+	/* Move page by rows entries down (if possible) */
+	for (i = 0; i < rows; i++) {
+		entry = ui_list_next(list->page);
+		/* Do not scroll that results in a short page */
+		if (entry != NULL && list->page_idx < max_idx) {
+			list->page = entry;
+			++list->page_idx;
+		}
+	}
+
+	/* Move cursor by rows entries down (if possible) */
+
+	for (i = 0; i < rows; i++) {
+		entry = ui_list_next(list->cursor);
+		if (entry != NULL) {
+			list->cursor = entry;
+			++list->cursor_idx;
+		}
+	}
+
+	if (list->page != old_page) {
+		/* We have scrolled. Need to repaint all entries */
+		ui_list_scrollbar_update(list);
+		(void) ui_list_paint(list);
+	} else if (list->cursor != old_cursor) {
+		/* No scrolling, but cursor has moved */
+		ui_list_entry_paint(old_cursor, old_idx);
+		ui_list_entry_paint(list->cursor, list->cursor_idx);
+
+		(void) gfx_update(gc);
+	}
+}
+
+/** Scroll one entry up.
+ *
+ * @param list UI list
+ */
+void ui_list_scroll_up(ui_list_t *list)
+{
+	ui_list_entry_t *prev;
+
+	prev = ui_list_prev(list->page);
+	if (prev == NULL)
+		return;
+
+	list->page = prev;
+	assert(list->page_idx > 0);
+	--list->page_idx;
+
+	ui_list_scrollbar_update(list);
+	(void) ui_list_paint(list);
+}
+
+/** Scroll one entry down.
+ *
+ * @param list UI list
+ */
+void ui_list_scroll_down(ui_list_t *list)
+{
+	ui_list_entry_t *next;
+	ui_list_entry_t *pgend;
+	size_t i;
+	size_t rows;
+
+	next = ui_list_next(list->page);
+	if (next == NULL)
+		return;
+
+	rows = ui_list_page_size(list);
+
+	/* Find last page entry */
+	pgend = list->page;
+	for (i = 0; i < rows && pgend != NULL; i++) {
+		pgend = ui_list_next(pgend);
+	}
+
+	/* Scroll down by one entry, if the page remains full */
+	if (pgend != NULL) {
+		list->page = next;
+		++list->page_idx;
+	}
+
+	ui_list_scrollbar_update(list);
+	(void) ui_list_paint(list);
+}
+
+/** Scroll one page up.
+ *
+ * @param list UI list
+ */
+void ui_list_scroll_page_up(ui_list_t *list)
+{
+	ui_list_entry_t *prev;
+	size_t i;
+	size_t rows;
+
+	prev = ui_list_prev(list->page);
+	if (prev == NULL)
+		return;
+
+	rows = ui_list_page_size(list);
+
+	for (i = 0; i < rows && prev != NULL; i++) {
+		list->page = prev;
+		assert(list->page_idx > 0);
+		--list->page_idx;
+		prev = ui_list_prev(prev);
+	}
+
+	ui_list_scrollbar_update(list);
+	(void) ui_list_paint(list);
+}
+
+/** Scroll one page down.
+ *
+ * @param list UI list
+ */
+void ui_list_scroll_page_down(ui_list_t *list)
+{
+	ui_list_entry_t *next;
+	ui_list_entry_t *pgend;
+	size_t i;
+	size_t rows;
+
+	next = ui_list_next(list->page);
+	if (next == NULL)
+		return;
+
+	rows = ui_list_page_size(list);
+
+	/* Find last page entry */
+	pgend = list->page;
+	for (i = 0; i < rows && pgend != NULL; i++) {
+		pgend = ui_list_next(pgend);
+	}
+
+	/* Scroll by up to 'rows' entries, keeping the page full */
+	for (i = 0; i < rows && pgend != NULL; i++) {
+		list->page = next;
+		++list->page_idx;
+		next = ui_list_next(next);
+		pgend = ui_list_next(pgend);
+	}
+
+	ui_list_scrollbar_update(list);
+	(void) ui_list_paint(list);
+}
+
+/** Scroll to a specific entry
+ *
+ * @param list UI list
+ * @param page_idx New index of first entry on the page
+ */
+void ui_list_scroll_pos(ui_list_t *list, size_t page_idx)
+{
+	ui_list_entry_t *entry;
+	size_t i;
+
+	entry = ui_list_first(list);
+	for (i = 0; i < page_idx; i++) {
+		entry = ui_list_next(entry);
+		assert(entry != NULL);
+	}
+
+	list->page = entry;
+	list->page_idx = page_idx;
+
+	(void) ui_list_paint(list);
+}
+
+/** Request UI list activation.
+ *
+ * Call back to request UI list activation.
+ *
+ * @param list UI list
+ */
+void ui_list_activate_req(ui_list_t *list)
+{
+	if (list->cb != NULL && list->cb->activate_req != NULL) {
+		list->cb->activate_req(list, list->cb_arg);
+	} else {
+		/*
+		 * If there is no callback for activation request,
+		 * just activate the list.
+		 */
+		ui_list_activate(list);
+	}
+}
+
+/** Sort list entries.
+ *
+ * @param list UI list
+ * @return EOK on success, ENOMEM if out of memory
+ */
+errno_t ui_list_sort(ui_list_t *list)
+{
+	ui_list_entry_t **emap;
+	ui_list_entry_t *entry;
+	size_t i;
+
+	/* Create an array to hold pointer to each entry */
+	emap = calloc(list->entries_cnt, sizeof(ui_list_entry_t *));
+	if (emap == NULL)
+		return ENOMEM;
+
+	/* Write entry pointers to array */
+	entry = ui_list_first(list);
+	i = 0;
+	while (entry != NULL) {
+		assert(i < list->entries_cnt);
+		emap[i++] = entry;
+		entry = ui_list_next(entry);
+	}
+
+	/* Sort the array of pointers */
+	qsort(emap, list->entries_cnt, sizeof(ui_list_entry_t *),
+	    ui_list_entry_ptr_cmp);
+
+	/* Unlink entries from entry list */
+	entry = ui_list_first(list);
+	while (entry != NULL) {
+		list_remove(&entry->lentries);
+		entry = ui_list_first(list);
+	}
+
+	/* Add entries back to entry list sorted */
+	for (i = 0; i < list->entries_cnt; i++)
+		list_append(&emap[i]->lentries, &list->entries);
+
+	free(emap);
+
+	list->page = ui_list_first(list);
+	list->page_idx = 0;
+	list->cursor = ui_list_first(list);
+	list->cursor_idx = 0;
+	return EOK;
+}
+
+/** Determine list entry index.
+ *
+ * @param entry List entry
+ * @return List entry index
+ */
+size_t ui_list_entry_get_idx(ui_list_entry_t *entry)
+{
+	ui_list_entry_t *ep;
+	size_t idx;
+
+	idx = 0;
+	ep = ui_list_prev(entry);
+	while (ep != NULL) {
+		++idx;
+		ep = ui_list_prev(ep);
+	}
+
+	return idx;
+}
+
+/** Center list cursor on entry.
+ *
+ * @param list UI list
+ * @param entry Entry
+ */
+void ui_list_cursor_center(ui_list_t *list, ui_list_entry_t *entry)
+{
+	ui_list_entry_t *prev;
+	size_t idx;
+	size_t max_idx;
+	size_t pg_size;
+	size_t i;
+
+	idx = ui_list_entry_get_idx(entry);
+	list->cursor = entry;
+	list->cursor_idx = idx;
+
+	/* Move page so that cursor is in the center */
+	list->page = list->cursor;
+	list->page_idx = list->cursor_idx;
+
+	pg_size = ui_list_page_size(list);
+
+	for (i = 0; i < pg_size / 2; i++) {
+		prev = ui_list_prev(list->page);
+		if (prev == NULL)
+			break;
+
+		list->page = prev;
+		--list->page_idx;
+	}
+
+	/* Make sure page is not beyond the end if possible */
+	if (list->entries_cnt > pg_size)
+		max_idx = list->entries_cnt - pg_size;
+	else
+		max_idx = 0;
+
+	while (list->page_idx > 0 && list->page_idx > max_idx) {
+		prev = ui_list_prev(list->page);
+		if (prev == NULL)
+			break;
+
+		list->page = prev;
+		--list->page_idx;
+	}
+}
+
+/** Call back when an entry is selected.
+ *
+ * @param entry UI list entry
+ * @param fname File name
+ */
+void ui_list_selected(ui_list_entry_t *entry)
+{
+	if (entry->list->cb != NULL && entry->list->cb->selected != NULL)
+		entry->list->cb->selected(entry, entry->arg);
+}
+
+/** UI list scrollbar up button pressed.
+ *
+ * @param scrollbar Scrollbar
+ * @param arg Argument (ui_list_t *)
+ */
+static void ui_list_scrollbar_up(ui_scrollbar_t *scrollbar, void *arg)
+{
+	ui_list_t *list = (ui_list_t *)arg;
+	ui_list_scroll_up(list);
+}
+
+/** UI list scrollbar down button pressed.
+ *
+ * @param scrollbar Scrollbar
+ * @param arg Argument (ui_list_t *)
+ */
+static void ui_list_scrollbar_down(ui_scrollbar_t *scrollbar, void *arg)
+{
+	ui_list_t *list = (ui_list_t *)arg;
+	ui_list_scroll_down(list);
+}
+
+/** UI list scrollbar page up pressed.
+ *
+ * @param scrollbar Scrollbar
+ * @param arg Argument (ui_list_t *)
+ */
+static void ui_list_scrollbar_page_up(ui_scrollbar_t *scrollbar, void *arg)
+{
+	ui_list_t *list = (ui_list_t *)arg;
+	ui_list_scroll_page_up(list);
+}
+
+/** UI list scrollbar page down pressed.
+ *
+ * @param scrollbar Scrollbar
+ * @param arg Argument (ui_list_t *)
+ */
+static void ui_list_scrollbar_page_down(ui_scrollbar_t *scrollbar,
+    void *arg)
+{
+	ui_list_t *list = (ui_list_t *)arg;
+	ui_list_scroll_page_down(list);
+}
+
+/** UI list scrollbar moved.
+ *
+ * @param scrollbar Scrollbar
+ * @param arg Argument (ui_list_t *)
+ */
+static void ui_list_scrollbar_moved(ui_scrollbar_t *scrollbar, void *arg,
+    gfx_coord_t pos)
+{
+	ui_list_t *list = (ui_list_t *)arg;
+	size_t entries;
+	size_t pglen;
+	size_t sbar_len;
+	size_t pgstart;
+
+	entries = list_count(&list->entries);
+	pglen = ui_list_page_size(list);
+	sbar_len = ui_scrollbar_move_length(list->scrollbar);
+
+	if (entries > pglen)
+		pgstart = (entries - pglen) * pos / (sbar_len - 1);
+	else
+		pgstart = 0;
+
+	ui_list_scroll_pos(list, pgstart);
+}
+
+/** Compare two list entries indirectly referenced by pointers.
+ *
+ * @param pa Pointer to pointer to first entry
+ * @param pb Pointer to pointer to second entry
+ * @return <0, =0, >=0 if pa < b, pa == pb, pa > pb, resp.
+ */
+int ui_list_entry_ptr_cmp(const void *pa, const void *pb)
+{
+	ui_list_entry_t *a = *(ui_list_entry_t **)pa;
+	ui_list_entry_t *b = *(ui_list_entry_t **)pb;
+
+	return a->list->cb->compare(a, b);
+}
+
+/** @}
+ */
