Nilorea Library
C utilities for networking, threading, graphics
Loading...
Searching...
No Matches
ex_gui_dropmenu.c
Go to the documentation of this file.
1/*
2 * Nilorea Library
3 * Copyright (C) 2005-2026 Castagnier Mickael
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
14 * implied. See the License for the specific language governing
15 * permissions and limitations under the License.
16 *
17 * SPDX-License-Identifier: Apache-2.0
18 */
19
27#define ALLEGRO_UNSTABLE 1
28
29#include <stdio.h>
30#include <stdlib.h>
31#include <string.h>
32#include <unistd.h>
33
34#include "nilorea/n_common.h"
35#include "nilorea/n_log.h"
36#include "nilorea/n_gui.h"
37
38static int log_level = LOG_ERR;
39static int failures = 0;
40static int click_tag = -1;
41static int combo_selected = -1;
42
43static void on_combo(int widget_id, int index, void* user_data) {
44 (void)widget_id;
45 (void)user_data;
46 combo_selected = index;
47}
48
49static int process_args(int argc, char** argv) {
50 int opt;
51 while ((opt = getopt(argc, argv, "V:")) != -1) {
52 switch (opt) {
53 case 'V':
54 if (!strcmp("LOG_NULL", optarg))
56 else if (!strcmp("LOG_NOTICE", optarg))
58 else if (!strcmp("LOG_INFO", optarg))
60 else if (!strcmp("LOG_ERR", optarg))
62 else if (!strcmp("LOG_DEBUG", optarg))
64 else
65 return -1;
66 break;
67 default:
68 return -1;
69 }
70 }
71 return 0;
72}
73
74static void expect_true(const char* label, int cond) {
75 if (!cond) {
76 n_log(LOG_ERR, "%s: condition false", label);
77 failures++;
78 }
79}
80
81static void on_entry(int widget_id, int entry_index, int tag, void* user_data) {
82 (void)widget_id;
83 (void)entry_index;
84 (void)user_data;
85 click_tag = tag;
86}
87
88/* the dropmenu label lives in N_GUI_DROPMENU_DATA (a public struct), so a headless
89 test can read it back through the widget accessor to check the label setter */
90static const char* dropmenu_label(N_GUI_CTX* gui, int id) {
92 if (!w || w->type != N_GUI_TYPE_DROPMENU || !w->data)
93 return NULL;
94 return ((N_GUI_DROPMENU_DATA*)w->data)->label;
95}
96
97int main(int argc, char** argv) {
99 if (process_args(argc, argv) != 0) {
100 fprintf(stderr, "Usage: %s [-V LOG_LEVEL]\n", argv[0]);
101 return 2;
102 }
104
105 if (!al_init() || !al_init_font_addon()) {
106 n_log(LOG_ERR, "allegro init failed");
107 return 1;
108 }
109 ALLEGRO_FONT* font = al_create_builtin_font();
110 if (!font) {
111 n_log(LOG_ERR, "al_create_builtin_font failed");
112 return 1;
113 }
114 N_GUI_CTX* gui = n_gui_new_ctx(font);
115 if (!gui) {
116 n_log(LOG_ERR, "n_gui_new_ctx failed");
117 al_destroy_font(font);
118 return 1;
119 }
120 n_gui_set_display_size(gui, 800.0f, 600.0f);
121
122 int win = n_gui_add_window(gui, "Panel", 0.0f, 0.0f, 800.0f, 600.0f);
123 expect_true("window created", win >= 0);
124 /* frameless so widget client coordinates line up with screen coordinates,
125 which the mouse-event consumption checks below depend on */
127
128 int dm = n_gui_add_dropmenu(gui, win, "Capture", 0.0f, 0.0f, 120.0f, 28.0f, NULL, NULL);
129 expect_true("dropmenu created", dm >= 0);
130 expect_true("initial label is Capture", dropmenu_label(gui, dm) && strcmp(dropmenu_label(gui, dm), "Capture") == 0);
131 expect_true("new dropmenu is empty", n_gui_dropmenu_get_count(gui, dm) == 0);
132
133 /* entries carry a tag delivered to the click callback */
134 expect_true("add entry 0", n_gui_dropmenu_add_entry(gui, dm, "Proxy", 11, on_entry, NULL) == 0);
135 expect_true("add entry 1", n_gui_dropmenu_add_entry(gui, dm, "Browser", 22, on_entry, NULL) == 1);
136 expect_true("count is 2", n_gui_dropmenu_get_count(gui, dm) == 2);
137
138 /* the label setter (used to reuse one widget for different categories) */
139 n_gui_dropmenu_set_label(gui, dm, "Auth & session");
140 expect_true("label updated", dropmenu_label(gui, dm) && strcmp(dropmenu_label(gui, dm), "Auth & session") == 0);
141 n_gui_dropmenu_set_label(gui, dm, NULL);
142 expect_true("label cleared", dropmenu_label(gui, dm) && dropmenu_label(gui, dm)[0] == '\0');
143
144 /* re-labelling and re-filling does not disturb the entry count */
145 n_gui_dropmenu_set_label(gui, dm, "Recon");
146 expect_true("count still 2 after relabel", n_gui_dropmenu_get_count(gui, dm) == 2);
147
148 /* set_label on a non-dropmenu widget is a safe no-op */
149 int btn = n_gui_add_button(gui, win, "Btn", 0.0f, 40.0f, 60.0f, 24.0f, N_GUI_SHAPE_ROUNDED, NULL, NULL);
150 n_gui_dropmenu_set_label(gui, btn, "ignored");
151 expect_true("set_label ignores non-dropmenu", dropmenu_label(gui, btn) == NULL);
152
153 /* the open panel widens beyond a narrow trigger button so long entries are not
154 truncated (a category button shows a short label but its entries can be long) */
155 {
156 int narrow = n_gui_add_dropmenu(gui, win, "Cap", 0.0f, 80.0f, 40.0f, 24.0f, NULL, NULL);
157 float long_w;
158 expect_true("narrow dropmenu created", narrow >= 0);
159 /* a non-dropmenu widget has no panel width */
160 expect_true("panel width 0 for non-dropmenu", n_gui_dropmenu_panel_width(gui, btn) == 0.0f);
161 /* with no entries the panel is exactly the button width */
162 expect_true("empty panel width equals button width", n_gui_dropmenu_panel_width(gui, narrow) == 40.0f);
163 n_gui_dropmenu_add_entry(gui, narrow, "DOM Invader proxy", 1, on_entry, NULL);
164 n_gui_dropmenu_add_entry(gui, narrow, "X", 2, on_entry, NULL);
165 long_w = (float)al_get_text_width(font, "DOM Invader proxy");
166 /* the panel grows past the 40px button to fit the longest entry text */
167 expect_true("panel widens past the narrow button", n_gui_dropmenu_panel_width(gui, narrow) > 40.0f);
168 expect_true("panel fits the widest entry", n_gui_dropmenu_panel_width(gui, narrow) >= long_w);
169 n_gui_dropmenu_clear(gui, narrow);
170 }
171
172 /* a hover over an OPEN dropmenu panel is consumed and must not leak through to
173 the widget sitting beneath the floating panel. The dropmenu (0,0,120,28) opens
174 its panel downward; a button placed under that panel must stay un-hovered when
175 the mouse is over the open panel, but hover normally when the menu is closed. */
176 {
178 int under = n_gui_add_button(gui, win, "Under", 70.0f, 32.0f, 100.0f, 40.0f, N_GUI_SHAPE_ROUNDED, NULL, NULL);
179 N_GUI_WIDGET* underw = n_gui_get_widget(gui, under);
180 ALLEGRO_EVENT ev;
181 int consumed;
182 expect_true("dropmenu has entries for the panel", n_gui_dropmenu_get_count(gui, dm) == 2);
183 /* control: menu CLOSED, a hover over the under-button hovers it */
184 gui->open_dropmenu_id = -1;
185 ((N_GUI_DROPMENU_DATA*)dmw->data)->is_open = 0;
186 underw->state &= ~N_GUI_STATE_HOVER;
187 memset(&ev, 0, sizeof(ev));
188 ev.type = ALLEGRO_EVENT_MOUSE_AXES;
189 ev.mouse.x = 100;
190 ev.mouse.y = 40;
192 expect_true("button hovers when no menu is open", underw && (underw->state & N_GUI_STATE_HOVER) != 0);
193 /* menu OPEN: clear the stale hover from the control step, then the same hover
194 falls over the panel; it must be consumed and leave the button un-hovered */
195 gui->open_dropmenu_id = dm;
196 ((N_GUI_DROPMENU_DATA*)dmw->data)->is_open = 1;
197 underw->state &= ~N_GUI_STATE_HOVER;
198 memset(&ev, 0, sizeof(ev));
199 ev.type = ALLEGRO_EVENT_MOUSE_AXES;
200 ev.mouse.x = 100;
201 ev.mouse.y = 40;
202 consumed = n_gui_process_event(gui, ev);
203 expect_true("hover over the open panel is consumed", consumed == 1);
204 expect_true("widget beneath the open panel is NOT hovered", underw && (underw->state & N_GUI_STATE_HOVER) == 0);
205 /* close it again so the remaining checks are unaffected */
206 gui->open_dropmenu_id = -1;
207 ((N_GUI_DROPMENU_DATA*)dmw->data)->is_open = 0;
208 }
209
210 /* clearing drops every entry */
212 expect_true("count is 0 after clear", n_gui_dropmenu_get_count(gui, dm) == 0);
213
214 /* the entry click callback receives the entry tag */
215 n_gui_dropmenu_add_entry(gui, dm, "Findings", 42, on_entry, NULL);
216 on_entry(dm, 0, 42, NULL);
217 expect_true("callback saw tag 42", click_tag == 42);
218
219 /* wheel and keyboard move the open panel's highlight cursor. Rebuild a list long
220 enough to scroll, open it, and drive the highlight without a real display: the
221 events are dispatched with the pointer far from the panel so the open-panel wheel
222 and key handlers (not the geometry-dependent hover path) do the work. */
223 {
225 N_GUI_DROPMENU_DATA* dmd = dmw ? (N_GUI_DROPMENU_DATA*)dmw->data : NULL;
226 ALLEGRO_EVENT ev;
227 int i;
229 for (i = 0; i < 5; i++) {
230 char name[16];
231 snprintf(name, sizeof(name), "Item %d", i);
232 n_gui_dropmenu_add_entry(gui, dm, name, 100 + i, on_entry, NULL);
233 }
234 expect_true("dropmenu has data", dmd != NULL);
235 dmd->max_visible = 3; /* force a scrolling list (5 entries, 3 visible) */
236 dmd->is_open = 1;
237 dmd->highlight_index = -1;
238 gui->open_dropmenu_id = dm;
239
240 /* wheel down (dz -1) with the cursor away from the panel: first step reveals
241 entry 0, and the highlight follows the wheel */
242 memset(&ev, 0, sizeof(ev));
243 ev.type = ALLEGRO_EVENT_MOUSE_AXES;
244 ev.mouse.x = 5000;
245 ev.mouse.y = 5000;
246 ev.mouse.dz = -1;
247 expect_true("wheel over open menu is consumed", n_gui_process_event(gui, ev) == 1);
248 expect_true("wheel down highlights the first entry", dmd->highlight_index == 0);
251 expect_true("wheel steps the highlight down", dmd->highlight_index == 2);
252 expect_true("panel scrolled to keep the highlight visible", dmd->scroll_offset == 0);
253 n_gui_process_event(gui, ev); /* to 3, now below the 3-item window */
254 expect_true("highlight advanced to 3", dmd->highlight_index == 3);
255 expect_true("panel scrolled down to follow", dmd->scroll_offset == 1);
256
257 /* Down / Up arrow keys move the same cursor */
258 memset(&ev, 0, sizeof(ev));
259 ev.type = ALLEGRO_EVENT_KEY_CHAR;
260 ev.keyboard.keycode = ALLEGRO_KEY_DOWN;
261 expect_true("Down key is consumed", n_gui_process_event(gui, ev) == 1);
262 expect_true("Down key advances the highlight", dmd->highlight_index == 4);
263 ev.keyboard.keycode = ALLEGRO_KEY_UP;
265 expect_true("Up key moves the highlight back", dmd->highlight_index == 3);
266 ev.keyboard.keycode = ALLEGRO_KEY_HOME;
268 expect_true("Home jumps to the first entry", dmd->highlight_index == 0);
269 ev.keyboard.keycode = ALLEGRO_KEY_END;
271 expect_true("End jumps to the last entry", dmd->highlight_index == 4);
272
273 /* Enter activates the highlighted entry (tag 104) and closes the menu */
274 click_tag = -1;
275 ev.keyboard.keycode = ALLEGRO_KEY_ENTER;
276 expect_true("Enter is consumed", n_gui_process_event(gui, ev) == 1);
277 expect_true("Enter activates the highlighted entry", click_tag == 104);
278 expect_true("Enter closes the menu", dmd->is_open == 0 && gui->open_dropmenu_id == -1);
279
280 /* Escape on a reopened menu closes it without activating anything */
281 dmd->is_open = 1;
282 dmd->highlight_index = 2;
283 gui->open_dropmenu_id = dm;
284 click_tag = -1;
285 ev.keyboard.keycode = ALLEGRO_KEY_ESCAPE;
286 expect_true("Escape is consumed", n_gui_process_event(gui, ev) == 1);
287 expect_true("Escape closes the menu", dmd->is_open == 0 && gui->open_dropmenu_id == -1);
288 expect_true("Escape activates nothing", click_tag == -1);
289 }
290
291 /* the same wheel/key highlight drives a combobox dropdown (the parallel path) */
292 {
293 int cb = n_gui_add_combobox(gui, win, 0.0f, 40.0f, 160.0f, 28.0f, on_combo, NULL);
295 N_GUI_COMBOBOX_DATA* cbd = cbw ? (N_GUI_COMBOBOX_DATA*)cbw->data : NULL;
296 ALLEGRO_EVENT ev;
297 int i;
298 expect_true("combobox created", cb >= 0 && cbd != NULL);
299 for (i = 0; i < 5; i++) {
300 char name[16];
301 snprintf(name, sizeof(name), "Opt %d", i);
302 n_gui_combobox_add_item(gui, cb, name);
303 }
305 /* open the panel the way the click handler does: highlight starts on the value */
306 cbd->max_visible = 3;
307 cbd->is_open = 1;
309 gui->open_combobox_id = cb;
310
311 memset(&ev, 0, sizeof(ev));
312 ev.type = ALLEGRO_EVENT_MOUSE_AXES;
313 ev.mouse.x = 5000;
314 ev.mouse.y = 5000;
315 ev.mouse.dz = -1;
316 expect_true("combobox wheel is consumed", n_gui_process_event(gui, ev) == 1);
317 expect_true("wheel steps down from the selected value", cbd->highlight_index == 3);
318
319 memset(&ev, 0, sizeof(ev));
320 ev.type = ALLEGRO_EVENT_KEY_CHAR;
321 ev.keyboard.keycode = ALLEGRO_KEY_UP;
323 expect_true("Up moves the combobox highlight", cbd->highlight_index == 2);
324 ev.keyboard.keycode = ALLEGRO_KEY_HOME;
326 expect_true("Home moves to the first option", cbd->highlight_index == 0);
327
328 /* Enter commits the highlighted option (selects it and fires on_select) */
329 combo_selected = -1;
330 ev.keyboard.keycode = ALLEGRO_KEY_ENTER;
331 expect_true("combobox Enter is consumed", n_gui_process_event(gui, ev) == 1);
332 expect_true("Enter selects the highlighted option", n_gui_combobox_get_selected(gui, cb) == 0);
333 expect_true("Enter fires on_select", combo_selected == 0);
334 expect_true("Enter closes the combobox", cbd->is_open == 0 && gui->open_combobox_id == -1);
335 }
336
337 /* global shape mode: round vs square GUI toggle (setter/getter round-trip) */
339 expect_true("shape mode set to rect", n_gui_get_shape_mode(gui) == N_GUI_SHAPE_RECT);
341 expect_true("shape mode set to rounded", n_gui_get_shape_mode(gui) == N_GUI_SHAPE_ROUNDED);
342 expect_true("get_shape_mode NULL-safe", n_gui_get_shape_mode(NULL) == N_GUI_SHAPE_ROUNDED);
343
345 al_destroy_font(font);
346
347 if (failures) {
348 n_log(LOG_ERR, "ex_gui_dropmenu: %d failure(s)", failures);
349 return 1;
350 }
351 n_log(LOG_NOTICE, "ex_gui_dropmenu: all checks passed");
352 return 0;
353}
static int failures
int main(void)
void process_args(int argc, char **argv)
Definition ex_common.c:48
int log_level
Definition ex_fluid.c:60
static void expect_true(const char *label, int cond)
static N_GUI_CTX * gui
static void on_entry(int widget_id, int entry_index, int tag, void *user_data)
static int combo_selected
static const char * dropmenu_label(N_GUI_CTX *gui, int id)
static void on_combo(int widget_id, int index, void *user_data)
static int click_tag
int is_open
is dropdown currently open
Definition n_gui.h:756
int open_combobox_id
id of the combobox whose dropdown is currently open, or -1
Definition n_gui.h:1390
int max_visible
max visible items in dropdown
Definition n_gui.h:767
int selected_index
currently selected index (-1 = none)
Definition n_gui.h:754
int open_dropmenu_id
id of the dropdown menu whose panel is currently open, or -1
Definition n_gui.h:1394
int type
widget type (N_GUI_TYPE_*)
Definition n_gui.h:886
int state
current state flags (N_GUI_STATE_*)
Definition n_gui.h:896
int max_visible
max visible items
Definition n_gui.h:862
int highlight_index
open-panel navigation cursor for the wheel and Up/Down keys (-1 = none).
Definition n_gui.h:860
int scroll_offset
scroll offset
Definition n_gui.h:855
int highlight_index
open-panel navigation cursor for the wheel and Up/Down keys (-1 = none).
Definition n_gui.h:763
void * data
widget-specific data (union via void pointer)
Definition n_gui.h:906
int is_open
is the menu currently open
Definition n_gui.h:853
void n_gui_set_display_size(N_GUI_CTX *ctx, float w, float h)
Set the display (viewport) size for global scrollbar computation.
Definition n_gui.c:9034
void n_gui_dropmenu_clear(N_GUI_CTX *ctx, int widget_id)
Remove all entries from a dropdown menu.
Definition n_gui.c:4723
#define N_GUI_STATE_HOVER
mouse is hovering the widget
Definition n_gui.h:201
int n_gui_dropmenu_get_count(N_GUI_CTX *ctx, int widget_id)
Get number of entries in a dropdown menu.
Definition n_gui.c:4748
#define N_GUI_TYPE_DROPMENU
widget type: dropdown menu with static and dynamic entries
Definition n_gui.h:111
int n_gui_process_event(N_GUI_CTX *ctx, ALLEGRO_EVENT event)
Process an allegro event through the GUI system.
Definition n_gui.c:9836
int n_gui_combobox_add_item(N_GUI_CTX *ctx, int widget_id, const char *text)
add an item to a combo box
Definition n_gui.c:4431
#define N_GUI_WIN_FRAMELESS
frameless window: no title bar drawn, drag via window body unless N_GUI_WIN_FIXED_POSITION is also se...
Definition n_gui.h:233
void n_gui_set_shape_mode(N_GUI_CTX *ctx, int shape_mode)
Set the global widget shape (round or square GUI).
Definition n_gui.c:9054
int n_gui_add_combobox(N_GUI_CTX *ctx, int window_id, float x, float y, float w, float h, void(*on_select)(int, int, void *), void *user_data)
Add a combo box widget (dropdown selector)
Definition n_gui.c:2874
N_GUI_WIDGET * n_gui_get_widget(N_GUI_CTX *ctx, int widget_id)
Get a widget pointer by id.
Definition n_gui.c:3028
int n_gui_dropmenu_add_entry(N_GUI_CTX *ctx, int widget_id, const char *text, int tag, void(*on_click)(int, int, int, void *), void *user_data)
Add a static entry to a dropdown menu.
Definition n_gui.c:4646
int n_gui_add_window(N_GUI_CTX *ctx, const char *title, float x, float y, float w, float h)
Add a new pseudo-window to the context.
Definition n_gui.c:1063
N_GUI_CTX * n_gui_new_ctx(ALLEGRO_FONT *default_font)
Create a new GUI context.
Definition n_gui.c:903
float n_gui_dropmenu_panel_width(N_GUI_CTX *ctx, int widget_id)
Width the open drop-down panel needs to show its entries.
Definition n_gui.c:4771
void n_gui_destroy_ctx(N_GUI_CTX **ctx)
Destroy a GUI context and all its windows/widgets.
Definition n_gui.c:995
int n_gui_get_shape_mode(N_GUI_CTX *ctx)
Get the global widget shape.
Definition n_gui.c:9064
void n_gui_dropmenu_set_label(N_GUI_CTX *ctx, int widget_id, const char *label)
Replace the button label shown on a dropmenu.
Definition n_gui.c:4631
int n_gui_combobox_get_selected(N_GUI_CTX *ctx, int widget_id)
get the selected item index in a combobox widget
Definition n_gui.c:4453
#define N_GUI_SHAPE_ROUNDED
rounded rectangle shape
Definition n_gui.h:149
void n_gui_window_set_flags(N_GUI_CTX *ctx, int window_id, int flags)
Set feature flags on a window.
Definition n_gui.c:1968
#define N_GUI_SHAPE_RECT
rectangle shape (default)
Definition n_gui.h:147
void n_gui_combobox_set_selected(N_GUI_CTX *ctx, int widget_id, int index)
set the selected item in a combobox widget
Definition n_gui.c:4478
int n_gui_add_dropmenu(N_GUI_CTX *ctx, int window_id, const char *label, float x, float y, float w, float h, void(*on_open)(int, void *), void *on_open_user_data)
Add a dropdown menu widget.
Definition n_gui.c:4568
int n_gui_add_button(N_GUI_CTX *ctx, int window_id, const char *label, float x, float y, float w, float h, int shape, void(*on_click)(int, void *), void *user_data)
Add a button widget to a window.
Definition n_gui.c:2124
combo box specific data
Definition n_gui.h:746
The top-level GUI context that holds all windows.
Definition n_gui.h:1354
dropdown menu specific data
Definition n_gui.h:843
A single GUI widget.
Definition n_gui.h:882
#define n_log(__LEVEL__,...)
Logging function wrapper to get line and func.
Definition n_log.h:89
#define LOG_DEBUG
debug-level messages
Definition n_log.h:84
#define LOG_ERR
error conditions
Definition n_log.h:76
void set_log_level(const int log_level)
Set the global log level value ( static int LOG_LEVEL )
Definition n_log.c:121
#define LOG_NOTICE
normal but significant condition
Definition n_log.h:80
#define LOG_NULL
no log output
Definition n_log.h:46
#define LOG_INFO
informational
Definition n_log.h:82
Common headers and low-level functions & define.
GUI system: buttons, sliders, text areas, checkboxes, scrollbars, dropdown menus, windows.
Generic log system.