Nilorea Library
C utilities for networking, threading, graphics
Loading...
Searching...
No Matches
ex_gui_tooltip.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
28
29#define ALLEGRO_UNSTABLE 1
30
31#include <stdio.h>
32#include <stdlib.h>
33#include <string.h>
34#include <unistd.h>
35
36#include "nilorea/n_common.h"
37#include "nilorea/n_log.h"
38#include "nilorea/n_gui.h"
39
40/* wide enough that an uncapped bubble can outrun style.tooltip_max_w */
41#define TT_W 1200
42#define TT_H 400
43
44/* the panel is deliberately tiny: it is the "70 px HUD gauge" shape that used to
45 squeeze the bubble down to a few characters per line */
46#define PANEL_X 10.0f
47#define PANEL_W 80.0f
48
49/* window holding the split pane whose divider band is the only tooltip target */
50#define SPLIT_X 400.0f
51#define SPLIT_Y 100.0f
52#define SPLIT_W 200.0f
53#define SPLIT_H 120.0f
54
55static int failures = 0;
56
57static void expect_true(const char* label, int cond) {
58 if (!cond) {
59 n_log(LOG_ERR, "%s: condition false", label);
60 failures++;
61 }
62}
63
64static void usage(void) {
65 fprintf(stderr,
66 " -v version\n"
67 " -V log level: LOG_NULL, LOG_NOTICE, LOG_INFO, LOG_ERR, LOG_DEBUG\n"
68 " -h help\n");
69}
70
71/* the example is quiet by default (LOG_ERR); -V raises it, which is what
72 run_tests.sh passes. */
73static void process_args(int argc, char** argv) {
74 int getoptret = 0;
75 int log_level = LOG_ERR;
76
77 while ((getoptret = getopt(argc, argv, "hvV:")) != EOF) {
78 switch (getoptret) {
79 case 'v':
80 fprintf(stderr, "build date: %s %s\n", __DATE__, __TIME__);
81 exit(0);
82 case 'V':
83 if (!strcmp("LOG_NULL", optarg))
85 else if (!strcmp("LOG_NOTICE", optarg))
87 else if (!strcmp("LOG_INFO", optarg))
89 else if (!strcmp("LOG_ERR", optarg))
91 else if (!strcmp("LOG_DEBUG", optarg))
93 else {
94 fprintf(stderr, "%s is not a valid log level\n", optarg);
95 exit(1);
96 }
97 break;
98 case 'h':
99 usage();
100 exit(0);
101 default:
102 case '?':
103 if (optopt == 'V') fprintf(stderr, "\n Missing log level\n");
104 usage();
105 exit(1);
106 }
107 }
109}
110
111/* Render the whole GUI into target, with the tooltip either armed or not. The
112 bubble is found by differencing the two renders, so nothing here depends on
113 the bubble's colours or on how its corners are rasterized. */
114static void render(N_GUI_CTX* gui, ALLEGRO_BITMAP* target, int armed, int widget_id) {
115 al_set_target_bitmap(target);
116 al_clear_to_color(al_map_rgb(0, 0, 0));
117 gui->tooltip_widget_id = armed ? widget_id : -1;
118 if (armed) {
119 /* pretend the hover happened long enough ago that the delay has elapsed */
120 gui->tooltip_armed_at = al_get_time() - (double)gui->style.tooltip_delay - 1.0;
121 }
123}
124
125/* Width in pixels of the region that appears only when the tooltip is armed,
126 i.e. the bubble. Returns 0 when the two renders are identical. */
127static int bubble_width(ALLEGRO_BITMAP* a, ALLEGRO_BITMAP* b) {
128 ALLEGRO_LOCKED_REGION* la = al_lock_bitmap(a, ALLEGRO_PIXEL_FORMAT_ABGR_8888_LE, ALLEGRO_LOCK_READONLY);
129 ALLEGRO_LOCKED_REGION* lb = al_lock_bitmap(b, ALLEGRO_PIXEL_FORMAT_ABGR_8888_LE, ALLEGRO_LOCK_READONLY);
130 int min_x = TT_W, max_x = -1;
131
132 if (la && lb) {
133 for (int y = 0; y < TT_H; y++) {
134 const unsigned char* ra = (const unsigned char*)la->data + (ptrdiff_t)y * la->pitch;
135 const unsigned char* rb = (const unsigned char*)lb->data + (ptrdiff_t)y * lb->pitch;
136 for (int x = 0; x < TT_W; x++) {
137 if (memcmp(ra + (ptrdiff_t)x * 4, rb + (ptrdiff_t)x * 4, 4) != 0) {
138 if (x < min_x) min_x = x;
139 if (x > max_x) max_x = x;
140 }
141 }
142 }
143 }
144 if (la) al_unlock_bitmap(a);
145 if (lb) al_unlock_bitmap(b);
146
147 return (max_x < 0) ? 0 : (max_x - min_x + 1);
148}
149
150/* Move the pointer to (x,y) through a real mouse event and report which widget
151 the arming scan picked, so the test exercises n_gui_process_event rather than
152 the tooltip_widget_id field it maintains. */
153static int hover_tooltip_at(N_GUI_CTX* gui, int x, int y) {
154 ALLEGRO_EVENT ev;
155 memset(&ev, 0, sizeof(ev));
156 ev.type = ALLEGRO_EVENT_MOUSE_AXES;
157 ev.mouse.x = x;
158 ev.mouse.y = y;
160 return gui->tooltip_widget_id;
161}
162
163int main(int argc, char** argv) {
164 process_args(argc, argv);
165
166 if (!al_init() || !al_init_font_addon()) {
167 n_log(LOG_ERR, "allegro init failed");
168 return 1;
169 }
170 al_init_primitives_addon();
171
172 /* memory bitmaps: the whole test runs without a display, like ex_gui_kvtable */
173 al_set_new_bitmap_flags(ALLEGRO_MEMORY_BITMAP);
174 ALLEGRO_BITMAP* plain = al_create_bitmap(TT_W, TT_H);
175 ALLEGRO_BITMAP* tipped = al_create_bitmap(TT_W, TT_H);
176 ALLEGRO_FONT* font = al_create_builtin_font();
177 if (!plain || !tipped || !font) {
178 n_log(LOG_ERR, "bitmap or font creation failed");
179 return 1;
180 }
181
183 if (!gui) {
184 n_log(LOG_ERR, "n_gui_new_ctx failed");
185 return 1;
186 }
187 n_gui_set_display_size(gui, (float)TT_W, (float)TT_H);
188
189 int win = n_gui_add_window(gui, "HUD", PANEL_X, 10.0f, PANEL_W, 60.0f);
190 int btn = n_gui_add_button(gui, win, "?", 5.0f, 5.0f, 30.0f, 20.0f, N_GUI_SHAPE_RECT, NULL, NULL);
191 expect_true("window created", win >= 0);
192 expect_true("button created", btn >= 0);
193
194 /* long enough that its natural width outruns both the panel and the cap */
196 "Promote this panel to a real OS window, and back again, "
197 "which is the sort of sentence a tooltip has to hold when it "
198 "explains something instead of restating the label beside it.");
199
200 /* The middle of that button, in display coordinates: the client area of a
201 titled window starts below its title bar, so 30,40 is the title bar and
202 not the button. The blocks that drive the arming scan through real mouse
203 motion need the real thing. */
204 const int btn_x = (int)(PANEL_X + 5.0f + 15.0f);
205 const int btn_y = (int)(10.0f + gui->style.titlebar_h + 5.0f + 10.0f);
206
207 /* cursor inside the tiny panel, with room to the right for the bubble */
208 gui->mouse_x = 30;
209 gui->mouse_y = 40;
210
211 /* An in-app window must not narrow the bubble. Before the fix the bubble was
212 clamped to the widget's window rect, so it came out no wider than the 80px
213 panel and the sentence wrapped to a few characters per line. */
214 {
215 render(gui, plain, 0, btn);
216 render(gui, tipped, 1, btn);
217 int w = bubble_width(plain, tipped);
218 expect_true("tooltip is drawn at all", w > 0);
219 expect_true("bubble is not narrowed to the in-app window", (float)w > PANEL_W);
220 n_log(LOG_INFO, "in-app bubble width: %d px (panel is %.0f px)", w, (double)PANEL_W);
221 }
222
223 /* style.tooltip_max_w caps it: the bubble stays near the cap, not the display */
224 {
225 gui->style.tooltip_max_w = 480.0f;
226 render(gui, plain, 0, btn);
227 render(gui, tipped, 1, btn);
228 int capped = bubble_width(plain, tipped);
229 /* text width + the bubble's 8px horizontal padding each side, plus a pixel
230 of slack for the rounded border */
231 expect_true("bubble honours tooltip_max_w", capped <= 480 + 16 + 2);
232 expect_true("bubble still uses the width it is allowed", capped > 480 / 2);
233
234 /* 0 disables the cap, and the same tooltip then runs far wider */
235 gui->style.tooltip_max_w = 0.0f;
236 render(gui, plain, 0, btn);
237 render(gui, tipped, 1, btn);
238 int uncapped = bubble_width(plain, tipped);
239 expect_true("tooltip_max_w = 0 restores wrap-at-the-clamp-edge", uncapped > capped);
240 n_log(LOG_INFO, "capped %d px, uncapped %d px", capped, uncapped);
241
242 gui->style.tooltip_max_w = 480.0f;
243 }
244
245 /* a widget with no tooltip draws nothing extra */
246 {
247 int bare = n_gui_add_button(gui, win, "x", 40.0f, 5.0f, 20.0f, 20.0f, N_GUI_SHAPE_RECT, NULL, NULL);
248 render(gui, plain, 0, bare);
249 render(gui, tipped, 1, bare);
250 expect_true("no tooltip text means no bubble", bubble_width(plain, tipped) == 0);
251 }
252
253 /* A split pane spans the whole region it divides, so only its divider band
254 arms its tooltip: taking the whole rect would answer for every
255 widget-free pixel of both panes. This drives the arming scan through
256 real mouse motion rather than poking tooltip_widget_id. */
257 {
258 int spwin = n_gui_add_window(gui, "Split", SPLIT_X, SPLIT_Y, SPLIT_W, SPLIT_H);
259 int sp = n_gui_add_splitpane(gui, spwin, 0.0f, 0.0f, SPLIT_W, SPLIT_H,
260 N_GUI_SPLIT_VERTICAL, 0.5f, NULL, NULL);
261 expect_true("split pane created", sp >= 0);
262 n_gui_set_widget_tooltip(gui, sp, "Drag to resize");
263
264 /* client-area origin of a titled window: y is below the title bar */
265 float ox = SPLIT_X;
266 float oy = SPLIT_Y + gui->style.titlebar_h;
267 int divider_x = (int)(ox + SPLIT_W * 0.5f);
268
269 expect_true("pointer on the divider arms the split pane",
270 hover_tooltip_at(gui, divider_x, (int)(oy + SPLIT_H * 0.5f)) == sp);
271 expect_true("pointer well left of the divider arms nothing",
272 hover_tooltip_at(gui, (int)(ox + 10.0f), (int)(oy + SPLIT_H * 0.5f)) == -1);
273 expect_true("pointer well right of the divider arms nothing",
274 hover_tooltip_at(gui, (int)(ox + SPLIT_W - 10.0f), (int)(oy + SPLIT_H * 0.5f)) == -1);
275
276 /* a widget sitting over a pane still gets its own tooltip */
277 int over = n_gui_add_button(gui, spwin, "b", 10.0f, 10.0f, 30.0f, 20.0f, N_GUI_SHAPE_RECT, NULL, NULL);
278 n_gui_set_widget_tooltip(gui, over, "on top of the pane");
279 expect_true("a widget above the pane still arms its own tooltip",
280 hover_tooltip_at(gui, (int)(ox + 20.0f), (int)(oy + 20.0f)) == over);
281
282 n_gui_close_window(gui, spwin);
283 }
284
285 /* A window opened over the armed widget hides the bubble. The bubble is
286 drawn last and over everything, and nothing re-runs the arming scan when
287 a window opens without the pointer moving, so it used to be painted over
288 the window that had just appeared - a context menu on a right click, with
289 its first entries covered until the pointer moved. */
290 {
291 int menu = 0;
292
293 gui->mouse_x = btn_x;
294 gui->mouse_y = btn_y;
295 render(gui, plain, 0, btn);
296 render(gui, tipped, 1, btn);
297 expect_true("the bubble shows with nothing over it", bubble_width(plain, tipped) > 0);
298
299 /* over the pointer, and above the panel: a pop-up sorts in front */
300 menu = n_gui_add_window(gui, "Menu", 20.0f, 30.0f, 120.0f, 90.0f);
301 expect_true("menu window created", menu >= 0);
304 expect_true("nothing is pending while a window covers the pointer",
306 render(gui, plain, 0, btn);
307 render(gui, tipped, 1, btn);
308 expect_true("and no bubble is drawn over it", bubble_width(plain, tipped) == 0);
309
310 /* the same window somewhere else leaves the bubble alone */
311 n_gui_window_set_rect(gui, menu, 600.0f, 200.0f, 120.0f, 90.0f);
312 render(gui, plain, 0, btn);
313 render(gui, tipped, 1, btn);
314 expect_true("a window that does not cover the pointer changes nothing",
315 bubble_width(plain, tipped) > 0);
316
317 /* and closing it brings the bubble back where it was */
318 n_gui_window_set_rect(gui, menu, 20.0f, 30.0f, 120.0f, 90.0f);
319 n_gui_close_window(gui, menu);
320 render(gui, plain, 0, btn);
321 render(gui, tipped, 1, btn);
322 expect_true("a closed window covers nothing", bubble_width(plain, tipped) > 0);
323 }
324
325 /* Any button dismisses an armed tooltip, not only the first. A right click
326 is what opens a context menu, and it used to leave the bubble armed. */
327 {
328 ALLEGRO_EVENT ev;
329 int button = 0;
330
331 for (button = 1; button <= 3; button++) {
332 expect_true("the pointer arms the button's tooltip",
333 hover_tooltip_at(gui, btn_x, btn_y) == btn);
334 memset(&ev, 0, sizeof(ev));
335 ev.type = ALLEGRO_EVENT_MOUSE_BUTTON_DOWN;
336 ev.mouse.button = (unsigned)button;
337 ev.mouse.x = btn_x;
338 ev.mouse.y = btn_y;
340 expect_true("and a press on any button dismisses it", gui->tooltip_widget_id == -1);
341
342 memset(&ev, 0, sizeof(ev));
343 ev.type = ALLEGRO_EVENT_MOUSE_BUTTON_UP;
344 ev.mouse.button = (unsigned)button;
345 ev.mouse.x = btn_x;
346 ev.mouse.y = btn_y;
348 }
349 }
350
352 al_destroy_font(font);
353 al_destroy_bitmap(plain);
354 al_destroy_bitmap(tipped);
355
356 if (failures) {
357 n_log(LOG_ERR, "ex_gui_tooltip: %d failure(s)", failures);
358 return 1;
359 }
360 n_log(LOG_NOTICE, "ex_gui_tooltip: all checks passed");
361 return 0;
362}
static void usage(void)
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
int getoptret
Definition ex_fluid.c:59
static N_GUI_CTX * gui
#define SPLIT_H
#define SPLIT_X
#define TT_H
#define TT_W
#define SPLIT_W
#define PANEL_W
#define PANEL_X
#define SPLIT_Y
static int bubble_width(ALLEGRO_BITMAP *a, ALLEGRO_BITMAP *b)
static int hover_tooltip_at(N_GUI_CTX *gui, int x, int y)
static void render(N_GUI_CTX *gui, ALLEGRO_BITMAP *target, int armed, int widget_id)
FONT * font
the default 8x8 font, valid after allegro_init()
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_set_widget_tooltip(N_GUI_CTX *ctx, int widget_id, const char *text)
Set (or clear) the tooltip text of any widget.
Definition n_gui.c:2880
void n_gui_window_set_rect(N_GUI_CTX *ctx, int window_id, float x, float y, float w, float h)
Move and resize a window at runtime, reflowing its widgets.
Definition n_gui.c:12196
int n_gui_process_event(N_GUI_CTX *ctx, ALLEGRO_EVENT event)
Process an allegro event through the GUI system.
Definition n_gui.c:13611
void n_gui_close_window(N_GUI_CTX *ctx, int window_id)
Close (hide) a window.
Definition n_gui.c:1406
int n_gui_tooltip_pending(N_GUI_CTX *ctx)
Report the tooltip phase.
Definition n_gui.c:2970
void n_gui_window_set_zorder(N_GUI_CTX *ctx, int window_id, int z_mode, int z_value)
Set window z-order mode and value.
Definition n_gui.c:2403
int n_gui_add_splitpane(N_GUI_CTX *ctx, int window_id, float x, float y, float w, float h, int orientation, float ratio, void(*on_change)(int, float, void *), void *user_data)
Add a split pane widget: a draggable divider between two regions.
Definition n_gui.c:3331
#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:276
void n_gui_draw(N_GUI_CTX *ctx)
draw all visible GUI windows and widgets
Definition n_gui.c:11735
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
#define N_GUI_SPLIT_VERTICAL
split pane: vertical divider, regions are left and right
Definition n_gui.h:146
void n_gui_destroy_ctx(N_GUI_CTX **ctx)
Destroy a GUI context and all its windows/widgets.
Definition n_gui.c:1209
#define N_GUI_ZORDER_POPUP
popup/modal dialogs: drawn above EVERYTHING including ALWAYS_ON_TOP windows.
Definition n_gui.h:369
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:2515
#define N_GUI_SHAPE_RECT
rectangle shape (default)
Definition n_gui.h:170
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:2720
The top-level GUI context that holds all windows.
Definition n_gui.h:1669
#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.