# HG changeset patch # User David Demelier # Date 1603811901 -3600 # Node ID 867b60e6258aac4c9521a7351fa563d0db758658 # Parent 43aec6678cad2bceb51bda8b59e90a4daf7352dd ui: add gridmenu object, closes #2511 @4h diff -r 43aec6678cad -r 867b60e6258a examples/CMakeLists.txt --- a/examples/CMakeLists.txt Tue Oct 27 14:07:18 2020 +0100 +++ b/examples/CMakeLists.txt Tue Oct 27 16:18:21 2020 +0100 @@ -91,6 +91,13 @@ ) molko_define_executable( + TARGET example-gridmenu + SOURCES example-gridmenu.c + FOLDER examples + LIBRARIES libui +) + +molko_define_executable( TARGET example-trace SOURCES example-trace.c FOLDER examples diff -r 43aec6678cad -r 867b60e6258a examples/example-gridmenu.c --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/examples/example-gridmenu.c Tue Oct 27 16:18:21 2020 +0100 @@ -0,0 +1,126 @@ +/* + * example-gridmenu.c -- show how to use grid menu + * + * Copyright (c) 2020 David Demelier + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#define W (1280) +#define H (720) + +static void +init(void) +{ + if (!core_init() || !ui_init()) + panic(); + if (!window_open("Example - Grid menu", W, H)) + panic(); +} + +static void +quit(void) +{ + window_finish(); + ui_finish(); + core_finish(); +} + +static void +run(void) +{ + struct clock clock = {0}; + struct gridmenu menu = { + .menu = { + "Feu mineur", + "Feu majeur", + "Feu septième", + "Glace mineure", + "Glace majeure", + "Glace septième", + "Foudre mineure", + "Foudre majeure", + "Foudre septième", + "Choc mineur", + "Choc majeur", + "Choc septième", + }, + .w = 300, + .h = 100, + .nrows = 3, + .ncols = 2 + }; + + clock_start(&clock); + align(ALIGN_CENTER, &menu.x, &menu.y, menu.w, menu.h, 0, 0, W, H); + + /* Need to repaint at least once. */ + gridmenu_repaint(&menu); + + for (;;) { + union event ev; + unsigned int elapsed = clock_elapsed(&clock); + + clock_start(&clock); + + while (event_poll(&ev)) { + switch (ev.type) { + case EVENT_QUIT: + return; + default: + gridmenu_handle(&menu, &ev); + break; + } + } + + if (menu.state == GRIDMENU_STATE_SELECTED) { + tracef("selected index: %u", (unsigned int)menu.selected); + gridmenu_reset(&menu); + } + + painter_set_color(0x4f8fbaff); + painter_clear(); + gridmenu_draw(&menu); + painter_present(); + + if ((elapsed = clock_elapsed(&clock)) < 20) + delay(20 - elapsed); + } +} + +int +main(int argc, char **argv) +{ + (void)argc; + (void)argv; + + init(); + run(); + quit(); +} diff -r 43aec6678cad -r 867b60e6258a libui/CMakeLists.txt --- a/libui/CMakeLists.txt Tue Oct 27 14:07:18 2020 +0100 +++ b/libui/CMakeLists.txt Tue Oct 27 16:18:21 2020 +0100 @@ -36,6 +36,8 @@ ${libui_SOURCE_DIR}/ui/debug.h ${libui_SOURCE_DIR}/ui/frame.c ${libui_SOURCE_DIR}/ui/frame.h + ${libui_SOURCE_DIR}/ui/gridmenu.c + ${libui_SOURCE_DIR}/ui/gridmenu.h ${libui_SOURCE_DIR}/ui/label.c ${libui_SOURCE_DIR}/ui/label.h ${libui_SOURCE_DIR}/ui/theme.c diff -r 43aec6678cad -r 867b60e6258a libui/ui/gridmenu.c --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libui/ui/gridmenu.c Tue Oct 27 16:18:21 2020 +0100 @@ -0,0 +1,294 @@ +/* + * gridmenu.c -- GUI grid menu + * + * Copyright (c) 2020 David Demelier + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "frame.h" +#include "label.h" +#include "gridmenu.h" +#include "theme.h" + +#define THEME(m) ((m)->theme ? (m)->theme : theme_default()) + +struct index { + unsigned int row; + unsigned int col; +}; + +static struct index +get_index(const struct gridmenu *menu) +{ + return (struct index) { + .row = menu->selected / menu->ncols, + .col = menu->selected % menu->ncols + }; +} + +static void +positionate(struct gridmenu *menu) +{ + const struct theme *theme = THEME(menu); + struct gridmenu_texture *gtex = &menu->tex; + unsigned int vreq, hreq; + + /* Compute which item has the bigger width to create a spacing. */ + for (size_t i = 0; i < GRIDMENU_ENTRY_MAX; ++i) { + unsigned int lw, lh; + + gtex->labels[i].theme = theme; + gtex->labels[i].flags = LABEL_FLAGS_SHADOW; + + if (!(gtex->labels[i].text = menu->menu[i])) + continue; + + label_query(>ex->labels[i], &lw, &lh); + gtex->eltw = lw > gtex->eltw ? lw : gtex->eltw; + gtex->elth = lh > gtex->elth ? lh : gtex->elth; + } + + vreq = (theme->padding * 2) + (gtex->elth * menu->nrows); + hreq = (theme->padding * 2) + (gtex->eltw * menu->ncols); + + /* Compute spacing between elements. */ + if (hreq > menu->w) { + tracef("gridmenu width is too small: %u < %u", menu->w, vreq); + gtex->spaceh = 1; + } else { + hreq -= theme->padding * 2; + gtex->spaceh = (menu->w - hreq) / menu->ncols; + } + + if (vreq > menu->h) { + tracef("gridmenu height is too small: %u < %u", menu->h, vreq); + gtex->spacev = 1; + } else { + vreq -= theme->padding * 2; + gtex->spacev = (menu->h - vreq) / menu->nrows; + } + + /* This is the whole height. */ + gtex->relh = theme->padding * 2; + gtex->relh += gtex->elth * (GRIDMENU_ENTRY_MAX / menu->ncols); + gtex->relh += gtex->spacev * (GRIDMENU_ENTRY_MAX / menu->ncols); +} + +static void +repaint_frame(struct gridmenu *menu) +{ + const struct frame f = { + .x = 0, + .y = menu->tex.rely, + .w = menu->w, + .h = menu->h, + .theme = menu->theme, + }; + + frame_draw(&f); +} + +static void +repaint_labels(struct gridmenu *menu) +{ + struct theme theme; + unsigned int r = 0, c = 0; + + /* Copy theme to change color if selected. */ + theme_shallow(&theme, THEME(menu)); + + for (size_t i = 0; i < GRIDMENU_ENTRY_MAX; ++i) { + struct label *l = &menu->tex.labels[i]; + + if (i == menu->selected) + theme.colors[THEME_COLOR_NORMAL] = THEME(menu)->colors[THEME_COLOR_SELECTED]; + else + theme.colors[THEME_COLOR_NORMAL] = THEME(menu)->colors[THEME_COLOR_NORMAL]; + + l->theme = &theme; + l->x = theme.padding + (c * menu->tex.eltw) + (c * menu->tex.spaceh); + l->y = theme.padding + (r * menu->tex.elth) + (r * menu->tex.spacev); + + if (++c >= menu->ncols) { + ++r; + c = 0; + } + + if (l->text) + label_draw(l); + } +} + +static void +repaint(struct gridmenu *menu) +{ + struct texture *tex = &menu->tex.texture; + + if (!texture_ok(tex) && !texture_new(tex, menu->w, menu->tex.relh)) + panic(); + + PAINTER_BEGIN(tex); + + painter_clear(); + repaint_frame(menu); + repaint_labels(menu); + + PAINTER_END(); +} + +static void +zoom(struct gridmenu *menu) +{ + struct gridmenu_texture *tex = &menu->tex; + struct label *cur = &tex->labels[menu->selected]; + + /* Readjust relative position. */ + if ((unsigned int)cur->y > tex->rely + menu->h || cur->y < tex->rely) + tex->rely = cur->y - THEME(menu)->padding; +} + +static void +handle_keydown(struct gridmenu *menu, const struct event_key *key) +{ + assert(key->type == EVENT_KEYDOWN); + + const struct index idx = get_index(menu); + const unsigned int save = menu->selected; + + switch (key->key) { + case KEY_UP: + if (idx.row > 0) + menu->selected -= menu->ncols; + break; + case KEY_RIGHT: + if (idx.col + 1U < menu->ncols) + menu->selected += 1; + break; + case KEY_DOWN: + if (idx.row + 1U < GRIDMENU_ENTRY_MAX / menu->ncols) + menu->selected += menu->ncols; + break; + case KEY_LEFT: + if (idx.col > 0) + menu->selected -= 1; + break; + case KEY_ENTER: + menu->state = GRIDMENU_STATE_SELECTED; + break; + default: + break; + } + + if (save != menu->selected) + gridmenu_repaint(menu); +} + +static void +handle_clickdown(struct gridmenu *menu, const struct event_click *click) +{ + assert(click->type == EVENT_CLICKDOWN); + + const unsigned int save = menu->selected; + + for (size_t i = 0; i < GRIDMENU_ENTRY_MAX; ++i) { + const struct label *l = &menu->tex.labels[i]; + const int x = menu->x + l->x; + const int y = menu->y + l->y - menu->tex.rely; + const unsigned int w = menu->tex.eltw; + const unsigned int h = menu->tex.elth; + + if (maths_is_boxed(x, y, w, h, click->x, click->y)) { + menu->selected = i; + break; + } + } + + if (save != menu->selected) + gridmenu_repaint(menu); +} + +void +gridmenu_reset(struct gridmenu *menu) +{ + assert(menu); + + menu->state = GRIDMENU_STATE_NONE; +} + +void +gridmenu_repaint(struct gridmenu *menu) +{ + assert(menu); + assert(GRIDMENU_ENTRY_MAX % menu->ncols == 0); + + /* Re-compute positions. */ + positionate(menu); + + /* Zoom to the appropriate y-relative. */ + zoom(menu); + + /* Redraw. */ + repaint(menu); +} + +void +gridmenu_handle(struct gridmenu *menu, const union event *ev) +{ + assert(menu); + assert(ev); + + switch (ev->type) { + case EVENT_KEYDOWN: + handle_keydown(menu, &ev->key); + break; + case EVENT_CLICKDOWN: + handle_clickdown(menu, &ev->click); + break; + default: + break; + } +} + +void +gridmenu_draw(const struct gridmenu *menu) +{ + assert(menu); + assert(menu->nrows > 0 && menu->ncols > 0); + assert(texture_ok(&menu->tex.texture)); + + texture_scale(&menu->tex.texture, + 0, menu->tex.rely, menu->w, menu->h, + menu->x, menu->y, menu->w, menu->h, 0.0); +} + +void +gridmenu_finish(struct gridmenu *menu) +{ + assert(menu); + + if (texture_ok(&menu->tex.texture)) + texture_finish(&menu->tex.texture); + + memset(menu, 0, sizeof (*menu)); +} diff -r 43aec6678cad -r 867b60e6258a libui/ui/gridmenu.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libui/ui/gridmenu.h Tue Oct 27 16:18:21 2020 +0100 @@ -0,0 +1,172 @@ +/* + * gridmenu.h -- GUI grid menu + * + * Copyright (c) 2020 David Demelier + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef MOLKO_GRIDMENU_H +#define MOLKO_GRIDMENU_H + +/** + * \file gridmenu.h + * \brief GUI grid menu. + */ + +#include + +#include + +#include "label.h" + +/** + * \brief Maximum number of entries. + */ +#define GRIDMENU_ENTRY_MAX (256) + +struct theme; + +union event; + +/** + * \brief Grid menu state. + */ +enum gridmenu_state { + GRIDMENU_STATE_NONE, /*!< No state yet. */ + GRIDMENU_STATE_SELECTED /*!< An entry has been selected. */ +}; + +/** + * \brief Internal texture representation. + * + * This structure contain several information for rendering the underlying grid + * into the screen. + */ +struct gridmenu_texture { + int rely; /*!< (-) View start in y. */ + unsigned int relh; /*!< (-) Real texture height. */ + unsigned int eltw; /*!< (-) Maximum label width */ + unsigned int elth; /*!< (-) Maximum label height. */ + unsigned int spacev; /*!< (-) Vertical space between labels. */ + unsigned int spaceh; /*!< (-) Horizontal space between labels. */ + struct texture texture; /*!< (*) The texture itself. */ + + /** + * (-) The list of labels. + */ + struct label labels[GRIDMENU_ENTRY_MAX]; +}; + +/** + * \brief Grid menu for selecting items. + * + * This menu offers a grid where user can specify a maximum number of columns to + * show entries. Content is automatically paginated vertically according to the + * current selection and the menu's length. + * + * It uses \ref frame.h and \ref label.h to draw elements so you may change the + * referenced theme if you want a different style. + * + * This module being a bit complex uses internal data for rendering that is + * repainted in case of event (when using \ref gridmenu_handle) but if you do + * modify the menu, you'll have to call \ref gridmenu_repaint yourself and you + * need to call it at least once before drawing. + */ +struct gridmenu { + int x; /*!< (+) Position in x. */ + int y; /*!< (+) Position in y. */ + unsigned int w; /*!< (+) Width. */ + unsigned int h; /*!< (+) Height. */ + enum gridmenu_state state; /*!< (+) Menu state. */ + size_t selected; /*!< (+) User selection. */ + const struct theme *theme; /*!< (+&?) Optional theme to use. */ + + /** + * (+&?) List of entries to show. + */ + const char *menu[GRIDMENU_ENTRY_MAX]; + + /** + * (+) Number of rows allowed per page. + */ + unsigned int nrows; + + /** + * (+) Number of columns allowed per page. + * + * \warning You must make sure to use a number of columns that can fit + * GRIDMENU_ENTRY_MAX, in other terms + * `GRIDMENU_ENTRY_MAX % ncols == 0` + */ + unsigned int ncols; + + /** + * (*) Internal grid menu texture. + */ + struct gridmenu_texture tex; +}; + +/** + * Reset the menu->state flag. + * + * \pre menu != NULL + * \param menu the menu to reset + */ +void +gridmenu_reset(struct gridmenu *menu); + +/** + * Rebuild internal texture for rendering. + * + * \pre menu != NULL + * \pre GRIDMENU_ENTRY_MAX % menu->ncols == 0 + * \param menu the menu to repaint + */ +void +gridmenu_repaint(struct gridmenu *menu); + +/** + * Handle an event in the menu. + * + * Mouse click will test the coordinate of the mouse to check if the pointer is + * on a menu item region but keyboard events aren't so make sure to have user + * "focus" prior to calling this function. + * + * \pre menu != NULL + * \pre ev != NULL + * \param menu the menu to use + * \param ev the event + */ +void +gridmenu_handle(struct gridmenu *menu, const union event *ev); + +/** + * Draw the menu. + * + * \pre menu != NULL && menu->nrows > 0 && menu->ncols > 0 + * \param menu the menu to draw + */ +void +gridmenu_draw(const struct gridmenu *menu); + +/** + * Close internal resources. + * + * \pre menu != NULL + * \param menu the menu to close + */ +void +gridmenu_finish(struct gridmenu *menu); + +#endif /* !MOLKO_GRIDMENU_H */