changeset 181:867b60e6258a

ui: add gridmenu object, closes #2511 @4h
author David Demelier <markand@malikania.fr>
date Tue, 27 Oct 2020 16:18:21 +0100
parents 43aec6678cad
children f6497ec74b49
files examples/CMakeLists.txt examples/example-gridmenu.c libui/CMakeLists.txt libui/ui/gridmenu.c libui/ui/gridmenu.h
diffstat 5 files changed, 601 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- 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
--- /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 <markand@malikania.fr>
+ *
+ * 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 <core/clock.h>
+#include <core/core.h>
+#include <core/event.h>
+#include <core/painter.h>
+#include <core/panic.h>
+#include <core/sys.h>
+#include <core/trace.h>
+#include <core/util.h>
+#include <core/window.h>
+
+#include <ui/align.h>
+#include <ui/gridmenu.h>
+#include <ui/label.h>
+#include <ui/theme.h>
+#include <ui/ui.h>
+
+#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();
+}
--- 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
--- /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 <markand@malikania.fr>
+ *
+ * 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 <assert.h>
+#include <string.h>
+
+#include <core/event.h>
+#include <core/maths.h>
+#include <core/painter.h>
+#include <core/panic.h>
+#include <core/texture.h>
+#include <core/trace.h>
+
+#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(&gtex->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));
+}
--- /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 <markand@malikania.fr>
+ *
+ * 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 <stddef.h>
+
+#include <core/texture.h>
+
+#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 */