HelenOS sources

root/uspace/lib/ui/src/list.c

/* [<][>][^][v][top][bottom][index][help] */

DEFINITIONS

This source file includes following definitions.
  1. ui_list_create
  2. ui_list_destroy
  3. ui_list_set_cb
  4. ui_list_get_cb_arg
  5. ui_list_entry_height
  6. ui_list_entry_paint
  7. ui_list_paint
  8. ui_list_kbd_event
  9. ui_list_pos_event
  10. ui_list_ctl
  11. ui_list_set_rect
  12. ui_list_page_size
  13. ui_list_inside_rect
  14. ui_list_scrollbar_rect
  15. ui_list_scrollbar_pos
  16. ui_list_scrollbar_update
  17. ui_list_is_active
  18. ui_list_activate
  19. ui_list_deactivate
  20. ui_list_entry_attr_init
  21. ui_list_ctl_destroy
  22. ui_list_ctl_paint
  23. ui_list_ctl_kbd_event
  24. ui_list_ctl_pos_event
  25. ui_list_entry_append
  26. ui_list_entry_move_up
  27. ui_list_entry_move_down
  28. ui_list_entry_destroy
  29. ui_list_entry_delete
  30. ui_list_entry_get_arg
  31. ui_list_entry_get_list
  32. ui_list_entry_set_caption
  33. ui_list_clear_entries
  34. ui_list_entries_cnt
  35. ui_list_first
  36. ui_list_last
  37. ui_list_next
  38. ui_list_prev
  39. ui_list_page_nth_entry
  40. ui_list_get_cursor
  41. ui_list_set_cursor
  42. ui_list_cursor_move
  43. ui_list_cursor_up
  44. ui_list_cursor_down
  45. ui_list_cursor_top
  46. ui_list_cursor_bottom
  47. ui_list_page_up
  48. ui_list_page_down
  49. ui_list_scroll_up
  50. ui_list_scroll_down
  51. ui_list_scroll_page_up
  52. ui_list_scroll_page_down
  53. ui_list_scroll_pos
  54. ui_list_activate_req
  55. ui_list_sort
  56. ui_list_entry_get_idx
  57. ui_list_cursor_center
  58. ui_list_selected
  59. ui_list_scrollbar_up
  60. ui_list_scrollbar_down
  61. ui_list_scrollbar_page_up
  62. ui_list_scrollbar_page_down
  63. ui_list_scrollbar_moved
  64. ui_list_entry_ptr_cmp

/*
 * Copyright (c) 2024 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 UI list callback argument.
 *
 * @param list UI list
 * @return Callback argument
 */
void *ui_list_get_cb_arg(ui_list_t *list)
{
        return list->cb_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;
}

/** Move UI list entry one position up.
 *
 * @parm entry UI list entry
 */
void ui_list_entry_move_up(ui_list_entry_t *entry)
{
        ui_list_t *list = entry->list;
        ui_list_entry_t *prev;

        prev = ui_list_prev(entry);
        if (prev == NULL) {
                /* Entry is already on first position, nothing to do. */
                return;
        }

        list_remove(&entry->lentries);
        list_insert_before(&entry->lentries, &prev->lentries);

        /* Make sure page stays on the same position/idx as it was before */
        if (list->page == entry) {
                list->page = prev;
        } else if (list->page == prev) {
                list->page = entry;
        }

        /*
         * Return cursor to the same position/idx as it was before,
         * but then move it using ui_list_cursor_move() to the new
         * position (this ensures scrolling when needed).
         */
        if (list->cursor == entry) {
                list->cursor = prev;
                ui_list_cursor_move(list, entry, list->cursor_idx - 1);
        } else if (list->cursor == prev) {
                list->cursor = entry;
                ui_list_cursor_move(list, prev, list->cursor_idx + 1);
        }
}

/** Move UI list entry one position down.
 *
 * @parm entry UI list entry
 */
void ui_list_entry_move_down(ui_list_entry_t *entry)
{
        ui_list_t *list = entry->list;
        ui_list_entry_t *next;

        next = ui_list_next(entry);
        if (next == NULL) {
                /* Entry is already on last position, nothing to do. */
                return;
        }

        list_remove(&entry->lentries);
        list_insert_after(&entry->lentries, &next->lentries);

        /* Make sure page stays on the same position/idx as it was before */
        if (list->page == entry) {
                list->page = next;
        } else if (list->page == next) {
                list->page = entry;
        }

        /*
         * Return cursor to the same position/idx as it was before,
         * but then move it using ui_list_cursor_move() to the new
         * position (this ensures scrolling when needed).
         */
        if (list->cursor == entry) {
                list->cursor = next;
                ui_list_cursor_move(list, entry, list->cursor_idx + 1);
        } else if (list->cursor == next) {
                list->cursor = entry;
                ui_list_cursor_move(list, next, list->cursor_idx - 1);
        }
}

/** Destroy UI list entry.
 *
 * This is the quick way, but does not update cursor or page position.
 *
 * @param entry UI list entry
 */
void ui_list_entry_destroy(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);
}

/** Delete UI list entry.
 *
 * If required, update cursor and page position and repaint.
 *
 * @param entry UI list entry
 */
void ui_list_entry_delete(ui_list_entry_t *entry)
{
        ui_list_t *list = entry->list;

        /* Try to make sure entry does not disappear under cursor or page */
        if (entry->list->cursor == entry)
                ui_list_cursor_up(entry->list);
        if (entry->list->cursor == entry)
                ui_list_cursor_down(entry->list);
        if (entry->list->page == entry)
                ui_list_scroll_up(entry->list);
        if (entry->list->page == entry)
                ui_list_scroll_down(entry->list);

        ui_list_entry_destroy(entry);

        /*
         * But it could still happen if there are not enough entries.
         * In that case just move page and/or cursor to the first
         * entry.
         */
        if (list->page == NULL) {
                list->page = ui_list_first(list);
                list->page_idx = 0;
        } else {
                /*
                 * Entry index might have changed if earlier entry
                 * was deleted.
                 */
                list->page_idx = ui_list_entry_get_idx(list->page);
        }

        if (list->cursor == NULL) {
                list->cursor = ui_list_first(list);
                list->cursor_idx = 0;
        } else {
                /*
                 * Entry index might have changed if earlier entry
                 * was deleted.
                 */
                list->cursor_idx = ui_list_entry_get_idx(list->cursor);
        }
}

/** 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;
}

/** Get containing list.
 *
 * @param entry UI list entry
 * @return Containing list
 */
ui_list_t *ui_list_entry_get_list(ui_list_entry_t *entry)
{
        return entry->list;
}

/** Change list entry caption.
 *
 * @param entry UI list entry
 * @param caption New caption
 *
 * @return EOK on success, ENOMEM if out of memory
 */
errno_t ui_list_entry_set_caption(ui_list_entry_t *entry, const char *caption)
{
        char *dcaption;

        dcaption = str_dup(caption);
        if (dcaption == NULL)
                return ENOMEM;

        free(entry->caption);
        entry->caption = dcaption;

        (void)ui_list_entry_paint(entry, ui_list_entry_get_idx(entry));
        return EOK;
}

/** 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_destroy(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;
}

/** Set new cursor position.
 *
 * O(N) in list size, use with caution.
 *
 * @param list UI list
 * @param entry New cursor position
 */
void ui_list_set_cursor(ui_list_t *list, ui_list_entry_t *entry)
{
        size_t idx;

        idx = ui_list_entry_get_idx(entry);
        ui_list_cursor_move(list, entry, idx);
}

/** 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 + 1 < rows; 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;

        if (list->page == NULL)
                return;

        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;

        if (list->page == NULL)
                return;

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

/** @}
 */

/* [<][>][^][v][top][bottom][index][help] */
HelenOS homepage, sources at GitHub