Nilorea Library
C utilities for networking, threading, graphics
Loading...
Searching...
No Matches
ex_gui_multiwin.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
33#define ALLEGRO_UNSTABLE 1
34
35#include <stdio.h>
36#include <stdlib.h>
37#include <string.h>
38#include <unistd.h>
39
40#include "nilorea/n_common.h"
41#include "nilorea/n_clipboard.h"
42#include "nilorea/n_gui.h"
43#include "nilorea/n_log.h"
44
45#define MAIN_W 640
46#define MAIN_H 480
47#define POPUP_W 240
48#define POPUP_H 160
49
50static int log_level = LOG_ERR;
51static int failures = 0;
52
53/* which button callback fired last, and how many times */
54static int g_main_clicks = 0;
55static int g_popup_clicks = 0;
56/* window id the close callback was invoked for, or -1 */
57static int g_closed_id = -1;
58
59static int process_args(int argc, char** argv) {
60 int opt;
61 while ((opt = getopt(argc, argv, "V:")) != -1) {
62 switch (opt) {
63 case 'V':
64 if (!strcmp("LOG_NULL", optarg))
66 else if (!strcmp("LOG_NOTICE", optarg))
68 else if (!strcmp("LOG_INFO", optarg))
70 else if (!strcmp("LOG_ERR", optarg))
72 else if (!strcmp("LOG_DEBUG", optarg))
74 else
75 return -1;
76 break;
77 default:
78 return -1;
79 }
80 }
81 return 0;
82}
83
84static void expect_true(const char* label, int cond) {
85 if (!cond) {
86 n_log(LOG_ERR, "FAIL %s", label);
87 failures++;
88 } else {
89 n_log(LOG_INFO, "ok %s", label);
90 }
91}
92
93static void expect_eq_int(const char* label, int got, int want) {
94 if (got != want) {
95 n_log(LOG_ERR, "FAIL %s: got %d, want %d", label, got, want);
96 failures++;
97 } else {
98 n_log(LOG_INFO, "ok %s", label);
99 }
100}
101
102static void expect_eq_float(const char* label, float got, float want) {
103 float d = got - want;
104 if (d < 0.0f) d = -d;
105 if (d > 0.5f) {
106 n_log(LOG_ERR, "FAIL %s: got %f, want %f", label, (double)got, (double)want);
107 failures++;
108 } else {
109 n_log(LOG_INFO, "ok %s", label);
110 }
111}
112
113static void on_main_click(int widget_id, void* user_data) {
114 (void)widget_id;
115 (void)user_data;
117}
118
119static void on_popup_click(int widget_id, void* user_data) {
120 (void)widget_id;
121 (void)user_data;
123}
124
125static void on_popup_close(int window_id, void* user_data) {
126 (void)user_data;
127 g_closed_id = window_id;
128}
129
130/* Synthesize a left click at (x,y) coming from a given display and feed it to
131 * the GUI. Passing the display is the whole point: it is what n_gui_process_event
132 * routes on, so the same coordinates can mean different windows. */
133static void click_on(N_GUI_CTX* gui, ALLEGRO_DISPLAY* from, int x, int y) {
134 ALLEGRO_EVENT ev;
135 memset(&ev, 0, sizeof(ev));
136 ev.mouse.display = from;
137 ev.mouse.x = x;
138 ev.mouse.y = y;
139 ev.mouse.button = 1;
140 ev.type = ALLEGRO_EVENT_MOUSE_BUTTON_DOWN;
142 ev.type = ALLEGRO_EVENT_MOUSE_BUTTON_UP;
144}
145
146/* Send a Ctrl+<keycode> KEY_CHAR event, the form the textarea handler expects */
147static void ctrl_key(N_GUI_CTX* gui, ALLEGRO_DISPLAY* from, int keycode) {
148 ALLEGRO_EVENT ev;
149 memset(&ev, 0, sizeof(ev));
150 ev.type = ALLEGRO_EVENT_KEY_CHAR;
151 ev.keyboard.display = from;
152 ev.keyboard.keycode = keycode;
153 ev.keyboard.modifiers = ALLEGRO_KEYMOD_CTRL;
154 ev.keyboard.unichar = 0;
156}
157
158/* Count pixels differing from a reference colour in a w*h box at (x,y).
159 * Used to prove that something was actually rasterized into a backbuffer. */
160static int count_pixels_differing(ALLEGRO_BITMAP* bmp, int x, int y, int w, int h, ALLEGRO_COLOR ref) {
161 int differing = 0;
162 unsigned char rr, rg, rb;
163 al_unmap_rgb(ref, &rr, &rg, &rb);
164 for (int py = y; py < y + h; py++) {
165 for (int px = x; px < x + w; px++) {
166 unsigned char cr, cg, cb;
167 al_unmap_rgb(al_get_pixel(bmp, px, py), &cr, &cg, &cb);
168 /* generous tolerance: blending, sRGB conversion and driver rounding
169 all move a pixel a little without changing what it means here */
170 if (abs((int)cr - (int)rr) > 24 || abs((int)cg - (int)rg) > 24 || abs((int)cb - (int)rb) > 24)
171 differing++;
172 }
173 }
174 return differing;
175}
176
177/* Pixels sampled by sample_chrome_titlebar, -1 until it has run. */
178static int g_chrome_titlebar_lit = -1;
179
180/* Count the painted pixels in the top-left corner of a detached window, where an
181 * own-chrome title bar lives.
182 *
183 * This runs as a content-draw callback, i.e. from inside n_gui_draw_detached,
184 * after the window and its title bar have been painted into the native
185 * backbuffer and BEFORE al_flip_display. The sample has to be taken while the
186 * frame is still the current one: after a flip the backbuffer contents are
187 * undefined, and a page-flipping driver (Mesa on real hardware) hands back a
188 * discarded buffer, so an after-the-fact readback only ever worked on a
189 * copy-swapping one such as llvmpipe under Xvfb.
190 *
191 * The frame was cleared to bg_normal and the title bar fills bg_active with the
192 * title text on top, so "differs from bg_normal" is exactly the title bar. */
193static void sample_chrome_titlebar(int window_id, void* user_data) {
194 N_GUI_CTX* ctx = (N_GUI_CTX*)user_data;
195 ALLEGRO_BITMAP* target = al_get_target_bitmap();
196 N_GUI_WINDOW* win = ctx ? n_gui_get_window(ctx, window_id) : NULL;
197 if (!target || !win) return;
198 g_chrome_titlebar_lit = count_pixels_differing(target, 2, 2, 60, 12, win->theme.bg_normal);
199}
200
201/* Draw a font glyph run and a bitmap into a display's backbuffer, then read the
202 * pixels back. Proves the resources created against another display are usable
203 * here, which is the real portability risk of a second ALLEGRO_DISPLAY. */
204static void check_shared_resources(const char* who, ALLEGRO_DISPLAY* d, ALLEGRO_FONT* font, ALLEGRO_BITMAP* bmp) {
205 char label[128];
206 ALLEGRO_COLOR black = al_map_rgb(0, 0, 0);
207 ALLEGRO_BITMAP* back;
208 int lit;
209
210 al_set_target_backbuffer(d);
211 al_clear_to_color(black);
212 al_draw_text(font, al_map_rgb(255, 255, 255), 4.0f, 4.0f, 0, "NILOREA");
213 al_draw_bitmap(bmp, 4.0f, 40.0f, 0);
214 back = al_get_backbuffer(d);
215
216 /* the text: at least a few glyph pixels must have been written */
217 lit = count_pixels_differing(back, 0, 0, 120, 24, black);
218 snprintf(label, sizeof(label), "%s: shared font rasterizes (%d lit pixels)", who, lit);
219 expect_true(label, lit > 10);
220
221 /* the bitmap: the 32x32 block must be there, in its own colour */
222 lit = count_pixels_differing(back, 6, 42, 24, 24, black);
223 snprintf(label, sizeof(label), "%s: shared bitmap draws (%d lit pixels)", who, lit);
224 expect_true(label, lit > 400);
225}
226
227int main(int argc, char** argv) {
229 if (process_args(argc, argv) != 0) {
230 fprintf(stderr, "Usage: %s [-V LOG_LEVEL]\n", argv[0]);
231 return 2;
232 }
234
235 if (!al_init() || !al_init_font_addon() || !al_init_primitives_addon()) {
236 n_log(LOG_ERR, "allegro init failed");
237 return 1;
238 }
239 al_install_keyboard();
240 al_install_mouse();
241
242 al_set_new_display_flags(ALLEGRO_WINDOWED);
243 ALLEGRO_DISPLAY* main_display = al_create_display(MAIN_W, MAIN_H);
244 if (!main_display) {
245 /* headless build machine: nothing to test, and nothing broken either */
246 n_log(LOG_NOTICE, "ex_gui_multiwin: no display available, test skipped");
247 return 0;
248 }
249
250 ALLEGRO_EVENT_QUEUE* queue = al_create_event_queue();
251 ALLEGRO_FONT* font = al_create_builtin_font();
252 if (!queue || !font) {
253 n_log(LOG_ERR, "queue/font creation failed");
254 if (queue) al_destroy_event_queue(queue);
255 if (font) al_destroy_font(font);
256 al_destroy_display(main_display);
257 return 1;
258 }
259 al_register_event_source(queue, al_get_display_event_source(main_display));
260
261 /* A video bitmap created while ONLY the main display exists. Everything it
262 * is drawn onto later happens after a second display was created, which is
263 * exactly the resource-sharing case that can bite on some drivers. */
264 ALLEGRO_BITMAP* shared_bmp = al_create_bitmap(32, 32);
265 if (!shared_bmp) {
266 n_log(LOG_ERR, "al_create_bitmap failed");
267 al_destroy_event_queue(queue);
268 al_destroy_font(font);
269 al_destroy_display(main_display);
270 return 1;
271 }
272 al_set_target_bitmap(shared_bmp);
273 al_clear_to_color(al_map_rgb(255, 0, 255));
274 al_set_target_backbuffer(main_display);
275
276 N_GUI_CTX* gui = n_gui_new_ctx(font);
277 if (!gui) {
278 n_log(LOG_ERR, "n_gui_new_ctx failed");
279 al_destroy_bitmap(shared_bmp);
280 al_destroy_event_queue(queue);
281 al_destroy_font(font);
282 al_destroy_display(main_display);
283 return 1;
284 }
285 n_gui_set_display(gui, main_display);
286 n_gui_set_display_size(gui, (float)MAIN_W, (float)MAIN_H);
288 expect_true("event queue round-trips", n_gui_get_event_queue(gui) == queue);
289
290 /* Two frameless windows both anchored at (0,0) with a button at the same
291 * spot. Once one of them is detached they live on different displays, so
292 * the identical coordinates are the sharpest possible routing test. */
293 int main_win = n_gui_add_window(gui, "Main Panel", 0.0f, 0.0f, 300.0f, 200.0f);
294 int popup_win = n_gui_add_window(gui, "Popup", 0.0f, 0.0f, (float)POPUP_W, (float)POPUP_H);
295 expect_true("windows created", main_win >= 0 && popup_win >= 0);
298 int main_btn = n_gui_add_button(gui, main_win, "main", 10.0f, 10.0f, 100.0f, 30.0f, N_GUI_SHAPE_RECT, on_main_click, NULL);
299 int popup_btn = n_gui_add_button(gui, popup_win, "popup", 10.0f, 10.0f, 100.0f, 30.0f, N_GUI_SHAPE_RECT, on_popup_click, NULL);
300 int popup_txt = n_gui_add_textarea(gui, popup_win, 10.0f, 60.0f, 200.0f, 24.0f, 0, 64, NULL, NULL);
301 expect_true("widgets created", main_btn >= 0 && popup_btn >= 0 && popup_txt >= 0);
302
303 /* baseline: still a pop-up, both windows on the main display */
304 expect_eq_int("nothing detached initially", n_gui_count_detached(gui), 0);
305 expect_true("popup not detached initially", !n_gui_window_is_detached(gui, popup_win));
306
307 /* DETACH */
308 expect_eq_int("detach succeeds", n_gui_window_detach(gui, popup_win, N_GUI_DETACH_RESIZABLE), 0);
309 ALLEGRO_DISPLAY* popup_display = n_gui_window_get_display(gui, popup_win);
310 expect_true("native display created", popup_display != NULL);
311 expect_true("native display differs from the main one", popup_display != main_display);
312 expect_eq_int("one window detached", n_gui_count_detached(gui), 1);
313 expect_eq_int("detached flag set", n_gui_window_is_detached(gui, popup_win), 1);
314 expect_eq_int("display maps back to the window", n_gui_window_from_display(gui, popup_display), popup_win);
315 expect_eq_int("main display maps to no detached window", n_gui_window_from_display(gui, main_display), -1);
316
317 if (!popup_display) {
318 n_log(LOG_ERR, "no native display, cannot continue");
320 al_destroy_bitmap(shared_bmp);
321 al_destroy_event_queue(queue);
322 al_destroy_font(font);
323 al_destroy_display(main_display);
324 return 1;
325 }
326
327 /* the detached window fills its display, at the origin, without our chrome */
328 {
329 N_GUI_WINDOW* w = n_gui_get_window(gui, popup_win);
330 expect_true("detached window sits at the origin", w && w->x == 0.0f && w->y == 0.0f);
331 expect_eq_float("detached window width matches the display", w ? w->w : -1.0f, (float)al_get_display_width(popup_display));
332 expect_eq_float("detached window height matches the display", w ? w->h : -1.0f, (float)al_get_display_height(popup_display));
333 expect_true("detached window is frameless", w && (w->flags & N_GUI_WIN_FRAMELESS));
334 }
335
336 /* SHARED RESOURCES: font glyphs and a bitmap created before the second
337 display existed must still rasterize on both displays. */
338 check_shared_resources("main display", main_display, font, shared_bmp);
339 check_shared_resources("native display", popup_display, font, shared_bmp);
340
341 /* EVENT ROUTING: identical coordinates, different display, different window */
343 click_on(gui, main_display, 30, 25);
344 expect_eq_int("click on the main display hits the main button", g_main_clicks, 1);
345 expect_eq_int("click on the main display does not reach the detached window", g_popup_clicks, 0);
346
348 click_on(gui, popup_display, 30, 25);
349 expect_eq_int("click on the native display hits the detached button", g_popup_clicks, 1);
350 expect_eq_int("click on the native display does not reach the main window", g_main_clicks, 0);
351
352 /* an event with no display (timers, and the headless tests that synthesize
353 events with memset) still reaches the main display's windows */
355 click_on(gui, NULL, 30, 25);
356 expect_eq_int("display-less event falls back to the main pass", g_main_clicks, 1);
357 expect_eq_int("display-less event does not reach the detached window", g_popup_clicks, 0);
358
359 /* n_gui_wants_mouse answers for the main display only */
360 expect_eq_int("wants_mouse still answers for the main display", n_gui_wants_mouse(gui), 1);
361
362 /* CLIPBOARD: the context follows the display input came from, so a copy
363 made inside the detached window addresses that display. */
364 {
365 const char* payload = "nilorea-multiwin-clipboard";
366 n_gui_textarea_set_text(gui, popup_txt, payload);
367 n_gui_set_focus(gui, popup_txt);
368
369 /* an event from the native display makes it the active one */
370 click_on(gui, popup_display, 30, 25);
371 n_gui_set_focus(gui, popup_txt);
372 expect_true("active display follows the native window", gui->active_display == popup_display);
373
374 ctrl_key(gui, popup_display, ALLEGRO_KEY_A); /* select all */
375 ctrl_key(gui, popup_display, ALLEGRO_KEY_C); /* copy */
376
377 /* read back through whichever backend the GUI wrote with */
378 char* got = NULL;
379 if (n_clipboard_available()) {
381 expect_true("clipboard copy from the detached window round-trips",
382 got != NULL && strcmp(got, payload) == 0);
383 if (got) {
384 if (strcmp(got, payload) != 0)
385 n_log(LOG_ERR, "clipboard: got '%s', want '%s'", got, payload);
386 free(got);
387 }
388 } else {
389 char* al_got = al_get_clipboard_text(popup_display);
390 expect_true("clipboard copy from the detached window round-trips",
391 al_got != NULL && strcmp(al_got, payload) == 0);
392 if (al_got) {
393 if (strcmp(al_got, payload) != 0)
394 n_log(LOG_ERR, "clipboard: got '%s', want '%s'", al_got, payload);
395 al_free(al_got);
396 }
397 }
398 n_gui_set_focus(gui, -1);
399 }
400
401 /* DRAWING: the main pass must not touch the native display, and the native
402 pass must not touch the main one. Both are exercised for real here. */
403 al_set_target_backbuffer(main_display);
404 al_clear_to_color(al_map_rgb(0, 0, 0));
406 {
407 /* the detached window drew nothing on the main display: its button
408 would have landed in this box if the pass filter were missing */
409 ALLEGRO_BITMAP* back = al_get_backbuffer(main_display);
410 int lit = count_pixels_differing(back, 320, 300, 40, 40, al_map_rgb(0, 0, 0));
411 expect_eq_int("main pass leaves the far corner untouched", lit, 0);
412 }
414 expect_eq_int("draw_detached keeps the window alive", n_gui_count_detached(gui), 1);
415 /* the caller's target survives a detached draw pass */
416 al_set_target_backbuffer(main_display);
418 expect_true("draw_detached restores the caller's target", al_get_target_bitmap() == al_get_backbuffer(main_display));
419
420 /* CLOSE REQUEST from the window manager: the callback runs, and the display
421 is only released at the next n_gui_draw_detached. */
422 {
423 ALLEGRO_EVENT ev;
425 g_closed_id = -1;
426 memset(&ev, 0, sizeof(ev));
427 ev.type = ALLEGRO_EVENT_DISPLAY_CLOSE;
428 ev.display.source = popup_display;
429 expect_eq_int("DISPLAY_CLOSE is consumed", n_gui_process_event(gui, ev), 1);
430 expect_eq_int("close callback fired for the right window", g_closed_id, popup_win);
431 expect_eq_int("display survives until the next draw pass", n_gui_count_detached(gui), 1);
433 expect_eq_int("display released by the deferred pass", n_gui_count_detached(gui), 0);
434 expect_eq_int("window is closed", n_gui_window_is_open(gui, popup_win), 0);
435 /* geometry fell back to the pop-up one */
436 N_GUI_WINDOW* w = n_gui_get_window(gui, popup_win);
437 expect_eq_float("pop-up width restored", w ? w->w : -1.0f, (float)POPUP_W);
438 expect_eq_float("pop-up height restored", w ? w->h : -1.0f, (float)POPUP_H);
439 }
440
441 /* RE-OPEN: the detach preference survived the close, so opening the window
442 brings the native window back rather than a pop-up. */
443 n_gui_open_window(gui, popup_win);
444 expect_eq_int("re-opening restores the native window", n_gui_count_detached(gui), 1);
445 popup_display = n_gui_window_get_display(gui, popup_win);
446 expect_true("a fresh native display was created", popup_display != NULL);
447
448 /* ATTACH: back to a pop-up, with the original geometry and flags */
449 expect_eq_int("attach succeeds", n_gui_window_attach(gui, popup_win), 0);
450 expect_eq_int("nothing detached after attach", n_gui_count_detached(gui), 0);
451 {
452 N_GUI_WINDOW* w = n_gui_get_window(gui, popup_win);
453 expect_eq_float("pop-up x restored", w ? w->x : -1.0f, 0.0f);
454 expect_eq_float("pop-up y restored", w ? w->y : -1.0f, 0.0f);
455 expect_eq_float("pop-up w restored", w ? w->w : -1.0f, (float)POPUP_W);
456 expect_eq_float("pop-up h restored", w ? w->h : -1.0f, (float)POPUP_H);
457 }
458 /* and it is hit-testable on the main display again: it is on top of the
459 main panel, so it takes the click */
461 click_on(gui, main_display, 30, 25);
462 expect_eq_int("re-attached pop-up takes clicks on the main display", g_popup_clicks, 1);
463
464 /* OWN CHROME (N_GUI_DETACH_OWN_CHROME): the window keeps drawing its own
465 title bar inside a frameless native window, and that chrome now drives
466 the OS window instead of x/y/w/h. */
467 {
468 int chrome_win = n_gui_add_window(gui, "Chrome", 0.0f, 0.0f, 260.0f, 180.0f);
469 expect_true("own-chrome window created", chrome_win >= 0);
471 expect_eq_int("detach with own chrome succeeds",
473 ALLEGRO_DISPLAY* cd = n_gui_window_get_display(gui, chrome_win);
474 expect_true("own-chrome native display created", cd != NULL);
475
476 N_GUI_WINDOW* cw = n_gui_get_window(gui, chrome_win);
477 /* the key difference from the default: our title bar stays */
478 expect_true("own-chrome window is NOT forced frameless", cw && !(cw->flags & N_GUI_WIN_FRAMELESS));
479 /* Allegro documents ALLEGRO_FRAMELESS as varying by platform, and with no
480 window manager running (bare Xvfb) there are no decorations to remove
481 in the first place, so the flag does not come back. What N_GUI owns is
482 the request, not the window manager's answer: report, do not fail. */
483 if (cd && !(al_get_display_flags(cd) & ALLEGRO_FRAMELESS))
484 n_log(LOG_NOTICE, "platform did not honour ALLEGRO_FRAMELESS (no window manager?), check skipped");
485 expect_true("own-chrome native window is resizable (needed by resize/maximize)",
486 cd && (al_get_display_flags(cd) & ALLEGRO_RESIZABLE));
487 /* the display covers the WHOLE window, title bar included, unlike the
488 default where it covers the body only */
489 expect_eq_float("own-chrome display height covers the title bar too",
490 cd ? (float)al_get_display_height(cd) : -1.0f, 180.0f);
491 expect_true("own-chrome window keeps a title bar height", cw && cw->titlebar_h > 0.0f);
492
493 /* the OS window carries the window's title */
494 expect_true("native window title was set before creation", cd != NULL);
495
496 /* dragging the title bar moves the OS window, not win->x/y */
497 if (cd && cw) {
498 int px0 = 0, py0 = 0, px1 = 0, py1 = 0;
499 ALLEGRO_EVENT ev;
500 al_get_window_position(cd, &px0, &py0);
501
502 memset(&ev, 0, sizeof(ev));
503 ev.mouse.display = cd;
504 ev.mouse.button = 1;
505 ev.mouse.x = 60;
506 ev.mouse.y = (int)(cw->titlebar_h / 2.0f);
507 ev.type = ALLEGRO_EVENT_MOUSE_BUTTON_DOWN;
509 expect_true("title bar press starts a drag", (cw->state & N_GUI_WIN_DRAGGING) != 0);
510
511 /* An own-chrome drag anchors on the DESKTOP cursor and moves the OS
512 window by how far the pointer has travelled on the desktop, not by a
513 display-local delta (see _native_drag_begin: a display-local delta
514 cannot work for a window that is itself moving). So a synthetic
515 MOUSE_AXES on its own moves nothing, the real pointer has to travel
516 with it. Warp it, then assert the window followed by the same amount.
517 Where the platform will not warp the pointer there is nothing to
518 measure, so the check is skipped rather than failed. */
519 int cbx = 0, cby = 0, cax = 0, cay = 0;
520 int warped = al_get_mouse_cursor_position(&cbx, &cby) &&
521 al_set_mouse_xy(cd, 90, (int)(cw->titlebar_h / 2.0f)) &&
522 al_get_mouse_cursor_position(&cax, &cay);
523
524 ev.type = ALLEGRO_EVENT_MOUSE_AXES;
525 ev.mouse.x = 90; /* +30 px to the right */
527 al_get_window_position(cd, &px1, &py1);
528 if (warped && cax != cbx)
529 expect_eq_int("title bar drag moved the OS window", px1 - px0, cax - cbx);
530 else
531 n_log(LOG_NOTICE, "ex_gui_multiwin: pointer cannot be warped here, skipping the OS-window drag check");
532 expect_eq_float("title bar drag left the window pinned at the origin", cw->x, 0.0f);
533
534 ev.type = ALLEGRO_EVENT_MOUSE_BUTTON_UP;
536 expect_true("drag ends on release", !(cw->state & N_GUI_WIN_DRAGGING));
537 }
538
539 /* minimise shrinks the OS window to the title bar and back.
540 ALLEGRO_MINIMIZED is read-only, so this is the portable mapping. */
541 if (cd && cw) {
542 float full_h = cw->h;
543 n_gui_minimize_window(gui, chrome_win);
544 expect_true("minimised flag set", (cw->state & N_GUI_WIN_MINIMISED) != 0);
545 expect_eq_float("minimise shrank the OS window to the title bar",
546 (float)al_get_display_height(cd), cw->titlebar_h);
547 expect_eq_float("minimise kept the full window height for the restore", cw->h, full_h);
548 n_gui_restore_window(gui, chrome_win);
549 expect_true("minimised flag cleared", !(cw->state & N_GUI_WIN_MINIMISED));
550 expect_eq_float("restore grew the OS window back",
551 (float)al_get_display_height(cd), full_h);
552 }
553
554 /* The same thing again, but driven through the title bar BUTTON and with the
555 flag set an application actually uses (RESIZABLE alongside OWN_CHROME).
556 n_gui_minimize_window is the API path; the button goes through
557 _tb_button_action, and a resizable native window is the configuration a
558 host detaches with, so neither was covered by the checks above. */
559 if (cd && cw) {
560 ALLEGRO_EVENT ev;
561 float full_h2;
562 float sz, mid_x;
563 n_gui_window_set_flags(gui, chrome_win,
565 /* A real minimum size, which is what an application sets so its layout
566 always has room. It is also what made minimise look broken on a real
567 desktop: the constraint is handed to the window manager at detach time
568 and a title bar is far below it, so the shrink was refused and the
569 window stayed full size while its content hid. Note this check cannot
570 reproduce that here: window constraints are enforced by the window
571 manager, and under bare Xvfb there is none, so the shrink succeeds
572 either way. What it does cover is the title-bar BUTTON path on a
573 RESIZABLE own-chrome window, which nothing else exercises. */
574 cw->min_w = 200.0f;
575 cw->min_h = 150.0f;
576 if (al_set_window_constraints(cd, 200, 150, 0, 0))
577 al_apply_window_constraints(cd, true);
578 n_gui_restore_window(gui, chrome_win);
579 full_h2 = cw->h;
580
581 /* buttons run right to left: close, maximize, minimize */
582 sz = gui->style.tb_btn_size > 0.0f ? gui->style.tb_btn_size : cw->titlebar_h - 4.0f;
583 mid_x = cw->x + cw->w - gui->style.tb_btn_right_margin - 2.0f * (sz + gui->style.tb_btn_spacing) - sz / 2.0f;
584 memset(&ev, 0, sizeof(ev));
585 ev.mouse.display = cd;
586 ev.mouse.button = 1;
587 ev.mouse.x = (int)mid_x;
588 ev.mouse.y = (int)(cw->y + cw->titlebar_h / 2.0f);
589 ev.type = ALLEGRO_EVENT_MOUSE_BUTTON_DOWN;
591 ev.type = ALLEGRO_EVENT_MOUSE_BUTTON_UP;
593
594 expect_true("title bar minimise button set the minimised flag",
595 (cw->state & N_GUI_WIN_MINIMISED) != 0);
596 expect_eq_float("title bar minimise shrank the OS window to the title bar",
597 (float)al_get_display_height(cd), cw->titlebar_h);
598 n_gui_restore_window(gui, chrome_win);
599 expect_eq_float("restore grew the resizable OS window back",
600 (float)al_get_display_height(cd), full_h2);
601 }
602
603 /* the resize grip drives al_resize_display */
604 if (cd && cw) {
605 ALLEGRO_EVENT ev;
606 float grip = gui->style.grip_size + 2.0f;
607 memset(&ev, 0, sizeof(ev));
608 ev.mouse.display = cd;
609 ev.mouse.button = 1;
610 ev.mouse.x = (int)(cw->w - grip / 2.0f);
611 ev.mouse.y = (int)(cw->h - grip / 2.0f);
612 ev.type = ALLEGRO_EVENT_MOUSE_BUTTON_DOWN;
614 expect_true("grip press starts a resize", (cw->state & N_GUI_WIN_RESIZING) != 0);
615
616 ev.type = ALLEGRO_EVENT_MOUSE_AXES;
617 ev.mouse.x += 40;
618 ev.mouse.y += 20;
620 expect_eq_float("grip resized the OS window (width)", (float)al_get_display_width(cd), 300.0f);
621 expect_eq_float("grip resized the OS window (height)", (float)al_get_display_height(cd), 200.0f);
622
623 ev.type = ALLEGRO_EVENT_MOUSE_BUTTON_UP;
625 expect_true("resize ends on release", !(cw->state & N_GUI_WIN_RESIZING));
626 }
627
628 /* window constraints were pushed to the WM so it cannot shrink the
629 window below what the layout can render */
630 if (cd && cw) {
631 int cmin_w = 0, cmin_h = 0, cmax_w = 0, cmax_h = 0;
632 if (al_get_window_constraints(cd, &cmin_w, &cmin_h, &cmax_w, &cmax_h)) {
633 expect_eq_int("min width constraint pushed to the window manager", cmin_w, (int)(cw->min_w + 0.5f));
634 expect_eq_int("min height constraint pushed to the window manager", cmin_h, (int)(cw->min_h + 0.5f));
635 } else {
636 n_log(LOG_NOTICE, "window constraints unsupported on this platform, check skipped");
637 }
638 }
639
640 /* an icon can be given to the OS window (borrowed bitmap) */
641 {
642 ALLEGRO_BITMAP* icons[1];
643 icons[0] = shared_bmp;
644 int refused;
645 expect_eq_int("native icon accepted", n_gui_window_set_native_icons(gui, chrome_win, icons, 1), 0);
646 /* expected to fail, mute the error it logs on the way out */
648 refused = n_gui_window_set_native_icons(gui, main_win, icons, 1);
650 expect_eq_int("native icon refused for a pop-up", refused, -1);
651 }
652
653 /* HALT_DRAWING is acknowledged and suppresses drawing until RESUME */
654 if (cd) {
655 ALLEGRO_EVENT ev;
656 memset(&ev, 0, sizeof(ev));
657 ev.type = ALLEGRO_EVENT_DISPLAY_HALT_DRAWING;
658 ev.display.source = cd;
659 expect_eq_int("HALT_DRAWING is consumed", n_gui_process_event(gui, ev), 1);
660 expect_eq_int("halted window reports itself hidden", n_gui_window_native_is_hidden(gui, chrome_win), 1);
661 n_gui_draw_detached(gui); /* must not touch the halted display */
662 ev.type = ALLEGRO_EVENT_DISPLAY_RESUME_DRAWING;
663 expect_eq_int("RESUME_DRAWING is consumed", n_gui_process_event(gui, ev), 1);
664 expect_eq_int("resumed window is drawable again", n_gui_window_native_is_hidden(gui, chrome_win), 0);
665 }
666
667 /* its own chrome draws into its own display: the title bar is at the
668 top of the native window, which the default detach mode has no room
669 for at all. The pixels are counted from inside the frame, see
670 sample_chrome_titlebar for why a readback after the flip is not
671 portable. */
672 if (cd && cw) {
676 n_gui_window_set_content_draw_callback(gui, chrome_win, NULL, NULL);
677 expect_true("own chrome drew a title bar into the native window", g_chrome_titlebar_lit > 10);
678 }
679
680 /* maximise drives the OS window through ALLEGRO_MAXIMIZED. Whether the
681 window manager honours it is its business, so only our bookkeeping is
682 asserted here, the granted geometry arrives as a DISPLAY_RESIZE. */
683 if (cw) {
684 n_gui_maximize_window(gui, chrome_win);
685 expect_true("maximise sets the maximised flag", (cw->state & N_GUI_WIN_MAXIMISED) != 0);
686 n_gui_maximize_window(gui, chrome_win);
687 expect_true("maximise toggles back off", !(cw->state & N_GUI_WIN_MAXIMISED));
688 }
689
690 /* attach restores the pop-up geometry AND the pop-up flags */
691 expect_eq_int("own-chrome attach succeeds", n_gui_window_attach(gui, chrome_win), 0);
692 {
693 N_GUI_WINDOW* w = n_gui_get_window(gui, chrome_win);
694 expect_eq_float("own-chrome pop-up w restored", w ? w->w : -1.0f, 260.0f);
695 expect_eq_float("own-chrome pop-up h restored", w ? w->h : -1.0f, 180.0f);
696 expect_true("own-chrome pop-up flags restored",
697 w && (w->flags & N_GUI_WIN_RESIZABLE) && !(w->flags & N_GUI_WIN_FRAMELESS));
698 }
699 n_gui_close_window(gui, chrome_win);
700 }
701
702#ifdef HAVE_CJSON
703 /* LAYOUT PERSISTENCE: the detached state and the native geometry survive a
704 save/destroy/rebuild/load cycle. */
705 {
706 const char* path = "ex_gui_multiwin_layout.json";
707 expect_eq_int("re-detach for the save", n_gui_window_detach(gui, popup_win, N_GUI_DETACH_RESIZABLE), 0);
708 expect_eq_int("layout saved", n_gui_save_layout_json(gui, path), 0);
709
710 float want_w = 0.0f, want_h = 0.0f;
711 {
712 ALLEGRO_DISPLAY* d = n_gui_window_get_display(gui, popup_win);
713 want_w = (float)al_get_display_width(d);
714 want_h = (float)al_get_display_height(d);
715 }
716
718 al_set_target_backbuffer(main_display);
719
720 gui = n_gui_new_ctx(font);
721 expect_true("second context created", gui != NULL);
722 if (gui) {
723 n_gui_set_display(gui, main_display);
724 n_gui_set_display_size(gui, (float)MAIN_W, (float)MAIN_H);
726 int w2_main = n_gui_add_window(gui, "Main Panel", 0.0f, 0.0f, 300.0f, 200.0f);
727 int w2_popup = n_gui_add_window(gui, "Popup", 0.0f, 0.0f, (float)POPUP_W, (float)POPUP_H);
728 expect_true("rebuilt windows", w2_main >= 0 && w2_popup >= 0);
729 expect_eq_int("layout loaded", n_gui_load_layout_json(gui, path), 0);
730 expect_eq_int("loaded layout re-created the native window", n_gui_window_is_detached(gui, w2_popup), 1);
731 ALLEGRO_DISPLAY* d2 = n_gui_window_get_display(gui, w2_popup);
732 expect_true("restored native display exists", d2 != NULL);
733 if (d2) {
734 expect_eq_float("restored native width", (float)al_get_display_width(d2), want_w);
735 expect_eq_float("restored native height", (float)al_get_display_height(d2), want_h);
736 }
737 /* the pop-up fallback geometry came back too, not the native size */
738 N_GUI_WINDOW* w = n_gui_get_window(gui, w2_popup);
739 expect_eq_float("restored pop-up fallback width", w ? w->saved_w : -1.0f, (float)POPUP_W);
740 expect_eq_float("restored pop-up fallback height", w ? w->saved_h : -1.0f, (float)POPUP_H);
741 }
742 remove(path);
743 }
744#endif
745
746 /* the context owns any display still attached: this must not leak one */
748 al_set_target_backbuffer(main_display);
749 al_destroy_bitmap(shared_bmp);
750 al_destroy_event_queue(queue);
751 al_destroy_font(font);
752 al_destroy_display(main_display);
753
754 if (failures) {
755 n_log(LOG_ERR, "ex_gui_multiwin: %d failure(s)", failures);
756 return 1;
757 }
758 n_log(LOG_NOTICE, "ex_gui_multiwin: all checks passed");
759 return 0;
760}
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_main_click(int widget_id, void *user_data)
static int g_chrome_titlebar_lit
static void expect_eq_float(const char *label, float got, float want)
static void on_popup_click(int widget_id, void *user_data)
#define MAIN_W
static int g_main_clicks
#define MAIN_H
static void ctrl_key(N_GUI_CTX *gui, ALLEGRO_DISPLAY *from, int keycode)
static int g_closed_id
static int count_pixels_differing(ALLEGRO_BITMAP *bmp, int x, int y, int w, int h, ALLEGRO_COLOR ref)
static void expect_eq_int(const char *label, int got, int want)
static void check_shared_resources(const char *who, ALLEGRO_DISPLAY *d, ALLEGRO_FONT *font, ALLEGRO_BITMAP *bmp)
#define POPUP_H
static int g_popup_clicks
static void sample_chrome_titlebar(int window_id, void *user_data)
#define POPUP_W
static void on_popup_close(int window_id, void *user_data)
static void click_on(N_GUI_CTX *gui, ALLEGRO_DISPLAY *from, int x, int y)
float x
position x on screen
Definition n_gui.h:1005
float titlebar_h
title bar height
Definition n_gui.h:1013
float w
window width
Definition n_gui.h:1009
float tb_btn_size
titlebar button size (0 = auto: titlebar_h - 4)
Definition n_gui.h:1162
N_GUI_THEME theme
color theme for this window chrome
Definition n_gui.h:1021
float saved_w
embedded width saved by n_gui_window_detach, restored by n_gui_window_attach
Definition n_gui.h:1100
float saved_h
embedded height saved by n_gui_window_detach, restored by n_gui_window_attach
Definition n_gui.h:1102
float grip_size
grip area size
Definition n_gui.h:1204
float min_w
minimum width
Definition n_gui.h:1029
float h
window height
Definition n_gui.h:1011
int state
state flags (N_GUI_WIN_*)
Definition n_gui.h:1015
int flags
feature flags (N_GUI_WIN_AUTO_SCROLLBAR, N_GUI_WIN_RESIZABLE, etc.)
Definition n_gui.h:1017
N_GUI_STYLE style
configurable style (sizes, colours, paddings)
Definition n_gui.h:1424
float min_h
minimum height
Definition n_gui.h:1031
float tb_btn_spacing
gap between titlebar buttons
Definition n_gui.h:1164
ALLEGRO_COLOR bg_normal
background normal
Definition n_gui.h:343
float tb_btn_right_margin
right margin from window edge to rightmost button
Definition n_gui.h:1166
float y
position y on screen
Definition n_gui.h:1007
ALLEGRO_DISPLAY * active_display
display that last received input, used to pick the display for clipboard and mouse-cursor calls.
Definition n_gui.h:1468
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_textarea_set_text(N_GUI_CTX *ctx, int widget_id, const char *text)
set the text content of a textarea widget
Definition n_gui.c:3263
int n_gui_save_layout_json(N_GUI_CTX *ctx, const char *filepath)
save the geometry + persistent state of every window to a JSON file
Definition n_gui.c:12712
int n_gui_window_detach(N_GUI_CTX *ctx, int window_id, int detach_flags)
Promote a pop-up window to a native OS window.
Definition n_gui.c:1409
void n_gui_set_display(N_GUI_CTX *ctx, ALLEGRO_DISPLAY *display)
Set the display pointer for clipboard operations (copy/paste).
Definition n_gui.c:9071
int n_gui_wants_mouse(N_GUI_CTX *ctx)
Check if the mouse is currently over any open GUI window.
Definition n_gui.c:12337
int n_gui_window_native_is_hidden(N_GUI_CTX *ctx, int window_id)
Check whether a detached window is currently not drawable.
Definition n_gui.c:1644
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_window_get_flags(N_GUI_CTX *ctx, int window_id)
Get feature flags of a window.
Definition n_gui.c:1977
void n_gui_minimize_window(N_GUI_CTX *ctx, int window_id)
Minimise a window (show title bar only)
Definition n_gui.c:1224
void n_gui_close_window(N_GUI_CTX *ctx, int window_id)
Close (hide) a window.
Definition n_gui.c:1191
void n_gui_maximize_window(N_GUI_CTX *ctx, int window_id)
Toggle maximised state (full display or restore to previous size).
Definition n_gui.c:1267
ALLEGRO_EVENT_QUEUE * n_gui_get_event_queue(N_GUI_CTX *ctx)
Get the event queue previously given to n_gui_set_event_queue.
Definition n_gui.c:1365
void n_gui_set_focus(N_GUI_CTX *ctx, int widget_id)
Set keyboard focus to a specific widget.
Definition n_gui.c:3105
#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
#define N_GUI_DETACH_RESIZABLE
the native window can be resized by the user through the OS window manager
Definition n_gui.h:250
int n_gui_window_is_detached(N_GUI_CTX *ctx, int window_id)
Check whether a window currently lives in a native OS window.
Definition n_gui.c:1575
void n_gui_draw(N_GUI_CTX *ctx)
Draw all visible windows and their widgets.
Definition n_gui.c:8542
#define N_GUI_WIN_MINIMISED
window is minimised (title bar only)
Definition n_gui.h:213
int n_gui_window_is_open(N_GUI_CTX *ctx, int window_id)
Check if a window is currently visible.
Definition n_gui.c:1959
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
int n_gui_window_attach(N_GUI_CTX *ctx, int window_id)
Return a detached window to being an in-display pop-up.
Definition n_gui.c:1542
void n_gui_draw_detached(N_GUI_CTX *ctx)
Render and flip every detached window, and release the ones whose native window was closed.
Definition n_gui.c:8737
N_GUI_CTX * n_gui_new_ctx(ALLEGRO_FONT *default_font)
Create a new GUI context.
Definition n_gui.c:903
#define N_GUI_WIN_MAXIMISED
window is maximised (full display size)
Definition n_gui.h:223
void n_gui_window_set_close_callback(N_GUI_CTX *ctx, int window_id, void(*on_close)(int, void *), void *user_data)
Set the close button callback for a window.
Definition n_gui.c:1655
#define N_GUI_DETACH_OWN_CHROME
keep N_GUI's own window chrome instead of the window manager's: the native window is created frameles...
Definition n_gui.h:266
void n_gui_destroy_ctx(N_GUI_CTX **ctx)
Destroy a GUI context and all its windows/widgets.
Definition n_gui.c:995
void n_gui_restore_window(N_GUI_CTX *ctx, int window_id)
Restore a minimised window to its full size.
Definition n_gui.c:1242
void n_gui_open_window(N_GUI_CTX *ctx, int window_id)
Open (show) a window.
Definition n_gui.c:1206
ALLEGRO_DISPLAY * n_gui_window_get_display(N_GUI_CTX *ctx, int window_id)
Get the native display backing a window, or NULL when it is a pop-up.
Definition n_gui.c:1584
void n_gui_window_set_content_draw_callback(N_GUI_CTX *ctx, int window_id, void(*on_content_draw)(int, void *), void *user_data)
Set the content-draw callback for a window.
Definition n_gui.c:1670
int n_gui_window_set_native_icons(N_GUI_CTX *ctx, int window_id, ALLEGRO_BITMAP **icons, int num_icons)
Give a detached window's OS window a taskbar / title bar icon.
Definition n_gui.c:1622
int n_gui_count_detached(N_GUI_CTX *ctx)
Count the windows currently detached into native OS windows.
Definition n_gui.c:1609
int n_gui_load_layout_json(N_GUI_CTX *ctx, const char *filepath)
load a JSON window layout file and apply it to matching windows
Definition n_gui.c:12870
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_WIN_BTN_ALL
enable all three title bar buttons (minimize + maximize + close)
Definition n_gui.h:241
#define N_GUI_WIN_RESIZING
window is being resized
Definition n_gui.h:217
int n_gui_window_from_display(N_GUI_CTX *ctx, ALLEGRO_DISPLAY *display)
Find the window detached into a given display.
Definition n_gui.c:1596
int n_gui_add_textarea(N_GUI_CTX *ctx, int window_id, float x, float y, float w, float h, int multiline, size_t char_limit, void(*on_change)(int, const char *, void *), void *user_data)
Add a text area widget.
Definition n_gui.c:2498
#define N_GUI_SHAPE_RECT
rectangle shape (default)
Definition n_gui.h:147
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
void n_gui_set_event_queue(N_GUI_CTX *ctx, ALLEGRO_EVENT_QUEUE *queue)
Set the event queue used to register native window event sources.
Definition n_gui.c:1356
#define N_GUI_WIN_RESIZABLE
enable user-resizable window with a drag handle at bottom-right
Definition n_gui.h:229
#define N_GUI_WIN_DRAGGING
window is being dragged
Definition n_gui.h:215
The top-level GUI context that holds all windows.
Definition n_gui.h:1354
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
int n_clipboard_available(void)
Whether a real clipboard backend (X11 or Win32) is active on this platform.
char * n_clipboard_get(int which)
Fetch the current text of which selection from its owner.
System clipboard: full X11 selection support on Linux, Win32 clipboard on Windows/MinGW.
#define N_CLIPBOARD_CLIPBOARD
the Ctrl+C / Ctrl+V clipboard selection
Definition n_clipboard.h:56
Common headers and low-level functions & define.
GUI system: buttons, sliders, text areas, checkboxes, scrollbars, dropdown menus, windows.
Generic log system.