Nilorea Library
C utilities for networking, threading, graphics
Loading...
Searching...
No Matches
ex_gui_kvtable.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
43#define ALLEGRO_UNSTABLE 1
44
45#include <stdio.h>
46#include <stdlib.h>
47#include <string.h>
48#include <unistd.h>
49
50#include "nilorea/n_common.h"
51#include "nilorea/n_log.h"
52#include "nilorea/n_gui.h"
53
54static int log_level = LOG_ERR;
55
56static int process_args(int argc, char** argv) {
57 int opt;
58 while ((opt = getopt(argc, argv, "V:")) != -1) {
59 switch (opt) {
60 case 'V':
61 if (!strcmp("LOG_NULL", optarg))
63 else if (!strcmp("LOG_NOTICE", optarg))
65 else if (!strcmp("LOG_INFO", optarg))
67 else if (!strcmp("LOG_ERR", optarg))
69 else if (!strcmp("LOG_DEBUG", optarg))
71 else
72 return -1;
73 break;
74 default:
75 return -1;
76 }
77 }
78 return 0;
79}
80
81static int failures = 0;
82
83static void expect_close(const char* label, float got, float want, float tol) {
84 float d = got - want;
85 if (d < 0) d = -d;
86 if (d > tol) {
87 n_log(LOG_ERR, "%s: got %.3f, expected %.3f (tol %.3f)",
88 label, got, want, tol);
89 failures++;
90 } else {
91 n_log(LOG_INFO, "%s: %.3f ~= %.3f OK", label, got, want);
92 }
93}
94
95static void expect_true(const char* label, int cond) {
96 if (!cond) {
97 n_log(LOG_ERR, "%s: condition false", label);
98 failures++;
99 } else {
100 n_log(LOG_INFO, "%s: OK", label);
101 }
102}
103
104int main(int argc, char** argv) {
106 if (process_args(argc, argv) != 0) {
107 fprintf(stderr, "Usage: %s [-V LOG_LEVEL]\n", argv[0]);
108 return 2;
109 }
111
112 if (!al_init()) {
113 n_log(LOG_ERR, "al_init failed");
114 return 1;
115 }
116 if (!al_init_font_addon()) {
117 n_log(LOG_ERR, "al_init_font_addon failed");
118 return 1;
119 }
120
121 /* Builtin font does not need a display. */
122 ALLEGRO_FONT* font = al_create_builtin_font();
123 if (!font) {
124 n_log(LOG_ERR, "al_create_builtin_font failed");
125 return 1;
126 }
127
128 N_GUI_CTX* gui = n_gui_new_ctx(font);
129 if (!gui) {
130 n_log(LOG_ERR, "n_gui_new_ctx failed");
131 al_destroy_font(font);
132 return 1;
133 }
134
135 /* Reference display matches the parent panel size used in Hook's
136 request panel (params/headers tab). Set display size BEFORE
137 switching to adaptive mode so ref_display_w/h is captured. */
138 n_gui_set_display_size(gui, 800.0f, 258.0f);
140
141 int win = n_gui_add_window(gui, "Panel", 0.0f, 0.0f, 800.0f, 258.0f);
142 expect_true("window created", win >= 0);
144
145 N_GUI_KVTABLE* kv = n_gui_kvtable_create(gui, win, 28.0f, 4.0f, NULL, NULL);
146 expect_true("kvtable created", kv != NULL);
147
148 /* Initial: one empty row, like Hook does. */
149 n_gui_kvtable_add_row(kv, "", "", "", 1);
150
151 N_GUI_WIDGET* add_btn = n_gui_get_widget(gui, kv->btn_add);
152 N_GUI_WIDGET* row0_key = n_gui_get_widget(gui, kv->rows[0].key_id);
153 expect_true("add button widget exists", add_btn != NULL);
154 expect_true("row 0 key widget exists", row0_key != NULL);
155
156 /* At initial size, "+" button must sit strictly below row 0. */
157 expect_true("+ below row 0 (initial)",
158 add_btn->y >= row0_key->y + row0_key->h - 0.5f);
159
160 /* Resize the display: window scales by 2x in both dimensions. */
161 n_gui_set_display_size(gui, 1600.0f, 516.0f);
162
163 /* After resize via apply_adaptive_resize, fetch fresh pointers
164 (same widgets, but their x/y/w/h have moved). */
165 add_btn = n_gui_get_widget(gui, kv->btn_add);
166 row0_key = n_gui_get_widget(gui, kv->rows[0].key_id);
167 expect_true("+ still exists after resize", add_btn != NULL);
168
169 /* The bug was: after resize, "+" reverted to the same y as row 0.
170 It should remain at or below row 0's bottom edge. */
171 expect_true("+ below row 0 (after resize)",
172 add_btn->y >= row0_key->y + row0_key->h - 0.5f);
173
174 /* Record row 0's scaled height for later comparison. */
175 float scaled_row_h = row0_key->h;
176 n_log(LOG_INFO, "row 0 scaled height after resize: %.3f", scaled_row_h);
177
178 /* Clear all rows then add new ones, mimicking Hook's "delete all
179 lines then add new lines" path on a maximized window. */
180 for (int i = kv->nb_rows - 1; i >= 0; i--) {
182 }
183 n_gui_kvtable_add_row(kv, "k", "v", "d", 1);
184 n_gui_kvtable_add_row(kv, "k2", "v2", "d2", 1);
185
186 add_btn = n_gui_get_widget(gui, kv->btn_add);
187 N_GUI_WIDGET* lbl_key = n_gui_get_widget(gui, kv->lbl_key);
188
189 /* "+" must NOT overlap the "Key" header label. */
190 expect_true("+ button below Key header (after clear+add)",
191 add_btn->y >= lbl_key->y + lbl_key->h - 0.5f);
192
193 /* New rows must match the scaled row height (within rounding tolerance). */
194 int last_active = -1;
195 for (int i = 0; i < kv->nb_rows; i++) {
196 if (kv->rows[i].active) last_active = i;
197 }
198 expect_true("at least one active row exists", last_active >= 0);
199 if (last_active >= 0) {
200 N_GUI_WIDGET* new_key = n_gui_get_widget(gui, kv->rows[last_active].key_id);
201 expect_close("new row height matches scaled row height",
202 new_key->h, scaled_row_h, 0.5f);
203 /* "+" sits immediately below the last active row. */
204 expect_true("+ directly below last active row",
205 add_btn->y >= new_key->y + new_key->h - 0.5f);
206 }
207
208 /* Public relayout entry-point smoke test. */
210 add_btn = n_gui_get_widget(gui, kv->btn_add);
211 expect_true("+ still placed after manual relayout",
212 add_btn != NULL && add_btn->y > 0.0f);
213
214 /* Reload-safe clear: repopulating the same row count reuses the freed
215 slots instead of growing the widget array toward N_GUI_KV_MAX. */
217 expect_true("clear -> 0 active rows", n_gui_kvtable_get_count(kv) == 0);
218 n_gui_kvtable_add_row(kv, "a", "1", "", 1);
219 n_gui_kvtable_add_row(kv, "b", "2", "", 1);
220 n_gui_kvtable_add_row(kv, "c", "3", "", 1);
221 int rows_after_first_fill = kv->nb_rows;
222 expect_true("3 active rows after fill", n_gui_kvtable_get_count(kv) == 3);
224 n_gui_kvtable_add_row(kv, "a", "1", "", 1);
225 n_gui_kvtable_add_row(kv, "b", "2", "", 1);
226 n_gui_kvtable_add_row(kv, "c", "3", "", 1);
227 expect_true("3 active rows after reload", n_gui_kvtable_get_count(kv) == 3);
228 expect_true("reload reuses slots (nb_rows does not grow)",
229 kv->nb_rows == rows_after_first_fill);
230
231 /* Single-column mode: only the key column and the remove button are shown;
232 the value/description/enabled widgets (and their headers) are hidden, and
233 the key column widens to fill the row. */
234 n_gui_kvtable_set_columns(kv, 0, 0, 0);
235 n_gui_kvtable_set_placeholders(kv, "host, .suffix, * or re:regex", NULL);
237 n_gui_kvtable_add_row(kv, ".example.com", "", "", 1);
238 {
239 int active = -1;
240 for (int i = 0; i < kv->nb_rows; i++) {
241 if (kv->rows[i].active) {
242 active = i;
243 break;
244 }
245 }
246 expect_true("single-column: one active row", active >= 0);
247 if (active >= 0) {
248 N_GUI_WIDGET* wk = n_gui_get_widget(gui, kv->rows[active].key_id);
249 N_GUI_WIDGET* wv = n_gui_get_widget(gui, kv->rows[active].value_id);
250 N_GUI_WIDGET* wd = n_gui_get_widget(gui, kv->rows[active].desc_id);
251 N_GUI_WIDGET* we = n_gui_get_widget(gui, kv->rows[active].enabled_id);
252 N_GUI_WIDGET* wr = n_gui_get_widget(gui, kv->rows[active].remove_id);
253 const N_GUI_WINDOW* wwin = n_gui_get_window(gui, win);
254 expect_true("single-column: key visible", wk && wk->visible);
255 expect_true("single-column: remove visible", wr && wr->visible);
256 expect_true("single-column: value hidden", wv && !wv->visible);
257 expect_true("single-column: description hidden", wd && !wd->visible);
258 expect_true("single-column: enabled hidden", we && !we->visible);
259 expect_true("single-column: value header hidden",
261 expect_true("single-column: key column widened",
262 wk && wwin && wk->w > wwin->w * 0.5f);
263 }
264 }
265
266 /* Top offset reserves space above the header row (for a toolbar/heading in
267 a shared window); the header (and rows) shift down by the scaled offset. */
268 {
269 N_GUI_WIDGET* lk_before = n_gui_get_widget(gui, kv->lbl_key);
270 float y_before = lk_before ? lk_before->y : 0.0f;
272 N_GUI_WIDGET* lk_after = n_gui_get_widget(gui, kv->lbl_key);
273 expect_true("top_offset pushes the header row down",
274 lk_after && lk_after->y > y_before + 10.0f);
275 }
276
277 /* Row heights follow the window only when the window scales its widgets. The
278 table above lives in a SCALE-policy window, so its rows scaled with the
279 display resize (asserted earlier). A MOVE-policy window instead keeps the
280 widgets' pixel layout, so the rows must keep their design height and a taller
281 window simply shows more of them, rather than the same rows drawn fatter. */
282 {
283 int win2 = n_gui_add_window(gui, "MovePanel", 0.0f, 0.0f, 800.0f, 258.0f);
285 N_GUI_KVTABLE* kv2 = n_gui_kvtable_create(gui, win2, 24.0f, 8.0f, NULL, NULL);
286 expect_true("move-policy table created", kv2 != NULL);
287 if (kv2) {
288 n_gui_kvtable_add_row(kv2, "k", "v", "d", 1);
290 float h_before = r0 ? r0->h : 0.0f;
291 expect_true("move-policy row has a height", h_before > 0.0f);
292
293 /* grow the window vertically only, then relayout */
294 N_GUI_WINDOW* w2 = n_gui_get_window(gui, win2);
295 if (w2) w2->h *= 2.0f;
297
298 r0 = n_gui_get_widget(gui, kv2->rows[0].key_id);
299 expect_close("move-policy row keeps its height", r0 ? r0->h : -1.0f, h_before, 0.5f);
300
301 /* a row added after the growth matches the existing ones */
302 n_gui_kvtable_add_row(kv2, "k2", "v2", "d2", 1);
304 expect_close("move-policy new row matches", r1 ? r1->h : -1.0f, h_before, 0.5f);
305
306 /* widths still follow the window: widen it and the key column grows */
307 N_GUI_WIDGET* k_before = n_gui_get_widget(gui, kv2->rows[0].key_id);
308 float w_before = k_before ? k_before->w : 0.0f;
309 if (w2) w2->w *= 2.0f;
311 N_GUI_WIDGET* k_after = n_gui_get_widget(gui, kv2->rows[0].key_id);
312 expect_true("move-policy key column still widens",
313 k_after && k_after->w > w_before + 1.0f);
314
315 n_gui_kvtable_free(&kv2);
316 }
317 }
318
321 al_destroy_font(font);
322
323 if (failures > 0) {
324 n_log(LOG_ERR, "ex_gui_kvtable: %d check(s) failed", failures);
325 return 1;
326 }
327 n_log(LOG_NOTICE, "ex_gui_kvtable: all checks passed");
328 return 0;
329}
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 expect_close(const char *label, float got, float want, float tol)
float h
height
Definition n_gui.h:894
int visible
visibility flag
Definition n_gui.h:898
int nb_rows
total rows (including removed)
Definition n_gui.h:2566
int btn_add
button widget ID for "+" add row
Definition n_gui.h:2577
float w
width
Definition n_gui.h:892
int remove_id
button widget ID for the remove action
Definition n_gui.h:2557
float w
window width
Definition n_gui.h:1009
N_GUI_KV_ROW rows[128]
row storage
Definition n_gui.h:2565
int key_id
textarea widget ID for the key
Definition n_gui.h:2553
int desc_id
textarea widget ID for the description
Definition n_gui.h:2555
int lbl_key
label widget ID for "Key" header
Definition n_gui.h:2573
float h
window height
Definition n_gui.h:1011
float y
position y relative to parent window
Definition n_gui.h:890
int active
1 if active, 0 if removed
Definition n_gui.h:2558
int lbl_value
label widget ID for "Value" header
Definition n_gui.h:2574
int enabled_id
checkbox widget ID for the enabled toggle
Definition n_gui.h:2556
int value_id
textarea widget ID for the value
Definition n_gui.h:2554
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_kvtable_set_top_offset(N_GUI_KVTABLE *table, float top_offset)
reserve top_offset unscaled pixels above the header row so other widgets (a toolbar,...
Definition n_gui.c:14166
void n_gui_kvtable_relayout(N_GUI_KVTABLE *table)
re-apply layout to all KV table widgets (positions, sizes, normalized coords).
Definition n_gui.c:14176
int n_gui_kvtable_get_count(const N_GUI_KVTABLE *table)
get the number of active rows
Definition n_gui.c:14172
N_GUI_KVTABLE * n_gui_kvtable_create(N_GUI_CTX *ctx, int window_id, float row_height, float padding, void(*on_remove)(int, void *), void *user_data)
create a KV table in an existing window
Definition n_gui.c:14004
#define N_GUI_WIN_RESIZE_SCALE
reposition AND resize proportionally, child widgets scale too
Definition n_gui.h:290
void n_gui_kvtable_free(N_GUI_KVTABLE **table)
free a KV table (does not destroy the N_GUI widgets)
Definition n_gui.c:14180
void n_gui_window_set_resize_policy(N_GUI_CTX *ctx, int window_id, int policy)
Set per-window resize policy (N_GUI_WIN_RESIZE_NONE / _MOVE / _SCALE).
Definition n_gui.c:8936
void n_gui_kvtable_clear(N_GUI_KVTABLE *table)
remove every row at once.
Definition n_gui.c:14126
void n_gui_kvtable_set_placeholders(N_GUI_KVTABLE *table, const char *key_hint, const char *value_hint)
set the placeholder/hint text shown in each row's key (and value) textarea while it is empty.
Definition n_gui.c:14152
void n_gui_kvtable_remove_row(N_GUI_KVTABLE *table, int row_index)
remove a row by index (hides widgets, marks inactive)
Definition n_gui.c:14112
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
N_GUI_WINDOW * n_gui_get_window(N_GUI_CTX *ctx, int window_id)
Get a window pointer by id.
Definition n_gui.c:1182
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
void n_gui_set_resize_mode(N_GUI_CTX *ctx, int mode)
Set context-level resize mode: N_GUI_RESIZE_VIRTUAL (default) or N_GUI_RESIZE_ADAPTIVE.
Definition n_gui.c:8897
void n_gui_kvtable_set_columns(N_GUI_KVTABLE *table, int show_value, int show_desc, int show_enabled)
choose which optional columns are shown: Value, Description, and the enabled checkbox.
Definition n_gui.c:14144
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_kvtable_add_row(N_GUI_KVTABLE *table, const char *key, const char *value, const char *description, int enabled)
add a row to the KV table
Definition n_gui.c:14053
#define N_GUI_WIN_RESIZE_MOVE
reposition proportionally, keep pixel size
Definition n_gui.h:288
#define N_GUI_RESIZE_ADAPTIVE
virtual size tracks display; windows adapt per their resize_policy
Definition n_gui.h:282
The top-level GUI context that holds all windows.
Definition n_gui.h:1354
key-value table built from textareas, checkboxes, and buttons
Definition n_gui.h:2562
A single GUI widget.
Definition n_gui.h:882
A pseudo window that contains widgets.
Definition n_gui.h:999
#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.