Nilorea Library
C utilities for networking, threading, graphics
Loading...
Searching...
No Matches
ex_gui_kvtable.c

N_GUI_KVTABLE adaptive-resize regression test.

N_GUI_KVTABLE adaptive-resize regression test. Headless test (no display, builtin font) that exercises the kvtable layout across N_GUI_WIN_RESIZE_SCALE display resizes and clear/add cycles. Asserts:

  • "+" button sits below the last active row at all sizes.
  • New rows added after a resize match the height of previously scaled rows.
  • After clear-all + add, the "+" button does not overlap the "Key" header label.
  • n_gui_kvtable_clear() + repopulate reuses freed row slots instead of growing nb_rows (no unbounded widget accumulation on reload).
  • n_gui_kvtable_set_columns(0,0,0) single-column mode: value, description and enabled widgets (and headers) are hidden and the key column widens to fill the row; set_placeholders is applied.
Author
Castagnier Mickael
Version
1.0
Date
20/05/26
/*
* Nilorea Library
* Copyright (C) 2005-2026 Castagnier Mickael
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
* implied. See the License for the specific language governing
* permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*/
#define ALLEGRO_UNSTABLE 1
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "nilorea/n_log.h"
#include "nilorea/n_gui.h"
static int log_level = LOG_ERR;
static int process_args(int argc, char** argv) {
int opt;
while ((opt = getopt(argc, argv, "V:")) != -1) {
switch (opt) {
case 'V':
if (!strcmp("LOG_NULL", optarg))
else if (!strcmp("LOG_NOTICE", optarg))
else if (!strcmp("LOG_INFO", optarg))
else if (!strcmp("LOG_ERR", optarg))
else if (!strcmp("LOG_DEBUG", optarg))
else
return -1;
break;
default:
return -1;
}
}
return 0;
}
static int failures = 0;
static void expect_close(const char* label, float got, float want, float tol) {
float d = got - want;
if (d < 0) d = -d;
if (d > tol) {
n_log(LOG_ERR, "%s: got %.3f, expected %.3f (tol %.3f)",
label, got, want, tol);
} else {
n_log(LOG_INFO, "%s: %.3f ~= %.3f OK", label, got, want);
}
}
static void expect_true(const char* label, int cond) {
if (!cond) {
n_log(LOG_ERR, "%s: condition false", label);
} else {
n_log(LOG_INFO, "%s: OK", label);
}
}
int main(int argc, char** argv) {
if (process_args(argc, argv) != 0) {
fprintf(stderr, "Usage: %s [-V LOG_LEVEL]\n", argv[0]);
return 2;
}
if (!al_init()) {
n_log(LOG_ERR, "al_init failed");
return 1;
}
if (!al_init_font_addon()) {
n_log(LOG_ERR, "al_init_font_addon failed");
return 1;
}
/* Builtin font does not need a display. */
ALLEGRO_FONT* font = al_create_builtin_font();
if (!font) {
n_log(LOG_ERR, "al_create_builtin_font failed");
return 1;
}
if (!gui) {
n_log(LOG_ERR, "n_gui_new_ctx failed");
al_destroy_font(font);
return 1;
}
/* Reference display matches the parent panel size used in Hook's
request panel (params/headers tab). Set display size BEFORE
switching to adaptive mode so ref_display_w/h is captured. */
n_gui_set_display_size(gui, 800.0f, 258.0f);
int win = n_gui_add_window(gui, "Panel", 0.0f, 0.0f, 800.0f, 258.0f);
expect_true("window created", win >= 0);
N_GUI_KVTABLE* kv = n_gui_kvtable_create(gui, win, 28.0f, 4.0f, NULL, NULL);
expect_true("kvtable created", kv != NULL);
/* Initial: one empty row, like Hook does. */
n_gui_kvtable_add_row(kv, "", "", "", 1);
expect_true("add button widget exists", add_btn != NULL);
expect_true("row 0 key widget exists", row0_key != NULL);
/* At initial size, "+" button must sit strictly below row 0. */
expect_true("+ below row 0 (initial)",
add_btn->y >= row0_key->y + row0_key->h - 0.5f);
/* Resize the display: window scales by 2x in both dimensions. */
n_gui_set_display_size(gui, 1600.0f, 516.0f);
/* After resize via apply_adaptive_resize, fetch fresh pointers
(same widgets, but their x/y/w/h have moved). */
add_btn = n_gui_get_widget(gui, kv->btn_add);
row0_key = n_gui_get_widget(gui, kv->rows[0].key_id);
expect_true("+ still exists after resize", add_btn != NULL);
/* The bug was: after resize, "+" reverted to the same y as row 0.
It should remain at or below row 0's bottom edge. */
expect_true("+ below row 0 (after resize)",
add_btn->y >= row0_key->y + row0_key->h - 0.5f);
/* Record row 0's scaled height for later comparison. */
float scaled_row_h = row0_key->h;
n_log(LOG_INFO, "row 0 scaled height after resize: %.3f", scaled_row_h);
/* Clear all rows then add new ones, mimicking Hook's "delete all
lines then add new lines" path on a maximized window. */
for (int i = kv->nb_rows - 1; i >= 0; i--) {
}
n_gui_kvtable_add_row(kv, "k", "v", "d", 1);
n_gui_kvtable_add_row(kv, "k2", "v2", "d2", 1);
add_btn = n_gui_get_widget(gui, kv->btn_add);
/* "+" must NOT overlap the "Key" header label. */
expect_true("+ button below Key header (after clear+add)",
add_btn->y >= lbl_key->y + lbl_key->h - 0.5f);
/* New rows must match the scaled row height (within rounding tolerance). */
int last_active = -1;
for (int i = 0; i < kv->nb_rows; i++) {
if (kv->rows[i].active) last_active = i;
}
expect_true("at least one active row exists", last_active >= 0);
if (last_active >= 0) {
N_GUI_WIDGET* new_key = n_gui_get_widget(gui, kv->rows[last_active].key_id);
expect_close("new row height matches scaled row height",
new_key->h, scaled_row_h, 0.5f);
/* "+" sits immediately below the last active row. */
expect_true("+ directly below last active row",
add_btn->y >= new_key->y + new_key->h - 0.5f);
}
/* Public relayout entry-point smoke test. */
add_btn = n_gui_get_widget(gui, kv->btn_add);
expect_true("+ still placed after manual relayout",
add_btn != NULL && add_btn->y > 0.0f);
/* Reload-safe clear: repopulating the same row count reuses the freed
slots instead of growing the widget array toward N_GUI_KV_MAX. */
expect_true("clear -> 0 active rows", n_gui_kvtable_get_count(kv) == 0);
n_gui_kvtable_add_row(kv, "a", "1", "", 1);
n_gui_kvtable_add_row(kv, "b", "2", "", 1);
n_gui_kvtable_add_row(kv, "c", "3", "", 1);
int rows_after_first_fill = kv->nb_rows;
expect_true("3 active rows after fill", n_gui_kvtable_get_count(kv) == 3);
n_gui_kvtable_add_row(kv, "a", "1", "", 1);
n_gui_kvtable_add_row(kv, "b", "2", "", 1);
n_gui_kvtable_add_row(kv, "c", "3", "", 1);
expect_true("3 active rows after reload", n_gui_kvtable_get_count(kv) == 3);
expect_true("reload reuses slots (nb_rows does not grow)",
kv->nb_rows == rows_after_first_fill);
/* Single-column mode: only the key column and the remove button are shown;
the value/description/enabled widgets (and their headers) are hidden, and
the key column widens to fill the row. */
n_gui_kvtable_set_placeholders(kv, "host, .suffix, * or re:regex", NULL);
n_gui_kvtable_add_row(kv, ".example.com", "", "", 1);
{
int active = -1;
for (int i = 0; i < kv->nb_rows; i++) {
if (kv->rows[i].active) {
active = i;
break;
}
}
expect_true("single-column: one active row", active >= 0);
if (active >= 0) {
const N_GUI_WINDOW* wwin = n_gui_get_window(gui, win);
expect_true("single-column: key visible", wk && wk->visible);
expect_true("single-column: remove visible", wr && wr->visible);
expect_true("single-column: value hidden", wv && !wv->visible);
expect_true("single-column: description hidden", wd && !wd->visible);
expect_true("single-column: enabled hidden", we && !we->visible);
expect_true("single-column: value header hidden",
expect_true("single-column: key column widened",
wk && wwin && wk->w > wwin->w * 0.5f);
}
}
/* Top offset reserves space above the header row (for a toolbar/heading in
a shared window); the header (and rows) shift down by the scaled offset. */
{
float y_before = lk_before ? lk_before->y : 0.0f;
expect_true("top_offset pushes the header row down",
lk_after && lk_after->y > y_before + 10.0f);
}
/* Row heights follow the window only when the window scales its widgets. The
table above lives in a SCALE-policy window, so its rows scaled with the
display resize (asserted earlier). A MOVE-policy window instead keeps the
widgets' pixel layout, so the rows must keep their design height and a taller
window simply shows more of them, rather than the same rows drawn fatter. */
{
int win2 = n_gui_add_window(gui, "MovePanel", 0.0f, 0.0f, 800.0f, 258.0f);
N_GUI_KVTABLE* kv2 = n_gui_kvtable_create(gui, win2, 24.0f, 8.0f, NULL, NULL);
expect_true("move-policy table created", kv2 != NULL);
if (kv2) {
n_gui_kvtable_add_row(kv2, "k", "v", "d", 1);
float h_before = r0 ? r0->h : 0.0f;
expect_true("move-policy row has a height", h_before > 0.0f);
/* grow the window vertically only, then relayout */
if (w2) w2->h *= 2.0f;
r0 = n_gui_get_widget(gui, kv2->rows[0].key_id);
expect_close("move-policy row keeps its height", r0 ? r0->h : -1.0f, h_before, 0.5f);
/* a row added after the growth matches the existing ones */
n_gui_kvtable_add_row(kv2, "k2", "v2", "d2", 1);
expect_close("move-policy new row matches", r1 ? r1->h : -1.0f, h_before, 0.5f);
/* widths still follow the window: widen it and the key column grows */
N_GUI_WIDGET* k_before = n_gui_get_widget(gui, kv2->rows[0].key_id);
float w_before = k_before ? k_before->w : 0.0f;
if (w2) w2->w *= 2.0f;
expect_true("move-policy key column still widens",
k_after && k_after->w > w_before + 1.0f);
}
}
/* Per-row actions: move up / move down / duplicate. Row slots are stable
(widgets are bound to them, freed slots are reused), so the operations
move the CONTENT and everything that walks the rows in slot order sees
the new order. */
{
int win3 = n_gui_add_window(gui, "ActionPanel", 0.0f, 0.0f, 800.0f, 300.0f);
N_GUI_KVTABLE* kv3 = n_gui_kvtable_create(gui, win3, 24.0f, 8.0f, NULL, NULL);
expect_true("actions table created", kv3 != NULL);
if (kv3) {
expect_true("actions off by default", kv3->show_actions == 0);
expect_true("actions enabled", kv3->show_actions == 1);
expect_true("add button drawn as a glyph",
n_gui_kvtable_add_row(kv3, "a", "1", "first", 1);
n_gui_kvtable_add_row(kv3, "b", "2", "second", 0);
n_gui_kvtable_add_row(kv3, "c", "3", "third", 1);
expect_true("three rows", n_gui_kvtable_get_count(kv3) == 3);
/* the action buttons are laid out inside the row, left of remove */
{
expect_true("action widgets exist", up && dn && dp && rm);
if (up && dn && dp && rm) {
expect_true("actions visible", up->visible && dn->visible && dp->visible);
expect_true("actions ordered up, down, duplicate, remove",
up->x < dn->x && dn->x < dp->x && dp->x < rm->x);
expect_true("actions stay inside the window", rm->x + rm->w <= 800.0f);
expect_true("actions share the row",
up->y == rm->y && dn->y == rm->y && dp->y == rm->y);
}
/* first row cannot move up, last cannot move down */
expect_true("first row up disabled",
expect_true("first row down enabled",
expect_true("last row down disabled",
}
/* move "c" up: rows read a, c, b */
expect_true("move up returns the target slot",
n_gui_kvtable_move_row(kv3, 2, -1) == 1);
expect_true("row 1 is now c",
strcmp(n_gui_textarea_get_text(gui, kv3->rows[1].key_id), "c") == 0);
expect_true("row 2 is now b",
strcmp(n_gui_textarea_get_text(gui, kv3->rows[2].key_id), "b") == 0);
expect_true("the enabled flag travels with the row",
/* edges refuse to move */
expect_true("first row cannot move up", n_gui_kvtable_move_row(kv3, 0, -1) == -1);
expect_true("last row cannot move down", n_gui_kvtable_move_row(kv3, 2, 1) == -1);
/* duplicate row 0 ("a"): rows read a, a, c, b */
expect_true("duplicate lands right after the source",
expect_true("four rows after duplicate", n_gui_kvtable_get_count(kv3) == 4);
expect_true("copy holds the source key",
strcmp(n_gui_textarea_get_text(gui, kv3->rows[1].key_id), "a") == 0);
expect_true("copy holds the source value",
strcmp(n_gui_textarea_get_text(gui, kv3->rows[1].value_id), "1") == 0);
expect_true("copy holds the source description",
strcmp(n_gui_textarea_get_text(gui, kv3->rows[1].desc_id), "first") == 0);
expect_true("source is untouched",
strcmp(n_gui_textarea_get_text(gui, kv3->rows[0].key_id), "a") == 0);
expect_true("rows after the copy shifted down",
strcmp(n_gui_textarea_get_text(gui, kv3->rows[2].key_id), "c") == 0 &&
strcmp(n_gui_textarea_get_text(gui, kv3->rows[3].key_id), "b") == 0);
/* a removed slot in the middle is reused, and the copy still lands
next to its source rather than in whatever slot came free */
expect_true("three rows after remove", n_gui_kvtable_get_count(kv3) == 3);
expect_true("duplicate of the last row appends",
{
int order[8], n = 0;
for (int i = 0; i < kv3->nb_rows && n < 8; i++) {
if (kv3->rows[i].active) order[n++] = i;
}
expect_true("four rows again", n == 4);
if (n == 4) {
expect_true("order is a, c, b, b",
strcmp(n_gui_textarea_get_text(gui, kv3->rows[order[0]].key_id), "a") == 0 &&
strcmp(n_gui_textarea_get_text(gui, kv3->rows[order[1]].key_id), "c") == 0 &&
strcmp(n_gui_textarea_get_text(gui, kv3->rows[order[2]].key_id), "b") == 0 &&
strcmp(n_gui_textarea_get_text(gui, kv3->rows[order[3]].key_id), "b") == 0);
}
}
/* turning the actions off hides the buttons and restores the plain
three-column layout, remove button included */
{
expect_true("actions hidden", up && dn && dp && !up->visible && !dn->visible && !dp->visible);
expect_true("remove button stays", rm && rm->visible);
expect_true("row still fits the window", rm && rm->x + rm->w <= 800.0f);
expect_true("remove button back to its label",
expect_true("add button back to its label",
}
}
}
al_destroy_font(font);
if (failures > 0) {
n_log(LOG_ERR, "ex_gui_kvtable: %d check(s) failed", failures);
return 1;
}
n_log(LOG_NOTICE, "ex_gui_kvtable: all checks passed");
return 0;
}
void process_args(int argc, char **argv)
static int failures
Definition ex_atlas.c:51
int main(void)
Definition ex_atlas.c:117
static void expect_true(const char *label, int cond)
Definition ex_atlas.c:53
static int log_level
static N_GUI_CTX * gui
static void expect_close(const char *label, float got, float want, float tol)
FONT * font
the default 8x8 font, valid after allegro_init()
float h
height
Definition n_gui.h:1104
int visible
visibility flag
Definition n_gui.h:1114
int show_actions
1 to show the per-row move-up / move-down / duplicate buttons (default 0)
Definition n_gui.h:3662
int nb_rows
total rows (including removed)
Definition n_gui.h:3645
int btn_add
button widget ID for "+" add row
Definition n_gui.h:3656
float w
width
Definition n_gui.h:1102
int remove_id
button widget ID for the remove action
Definition n_gui.h:3630
float w
window width
Definition n_gui.h:1316
N_GUI_KV_ROW rows[128]
row storage
Definition n_gui.h:3644
int key_id
textarea widget ID for the key
Definition n_gui.h:3626
float x
position x relative to parent window
Definition n_gui.h:1098
int dup_id
button widget ID for the duplicate action (row actions only)
Definition n_gui.h:3633
int desc_id
textarea widget ID for the description
Definition n_gui.h:3628
int lbl_key
label widget ID for "Key" header
Definition n_gui.h:3652
float h
window height
Definition n_gui.h:1318
float y
position y relative to parent window
Definition n_gui.h:1100
int active
1 if active, 0 if removed
Definition n_gui.h:3634
int up_id
button widget ID for the move-up action (row actions only)
Definition n_gui.h:3631
int lbl_value
label widget ID for "Value" header
Definition n_gui.h:3653
int enabled_id
checkbox widget ID for the enabled toggle
Definition n_gui.h:3629
int down_id
button widget ID for the move-down action (row actions only)
Definition n_gui.h:3632
int value_id
textarea widget ID for the value
Definition n_gui.h:3627
void n_gui_set_display_size(N_GUI_CTX *ctx, float w, float h)
set the display (viewport) size so global scrollbars can be computed.
Definition n_gui.c:12348
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:19011
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:19021
int n_gui_kvtable_get_count(const N_GUI_KVTABLE *table)
get the number of active rows
Definition n_gui.c:19017
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:18672
#define N_GUI_GLYPH_NONE
no glyph: the button draws its text label (default)
Definition n_gui.h:418
#define N_GUI_WIN_RESIZE_SCALE
reposition AND resize proportionally, child widgets scale too
Definition n_gui.h:333
void n_gui_kvtable_free(N_GUI_KVTABLE **table)
free a KV table (does not destroy the N_GUI widgets)
Definition n_gui.c:19025
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:12132
void n_gui_kvtable_clear(N_GUI_KVTABLE *table)
remove every row at once.
Definition n_gui.c:18837
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:18997
int n_gui_kvtable_move_row(N_GUI_KVTABLE *table, int row_index, int direction)
Move a row one position up or down among the active rows.
Definition n_gui.c:18912
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:18819
int n_gui_button_get_glyph(N_GUI_CTX *ctx, int widget_id)
Get the glyph currently drawn on a button.
Definition n_gui.c:2859
int n_gui_is_widget_enabled(N_GUI_CTX *ctx, int widget_id)
Check if a widget is enabled.
Definition n_gui.c:3896
N_GUI_WIDGET * n_gui_get_widget(N_GUI_CTX *ctx, int widget_id)
Get a widget pointer by id.
Definition n_gui.c:3701
N_GUI_WINDOW * n_gui_get_window(N_GUI_CTX *ctx, int window_id)
Get a window pointer by id.
Definition n_gui.c:1397
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:1277
N_GUI_CTX * n_gui_new_ctx(ALLEGRO_FONT *default_font)
Create a new GUI context.
Definition n_gui.c:1103
void n_gui_kvtable_set_row_actions(N_GUI_KVTABLE *table, int enabled)
Show or hide the per-row move-up / move-down / duplicate buttons.
Definition n_gui.c:18880
int n_gui_kvtable_duplicate_row(N_GUI_KVTABLE *table, int row_index)
Insert a copy of a row directly after it.
Definition n_gui.c:18956
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:12093
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:18858
#define N_GUI_GLYPH_PLUS
plus sign
Definition n_gui.h:428
void n_gui_destroy_ctx(N_GUI_CTX **ctx)
Destroy a GUI context and all its windows/widgets.
Definition n_gui.c:1209
const char * n_gui_textarea_get_text(N_GUI_CTX *ctx, int widget_id)
get the text content of a textarea widget
Definition n_gui.c:4196
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:18733
int n_gui_checkbox_is_checked(N_GUI_CTX *ctx, int widget_id)
check if a checkbox widget is checked
Definition n_gui.c:4237
#define N_GUI_WIN_RESIZE_MOVE
reposition proportionally, keep pixel size
Definition n_gui.h:331
#define N_GUI_RESIZE_ADAPTIVE
virtual size tracks display; windows adapt per their resize_policy
Definition n_gui.h:325
The top-level GUI context that holds all windows.
Definition n_gui.h:1669
key-value table built from textareas, checkboxes, and buttons.
Definition n_gui.h:3641
A single GUI widget.
Definition n_gui.h:1092
A pseudo window that contains widgets.
Definition n_gui.h:1298
#define n_log(__LEVEL__,...)
Logging function wrapper to get line and func.
Definition n_log.h:113
#define LOG_DEBUG
debug-level messages
Definition n_log.h:108
#define LOG_ERR
error conditions
Definition n_log.h:100
void set_log_level(const int log_level)
Set the global log level value ( static int LOG_LEVEL ).
Definition n_log.c:123
#define LOG_NOTICE
normal but significant condition
Definition n_log.h:104
#define LOG_NULL
no log output
Definition n_log.h:70
#define LOG_INFO
informational
Definition n_log.h:106
Common headers and low-level functions & define.
GUI system: buttons, sliders, text areas, checkboxes, scrollbars, dropdown menus, windows.
Generic log system.