Nilorea Library
C utilities for networking, threading, graphics
Loading...
Searching...
No Matches
n_gui.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#include "nilorea/n_gui.h"
29#include "nilorea/n_clipboard.h"
30
31#include <limits.h> /* INT_MIN, the "native window position unknown" marker */
32
33#ifdef HAVE_CJSON
34#include "cJSON.h"
35#endif
36
37/* Clipboard helpers: prefer the n_clipboard backend (full X11 target negotiation so
38 * strict apps like Chromium can paste, plus the PRIMARY selection for select-to-copy /
39 * middle-click), and fall back to Allegro's clipboard (CLIPBOARD only) where n_clipboard
40 * has no backend (non-X11). */
41static void _ngui_clip_set(ALLEGRO_DISPLAY* display, int which, const char* text) {
43 n_clipboard_set(which, text);
44 return;
45 }
46 if (which == N_CLIPBOARD_CLIPBOARD && display && text)
47 al_set_clipboard_text(display, text);
48}
49
50/* Returns a malloc'd string (free with al_free/free) or NULL. n_clipboard_get returns a
51 * malloc'd buffer; al_get_clipboard_text returns an al_malloc'd buffer - both are freed
52 * with al_free on this path, so the caller frees uniformly with al_free. */
53static char* _ngui_clip_get(ALLEGRO_DISPLAY* display, int which) {
55 char* s = n_clipboard_get(which);
56 if (s) {
57 char* a = al_malloc(strlen(s) + 1); /* re-home onto al_malloc so callers al_free uniformly */
58 if (a)
59 strcpy(a, s);
60 free(s);
61 return a;
62 }
63 return NULL;
64 }
65 return (which == N_CLIPBOARD_CLIPBOARD && display) ? al_get_clipboard_text(display) : NULL;
66}
67
68/* Duration (seconds) of the "pressed" visual flash applied to a button when
69 * it is activated through its bound keyboard shortcut. Long enough for the
70 * user to perceive a press, short enough to feel snappy. */
71#define N_GUI_KEY_PRESS_FLASH_SEC 0.12
72
73/* Small tail (seconds) added to an animation deadline in ctx->anim_until so
74 * n_gui_needs_redraw keeps returning non-zero just past the deadline, which
75 * guarantees the one frame that renders the post-animation state (the button
76 * back to normal, the tooltip bubble revealed) is actually drawn before an
77 * event-driven host is allowed to stop redrawing. */
78#define N_GUI_ANIM_TAIL_SEC 0.05
79
80/* INTERNAL HELPERS */
81
83static double _clamp(double v, double lo, double hi) {
84 if (v < lo) return lo;
85 if (v > hi) return hi;
86 return v;
87}
88
92static double _slider_snap_value(double val, double min_val, double max_val, double step) {
93 if (step <= 0.0) step = 1.0;
94 double steps = round((val - min_val) / step);
95 double snapped = min_val + steps * step;
96 if (snapped < min_val) snapped = min_val;
97 if (snapped > max_val) snapped = max_val;
98 return snapped;
99}
100
103N_GUI_TEXT_DIMS n_gui_get_text_dims(ALLEGRO_FONT* font, const char* text) {
104 N_GUI_TEXT_DIMS d = {0, 0, 0, 0};
105 if (font && text) al_get_text_dimensions(font, text, &d.x, &d.y, &d.w, &d.h);
106 return d;
107}
108
110static int _is_focusable_type(int type) {
111 return type == N_GUI_TYPE_TEXTAREA ||
112 type == N_GUI_TYPE_SLIDER ||
113 type == N_GUI_TYPE_CHECKBOX ||
114 type == N_GUI_TYPE_LISTBOX ||
115 type == N_GUI_TYPE_RADIOLIST ||
116 type == N_GUI_TYPE_COMBOBOX ||
117 type == N_GUI_TYPE_SCROLLBAR ||
118 type == N_GUI_TYPE_DROPMENU;
119}
120
123 if (ctx->focused_widget_id < 0) return NULL;
124 list_foreach(wnode, ctx->windows) {
125 N_GUI_WINDOW* win = (N_GUI_WINDOW*)wnode->ptr;
126 if (!win || !(win->state & N_GUI_WIN_OPEN)) continue;
127 list_foreach(wgn, win->widgets) {
128 const N_GUI_WIDGET* w = (const N_GUI_WIDGET*)wgn->ptr;
129 if (w && w->id == ctx->focused_widget_id) return win;
130 }
131 }
132 return NULL;
133}
134
137static void _normalize_crlf(char* s) {
138 if (!s) return;
139 char* dst = s;
140 for (char* src = s; *src; src++) {
141 if (*src != '\r') *dst++ = *src;
142 }
143 *dst = '\0';
144}
145
146/* Text-measurement memoization.
147 *
148 * al_get_text_width / al_get_text_dimensions walk the string glyph by glyph
149 * (a font glyph lookup per character) and are called per item, per cell, per
150 * run, per word and per character every frame, over text that is almost always
151 * identical from one frame to the next. This small direct-mapped cache turns
152 * those repeats into a hash + compare.
153 *
154 * The cache is file scope on purpose: GUI drawing is single-threaded (the
155 * _draw_* path and the hit tests take no locks), so a global cache is safe and
156 * lets _text_w / _text_dims stay plain (font, text) helpers without threading a
157 * context through every draw function. Each entry is a pure function of the
158 * font metrics and the string, so sharing the cache across contexts is correct.
159 *
160 * The key includes the font's line height, so two different-size fonts that
161 * happen to reuse the same freed address never return each other's metrics; the
162 * only residual staleness is a same-size font reused at a freed address, which
163 * at worst yields a stale extent for one string until that slot is reclaimed.
164 * Strings at or beyond N_GUI_MEAS_KEY_MAX bypass the cache and measure directly.
165 */
166#define N_GUI_MEAS_CACHE_SIZE 1024u /* power of two */
167#define N_GUI_MEAS_KEY_MAX 32 /* strings this long or longer bypass the cache */
168
169typedef struct N_GUI_MEAS_ENTRY {
170 const ALLEGRO_FONT* font; /* NULL: slot empty */
171 int line_h; /* part of the key: separates same-address fonts of different sizes */
172 unsigned int hash;
173 unsigned short len;
175 float adv_w; /* al_get_text_width result */
176 int dx, dy, dw, dh; /* al_get_text_dimensions result */
177 unsigned char have_adv; /* adv_w is populated */
178 unsigned char have_dims; /* dx/dy/dw/dh are populated */
180
182
186static N_GUI_MEAS_ENTRY* _ngui_meas_slot(ALLEGRO_FONT* font, const char* text, size_t len) {
187 unsigned int h = 2166136261u; /* FNV-1a */
188 for (size_t i = 0; i < len; i++) {
189 h ^= (unsigned char)text[i];
190 h *= 16777619u;
191 }
192 int line_h = al_get_font_line_height(font);
193 h ^= (unsigned int)line_h * 2654435761u;
195 if (e->font == font && e->line_h == line_h && e->hash == h &&
196 e->len == (unsigned short)len && memcmp(e->key, text, len) == 0) {
197 return e; /* hit */
198 }
199 /* claim the slot for this key */
200 e->font = font;
201 e->line_h = line_h;
202 e->hash = h;
203 e->len = (unsigned short)len;
204 memcpy(e->key, text, len);
205 e->have_adv = 0;
206 e->have_dims = 0;
207 return e;
208}
209
214static float _text_w(ALLEGRO_FONT* font, const char* text) {
215 if (!font || !text || !text[0]) return 0.0f;
216 size_t len = strlen(text);
217 if (len >= N_GUI_MEAS_KEY_MAX) return (float)al_get_text_width(font, text);
218 N_GUI_MEAS_ENTRY* e = _ngui_meas_slot(font, text, len);
219 if (!e->have_adv) {
220 e->adv_w = (float)al_get_text_width(font, text);
221 e->have_adv = 1;
222 }
223 return e->adv_w;
224}
225
229static void _text_dims(ALLEGRO_FONT* font, const char* text, int* x, int* y, int* w, int* h) {
230 int bx = 0, by = 0, bw = 0, bh = 0;
231 if (font && text && text[0]) {
232 size_t len = strlen(text);
233 if (len >= N_GUI_MEAS_KEY_MAX) {
234 al_get_text_dimensions(font, text, &bx, &by, &bw, &bh);
235 } else {
236 N_GUI_MEAS_ENTRY* e = _ngui_meas_slot(font, text, len);
237 if (!e->have_dims) {
238 al_get_text_dimensions(font, text, &e->dx, &e->dy, &e->dw, &e->dh);
239 e->have_dims = 1;
240 }
241 bx = e->dx;
242 by = e->dy;
243 bw = e->dw;
244 bh = e->dh;
245 }
246 }
247 if (x) *x = bx;
248 if (y) *y = by;
249 if (w) *w = bw;
250 if (h) *h = bh;
251}
252
254static float _win_tbh(const N_GUI_WINDOW* win) {
255 return (win->flags & N_GUI_WIN_FRAMELESS) ? 0.0f : win->titlebar_h;
256}
257
263static int _win_on_pass(const N_GUI_CTX* ctx, const N_GUI_WINDOW* win) {
264 return win && win->native == ctx->pass_display;
265}
266
271static int _native_own_chrome(const N_GUI_WINDOW* win) {
272 return (win && win->native && (win->detach_flags & N_GUI_DETACH_OWN_CHROME)) ? 1 : 0;
273}
274
293 if (!_native_own_chrome(win)) return;
294 win->native_drag_anchored = 0;
295 int cx = 0, cy = 0;
296 if (!al_get_mouse_cursor_position(&cx, &cy)) return;
297 int wx = 0, wy = 0;
298 al_get_window_position(win->native, &wx, &wy);
299 win->native_drag_cx = cx;
300 win->native_drag_cy = cy;
301 win->native_drag_wx = wx;
302 win->native_drag_wy = wy;
303 win->native_drag_anchored = 1;
304}
305
318 if (!_native_own_chrome(win)) return;
319 int minimised = (win->state & N_GUI_WIN_MINIMISED) ? 1 : 0;
320 int want_w = (int)(win->w + 0.5f);
321 int want_h = minimised ? (int)(_win_tbh(win) + 0.5f) : (int)(win->h + 0.5f);
322 int resizable = (al_get_display_flags(win->native) & ALLEGRO_RESIZABLE) ? 1 : 0;
323 if (want_w < 1) want_w = 1;
324 if (want_h < 1) want_h = 1;
325
326 /* The minimum-height constraint set at detach time exists to stop the window
327 * manager shrinking a window below what its layout can render, but a title-bar
328 * height is deliberately below it: with a real min_h in place the window manager
329 * simply refuses the resize, and the window stayed full size while its content
330 * hid, which looked like minimise doing nothing. So relax the constraint before
331 * shrinking and restore it once the window has grown back. */
332 if (resizable && minimised) {
333 int cmin_w = (int)(win->min_w + 0.5f);
334 if (cmin_w < 1) cmin_w = 1;
335 if (al_set_window_constraints(win->native, cmin_w, want_h, 0, 0))
336 al_apply_window_constraints(win->native, true);
337 }
338
339 if (want_w != al_get_display_width(win->native) || want_h != al_get_display_height(win->native)) {
340 al_resize_display(win->native, want_w, want_h);
341 }
342
343 if (resizable && !minimised) {
344 int cmin_w = (int)(win->min_w + 0.5f);
345 int cmin_h = (int)(win->min_h + 0.5f);
346 if (cmin_w < 1) cmin_w = 1;
347 if (cmin_h < 1) cmin_h = 1;
348 if (al_set_window_constraints(win->native, cmin_w, cmin_h, 0, 0))
349 al_apply_window_constraints(win->native, true);
350 }
351}
352
361static int _native_monitor_info(ALLEGRO_DISPLAY* display, ALLEGRO_MONITOR_INFO* info) {
362 if (!display || !info) return 0;
363 int wx = 0, wy = 0;
364 al_get_window_position(display, &wx, &wy);
365 int nb_adapters = al_get_num_video_adapters();
366 for (int adapter = 0; adapter < nb_adapters; adapter++) {
367 ALLEGRO_MONITOR_INFO mi;
368 if (!al_get_monitor_info(adapter, &mi)) continue;
369 if (wx >= mi.x1 && wx < mi.x2 && wy >= mi.y1 && wy < mi.y2) {
370 *info = mi;
371 return 1;
372 }
373 }
374 return al_get_monitor_info(0, info) ? 1 : 0;
375}
376
380static ALLEGRO_DISPLAY* _event_display(const ALLEGRO_EVENT* event) {
381 switch (event->type) {
382 case ALLEGRO_EVENT_MOUSE_AXES:
383 case ALLEGRO_EVENT_MOUSE_BUTTON_DOWN:
384 case ALLEGRO_EVENT_MOUSE_BUTTON_UP:
385 case ALLEGRO_EVENT_MOUSE_ENTER_DISPLAY:
386 case ALLEGRO_EVENT_MOUSE_LEAVE_DISPLAY:
387 case ALLEGRO_EVENT_MOUSE_WARPED:
388 return event->mouse.display;
389 case ALLEGRO_EVENT_KEY_DOWN:
390 case ALLEGRO_EVENT_KEY_UP:
391 case ALLEGRO_EVENT_KEY_CHAR:
392 return event->keyboard.display;
393 case ALLEGRO_EVENT_TOUCH_BEGIN:
394 case ALLEGRO_EVENT_TOUCH_END:
395 case ALLEGRO_EVENT_TOUCH_MOVE:
396 case ALLEGRO_EVENT_TOUCH_CANCEL:
397 return event->touch.display;
398 case ALLEGRO_EVENT_DISPLAY_EXPOSE:
399 case ALLEGRO_EVENT_DISPLAY_RESIZE:
400 case ALLEGRO_EVENT_DISPLAY_CLOSE:
401 case ALLEGRO_EVENT_DISPLAY_LOST:
402 case ALLEGRO_EVENT_DISPLAY_FOUND:
403 case ALLEGRO_EVENT_DISPLAY_SWITCH_IN:
404 case ALLEGRO_EVENT_DISPLAY_SWITCH_OUT:
405 case ALLEGRO_EVENT_DISPLAY_ORIENTATION:
406 case ALLEGRO_EVENT_DISPLAY_HALT_DRAWING:
407 case ALLEGRO_EVENT_DISPLAY_RESUME_DRAWING:
408 return event->display.source;
409 default:
410 /* timer, user, joystick, and the monitor hotplug events, none of
411 * which name a display we could route on */
412 return NULL;
413 }
414}
415
418static void _release_native_window(N_GUI_CTX* ctx, N_GUI_WINDOW* win);
419
424static ALLEGRO_DISPLAY* _ctx_io_display(const N_GUI_CTX* ctx) {
425 return ctx->active_display ? ctx->active_display : ctx->display;
426}
427
429static int _point_in_rect(float px, float py, float rx, float ry, float rw, float rh) {
430 return (px >= rx && px <= rx + rw && py >= ry && py <= ry + rh);
431}
432
439static int _pointer_over_window(const N_GUI_CTX* ctx, float px, float py) {
440 list_foreach(wnode, ctx->windows) {
441 const N_GUI_WINDOW* win = (const N_GUI_WINDOW*)wnode->ptr;
442 if (!win || !(win->state & N_GUI_WIN_OPEN)) continue;
443 if (!_win_on_pass(ctx, win)) continue;
444 float win_h_check = (win->state & N_GUI_WIN_MINIMISED) ? _win_tbh(win) : win->h;
445 if (_point_in_rect(px, py, win->x, win->y, win->w, win_h_check)) return 1;
446 }
447 return 0;
448}
449
461static float _scrollbar_calc_scroll(float mouse, float track_start, float track_length, float viewport, float content, float thumb_min) {
462 float ratio = viewport / content;
463 if (ratio > 1.0f) ratio = 1.0f;
464 float thumb = ratio * track_length;
465 if (thumb < thumb_min) thumb = thumb_min;
466 float max_scroll = content - viewport;
467 float track_range = track_length - thumb;
468 if (track_range <= 0 || max_scroll <= 0) return 0;
469 float pos_ratio = (mouse - track_start - thumb / 2.0f) / track_range;
470 if (pos_ratio < 0) pos_ratio = 0;
471 if (pos_ratio > 1) pos_ratio = 1;
472 return pos_ratio * max_scroll;
473}
474
484static int _scrollbar_calc_scroll_int(float mouse, float track_start, float track_length, int visible_items, int total_items, float thumb_min) {
485 int max_off = total_items - visible_items;
486 if (max_off <= 0) return 0;
487 float scroll = _scrollbar_calc_scroll(mouse, track_start, track_length,
488 (float)visible_items, (float)total_items, thumb_min);
489 int offset = (int)(scroll + 0.5f);
490 if (offset < 0) offset = 0;
491 if (offset > max_off) offset = max_off;
492 return offset;
493}
494
503static N_GUI_WINDOW* _find_widget_window(N_GUI_CTX* ctx, int wgt_id, float* ox, float* oy) {
504 list_foreach(wnode, ctx->windows) {
505 N_GUI_WINDOW* win = (N_GUI_WINDOW*)wnode->ptr;
506 if (!(win->state & N_GUI_WIN_OPEN)) continue;
507 list_foreach(wgn, win->widgets) {
508 if (((N_GUI_WIDGET*)wgn->ptr)->id == wgt_id) {
509 *ox = win->x - win->scroll_x;
510 *oy = win->y + _win_tbh(win) - win->scroll_y;
511 return win;
512 }
513 }
514 }
515 *ox = 0;
516 *oy = 0;
517 return NULL;
518}
519
522static int _text_line_count(const char* s);
526
527static void _destroy_widget(void* ptr) {
528 if (!ptr) return;
529 N_GUI_WIDGET* w = (N_GUI_WIDGET*)ptr;
530 FreeNoLog(w->tooltip);
531 if (w->data) {
532 /* free inner allocations for types that have them */
533 if (w->type == N_GUI_TYPE_LISTBOX) {
535 FreeNoLog(ld->items);
536 } else if (w->type == N_GUI_TYPE_RADIOLIST) {
538 FreeNoLog(rd->items);
539 } else if (w->type == N_GUI_TYPE_COMBOBOX) {
541 FreeNoLog(cd->items);
542 } else if (w->type == N_GUI_TYPE_DROPMENU) {
544 FreeNoLog(dd->entries);
545 } else if (w->type == N_GUI_TYPE_TEXTAREA) {
547 FreeNoLog(td->text);
549 } else if (w->type == N_GUI_TYPE_HEXVIEW) {
551 FreeNoLog(hd->data);
552 } else if (w->type == N_GUI_TYPE_SYNTAXVIEW) {
554 FreeNoLog(yd->text);
555 } else if (w->type == N_GUI_TYPE_DATAGRID) {
557 if (gd->cells) {
558 size_t ci;
559 for (ci = 0; ci < gd->nb_rows * gd->nb_cols; ci++)
560 FreeNoLog(gd->cells[ci]);
561 FreeNoLog(gd->cells);
562 }
563 FreeNoLog(gd->cols);
564 FreeNoLog(gd->col_order);
565 FreeNoLog(gd->row_sel);
566 FreeNoLog(gd->row_color);
568 }
569 FreeNoLog(w->data);
570 }
571 FreeNoLog(w);
572}
573
575static void _destroy_window(void* ptr) {
576 if (!ptr) return;
577 N_GUI_WINDOW* win = (N_GUI_WINDOW*)ptr;
578 if (win->widgets) {
579 list_destroy(&win->widgets);
580 }
581 FreeNoLog(win);
582}
583
585static LIST_NODE* _find_window_node(N_GUI_CTX* ctx, int window_id) {
586 __n_assert(ctx, return NULL);
587 __n_assert(ctx->windows, return NULL);
588 list_foreach(node, ctx->windows) {
589 const N_GUI_WINDOW* win = (const N_GUI_WINDOW*)node->ptr;
590 if (win && win->id == window_id) return node;
591 }
592 return NULL;
593}
594
597 __n_assert(ctx, return);
598 __n_assert(w, return);
599 char key[32];
600 snprintf(key, sizeof(key), "%d", w->id);
601 ht_put_ptr(ctx->widgets_by_id, key, w, NULL, NULL);
602 ctx->dirty = 1; /* a new widget changes what will be drawn */
603}
604
606static int _items_grow(N_GUI_LISTITEM** items, const size_t* nb, size_t* cap) {
607 if (*nb >= *cap) {
608 size_t new_cap = (*cap == 0) ? 8 : (*cap) * 2;
609 if (new_cap < *cap) return 0; /* overflow */
610 N_GUI_LISTITEM* tmp = NULL;
611 Malloc(tmp, N_GUI_LISTITEM, new_cap);
612 if (!tmp) return 0;
613 if (*items && *nb > 0) {
614 memcpy(tmp, *items, (*nb) * sizeof(N_GUI_LISTITEM));
615 }
616 FreeNoLog(*items);
617 *items = tmp;
618 *cap = new_cap;
619 }
620 return 1;
621}
622
624static N_GUI_WIDGET* _new_widget(N_GUI_CTX* ctx, int type, float x, float y, float w, float h) {
625 __n_assert(ctx, return NULL);
626 N_GUI_WIDGET* wgt = NULL;
627 Malloc(wgt, N_GUI_WIDGET, 1);
628 __n_assert(wgt, return NULL);
629 wgt->id = ctx->next_widget_id++;
630 wgt->type = type;
631 wgt->x = x;
632 wgt->y = y;
633 wgt->w = w;
634 wgt->h = h;
635 wgt->state = N_GUI_STATE_IDLE;
636 wgt->visible = 1;
637 wgt->enabled = 1;
638 wgt->theme = ctx->default_theme;
639 wgt->font = NULL;
640 wgt->data = NULL;
641 wgt->tooltip = NULL;
642 return wgt;
643}
644
645/* THEME */
646
652 N_GUI_THEME t;
653 t.bg_normal = al_map_rgba(50, 50, 60, 230);
654 t.bg_hover = al_map_rgba(70, 70, 85, 240);
655 t.bg_active = al_map_rgba(90, 90, 110, 250);
656 t.border_normal = al_map_rgba(120, 120, 140, 255);
657 t.border_hover = al_map_rgba(160, 160, 180, 255);
658 t.border_active = al_map_rgba(200, 200, 220, 255);
659 t.text_normal = al_map_rgba(220, 220, 220, 255);
660 t.text_hover = al_map_rgba(255, 255, 255, 255);
661 t.text_active = al_map_rgba(255, 255, 255, 255);
662 t.border_thickness = 1.0f;
663 t.corner_rx = 4.0f;
664 t.corner_ry = 4.0f;
665 t.selection_color = al_map_rgba(50, 100, 200, 120);
666 return t;
667}
668
686 ALLEGRO_COLOR bg,
687 ALLEGRO_COLOR bg_hover,
688 ALLEGRO_COLOR bg_active,
689 ALLEGRO_COLOR border,
690 ALLEGRO_COLOR border_hover,
691 ALLEGRO_COLOR border_active,
692 ALLEGRO_COLOR text,
693 ALLEGRO_COLOR text_hover,
694 ALLEGRO_COLOR text_active,
695 float border_thickness,
696 float corner_rx,
697 float corner_ry) {
698 N_GUI_THEME t;
699 t.bg_normal = bg;
700 t.bg_hover = bg_hover;
701 t.bg_active = bg_active;
702 t.border_normal = border;
703 t.border_hover = border_hover;
704 t.border_active = border_active;
705 t.text_normal = text;
706 t.text_hover = text_hover;
707 t.text_active = text_active;
708 t.border_thickness = border_thickness;
709 t.corner_rx = corner_rx;
710 t.corner_ry = corner_ry;
711 t.selection_color = al_map_rgba(50, 100, 200, 120);
712 return t;
713}
714
718static ALLEGRO_COLOR _color_with_alpha(ALLEGRO_COLOR c, float a) {
719 c.a = a;
720 return c;
721}
722
729 N_GUI_THEME t;
730 t.bg_normal = al_map_rgba(0, 0, 0, 0);
731 t.bg_hover = al_map_rgba(255, 255, 255, 40);
732 t.bg_active = al_map_rgba(255, 255, 255, 80);
733 t.border_normal = al_map_rgba(0, 0, 0, 0);
734 t.border_hover = al_map_rgba(0, 0, 0, 0);
735 t.border_active = al_map_rgba(0, 0, 0, 0);
736 t.text_normal = al_map_rgba(200, 200, 200, 255);
737 t.text_hover = al_map_rgba(255, 255, 255, 255);
738 t.text_active = al_map_rgba(255, 255, 255, 255);
739 t.border_thickness = 0;
740 t.corner_rx = 2.0f;
741 t.corner_ry = 2.0f;
742 t.selection_color = al_map_rgba(0, 0, 0, 0);
743 return t;
744}
745
753 t.bg_hover = al_map_rgba(232, 17, 35, 220);
754 t.bg_active = al_map_rgba(200, 10, 25, 255);
755 return t;
756}
757
759 if (!ctx) return;
760 ctx->default_theme = theme;
761
762 /* Derive scrollbar colors from the theme so they stay in sync */
764 _color_with_alpha(theme.bg_normal, 0.78f);
766 _color_with_alpha(theme.border_normal, 0.86f);
768 _color_with_alpha(theme.bg_active, 0.78f);
770 _color_with_alpha(theme.border_normal, 0.86f);
772 _color_with_alpha(theme.text_normal, 0.50f);
773
774 /* Derive titlebar button glyph colors from the theme */
781}
782
783/* STYLE DEFAULTS */
784
789 N_GUI_STYLE s;
790
791 /* tooltips */
792 s.tooltip_delay = 0.6f;
793 s.tooltip_bg = al_map_rgba(28, 30, 36, 240);
794 s.tooltip_border = al_map_rgb(110, 115, 130);
795 s.tooltip_fg = al_map_rgb(225, 225, 230);
796
797 /* window chrome */
798 s.titlebar_h = 28.0f;
799 s.min_win_w = 120.0f;
800 s.min_win_h = 60.0f;
801 s.title_padding = 8.0f;
802 s.title_max_w_reserve = 16.0f;
803
804 /* titlebar buttons */
805 s.tb_btn_size = 0.0f;
806 s.tb_btn_spacing = 2.0f;
807 s.tb_btn_right_margin = 4.0f;
808 s.tb_btn_glyph_thickness = 1.5f;
809
810 /* window auto-scrollbar */
811 s.scrollbar_size = 12.0f;
812 s.scrollbar_thumb_min = 16.0f;
815 s.scrollbar_track_color = al_map_rgba(40, 40, 50, 200);
816 s.scrollbar_thumb_color = al_map_rgba(120, 120, 140, 220);
817
818 /* global display scrollbar */
819 s.global_scrollbar_size = 14.0f;
824 s.global_scrollbar_track_color = al_map_rgba(30, 30, 40, 200);
825 s.global_scrollbar_thumb_color = al_map_rgba(100, 100, 120, 220);
826 s.global_scrollbar_thumb_border_color = al_map_rgba(140, 140, 160, 255);
827
828 /* resize grip */
829 s.grip_size = 12.0f;
830 s.grip_line_thickness = 1.0f;
831 s.grip_color = al_map_rgba(160, 160, 180, 200);
832
833 /* slider */
834 s.slider_track_size = 6.0f;
835 s.slider_track_corner_r = 3.0f;
837 s.slider_handle_min_r = 4.0f;
841
842 /* text area */
843 s.textarea_padding = 4.0f;
844 s.textarea_cursor_width = 2.0f;
846
847 /* checkbox */
848 s.checkbox_max_size = 20.0f;
849 s.checkbox_mark_margin = 4.0f;
851 s.checkbox_label_gap = 10.0f;
852 s.checkbox_label_offset = 6.0f;
853
854 /* radio list */
855 s.radio_circle_min_r = 4.0f;
857 s.radio_inner_offset = 3.0f;
858 s.radio_label_gap = 6.0f;
859
860 /* list items */
865 s.shape_mode = -1; /* -1 = per-widget shape (buttons keep their own); the
866 dropmenu panel still defaults to rounded. Set to
867 N_GUI_SHAPE_ROUNDED / N_GUI_SHAPE_RECT to force a global
868 round or square look across shape-aware widgets. */
869 s.item_text_padding = 6.0f;
870 s.item_selection_inset = 1.0f;
871 s.item_height_pad = 4.0f;
872
873 /* dropdown arrow */
874 s.dropdown_arrow_reserve = 16.0f;
876 s.dropdown_arrow_half_h = 3.0f;
877 s.dropdown_arrow_half_w = 5.0f;
879
880 /* label */
881 s.label_padding = 4.0f;
883 s.link_color_normal = al_map_rgba(80, 140, 220, 255);
884 s.link_color_hover = al_map_rgba(120, 180, 255, 255);
885
886 /* scroll step */
887 s.scroll_step = 20.0f;
888 s.global_scroll_step = 30.0f;
889
890 /* combobox auto-width cap */
891 s.combobox_max_dropdown_width = 0.0f; /* 0 = clamp to display width */
892
893 return s;
894}
895
896/* CONTEXT */
897
903N_GUI_CTX* n_gui_new_ctx(ALLEGRO_FONT* default_font) {
904 __n_assert(default_font, return NULL);
905 N_GUI_CTX* ctx = NULL;
906 Malloc(ctx, N_GUI_CTX, 1);
907 __n_assert(ctx, return NULL);
909 if (!ctx->windows) {
910 n_log(LOG_ERR, "n_gui_new_ctx: failed to allocate window list");
911 Free(ctx);
912 return NULL;
913 }
914 ctx->widgets_by_id = new_ht(256);
915 if (!ctx->widgets_by_id) {
916 n_log(LOG_ERR, "n_gui_new_ctx: failed to allocate widget hash table");
917 list_destroy(&ctx->windows);
918 Free(ctx);
919 return NULL;
920 }
921 ctx->next_widget_id = 1;
922 ctx->next_window_id = 1;
923 ctx->default_font = default_font;
927 ctx->focused_widget_id = -1;
928 ctx->cursor_shape = ALLEGRO_SYSTEM_MOUSE_CURSOR_DEFAULT;
929 ctx->tooltip_widget_id = -1;
930 ctx->tooltip_armed_at = 0.0;
931 ctx->tooltip_anchor_x = -10000;
932 ctx->tooltip_anchor_y = -10000;
933 ctx->mouse_x = 0;
934 ctx->mouse_y = 0;
935 ctx->mouse_b1 = 0;
936 ctx->mouse_b1_prev = 0;
937 ctx->open_combobox_id = -1;
938 ctx->scrollbar_drag_widget_id = -1;
939 ctx->open_dropmenu_id = -1;
940 ctx->display_w = 0;
941 ctx->display_h = 0;
942 ctx->global_scroll_x = 0;
943 ctx->global_scroll_y = 0;
944 ctx->gui_bounds_w = 0;
945 ctx->gui_bounds_h = 0;
946 ctx->dpi_scale = 1.0f;
947 ctx->virtual_w = 0;
948 ctx->virtual_h = 0;
949 ctx->gui_scale = 1.0f;
950 ctx->gui_offset_x = 0;
951 ctx->gui_offset_y = 0;
952 ctx->global_vscroll_drag = 0;
953 ctx->global_hscroll_drag = 0;
954 ctx->style = n_gui_default_style();
955 ctx->display = NULL;
956 ctx->selected_label_id = -1;
957 ctx->selected_syntaxview_id = -1;
959 ctx->ref_display_w = 0.0f;
960 ctx->ref_display_h = 0.0f;
961 ctx->pending_layout = NULL;
962 ctx->pending_layout_count = 0;
963 ctx->pending_widgets = NULL;
964 ctx->pending_widgets_count = 0;
965 ctx->event_queue = NULL;
966 ctx->pass_display = NULL;
967 ctx->active_display = NULL;
968 ctx->dirty = 1; /* the empty context still needs its first frame drawn */
969 ctx->anim_until = 0.0;
970 ctx->prev_over_win = 0;
971 return ctx;
972}
973
975 if (ctx) ctx->dirty = 1;
976}
977
979 if (!ctx) return 1;
980 if (ctx->dirty) return 1;
981 /* a timed animation (button key-press flash, tooltip reveal) is pending */
982 if (ctx->anim_until > 0.0 && al_get_time() < ctx->anim_until) return 1;
983 /* a focused text area blinks its caret, so it must keep redrawing */
984 if (ctx->focused_widget_id >= 0) {
985 const N_GUI_WIDGET* fw = n_gui_get_widget(ctx, ctx->focused_widget_id);
986 if (fw && fw->type == N_GUI_TYPE_TEXTAREA) return 1;
987 }
988 return 0;
989}
990
996 __n_assert(ctx && *ctx, return);
997 n_clipboard_destroy(); /* stop the X11 clipboard serving thread, if it was started */
998 if ((*ctx)->windows) {
999 /* Release native windows first: the list destructor only gets a void*
1000 * and cannot reach the context's event queue to unregister their event
1001 * sources. Done before list_destroy so no display outlives the context. */
1002 list_foreach(dnode, (*ctx)->windows) {
1003 N_GUI_WINDOW* dwin = (N_GUI_WINDOW*)dnode->ptr;
1004 if (dwin && dwin->native) {
1005 _release_native_window(*ctx, dwin);
1006 }
1007 }
1008 list_destroy(&(*ctx)->windows);
1009 }
1010 if ((*ctx)->widgets_by_id) {
1011 destroy_ht(&(*ctx)->widgets_by_id);
1012 }
1013 FreeNoLog((*ctx)->pending_layout);
1014 FreeNoLog((*ctx)->pending_widgets);
1015 Free((*ctx));
1016}
1017
1018/* ADAPTIVE RESIZE HELPERS */
1019
1022 float dw = ctx->ref_display_w;
1023 float dh = ctx->ref_display_h;
1024 if (dw <= 0) dw = ctx->display_w;
1025 if (dh <= 0) dh = ctx->display_h;
1026 if (dw <= 0 || dh <= 0) return;
1027
1028 win->norm_x = win->x / dw;
1029 win->norm_y = win->y / dh;
1030 win->norm_w = win->w / dw;
1031 win->norm_h = win->h / dh;
1032
1033 /* capture widget normalized coords relative to window */
1034 if (win->resize_policy == N_GUI_WIN_RESIZE_SCALE && win->w > 0 && win->h > 0) {
1035 list_foreach(wnode, win->widgets) {
1036 N_GUI_WIDGET* wgt = (N_GUI_WIDGET*)wnode->ptr;
1037 wgt->norm_x = wgt->x / win->w;
1038 wgt->norm_y = wgt->y / win->h;
1039 wgt->norm_w = wgt->w / win->w;
1040 wgt->norm_h = wgt->h / win->h;
1041 }
1042 }
1043}
1044
1047 if (win->w > 0 && win->h > 0) {
1048 wgt->norm_x = wgt->x / win->w;
1049 wgt->norm_y = wgt->y / win->h;
1050 wgt->norm_w = wgt->w / win->w;
1051 wgt->norm_h = wgt->h / win->h;
1052 }
1053}
1054
1055/* WINDOW MANAGEMENT */
1056
1057static void _sort_windows_by_zorder(N_GUI_CTX* ctx);
1058
1063int n_gui_add_window(N_GUI_CTX* ctx, const char* title, float x, float y, float w, float h) {
1064 __n_assert(ctx, return -1);
1065 N_GUI_WINDOW* win = NULL;
1066 Malloc(win, N_GUI_WINDOW, 1);
1067 __n_assert(win, return -1);
1068 win->id = ctx->next_window_id++;
1069 if (title) {
1070 strncpy(win->title, title, N_GUI_ID_MAX - 1);
1071 win->title[N_GUI_ID_MAX - 1] = '\0';
1072 } else {
1073 win->title[0] = '\0';
1074 }
1075 win->x = x;
1076 win->y = y;
1077 win->w = w;
1078 win->h = h;
1079 win->titlebar_h = ctx->style.titlebar_h;
1080 win->state = N_GUI_WIN_OPEN;
1082 if (!win->widgets) {
1083 n_log(LOG_ERR, "n_gui_add_window: failed to allocate widget list");
1084 Free(win);
1085 return -1;
1086 }
1087 win->theme = ctx->default_theme;
1088 win->font = NULL;
1089 win->drag_ox = 0;
1090 win->drag_oy = 0;
1091 win->min_w = ctx->style.min_win_w;
1092 win->min_h = ctx->style.min_win_h;
1093 win->flags = 0;
1094 win->scroll_x = 0.0f;
1095 win->scroll_y = 0.0f;
1096 win->content_w = 0.0f;
1097 win->content_h = 0.0f;
1099 win->z_value = 0;
1100 win->autofit_flags = 0;
1101 win->autofit_border = 0.0f;
1102 win->autofit_origin_x = x;
1103 win->autofit_origin_y = y;
1105 win->norm_x = 0.0f;
1106 win->norm_y = 0.0f;
1107 win->norm_w = 0.0f;
1108 win->norm_h = 0.0f;
1109 memset(&win->tb_buttons, 0, sizeof(N_GUI_TB_BUTTONS));
1112 win->native = NULL;
1114 win->want_native = 0;
1115 win->native_close_pending = 0;
1116 win->saved_x = x;
1117 win->saved_y = y;
1118 win->saved_w = w;
1119 win->saved_h = h;
1120 win->saved_flags = 0;
1121 win->native_w = 0.0f;
1122 win->native_h = 0.0f;
1123 win->native_pos_x = INT_MIN;
1124 win->native_pos_y = INT_MIN;
1125 win->native_drag_cx = 0;
1126 win->native_drag_cy = 0;
1127 win->native_drag_wx = 0;
1128 win->native_drag_wy = 0;
1129 win->native_drag_anchored = 0;
1130 win->native_halted = 0;
1131 /* Cross-session restore for lazily-created windows: if the loaded layout
1132 * carried geometry for a window that did NOT exist at load time (so it
1133 * couldn't be matched then), apply that saved position, and size when the
1134 * file recorded one, the first time a window with this title is created.
1135 * Consume-once so subsequent destroy+recreate rebuilds keep the caller's
1136 * (possibly user-dragged/resized) geometry instead of snapping back to the
1137 * file. Of the persisted state bits only minimised/maximised are applied: they
1138 * describe how the window is displayed and must survive a restart, whereas
1139 * N_GUI_WIN_OPEN stays caller-driven here, a lazily-created window is created
1140 * because something is about to open it, so forcing OPEN at creation time would
1141 * be meaningless (windows that already exist at load time do restore OPEN). */
1142 if (ctx->pending_layout && win->title[0]) {
1143 for (int i = 0; i < ctx->pending_layout_count; i++) {
1144 if (!ctx->pending_layout[i].consumed &&
1145 strncmp(ctx->pending_layout[i].title, win->title, N_GUI_ID_MAX) == 0) {
1146 win->x = ctx->pending_layout[i].x;
1147 win->y = ctx->pending_layout[i].y;
1148 if (ctx->display_w > 0.0f && (win->x < 0.0f || win->x > ctx->display_w)) win->x = 0.0f;
1149 if (ctx->display_h > 0.0f && (win->y < 0.0f || win->y > ctx->display_h)) win->y = 0.0f;
1150 /* size only when the file carried one; clamp to the display so a
1151 * layout saved on a larger screen cannot exceed a smaller one */
1152 if (ctx->pending_layout[i].w > 0.0f && ctx->pending_layout[i].h > 0.0f) {
1153 win->w = ctx->pending_layout[i].w;
1154 win->h = ctx->pending_layout[i].h;
1155 if (ctx->display_w > 0.0f && win->w > ctx->display_w) win->w = ctx->display_w;
1156 if (ctx->display_h > 0.0f && win->h > ctx->display_h) win->h = ctx->display_h;
1157 }
1158 win->state = (win->state & ~(N_GUI_WIN_MINIMISED | N_GUI_WIN_MAXIMISED)) |
1160 ctx->pending_layout[i].consumed = 1;
1161 break;
1162 }
1163 }
1164 }
1165 list_push(ctx->windows, win, _destroy_window);
1166 if (ctx->resize_mode == N_GUI_RESIZE_ADAPTIVE) {
1168 }
1169 /* Re-sort so z-order groups hold from the moment of creation: without
1170 * this, a freshly added NORMAL window sits at the list tail and is
1171 * drawn ABOVE every ALWAYS_ON_TOP window until some later
1172 * raise/lower/set_zorder call happens to re-sort the list. The sort is
1173 * stable, so the new window still lands on top of its own group. */
1175 ctx->dirty = 1; /* a new window changes what will be drawn */
1176 return win->id;
1177}
1178
1183 LIST_NODE* node = _find_window_node(ctx, window_id);
1184 if (node) return (N_GUI_WINDOW*)node->ptr;
1185 return NULL;
1186}
1187
1191void n_gui_close_window(N_GUI_CTX* ctx, int window_id) {
1192 N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
1193 if (!win) return;
1194 win->state &= ~N_GUI_WIN_OPEN;
1195 /* A native window has no "hidden" state: closing it means taking the OS
1196 * window down. want_native survives, so re-opening brings it back as a
1197 * native window rather than as a pop-up. Deferred to the next
1198 * n_gui_draw_detached because a caller may well close from inside an event
1199 * callback, with events for this display still queued. */
1200 if (win->native) win->native_close_pending = 1;
1201}
1202
1206void n_gui_open_window(N_GUI_CTX* ctx, int window_id) {
1207 N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
1208 if (!win) return;
1209 win->state |= N_GUI_WIN_OPEN;
1210 if (win->native) {
1211 win->native_close_pending = 0;
1212 return;
1213 }
1214 /* re-create the native window a previous close took down, and honour the
1215 * detached state a loaded layout asked for */
1216 if (win->want_native && ctx && ctx->event_queue) {
1217 n_gui_window_detach(ctx, window_id, win->detach_flags);
1218 }
1219}
1220
1224void n_gui_minimize_window(N_GUI_CTX* ctx, int window_id) {
1225 N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
1226 if (!win) return;
1227 win->state ^= N_GUI_WIN_MINIMISED;
1229}
1230
1242void n_gui_restore_window(N_GUI_CTX* ctx, int window_id) {
1243 N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
1244 if (!win) return;
1245 win->state &= ~N_GUI_WIN_MINIMISED;
1247}
1248
1255int n_gui_window_is_minimised(N_GUI_CTX* ctx, int window_id) {
1256 const N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
1257 return (win && (win->state & N_GUI_WIN_MINIMISED)) ? 1 : 0;
1258}
1259
1267void n_gui_maximize_window(N_GUI_CTX* ctx, int window_id) {
1268 __n_assert(ctx, return);
1269 N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
1270 if (!win) return;
1271
1272 /* An own-chrome window is maximised by the window manager, not by expanding
1273 * into the main display's coordinate space. ALLEGRO_MAXIMIZED is one of the
1274 * three flags Allegro documents as changeable after creation (and it needs
1275 * ALLEGRO_RESIZABLE, which n_gui_window_detach always sets for own chrome).
1276 * Not every window manager honours it, so the geometry the WM actually
1277 * grants arrives as a DISPLAY_RESIZE and our own bookkeeping follows that
1278 * rather than assuming a size. */
1279 if (_native_own_chrome(win)) {
1280 int now_max = (win->state & N_GUI_WIN_MAXIMISED) ? 1 : 0;
1281 if (!now_max) {
1282 /* remember what to grow back from, the WM restores the size itself
1283 but our pop-up fallback geometry has to survive either way */
1284 win->tb_buttons.restore_x = win->x;
1285 win->tb_buttons.restore_y = win->y;
1286 win->tb_buttons.restore_w = win->w;
1287 win->tb_buttons.restore_h = win->h;
1288 win->state &= ~N_GUI_WIN_MINIMISED;
1290 }
1291 if (!al_set_display_flag(win->native, ALLEGRO_MAXIMIZED, now_max ? false : true)) {
1292 n_log(LOG_DEBUG, "n_gui_maximize_window: window manager refused ALLEGRO_MAXIMIZED for window %d", window_id);
1293 /* fall back to resizing the window to its adapter's work area, so
1294 the button still does something on a WM without the hint */
1295 if (!now_max) {
1296 ALLEGRO_MONITOR_INFO mi;
1297 if (_native_monitor_info(win->native, &mi)) {
1298 al_set_window_position(win->native, mi.x1, mi.y1);
1299 al_resize_display(win->native, mi.x2 - mi.x1, mi.y2 - mi.y1);
1300 }
1301 } else {
1302 al_resize_display(win->native, (int)(win->tb_buttons.restore_w + 0.5f),
1303 (int)(win->tb_buttons.restore_h + 0.5f));
1304 }
1305 }
1306 if (now_max)
1307 win->state &= ~N_GUI_WIN_MAXIMISED;
1308 else
1309 win->state |= N_GUI_WIN_MAXIMISED;
1310 return;
1311 }
1312
1313 if (win->state & N_GUI_WIN_MAXIMISED) {
1314 /* restore */
1315 win->x = win->tb_buttons.restore_x;
1316 win->y = win->tb_buttons.restore_y;
1317 win->w = win->tb_buttons.restore_w;
1318 win->h = win->tb_buttons.restore_h;
1319 win->state &= ~N_GUI_WIN_MAXIMISED;
1320 } else {
1321 /* save current geometry */
1322 win->tb_buttons.restore_x = win->x;
1323 win->tb_buttons.restore_y = win->y;
1324 win->tb_buttons.restore_w = win->w;
1325 win->tb_buttons.restore_h = win->h;
1326 /* clear minimised */
1327 win->state &= ~N_GUI_WIN_MINIMISED;
1328 /* expand to display/virtual size */
1329 win->x = 0;
1330 win->y = 0;
1331 win->w = (ctx->virtual_w > 0) ? ctx->virtual_w : ctx->display_w;
1332 win->h = (ctx->virtual_h > 0) ? ctx->virtual_h : ctx->display_h;
1333 win->state |= N_GUI_WIN_MAXIMISED;
1334 }
1335 if (ctx->resize_mode == N_GUI_RESIZE_ADAPTIVE) {
1337 }
1338}
1339
1344int n_gui_window_is_maximised(N_GUI_CTX* ctx, int window_id) {
1345 __n_assert(ctx, return 0);
1346 const N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
1347 if (!win) return 0;
1348 return (win->state & N_GUI_WIN_MAXIMISED) ? 1 : 0;
1349}
1350
1351/* NATIVE (DETACHED) WINDOWS */
1352
1356void n_gui_set_event_queue(N_GUI_CTX* ctx, ALLEGRO_EVENT_QUEUE* queue) {
1357 __n_assert(ctx, return);
1358 ctx->event_queue = queue;
1359}
1360
1364// cppcheck-suppress constParameterPointer ; public API uses non-const for consistency
1365ALLEGRO_EVENT_QUEUE* n_gui_get_event_queue(N_GUI_CTX* ctx) {
1366 __n_assert(ctx, return NULL);
1367 return ctx->event_queue;
1368}
1369
1375 if (!win || !win->native) return;
1376 ALLEGRO_DISPLAY* d = win->native;
1377 /* remember where the user left the window, so re-creating it (and a saved
1378 * layout) puts it back on the same spot of the desktop */
1379 {
1380 int wx = 0, wy = 0;
1381 al_get_window_position(d, &wx, &wy);
1382 win->native_pos_x = wx;
1383 win->native_pos_y = wy;
1384 win->native_w = (float)al_get_display_width(d);
1385 win->native_h = (float)al_get_display_height(d);
1386 }
1387 /* Clear the back-pointer BEFORE destroying: al_destroy_display can pump the
1388 * platform event loop, and anything that re-enters the GUI in the meantime
1389 * must not find a window still claiming a half-destroyed display. */
1390 win->native = NULL;
1391 win->native_close_pending = 0;
1392 if (ctx->active_display == d) ctx->active_display = NULL;
1393 if (ctx->pass_display == d) ctx->pass_display = NULL;
1394 if (ctx->event_queue) {
1395 al_unregister_event_source(ctx->event_queue, al_get_display_event_source(d));
1396 }
1397 al_destroy_display(d);
1398 /* al_destroy_display leaves no current target when it destroyed the target
1399 * display, restore the host's main display so the caller's next draw call
1400 * does not land nowhere. */
1401 if (ctx->display) {
1402 al_set_target_backbuffer(ctx->display);
1403 }
1404}
1405
1409int n_gui_window_detach(N_GUI_CTX* ctx, int window_id, int detach_flags) {
1410 __n_assert(ctx, return -1);
1411 N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
1412 if (!win) {
1413 n_log(LOG_ERR, "n_gui_window_detach: no window with id %d", window_id);
1414 return -1;
1415 }
1416 if (win->native) {
1417 win->detach_flags = detach_flags;
1418 return 0; /* already detached */
1419 }
1420 if (!ctx->event_queue) {
1421 n_log(LOG_ERR, "n_gui_window_detach: no event queue set, call n_gui_set_event_queue first");
1422 return -1;
1423 }
1424
1425 /* How much of the window the native display has to show. With the OS
1426 * chrome (the default) that is the body only, the window manager draws the
1427 * title bar. With N_GUI_DETACH_OWN_CHROME the window keeps drawing its own
1428 * title bar inside a frameless native window, so the display covers the
1429 * whole window. A previously known native size wins in both cases, so a
1430 * close/open cycle and a restored layout bring the window back at the size
1431 * the user left it. */
1432 int own_chrome = (detach_flags & N_GUI_DETACH_OWN_CHROME) ? 1 : 0;
1433 float shown_h = own_chrome ? win->h : (win->h - _win_tbh(win));
1434 if (shown_h < 1.0f) shown_h = win->h;
1435 int dw = (int)(win->w + 0.5f);
1436 int dh = (int)(shown_h + 0.5f);
1437 if (win->native_w > 0.0f && win->native_h > 0.0f) {
1438 dw = (int)(win->native_w + 0.5f);
1439 dh = (int)(win->native_h + 0.5f);
1440 }
1441 if (dw < 1) dw = 1;
1442 if (dh < 1) dh = 1;
1443
1444 /* al_create_display makes the new display current and retargets drawing to
1445 * its backbuffer, restore whatever the caller was drawing into */
1446 ALLEGRO_BITMAP* prev_target = al_get_target_bitmap();
1447 int prev_new_flags = al_get_new_display_flags();
1448 int new_flags = ALLEGRO_WINDOWED;
1449 if (detach_flags & N_GUI_DETACH_RESIZABLE) new_flags |= ALLEGRO_RESIZABLE;
1450 if (own_chrome) {
1451 /* frameless so only our chrome shows, and resizable because
1452 * al_resize_display and the ALLEGRO_MAXIMIZED flag both need it: that
1453 * is how the grip and the maximise button drive the OS window. */
1454 new_flags |= ALLEGRO_FRAMELESS | ALLEGRO_RESIZABLE;
1455 }
1456 al_set_new_display_flags(new_flags);
1457 /* Title and position are set BEFORE creation so the window never appears
1458 * with Allegro's default title or at the default spot and then visibly
1459 * jumps. al_set_window_title after the fact stays as the fallback for
1460 * platforms that ignore the new-window hints. */
1461 if (win->title[0]) al_set_new_window_title(win->title);
1462 int prev_pos_x = 0, prev_pos_y = 0;
1463 al_get_new_window_position(&prev_pos_x, &prev_pos_y);
1464 if (win->native_pos_x != INT_MIN && win->native_pos_y != INT_MIN) {
1465 al_set_new_window_position(win->native_pos_x, win->native_pos_y);
1466 }
1467 ALLEGRO_DISPLAY* d = al_create_display(dw, dh);
1468 al_set_new_display_flags(prev_new_flags);
1469 al_set_new_window_position(prev_pos_x, prev_pos_y);
1470 al_set_new_window_title("");
1471 if (prev_target) al_set_target_bitmap(prev_target);
1472 if (!d) {
1473 n_log(LOG_ERR, "n_gui_window_detach: al_create_display(%d, %d) failed for window %d", dw, dh, window_id);
1474 return -1;
1475 }
1476 if (win->title[0]) al_set_window_title(d, win->title);
1477 if (win->native_pos_x != INT_MIN && win->native_pos_y != INT_MIN) {
1478 al_set_window_position(d, win->native_pos_x, win->native_pos_y);
1479 }
1480 /* Keep the window manager from shrinking the window below what the layout
1481 * can render. Constraints are hints and only apply to resizable displays,
1482 * so this is best-effort by design: the grip and DISPLAY_RESIZE handler
1483 * still clamp on our side. A zero max means "no upper bound". */
1484 if (new_flags & ALLEGRO_RESIZABLE) {
1485 int cmin_w = (int)(win->min_w + 0.5f);
1486 int cmin_h = (int)((own_chrome ? win->min_h : win->min_h - _win_tbh(win)) + 0.5f);
1487 if (cmin_w < 1) cmin_w = 1;
1488 if (cmin_h < 1) cmin_h = 1;
1489 if (al_set_window_constraints(d, cmin_w, cmin_h, 0, 0)) {
1490 al_apply_window_constraints(d, true);
1491 }
1492 }
1493 al_register_event_source(ctx->event_queue, al_get_display_event_source(d));
1494
1495 /* remember the pop-up geometry so attach can put it back exactly */
1496 win->saved_x = win->x;
1497 win->saved_y = win->y;
1498 win->saved_w = win->w;
1499 win->saved_h = win->h;
1500 win->saved_flags = win->flags;
1501
1502 win->native = d;
1503 win->detach_flags = detach_flags;
1504 win->want_native = 1;
1505 win->native_close_pending = 0;
1506 /* The window fills its display. With the OS chrome our own title bar (and
1507 * with it the drag handle and the three buttons) goes away because the
1508 * window manager already provides them; with N_GUI_DETACH_OWN_CHROME the
1509 * title bar stays and drives the OS window instead. */
1510 if (!own_chrome) {
1511 win->flags |= N_GUI_WIN_FRAMELESS;
1512 }
1513 win->x = 0.0f;
1514 win->y = 0.0f;
1515 win->w = (float)dw;
1516 win->h = (float)dh;
1517 win->native_w = (float)dw;
1518 win->native_h = (float)dh;
1519 win->state |= N_GUI_WIN_OPEN;
1522 win->native_drag_anchored = 0;
1525 win->scroll_x = 0.0f;
1526 win->scroll_y = 0.0f;
1527 /* capture widget norms against the new size so a user resize of the native
1528 * window can scale the content (N_GUI_DETACH_SCALE_CONTENT) */
1529 if (win->w > 0.0f && win->h > 0.0f) {
1530 list_foreach(wgn, win->widgets) {
1531 N_GUI_WIDGET* wgt = (N_GUI_WIDGET*)wgn->ptr;
1532 if (wgt) _n_gui_widget_capture_normalized(win, wgt);
1533 }
1534 }
1535 n_log(LOG_DEBUG, "n_gui_window_detach: window %d ('%s') detached into a %dx%d native window", window_id, win->title, dw, dh);
1536 return 0;
1537}
1538
1542int n_gui_window_attach(N_GUI_CTX* ctx, int window_id) {
1543 __n_assert(ctx, return -1);
1544 N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
1545 if (!win) {
1546 n_log(LOG_ERR, "n_gui_window_attach: no window with id %d", window_id);
1547 return -1;
1548 }
1549 if (!win->native) {
1550 win->want_native = 0;
1551 return 0; /* not detached */
1552 }
1553 _release_native_window(ctx, win);
1554 win->want_native = 0;
1555 win->x = win->saved_x;
1556 win->y = win->saved_y;
1557 win->w = win->saved_w;
1558 win->h = win->saved_h;
1559 win->flags = win->saved_flags;
1560 win->scroll_x = 0.0f;
1561 win->scroll_y = 0.0f;
1562 if (win->w > 0.0f && win->h > 0.0f) {
1563 list_foreach(wgn, win->widgets) {
1564 N_GUI_WIDGET* wgt = (N_GUI_WIDGET*)wgn->ptr;
1565 if (wgt) _n_gui_widget_capture_normalized(win, wgt);
1566 }
1567 }
1568 n_log(LOG_DEBUG, "n_gui_window_attach: window %d ('%s') is a pop-up again", window_id, win->title);
1569 return 0;
1570}
1571
1575int n_gui_window_is_detached(N_GUI_CTX* ctx, int window_id) {
1576 __n_assert(ctx, return 0);
1577 const N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
1578 return (win && win->native) ? 1 : 0;
1579}
1580
1584ALLEGRO_DISPLAY* n_gui_window_get_display(N_GUI_CTX* ctx, int window_id) {
1585 __n_assert(ctx, return NULL);
1586 // cppcheck-suppress constVariablePointer ; returned from non-const API
1587 N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
1588 return win ? win->native : NULL;
1589}
1590
1594// cppcheck-suppress constParameterPointer ; public API uses non-const for consistency
1595// cppcheck-suppress constParameter ; same check under the pre-2.11 cppcheck id used by CI
1596int n_gui_window_from_display(N_GUI_CTX* ctx, ALLEGRO_DISPLAY* display) {
1597 __n_assert(ctx, return -1);
1598 if (!display) return -1;
1599 list_foreach(wnode, ctx->windows) {
1600 const N_GUI_WINDOW* win = (const N_GUI_WINDOW*)wnode->ptr;
1601 if (win && win->native == display) return win->id;
1602 }
1603 return -1;
1604}
1605
1610 __n_assert(ctx, return 0);
1611 int n = 0;
1612 list_foreach(wnode, ctx->windows) {
1613 const N_GUI_WINDOW* win = (const N_GUI_WINDOW*)wnode->ptr;
1614 if (win && win->native) n++;
1615 }
1616 return n;
1617}
1618
1622int n_gui_window_set_native_icons(N_GUI_CTX* ctx, int window_id, ALLEGRO_BITMAP** icons, int num_icons) {
1623 __n_assert(ctx, return -1);
1624 __n_assert(icons, return -1);
1625 if (num_icons < 1) {
1626 n_log(LOG_ERR, "n_gui_window_set_native_icons: num_icons must be >= 1");
1627 return -1;
1628 }
1629 N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
1630 if (!win || !win->native) {
1631 n_log(LOG_ERR, "n_gui_window_set_native_icons: window %d is not detached", window_id);
1632 return -1;
1633 }
1634 if (num_icons == 1)
1635 al_set_display_icon(win->native, icons[0]);
1636 else
1637 al_set_display_icons(win->native, num_icons, icons);
1638 return 0;
1639}
1640
1645 __n_assert(ctx, return 0);
1646 const N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
1647 if (!win || !win->native) return 0;
1648 if (win->native_halted) return 1;
1649 return (al_get_display_flags(win->native) & ALLEGRO_MINIMIZED) ? 1 : 0;
1650}
1651
1655void n_gui_window_set_close_callback(N_GUI_CTX* ctx, int window_id, void (*on_close)(int, void*), void* user_data) {
1656 __n_assert(ctx, return);
1657 N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
1658 if (!win) return;
1659 win->tb_buttons.on_close = on_close;
1660 win->tb_buttons.on_close_user_data = user_data;
1661}
1662
1670void n_gui_window_set_content_draw_callback(N_GUI_CTX* ctx, int window_id, void (*on_content_draw)(int, void*), void* user_data) {
1671 __n_assert(ctx, return);
1672 N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
1673 if (!win) return;
1674 win->on_content_draw = on_content_draw;
1675 win->on_content_draw_data = user_data;
1676}
1677
1681void n_gui_window_set_minimize_callback(N_GUI_CTX* ctx, int window_id, void (*on_minimize)(int, void*), void* user_data) {
1682 __n_assert(ctx, return);
1683 N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
1684 if (!win) return;
1685 win->tb_buttons.on_minimize = on_minimize;
1686 win->tb_buttons.on_minimize_user_data = user_data;
1687}
1688
1692void n_gui_window_set_maximize_callback(N_GUI_CTX* ctx, int window_id, void (*on_maximize)(int, void*), void* user_data) {
1693 __n_assert(ctx, return);
1694 N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
1695 if (!win) return;
1696 win->tb_buttons.on_maximize = on_maximize;
1697 win->tb_buttons.on_maximize_user_data = user_data;
1698}
1699
1703void n_gui_window_set_tb_btn_theme(N_GUI_CTX* ctx, int window_id, N_GUI_THEME theme) {
1704 __n_assert(ctx, return);
1705 N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
1706 if (!win) return;
1707 win->tb_buttons.btn_theme = theme;
1708}
1709
1714 __n_assert(ctx, return);
1715 N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
1716 if (!win) return;
1717 win->tb_buttons.close_theme = theme;
1718}
1719
1723void n_gui_window_set_tb_button_bitmaps(N_GUI_CTX* ctx, int window_id, int btn_type, ALLEGRO_BITMAP* normal, ALLEGRO_BITMAP* hover, ALLEGRO_BITMAP* active) {
1724 __n_assert(ctx, return);
1725 N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
1726 if (!win) return;
1727 if (btn_type == N_GUI_TB_BTN_MINIMIZE) {
1728 win->tb_buttons.minimize_bitmap = normal;
1729 win->tb_buttons.minimize_hover_bitmap = hover;
1730 win->tb_buttons.minimize_active_bitmap = active;
1731 } else if (btn_type == N_GUI_TB_BTN_MAXIMIZE) {
1732 win->tb_buttons.maximize_bitmap = normal;
1733 win->tb_buttons.maximize_hover_bitmap = hover;
1734 win->tb_buttons.maximize_active_bitmap = active;
1735 } else if (btn_type == N_GUI_TB_BTN_CLOSE) {
1736 win->tb_buttons.close_bitmap = normal;
1737 win->tb_buttons.close_hover_bitmap = hover;
1738 win->tb_buttons.close_active_bitmap = active;
1739 }
1740}
1741
1742/* Return the group ordinal for a window's z-order:
1743 * 0 = ALWAYS_BEHIND, 1 = FIXED, 2 = NORMAL, 3 = ALWAYS_ON_TOP, 4 = POPUP.
1744 * Ordering is ALWAYS_BEHIND -> FIXED -> NORMAL -> ALWAYS_ON_TOP -> POPUP. */
1745static int _zorder_group(const N_GUI_WINDOW* w) {
1746 if (!w) return 2; /* treat NULL as NORMAL */
1747 switch (w->z_order) {
1749 return 0;
1750 case N_GUI_ZORDER_FIXED:
1751 return 1;
1753 return 3;
1754 case N_GUI_ZORDER_POPUP:
1755 return 4;
1756 default:
1757 return 2;
1758 }
1759}
1760
1767 if (!ctx || !ctx->windows || ctx->windows->nb_items < 2) return;
1768
1769 /* Sort by (group, z_value) so that group ordering is always respected:
1770 * ALWAYS_BEHIND windows always precede FIXED, which always precede NORMAL,
1771 * which always precede ALWAYS_ON_TOP, regardless of z_value magnitude.
1772 * Within the FIXED group, windows are ordered by z_value (lower = behind).
1773 * Within the same (group, z_value), list order is preserved (stable sort). */
1774
1775 /* collect into array for stable sort */
1776 int n = (int)ctx->windows->nb_items;
1777 N_GUI_WINDOW** arr = NULL;
1778 Malloc(arr, N_GUI_WINDOW*, (size_t)n);
1779 if (!arr) return;
1780
1781 int idx = 0;
1782 list_foreach(node, ctx->windows) {
1783 arr[idx++] = (N_GUI_WINDOW*)node->ptr;
1784 }
1785
1786 /* stable insertion sort by (group, z_value) */
1787 for (int i = 1; i < n; i++) {
1788 N_GUI_WINDOW* tmp = arr[i];
1789 int gi = _zorder_group(tmp);
1790 int j = i - 1;
1791 while (j >= 0) {
1792 int gj = _zorder_group(arr[j]);
1793 /* z_value secondary sort applies only within the FIXED group (ordinal 1) */
1794 if (gj > gi || (gj == gi && gi == 1 && arr[j]->z_value > tmp->z_value)) {
1795 arr[j + 1] = arr[j];
1796 j--;
1797 } else {
1798 break;
1799 }
1800 }
1801 arr[j + 1] = tmp;
1802 }
1803
1804 /* rebuild list in sorted order */
1805 /* detach all nodes without destroying windows */
1806 while (ctx->windows->nb_items > 0) {
1807 LIST_NODE* node = ctx->windows->start;
1808 node->destroy_func = NULL;
1809 remove_list_node_f(ctx->windows, node);
1810 }
1811 for (int i = 0; i < n; i++) {
1812 list_push(ctx->windows, arr[i], _destroy_window);
1813 }
1814 Free(arr);
1815}
1816
1824void n_gui_raise_window(N_GUI_CTX* ctx, int window_id) {
1825 __n_assert(ctx, return);
1826 LIST_NODE* node = _find_window_node(ctx, window_id);
1827 if (!node) return;
1828 N_GUI_WINDOW* win = (N_GUI_WINDOW*)node->ptr;
1829 /* only NORMAL and POPUP windows can be freely raised (within their group) */
1830 if (win->z_order != N_GUI_ZORDER_NORMAL && win->z_order != N_GUI_ZORDER_POPUP) return;
1831 /* remove from list without destroying, then push to end */
1832 node->destroy_func = NULL;
1833 remove_list_node_f(ctx->windows, node);
1834 list_push(ctx->windows, win, _destroy_window);
1836}
1837
1842void n_gui_lower_window(N_GUI_CTX* ctx, int window_id) {
1843 __n_assert(ctx, return);
1844 LIST_NODE* node = _find_window_node(ctx, window_id);
1845 if (!node) return;
1846 N_GUI_WINDOW* win = (N_GUI_WINDOW*)node->ptr;
1847 if (win->z_order != N_GUI_ZORDER_NORMAL) return;
1848 node->destroy_func = NULL;
1849 remove_list_node_f(ctx->windows, node);
1852}
1853
1862 __n_assert(ctx, return -1);
1863 if (!ctx->windows) return -1;
1864 for (const LIST_NODE* node = ctx->windows->end; node; node = node->prev) {
1865 const N_GUI_WINDOW* win = (const N_GUI_WINDOW*)node->ptr;
1866 if (win && (win->state & N_GUI_WIN_OPEN) && !(win->state & N_GUI_WIN_MINIMISED)) {
1867 return win->id;
1868 }
1869 }
1870 return -1;
1871}
1872
1881void n_gui_focus_window(N_GUI_CTX* ctx, int window_id) {
1882 __n_assert(ctx, return);
1883 N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
1884 if (!win) return;
1885 n_gui_raise_window(ctx, window_id);
1886 int target = -1;
1887 if (win->widgets) {
1888 list_foreach(node, win->widgets) {
1889 const N_GUI_WIDGET* w = (const N_GUI_WIDGET*)node->ptr;
1890 if (w && w->visible && w->enabled && _is_focusable_type(w->type)) {
1891 target = w->id;
1892 break;
1893 }
1894 }
1895 }
1896 n_gui_set_focus(ctx, target);
1897}
1898
1903void n_gui_window_set_zorder(N_GUI_CTX* ctx, int window_id, int z_mode, int z_value) {
1904 __n_assert(ctx, return);
1905 N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
1906 if (!win) return;
1907 if (z_mode != N_GUI_ZORDER_NORMAL && z_mode != N_GUI_ZORDER_ALWAYS_ON_TOP &&
1908 z_mode != N_GUI_ZORDER_ALWAYS_BEHIND && z_mode != N_GUI_ZORDER_FIXED &&
1909 z_mode != N_GUI_ZORDER_POPUP) {
1910 n_log(LOG_ERR, "n_gui_window_set_zorder: unknown z_mode %d for window %d (expected N_GUI_ZORDER_NORMAL, N_GUI_ZORDER_ALWAYS_ON_TOP, N_GUI_ZORDER_ALWAYS_BEHIND, N_GUI_ZORDER_FIXED, or N_GUI_ZORDER_POPUP), falling back to N_GUI_ZORDER_NORMAL", z_mode, window_id);
1911 z_mode = N_GUI_ZORDER_NORMAL;
1912 }
1913 win->z_order = z_mode;
1914 win->z_value = z_value;
1916}
1917
1921int n_gui_window_get_zorder(N_GUI_CTX* ctx, int window_id) {
1922 __n_assert(ctx, return N_GUI_ZORDER_NORMAL);
1923 const N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
1924 if (!win) return N_GUI_ZORDER_NORMAL;
1925 return win->z_order;
1926}
1927
1931int n_gui_window_get_zvalue(N_GUI_CTX* ctx, int window_id) {
1932 __n_assert(ctx, return 0);
1933 const N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
1934 if (!win) return 0;
1935 return win->z_value;
1936}
1937
1942int n_gui_add_window_auto(N_GUI_CTX* ctx, const char* title, float x, float y) {
1943 /* start with minimum size, will be expanded by n_gui_window_autosize */
1944 return n_gui_add_window(ctx, title, x, y, ctx->style.min_win_w, ctx->style.min_win_h);
1945}
1946
1950void n_gui_toggle_window(N_GUI_CTX* ctx, int window_id) {
1951 N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
1952 if (win) win->state ^= N_GUI_WIN_OPEN;
1953}
1954
1959int n_gui_window_is_open(N_GUI_CTX* ctx, int window_id) {
1960 const N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
1961 if (win) return (win->state & N_GUI_WIN_OPEN) ? 1 : 0;
1962 return 0;
1963}
1964
1968void n_gui_window_set_flags(N_GUI_CTX* ctx, int window_id, int flags) {
1969 N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
1970 if (win) win->flags = flags;
1971}
1972
1977int n_gui_window_get_flags(N_GUI_CTX* ctx, int window_id) {
1978 const N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
1979 if (win) return win->flags;
1980 return 0;
1981}
1982
1987void n_gui_window_autosize(N_GUI_CTX* ctx, int window_id) {
1988 N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
1989 if (!win || !win->widgets) return;
1990
1991 float max_right = 0.0f;
1992 float max_bottom = 0.0f;
1993 float pad = 20.0f;
1994
1995 list_foreach(node, win->widgets) {
1996 const N_GUI_WIDGET* wgt = (const N_GUI_WIDGET*)node->ptr;
1997 if (!wgt) continue;
1998 float r = wgt->x + wgt->w;
1999 float b = wgt->y + wgt->h;
2000 if (r > max_right) max_right = r;
2001 if (b > max_bottom) max_bottom = b;
2002 }
2003
2004 float new_w = max_right + pad;
2005 float new_h = max_bottom + pad + _win_tbh(win);
2006 if (new_w < win->min_w) new_w = win->min_w;
2007 if (new_h < win->min_h) new_h = win->min_h;
2008 win->w = new_w;
2009 win->h = new_h;
2010}
2011
2016void n_gui_window_set_autofit(N_GUI_CTX* ctx, int window_id, int autofit_flags, float border) {
2017 __n_assert(ctx, return);
2018 N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
2019 if (!win) {
2020 n_log(LOG_ERR, "n_gui_window_set_autofit: window %d not found", window_id);
2021 return;
2022 }
2023 win->autofit_flags = autofit_flags;
2024 win->autofit_border = border;
2025 /* capture current position as the insertion point for centering */
2026 win->autofit_origin_x = win->x;
2027 win->autofit_origin_y = win->y;
2028}
2029
2035void n_gui_window_apply_autofit(N_GUI_CTX* ctx, int window_id) {
2036 __n_assert(ctx, return);
2037 N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
2038 if (!win) return;
2039 if (win->autofit_flags == 0) return;
2040 if (!win->widgets) return;
2041
2042 /* compute content bounding box from visible widgets */
2043 float max_right = 0.0f;
2044 float max_bottom = 0.0f;
2045 list_foreach(node, win->widgets) {
2046 const N_GUI_WIDGET* wgt = (const N_GUI_WIDGET*)node->ptr;
2047 if (!wgt || !wgt->visible) continue;
2048 float r = wgt->x + wgt->w;
2049 float b = wgt->y + wgt->h;
2050 if (r > max_right) max_right = r;
2051 if (b > max_bottom) max_bottom = b;
2052 }
2053
2054 float border = win->autofit_border;
2055 float tbh = _win_tbh(win);
2056
2057 int center = (win->autofit_flags & N_GUI_AUTOFIT_CENTER) ? 1 : 0;
2058
2059 /* apply width adjustment */
2060 if (win->autofit_flags & N_GUI_AUTOFIT_W) {
2061 float needed_w = max_right + border * 2.0f;
2062 if (needed_w < win->min_w) needed_w = win->min_w;
2063 if (center) {
2064 win->x = win->autofit_origin_x - needed_w / 2.0f;
2065 } else if (win->autofit_flags & N_GUI_AUTOFIT_EXPAND_LEFT) {
2066 win->x -= (needed_w - win->w);
2067 }
2068 win->w = needed_w;
2069 }
2070
2071 /* apply height adjustment */
2072 if (win->autofit_flags & N_GUI_AUTOFIT_H) {
2073 float needed_h = max_bottom + border * 2.0f + tbh;
2074 if (needed_h < win->min_h) needed_h = win->min_h;
2075 if (center) {
2076 win->y = win->autofit_origin_y - needed_h / 2.0f;
2077 } else if (win->autofit_flags & N_GUI_AUTOFIT_EXPAND_UP) {
2078 win->y -= (needed_h - win->h);
2079 }
2080 win->h = needed_h;
2081 }
2082
2083 /* update content extents for scrollbar calculations */
2084 win->content_w = max_right;
2085 win->content_h = max_bottom;
2086}
2087
2089static void _window_update_content_size(N_GUI_WINDOW* win, ALLEGRO_FONT* default_font) {
2090 if (!win || !win->widgets) return;
2091 float max_right = 0.0f;
2092 float max_bottom = 0.0f;
2093 list_foreach(node, win->widgets) {
2094 N_GUI_WIDGET* wgt = (N_GUI_WIDGET*)node->ptr;
2095 if (!wgt || !wgt->visible) continue;
2096 float r = wgt->x + wgt->w;
2097 float b = wgt->y + wgt->h;
2098 /* for labels in auto-scrollbar windows, use actual text pixel width
2099 * so that horizontal scrollbar appears when text overflows */
2100 if (wgt->type == N_GUI_TYPE_LABEL && (win->flags & N_GUI_WIN_AUTO_SCROLLBAR)) {
2101 const N_GUI_LABEL_DATA* lb = (const N_GUI_LABEL_DATA*)wgt->data;
2102 if (lb && lb->text[0] && lb->align != N_GUI_ALIGN_JUSTIFIED) {
2103 ALLEGRO_FONT* font = wgt->font ? wgt->font : default_font;
2104 if (font) {
2105 float tw = _text_w(font, lb->text);
2106 float text_r = wgt->x + tw + 8.0f; /* small padding */
2107 if (text_r > r) r = text_r;
2108 }
2109 }
2110 }
2111 if (r > max_right) max_right = r;
2112 if (b > max_bottom) max_bottom = b;
2113 }
2114 win->content_w = max_right;
2115 win->content_h = max_bottom;
2116}
2117
2118/* WIDGET CREATION */
2119
2124int 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) {
2125 __n_assert(ctx, return -1);
2126 N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
2127 __n_assert(win, return -1);
2128
2129 N_GUI_WIDGET* wgt = _new_widget(ctx, N_GUI_TYPE_BUTTON, x, y, w, h);
2130 __n_assert(wgt, return -1);
2131
2132 N_GUI_BUTTON_DATA* bd = NULL;
2133 Malloc(bd, N_GUI_BUTTON_DATA, 1);
2134 __n_assert(bd, Free(wgt); return -1);
2135 if (label) {
2136 strncpy(bd->label, label, N_GUI_ID_MAX - 1);
2137 bd->label[N_GUI_ID_MAX - 1] = '\0';
2138 }
2139 bd->bitmap = NULL;
2140 bd->bitmap_hover = NULL;
2141 bd->bitmap_active = NULL;
2142 bd->shape = shape;
2143 bd->toggle_mode = 0;
2144 bd->toggled = 0;
2145 bd->keycode = 0;
2146 bd->key_modifiers = 0;
2147 bd->on_click = on_click;
2148 bd->user_data = user_data;
2149 wgt->data = bd;
2150 wgt->norm_x = 0.0f;
2151 wgt->norm_y = 0.0f;
2152 wgt->norm_w = 0.0f;
2153 wgt->norm_h = 0.0f;
2154
2155 list_push(win->widgets, wgt, _destroy_widget);
2157 _register_widget(ctx, wgt);
2158 return wgt->id;
2159}
2160
2165int n_gui_add_button_bitmap(N_GUI_CTX* ctx, int window_id, const char* label, float x, float y, float w, float h, ALLEGRO_BITMAP* normal, ALLEGRO_BITMAP* hover, ALLEGRO_BITMAP* active, void (*on_click)(int, void*), void* user_data) {
2166 int id = n_gui_add_button(ctx, window_id, label, x, y, w, h, N_GUI_SHAPE_BITMAP, on_click, user_data);
2167 if (id < 0) return -1;
2168 N_GUI_WIDGET* wgt = n_gui_get_widget(ctx, id);
2169 if (wgt && wgt->data) {
2171 bd->bitmap = normal;
2172 bd->bitmap_hover = hover;
2173 bd->bitmap_active = active;
2174 }
2175 return id;
2176}
2177
2193int n_gui_add_toggle_button(N_GUI_CTX* ctx, int window_id, const char* label, float x, float y, float w, float h, int shape, int initial_state, void (*on_click)(int, void*), void* user_data) {
2194 int id = n_gui_add_button(ctx, window_id, label, x, y, w, h, shape, on_click, user_data);
2195 if (id < 0) return -1;
2196 N_GUI_WIDGET* wgt = n_gui_get_widget(ctx, id);
2197 if (wgt && wgt->data) {
2199 bd->toggle_mode = 1;
2200 bd->toggled = initial_state ? 1 : 0;
2201 }
2202 return id;
2203}
2204
2209int n_gui_button_is_toggled(N_GUI_CTX* ctx, int widget_id) {
2210 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
2211 if (w && w->type == N_GUI_TYPE_BUTTON && w->data) {
2212 return ((N_GUI_BUTTON_DATA*)w->data)->toggled;
2213 }
2214 return 0;
2215}
2216
2221void n_gui_button_set_label(N_GUI_CTX* ctx, int widget_id, const char* label) {
2222 if (!label) return;
2223 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
2224 if (w && w->type == N_GUI_TYPE_BUTTON && w->data) {
2226 snprintf(bd->label, sizeof(bd->label), "%s", label);
2227 }
2228}
2229
2240void n_gui_set_widget_tooltip(N_GUI_CTX* ctx, int widget_id, const char* text) {
2241 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
2242 if (!w) return;
2243 FreeNoLog(w->tooltip); /* replace any previous value */
2244 w->tooltip = (text && text[0]) ? strdup(text) : NULL;
2245}
2246
2247/* Apply a saved datagrid column layout (order/visibility/width) to a live grid:
2248 * visible/width are per-column at index d, order[d] is the physical column shown at
2249 * display position d (grids start in identity display order). Mirrors the capture in
2250 * n_gui_save_layout_json so the round-trip is exact. */
2251static void _datagrid_apply_columns(N_GUI_CTX* ctx, int widget_id, int ncols, const int* order, const int* visible, const float* width) {
2252 int dp;
2253 for (dp = 0; dp < ncols; dp++) {
2254 n_gui_datagrid_set_column_visible(ctx, widget_id, dp, visible[dp]);
2255 if (width[dp] > 0.0f)
2256 n_gui_datagrid_set_column_width(ctx, widget_id, dp, width[dp]);
2257 }
2258 for (dp = 0; dp < ncols; dp++) {
2259 int cur;
2260 for (cur = dp; cur < ncols; cur++)
2261 if (n_gui_datagrid_display_to_physical(ctx, widget_id, cur) == order[dp])
2262 break;
2263 if (cur < ncols && cur != dp)
2264 n_gui_datagrid_move_column(ctx, widget_id, cur, dp);
2265 }
2266}
2267
2268/* Apply the first unconsumed pending-widget entry matching (window title, key) to a
2269 * freshly keyed widget, then mark it consumed (the widget-level analog of the
2270 * pending_layout apply in n_gui_add_window). */
2271static void _apply_pending_widget(N_GUI_CTX* ctx, const char* title, const N_GUI_WIDGET* wgt) {
2272 int i;
2273 if (!ctx || !ctx->pending_widgets || !title || !wgt || !wgt->persist_key[0])
2274 return;
2275 for (i = 0; i < ctx->pending_widgets_count; i++) {
2277 if (p->consumed) continue;
2278 if (strncmp(p->title, title, N_GUI_ID_MAX) != 0) continue;
2279 if (strncmp(p->key, wgt->persist_key, N_GUI_ID_MAX) != 0) continue;
2280 if (p->kind != wgt->type) continue; /* the widget type changed since the save */
2281 if (wgt->type == N_GUI_TYPE_SPLITPANE) {
2282 n_gui_splitpane_set_ratio(ctx, wgt->id, p->ratio);
2283 } else if (wgt->type == N_GUI_TYPE_DATAGRID) {
2284 int live = (int)n_gui_datagrid_get_column_count(ctx, wgt->id);
2285 if (live == p->ncols && p->ncols > 0)
2286 _datagrid_apply_columns(ctx, wgt->id, p->ncols, p->order, p->visible, p->width);
2287 }
2288 p->consumed = 1;
2289 return;
2290 }
2291}
2292
2293void n_gui_widget_set_persist_key(N_GUI_CTX* ctx, int widget_id, const char* key) {
2294 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
2295 float ox = 0.0f, oy = 0.0f;
2296 const N_GUI_WINDOW* win;
2297 if (!w) return;
2298 snprintf(w->persist_key, sizeof(w->persist_key), "%s", key ? key : "");
2299 if (!w->persist_key[0]) return;
2300 /* if a saved layout was already loaded, restore this widget's state now */
2301 win = _find_widget_window(ctx, widget_id, &ox, &oy);
2302 if (win)
2303 _apply_pending_widget(ctx, win->title, w);
2304}
2305
2306const char* n_gui_widget_get_persist_key(N_GUI_CTX* ctx, int widget_id) {
2307 const N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
2308 return w ? w->persist_key : NULL;
2309}
2310
2324 if (!ctx || ctx->style.tooltip_delay <= 0.0f || ctx->tooltip_widget_id < 0) return 0;
2325 return (al_get_time() - ctx->tooltip_armed_at >= (double)ctx->style.tooltip_delay) ? 2 : 1;
2326}
2327
2329static int _tooltip_widget_at(N_GUI_CTX* ctx, float px, float py) {
2330 int found = -1;
2331 /* ctx->windows is ordered back to front: the last hit wins */
2332 list_foreach(wnode, ctx->windows) {
2333 N_GUI_WINDOW* win = (N_GUI_WINDOW*)wnode->ptr;
2334 if (!win || !(win->state & N_GUI_WIN_OPEN)) continue;
2335 /* display routing: only windows on the pass the pointer is on */
2336 if (!_win_on_pass(ctx, win)) continue;
2337 float ox = win->x - win->scroll_x;
2338 float oy = win->y + _win_tbh(win) - win->scroll_y;
2339 list_foreach(wgn, win->widgets) {
2340 const N_GUI_WIDGET* wgt = (N_GUI_WIDGET*)wgn->ptr;
2341 if (!wgt || !wgt->visible || !wgt->tooltip || !wgt->tooltip[0]) continue;
2342 if (_point_in_rect(px, py, ox + wgt->x, oy + wgt->y, wgt->w, wgt->h))
2343 found = wgt->id;
2344 }
2345 }
2346 return found;
2347}
2348
2365void n_gui_button_set_state_bitmaps(N_GUI_CTX* ctx, int widget_id, ALLEGRO_BITMAP* normal, ALLEGRO_BITMAP* hover, ALLEGRO_BITMAP* active) {
2366 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
2367 if (w && w->type == N_GUI_TYPE_BUTTON && w->data) {
2370 bd->bitmap = normal;
2371 bd->bitmap_hover = hover;
2372 bd->bitmap_active = active;
2373 }
2374}
2375
2379void n_gui_button_set_toggled(N_GUI_CTX* ctx, int widget_id, int toggled) {
2380 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
2381 if (w && w->type == N_GUI_TYPE_BUTTON && w->data) {
2382 ((N_GUI_BUTTON_DATA*)w->data)->toggled = toggled ? 1 : 0;
2383 }
2384}
2385
2392void n_gui_button_set_toggle_mode(N_GUI_CTX* ctx, int widget_id, int toggle_mode) {
2393 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
2394 if (w && w->type == N_GUI_TYPE_BUTTON && w->data) {
2396 bd->toggle_mode = toggle_mode ? 1 : 0;
2397 if (!bd->toggle_mode) bd->toggled = 0;
2398 }
2399}
2400
2411void n_gui_button_set_keycode(N_GUI_CTX* ctx, int widget_id, int keycode, int modifiers) {
2412 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
2413 if (w && w->type == N_GUI_TYPE_BUTTON && w->data) {
2415 bd->keycode = keycode;
2416 bd->key_modifiers = modifiers & N_GUI_KEY_MOD_MASK;
2417 }
2418}
2419
2421void n_gui_button_set_keycode_focused(N_GUI_CTX* ctx, int widget_id, int keycode, int modifiers, const int* sources, int source_count) {
2422 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
2423 if (w && w->type == N_GUI_TYPE_BUTTON && w->data) {
2425 bd->keycode = keycode;
2426 bd->key_modifiers = modifiers & N_GUI_KEY_MOD_MASK;
2427 bd->key_focus_only = 1;
2428 int count = source_count < N_GUI_KEY_SOURCES_MAX ? source_count : N_GUI_KEY_SOURCES_MAX;
2429 for (int i = 0; i < count; i++) {
2430 bd->key_sources[i] = sources[i];
2431 }
2432 if (count < N_GUI_KEY_SOURCES_MAX) {
2433 bd->key_sources[count] = -1;
2434 }
2435 }
2436}
2437
2442int n_gui_add_slider(N_GUI_CTX* ctx, int window_id, float x, float y, float w, float h, double min_val, double max_val, double initial, int mode, void (*on_change)(int, double, void*), void* user_data) {
2443 __n_assert(ctx, return -1);
2444 N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
2445 __n_assert(win, return -1);
2446
2447 N_GUI_WIDGET* wgt = _new_widget(ctx, N_GUI_TYPE_SLIDER, x, y, w, h);
2448 __n_assert(wgt, return -1);
2449
2450 N_GUI_SLIDER_DATA* sd = NULL;
2451 Malloc(sd, N_GUI_SLIDER_DATA, 1);
2452 __n_assert(sd, Free(wgt); return -1);
2453 if (mode == N_GUI_SLIDER_PERCENT) {
2454 sd->min_val = 0.0;
2455 sd->max_val = 100.0;
2456 } else {
2457 sd->min_val = min_val;
2458 sd->max_val = max_val;
2459 }
2460 sd->step = 0.0; /* 0 = no step constraint (treated as continuous, snaps with step=1 only when explicitly set) */
2461 sd->value = _clamp(initial, sd->min_val, sd->max_val);
2462 sd->mode = mode;
2464 sd->on_change = on_change;
2465 sd->user_data = user_data;
2466 sd->value_format[0] = '\0'; /* empty = legacy defaults */
2467 sd->value_visible = 1; /* built-in readout on by default */
2468 wgt->data = sd;
2469 wgt->norm_x = 0.0f;
2470 wgt->norm_y = 0.0f;
2471 wgt->norm_w = 0.0f;
2472 wgt->norm_h = 0.0f;
2473
2474 list_push(win->widgets, wgt, _destroy_widget);
2476 _register_widget(ctx, wgt);
2477 return wgt->id;
2478}
2479
2484int n_gui_add_vslider(N_GUI_CTX* ctx, int window_id, float x, float y, float w, float h, double min_val, double max_val, double initial, int mode, void (*on_change)(int, double, void*), void* user_data) {
2485 int id = n_gui_add_slider(ctx, window_id, x, y, w, h, min_val, max_val, initial, mode, on_change, user_data);
2486 if (id < 0) return -1;
2487 N_GUI_WIDGET* wgt = n_gui_get_widget(ctx, id);
2488 if (wgt && wgt->data) {
2489 ((N_GUI_SLIDER_DATA*)wgt->data)->orientation = N_GUI_SLIDER_V;
2490 }
2491 return id;
2492}
2493
2498int 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) {
2499 __n_assert(ctx, return -1);
2500 N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
2501 __n_assert(win, return -1);
2502
2503 N_GUI_WIDGET* wgt = _new_widget(ctx, N_GUI_TYPE_TEXTAREA, x, y, w, h);
2504 __n_assert(wgt, return -1);
2505
2506 N_GUI_TEXTAREA_DATA* td = NULL;
2508 __n_assert(td, Free(wgt); return -1);
2509 memset(td, 0, sizeof(*td));
2510 td->char_limit = (char_limit > 0) ? char_limit : N_GUI_TEXT_MAX - 1;
2511 td->text_alloc = td->char_limit + 1;
2512 Malloc(td->text, char, td->text_alloc);
2513 __n_assert(td->text, Free(td); Free(wgt); return -1);
2514 td->text[0] = '\0';
2515 td->text_len = 0;
2516 td->multiline = multiline;
2517 td->cursor_pos = 0;
2518 td->sel_start = 0;
2519 td->sel_end = 0;
2520 td->scroll_y = 0;
2521 td->scroll_x = 0.0f;
2522 td->cursor_time = 0.0;
2523 td->bg_bitmap = NULL;
2524 td->mask_char = '\0';
2525 td->on_change = on_change;
2526 td->user_data = user_data;
2527 wgt->data = td;
2528 wgt->norm_x = 0.0f;
2529 wgt->norm_y = 0.0f;
2530 wgt->norm_w = 0.0f;
2531 wgt->norm_h = 0.0f;
2532
2533 list_push(win->widgets, wgt, _destroy_widget);
2535 _register_widget(ctx, wgt);
2536 return wgt->id;
2537}
2538
2543int n_gui_add_checkbox(N_GUI_CTX* ctx, int window_id, const char* label, float x, float y, float w, float h, int initial_checked, void (*on_toggle)(int, int, void*), void* user_data) {
2544 __n_assert(ctx, return -1);
2545 N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
2546 __n_assert(win, return -1);
2547
2548 N_GUI_WIDGET* wgt = _new_widget(ctx, N_GUI_TYPE_CHECKBOX, x, y, w, h);
2549 __n_assert(wgt, return -1);
2550
2551 N_GUI_CHECKBOX_DATA* cd = NULL;
2553 __n_assert(cd, Free(wgt); return -1);
2554 if (label) {
2555 strncpy(cd->label, label, N_GUI_ID_MAX - 1);
2556 cd->label[N_GUI_ID_MAX - 1] = '\0';
2557 }
2558 cd->checked = initial_checked ? 1 : 0;
2559 cd->on_toggle = on_toggle;
2560 cd->user_data = user_data;
2561 wgt->data = cd;
2562 wgt->norm_x = 0.0f;
2563 wgt->norm_y = 0.0f;
2564 wgt->norm_w = 0.0f;
2565 wgt->norm_h = 0.0f;
2566
2567 list_push(win->widgets, wgt, _destroy_widget);
2569 _register_widget(ctx, wgt);
2570 return wgt->id;
2571}
2572
2577int n_gui_add_scrollbar(N_GUI_CTX* ctx, int window_id, float x, float y, float w, float h, int orientation, int shape, double content_size, double viewport_size, void (*on_scroll)(int, double, void*), void* user_data) {
2578 __n_assert(ctx, return -1);
2579 N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
2580 __n_assert(win, return -1);
2581
2582 N_GUI_WIDGET* wgt = _new_widget(ctx, N_GUI_TYPE_SCROLLBAR, x, y, w, h);
2583 __n_assert(wgt, return -1);
2584
2585 N_GUI_SCROLLBAR_DATA* sb = NULL;
2587 __n_assert(sb, Free(wgt); return -1);
2588 sb->orientation = orientation;
2589 sb->content_size = content_size > 0 ? content_size : 1;
2590 sb->viewport_size = viewport_size > 0 ? viewport_size : 1;
2591 sb->scroll_pos = 0;
2592 sb->shape = shape;
2593 sb->on_scroll = on_scroll;
2594 sb->user_data = user_data;
2595 wgt->data = sb;
2596 wgt->norm_x = 0.0f;
2597 wgt->norm_y = 0.0f;
2598 wgt->norm_w = 0.0f;
2599 wgt->norm_h = 0.0f;
2600
2601 list_push(win->widgets, wgt, _destroy_widget);
2603 _register_widget(ctx, wgt);
2604 return wgt->id;
2605}
2606
2620int n_gui_add_listbox(N_GUI_CTX* ctx, int window_id, float x, float y, float w, float h, int selection_mode, void (*on_select)(int, int, int, void*), void* user_data) {
2621 __n_assert(ctx, return -1);
2622 N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
2623 __n_assert(win, return -1);
2624
2625 N_GUI_WIDGET* wgt = _new_widget(ctx, N_GUI_TYPE_LISTBOX, x, y, w, h);
2626 __n_assert(wgt, return -1);
2627
2628 N_GUI_LISTBOX_DATA* ld = NULL;
2629 Malloc(ld, N_GUI_LISTBOX_DATA, 1);
2630 __n_assert(ld, Free(wgt); return -1);
2631 ld->items = NULL;
2632 ld->nb_items = 0;
2633 ld->items_capacity = 0;
2634 ld->selection_mode = selection_mode;
2635 ld->scroll_offset = 0;
2637 ld->on_select = on_select;
2638 ld->user_data = user_data;
2639 ld->hover_row = -1;
2640 wgt->data = ld;
2641 wgt->norm_x = 0.0f;
2642 wgt->norm_y = 0.0f;
2643 wgt->norm_w = 0.0f;
2644 wgt->norm_h = 0.0f;
2645
2646 list_push(win->widgets, wgt, _destroy_widget);
2648 _register_widget(ctx, wgt);
2649 return wgt->id;
2650}
2651
2666int 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) {
2667 __n_assert(ctx, return -1);
2668 N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
2669 __n_assert(win, return -1);
2670
2671 N_GUI_WIDGET* wgt = _new_widget(ctx, N_GUI_TYPE_SPLITPANE, x, y, w, h);
2672 __n_assert(wgt, return -1);
2673
2674 N_GUI_SPLITPANE_DATA* sd = NULL;
2676 __n_assert(sd, Free(wgt); return -1);
2678 sd->min_ratio = 0.05f;
2679 sd->max_ratio = 0.95f;
2680 sd->ratio = (ratio < sd->min_ratio) ? sd->min_ratio : (ratio > sd->max_ratio) ? sd->max_ratio
2681 : ratio;
2682 sd->divider = 6.0f;
2683 sd->on_change = on_change;
2684 sd->user_data = user_data;
2685 wgt->data = sd;
2686 wgt->norm_x = 0.0f;
2687 wgt->norm_y = 0.0f;
2688 wgt->norm_w = 0.0f;
2689 wgt->norm_h = 0.0f;
2690
2691 list_push(win->widgets, wgt, _destroy_widget);
2693 _register_widget(ctx, wgt);
2694 return wgt->id;
2695}
2696
2707int n_gui_add_hexview(N_GUI_CTX* ctx, int window_id, float x, float y, float w, float h) {
2708 __n_assert(ctx, return -1);
2709 N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
2710 __n_assert(win, return -1);
2711
2712 N_GUI_WIDGET* wgt = _new_widget(ctx, N_GUI_TYPE_HEXVIEW, x, y, w, h);
2713 __n_assert(wgt, return -1);
2714
2715 N_GUI_HEXVIEW_DATA* hd = NULL;
2716 Malloc(hd, N_GUI_HEXVIEW_DATA, 1);
2717 __n_assert(hd, Free(wgt); return -1);
2718 hd->data = NULL;
2719 hd->len = 0;
2720 hd->bytes_per_row = 16;
2721 hd->scroll_offset = 0;
2722 wgt->data = hd;
2723 wgt->norm_x = 0.0f;
2724 wgt->norm_y = 0.0f;
2725 wgt->norm_w = 0.0f;
2726 wgt->norm_h = 0.0f;
2727
2728 list_push(win->widgets, wgt, _destroy_widget);
2730 _register_widget(ctx, wgt);
2731 return wgt->id;
2732}
2733
2745int n_gui_add_syntaxview(N_GUI_CTX* ctx, int window_id, float x, float y, float w, float h, int mode) {
2746 __n_assert(ctx, return -1);
2747 N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
2748 __n_assert(win, return -1);
2749
2750 N_GUI_WIDGET* wgt = _new_widget(ctx, N_GUI_TYPE_SYNTAXVIEW, x, y, w, h);
2751 __n_assert(wgt, return -1);
2752
2753 N_GUI_SYNTAXVIEW_DATA* yd = NULL;
2755 __n_assert(yd, Free(wgt); return -1);
2756 yd->text = NULL;
2757 yd->len = 0;
2758 yd->mode = mode;
2759 yd->scroll_offset = 0;
2760 yd->sel_start = -1;
2761 yd->sel_end = -1;
2762 yd->sel_dragging = 0;
2763 wgt->data = yd;
2764 wgt->norm_x = 0.0f;
2765 wgt->norm_y = 0.0f;
2766 wgt->norm_w = 0.0f;
2767 wgt->norm_h = 0.0f;
2768
2769 list_push(win->widgets, wgt, _destroy_widget);
2771 _register_widget(ctx, wgt);
2772 return wgt->id;
2773}
2774
2787int n_gui_add_datagrid(N_GUI_CTX* ctx, int window_id, float x, float y, float w, float h, void (*on_select)(int, int, void*), void* user_data) {
2788 __n_assert(ctx, return -1);
2789 N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
2790 __n_assert(win, return -1);
2791
2792 N_GUI_WIDGET* wgt = _new_widget(ctx, N_GUI_TYPE_DATAGRID, x, y, w, h);
2793 __n_assert(wgt, return -1);
2794
2795 N_GUI_DATAGRID_DATA* gd = NULL;
2797 __n_assert(gd, Free(wgt); return -1);
2798 gd->cols = NULL;
2799 gd->nb_cols = 0;
2800 gd->cols_cap = 0;
2801 gd->col_order = NULL;
2802 gd->col_resize_col = -1;
2803 gd->col_resize_x0 = 0.0f;
2804 gd->col_resize_w0 = 0.0f;
2805 gd->on_columns_changed = NULL;
2806 gd->cells = NULL;
2807 gd->nb_rows = 0;
2808 gd->rows_cap = 0;
2809 gd->sort_col = -1;
2810 gd->sort_dir = 1;
2811 gd->selected_row = -1;
2812 gd->multiselect = 0;
2813 gd->row_sel = NULL;
2814 gd->anchor_row = -1;
2815 gd->row_color = NULL;
2816 gd->row_has_color = NULL;
2817 gd->scroll_offset = 0;
2818 gd->h_scroll = 0.0f;
2819 gd->h_scroll_dragging = 0;
2820 gd->on_select = on_select;
2821 gd->on_context = NULL;
2822 gd->user_data = user_data;
2823 wgt->data = gd;
2824 wgt->norm_x = 0.0f;
2825 wgt->norm_y = 0.0f;
2826 wgt->norm_w = 0.0f;
2827 wgt->norm_h = 0.0f;
2828
2829 list_push(win->widgets, wgt, _destroy_widget);
2831 _register_widget(ctx, wgt);
2832 return wgt->id;
2833}
2834
2839int n_gui_add_radiolist(N_GUI_CTX* ctx, int window_id, float x, float y, float w, float h, void (*on_select)(int, int, void*), void* user_data) {
2840 __n_assert(ctx, return -1);
2841 N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
2842 __n_assert(win, return -1);
2843
2844 N_GUI_WIDGET* wgt = _new_widget(ctx, N_GUI_TYPE_RADIOLIST, x, y, w, h);
2845 __n_assert(wgt, return -1);
2846
2847 N_GUI_RADIOLIST_DATA* rd = NULL;
2849 __n_assert(rd, Free(wgt); return -1);
2850 rd->items = NULL;
2851 rd->nb_items = 0;
2852 rd->items_capacity = 0;
2853 rd->selected_index = -1;
2854 rd->scroll_offset = 0;
2856 rd->on_select = on_select;
2857 rd->user_data = user_data;
2858 wgt->data = rd;
2859 wgt->norm_x = 0.0f;
2860 wgt->norm_y = 0.0f;
2861 wgt->norm_w = 0.0f;
2862 wgt->norm_h = 0.0f;
2863
2864 list_push(win->widgets, wgt, _destroy_widget);
2866 _register_widget(ctx, wgt);
2867 return wgt->id;
2868}
2869
2874int n_gui_add_combobox(N_GUI_CTX* ctx, int window_id, float x, float y, float w, float h, void (*on_select)(int, int, void*), void* user_data) {
2875 __n_assert(ctx, return -1);
2876 N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
2877 __n_assert(win, return -1);
2878
2879 N_GUI_WIDGET* wgt = _new_widget(ctx, N_GUI_TYPE_COMBOBOX, x, y, w, h);
2880 __n_assert(wgt, return -1);
2881
2882 N_GUI_COMBOBOX_DATA* cd = NULL;
2884 __n_assert(cd, Free(wgt); return -1);
2885 cd->items = NULL;
2886 cd->nb_items = 0;
2887 cd->items_capacity = 0;
2888 cd->selected_index = -1;
2889 cd->is_open = 0;
2890 cd->scroll_offset = 0;
2891 cd->highlight_index = -1;
2892 cd->item_height = h;
2894 cd->on_select = on_select;
2895 cd->user_data = user_data;
2896 cd->flags = 0;
2897 wgt->data = cd;
2898 wgt->norm_x = 0.0f;
2899 wgt->norm_y = 0.0f;
2900 wgt->norm_w = 0.0f;
2901 wgt->norm_h = 0.0f;
2902
2903 list_push(win->widgets, wgt, _destroy_widget);
2905 _register_widget(ctx, wgt);
2906 return wgt->id;
2907}
2908
2915void n_gui_combobox_set_flags(N_GUI_CTX* ctx, int widget_id, int flags) {
2916 __n_assert(ctx, return);
2917 N_GUI_WIDGET* wgt = n_gui_get_widget(ctx, widget_id);
2918 __n_assert(wgt, return);
2919 if (wgt->type != N_GUI_TYPE_COMBOBOX) return;
2921 __n_assert(cd, return);
2922 cd->flags = flags;
2923}
2924
2937int n_gui_add_image(N_GUI_CTX* ctx, int window_id, float x, float y, float w, float h, ALLEGRO_BITMAP* bitmap, int scale_mode) {
2938 __n_assert(ctx, return -1);
2939 N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
2940 __n_assert(win, return -1);
2941
2942 N_GUI_WIDGET* wgt = _new_widget(ctx, N_GUI_TYPE_IMAGE, x, y, w, h);
2943 __n_assert(wgt, return -1);
2944
2945 N_GUI_IMAGE_DATA* id = NULL;
2946 Malloc(id, N_GUI_IMAGE_DATA, 1);
2947 __n_assert(id, Free(wgt); return -1);
2948 id->bitmap = bitmap;
2949 id->scale_mode = scale_mode;
2950 wgt->data = id;
2951 wgt->norm_x = 0.0f;
2952 wgt->norm_y = 0.0f;
2953 wgt->norm_w = 0.0f;
2954 wgt->norm_h = 0.0f;
2955
2956 list_push(win->widgets, wgt, _destroy_widget);
2958 _register_widget(ctx, wgt);
2959 return wgt->id;
2960}
2961
2966int n_gui_add_label(N_GUI_CTX* ctx, int window_id, const char* text, float x, float y, float w, float h, int align) {
2967 __n_assert(ctx, return -1);
2968 N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
2969 __n_assert(win, return -1);
2970
2971 N_GUI_WIDGET* wgt = _new_widget(ctx, N_GUI_TYPE_LABEL, x, y, w, h);
2972 __n_assert(wgt, return -1);
2973
2974 N_GUI_LABEL_DATA* lb = NULL;
2975 Malloc(lb, N_GUI_LABEL_DATA, 1);
2976 __n_assert(lb, Free(wgt); return -1);
2977 lb->text[0] = '\0';
2978 if (text) {
2979 strncpy(lb->text, text, N_GUI_TEXT_MAX - 1);
2980 lb->text[N_GUI_TEXT_MAX - 1] = '\0';
2981 _normalize_crlf(lb->text); /* normalize CRLF for Allegro5 compatibility */
2982 }
2983 lb->link[0] = '\0';
2984 lb->align = align;
2985 lb->scroll_y = 0.0f;
2986 lb->sel_start = -1;
2987 lb->sel_end = -1;
2988 lb->sel_dragging = 0;
2989 lb->on_link_click = NULL;
2990 lb->user_data = NULL;
2991 wgt->data = lb;
2992 wgt->norm_x = 0.0f;
2993 wgt->norm_y = 0.0f;
2994 wgt->norm_w = 0.0f;
2995 wgt->norm_h = 0.0f;
2996
2997 list_push(win->widgets, wgt, _destroy_widget);
2999 _register_widget(ctx, wgt);
3000 return wgt->id;
3001}
3002
3007int n_gui_add_label_link(N_GUI_CTX* ctx, int window_id, const char* text, const char* link, float x, float y, float w, float h, int align, void (*on_link_click)(int, const char*, void*), void* user_data) {
3008 int wid = n_gui_add_label(ctx, window_id, text, x, y, w, h, align);
3009 if (wid < 0) return -1;
3010 N_GUI_WIDGET* wgt = n_gui_get_widget(ctx, wid);
3011 if (wgt && wgt->data) {
3013 if (link) {
3014 strncpy(lb->link, link, N_GUI_TEXT_MAX - 1);
3015 lb->link[N_GUI_TEXT_MAX - 1] = '\0';
3016 }
3018 lb->user_data = user_data;
3019 }
3020 return wid;
3021}
3022
3023/* WIDGET ACCESS */
3024
3029 __n_assert(ctx, return NULL);
3030 char key[32];
3031 snprintf(key, sizeof(key), "%d", widget_id);
3032 void* ptr = NULL;
3033 if (ht_get_ptr(ctx->widgets_by_id, key, &ptr) == TRUE && ptr) {
3034 return (N_GUI_WIDGET*)ptr;
3035 }
3036 return NULL;
3037}
3038
3042void n_gui_set_widget_theme(N_GUI_CTX* ctx, int widget_id, N_GUI_THEME theme) {
3043 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
3044 if (w) w->theme = theme;
3045}
3046
3057 if (!ctx || !ctx->windows) return;
3058 list_foreach(wnode, ctx->windows) {
3059 N_GUI_WINDOW* win = (N_GUI_WINDOW*)wnode->ptr;
3060 if (!win) continue;
3061 win->theme = ctx->default_theme;
3064 if (!win->widgets) continue;
3065 list_foreach(wgn, win->widgets) {
3066 N_GUI_WIDGET* w = (N_GUI_WIDGET*)wgn->ptr;
3067 if (w) w->theme = ctx->default_theme;
3068 }
3069 }
3070}
3071
3075void n_gui_set_widget_visible(N_GUI_CTX* ctx, int widget_id, int visible) {
3076 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
3077 if (w) w->visible = visible;
3078}
3079
3084void n_gui_set_widget_enabled(N_GUI_CTX* ctx, int widget_id, int enabled) {
3085 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
3086 if (w) w->enabled = enabled;
3087}
3088
3093int n_gui_is_widget_enabled(N_GUI_CTX* ctx, int widget_id) {
3094 const N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
3095 if (w) return w->enabled;
3096 return 0;
3097}
3098
3105void n_gui_set_focus(N_GUI_CTX* ctx, int widget_id) {
3106 __n_assert(ctx, return);
3107
3108 /* clear focus from previously focused widget */
3109 if (ctx->focused_widget_id >= 0) {
3111 if (prev) prev->state &= ~N_GUI_STATE_FOCUSED;
3112 }
3113
3114 if (widget_id < 0) {
3115 ctx->focused_widget_id = -1;
3116 return;
3117 }
3118
3119 N_GUI_WIDGET* wgt = n_gui_get_widget(ctx, widget_id);
3120 if (!wgt) {
3121 n_log(LOG_ERR, "n_gui_set_focus: widget %d not found", widget_id);
3122 ctx->focused_widget_id = -1;
3123 return;
3124 }
3125
3126 ctx->focused_widget_id = widget_id;
3127 wgt->state |= N_GUI_STATE_FOCUSED;
3128
3129 /* reset cursor blink timer for textareas */
3130 if (wgt->type == N_GUI_TYPE_TEXTAREA && wgt->data) {
3132 td->cursor_time = al_get_time();
3133 }
3134
3135 /* raise the parent window */
3136 list_foreach(wnode, ctx->windows) {
3137 N_GUI_WINDOW* win = (N_GUI_WINDOW*)wnode->ptr;
3138 if (!win || !win->widgets) continue;
3139 list_foreach(wgnode, win->widgets) {
3140 const N_GUI_WIDGET* w = (const N_GUI_WIDGET*)wgnode->ptr;
3141 if (w && w->id == widget_id) {
3142 if (win->state & N_GUI_WIN_OPEN) {
3143 n_gui_raise_window(ctx, win->id);
3144 }
3145 return;
3146 }
3147 }
3148 }
3149}
3150
3151/* slider helpers */
3152
3159double n_gui_slider_get_value(N_GUI_CTX* ctx, int widget_id) {
3160 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
3161 if (w && w->type == N_GUI_TYPE_SLIDER && w->data) {
3162 return ((N_GUI_SLIDER_DATA*)w->data)->value;
3163 }
3164 return 0.0;
3165}
3166
3173void n_gui_slider_set_value(N_GUI_CTX* ctx, int widget_id, double value) {
3174 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
3175 if (w && w->type == N_GUI_TYPE_SLIDER && w->data) {
3177 if (sd->step > 0.0)
3178 sd->value = _slider_snap_value(value, sd->min_val, sd->max_val, sd->step);
3179 else
3180 sd->value = _clamp(value, sd->min_val, sd->max_val);
3181 }
3182}
3183
3191void n_gui_slider_set_range(N_GUI_CTX* ctx, int widget_id, double min_val, double max_val) {
3192 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
3193 if (w && w->type == N_GUI_TYPE_SLIDER && w->data) {
3195 sd->min_val = min_val;
3196 sd->max_val = max_val;
3197 if (sd->step > 0.0)
3198 sd->value = _slider_snap_value(sd->value, min_val, max_val, sd->step);
3199 else
3200 sd->value = _clamp(sd->value, min_val, max_val);
3201 }
3202}
3203
3211void n_gui_slider_set_step(N_GUI_CTX* ctx, int widget_id, double step) {
3212 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
3213 if (w && w->type == N_GUI_TYPE_SLIDER && w->data) {
3215 sd->step = step;
3216 if (step > 0.0) {
3217 sd->value = _slider_snap_value(sd->value, sd->min_val, sd->max_val, step);
3218 }
3219 }
3220}
3221
3222void n_gui_slider_set_value_format(N_GUI_CTX* ctx, int widget_id, const char* fmt) {
3223 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
3224 if (!w || w->type != N_GUI_TYPE_SLIDER || !w->data) return;
3226 if (!fmt || !fmt[0]) {
3227 sd->value_format[0] = '\0';
3228 } else {
3229 /* snprintf truncates safely at the buffer bound. */
3230 snprintf(sd->value_format, sizeof(sd->value_format), "%s", fmt);
3231 }
3232}
3233
3234void n_gui_slider_set_value_visible(N_GUI_CTX* ctx, int widget_id, int visible) {
3235 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
3236 if (!w || w->type != N_GUI_TYPE_SLIDER || !w->data) return;
3238 sd->value_visible = visible ? 1 : 0;
3239}
3240
3241/* textarea helpers */
3242
3249const char* n_gui_textarea_get_text(N_GUI_CTX* ctx, int widget_id) {
3250 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
3251 if (w && w->type == N_GUI_TYPE_TEXTAREA && w->data) {
3252 return ((N_GUI_TEXTAREA_DATA*)w->data)->text;
3253 }
3254 return "";
3255}
3256
3263void n_gui_textarea_set_text(N_GUI_CTX* ctx, int widget_id, const char* text) {
3264 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
3265 if (w && w->type == N_GUI_TYPE_TEXTAREA && w->data) {
3267 if (text) {
3268 strncpy(td->text, text, td->char_limit);
3269 td->text[td->char_limit] = '\0';
3270 _normalize_crlf(td->text); /* normalize CRLF for Allegro5 compatibility */
3271 td->text_len = strlen(td->text);
3272 td->cursor_pos = td->text_len;
3273 } else {
3274 td->text[0] = '\0';
3275 td->text_len = 0;
3276 td->cursor_pos = 0;
3277 }
3278 td->scroll_x = 0.0f;
3279 }
3280}
3281
3282/* checkbox helpers */
3283
3290int n_gui_checkbox_is_checked(N_GUI_CTX* ctx, int widget_id) {
3291 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
3292 if (w && w->type == N_GUI_TYPE_CHECKBOX && w->data) {
3293 return ((N_GUI_CHECKBOX_DATA*)w->data)->checked;
3294 }
3295 return 0;
3296}
3297
3304void n_gui_checkbox_set_checked(N_GUI_CTX* ctx, int widget_id, int checked) {
3305 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
3306 if (w && w->type == N_GUI_TYPE_CHECKBOX && w->data) {
3307 ((N_GUI_CHECKBOX_DATA*)w->data)->checked = checked ? 1 : 0;
3308 }
3309}
3310
3311/* scrollbar helpers */
3312
3319double n_gui_scrollbar_get_pos(N_GUI_CTX* ctx, int widget_id) {
3320 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
3321 if (w && w->type == N_GUI_TYPE_SCROLLBAR && w->data) {
3322 return ((N_GUI_SCROLLBAR_DATA*)w->data)->scroll_pos;
3323 }
3324 return 0.0;
3325}
3326
3333void n_gui_scrollbar_set_pos(N_GUI_CTX* ctx, int widget_id, double pos) {
3334 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
3335 if (w && w->type == N_GUI_TYPE_SCROLLBAR && w->data) {
3337 double max_scroll = sb->content_size - sb->viewport_size;
3338 if (max_scroll < 0) max_scroll = 0;
3339 sb->scroll_pos = _clamp(pos, 0, max_scroll);
3340 }
3341}
3342
3350void n_gui_scrollbar_set_sizes(N_GUI_CTX* ctx, int widget_id, double content_size, double viewport_size) {
3351 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
3352 if (w && w->type == N_GUI_TYPE_SCROLLBAR && w->data) {
3354 sb->content_size = content_size > 0 ? content_size : 1;
3355 sb->viewport_size = viewport_size > 0 ? viewport_size : 1;
3356 double max_scroll = sb->content_size - sb->viewport_size;
3357 if (max_scroll < 0) max_scroll = 0;
3358 sb->scroll_pos = _clamp(sb->scroll_pos, 0, max_scroll);
3359 }
3360}
3361
3362/* listbox helpers */
3363
3371int n_gui_listbox_add_item(N_GUI_CTX* ctx, int widget_id, const char* text) {
3372 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
3373 if (!w || w->type != N_GUI_TYPE_LISTBOX || !w->data) return -1;
3375 if (!_items_grow(&ld->items, &ld->nb_items, &ld->items_capacity)) return -1;
3376 N_GUI_LISTITEM* item = &ld->items[ld->nb_items];
3377 item->text[0] = '\0';
3378 if (text) {
3379 strncpy(item->text, text, N_GUI_ID_MAX - 1);
3380 item->text[N_GUI_ID_MAX - 1] = '\0';
3381 }
3382 item->selected = 0;
3383 ld->nb_items++;
3384 return (int)(ld->nb_items - 1);
3385}
3386
3394int n_gui_listbox_remove_item(N_GUI_CTX* ctx, int widget_id, int index) {
3395 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
3396 if (!w || w->type != N_GUI_TYPE_LISTBOX || !w->data) return -1;
3398 if (index < 0 || (size_t)index >= ld->nb_items) return -1;
3399 if ((size_t)index < ld->nb_items - 1) {
3400 memmove(&ld->items[index], &ld->items[index + 1],
3401 (ld->nb_items - (size_t)index - 1) * sizeof(N_GUI_LISTITEM));
3402 }
3403 ld->nb_items--;
3404 /* clamp scroll_offset after removal */
3405 if (ld->nb_items == 0) {
3406 ld->scroll_offset = 0;
3407 } else if (ld->scroll_offset > 0 && (size_t)ld->scroll_offset >= ld->nb_items) {
3408 ld->scroll_offset = (int)(ld->nb_items - 1);
3409 }
3410 return 0;
3411}
3412
3418void n_gui_listbox_clear(N_GUI_CTX* ctx, int widget_id) {
3419 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
3420 if (w && w->type == N_GUI_TYPE_LISTBOX && w->data) {
3422 ld->nb_items = 0;
3423 ld->scroll_offset = 0;
3424 }
3425}
3426
3433int n_gui_listbox_get_count(N_GUI_CTX* ctx, int widget_id) {
3434 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
3435 if (w && w->type == N_GUI_TYPE_LISTBOX && w->data) {
3436 return (int)((N_GUI_LISTBOX_DATA*)w->data)->nb_items;
3437 }
3438 return 0;
3439}
3440
3448const char* n_gui_listbox_get_item_text(N_GUI_CTX* ctx, int widget_id, int index) {
3449 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
3450 if (w && w->type == N_GUI_TYPE_LISTBOX && w->data) {
3452 if (index >= 0 && (size_t)index < ld->nb_items) {
3453 return ld->items[index].text;
3454 }
3455 }
3456 return "";
3457}
3458
3465int n_gui_listbox_get_selected(N_GUI_CTX* ctx, int widget_id) {
3466 const N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
3467 if (w && w->type == N_GUI_TYPE_LISTBOX && w->data) {
3468 const N_GUI_LISTBOX_DATA* ld = (const N_GUI_LISTBOX_DATA*)w->data;
3469 for (size_t i = 0; i < ld->nb_items; i++) {
3470 if (ld->items[i].selected) return (int)i;
3471 }
3472 }
3473 return -1;
3474}
3475
3483int n_gui_listbox_is_selected(N_GUI_CTX* ctx, int widget_id, int index) {
3484 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
3485 if (w && w->type == N_GUI_TYPE_LISTBOX && w->data) {
3487 if (index >= 0 && (size_t)index < ld->nb_items) {
3488 return ld->items[index].selected;
3489 }
3490 }
3491 return 0;
3492}
3493
3501void n_gui_listbox_set_selected(N_GUI_CTX* ctx, int widget_id, int index, int selected) {
3502 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
3503 if (!w || w->type != N_GUI_TYPE_LISTBOX || !w->data) return;
3505 if (index < 0 || (size_t)index >= ld->nb_items) return;
3506 if (ld->selection_mode == N_GUI_SELECT_SINGLE && selected) {
3507 for (size_t i = 0; i < ld->nb_items; i++) ld->items[i].selected = 0;
3508 }
3509 ld->items[index].selected = selected ? 1 : 0;
3510}
3511
3513 const N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
3514 if (!w || w->type != N_GUI_TYPE_LISTBOX || !w->data) return 0;
3515 const N_GUI_LISTBOX_DATA* ld = (const N_GUI_LISTBOX_DATA*)w->data;
3516 return ld->scroll_offset;
3517}
3518
3519void n_gui_listbox_set_scroll_offset(N_GUI_CTX* ctx, int widget_id, int offset) {
3520 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
3521 if (!w || w->type != N_GUI_TYPE_LISTBOX || !w->data) return;
3523 int max_offset = (int)ld->nb_items - (int)(w->h / ld->item_height);
3524 if (max_offset < 0) max_offset = 0;
3525 if (offset < 0) offset = 0;
3526 if (offset > max_offset) offset = max_offset;
3527 ld->scroll_offset = offset;
3528}
3529
3530/* split pane helpers */
3531
3538float n_gui_splitpane_get_ratio(N_GUI_CTX* ctx, int widget_id) {
3539 const N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
3540 if (!w || w->type != N_GUI_TYPE_SPLITPANE || !w->data) return 0.5f;
3541 return ((const N_GUI_SPLITPANE_DATA*)w->data)->ratio;
3542}
3543
3550void n_gui_splitpane_set_ratio(N_GUI_CTX* ctx, int widget_id, float ratio) {
3551 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
3552 if (!w || w->type != N_GUI_TYPE_SPLITPANE || !w->data) return;
3554 if (ratio < sd->min_ratio) ratio = sd->min_ratio;
3555 if (ratio > sd->max_ratio) ratio = sd->max_ratio;
3556 sd->ratio = ratio;
3557}
3558
3566void n_gui_splitpane_set_limits(N_GUI_CTX* ctx, int widget_id, float min_ratio, float max_ratio) {
3567 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
3568 if (!w || w->type != N_GUI_TYPE_SPLITPANE || !w->data) return;
3570 if (min_ratio < 0.0f) min_ratio = 0.0f;
3571 if (max_ratio > 1.0f) max_ratio = 1.0f;
3572 if (min_ratio > max_ratio) min_ratio = max_ratio;
3573 sd->min_ratio = min_ratio;
3574 sd->max_ratio = max_ratio;
3575 if (sd->ratio < min_ratio) sd->ratio = min_ratio;
3576 if (sd->ratio > max_ratio) sd->ratio = max_ratio;
3577}
3578
3579/* hex view helpers */
3580
3588void n_gui_hexview_set_data(N_GUI_CTX* ctx, int widget_id, const unsigned char* data, size_t len) {
3589 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
3590 if (!w || w->type != N_GUI_TYPE_HEXVIEW || !w->data) return;
3592 FreeNoLog(hd->data);
3593 hd->len = 0;
3594 hd->scroll_offset = 0;
3595 if (data && len > 0) {
3596 Malloc(hd->data, unsigned char, len);
3597 if (hd->data) {
3598 memcpy(hd->data, data, len);
3599 hd->len = len;
3600 }
3601 }
3602}
3603
3610size_t n_gui_hexview_get_length(N_GUI_CTX* ctx, int widget_id) {
3611 const N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
3612 if (!w || w->type != N_GUI_TYPE_HEXVIEW || !w->data) return 0;
3613 return ((const N_GUI_HEXVIEW_DATA*)w->data)->len;
3614}
3615
3616/* syntax view helpers */
3617
3624void n_gui_syntaxview_set_text(N_GUI_CTX* ctx, int widget_id, const char* text) {
3625 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
3626 if (!w || w->type != N_GUI_TYPE_SYNTAXVIEW || !w->data) return;
3628 FreeNoLog(yd->text);
3629 yd->len = 0;
3630 yd->scroll_offset = 0;
3631 /* the cached line count / headers-end no longer describe this text */
3632 yd->lines_valid = 0;
3633 /* the byte offsets no longer refer to anything, so drop any selection */
3634 yd->sel_start = -1;
3635 yd->sel_end = -1;
3636 yd->sel_dragging = 0;
3637 if (ctx && ctx->selected_syntaxview_id == widget_id)
3638 ctx->selected_syntaxview_id = -1;
3639 if (text && text[0]) {
3640 size_t len = strlen(text);
3641 Malloc(yd->text, char, len + 1);
3642 if (yd->text) {
3643 memcpy(yd->text, text, len + 1);
3644 yd->len = len;
3645 }
3646 }
3647}
3648
3655void n_gui_syntaxview_set_mode(N_GUI_CTX* ctx, int widget_id, int mode) {
3656 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
3657 if (!w || w->type != N_GUI_TYPE_SYNTAXVIEW || !w->data) return;
3658 ((N_GUI_SYNTAXVIEW_DATA*)w->data)->mode = mode;
3659}
3660
3668 const N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
3669 if (!w || w->type != N_GUI_TYPE_SYNTAXVIEW || !w->data) return 0;
3670 return _text_line_count(((const N_GUI_SYNTAXVIEW_DATA*)w->data)->text);
3671}
3672
3679const char* n_gui_syntaxview_get_text(N_GUI_CTX* ctx, int widget_id) {
3680 const N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
3681 if (!w || w->type != N_GUI_TYPE_SYNTAXVIEW || !w->data) return NULL;
3682 return ((const N_GUI_SYNTAXVIEW_DATA*)w->data)->text;
3683}
3684
3693 const N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
3694 if (!w || w->type != N_GUI_TYPE_SYNTAXVIEW || !w->data) return NULL;
3695 const N_GUI_SYNTAXVIEW_DATA* yd = (const N_GUI_SYNTAXVIEW_DATA*)w->data;
3696 if (!yd->text || yd->sel_start < 0 || yd->sel_end < 0 || yd->sel_start == yd->sel_end)
3697 return NULL;
3698 int lo = yd->sel_start < yd->sel_end ? yd->sel_start : yd->sel_end;
3699 int hi = yd->sel_start < yd->sel_end ? yd->sel_end : yd->sel_start;
3700 if ((size_t)lo > yd->len) lo = (int)yd->len;
3701 if ((size_t)hi > yd->len) hi = (int)yd->len;
3702 int n = hi - lo;
3703 if (n <= 0) return NULL;
3704 char* out = NULL;
3705 Malloc(out, char, (size_t)n + 1);
3706 if (!out) return NULL;
3707 memcpy(out, yd->text + lo, (size_t)n);
3708 out[n] = '\0';
3709 return out;
3710}
3711
3719void n_gui_syntaxview_set_selection(N_GUI_CTX* ctx, int widget_id, int start, int end) {
3720 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
3721 if (!w || w->type != N_GUI_TYPE_SYNTAXVIEW || !w->data) return;
3723 int len = (int)yd->len;
3724 if (start < 0) start = 0;
3725 if (end < 0) end = 0;
3726 if (start > len) start = len;
3727 if (end > len) end = len;
3728 yd->sel_start = start;
3729 yd->sel_end = end;
3730 if (ctx && start != end)
3731 ctx->selected_syntaxview_id = widget_id;
3732}
3733
3739void n_gui_syntaxview_select_all(N_GUI_CTX* ctx, int widget_id) {
3740 const N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
3741 if (!w || w->type != N_GUI_TYPE_SYNTAXVIEW || !w->data) return;
3742 n_gui_syntaxview_set_selection(ctx, widget_id, 0, (int)((const N_GUI_SYNTAXVIEW_DATA*)w->data)->len);
3743}
3744
3751void n_gui_syntaxview_scroll_to_offset(N_GUI_CTX* ctx, int widget_id, int byte_offset) {
3752 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
3753 if (!w || w->type != N_GUI_TYPE_SYNTAXVIEW || !w->data) return;
3755 if (!yd->text || yd->len == 0) return;
3756 if (byte_offset < 0) byte_offset = 0;
3757 if ((size_t)byte_offset > yd->len) byte_offset = (int)yd->len;
3758
3759 int line = 0;
3760 for (int it = 0; it < byte_offset; it++) {
3761 if (yd->text[it] == '\n') line++;
3762 }
3763
3764 /* mirror the row math of _draw_syntaxview so the clamp agrees with it */
3765 ALLEGRO_FONT* font = w->font ? w->font : (ctx ? ctx->default_font : NULL);
3766 if (!font) return;
3767 float fh = (float)al_get_font_line_height(font);
3768 float pad = ctx->style.textarea_padding;
3769 float row_h = fh + 2.0f;
3770 int visible = (int)((w->h - pad * 2.0f) / row_h);
3771 if (visible < 1) visible = 1;
3772 int nb_lines = (_syntaxview_ensure_lines(yd), yd->cached_nb_lines);
3773 int max_off = nb_lines - visible;
3774 if (max_off < 0) max_off = 0;
3775 int target = line - visible / 2;
3776 if (target > max_off) target = max_off;
3777 if (target < 0) target = 0;
3778 yd->scroll_offset = target;
3779}
3780
3781/* data grid helpers */
3782
3784static int _cell_is_num(const char* s) {
3785 if (!s || !*s) return 0;
3786 while (*s) {
3787 if (*s < '0' || *s > '9') return 0;
3788 s++;
3789 }
3790 return 1;
3791}
3792
3794static int _cell_cmp(const char* a, const char* b) {
3795 if (!a) a = "";
3796 if (!b) b = "";
3797 if (_cell_is_num(a) && _cell_is_num(b)) {
3798 long la = atol(a), lb = atol(b);
3799 return (la < lb) ? -1 : (la > lb) ? 1
3800 : 0;
3801 }
3802 return strcasecmp(a, b);
3803}
3804
3805int n_gui_datagrid_add_column(N_GUI_CTX* ctx, int widget_id, const char* title, float width) {
3806 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
3807 if (!w || w->type != N_GUI_TYPE_DATAGRID || !w->data) return -1;
3809 if (gd->nb_rows > 0) return -1; /* columns must be defined before rows */
3810 if (gd->nb_cols >= gd->cols_cap) {
3811 size_t nc = gd->cols_cap ? gd->cols_cap * 2 : 4;
3812 N_GUI_DATAGRID_COL* tmp = NULL;
3813 size_t* ord = NULL;
3814 Malloc(tmp, N_GUI_DATAGRID_COL, nc);
3815 if (!tmp) return -1;
3816 if (gd->cols && gd->nb_cols > 0)
3817 memcpy(tmp, gd->cols, gd->nb_cols * sizeof(N_GUI_DATAGRID_COL));
3818 FreeNoLog(gd->cols);
3819 gd->cols = tmp;
3820 /* the display-order table grows alongside the columns */
3821 Malloc(ord, size_t, nc);
3822 if (ord) {
3823 if (gd->col_order && gd->nb_cols > 0)
3824 memcpy(ord, gd->col_order, gd->nb_cols * sizeof(size_t));
3825 FreeNoLog(gd->col_order);
3826 gd->col_order = ord;
3827 }
3828 gd->cols_cap = nc;
3829 }
3830 snprintf(gd->cols[gd->nb_cols].title, N_GUI_ID_MAX, "%s", title ? title : "");
3831 gd->cols[gd->nb_cols].width = width > 0 ? width : 80.0f;
3832 gd->cols[gd->nb_cols].visible = 1;
3833 if (gd->col_order)
3834 gd->col_order[gd->nb_cols] = gd->nb_cols; /* new column appended at its own display position */
3835 return (int)gd->nb_cols++;
3836}
3837
3839static size_t _datagrid_phys(const N_GUI_DATAGRID_DATA* gd, size_t dp) {
3840 return (gd->col_order && dp < gd->nb_cols) ? gd->col_order[dp] : dp;
3841}
3842
3845 if (!gd || gd->rows_cap == 0 || gd->row_sel) return;
3846 Malloc(gd->row_sel, unsigned char, gd->rows_cap);
3847 if (gd->row_sel) memset(gd->row_sel, 0, gd->rows_cap);
3848}
3849
3853 if (!gd || gd->rows_cap == 0) return 0;
3854 if (gd->row_color && gd->row_has_color) return 1;
3855 if (!gd->row_color) Malloc(gd->row_color, ALLEGRO_COLOR, gd->rows_cap);
3856 if (!gd->row_has_color) Malloc(gd->row_has_color, unsigned char, gd->rows_cap);
3857 if (!gd->row_color || !gd->row_has_color) return 0;
3858 memset(gd->row_has_color, 0, gd->rows_cap);
3859 return 1;
3860}
3861
3862int n_gui_datagrid_add_row(N_GUI_CTX* ctx, int widget_id, const char** values) {
3863 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
3864 if (!w || w->type != N_GUI_TYPE_DATAGRID || !w->data) return -1;
3866 if (gd->nb_cols == 0) return -1;
3867 if (gd->nb_rows >= gd->rows_cap) {
3868 size_t nr = gd->rows_cap ? gd->rows_cap * 2 : 16;
3869 char** tmp = NULL;
3870 Malloc(tmp, char*, nr * gd->nb_cols);
3871 if (!tmp) return -1;
3872 if (gd->cells && gd->nb_rows > 0)
3873 memcpy(tmp, gd->cells, gd->nb_rows * gd->nb_cols * sizeof(char*));
3874 FreeNoLog(gd->cells);
3875 gd->cells = tmp;
3876 gd->rows_cap = nr;
3877 /* grow the per-row selection array in lockstep (new rows start unselected) */
3878 if (gd->row_sel) {
3879 unsigned char* ts = NULL;
3880 Malloc(ts, unsigned char, nr);
3881 if (ts) {
3882 memset(ts, 0, nr);
3883 if (gd->nb_rows > 0) memcpy(ts, gd->row_sel, gd->nb_rows);
3884 FreeNoLog(gd->row_sel);
3885 gd->row_sel = ts;
3886 }
3887 }
3888 /* grow the per-row tint arrays in lockstep (new rows start untinted) */
3889 if (gd->row_color && gd->row_has_color) {
3890 ALLEGRO_COLOR* tc = NULL;
3891 unsigned char* th = NULL;
3892 Malloc(tc, ALLEGRO_COLOR, nr);
3893 Malloc(th, unsigned char, nr);
3894 if (tc && th) {
3895 memset(th, 0, nr);
3896 if (gd->nb_rows > 0) {
3897 memcpy(tc, gd->row_color, gd->nb_rows * sizeof(ALLEGRO_COLOR));
3898 memcpy(th, gd->row_has_color, gd->nb_rows);
3899 }
3900 FreeNoLog(gd->row_color);
3902 gd->row_color = tc;
3903 gd->row_has_color = th;
3904 } else {
3905 FreeNoLog(tc);
3906 FreeNoLog(th);
3907 }
3908 }
3909 }
3910 {
3911 size_t c;
3912 for (c = 0; c < gd->nb_cols; c++) {
3913 const char* v = (values && values[c]) ? values[c] : "";
3914 gd->cells[gd->nb_rows * gd->nb_cols + c] = strdup(v);
3915 }
3916 }
3917 return (int)gd->nb_rows++;
3918}
3919
3920void n_gui_datagrid_clear_rows(N_GUI_CTX* ctx, int widget_id) {
3921 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
3922 if (!w || w->type != N_GUI_TYPE_DATAGRID || !w->data) return;
3924 if (gd->cells) {
3925 size_t ci;
3926 for (ci = 0; ci < gd->nb_rows * gd->nb_cols; ci++)
3927 FreeNoLog(gd->cells[ci]);
3928 }
3929 gd->nb_rows = 0;
3930 gd->selected_row = -1;
3931 if (gd->row_sel) memset(gd->row_sel, 0, gd->rows_cap);
3932 /* the tints belong to the rows that are going away */
3933 if (gd->row_has_color) memset(gd->row_has_color, 0, gd->rows_cap);
3934 gd->anchor_row = -1;
3935 gd->scroll_offset = 0;
3936}
3937
3938int n_gui_datagrid_get_row_count(N_GUI_CTX* ctx, int widget_id) {
3939 const N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
3940 if (!w || w->type != N_GUI_TYPE_DATAGRID || !w->data) return 0;
3941 return (int)((const N_GUI_DATAGRID_DATA*)w->data)->nb_rows;
3942}
3943
3944const char* n_gui_datagrid_get_cell(N_GUI_CTX* ctx, int widget_id, int row, int col) {
3945 const N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
3946 if (!w || w->type != N_GUI_TYPE_DATAGRID || !w->data) return NULL;
3947 const N_GUI_DATAGRID_DATA* gd = (const N_GUI_DATAGRID_DATA*)w->data;
3948 if (row < 0 || (size_t)row >= gd->nb_rows || col < 0 || (size_t)col >= gd->nb_cols) return NULL;
3949 return gd->cells[(size_t)row * gd->nb_cols + (size_t)col];
3950}
3951
3952int n_gui_datagrid_get_selected(N_GUI_CTX* ctx, int widget_id) {
3953 const N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
3954 if (!w || w->type != N_GUI_TYPE_DATAGRID || !w->data) return -1;
3955 return ((const N_GUI_DATAGRID_DATA*)w->data)->selected_row;
3956}
3957
3958void n_gui_datagrid_set_selected(N_GUI_CTX* ctx, int widget_id, int row) {
3959 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
3960 if (!w || w->type != N_GUI_TYPE_DATAGRID || !w->data) return;
3962 if (row < -1 || (size_t)row >= gd->nb_rows) return;
3963 gd->selected_row = row;
3964 if (row >= 0 && gd->on_select) gd->on_select(widget_id, row, gd->user_data);
3965}
3966
3967void n_gui_datagrid_set_on_context(N_GUI_CTX* ctx, int widget_id, void (*on_context)(int, int, int, int, void*)) {
3968 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
3969 if (!w || w->type != N_GUI_TYPE_DATAGRID || !w->data) return;
3970 ((N_GUI_DATAGRID_DATA*)w->data)->on_context = on_context;
3971}
3972
3973void n_gui_datagrid_set_multiselect(N_GUI_CTX* ctx, int widget_id, int enabled) {
3974 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
3975 if (!w || w->type != N_GUI_TYPE_DATAGRID || !w->data) return;
3977 gd->multiselect = enabled ? 1 : 0;
3978 if (!enabled) {
3979 if (gd->row_sel) memset(gd->row_sel, 0, gd->rows_cap);
3980 gd->anchor_row = -1;
3981 } else {
3983 }
3984}
3985
3986int n_gui_datagrid_is_row_selected(N_GUI_CTX* ctx, int widget_id, int row) {
3987 const N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
3988 if (!w || w->type != N_GUI_TYPE_DATAGRID || !w->data) return 0;
3989 const N_GUI_DATAGRID_DATA* gd = (const N_GUI_DATAGRID_DATA*)w->data;
3990 if (row < 0 || (size_t)row >= gd->nb_rows) return 0;
3991 if (gd->multiselect && gd->row_sel) return gd->row_sel[row] ? 1 : 0;
3992 return (row == gd->selected_row) ? 1 : 0;
3993}
3994
3995size_t n_gui_datagrid_get_selected_rows(N_GUI_CTX* ctx, int widget_id, int* out, size_t max) {
3996 const N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
3997 if (!w || w->type != N_GUI_TYPE_DATAGRID || !w->data) return 0;
3998 const N_GUI_DATAGRID_DATA* gd = (const N_GUI_DATAGRID_DATA*)w->data;
3999 if (gd->multiselect && gd->row_sel) {
4000 size_t r, n = 0;
4001 for (r = 0; r < gd->nb_rows; r++) {
4002 if (gd->row_sel[r]) {
4003 if (out && n < max) out[n] = (int)r;
4004 n++;
4005 }
4006 }
4007 return n;
4008 }
4009 if (gd->selected_row >= 0 && (size_t)gd->selected_row < gd->nb_rows) {
4010 if (out && max > 0) out[0] = gd->selected_row;
4011 return 1;
4012 }
4013 return 0;
4014}
4015
4017 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
4018 if (!w || w->type != N_GUI_TYPE_DATAGRID || !w->data) return;
4020 if (gd->row_sel) memset(gd->row_sel, 0, gd->rows_cap);
4021 gd->selected_row = -1;
4022 gd->anchor_row = -1;
4023}
4024
4025void n_gui_datagrid_select_row(N_GUI_CTX* ctx, int widget_id, int row, int selected) {
4026 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
4027 if (!w || w->type != N_GUI_TYPE_DATAGRID || !w->data) return;
4029 if (row < 0 || (size_t)row >= gd->nb_rows) return;
4031 if (!gd->row_sel) return;
4032 gd->row_sel[row] = selected ? 1 : 0;
4033 if (selected) {
4034 gd->selected_row = row;
4035 gd->anchor_row = row;
4036 } else if (gd->selected_row == row) {
4037 gd->selected_row = -1;
4038 }
4039}
4040
4041void n_gui_datagrid_set_row_color(N_GUI_CTX* ctx, int widget_id, int row, ALLEGRO_COLOR color) {
4042 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
4043 if (!w || w->type != N_GUI_TYPE_DATAGRID || !w->data) return;
4045 if (row < 0 || (size_t)row >= gd->nb_rows) return;
4046 if (!_datagrid_ensure_colors(gd)) return;
4047 gd->row_color[row] = color;
4048 gd->row_has_color[row] = 1;
4049}
4050
4051void n_gui_datagrid_clear_row_color(N_GUI_CTX* ctx, int widget_id, int row) {
4052 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
4053 if (!w || w->type != N_GUI_TYPE_DATAGRID || !w->data) return;
4055 if (row < 0 || (size_t)row >= gd->nb_rows || !gd->row_has_color) return;
4056 gd->row_has_color[row] = 0;
4057}
4058
4059int n_gui_datagrid_get_row_color(N_GUI_CTX* ctx, int widget_id, int row, ALLEGRO_COLOR* out) {
4060 const N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
4061 if (!w || w->type != N_GUI_TYPE_DATAGRID || !w->data) return 0;
4062 const N_GUI_DATAGRID_DATA* gd = (const N_GUI_DATAGRID_DATA*)w->data;
4063 if (row < 0 || (size_t)row >= gd->nb_rows) return 0;
4064 if (!gd->row_has_color || !gd->row_color || !gd->row_has_color[row]) return 0;
4065 if (out) *out = gd->row_color[row];
4066 return 1;
4067}
4068
4070 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
4071 if (!w || w->type != N_GUI_TYPE_DATAGRID || !w->data) return;
4073 if (gd->row_has_color) memset(gd->row_has_color, 0, gd->rows_cap);
4074}
4075
4077 const N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
4078 if (!w || w->type != N_GUI_TYPE_DATAGRID || !w->data) return 0;
4079 return ((const N_GUI_DATAGRID_DATA*)w->data)->scroll_offset;
4080}
4081
4082void n_gui_datagrid_set_scroll_offset(N_GUI_CTX* ctx, int widget_id, int offset) {
4083 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
4084 if (!w || w->type != N_GUI_TYPE_DATAGRID || !w->data) return;
4086 /* clamp to a valid first row; the draw path re-clamps to the live viewport
4087 height each frame, so passing a stale offset after a rebuild is safe */
4088 if (offset < 0) offset = 0;
4089 if ((size_t)offset >= gd->nb_rows) offset = gd->nb_rows ? (int)gd->nb_rows - 1 : 0;
4090 gd->scroll_offset = offset;
4091}
4092
4093float n_gui_datagrid_get_h_scroll(N_GUI_CTX* ctx, int widget_id) {
4094 const N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
4095 if (!w || w->type != N_GUI_TYPE_DATAGRID || !w->data) return 0.0f;
4096 return ((const N_GUI_DATAGRID_DATA*)w->data)->h_scroll;
4097}
4098
4099void n_gui_datagrid_set_h_scroll(N_GUI_CTX* ctx, int widget_id, float px) {
4100 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
4101 if (!w || w->type != N_GUI_TYPE_DATAGRID || !w->data) return;
4102 /* clamp >= 0 only; the draw path re-clamps to the live content width each frame, so a
4103 value larger than the current maximum after a rebuild is harmless */
4104 if (px < 0.0f) px = 0.0f;
4105 ((N_GUI_DATAGRID_DATA*)w->data)->h_scroll = px;
4106}
4107
4109static N_GUI_DATAGRID_DATA* _datagrid_data(N_GUI_CTX* ctx, int widget_id) {
4110 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
4111 if (!w || w->type != N_GUI_TYPE_DATAGRID || !w->data) return NULL;
4112 return (N_GUI_DATAGRID_DATA*)w->data;
4113}
4114
4115size_t n_gui_datagrid_get_column_count(N_GUI_CTX* ctx, int widget_id) {
4116 const N_GUI_DATAGRID_DATA* gd = _datagrid_data(ctx, widget_id);
4117 return gd ? gd->nb_cols : 0;
4118}
4119
4120const char* n_gui_datagrid_get_column_title(N_GUI_CTX* ctx, int widget_id, int col) {
4121 N_GUI_DATAGRID_DATA* gd = _datagrid_data(ctx, widget_id);
4122 if (!gd || col < 0 || (size_t)col >= gd->nb_cols) return "";
4123 return gd->cols[col].title;
4124}
4125
4126void n_gui_datagrid_set_column_visible(N_GUI_CTX* ctx, int widget_id, int col, int visible) {
4127 N_GUI_DATAGRID_DATA* gd = _datagrid_data(ctx, widget_id);
4128 if (!gd || col < 0 || (size_t)col >= gd->nb_cols) return;
4129 gd->cols[col].visible = visible ? 1 : 0;
4130}
4131
4132int n_gui_datagrid_get_column_visible(N_GUI_CTX* ctx, int widget_id, int col) {
4133 N_GUI_DATAGRID_DATA* gd = _datagrid_data(ctx, widget_id);
4134 if (!gd || col < 0 || (size_t)col >= gd->nb_cols) return 0;
4135 return gd->cols[col].visible;
4136}
4137
4138void n_gui_datagrid_set_column_width(N_GUI_CTX* ctx, int widget_id, int col, float width) {
4139 N_GUI_DATAGRID_DATA* gd = _datagrid_data(ctx, widget_id);
4140 if (!gd || col < 0 || (size_t)col >= gd->nb_cols) return;
4141 gd->cols[col].width = width < 16.0f ? 16.0f : width;
4142}
4143
4144float n_gui_datagrid_get_column_width(N_GUI_CTX* ctx, int widget_id, int col) {
4145 N_GUI_DATAGRID_DATA* gd = _datagrid_data(ctx, widget_id);
4146 if (!gd || col < 0 || (size_t)col >= gd->nb_cols) return 0.0f;
4147 return gd->cols[col].width;
4148}
4149
4150int n_gui_datagrid_display_to_physical(N_GUI_CTX* ctx, int widget_id, int display_pos) {
4151 const N_GUI_DATAGRID_DATA* gd = _datagrid_data(ctx, widget_id);
4152 if (!gd || display_pos < 0 || (size_t)display_pos >= gd->nb_cols) return -1;
4153 return (int)_datagrid_phys(gd, (size_t)display_pos);
4154}
4155
4156void n_gui_datagrid_move_column(N_GUI_CTX* ctx, int widget_id, int from, int to) {
4157 N_GUI_DATAGRID_DATA* gd = _datagrid_data(ctx, widget_id);
4158 size_t f, t, moved, i;
4159 if (!gd || !gd->col_order) return;
4160 if (from < 0 || (size_t)from >= gd->nb_cols || to < 0 || (size_t)to >= gd->nb_cols || from == to) return;
4161 f = (size_t)from;
4162 t = (size_t)to;
4163 moved = gd->col_order[f];
4164 if (f < t) /* shift the span (f, t] left, then drop moved at t */
4165 for (i = f; i < t; i++) gd->col_order[i] = gd->col_order[i + 1];
4166 else /* shift the span [t, f) right, then drop moved at t */
4167 for (i = f; i > t; i--) gd->col_order[i] = gd->col_order[i - 1];
4168 gd->col_order[t] = moved;
4169}
4170
4171void n_gui_datagrid_set_on_columns_changed(N_GUI_CTX* ctx, int widget_id, void (*cb)(int, void*)) {
4172 N_GUI_DATAGRID_DATA* gd = _datagrid_data(ctx, widget_id);
4173 if (gd) gd->on_columns_changed = cb;
4174}
4175
4176void n_gui_datagrid_sort(N_GUI_CTX* ctx, int widget_id, int col, int dir) {
4177 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
4178 if (!w || w->type != N_GUI_TYPE_DATAGRID || !w->data) return;
4180 if (col < 0 || (size_t)col >= gd->nb_cols || gd->nb_rows < 2) {
4181 gd->sort_col = col;
4182 gd->sort_dir = (dir < 0) ? -1 : 1;
4183 return;
4184 }
4185 {
4186 size_t n = gd->nb_rows, cols = gd->nb_cols;
4187 size_t* idx = NULL;
4188 char** nc = NULL;
4189 size_t i, j;
4190 int d = (dir < 0) ? -1 : 1;
4191 Malloc(idx, size_t, n);
4192 Malloc(nc, char*, n* cols);
4193 if (!idx || !nc) {
4194 FreeNoLog(idx);
4195 FreeNoLog(nc);
4196 return;
4197 }
4198 for (i = 0; i < n; i++) idx[i] = i;
4199 /* insertion sort on the index array (stable, fine for grid sizes) */
4200 for (i = 1; i < n; i++) {
4201 size_t key = idx[i];
4202 const char* ka = gd->cells[key * cols + (size_t)col];
4203 j = i;
4204 while (j > 0 && d * _cell_cmp(gd->cells[idx[j - 1] * cols + (size_t)col], ka) > 0) {
4205 idx[j] = idx[j - 1];
4206 j--;
4207 }
4208 idx[j] = key;
4209 }
4210 for (i = 0; i < n; i++)
4211 for (j = 0; j < cols; j++)
4212 nc[i * cols + j] = gd->cells[idx[i] * cols + j];
4213 FreeNoLog(gd->cells);
4214 gd->cells = nc;
4215 /* the per-row tints are indexed by row, so they must be permuted with the
4216 cells (and shrunk to n like the cell buffer below) or every highlight
4217 would detach from the row it belongs to on the first sort */
4218 if (gd->row_color && gd->row_has_color) {
4219 ALLEGRO_COLOR* tc = NULL;
4220 unsigned char* th = NULL;
4221 Malloc(tc, ALLEGRO_COLOR, n);
4222 Malloc(th, unsigned char, n);
4223 if (tc && th) {
4224 for (i = 0; i < n; i++) {
4225 tc[i] = gd->row_color[idx[i]];
4226 th[i] = gd->row_has_color[idx[i]];
4227 }
4228 FreeNoLog(gd->row_color);
4230 gd->row_color = tc;
4231 gd->row_has_color = th;
4232 } else {
4233 FreeNoLog(tc);
4234 FreeNoLog(th);
4235 }
4236 }
4237 /* nc holds exactly n rows; keep rows_cap in step or the next add_row
4238 would treat the stale (larger) capacity as free space and write past
4239 the reallocated buffer (heap overflow -> later double free) */
4240 gd->rows_cap = n;
4241 FreeNoLog(idx);
4242 gd->sort_col = col;
4243 gd->sort_dir = d;
4244 gd->selected_row = -1;
4245 /* row indices changed: clear the selection rather than mis-map it */
4246 if (gd->row_sel) memset(gd->row_sel, 0, gd->rows_cap);
4247 gd->anchor_row = -1;
4248 }
4249}
4250
4251/* progress bar */
4252
4253int n_gui_add_progressbar(N_GUI_CTX* ctx, int window_id, float x, float y, float w, float h) {
4254 __n_assert(ctx, return -1);
4255 N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
4256 __n_assert(win, return -1);
4257
4258 N_GUI_WIDGET* wgt = _new_widget(ctx, N_GUI_TYPE_PROGRESSBAR, x, y, w, h);
4259 __n_assert(wgt, return -1);
4260
4261 N_GUI_PROGRESSBAR_DATA* pd = NULL;
4263 __n_assert(pd, Free(wgt); return -1);
4264 pd->value = 0.0f;
4265 pd->text[0] = '\0';
4266 wgt->enabled = 0; /* display only: not interactive/focusable */
4267 wgt->data = pd;
4268 wgt->norm_x = 0.0f;
4269 wgt->norm_y = 0.0f;
4270 wgt->norm_w = 0.0f;
4271 wgt->norm_h = 0.0f;
4272
4273 list_push(win->widgets, wgt, _destroy_widget);
4275 _register_widget(ctx, wgt);
4276 return wgt->id;
4277}
4278
4279void n_gui_progressbar_set_value(N_GUI_CTX* ctx, int widget_id, float value) {
4280 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
4281 if (!w || w->type != N_GUI_TYPE_PROGRESSBAR || !w->data)
4282 return;
4283 if (value < 0.0f)
4284 value = 0.0f;
4285 if (value > 1.0f)
4286 value = 1.0f;
4287 ((N_GUI_PROGRESSBAR_DATA*)w->data)->value = value;
4288}
4289
4290float n_gui_progressbar_get_value(N_GUI_CTX* ctx, int widget_id) {
4291 const N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
4292 if (!w || w->type != N_GUI_TYPE_PROGRESSBAR || !w->data)
4293 return 0.0f;
4294 return ((const N_GUI_PROGRESSBAR_DATA*)w->data)->value;
4295}
4296
4297void n_gui_progressbar_set_text(N_GUI_CTX* ctx, int widget_id, const char* text) {
4298 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
4300 if (!w || w->type != N_GUI_TYPE_PROGRESSBAR || !w->data)
4301 return;
4302 pd = (N_GUI_PROGRESSBAR_DATA*)w->data;
4303 pd->text[0] = '\0';
4304 if (text) {
4305 strncpy(pd->text, text, N_GUI_TEXT_MAX - 1);
4306 pd->text[N_GUI_TEXT_MAX - 1] = '\0';
4307 }
4308}
4309
4310/* custom (owner-draw) widget */
4311
4315int n_gui_add_custom(N_GUI_CTX* ctx, int window_id, float x, float y, float w, float h, N_GUI_CUSTOM_DRAW draw, void* user_data) {
4316 __n_assert(ctx, return -1);
4317 N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
4318 __n_assert(win, return -1);
4319
4320 N_GUI_WIDGET* wgt = _new_widget(ctx, N_GUI_TYPE_CUSTOM, x, y, w, h);
4321 __n_assert(wgt, return -1);
4322
4323 N_GUI_CUSTOM_DATA* cd = NULL;
4324 Malloc(cd, N_GUI_CUSTOM_DATA, 1);
4325 __n_assert(cd, Free(wgt); return -1);
4326 cd->draw = draw;
4327 cd->user_data = user_data;
4328 wgt->data = cd;
4329 wgt->norm_x = 0.0f;
4330 wgt->norm_y = 0.0f;
4331 wgt->norm_w = 0.0f;
4332 wgt->norm_h = 0.0f;
4333
4334 list_push(win->widgets, wgt, _destroy_widget);
4336 _register_widget(ctx, wgt);
4337 return wgt->id;
4338}
4339
4340/* radiolist helpers */
4341
4349int n_gui_radiolist_add_item(N_GUI_CTX* ctx, int widget_id, const char* text) {
4350 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
4351 if (!w || w->type != N_GUI_TYPE_RADIOLIST || !w->data) return -1;
4353 if (!_items_grow(&rd->items, &rd->nb_items, &rd->items_capacity)) return -1;
4354 N_GUI_LISTITEM* item = &rd->items[rd->nb_items];
4355 item->text[0] = '\0';
4356 if (text) {
4357 strncpy(item->text, text, N_GUI_ID_MAX - 1);
4358 item->text[N_GUI_ID_MAX - 1] = '\0';
4359 }
4360 item->selected = 0;
4361 rd->nb_items++;
4362 return (int)(rd->nb_items - 1);
4363}
4364
4370void n_gui_radiolist_clear(N_GUI_CTX* ctx, int widget_id) {
4371 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
4372 if (w && w->type == N_GUI_TYPE_RADIOLIST && w->data) {
4374 rd->nb_items = 0;
4375 rd->selected_index = -1;
4376 rd->scroll_offset = 0;
4377 }
4378}
4379
4386int n_gui_radiolist_get_selected(N_GUI_CTX* ctx, int widget_id) {
4387 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
4388 if (w && w->type == N_GUI_TYPE_RADIOLIST && w->data) {
4389 return ((N_GUI_RADIOLIST_DATA*)w->data)->selected_index;
4390 }
4391 return -1;
4392}
4393
4400void n_gui_radiolist_set_selected(N_GUI_CTX* ctx, int widget_id, int index) {
4401 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
4402 if (w && w->type == N_GUI_TYPE_RADIOLIST && w->data) {
4404 if (index >= -1 && (index == -1 || (size_t)index < rd->nb_items)) {
4405 rd->selected_index = index;
4406 }
4407 }
4408}
4409
4410/* combobox helpers */
4411
4417void n_gui_combobox_clear(N_GUI_CTX* ctx, int widget_id) {
4418 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
4419 if (!w || w->type != N_GUI_TYPE_COMBOBOX || !w->data) return;
4421 if (cd->items && cd->nb_items > 0) {
4422 memset(cd->items, 0, cd->nb_items * sizeof(N_GUI_LISTITEM));
4423 }
4424 cd->nb_items = 0;
4425 cd->selected_index = -1;
4426 cd->scroll_offset = 0;
4427 cd->highlight_index = -1;
4428 cd->is_open = 0;
4429}
4430
4431int n_gui_combobox_add_item(N_GUI_CTX* ctx, int widget_id, const char* text) {
4432 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
4433 if (!w || w->type != N_GUI_TYPE_COMBOBOX || !w->data) return -1;
4435 if (!_items_grow(&cd->items, &cd->nb_items, &cd->items_capacity)) return -1;
4436 N_GUI_LISTITEM* item = &cd->items[cd->nb_items];
4437 item->text[0] = '\0';
4438 if (text) {
4439 strncpy(item->text, text, N_GUI_ID_MAX - 1);
4440 item->text[N_GUI_ID_MAX - 1] = '\0';
4441 }
4442 item->selected = 0;
4443 cd->nb_items++;
4444 return (int)(cd->nb_items - 1);
4445}
4446
4453int n_gui_combobox_get_selected(N_GUI_CTX* ctx, int widget_id) {
4454 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
4455 if (w && w->type == N_GUI_TYPE_COMBOBOX && w->data) {
4456 return ((N_GUI_COMBOBOX_DATA*)w->data)->selected_index;
4457 }
4458 return -1;
4459}
4460
4461const char* n_gui_combobox_get_item_text(N_GUI_CTX* ctx, int widget_id, int index) {
4462 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
4463 if (w && w->type == N_GUI_TYPE_COMBOBOX && w->data) {
4465 if (index >= 0 && (size_t)index < cd->nb_items) {
4466 return cd->items[index].text;
4467 }
4468 }
4469 return "";
4470}
4471
4478void n_gui_combobox_set_selected(N_GUI_CTX* ctx, int widget_id, int index) {
4479 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
4480 if (w && w->type == N_GUI_TYPE_COMBOBOX && w->data) {
4482 if (index >= -1 && (index == -1 || (size_t)index < cd->nb_items)) {
4483 cd->selected_index = index;
4484 }
4485 }
4486}
4487
4488/* image helpers */
4489
4496void n_gui_image_set_bitmap(N_GUI_CTX* ctx, int widget_id, ALLEGRO_BITMAP* bitmap) {
4497 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
4498 if (w && w->type == N_GUI_TYPE_IMAGE && w->data) {
4499 ((N_GUI_IMAGE_DATA*)w->data)->bitmap = bitmap;
4500 }
4501}
4502
4503/* label helpers */
4504
4511void n_gui_label_set_text(N_GUI_CTX* ctx, int widget_id, const char* text) {
4512 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
4513 if (w && w->type == N_GUI_TYPE_LABEL && w->data) {
4515 if (text) {
4516 strncpy(lb->text, text, N_GUI_TEXT_MAX - 1);
4517 lb->text[N_GUI_TEXT_MAX - 1] = '\0';
4518 _normalize_crlf(lb->text); /* normalize CRLF for Allegro5 compatibility */
4519 } else {
4520 lb->text[0] = '\0';
4521 }
4522 }
4523}
4524
4531void n_gui_label_set_link(N_GUI_CTX* ctx, int widget_id, const char* link) {
4532 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
4533 if (w && w->type == N_GUI_TYPE_LABEL && w->data) {
4535 if (link) {
4536 strncpy(lb->link, link, N_GUI_TEXT_MAX - 1);
4537 lb->link[N_GUI_TEXT_MAX - 1] = '\0';
4538 } else {
4539 lb->link[0] = '\0';
4540 }
4541 }
4542}
4543
4544/* DROPDOWN MENU */
4545
4547static int _dropmenu_entries_grow(N_GUI_DROPMENU_ENTRY** entries, const size_t* nb, size_t* cap) {
4548 if (*nb >= *cap) {
4549 size_t new_cap = (*cap == 0) ? 8 : (*cap) * 2;
4550 if (new_cap < *cap) return 0; /* overflow */
4551 N_GUI_DROPMENU_ENTRY* tmp = NULL;
4552 Malloc(tmp, N_GUI_DROPMENU_ENTRY, new_cap);
4553 if (!tmp) return 0;
4554 if (*entries && *nb > 0) {
4555 memcpy(tmp, *entries, (*nb) * sizeof(N_GUI_DROPMENU_ENTRY));
4556 }
4557 FreeNoLog(*entries);
4558 *entries = tmp;
4559 *cap = new_cap;
4560 }
4561 return 1;
4562}
4563
4568int n_gui_add_dropmenu(N_GUI_CTX* ctx, int window_id, const char* label, float x, float y, float w, float h, void (*on_open)(int, void*), void* on_open_user_data) {
4569 __n_assert(ctx, return -1);
4570 N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
4571 __n_assert(win, return -1);
4572
4573 N_GUI_WIDGET* wgt = _new_widget(ctx, N_GUI_TYPE_DROPMENU, x, y, w, h);
4574 __n_assert(wgt, return -1);
4575
4576 N_GUI_DROPMENU_DATA* dd = NULL;
4578 __n_assert(dd, Free(wgt); return -1);
4579 dd->label[0] = '\0';
4580 if (label) {
4581 strncpy(dd->label, label, N_GUI_ID_MAX - 1);
4582 dd->label[N_GUI_ID_MAX - 1] = '\0';
4583 }
4584 dd->entries = NULL;
4585 dd->nb_entries = 0;
4586 dd->entries_capacity = 0;
4587 dd->is_open = 0;
4588 dd->scroll_offset = 0;
4589 dd->highlight_index = -1;
4591 dd->item_height = h;
4592 dd->on_open = on_open;
4593 dd->on_open_user_data = on_open_user_data;
4594 dd->panel_bitmap = NULL;
4595 dd->item_hover_bitmap = NULL;
4596 dd->flags = 0;
4597 wgt->data = dd;
4598 wgt->norm_x = 0.0f;
4599 wgt->norm_y = 0.0f;
4600 wgt->norm_w = 0.0f;
4601 wgt->norm_h = 0.0f;
4602
4603 list_push(win->widgets, wgt, _destroy_widget);
4605 _register_widget(ctx, wgt);
4606 return wgt->id;
4607}
4608
4612void n_gui_dropmenu_set_flags(N_GUI_CTX* ctx, int widget_id, int flags) {
4613 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
4614 if (!w || w->type != N_GUI_TYPE_DROPMENU || !w->data) return;
4616 dd->flags = flags;
4617}
4618
4631void n_gui_dropmenu_set_label(N_GUI_CTX* ctx, int widget_id, const char* label) {
4632 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
4633 if (!w || w->type != N_GUI_TYPE_DROPMENU || !w->data) return;
4635 if (label) {
4636 snprintf(dd->label, sizeof(dd->label), "%s", label);
4637 } else {
4638 dd->label[0] = '\0';
4639 }
4640}
4641
4646int n_gui_dropmenu_add_entry(N_GUI_CTX* ctx, int widget_id, const char* text, int tag, void (*on_click)(int, int, int, void*), void* user_data) {
4647 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
4648 if (!w || w->type != N_GUI_TYPE_DROPMENU || !w->data) return -1;
4650 if (!_dropmenu_entries_grow(&dd->entries, &dd->nb_entries, &dd->entries_capacity)) return -1;
4652 memset(e, 0, sizeof(*e));
4653 if (text) {
4654 strncpy(e->text, text, N_GUI_ID_MAX - 1);
4655 e->text[N_GUI_ID_MAX - 1] = '\0';
4656 }
4657 e->is_dynamic = 0;
4658 e->tag = tag;
4659 e->on_click = on_click;
4660 e->user_data = user_data;
4661 dd->nb_entries++;
4662 return (int)(dd->nb_entries - 1);
4663}
4664
4669int n_gui_dropmenu_add_dynamic_entry(N_GUI_CTX* ctx, int widget_id, const char* text, int tag, void (*on_click)(int, int, int, void*), void* user_data) {
4670 int idx = n_gui_dropmenu_add_entry(ctx, widget_id, text, tag, on_click, user_data);
4671 if (idx < 0)
4672 return idx;
4673
4674 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
4675 if (!w) {
4676 n_log(LOG_ERR, "Invalid NULL widget for ctx %p widget_id %d", ctx, widget_id);
4677 return -1;
4678 }
4679
4681 if (!dd) {
4682 n_log(LOG_ERR, "Widget has NULL data for ctx %p widget_id %d", ctx, widget_id);
4683 return -1;
4684 }
4685
4686 dd->entries[idx].is_dynamic = 1;
4687
4688 return idx;
4689}
4690
4694void n_gui_dropmenu_clear_dynamic(N_GUI_CTX* ctx, int widget_id) {
4695 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
4696 if (!w || w->type != N_GUI_TYPE_DROPMENU || !w->data) return;
4698 /* compact: keep only non-dynamic entries */
4699 size_t write = 0;
4700 for (size_t i = 0; i < dd->nb_entries; i++) {
4701 if (!dd->entries[i].is_dynamic) {
4702 if (write != i) {
4703 dd->entries[write] = dd->entries[i];
4704 }
4705 write++;
4706 }
4707 }
4708 dd->nb_entries = write;
4709 /* clamp scroll_offset after removal */
4710 if (dd->nb_entries == 0) {
4711 dd->scroll_offset = 0;
4712 } else {
4713 int max_vis = dd->max_visible > 0 ? dd->max_visible : 8;
4714 int dm_max = (int)dd->nb_entries - max_vis;
4715 if (dm_max < 0) dm_max = 0;
4716 if (dd->scroll_offset > dm_max) dd->scroll_offset = dm_max;
4717 }
4718}
4719
4723void n_gui_dropmenu_clear(N_GUI_CTX* ctx, int widget_id) {
4724 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
4725 if (!w || w->type != N_GUI_TYPE_DROPMENU || !w->data) return;
4727 dd->nb_entries = 0;
4728 dd->scroll_offset = 0;
4729}
4730
4734void n_gui_dropmenu_set_entry_text(N_GUI_CTX* ctx, int widget_id, int index, const char* text) {
4735 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
4736 if (!w || w->type != N_GUI_TYPE_DROPMENU || !w->data) return;
4738 if (index < 0 || (size_t)index >= dd->nb_entries) return;
4739 if (text) {
4740 strncpy(dd->entries[index].text, text, N_GUI_ID_MAX - 1);
4741 dd->entries[index].text[N_GUI_ID_MAX - 1] = '\0';
4742 }
4743}
4744
4748int n_gui_dropmenu_get_count(N_GUI_CTX* ctx, int widget_id) {
4749 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
4750 if (w && w->type == N_GUI_TYPE_DROPMENU && w->data) {
4751 return (int)((N_GUI_DROPMENU_DATA*)w->data)->nb_entries;
4752 }
4753 return 0;
4754}
4755
4771float n_gui_dropmenu_panel_width(N_GUI_CTX* ctx, int widget_id) {
4772 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
4773 if (!w || w->type != N_GUI_TYPE_DROPMENU || !w->data) return 0.0f;
4774 const N_GUI_DROPMENU_DATA* dd = (const N_GUI_DROPMENU_DATA*)w->data;
4775 ALLEGRO_FONT* font = w->font ? w->font : ctx->default_font;
4776 float pad = ctx->style.item_text_padding;
4777 float sb = ctx->style.scrollbar_size;
4778 float pw = w->w;
4779 int need_sb = ((int)dd->nb_entries > dd->max_visible);
4780 size_t i;
4781 if (sb < 10.0f) sb = 10.0f;
4782 if (!font) return pw;
4783 for (i = 0; i < dd->nb_entries; i++) {
4784 float tw = _text_w(font, dd->entries[i].text) + pad * 2.0f + (need_sb ? sb : 0.0f);
4785 if (tw > pw) pw = tw;
4786 }
4787 return pw;
4788}
4789
4790/* BITMAP SKINNING - setter functions */
4791
4797void n_gui_window_set_bitmaps(N_GUI_CTX* ctx, int window_id, ALLEGRO_BITMAP* bg, ALLEGRO_BITMAP* titlebar, int bg_scale_mode) {
4798 N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
4799 if (!win) {
4800 n_log(LOG_WARNING, "n_gui_window_set_bitmaps: window %d not found", window_id);
4801 return;
4802 }
4803 win->bg_bitmap = bg;
4804 win->titlebar_bitmap = titlebar;
4805 win->bg_scale_mode = bg_scale_mode;
4806}
4807
4812void n_gui_slider_set_bitmaps(N_GUI_CTX* ctx, int widget_id, ALLEGRO_BITMAP* track, ALLEGRO_BITMAP* fill, ALLEGRO_BITMAP* handle, ALLEGRO_BITMAP* handle_hover, ALLEGRO_BITMAP* handle_active) {
4813 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
4814 if (!w || w->type != N_GUI_TYPE_SLIDER || !w->data) {
4815 n_log(LOG_WARNING, "n_gui_slider_set_bitmaps: widget %d is not a slider", widget_id);
4816 return;
4817 }
4819 sd->track_bitmap = track;
4820 sd->fill_bitmap = fill;
4821 sd->handle_bitmap = handle;
4822 sd->handle_hover_bitmap = handle_hover;
4823 sd->handle_active_bitmap = handle_active;
4824}
4825
4830void n_gui_scrollbar_set_bitmaps(N_GUI_CTX* ctx, int widget_id, ALLEGRO_BITMAP* track, ALLEGRO_BITMAP* thumb, ALLEGRO_BITMAP* thumb_hover, ALLEGRO_BITMAP* thumb_active) {
4831 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
4832 if (!w || w->type != N_GUI_TYPE_SCROLLBAR || !w->data) {
4833 n_log(LOG_WARNING, "n_gui_scrollbar_set_bitmaps: widget %d is not a scrollbar", widget_id);
4834 return;
4835 }
4837 sb->track_bitmap = track;
4838 sb->thumb_bitmap = thumb;
4839 sb->thumb_hover_bitmap = thumb_hover;
4840 sb->thumb_active_bitmap = thumb_active;
4841}
4842
4847void n_gui_checkbox_set_bitmaps(N_GUI_CTX* ctx, int widget_id, ALLEGRO_BITMAP* box, ALLEGRO_BITMAP* box_checked, ALLEGRO_BITMAP* box_hover) {
4848 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
4849 if (!w || w->type != N_GUI_TYPE_CHECKBOX || !w->data) {
4850 n_log(LOG_WARNING, "n_gui_checkbox_set_bitmaps: widget %d is not a checkbox", widget_id);
4851 return;
4852 }
4854 cd->box_bitmap = box;
4855 cd->box_checked_bitmap = box_checked;
4856 cd->box_hover_bitmap = box_hover;
4857}
4858
4863void n_gui_textarea_set_bitmap(N_GUI_CTX* ctx, int widget_id, ALLEGRO_BITMAP* bg) {
4864 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
4865 if (!w || w->type != N_GUI_TYPE_TEXTAREA || !w->data) {
4866 n_log(LOG_WARNING, "n_gui_textarea_set_bitmap: widget %d is not a textarea", widget_id);
4867 return;
4868 }
4869 ((N_GUI_TEXTAREA_DATA*)w->data)->bg_bitmap = bg;
4870}
4871
4877void n_gui_textarea_set_mask_char(N_GUI_CTX* ctx, int widget_id, char mask) {
4878 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
4879 if (!w || w->type != N_GUI_TYPE_TEXTAREA || !w->data) {
4880 n_log(LOG_WARNING, "n_gui_textarea_set_mask_char: widget %d is not a textarea", widget_id);
4881 return;
4882 }
4883 ((N_GUI_TEXTAREA_DATA*)w->data)->mask_char = mask;
4884}
4885
4889void n_gui_textarea_set_placeholder(N_GUI_CTX* ctx, int widget_id, const char* text) {
4890 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
4891 N_GUI_TEXTAREA_DATA* td = NULL;
4892 if (!w || w->type != N_GUI_TYPE_TEXTAREA || !w->data) {
4893 n_log(LOG_WARNING, "n_gui_textarea_set_placeholder: widget %d is not a textarea", widget_id);
4894 return;
4895 }
4896 td = (N_GUI_TEXTAREA_DATA*)w->data;
4898 if (text && text[0])
4899 td->placeholder = strdup(text);
4900}
4901
4906void n_gui_listbox_set_bitmaps(N_GUI_CTX* ctx, int widget_id, ALLEGRO_BITMAP* bg, ALLEGRO_BITMAP* item_bg, ALLEGRO_BITMAP* item_selected) {
4907 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
4908 if (!w || w->type != N_GUI_TYPE_LISTBOX || !w->data) {
4909 n_log(LOG_WARNING, "n_gui_listbox_set_bitmaps: widget %d is not a listbox", widget_id);
4910 return;
4911 }
4913 ld->bg_bitmap = bg;
4914 ld->item_bg_bitmap = item_bg;
4915 ld->item_selected_bitmap = item_selected;
4916}
4917
4922void n_gui_radiolist_set_bitmaps(N_GUI_CTX* ctx, int widget_id, ALLEGRO_BITMAP* bg, ALLEGRO_BITMAP* item_bg, ALLEGRO_BITMAP* item_selected) {
4923 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
4924 if (!w || w->type != N_GUI_TYPE_RADIOLIST || !w->data) {
4925 n_log(LOG_WARNING, "n_gui_radiolist_set_bitmaps: widget %d is not a radiolist", widget_id);
4926 return;
4927 }
4929 rd->bg_bitmap = bg;
4930 rd->item_bg_bitmap = item_bg;
4931 rd->item_selected_bitmap = item_selected;
4932}
4933
4938void n_gui_combobox_set_bitmaps(N_GUI_CTX* ctx, int widget_id, ALLEGRO_BITMAP* bg, ALLEGRO_BITMAP* item_bg, ALLEGRO_BITMAP* item_selected) {
4939 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
4940 if (!w || w->type != N_GUI_TYPE_COMBOBOX || !w->data) {
4941 n_log(LOG_WARNING, "n_gui_combobox_set_bitmaps: widget %d is not a combobox", widget_id);
4942 return;
4943 }
4945 cd->bg_bitmap = bg;
4946 cd->item_bg_bitmap = item_bg;
4947 cd->item_selected_bitmap = item_selected;
4948}
4949
4954void n_gui_dropmenu_set_bitmaps(N_GUI_CTX* ctx, int widget_id, ALLEGRO_BITMAP* panel, ALLEGRO_BITMAP* item_hover) {
4955 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
4956 if (!w || w->type != N_GUI_TYPE_DROPMENU || !w->data) {
4957 n_log(LOG_WARNING, "n_gui_dropmenu_set_bitmaps: widget %d is not a dropmenu", widget_id);
4958 return;
4959 }
4961 dd->panel_bitmap = panel;
4962 dd->item_hover_bitmap = item_hover;
4963}
4964
4969void n_gui_label_set_bitmap(N_GUI_CTX* ctx, int widget_id, ALLEGRO_BITMAP* bg) {
4970 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
4971 if (!w || w->type != N_GUI_TYPE_LABEL || !w->data) {
4972 n_log(LOG_WARNING, "n_gui_label_set_bitmap: widget %d is not a label", widget_id);
4973 return;
4974 }
4975 ((N_GUI_LABEL_DATA*)w->data)->bg_bitmap = bg;
4976}
4977
4978/* DRAWING - individual widget renderers */
4979
4982static void _draw_bitmap_scaled(ALLEGRO_BITMAP* bmp, float dx, float dy, float dw, float dh, int mode) {
4983 float bw = (float)al_get_bitmap_width(bmp);
4984 float bh = (float)al_get_bitmap_height(bmp);
4985 if (mode == N_GUI_IMAGE_STRETCH) {
4986 al_draw_scaled_bitmap(bmp, 0, 0, bw, bh, dx, dy, dw, dh, 0);
4987 } else if (mode == N_GUI_IMAGE_CENTER) {
4988 al_draw_bitmap(bmp, dx + (dw - bw) / 2.0f, dy + (dh - bh) / 2.0f, 0);
4989 } else {
4990 /* N_GUI_IMAGE_FIT: maintain aspect ratio */
4991 float sx = dw / bw;
4992 float sy = dh / bh;
4993 float s = (sx < sy) ? sx : sy;
4994 float rw = bw * s;
4995 float rh = bh * s;
4996 al_draw_scaled_bitmap(bmp, 0, 0, bw, bh, dx + (dw - rw) / 2.0f, dy + (dh - rh) / 2.0f, rw, rh, 0);
4997 }
4998}
4999
5003static ALLEGRO_BITMAP* _select_state_bitmap(int state, ALLEGRO_BITMAP* normal_bmp, ALLEGRO_BITMAP* hover_bmp, ALLEGRO_BITMAP* active_bmp) {
5004 if ((state & N_GUI_STATE_ACTIVE) && active_bmp) return active_bmp;
5005 if ((state & N_GUI_STATE_HOVER) && hover_bmp) return hover_bmp;
5006 if (normal_bmp) return normal_bmp;
5007 return NULL;
5008}
5009
5015static void _set_clipping_rect_transformed(int wx, int wy, int ww, int wh) {
5016 /* Transform all 4 corners from world space to screen space so that the
5017 * axis-aligned bounding box is computed correctly for any affine transform
5018 * (including negative scaling, rotation, or shear). */
5019 const ALLEGRO_TRANSFORM* tf = al_get_current_transform();
5020 float cx[4], cy[4];
5021 cx[0] = (float)wx;
5022 cy[0] = (float)wy;
5023 cx[1] = (float)(wx + ww);
5024 cy[1] = (float)wy;
5025 cx[2] = (float)wx;
5026 cy[2] = (float)(wy + wh);
5027 cx[3] = (float)(wx + ww);
5028 cy[3] = (float)(wy + wh);
5029 for (int i = 0; i < 4; i++)
5030 al_transform_coordinates(tf, &cx[i], &cy[i]);
5031
5032 float min_x = cx[0], max_x = cx[0];
5033 float min_y = cy[0], max_y = cy[0];
5034 for (int i = 1; i < 4; i++) {
5035 if (cx[i] < min_x) min_x = cx[i];
5036 if (cx[i] > max_x) max_x = cx[i];
5037 if (cy[i] < min_y) min_y = cy[i];
5038 if (cy[i] > max_y) max_y = cy[i];
5039 }
5040
5041 int new_x = (int)min_x;
5042 int new_y = (int)min_y;
5043 int new_w = (int)(max_x - min_x);
5044 int new_h = (int)(max_y - min_y);
5045
5046 /* intersect with the current (parent) clipping rectangle */
5047 int pcx, pcy, pcw, pch;
5048 al_get_clipping_rectangle(&pcx, &pcy, &pcw, &pch);
5049
5050 int right = new_x + new_w;
5051 int bottom = new_y + new_h;
5052 int prev_right = pcx + pcw;
5053 int prev_bottom = pcy + pch;
5054
5055 if (new_x < pcx) new_x = pcx;
5056 if (new_y < pcy) new_y = pcy;
5057 if (right > prev_right) right = prev_right;
5058 if (bottom > prev_bottom) bottom = prev_bottom;
5059
5060 new_w = right - new_x;
5061 new_h = bottom - new_y;
5062 if (new_w < 0) new_w = 0;
5063 if (new_h < 0) new_h = 0;
5064
5065 al_set_clipping_rectangle(new_x, new_y, new_w, new_h);
5066}
5067
5070static void _draw_text_truncated(ALLEGRO_FONT* font, ALLEGRO_COLOR color, float x, float y, float max_w, const char* text) {
5071 if (!font || !text || !text[0]) return;
5072 float tw = _text_w(font, text);
5073 if (tw <= max_w) {
5074 al_draw_text(font, color, x, y, 0, text);
5075 return;
5076 }
5077 /* find how many chars fit + "...", stepping one UTF-8 sequence at a
5078 * time so a multi-byte codepoint is never measured or kept halfway */
5079 float ellipsis_w = _text_w(font, "...");
5080 float avail = max_w - ellipsis_w;
5081 if (avail < 0) avail = 0;
5082 size_t len = strlen(text);
5083 char buf[N_GUI_TEXT_MAX];
5084 size_t fit = 0;
5085 size_t i = 0;
5086 while (i < len) {
5087 size_t step = 1;
5088 unsigned char lead = (unsigned char)text[i];
5089 if ((lead & 0xE0) == 0xC0)
5090 step = 2;
5091 else if ((lead & 0xF0) == 0xE0)
5092 step = 3;
5093 else if ((lead & 0xF8) == 0xF0)
5094 step = 4;
5095 if (i + step > len || i + step > (size_t)(N_GUI_TEXT_MAX - 5)) break;
5096 memcpy(buf + i, text + i, step);
5097 buf[i + step] = '\0';
5098 float cw = _text_w(font, buf);
5099 if (cw > avail) break;
5100 fit = i + step;
5101 i += step;
5102 }
5103 buf[fit] = '.';
5104 buf[fit + 1] = '.';
5105 buf[fit + 2] = '.';
5106 buf[fit + 3] = '\0';
5107 al_draw_text(font, color, x, y, 0, buf);
5108}
5109
5114static void _draw_text_justified(ALLEGRO_FONT* font, ALLEGRO_COLOR color, float x, float y, float max_w, float max_h, const char* text) {
5115 if (!font || !text || !text[0]) return;
5116
5117 /* split text into words */
5118 char buf[N_GUI_TEXT_MAX];
5119 snprintf(buf, N_GUI_TEXT_MAX, "%s", text);
5120
5121 char* words[256];
5122 float word_widths[256];
5123 int nwords = 0;
5124 char* tok = strtok(buf, " \t");
5125 while (tok && nwords < 256) {
5126 words[nwords] = tok;
5127 word_widths[nwords] = _text_w(font, tok);
5128 nwords++;
5129 tok = strtok(NULL, " \t");
5130 }
5131 if (nwords == 0) return;
5132 if (nwords == 1) {
5133 _draw_text_truncated(font, color, x, y, max_w, text);
5134 return;
5135 }
5136
5137 float fh = (float)al_get_font_line_height(font);
5138 float space_w = _text_w(font, " ");
5139 float cy = y;
5140 int line_start = 0;
5141
5142 while (line_start < nwords) {
5143 /* stop if there is no vertical space for another line */
5144 if (cy + fh > y + max_h + 0.5f) break;
5145
5146 /* pack as many words as fit on this line with minimum (single-space) gaps */
5147 float line_words_w = word_widths[line_start];
5148 int line_end = line_start + 1;
5149 for (int i = line_start + 1; i < nwords; i++) {
5150 float test_w = line_words_w + space_w + word_widths[i];
5151 if (test_w > max_w) break;
5152 line_words_w = test_w;
5153 line_end = i + 1;
5154 }
5155
5156 int words_on_line = line_end - line_start;
5157 int is_last_line = (line_end >= nwords);
5158
5159 if (words_on_line == 1) {
5160 /* single word: truncate with "..." if it exceeds the line width */
5161 if (word_widths[line_start] > max_w) {
5162 _draw_text_truncated(font, color, x, cy, max_w, words[line_start]);
5163 } else {
5164 al_draw_text(font, color, x, cy, 0, words[line_start]);
5165 }
5166 } else if (is_last_line) {
5167 /* last line: left-align with normal spacing */
5168 float cx = x;
5169 for (int i = line_start; i < line_end; i++) {
5170 al_draw_text(font, color, cx, cy, 0, words[i]);
5171 cx += word_widths[i] + space_w;
5172 }
5173 } else {
5174 /* full line: justify by distributing extra space between words */
5175 float total_word_w = 0;
5176 for (int i = line_start; i < line_end; i++) total_word_w += word_widths[i];
5177 float total_space = max_w - total_word_w;
5178 if (total_space < 0) total_space = 0;
5179 float gap = total_space / (float)(words_on_line - 1);
5180 float cx = x;
5181 for (int i = line_start; i < line_end; i++) {
5182 al_draw_text(font, color, cx, cy, 0, words[i]);
5183 cx += word_widths[i] + gap;
5184 }
5185 }
5186
5187 line_start = line_end;
5188 cy += fh;
5189 }
5190}
5191
5193static int _utf8_char_len(unsigned char c) {
5194 if (c < 0x80) return 1;
5195 if ((c & 0xE0) == 0xC0) return 2;
5196 if ((c & 0xF0) == 0xE0) return 3;
5197 if ((c & 0xF8) == 0xF0) return 4;
5198 return 1; /* invalid byte, treat as single byte */
5199}
5200
5203static float _textarea_content_height(const N_GUI_TEXTAREA_DATA* td, ALLEGRO_FONT* font, float widget_w, float pad) {
5204 if (!font || td->text_len == 0) return 0;
5205 float fh = (float)al_get_font_line_height(font);
5206 float cx = 0;
5207 float cy = fh; /* at least one line */
5208 for (size_t i = 0; i < td->text_len;) {
5209 if (td->text[i] == '\n') {
5210 cx = 0;
5211 cy += fh;
5212 i++;
5213 continue;
5214 }
5215 int clen = _utf8_char_len((unsigned char)td->text[i]);
5216 if (i + (size_t)clen > td->text_len) clen = (int)(td->text_len - i);
5217 char ch[5];
5218 memcpy(ch, &td->text[i], (size_t)clen);
5219 ch[clen] = '\0';
5220 float cw = _text_w(font, ch);
5221 if (cx + cw > widget_w - pad * 2) {
5222 cx = 0;
5223 cy += fh;
5224 }
5225 cx += cw;
5226 i += (size_t)clen;
5227 }
5228 return cy;
5229}
5230
5237 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
5238 if (!w || w->type != N_GUI_TYPE_TEXTAREA || !w->data) return;
5240 if (!td->multiline || td->text_len == 0) return;
5241
5242 float pad = ctx->style.textarea_padding;
5243 ALLEGRO_FONT* font = w->font ? w->font : ctx->default_font;
5244 float view_h = w->h - pad * 2;
5245 float ch = _textarea_content_height(td, font, w->w, pad);
5246 float max_sy = ch - view_h;
5247 if (max_sy < 0) max_sy = 0;
5248 td->scroll_y = (int)max_sy;
5249} /* n_gui_textarea_scroll_to_bottom */
5250
5258void n_gui_textarea_set_selection(N_GUI_CTX* ctx, int widget_id, size_t start, size_t end) {
5259 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
5260 if (!w || w->type != N_GUI_TYPE_TEXTAREA || !w->data) return;
5262 if (start > td->text_len) start = td->text_len;
5263 if (end > td->text_len) end = td->text_len;
5264 td->sel_start = start;
5265 td->sel_end = end;
5266 td->cursor_pos = end;
5267} /* n_gui_textarea_set_selection */
5268
5275void n_gui_textarea_scroll_to_offset(N_GUI_CTX* ctx, int widget_id, size_t byte_offset) {
5276 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
5277 if (!w || w->type != N_GUI_TYPE_TEXTAREA || !w->data) return;
5279 if (!td->multiline || td->text_len == 0) return;
5280
5281 float pad = ctx->style.textarea_padding;
5282 ALLEGRO_FONT* font = w->font ? w->font : ctx->default_font;
5283 if (!font) return;
5284 float fh = (float)al_get_font_line_height(font);
5285 float view_h = w->h - pad * 2;
5286
5287 /* Account for scrollbar width, matching the draw function logic:
5288 first check if scrollbar is needed, then use the narrower width */
5289 float content_h_initial = _textarea_content_height(td, font, w->w, pad);
5290 float sb_size = (content_h_initial > view_h) ? ctx->style.scrollbar_size : 0;
5291 float text_area_w = w->w - sb_size;
5292
5293 /* compute pixel Y of the target byte offset using line-wrap logic
5294 with the same text_area_w the draw function will use */
5295 float cx = 0;
5296 float cy = 0;
5297 size_t target = (byte_offset <= td->text_len) ? byte_offset : td->text_len;
5298 for (size_t i = 0; i < target && i < td->text_len;) {
5299 if (td->text[i] == '\n') {
5300 cx = 0;
5301 cy += fh;
5302 i++;
5303 continue;
5304 }
5305 int clen = _utf8_char_len((unsigned char)td->text[i]);
5306 if (i + (size_t)clen > td->text_len) clen = (int)(td->text_len - i);
5307 char ch[5];
5308 memcpy(ch, &td->text[i], (size_t)clen);
5309 ch[clen] = '\0';
5310 float cw = _text_w(font, ch);
5311 if (cx + cw > text_area_w - pad * 2) {
5312 cx = 0;
5313 cy += fh;
5314 }
5315 cx += cw;
5316 i += (size_t)clen;
5317 }
5318
5319 /* recompute content_h with the same text_area_w for consistent max_sy */
5320 float content_h = _textarea_content_height(td, font, text_area_w, pad);
5321 float max_sy = content_h - view_h;
5322 if (max_sy < 0) max_sy = 0;
5323 float ideal = cy - view_h / 2 + fh / 2;
5324 if (ideal < 0) ideal = 0;
5325 if (ideal > max_sy) ideal = max_sy;
5326 td->scroll_y = (int)ideal;
5327 td->scroll_from_wheel = 0;
5328} /* n_gui_textarea_scroll_to_offset */
5329
5336size_t n_gui_textarea_get_text_length(N_GUI_CTX* ctx, int widget_id) {
5337 const N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
5338 if (!w || w->type != N_GUI_TYPE_TEXTAREA || !w->data) return 0;
5339 const N_GUI_TEXTAREA_DATA* td = (const N_GUI_TEXTAREA_DATA*)w->data;
5340 return td->text_len;
5341} /* n_gui_textarea_get_text_length */
5342
5346static int _justified_char_at_pos(const char* text, ALLEGRO_FONT* font, float max_w, float text_x, float text_y, float scroll_y, float click_mx, float click_my) {
5347 if (!font || !text || !text[0]) return -1;
5348
5349 /* split text into words, tracking their byte offsets in original text */
5350 size_t text_len = strlen(text);
5351 char buf[N_GUI_TEXT_MAX];
5352 snprintf(buf, N_GUI_TEXT_MAX, "%s", text);
5353
5354 /* word start byte offsets in original text */
5355 int word_starts[256];
5356 int word_lens[256]; /* byte lengths */
5357 float word_widths[256];
5358 int nwords = 0;
5359
5360 size_t i = 0;
5361 while (i < text_len && nwords < 256) {
5362 /* skip whitespace */
5363 while (i < text_len && (text[i] == ' ' || text[i] == '\t')) i++;
5364 if (i >= text_len) break;
5365 size_t ws = i;
5366 while (i < text_len && text[i] != ' ' && text[i] != '\t') i++;
5367 int wlen = (int)(i - ws);
5368 word_starts[nwords] = (int)ws;
5369 word_lens[nwords] = wlen;
5370 char tmp[N_GUI_TEXT_MAX];
5371 memcpy(tmp, &text[ws], (size_t)wlen);
5372 tmp[wlen] = '\0';
5373 word_widths[nwords] = _text_w(font, tmp);
5374 nwords++;
5375 }
5376 if (nwords == 0) return 0;
5377
5378 float fh = (float)al_get_font_line_height(font);
5379 float space_w = _text_w(font, " ");
5380 float cy = text_y - scroll_y;
5381 float click_y_content = click_my;
5382
5383 /* find the target line */
5384 int line_start = 0;
5385 while (line_start < nwords) {
5386 float line_words_w = word_widths[line_start];
5387 int line_end = line_start + 1;
5388 for (int j = line_start + 1; j < nwords; j++) {
5389 float test_w = line_words_w + space_w + word_widths[j];
5390 if (test_w > max_w) break;
5391 line_words_w = test_w;
5392 line_end = j + 1;
5393 }
5394
5395 int words_on_line = line_end - line_start;
5396 int is_last_line = (line_end >= nwords);
5397
5398 if (click_y_content >= cy && click_y_content < cy + fh) {
5399 /* this is the target line - find character */
5400 float click_x_rel = click_mx - text_x;
5401 if (click_x_rel < 0) return word_starts[line_start];
5402
5403 /* compute gap for this line */
5404 float gap = space_w;
5405 if (!is_last_line && words_on_line > 1) {
5406 float total_word_w = 0;
5407 for (int j = line_start; j < line_end; j++) total_word_w += word_widths[j];
5408 float total_space = max_w - total_word_w;
5409 if (total_space < 0) total_space = 0;
5410 gap = total_space / (float)(words_on_line - 1);
5411 }
5412
5413 /* walk through words on this line */
5414 float cx = 0;
5415 for (int j = line_start; j < line_end; j++) {
5416 /* check within this word */
5417 if (click_x_rel >= cx && click_x_rel < cx + word_widths[j]) {
5418 /* find character within word */
5419 float wx = 0;
5420 int best_off = 0;
5421 float best_dist = click_x_rel - cx;
5422 if (best_dist < 0) best_dist = -best_dist;
5423 for (int k = 0; k < word_lens[j];) {
5424 int clen = _utf8_char_len((unsigned char)text[word_starts[j] + k]);
5425 if (k + clen > word_lens[j]) clen = word_lens[j] - k;
5426 char ch[5];
5427 memcpy(ch, &text[word_starts[j] + k], (size_t)clen);
5428 ch[clen] = '\0';
5429 float cw = _text_w(font, ch);
5430 wx += cw;
5431 float dist = (click_x_rel - cx) - wx;
5432 if (dist < 0) dist = -dist;
5433 if (dist < best_dist) {
5434 best_dist = dist;
5435 best_off = k + clen;
5436 }
5437 k += clen;
5438 }
5439 return word_starts[j] + best_off;
5440 }
5441 /* check in gap between words */
5442 if (j < line_end - 1 && click_x_rel >= cx + word_widths[j] && click_x_rel < cx + word_widths[j] + gap) {
5443 /* in the gap: snap to end of current word or start of next */
5444 float mid = cx + word_widths[j] + gap / 2.0f;
5445 if (click_x_rel < mid)
5446 return word_starts[j] + word_lens[j];
5447 else
5448 return word_starts[j + 1];
5449 }
5450 cx += word_widths[j] + gap;
5451 }
5452 /* past end of line */
5453 return word_starts[line_end - 1] + word_lens[line_end - 1];
5454 }
5455
5456 cy += fh;
5457 line_start = line_end;
5458 }
5459 /* below all text: return end */
5460 return (int)text_len;
5461}
5462
5465static void _draw_justified_selection(ALLEGRO_FONT* font, float x, float y, float max_w, float max_h, const char* text, int sel_start, int sel_end, ALLEGRO_COLOR sel_color) {
5466 if (!font || !text || !text[0] || sel_start == sel_end) return;
5467 int slo = sel_start < sel_end ? sel_start : sel_end;
5468 int shi = sel_start < sel_end ? sel_end : sel_start;
5469 size_t text_len = strlen(text);
5470 if ((size_t)slo > text_len) slo = (int)text_len;
5471 if ((size_t)shi > text_len) shi = (int)text_len;
5472
5473 /* split into words with byte offsets */
5474 int word_starts[256], word_lens[256];
5475 float word_widths[256];
5476 int nwords = 0;
5477 size_t i = 0;
5478 while (i < text_len && nwords < 256) {
5479 while (i < text_len && (text[i] == ' ' || text[i] == '\t')) i++;
5480 if (i >= text_len) break;
5481 size_t ws = i;
5482 while (i < text_len && text[i] != ' ' && text[i] != '\t') i++;
5483 int wlen = (int)(i - ws);
5484 word_starts[nwords] = (int)ws;
5485 word_lens[nwords] = wlen;
5486 char tmp[N_GUI_TEXT_MAX];
5487 memcpy(tmp, &text[ws], (size_t)wlen);
5488 tmp[wlen] = '\0';
5489 word_widths[nwords] = _text_w(font, tmp);
5490 nwords++;
5491 }
5492 if (nwords == 0) return;
5493
5494 float fh = (float)al_get_font_line_height(font);
5495 float space_w = _text_w(font, " ");
5496 float cy = y;
5497 int line_start = 0;
5498
5499 while (line_start < nwords) {
5500 if (cy + fh > y + max_h + 0.5f) break;
5501 float line_words_w = word_widths[line_start];
5502 int line_end = line_start + 1;
5503 for (int j = line_start + 1; j < nwords; j++) {
5504 float test_w = line_words_w + space_w + word_widths[j];
5505 if (test_w > max_w) break;
5506 line_words_w = test_w;
5507 line_end = j + 1;
5508 }
5509 int words_on_line = line_end - line_start;
5510 int is_last_line = (line_end >= nwords);
5511
5512 /* compute gap for this line */
5513 float gap = space_w;
5514 if (!is_last_line && words_on_line > 1) {
5515 float total_word_w = 0;
5516 for (int j = line_start; j < line_end; j++) total_word_w += word_widths[j];
5517 float total_space = max_w - total_word_w;
5518 if (total_space < 0) total_space = 0;
5519 gap = total_space / (float)(words_on_line - 1);
5520 }
5521
5522 /* walk words and draw selection rects where overlapping */
5523 float cx = x;
5524 for (int j = line_start; j < line_end; j++) {
5525 int ws2 = word_starts[j];
5526 int we = ws2 + word_lens[j];
5527 /* check if selection overlaps this word */
5528 if (slo < we && shi > ws2) {
5529 /* find pixel range within the word */
5530 float sx1 = 0, sx2 = word_widths[j];
5531 if (slo > ws2) {
5532 char tmp2[N_GUI_TEXT_MAX];
5533 int off = slo - ws2;
5534 memcpy(tmp2, &text[ws2], (size_t)off);
5535 tmp2[off] = '\0';
5536 sx1 = _text_w(font, tmp2);
5537 }
5538 if (shi < we) {
5539 char tmp2[N_GUI_TEXT_MAX];
5540 int off = shi - ws2;
5541 memcpy(tmp2, &text[ws2], (size_t)off);
5542 tmp2[off] = '\0';
5543 sx2 = _text_w(font, tmp2);
5544 }
5545 al_draw_filled_rectangle(cx + sx1, cy, cx + sx2, cy + fh, sel_color);
5546 }
5547 /* also highlight the gap (space) between words if selected */
5548 if (j < line_end - 1) {
5549 int gap_start = we; /* byte after word end */
5550 int gap_end = word_starts[j + 1]; /* byte of next word start */
5551 if (slo < gap_end && shi > gap_start) {
5552 al_draw_filled_rectangle(cx + word_widths[j], cy,
5553 cx + word_widths[j] + gap, cy + fh, sel_color);
5554 }
5555 }
5556 cx += word_widths[j] + gap;
5557 }
5558
5559 cy += fh;
5560 line_start = line_end;
5561 }
5562}
5563
5566static float _label_content_height(const char* text, ALLEGRO_FONT* font, float max_w) {
5567 if (!font || !text || !text[0]) return 0;
5568 float fh = (float)al_get_font_line_height(font);
5569 float space_w = _text_w(font, " ");
5570
5571 char buf[N_GUI_TEXT_MAX];
5572 snprintf(buf, N_GUI_TEXT_MAX, "%s", text);
5573
5574 float word_widths[256];
5575 int nwords = 0;
5576 const char* tok = strtok(buf, " \t");
5577 while (tok && nwords < 256) {
5578 word_widths[nwords] = _text_w(font, tok);
5579 nwords++;
5580 tok = strtok(NULL, " \t");
5581 }
5582 if (nwords == 0) return 0;
5583 if (nwords == 1) return fh;
5584
5585 int line_start = 0;
5586 int nlines = 0;
5587 while (line_start < nwords) {
5588 float line_words_w = word_widths[line_start];
5589 int line_end = line_start + 1;
5590 for (int i = line_start + 1; i < nwords; i++) {
5591 float test_w = line_words_w + space_w + word_widths[i];
5592 if (test_w > max_w) break;
5593 line_words_w = test_w;
5594 line_end = i + 1;
5595 }
5596 nlines++;
5597 line_start = line_end;
5598 }
5599 return (float)nlines * fh;
5600}
5601
5606static void _draw_widget_vscrollbar(float area_x, float area_y, float area_w, float view_h, float content_h, float scroll_y, N_GUI_STYLE* style) {
5607 float sb_size = style->scrollbar_size;
5608 float sb_x = area_x + area_w - sb_size;
5609
5610 /* track */
5611 al_draw_filled_rectangle(sb_x, area_y, sb_x + sb_size, area_y + view_h,
5612 style->scrollbar_track_color);
5613
5614 /* thumb */
5615 float ratio = view_h / content_h;
5616 if (ratio > 1.0f) ratio = 1.0f;
5617 float thumb_h = ratio * view_h;
5618 if (thumb_h < style->scrollbar_thumb_min) thumb_h = style->scrollbar_thumb_min;
5619 float max_scroll = content_h - view_h;
5620 float pos_ratio = (max_scroll > 0) ? scroll_y / max_scroll : 0;
5621 float thumb_y = area_y + pos_ratio * (view_h - thumb_h);
5622 al_draw_filled_rounded_rectangle(sb_x + style->scrollbar_thumb_padding, thumb_y,
5623 sb_x + sb_size - style->scrollbar_thumb_padding,
5624 thumb_y + thumb_h,
5626 style->scrollbar_thumb_color);
5627}
5628
5633static float _min_thickness(float requested) {
5634 const ALLEGRO_TRANSFORM* tf = al_get_current_transform();
5635 /* length of each axis basis vector gives the true scale for that axis */
5636 float sx = hypotf(tf->m[0][0], tf->m[0][1]);
5637 float sy = hypotf(tf->m[1][0], tf->m[1][1]);
5638 /* use the larger component to avoid overly thick lines on the minor axis */
5639 float scale = (sx > sy) ? sx : sy;
5640 if (scale < 0.01f) scale = 0.01f;
5641 /* ensure the line is at least 1 physical pixel */
5642 float min_t = 1.0f / scale;
5643 return (requested > min_t) ? requested : min_t;
5644}
5645
5647static ALLEGRO_COLOR _bg_for_state(const N_GUI_THEME* t, int state) {
5648 if (state & N_GUI_STATE_ACTIVE) return t->bg_active;
5649 if (state & N_GUI_STATE_HOVER) return t->bg_hover;
5650 return t->bg_normal;
5651}
5652
5653static ALLEGRO_COLOR _border_for_state(const N_GUI_THEME* t, int state) {
5654 if (state & N_GUI_STATE_ACTIVE) return t->border_active;
5655 if (state & N_GUI_STATE_HOVER) return t->border_hover;
5656 return t->border_normal;
5657}
5658
5659static ALLEGRO_COLOR _text_for_state(const N_GUI_THEME* t, int state) {
5660 if (state & N_GUI_STATE_ACTIVE) return t->text_active;
5661 if (state & N_GUI_STATE_HOVER) return t->text_hover;
5662 return t->text_normal;
5663}
5664
5669static void _draw_themed_rect(N_GUI_THEME* t, int state, float x, float y, float w, float h, int rounded) {
5670 ALLEGRO_COLOR bg = _bg_for_state(t, state);
5671 ALLEGRO_COLOR bd = _border_for_state(t, state);
5672 float thickness = _min_thickness(t->border_thickness);
5673 float ht = thickness * 0.5f;
5674 if (rounded) {
5675 al_draw_filled_rounded_rectangle(x + ht, y + ht, x + w - ht, y + h - ht, t->corner_rx, t->corner_ry, bg);
5676 al_draw_rounded_rectangle(x + ht, y + ht, x + w - ht, y + h - ht, t->corner_rx, t->corner_ry, bd, thickness);
5677 } else {
5678 al_draw_filled_rectangle(x, y, x + w, y + h, bg);
5679 al_draw_rectangle(x + ht, y + ht, x + w - ht, y + h - ht, bd, thickness);
5680 }
5681}
5682
5686static int _shape_rounded(const N_GUI_STYLE* style, int widget_shape) {
5687 if (style->shape_mode == N_GUI_SHAPE_ROUNDED) return 1;
5688 if (style->shape_mode == N_GUI_SHAPE_RECT) return 0;
5689 return (widget_shape == N_GUI_SHAPE_ROUNDED) ? 1 : 0;
5690}
5691
5693static void _draw_button(N_GUI_WIDGET* wgt, float ox, float oy, ALLEGRO_FONT* default_font, const N_GUI_STYLE* style) {
5695 float ax = ox + wgt->x;
5696 float ay = oy + wgt->y;
5697 ALLEGRO_FONT* font = wgt->font ? wgt->font : default_font;
5698
5699 /* For toggle buttons, use the active visual state when toggled on */
5700 int draw_state = wgt->state;
5701 if (bd->toggle_mode && bd->toggled) {
5702 draw_state |= N_GUI_STATE_ACTIVE;
5703 }
5704 /* Flash the pressed visual when a keybind trigger is still within its
5705 * visual-feedback window, so a keyboard-activated button looks pressed
5706 * just like a mouse-clicked one. */
5707 if (bd->key_press_until > 0.0 && al_get_time() < bd->key_press_until) {
5708 draw_state |= N_GUI_STATE_ACTIVE;
5709 }
5710
5711 if (bd->shape == N_GUI_SHAPE_BITMAP && bd->bitmap) {
5712 ALLEGRO_BITMAP* bmp = bd->bitmap;
5713 if ((draw_state & N_GUI_STATE_ACTIVE) && bd->bitmap_active)
5714 bmp = bd->bitmap_active;
5715 else if ((draw_state & N_GUI_STATE_HOVER) && bd->bitmap_hover)
5716 bmp = bd->bitmap_hover;
5717 al_draw_scaled_bitmap(bmp, 0, 0,
5718 (float)al_get_bitmap_width(bmp), (float)al_get_bitmap_height(bmp),
5719 ax, ay, wgt->w, wgt->h, 0);
5720 } else {
5721 int rounded = _shape_rounded(style, bd->shape);
5722 _draw_themed_rect(&wgt->theme, draw_state, ax, ay, wgt->w, wgt->h, rounded);
5723 }
5724
5725 if (font && bd->label[0]) {
5726 ALLEGRO_COLOR tc = _text_for_state(&wgt->theme, draw_state);
5727 int bbx = 0, bby = 0, bbw = 0, bbh = 0;
5728 _text_dims(font, bd->label, &bbx, &bby, &bbw, &bbh);
5729 float tw = (float)bbw;
5730 float th = (float)bbh;
5731 float pad = style->label_padding;
5732 float max_text_w = wgt->w - pad * 2.0f;
5733 if (tw > max_text_w) {
5734 _draw_text_truncated(font, tc, ax + pad, ay + (wgt->h - th) / 2.0f - (float)bby, max_text_w, bd->label);
5735 } else {
5736 al_draw_text(font, tc, ax + (wgt->w - tw) / 2.0f - (float)bbx, ay + (wgt->h - th) / 2.0f - (float)bby, 0, bd->label);
5737 }
5738 }
5739}
5740
5742static void _draw_slider(N_GUI_WIDGET* wgt, float ox, float oy, ALLEGRO_FONT* default_font, N_GUI_STYLE* style) {
5744 float ax = ox + wgt->x;
5745 float ay = oy + wgt->y;
5746 ALLEGRO_FONT* font = wgt->font ? wgt->font : default_font;
5747
5748 double range = sd->max_val - sd->min_val;
5749 double ratio = (range > 0) ? (sd->value - sd->min_val) / range : 0;
5750
5751 if (sd->orientation == N_GUI_SLIDER_V) {
5752 /* vertical slider */
5753 float track_w = style->slider_track_size;
5754 float track_x = ax + (wgt->w - track_w) / 2.0f;
5755 if (sd->track_bitmap) {
5756 al_draw_scaled_bitmap(sd->track_bitmap, 0, 0,
5757 (float)al_get_bitmap_width(sd->track_bitmap), (float)al_get_bitmap_height(sd->track_bitmap),
5758 track_x, ay, track_w, wgt->h, 0);
5759 } else {
5760 al_draw_filled_rounded_rectangle(track_x, ay, track_x + track_w, ay + wgt->h, style->slider_track_corner_r, style->slider_track_corner_r, wgt->theme.bg_normal);
5761 al_draw_rounded_rectangle(track_x, ay, track_x + track_w, ay + wgt->h, style->slider_track_corner_r, style->slider_track_corner_r, wgt->theme.border_normal, _min_thickness(style->slider_track_border_thickness));
5762 }
5763
5764 /* fill from bottom up */
5765 float fill_h = (float)(ratio * (double)wgt->h);
5766 if (sd->fill_bitmap) {
5767 al_draw_scaled_bitmap(sd->fill_bitmap, 0, 0,
5768 (float)al_get_bitmap_width(sd->fill_bitmap), (float)al_get_bitmap_height(sd->fill_bitmap),
5769 track_x, ay + wgt->h - fill_h, track_w, fill_h, 0);
5770 } else {
5771 al_draw_filled_rounded_rectangle(track_x, ay + wgt->h - fill_h, track_x + track_w, ay + wgt->h, style->slider_track_corner_r, style->slider_track_corner_r, wgt->theme.bg_active);
5772 }
5773
5774 /* handle */
5775 float hy = ay + wgt->h - fill_h;
5776 float handle_r = wgt->w / 2.0f - style->slider_handle_edge_offset;
5777 if (handle_r < style->slider_handle_min_r) handle_r = style->slider_handle_min_r;
5778 ALLEGRO_BITMAP* hbmp = _select_state_bitmap(wgt->state, sd->handle_bitmap, sd->handle_hover_bitmap, sd->handle_active_bitmap);
5779 if (hbmp) {
5780 float hd = handle_r * 2.0f;
5781 al_draw_scaled_bitmap(hbmp, 0, 0,
5782 (float)al_get_bitmap_width(hbmp), (float)al_get_bitmap_height(hbmp),
5783 ax + wgt->w / 2.0f - handle_r, hy - handle_r, hd, hd, 0);
5784 } else {
5785 ALLEGRO_COLOR hc = _bg_for_state(&wgt->theme, wgt->state);
5786 ALLEGRO_COLOR hb = _border_for_state(&wgt->theme, wgt->state);
5787 al_draw_filled_circle(ax + wgt->w / 2.0f, hy, handle_r, hc);
5788 al_draw_circle(ax + wgt->w / 2.0f, hy, handle_r, hb, _min_thickness(style->slider_handle_border_thickness));
5789 }
5790
5791 /* value label, respects value_visible + value_format */
5792 if (font && sd->value_visible) {
5793 char val_str[32];
5794 if (sd->value_format[0]) {
5795 /* caller-supplied printf format stored in value_format, intentional non-literal */
5796#pragma GCC diagnostic push
5797#pragma GCC diagnostic ignored "-Wformat-nonliteral"
5798 snprintf(val_str, sizeof(val_str), sd->value_format, sd->value);
5799#pragma GCC diagnostic pop
5800 } else if (sd->mode == N_GUI_SLIDER_PERCENT) {
5801 snprintf(val_str, sizeof(val_str), "%.0f%%", sd->value);
5802 } else {
5803 snprintf(val_str, sizeof(val_str), "%.1f", sd->value);
5804 }
5805 int vbbx = 0, vbby = 0, vbbw = 0, vbbh = 0;
5806 _text_dims(font, val_str, &vbbx, &vbby, &vbbw, &vbbh);
5807 al_draw_text(font, wgt->theme.text_normal, ax + (wgt->w - (float)vbbw) / 2.0f - (float)vbbx, ay + wgt->h + (float)style->slider_value_label_offset, 0, val_str);
5808 }
5809 return;
5810 }
5811
5812 /* horizontal slider (original) */
5813
5814 /* track */
5815 float track_h = style->slider_track_size;
5816 float track_y = ay + (wgt->h - track_h) / 2.0f;
5817 if (sd->track_bitmap) {
5818 al_draw_scaled_bitmap(sd->track_bitmap, 0, 0,
5819 (float)al_get_bitmap_width(sd->track_bitmap), (float)al_get_bitmap_height(sd->track_bitmap),
5820 ax, track_y, wgt->w, track_h, 0);
5821 } else {
5822 al_draw_filled_rounded_rectangle(ax, track_y, ax + wgt->w, track_y + track_h, style->slider_track_corner_r, style->slider_track_corner_r, wgt->theme.bg_normal);
5823 al_draw_rounded_rectangle(ax, track_y, ax + wgt->w, track_y + track_h, style->slider_track_corner_r, style->slider_track_corner_r, wgt->theme.border_normal, _min_thickness(style->slider_track_border_thickness));
5824 }
5825
5826 /* fill */
5827 float fill_w = (float)(ratio * (double)wgt->w);
5828 if (sd->fill_bitmap) {
5829 al_draw_scaled_bitmap(sd->fill_bitmap, 0, 0,
5830 (float)al_get_bitmap_width(sd->fill_bitmap), (float)al_get_bitmap_height(sd->fill_bitmap),
5831 ax, track_y, fill_w, track_h, 0);
5832 } else {
5833 al_draw_filled_rounded_rectangle(ax, track_y, ax + fill_w, track_y + track_h, style->slider_track_corner_r, style->slider_track_corner_r, wgt->theme.bg_active);
5834 }
5835
5836 /* handle */
5837 float hx = ax + fill_w;
5838 float handle_r = wgt->h / 2.0f - style->slider_handle_edge_offset;
5839 if (handle_r < style->slider_handle_min_r) handle_r = style->slider_handle_min_r;
5840 ALLEGRO_BITMAP* hbmp = _select_state_bitmap(wgt->state, sd->handle_bitmap, sd->handle_hover_bitmap, sd->handle_active_bitmap);
5841 if (hbmp) {
5842 float hd = handle_r * 2.0f;
5843 al_draw_scaled_bitmap(hbmp, 0, 0,
5844 (float)al_get_bitmap_width(hbmp), (float)al_get_bitmap_height(hbmp),
5845 hx - handle_r, ay + wgt->h / 2.0f - handle_r, hd, hd, 0);
5846 } else {
5847 ALLEGRO_COLOR hc = _bg_for_state(&wgt->theme, wgt->state);
5848 ALLEGRO_COLOR hb = _border_for_state(&wgt->theme, wgt->state);
5849 al_draw_filled_circle(hx, ay + wgt->h / 2.0f, handle_r, hc);
5850 al_draw_circle(hx, ay + wgt->h / 2.0f, handle_r, hb, _min_thickness(style->slider_handle_border_thickness));
5851 }
5852
5853 /* value label, respects value_visible + value_format */
5854 if (font && sd->value_visible) {
5855 char val_str[32];
5856 if (sd->value_format[0]) {
5857 /* caller-supplied printf format stored in value_format, intentional non-literal */
5858#pragma GCC diagnostic push
5859#pragma GCC diagnostic ignored "-Wformat-nonliteral"
5860 snprintf(val_str, sizeof(val_str), sd->value_format, sd->value);
5861#pragma GCC diagnostic pop
5862 } else if (sd->mode == N_GUI_SLIDER_PERCENT) {
5863 snprintf(val_str, sizeof(val_str), "%.0f%%", sd->value);
5864 } else {
5865 snprintf(val_str, sizeof(val_str), "%.1f", sd->value);
5866 }
5867 int sbbx = 0, sbby = 0, sbbw = 0, sbbh = 0;
5868 _text_dims(font, val_str, &sbbx, &sbby, &sbbw, &sbbh);
5869 float th = (float)sbbh;
5870 al_draw_text(font, wgt->theme.text_normal, ax + wgt->w + style->slider_value_label_offset, ay + (wgt->h - th) / 2.0f - (float)sbby, 0, val_str);
5871 }
5872}
5873
5877static size_t _textarea_pos_from_mouse(const N_GUI_TEXTAREA_DATA* td, ALLEGRO_FONT* font, float mx, float my, float ax, float ay, float widget_w, float widget_h, float pad, float scrollbar_size) {
5878 if (!font || td->text_len == 0) return 0;
5879 float fh = (float)al_get_font_line_height(font);
5880
5881 if (!td->multiline) {
5882 float click_x = mx - (ax + pad) + td->scroll_x;
5883 if (click_x < 0) click_x = 0;
5884 /* build display text for width calculation (masked if mask_char set) */
5885 char display[N_GUI_TEXT_MAX];
5886 if (td->mask_char) {
5887 size_t n = td->text_len;
5888 if (n >= N_GUI_TEXT_MAX) n = N_GUI_TEXT_MAX - 1;
5889 memset(display, td->mask_char, n);
5890 display[n] = '\0';
5891 } else {
5892 memcpy(display, td->text, td->text_len);
5893 display[td->text_len] = '\0';
5894 }
5895 size_t best = 0;
5896 float best_dist = click_x;
5897 char ctmp[N_GUI_TEXT_MAX];
5898 for (size_t ci = 0; ci < td->text_len;) {
5899 int clen = _utf8_char_len((unsigned char)td->text[ci]);
5900 if (ci + (size_t)clen > td->text_len) clen = (int)(td->text_len - ci);
5901 size_t pos = ci + (size_t)clen;
5902 memcpy(ctmp, display, pos);
5903 ctmp[pos] = '\0';
5904 float tw = _text_w(font, ctmp);
5905 float dist = click_x - tw;
5906 if (dist < 0) dist = -dist;
5907 if (dist < best_dist) {
5908 best_dist = dist;
5909 best = pos;
5910 } else {
5911 break;
5912 }
5913 ci += (size_t)clen;
5914 }
5915 return best;
5916 }
5917
5918 /* multiline */
5919 float view_h = widget_h - pad * 2;
5920 float content_h = _textarea_content_height(td, font, widget_w, pad);
5921 float sb_size = (content_h > view_h) ? scrollbar_size : 0.0f;
5922 float text_area_w = widget_w - sb_size;
5923 float avail_w = text_area_w - pad * 2;
5924 float click_x_rel = mx - (ax + pad);
5925 float click_y_content = my - (ay + pad) + (float)td->scroll_y;
5926 if (click_x_rel < 0) click_x_rel = 0;
5927 if (click_y_content < 0) click_y_content = 0;
5928 int target_line = (int)(click_y_content / fh);
5929
5930 float cur_cx = 0;
5931 int cur_line = 0;
5932 size_t best_pos = 0;
5933 float best_dist = click_x_rel;
5934 int found_line = (target_line == 0) ? 1 : 0;
5935
5936 for (size_t ci = 0; ci < td->text_len;) {
5937 if (cur_line > target_line) break;
5938 if (td->text[ci] == '\n') {
5939 if (cur_line == target_line) break;
5940 cur_cx = 0;
5941 cur_line++;
5942 if (cur_line == target_line) {
5943 found_line = 1;
5944 best_pos = ci + 1;
5945 best_dist = click_x_rel;
5946 }
5947 ci++;
5948 continue;
5949 }
5950 int clen = _utf8_char_len((unsigned char)td->text[ci]);
5951 if (ci + (size_t)clen > td->text_len) clen = (int)(td->text_len - ci);
5952 char ch2[5];
5953 memcpy(ch2, &td->text[ci], (size_t)clen);
5954 ch2[clen] = '\0';
5955 float cw = _text_w(font, ch2);
5956 if (cur_cx + cw > avail_w) {
5957 if (cur_line == target_line) break;
5958 cur_cx = 0;
5959 cur_line++;
5960 if (cur_line == target_line) {
5961 found_line = 1;
5962 best_pos = ci;
5963 best_dist = click_x_rel;
5964 }
5965 }
5966 if (cur_line == target_line) {
5967 float dist = click_x_rel - (cur_cx + cw);
5968 if (dist < 0) dist = -dist;
5969 if (dist < best_dist) {
5970 best_dist = dist;
5971 best_pos = ci + (size_t)clen;
5972 }
5973 }
5974 cur_cx += cw;
5975 ci += (size_t)clen;
5976 }
5977 if (!found_line) best_pos = td->text_len;
5978 return best_pos;
5979}
5980
5983 return td->sel_start != td->sel_end;
5984}
5985
5987static void _textarea_sel_range(const N_GUI_TEXTAREA_DATA* td, size_t* lo, size_t* hi) {
5988 if (td->sel_start <= td->sel_end) {
5989 *lo = td->sel_start;
5990 *hi = td->sel_end;
5991 } else {
5992 *lo = td->sel_end;
5993 *hi = td->sel_start;
5994 }
5995}
5996
5999static void _draw_textarea_placeholder(const N_GUI_TEXTAREA_DATA* td, const N_GUI_WIDGET* wgt, ALLEGRO_FONT* font, float tx, float ty) {
6000 float tr, tg, tb, br, bg, bb;
6001 ALLEGRO_COLOR dim;
6002 if (!font || !td->placeholder || !td->placeholder[0] || td->text_len != 0)
6003 return;
6004 al_unmap_rgb_f(wgt->theme.text_normal, &tr, &tg, &tb);
6005 al_unmap_rgb_f(wgt->theme.bg_normal, &br, &bg, &bb);
6006 dim = al_map_rgb_f((tr + br) * 0.5f, (tg + bg) * 0.5f, (tb + bb) * 0.5f);
6007 al_draw_text(font, dim, tx, ty, 0, td->placeholder);
6008}
6009
6011static void _draw_textarea(N_GUI_WIDGET* wgt, float ox, float oy, ALLEGRO_FONT* default_font, N_GUI_STYLE* style) {
6013 float ax = ox + wgt->x;
6014 float ay = oy + wgt->y;
6015 ALLEGRO_FONT* font = wgt->font ? wgt->font : default_font;
6016
6017 int focused = (wgt->state & N_GUI_STATE_FOCUSED) ? 1 : 0;
6018 int hovered = (wgt->state & N_GUI_STATE_HOVER) ? 1 : 0;
6019
6020 /* background, focused wins over hover wins over normal */
6021 if (td->bg_bitmap) {
6022 al_draw_scaled_bitmap(td->bg_bitmap, 0, 0,
6023 (float)al_get_bitmap_width(td->bg_bitmap), (float)al_get_bitmap_height(td->bg_bitmap),
6024 ax, ay, wgt->w, wgt->h, 0);
6025 } else {
6026 ALLEGRO_COLOR bg = wgt->theme.bg_normal;
6027 if (focused)
6028 bg = wgt->theme.bg_active;
6029 else if (hovered)
6030 bg = wgt->theme.bg_hover;
6031 al_draw_filled_rectangle(ax, ay, ax + wgt->w, ay + wgt->h, bg);
6032 }
6033 ALLEGRO_COLOR bd = wgt->theme.border_normal;
6034 if (focused)
6035 bd = wgt->theme.border_active;
6036 else if (hovered)
6037 bd = wgt->theme.border_hover;
6038 al_draw_rectangle(ax, ay, ax + wgt->w, ay + wgt->h, bd, _min_thickness(wgt->theme.border_thickness));
6039
6040 if (!font) return;
6041 float fh = (float)al_get_font_line_height(font);
6042 float pad = style->textarea_padding;
6043
6044 /* cursor blink: visible during first half of blink period */
6045 int cursor_visible = 0;
6046 if (focused) {
6047 double now = al_get_time();
6048 double elapsed = now - td->cursor_time;
6049 float period = style->textarea_cursor_blink_period;
6050 if (period <= 0.0f) period = 1.0f;
6051 double phase = elapsed - (double)(int)(elapsed / (double)period) * (double)period;
6052 cursor_visible = (phase < (double)period * 0.5) ? 1 : 0;
6053 }
6054 float cursor_w = _min_thickness(style->textarea_cursor_width);
6055
6056 if (td->multiline) {
6057 float view_h = wgt->h - pad * 2;
6058 /* two-pass: first check if scrollbar is needed at full width,
6059 * then recompute content_h at the narrower width if so */
6060 float content_h = _textarea_content_height(td, font, wgt->w, pad);
6061 int need_scrollbar = (content_h > view_h) ? 1 : 0;
6062 float sb_size = need_scrollbar ? style->scrollbar_size : 0;
6063 float text_area_w = wgt->w - sb_size;
6064 if (need_scrollbar) {
6065 content_h = _textarea_content_height(td, font, text_area_w, pad);
6066 }
6067
6068 /* auto-scroll to keep cursor visible (skip if user just scrolled with mouse wheel) */
6069 if (focused && !td->scroll_from_wheel) {
6070 /* compute cursor Y position within content */
6071 float cur_cx = 0;
6072 float cur_cy = 0;
6073 for (size_t i = 0; i < td->cursor_pos && i < td->text_len;) {
6074 if (td->text[i] == '\n') {
6075 cur_cx = 0;
6076 cur_cy += fh;
6077 i++;
6078 continue;
6079 }
6080 int clen = _utf8_char_len((unsigned char)td->text[i]);
6081 if (i + (size_t)clen > td->text_len) clen = (int)(td->text_len - i);
6082 char ch[5];
6083 memcpy(ch, &td->text[i], (size_t)clen);
6084 ch[clen] = '\0';
6085 float cw2 = _text_w(font, ch);
6086 if (cur_cx + cw2 > text_area_w - pad * 2) {
6087 cur_cx = 0;
6088 cur_cy += fh;
6089 }
6090 cur_cx += cw2;
6091 i += (size_t)clen;
6092 }
6093 /* ensure cursor line is within visible region */
6094 if (cur_cy < (float)td->scroll_y) {
6095 td->scroll_y = (int)cur_cy;
6096 }
6097 if (cur_cy + fh > (float)td->scroll_y + view_h) {
6098 td->scroll_y = (int)(cur_cy + fh - view_h);
6099 }
6100 }
6101
6102 /* clamp scroll_y */
6103 if (need_scrollbar) {
6104 float max_sy = content_h - view_h;
6105 if (max_sy < 0) max_sy = 0;
6106 if ((float)td->scroll_y > max_sy) td->scroll_y = (int)max_sy;
6107 if (td->scroll_y < 0) td->scroll_y = 0;
6108 } else {
6109 td->scroll_y = 0;
6110 }
6111
6112 /* clip to widget bounds (text area only, leaving room for scrollbar) */
6113 int pcx, pcy, pcw, pch;
6114 al_get_clipping_rectangle(&pcx, &pcy, &pcw, &pch);
6115 _set_clipping_rect_transformed((int)ax, (int)ay, (int)text_area_w, (int)wgt->h);
6116 /* selection range */
6117 size_t sel_lo = 0, sel_hi = 0;
6118 int has_sel = _textarea_has_selection(td);
6119 if (has_sel) _textarea_sel_range(td, &sel_lo, &sel_hi);
6120 ALLEGRO_COLOR sel_bg = wgt->theme.selection_color;
6121
6122 /* simple line-wrap drawing with UTF-8 support */
6123 float cx = ax + pad;
6124 float cy = ay + pad - (float)td->scroll_y;
6125 /* cursor position tracked during layout */
6126 float cursor_cx = cx;
6127 float cursor_cy = cy;
6128 _draw_textarea_placeholder(td, wgt, font, cx, cy);
6129 /* Consecutive drawable characters on one visual line share a colour and
6130 a baseline, so they are accumulated into a run buffer and emitted with
6131 a single al_draw_text instead of one call per glyph. The layout walk
6132 (per-character wrap, cursor tracking, selection rects) is unchanged;
6133 only the text emission is batched. A run is flushed at a wrap, a
6134 newline, when the buffer fills, or at the end. Selection rects stay
6135 per-character and are drawn during the walk, i.e. before the run they
6136 belong to is flushed, so the text still lands on top of them. */
6137 char run[256];
6138 int run_len = 0;
6139 float run_x = cx, run_y = cy;
6140 int run_vis = 0;
6141 for (size_t i = 0; i < td->text_len;) {
6142 if (i == td->cursor_pos) {
6143 cursor_cx = cx;
6144 cursor_cy = cy;
6145 }
6146 if (td->text[i] == '\n') {
6147 if (run_len > 0) {
6148 if (run_vis) {
6149 run[run_len] = '\0';
6150 al_draw_text(font, wgt->theme.text_normal, run_x, run_y, 0, run);
6151 }
6152 run_len = 0;
6153 }
6154 /* draw selection highlight on newline (small rect at end of line) */
6155 if (has_sel && i >= sel_lo && i < sel_hi && cy + fh > ay && cy < ay + wgt->h) {
6156 float space_w = _text_w(font, " ");
6157 al_draw_filled_rectangle(cx, cy, cx + space_w, cy + fh, sel_bg);
6158 }
6159 cx = ax + pad;
6160 cy += fh;
6161 i++;
6162 continue;
6163 }
6164 int clen = _utf8_char_len((unsigned char)td->text[i]);
6165 if (i + (size_t)clen > td->text_len) clen = (int)(td->text_len - i);
6166 char ch[5];
6167 memcpy(ch, &td->text[i], (size_t)clen);
6168 ch[clen] = '\0';
6169 float cw = _text_w(font, ch);
6170 if (cx + cw > ax + text_area_w - pad) {
6171 /* wrap: the run so far ends on the current line */
6172 if (run_len > 0) {
6173 if (run_vis) {
6174 run[run_len] = '\0';
6175 al_draw_text(font, wgt->theme.text_normal, run_x, run_y, 0, run);
6176 }
6177 run_len = 0;
6178 }
6179 cx = ax + pad;
6180 cy += fh;
6181 if (i == td->cursor_pos) {
6182 cursor_cx = cx;
6183 cursor_cy = cy;
6184 }
6185 }
6186 int vis = (cy + fh > ay && cy < ay + wgt->h);
6187 if (vis) {
6188 /* draw selection highlight behind character */
6189 if (has_sel && i >= sel_lo && i < sel_hi) {
6190 al_draw_filled_rectangle(cx, cy, cx + cw, cy + fh, sel_bg);
6191 }
6192 /* start a run here if none is open */
6193 if (run_len == 0) {
6194 run_x = cx;
6195 run_y = cy;
6196 run_vis = 1;
6197 }
6198 /* flush and restart if the buffer would overflow */
6199 if (run_len + clen >= (int)sizeof(run) - 1) {
6200 run[run_len] = '\0';
6201 al_draw_text(font, wgt->theme.text_normal, run_x, run_y, 0, run);
6202 run_len = 0;
6203 run_x = cx;
6204 run_y = cy;
6205 }
6206 memcpy(run + run_len, ch, (size_t)clen);
6207 run_len += clen;
6208 }
6209 cx += cw;
6210 /* check cursor positions within the multi-byte character */
6211 for (int b = 1; b < clen; b++) {
6212 if (i + (size_t)b == td->cursor_pos) {
6213 cursor_cx = cx;
6214 cursor_cy = cy;
6215 }
6216 }
6217 i += (size_t)clen;
6218 }
6219 /* flush the trailing run */
6220 if (run_len > 0 && run_vis) {
6221 run[run_len] = '\0';
6222 al_draw_text(font, wgt->theme.text_normal, run_x, run_y, 0, run);
6223 }
6224 /* cursor is after all text when cursor_pos >= text_len */
6225 if (td->cursor_pos >= td->text_len) {
6226 cursor_cx = cx;
6227 cursor_cy = cy;
6228 }
6229 /* cursor */
6230 if (cursor_visible) {
6231 al_draw_filled_rectangle(cursor_cx, cursor_cy, cursor_cx + cursor_w, cursor_cy + fh, wgt->theme.text_active);
6232 }
6233 al_set_clipping_rectangle(pcx, pcy, pcw, pch);
6234
6235 /* draw scrollbar outside clipping region */
6236 if (need_scrollbar) {
6237 _draw_widget_vscrollbar(ax, ay + pad, wgt->w, view_h, content_h, (float)td->scroll_y, style);
6238 }
6239 } else {
6240 /* single line with horizontal scrolling */
6241 float ty = ay + (wgt->h - fh) / 2.0f;
6242 float inner_w = wgt->w - pad * 2;
6243
6244 /* build display text (masked if mask_char is set) */
6245 char display_text[N_GUI_TEXT_MAX];
6246 if (td->mask_char && td->text_len > 0) {
6247 size_t n = td->text_len;
6248 if (n >= N_GUI_TEXT_MAX) n = N_GUI_TEXT_MAX - 1;
6249 memset(display_text, td->mask_char, n);
6250 display_text[n] = '\0';
6251 } else {
6252 memcpy(display_text, td->text, td->text_len);
6253 display_text[td->text_len] = '\0';
6254 }
6255
6256 /* compute cursor pixel offset from text start */
6257 char tmp[N_GUI_TEXT_MAX];
6258 size_t cpos = td->cursor_pos;
6259 if (cpos > td->text_len) cpos = td->text_len;
6260 memcpy(tmp, display_text, cpos);
6261 tmp[cpos] = '\0';
6262 float cursor_px = _text_w(font, tmp);
6263
6264 /* adjust scroll_x so cursor stays visible within the inner area */
6265 if (cursor_px - td->scroll_x < 0) {
6266 td->scroll_x = cursor_px;
6267 }
6268 if (cursor_px - td->scroll_x > inner_w - cursor_w) {
6269 td->scroll_x = cursor_px - inner_w + cursor_w;
6270 }
6271 if (td->scroll_x < 0) td->scroll_x = 0;
6272
6273 /* clip text to widget inner bounds */
6274 int pcx, pcy, pcw, pch;
6275 al_get_clipping_rectangle(&pcx, &pcy, &pcw, &pch);
6276 _set_clipping_rect_transformed((int)(ax + pad), (int)ay, (int)inner_w, (int)wgt->h);
6277 /* draw selection highlight for single-line */
6278 if (_textarea_has_selection(td)) {
6279 size_t sl, sh;
6280 _textarea_sel_range(td, &sl, &sh);
6281 char stmp[N_GUI_TEXT_MAX];
6282 memcpy(stmp, display_text, sl);
6283 stmp[sl] = '\0';
6284 float sel_x1 = _text_w(font, stmp);
6285 memcpy(stmp, display_text, sh);
6286 stmp[sh] = '\0';
6287 float sel_x2 = _text_w(font, stmp);
6288 float sx1 = ax + pad + sel_x1 - td->scroll_x;
6289 float sx2 = ax + pad + sel_x2 - td->scroll_x;
6290 al_draw_filled_rectangle(sx1, ty, sx2, ty + fh, wgt->theme.selection_color);
6291 }
6292 _draw_textarea_placeholder(td, wgt, font, ax + pad, ty);
6293 al_draw_text(font, wgt->theme.text_normal, ax + pad - td->scroll_x, ty, 0, display_text);
6294 /* cursor */
6295 if (cursor_visible) {
6296 float cx = ax + pad + cursor_px - td->scroll_x;
6297 al_draw_filled_rectangle(cx, ty, cx + cursor_w, ty + fh, wgt->theme.text_active);
6298 }
6299 al_set_clipping_rectangle(pcx, pcy, pcw, pch);
6300 }
6301}
6302
6304static void _draw_checkbox(N_GUI_WIDGET* wgt, float ox, float oy, ALLEGRO_FONT* default_font, N_GUI_STYLE* style) {
6306 float ax = ox + wgt->x;
6307 float ay = oy + wgt->y;
6308 ALLEGRO_FONT* font = wgt->font ? wgt->font : default_font;
6309
6310 float box_size = wgt->h < style->checkbox_max_size ? wgt->h : style->checkbox_max_size;
6311 float box_y = ay + (wgt->h - box_size) / 2.0f;
6312
6313 /* select bitmap: hover > checked/unchecked > color theme */
6314 ALLEGRO_BITMAP* box_bmp = NULL;
6315 if ((wgt->state & N_GUI_STATE_HOVER) && cd->box_hover_bitmap) {
6316 box_bmp = cd->box_hover_bitmap;
6317 } else if (cd->checked && cd->box_checked_bitmap) {
6318 box_bmp = cd->box_checked_bitmap;
6319 } else if (cd->box_bitmap) {
6320 box_bmp = cd->box_bitmap;
6321 }
6322
6323 if (box_bmp) {
6324 al_draw_scaled_bitmap(box_bmp, 0, 0,
6325 (float)al_get_bitmap_width(box_bmp), (float)al_get_bitmap_height(box_bmp),
6326 ax, box_y, box_size, box_size, 0);
6327 } else {
6328 _draw_themed_rect(&wgt->theme, wgt->state, ax, box_y, box_size, box_size, 0);
6329
6330 if (cd->checked) {
6331 /* draw checkmark as two lines */
6332 ALLEGRO_COLOR tc = wgt->theme.text_active;
6333 float m = style->checkbox_mark_margin;
6334 al_draw_line(ax + m, box_y + box_size / 2.0f,
6335 ax + box_size / 2.0f, box_y + box_size - m, tc, _min_thickness(style->checkbox_mark_thickness));
6336 al_draw_line(ax + box_size / 2.0f, box_y + box_size - m,
6337 ax + box_size - m, box_y + m, tc, _min_thickness(style->checkbox_mark_thickness));
6338 }
6339 }
6340
6341 if (font && cd->label[0]) {
6342 int cbbx = 0, cbby = 0, cbbw = 0, cbbh = 0;
6343 _text_dims(font, cd->label, &cbbx, &cbby, &cbbw, &cbbh);
6344 float fh = (float)cbbh;
6345 float label_max_w = wgt->w - box_size - style->checkbox_label_gap;
6347 ax + box_size + style->checkbox_label_offset, ay + (wgt->h - fh) / 2.0f - (float)cbby, label_max_w, cd->label);
6348 }
6349}
6350
6352static void _draw_scrollbar(N_GUI_WIDGET* wgt, float ox, float oy, const N_GUI_STYLE* style) {
6354 float ax = ox + wgt->x;
6355 float ay = oy + wgt->y;
6356 int rounded = (sb->shape == N_GUI_SHAPE_ROUNDED) ? 1 : 0;
6357
6358 /* track */
6359 if (sb->track_bitmap) {
6360 al_draw_scaled_bitmap(sb->track_bitmap, 0, 0,
6361 (float)al_get_bitmap_width(sb->track_bitmap), (float)al_get_bitmap_height(sb->track_bitmap),
6362 ax, ay, wgt->w, wgt->h, 0);
6363 } else {
6364 _draw_themed_rect(&wgt->theme, N_GUI_STATE_IDLE, ax, ay, wgt->w, wgt->h, rounded);
6365 }
6366
6367 /* compute thumb */
6368 double ratio = sb->viewport_size / sb->content_size;
6369 if (ratio > 1.0) ratio = 1.0;
6370 double max_scroll = sb->content_size - sb->viewport_size;
6371 if (max_scroll < 0) max_scroll = 0;
6372 double pos_ratio = (max_scroll > 0) ? sb->scroll_pos / max_scroll : 0;
6373
6374 float thumb_x, thumb_y, thumb_w, thumb_h;
6375 if (sb->orientation == N_GUI_SCROLLBAR_V) {
6376 thumb_h = (float)(ratio * (double)wgt->h);
6377 if (thumb_h < style->scrollbar_thumb_min) thumb_h = style->scrollbar_thumb_min;
6378 thumb_w = wgt->w - style->scrollbar_thumb_padding * 2;
6379 thumb_x = ax + style->scrollbar_thumb_padding;
6380 float track_range = wgt->h - thumb_h;
6381 thumb_y = ay + (float)(pos_ratio * (double)track_range);
6382 } else {
6383 thumb_w = (float)(ratio * (double)wgt->w);
6384 if (thumb_w < style->scrollbar_thumb_min) thumb_w = style->scrollbar_thumb_min;
6385 thumb_h = wgt->h - style->scrollbar_thumb_padding * 2;
6386 thumb_y = ay + style->scrollbar_thumb_padding;
6387 float track_range = wgt->w - thumb_w;
6388 thumb_x = ax + (float)(pos_ratio * (double)track_range);
6389 }
6390
6391 ALLEGRO_BITMAP* tbmp = _select_state_bitmap(wgt->state, sb->thumb_bitmap, sb->thumb_hover_bitmap, sb->thumb_active_bitmap);
6392 if (tbmp) {
6393 al_draw_scaled_bitmap(tbmp, 0, 0,
6394 (float)al_get_bitmap_width(tbmp), (float)al_get_bitmap_height(tbmp),
6395 thumb_x, thumb_y, thumb_w, thumb_h, 0);
6396 } else {
6397 _draw_themed_rect(&wgt->theme, wgt->state, thumb_x, thumb_y, thumb_w, thumb_h, rounded);
6398 }
6399}
6400
6404static void _draw_rows_scrollbar(float ax, float ay, float w, float h, int nb_rows, int visible, int scroll_offset, const N_GUI_STYLE* style, N_GUI_THEME* theme) {
6405 float sb_w = style->scrollbar_size;
6406 float sb_x = ax + w - sb_w;
6407 int max_off = nb_rows - visible;
6408 float ratio, thumb_h, track_range, pos_ratio, thumb_y;
6409 if (max_off < 0) max_off = 0;
6410 /* use the dedicated scrollbar colours (like _draw_widget_vscrollbar) so the
6411 track and thumb stay visible regardless of the widget bg/hover theme */
6412 al_draw_filled_rectangle(sb_x, ay, ax + w, ay + h, style->scrollbar_track_color);
6413 al_draw_line(sb_x, ay, sb_x, ay + h, theme->border_normal, 1.0f);
6414 ratio = (nb_rows > 0) ? (float)visible / (float)nb_rows : 1.0f;
6415 if (ratio > 1.0f) ratio = 1.0f;
6416 thumb_h = ratio * h;
6417 if (thumb_h < style->scrollbar_thumb_min) thumb_h = style->scrollbar_thumb_min;
6418 track_range = h - thumb_h;
6419 pos_ratio = (max_off > 0) ? (float)scroll_offset / (float)max_off : 0.0f;
6420 thumb_y = ay + pos_ratio * track_range;
6421 al_draw_filled_rounded_rectangle(sb_x + style->scrollbar_thumb_padding, thumb_y, ax + w - style->scrollbar_thumb_padding, thumb_y + thumb_h, style->scrollbar_thumb_corner_r, style->scrollbar_thumb_corner_r, style->scrollbar_thumb_color);
6422}
6423
6427static void _draw_cols_scrollbar(float ax, float bar_y, float track_w, float content_w, float h_scroll, const N_GUI_STYLE* style, N_GUI_THEME* theme) {
6428 float sb_h = style->scrollbar_size;
6429 float max_off = content_w - track_w;
6430 float ratio, thumb_w, track_range, pos_ratio, thumb_x;
6431 if (max_off < 0) max_off = 0;
6432 al_draw_filled_rectangle(ax, bar_y, ax + track_w, bar_y + sb_h, style->scrollbar_track_color);
6433 al_draw_line(ax, bar_y, ax + track_w, bar_y, theme->border_normal, 1.0f);
6434 ratio = (content_w > 0) ? track_w / content_w : 1.0f;
6435 if (ratio > 1.0f) ratio = 1.0f;
6436 thumb_w = ratio * track_w;
6437 if (thumb_w < style->scrollbar_thumb_min) thumb_w = style->scrollbar_thumb_min;
6438 track_range = track_w - thumb_w;
6439 pos_ratio = (max_off > 0) ? h_scroll / max_off : 0.0f;
6440 thumb_x = ax + pos_ratio * track_range;
6441 al_draw_filled_rounded_rectangle(thumb_x + style->scrollbar_thumb_padding, bar_y + style->scrollbar_thumb_padding, thumb_x + thumb_w - style->scrollbar_thumb_padding, bar_y + sb_h - style->scrollbar_thumb_padding, style->scrollbar_thumb_corner_r, style->scrollbar_thumb_corner_r, style->scrollbar_thumb_color);
6442}
6443
6449static void _datagrid_hmetrics(const N_GUI_WIDGET* wgt, const N_GUI_DATAGRID_DATA* gd, ALLEGRO_FONT* font, const N_GUI_STYLE* style, float* content_w, float* pane_w, int* need_hsb) {
6450 float fh = font ? (float)al_get_font_line_height(font) : 16.0f;
6451 float pad = style->item_text_padding;
6452 float row_h = fh + style->item_height_pad;
6453 float data_h = wgt->h - row_h; /* minus the header row */
6454 int visible = (int)(data_h / row_h);
6455 int need_sb;
6456 float cw = pad, pw;
6457 size_t c;
6458 if (visible < 1) visible = 1;
6459 need_sb = ((int)gd->nb_rows > visible) ? 1 : 0;
6460 for (c = 0; c < gd->nb_cols; c++) {
6461 size_t pc = _datagrid_phys(gd, c);
6462 if (gd->cols[pc].visible) cw += gd->cols[pc].width;
6463 }
6464 pw = wgt->w - (need_sb ? style->scrollbar_size : 0.0f);
6465 if (content_w) *content_w = cw;
6466 if (pane_w) *pane_w = pw;
6467 if (need_hsb) *need_hsb = (cw > pw + 1.0f) ? 1 : 0;
6468}
6469
6471static void _draw_hexview(N_GUI_WIDGET* wgt, float ox, float oy, ALLEGRO_FONT* default_font, N_GUI_STYLE* style) {
6473 float ax = ox + wgt->x;
6474 float ay = oy + wgt->y;
6475 ALLEGRO_FONT* font = wgt->font ? wgt->font : default_font;
6476 float fh = font ? (float)al_get_font_line_height(font) : 16.0f;
6477 float pad = style->textarea_padding;
6478 float row_h = fh + 2.0f;
6479 int bpr = hd->bytes_per_row > 0 ? hd->bytes_per_row : 16;
6480 int nb_rows = (int)((hd->len + (size_t)bpr - 1) / (size_t)bpr);
6481 int visible = (int)((wgt->h - pad * 2.0f) / row_h);
6482 int need_sb, max_off, i, pcx, pcy, pcw, pch;
6483 float sb_w;
6484
6485 al_draw_filled_rectangle(ax, ay, ax + wgt->w, ay + wgt->h, wgt->theme.bg_normal);
6486 al_draw_rectangle(ax, ay, ax + wgt->w, ay + wgt->h, wgt->theme.border_normal, _min_thickness(wgt->theme.border_thickness));
6487 if (visible < 1) visible = 1;
6488 need_sb = (nb_rows > visible) ? 1 : 0;
6489 sb_w = need_sb ? style->scrollbar_size : 0.0f;
6490 max_off = nb_rows - visible;
6491 if (max_off < 0) max_off = 0;
6492 if (hd->scroll_offset > max_off) hd->scroll_offset = max_off;
6493 if (hd->scroll_offset < 0) hd->scroll_offset = 0;
6494
6495 al_get_clipping_rectangle(&pcx, &pcy, &pcw, &pch);
6496 al_set_clipping_rectangle((int)ax, (int)ay, (int)(wgt->w - sb_w), (int)wgt->h);
6497 for (i = 0; i < visible; i++) {
6498 int row = hd->scroll_offset + i;
6499 size_t base = (size_t)row * (size_t)bpr;
6500 char line[256];
6501 char asc[32];
6502 int n = 0, j, a = 0;
6503 float ty = ay + pad + (float)i * row_h;
6504 if (base >= hd->len) break;
6505 n += snprintf(line + n, sizeof(line) - (size_t)n, "%08lX ", (unsigned long)base);
6506 for (j = 0; j < bpr; j++) {
6507 if (base + (size_t)j < hd->len) {
6508 unsigned char c = hd->data[base + (size_t)j];
6509 n += snprintf(line + n, sizeof(line) - (size_t)n, "%02X ", c);
6510 asc[a++] = (c >= 32 && c < 127) ? (char)c : '.';
6511 } else {
6512 n += snprintf(line + n, sizeof(line) - (size_t)n, " ");
6513 }
6514 }
6515 asc[a] = '\0';
6516 snprintf(line + n, sizeof(line) - (size_t)n, " |%s|", asc);
6517 al_draw_text(font, wgt->theme.text_normal, ax + pad, ty, 0, line);
6518 }
6519 al_set_clipping_rectangle(pcx, pcy, pcw, pch);
6520 if (need_sb)
6521 _draw_rows_scrollbar(ax, ay, wgt->w, wgt->h, nb_rows, visible, hd->scroll_offset, style, &wgt->theme);
6522}
6523
6525static int _text_line_count(const char* s) {
6526 int n = 0;
6527 if (!s || !s[0]) return 0;
6528 n = 1;
6529 while (*s) {
6530 if (*s == '\n') n++;
6531 s++;
6532 }
6533 return n;
6534}
6535
6537static void _syntax_run(ALLEGRO_FONT* font, ALLEGRO_COLOR col, float* cx, float y, const char* s, int len) {
6538 char buf[512];
6539 if (len <= 0) return;
6540 if (len > (int)sizeof(buf) - 1) len = (int)sizeof(buf) - 1;
6541 memcpy(buf, s, (size_t)len);
6542 buf[len] = '\0';
6543 al_draw_text(font, col, *cx, y, 0, buf);
6544 *cx += _text_w(font, buf);
6545}
6546
6549static int _syntax_js_regex_kw(const char* s, int n) {
6550 static const char* kw[] = {"await", "case", "delete", "do", "else", "in",
6551 "instanceof", "new", "of", "return", "throw",
6552 "typeof", "void", "yield", NULL};
6553 for (size_t it = 0; kw[it]; it++) {
6554 if ((int)strlen(kw[it]) == n && strncmp(s, kw[it], (size_t)n) == 0) return 1;
6555 }
6556 return 0;
6557}
6558
6561static int _syntax_js_regex_possible(char c) {
6562 if (c == 0) return 1;
6563 return strchr("(,=:[!&|?;{}<>+-*%~^", c) != NULL;
6564}
6565
6567static int _syntax_js_keyword(const char* s, int n) {
6568 static const char* kw[] = {"async", "await", "break", "case", "catch", "class", "const",
6569 "continue", "debugger", "default", "delete", "do", "else",
6570 "export", "extends", "false", "finally", "for", "function",
6571 "if", "import", "in", "instanceof", "let", "new", "null",
6572 "of", "return", "static", "super", "switch", "this", "throw",
6573 "true", "try", "typeof", "undefined", "var", "void", "while",
6574 "with", "yield", NULL};
6575 for (size_t it = 0; kw[it]; it++) {
6576 if ((int)strlen(kw[it]) == n && strncmp(s, kw[it], (size_t)n) == 0) return 1;
6577 }
6578 return 0;
6579}
6580
6582static void _syntax_draw_line(ALLEGRO_FONT* font, N_GUI_THEME* th, float x, float y, const char* s, int n, int mode, int line_idx, int in_headers) {
6583 float cx = x;
6584 if (mode == N_GUI_SYNTAX_HTTP) {
6585 if (line_idx == 0) {
6586 _syntax_run(font, th->text_active, &cx, y, s, n);
6587 return;
6588 }
6589 if (in_headers && n > 0) {
6590 int c = 0;
6591 while (c < n && s[c] != ':') c++;
6592 if (c > 0 && c < n) {
6593 _syntax_run(font, th->border_active, &cx, y, s, c);
6594 _syntax_run(font, th->text_normal, &cx, y, s + c, n - c);
6595 return;
6596 }
6597 }
6598 _syntax_run(font, th->text_normal, &cx, y, s, n);
6599 return;
6600 }
6601 if (mode == N_GUI_SYNTAX_JSON) {
6602 int i = 0;
6603 while (i < n) {
6604 char c = s[i];
6605 if (c == '"') {
6606 int j = i + 1;
6607 while (j < n && s[j] != '"') {
6608 if (s[j] == '\\' && j + 1 < n) j++;
6609 j++;
6610 }
6611 if (j < n) j++;
6612 _syntax_run(font, th->text_active, &cx, y, s + i, j - i);
6613 i = j;
6614 } else if ((c >= '0' && c <= '9') || (c == '-' && i + 1 < n && s[i + 1] >= '0' && s[i + 1] <= '9')) {
6615 int j = i + 1;
6616 while (j < n && ((s[j] >= '0' && s[j] <= '9') || s[j] == '.' || s[j] == 'e' || s[j] == 'E' || s[j] == '+' || s[j] == '-')) j++;
6617 _syntax_run(font, th->border_active, &cx, y, s + i, j - i);
6618 i = j;
6619 } else if (c == '{' || c == '}' || c == '[' || c == ']' || c == ':' || c == ',') {
6620 _syntax_run(font, th->border_hover, &cx, y, s + i, 1);
6621 i++;
6622 } else {
6623 int j = i + 1;
6624 while (j < n && !(s[j] == '"' || (s[j] >= '0' && s[j] <= '9') || s[j] == '{' || s[j] == '}' || s[j] == '[' || s[j] == ']' || s[j] == ':' || s[j] == ',')) j++;
6625 _syntax_run(font, th->text_normal, &cx, y, s + i, j - i);
6626 i = j;
6627 }
6628 }
6629 return;
6630 }
6631 if (mode == N_GUI_SYNTAX_XML) {
6632 int i = 0;
6633 while (i < n) {
6634 if (s[i] == '<') {
6635 if (i + 4 <= n && memcmp(s + i, "<!--", 4) == 0) {
6636 /* comment: color until --> or the end of the line */
6637 int j = i + 4;
6638 while (j + 3 <= n && memcmp(s + j, "-->", 3) != 0) j++;
6639 j = (j + 3 <= n) ? j + 3 : n;
6640 _syntax_run(font, th->border_hover, &cx, y, s + i, j - i);
6641 i = j;
6642 continue;
6643 }
6644 /* '<' plus optional '/', '!' or '?' */
6645 int j = i + 1;
6646 while (j < n && (s[j] == '/' || s[j] == '!' || s[j] == '?')) j++;
6647 _syntax_run(font, th->border_hover, &cx, y, s + i, j - i);
6648 i = j;
6649 /* element name (j continues from the delimiter run) */
6650 while (j < n && (isalnum((unsigned char)s[j]) || s[j] == ':' || s[j] == '-' || s[j] == '_')) j++;
6651 if (j > i) _syntax_run(font, th->border_active, &cx, y, s + i, j - i);
6652 i = j;
6653 /* attributes until '>' (quote aware) */
6654 while (i < n && s[i] != '>') {
6655 char q = s[i];
6656 if (q == '"' || q == '\'') {
6657 j = i + 1;
6658 while (j < n && s[j] != q) j++;
6659 if (j < n) j++;
6660 _syntax_run(font, th->text_active, &cx, y, s + i, j - i);
6661 i = j;
6662 } else {
6663 j = i + 1;
6664 while (j < n && s[j] != '>' && s[j] != '"' && s[j] != '\'') j++;
6665 _syntax_run(font, th->text_normal, &cx, y, s + i, j - i);
6666 i = j;
6667 }
6668 }
6669 if (i < n) {
6670 _syntax_run(font, th->border_hover, &cx, y, s + i, 1);
6671 i++;
6672 }
6673 } else {
6674 int j = i + 1;
6675 while (j < n && s[j] != '<') j++;
6676 _syntax_run(font, th->text_normal, &cx, y, s + i, j - i);
6677 i = j;
6678 }
6679 }
6680 return;
6681 }
6682 if (mode == N_GUI_SYNTAX_YAML) {
6683 int i = 0;
6684 /* leading indentation, then '- ' list markers */
6685 while (i < n && (s[i] == ' ' || s[i] == '\t')) i++;
6686 if (i > 0) _syntax_run(font, th->text_normal, &cx, y, s, i);
6687 while (i < n && s[i] == '-' && (i + 1 >= n || s[i + 1] == ' ')) {
6688 _syntax_run(font, th->border_hover, &cx, y, s + i, 1);
6689 i++;
6690 if (i < n) {
6691 _syntax_run(font, th->text_normal, &cx, y, s + i, 1);
6692 i++;
6693 }
6694 }
6695 if (i < n && s[i] == '#') {
6696 _syntax_run(font, th->border_hover, &cx, y, s + i, n - i);
6697 return;
6698 }
6699 /* key: up to the first ':' followed by a space or the line end, outside quotes */
6700 {
6701 int key_end = -1;
6702 char q = 0;
6703 for (int j = i; j < n; j++) {
6704 if (q) {
6705 if (s[j] == q) q = 0;
6706 continue;
6707 }
6708 if (s[j] == '"' || s[j] == '\'') {
6709 q = s[j];
6710 continue;
6711 }
6712 if (s[j] == '#') break;
6713 if (s[j] == ':' && (j + 1 >= n || s[j + 1] == ' ')) {
6714 key_end = j;
6715 break;
6716 }
6717 }
6718 if (key_end >= 0) {
6719 _syntax_run(font, th->border_active, &cx, y, s + i, key_end - i);
6720 _syntax_run(font, th->border_hover, &cx, y, s + key_end, 1);
6721 i = key_end + 1;
6722 }
6723 }
6724 /* value: quoted runs, a trailing comment, plain text */
6725 while (i < n) {
6726 char c = s[i];
6727 if (c == '"' || c == '\'') {
6728 int j = i + 1;
6729 while (j < n && s[j] != c) j++;
6730 if (j < n) j++;
6731 _syntax_run(font, th->text_active, &cx, y, s + i, j - i);
6732 i = j;
6733 } else if (c == '#') {
6734 _syntax_run(font, th->border_hover, &cx, y, s + i, n - i);
6735 i = n;
6736 } else {
6737 int j = i + 1;
6738 while (j < n && s[j] != '"' && s[j] != '\'' && s[j] != '#') j++;
6739 _syntax_run(font, th->text_normal, &cx, y, s + i, j - i);
6740 i = j;
6741 }
6742 }
6743 return;
6744 }
6745 if (mode == N_GUI_SYNTAX_JS) {
6746 int i = 0;
6747 char last_sig = 0;
6748 int kw_regex = 0;
6749 while (i < n) {
6750 char c = s[i];
6751 if (c == '/' && i + 1 < n && s[i + 1] == '/') {
6752 _syntax_run(font, th->border_hover, &cx, y, s + i, n - i);
6753 return;
6754 }
6755 if (c == '/' && i + 1 < n && s[i + 1] == '*') {
6756 int j = i + 2;
6757 while (j + 2 <= n && memcmp(s + j, "*/", 2) != 0) j++;
6758 j = (j + 2 <= n) ? j + 2 : n;
6759 _syntax_run(font, th->border_hover, &cx, y, s + i, j - i);
6760 i = j;
6761 } else if (c == '/' && (kw_regex || _syntax_js_regex_possible(last_sig))) {
6762 /* regex literal: escapes and character classes honored */
6763 int j = i + 1;
6764 int in_class = 0;
6765 int closed = 0;
6766 while (j < n) {
6767 if (s[j] == '\\' && j + 1 < n) {
6768 j += 2;
6769 continue;
6770 }
6771 if (in_class) {
6772 if (s[j] == ']') in_class = 0;
6773 } else if (s[j] == '[') {
6774 in_class = 1;
6775 } else if (s[j] == '/') {
6776 j++;
6777 closed = 1;
6778 break;
6779 }
6780 j++;
6781 }
6782 if (closed) {
6783 while (j < n && isalpha((unsigned char)s[j])) j++;
6784 }
6785 _syntax_run(font, th->text_active, &cx, y, s + i, j - i);
6786 last_sig = '/';
6787 kw_regex = 0;
6788 i = j;
6789 } else if (c == '"' || c == '\'' || c == '`') {
6790 int j = i + 1;
6791 while (j < n && s[j] != c) {
6792 if (s[j] == '\\' && j + 1 < n) j++;
6793 j++;
6794 }
6795 if (j < n) j++;
6796 _syntax_run(font, th->text_active, &cx, y, s + i, j - i);
6797 last_sig = c;
6798 kw_regex = 0;
6799 i = j;
6800 } else if (c >= '0' && c <= '9') {
6801 int j = i + 1;
6802 while (j < n && (isalnum((unsigned char)s[j]) || s[j] == '.')) j++;
6803 _syntax_run(font, th->border_active, &cx, y, s + i, j - i);
6804 last_sig = s[j - 1];
6805 kw_regex = 0;
6806 i = j;
6807 } else if (isalpha((unsigned char)c) || c == '_' || c == '$') {
6808 int j = i + 1;
6809 while (j < n && (isalnum((unsigned char)s[j]) || s[j] == '_' || s[j] == '$')) j++;
6810 _syntax_run(font, _syntax_js_keyword(s + i, j - i) ? th->border_active : th->text_normal, &cx, y, s + i, j - i);
6811 last_sig = s[j - 1];
6812 kw_regex = _syntax_js_regex_kw(s + i, j - i);
6813 i = j;
6814 } else if (c == '{' || c == '}' || c == '[' || c == ']' || c == '(' || c == ')' || c == ';' || c == ',' || c == ':') {
6815 _syntax_run(font, th->border_hover, &cx, y, s + i, 1);
6816 last_sig = c;
6817 kw_regex = 0;
6818 i++;
6819 } else {
6820 _syntax_run(font, th->text_normal, &cx, y, s + i, 1);
6821 if (c != ' ' && c != '\t') {
6822 last_sig = c;
6823 kw_regex = 0;
6824 }
6825 i++;
6826 }
6827 }
6828 return;
6829 }
6830 _syntax_run(font, th->text_normal, &cx, y, s, n);
6831}
6832
6834static float _syntax_prefix_w(ALLEGRO_FONT* font, const char* s, int n) {
6835 char buf[4096];
6836 if (!font || !s || n <= 0) return 0.0f;
6837 if (n > (int)sizeof(buf) - 1) n = (int)sizeof(buf) - 1;
6838 memcpy(buf, s, (size_t)n);
6839 buf[n] = '\0';
6840 return _text_w(font, buf);
6841}
6842
6848static int _syntaxview_offset_from_mouse(const N_GUI_SYNTAXVIEW_DATA* yd, ALLEGRO_FONT* font, float mx, float my, float ax, float ay, float pad) {
6849 if (!yd || !yd->text || !font) return 0;
6850 float fh = (float)al_get_font_line_height(font);
6851 float row_h = fh + 2.0f;
6852 int row = (int)((my - ay - pad) / row_h);
6853 if (row < 0) row = 0;
6854 int target_line = yd->scroll_offset + row;
6855 const char* p = yd->text;
6856 int ln = 0;
6857 while (ln < target_line && *p) {
6858 const char* nl = strchr(p, '\n');
6859 if (!nl) {
6860 p += strlen(p);
6861 break;
6862 }
6863 p = nl + 1;
6864 ln++;
6865 }
6866 size_t line_start = (size_t)(p - yd->text);
6867 const char* nl = strchr(p, '\n');
6868 int dlen = nl ? (int)(nl - p) : (int)strlen(p);
6869 if (dlen > 0 && p[dlen - 1] == '\r') dlen--;
6870 float target = mx - (ax + pad);
6871 if (target <= 0.0f) return (int)line_start;
6872 float prevw = 0.0f;
6873 int col;
6874 for (col = 1; col <= dlen; col++) {
6875 float ww = _syntax_prefix_w(font, p, col);
6876 if (ww >= target)
6877 return (int)line_start + ((target - prevw < ww - target) ? (col - 1) : col);
6878 prevw = ww;
6879 }
6880 return (int)line_start + dlen;
6881}
6882
6886 if (!ctx || ctx->selected_syntaxview_id < 0) return;
6888 if (pv && pv->type == N_GUI_TYPE_SYNTAXVIEW && pv->data) {
6890 pyd->sel_start = -1;
6891 pyd->sel_end = -1;
6892 pyd->sel_dragging = 0;
6893 }
6894 ctx->selected_syntaxview_id = -1;
6895}
6896
6904 if (!yd || yd->lines_valid) return;
6905 int nb = 0;
6906 int headers_end = 0x7fffffff;
6907 const char* s = yd->text;
6908 if (s && s[0]) {
6909 /* line count: 1 + number of '\n' (matches _text_line_count) */
6910 nb = 1;
6911 for (const char* t = s; *t; t++)
6912 if (*t == '\n') nb++;
6913 /* first blank line: HTTP headers end there */
6914 const char* q = s;
6915 int ln = 0;
6916 while (*q) {
6917 const char* nl = strchr(q, '\n');
6918 int len = nl ? (int)(nl - q) : (int)strlen(q);
6919 if (len == 0 || (len == 1 && q[0] == '\r')) {
6920 headers_end = ln;
6921 break;
6922 }
6923 ln++;
6924 if (!nl) break;
6925 q = nl + 1;
6926 }
6927 }
6928 yd->cached_nb_lines = nb;
6929 yd->cached_headers_end = headers_end;
6930 yd->lines_valid = 1;
6931}
6932
6933static void _draw_syntaxview(N_GUI_WIDGET* wgt, float ox, float oy, ALLEGRO_FONT* default_font, N_GUI_STYLE* style) {
6935 float ax = ox + wgt->x;
6936 float ay = oy + wgt->y;
6937 ALLEGRO_FONT* font = wgt->font ? wgt->font : default_font;
6938 float fh = font ? (float)al_get_font_line_height(font) : 16.0f;
6939 float pad = style->textarea_padding;
6940 float row_h = fh + 2.0f;
6942 int nb_lines = yd->cached_nb_lines;
6943 int visible = (int)((wgt->h - pad * 2.0f) / row_h);
6944 int need_sb, max_off, headers_end, line_no, drawn, pcx, pcy, pcw, pch;
6945 int has_sel, sel_lo = 0, sel_hi = 0;
6946 float sb_w;
6947 const char* p;
6948
6949 al_draw_filled_rectangle(ax, ay, ax + wgt->w, ay + wgt->h, wgt->theme.bg_normal);
6950 al_draw_rectangle(ax, ay, ax + wgt->w, ay + wgt->h, wgt->theme.border_normal, _min_thickness(wgt->theme.border_thickness));
6951 if (visible < 1) visible = 1;
6952 need_sb = (nb_lines > visible) ? 1 : 0;
6953 sb_w = need_sb ? style->scrollbar_size : 0.0f;
6954 max_off = nb_lines - visible;
6955 if (max_off < 0) max_off = 0;
6956 if (yd->scroll_offset > max_off) yd->scroll_offset = max_off;
6957 if (yd->scroll_offset < 0) yd->scroll_offset = 0;
6958 if (!yd->text) return;
6959
6960 /* first blank line (HTTP headers end there), cached with the line count */
6961 headers_end = yd->cached_headers_end;
6962
6963 /* order the selection endpoints once (byte offsets into the text) */
6964 has_sel = (yd->sel_start >= 0 && yd->sel_end >= 0 && yd->sel_start != yd->sel_end);
6965 if (has_sel) {
6966 sel_lo = yd->sel_start < yd->sel_end ? yd->sel_start : yd->sel_end;
6967 sel_hi = yd->sel_start < yd->sel_end ? yd->sel_end : yd->sel_start;
6968 if ((size_t)sel_lo > yd->len) sel_lo = (int)yd->len;
6969 if ((size_t)sel_hi > yd->len) sel_hi = (int)yd->len;
6970 }
6971
6972 al_get_clipping_rectangle(&pcx, &pcy, &pcw, &pch);
6973 al_set_clipping_rectangle((int)ax, (int)ay, (int)(wgt->w - sb_w), (int)wgt->h);
6974 /* skip to the first visible line */
6975 p = yd->text;
6976 line_no = 0;
6977 while (line_no < yd->scroll_offset && p) {
6978 const char* nl = strchr(p, '\n');
6979 if (!nl) {
6980 p = NULL;
6981 break;
6982 }
6983 p = nl + 1;
6984 line_no++;
6985 }
6986 /* pass 1: selection highlights, drawn as primitives behind the text. Kept
6987 out of the held text pass below because drawing a primitive while bitmap
6988 drawing is held is undefined in Allegro. Only walks when a selection
6989 exists, so the common (no-selection) frame skips it entirely. */
6990 if (has_sel && p) {
6991 const char* sp = p;
6992 for (int sdrawn = 0; sp && *sp && sdrawn < visible; sdrawn++) {
6993 const char* nl = strchr(sp, '\n');
6994 int len = nl ? (int)(nl - sp) : (int)strlen(sp);
6995 float ty = ay + pad + (float)sdrawn * row_h;
6996 if (len > 0 && sp[len - 1] == '\r') len--;
6997 int ls = (int)(sp - yd->text);
6998 int le = ls + len;
6999 int a = sel_lo > ls ? sel_lo : ls;
7000 int b = sel_hi < le ? sel_hi : le;
7001 if (a < b) {
7002 float x0 = ax + pad + _syntax_prefix_w(font, sp, a - ls);
7003 float x1 = ax + pad + _syntax_prefix_w(font, sp, b - ls);
7004 al_draw_filled_rectangle(x0, ty, x1, ty + row_h, wgt->theme.selection_color);
7005 }
7006 if (!nl) break;
7007 sp = nl + 1;
7008 }
7009 }
7010 /* pass 2: the text runs, batched into one held block. Every visible line's
7011 coloured runs share the font glyph atlas, so holding collapses them into
7012 a single flush instead of one draw call per run. */
7013 al_hold_bitmap_drawing(true);
7014 for (drawn = 0; p && *p && drawn < visible; drawn++, line_no++) {
7015 const char* nl = strchr(p, '\n');
7016 int len = nl ? (int)(nl - p) : (int)strlen(p);
7017 int in_headers = (line_no <= headers_end);
7018 float ty = ay + pad + (float)drawn * row_h;
7019 if (len > 0 && p[len - 1] == '\r') len--;
7020 _syntax_draw_line(font, &wgt->theme, ax + pad, ty, p, len, yd->mode, line_no, in_headers);
7021 if (!nl) break;
7022 p = nl + 1;
7023 }
7024 al_hold_bitmap_drawing(false);
7025 al_set_clipping_rectangle(pcx, pcy, pcw, pch);
7026 if (need_sb)
7027 _draw_rows_scrollbar(ax, ay, wgt->w, wgt->h, nb_lines, visible, yd->scroll_offset, style, &wgt->theme);
7028}
7029
7031static void _draw_progressbar(N_GUI_WIDGET* wgt, float ox, float oy, ALLEGRO_FONT* default_font, N_GUI_STYLE* style) {
7032 const N_GUI_PROGRESSBAR_DATA* pd = (const N_GUI_PROGRESSBAR_DATA*)wgt->data;
7033 float ax = ox + wgt->x;
7034 float ay = oy + wgt->y;
7035 ALLEGRO_FONT* font = wgt->font ? wgt->font : default_font;
7036 float r = 3.0f;
7037 float fillw;
7038 (void)style;
7039 if (!pd)
7040 return;
7041 /* track */
7042 al_draw_filled_rounded_rectangle(ax, ay, ax + wgt->w, ay + wgt->h, r, r, wgt->theme.bg_normal);
7043 /* filled portion, proportional to value */
7044 fillw = pd->value * wgt->w;
7045 if (fillw > 0.0f) {
7046 if (fillw < 1.0f)
7047 fillw = 1.0f;
7048 al_draw_filled_rounded_rectangle(ax, ay, ax + fillw, ay + wgt->h, r, r, wgt->theme.bg_active);
7049 }
7050 /* border */
7051 al_draw_rounded_rectangle(ax, ay, ax + wgt->w, ay + wgt->h, r, r, wgt->theme.border_normal, 1.0f);
7052 /* optional centered overlay text */
7053 if (font && pd->text[0]) {
7054 float th = (float)al_get_font_line_height(font);
7055 al_draw_text(font, wgt->theme.text_normal, ax + wgt->w / 2.0f, ay + (wgt->h - th) / 2.0f, ALLEGRO_ALIGN_CENTER, pd->text);
7056 }
7057}
7058
7059static void _draw_datagrid(N_GUI_WIDGET* wgt, float ox, float oy, ALLEGRO_FONT* default_font, N_GUI_STYLE* style) {
7061 float ax = ox + wgt->x;
7062 float ay = oy + wgt->y;
7063 ALLEGRO_FONT* font = wgt->font ? wgt->font : default_font;
7064 float fh = font ? (float)al_get_font_line_height(font) : 16.0f;
7065 float pad = style->item_text_padding;
7066 float row_h = fh + style->item_height_pad;
7067 float header_h = row_h;
7068 float data_y = ay + header_h;
7069 float data_h = wgt->h - header_h;
7070 int visible = (int)(data_h / row_h);
7071 int nb_rows = (int)gd->nb_rows;
7072 int need_sb, max_off, i, pcx, pcy, pcw, pch, need_hsb = 0;
7073 float sb_w, sb_h = 0.0f, cx, content_w = 0.0f, pane_w = 0.0f, maxh;
7074 size_t c;
7075
7076 al_draw_filled_rectangle(ax, ay, ax + wgt->w, ay + wgt->h, wgt->theme.bg_normal);
7077 if (visible < 1) visible = 1;
7078 need_sb = (nb_rows > visible) ? 1 : 0;
7079 sb_w = need_sb ? style->scrollbar_size : 0.0f;
7080 /* horizontal scroll: when the columns are wider than the pane, reserve a scrollbar along
7081 the bottom (shrinking the data rows), and clamp the horizontal offset to the content */
7082 _datagrid_hmetrics(wgt, gd, font, style, &content_w, &pane_w, &need_hsb);
7083 if (need_hsb) {
7084 sb_h = style->scrollbar_size;
7085 data_h -= sb_h;
7086 visible = (int)(data_h / row_h);
7087 if (visible < 1) visible = 1;
7088 }
7089 maxh = content_w - pane_w;
7090 if (maxh < 0) maxh = 0;
7091 if (gd->h_scroll > maxh) gd->h_scroll = maxh;
7092 if (gd->h_scroll < 0) gd->h_scroll = 0;
7093 max_off = nb_rows - visible;
7094 if (max_off < 0) max_off = 0;
7095 if (gd->scroll_offset > max_off) gd->scroll_offset = max_off;
7096 if (gd->scroll_offset < 0) gd->scroll_offset = 0;
7097
7098 /* header */
7099 al_draw_filled_rectangle(ax, ay, ax + wgt->w, ay + header_h, wgt->theme.bg_active);
7100 al_get_clipping_rectangle(&pcx, &pcy, &pcw, &pch);
7101 al_set_clipping_rectangle((int)ax, (int)ay, (int)(wgt->w - sb_w), (int)header_h);
7102 cx = ax + pad - gd->h_scroll;
7103 al_hold_bitmap_drawing(true);
7104 for (c = 0; c < gd->nb_cols; c++) {
7105 size_t pc = _datagrid_phys(gd, c);
7106 char title[N_GUI_ID_MAX + 4];
7107 const char* ind = "";
7108 if (!gd->cols[pc].visible) continue; /* hidden column: no header cell, no gap */
7109 if ((int)pc == gd->sort_col) ind = gd->sort_dir < 0 ? " v" : " ^";
7110 snprintf(title, sizeof(title), "%s%s", gd->cols[pc].title, ind);
7111 _draw_text_truncated(font, wgt->theme.text_active, cx, ay + (header_h - fh) * 0.5f, gd->cols[pc].width - pad, title);
7112 cx += gd->cols[pc].width;
7113 }
7114 al_hold_bitmap_drawing(false);
7115 al_set_clipping_rectangle(pcx, pcy, pcw, pch);
7116
7117 /* data rows */
7118 al_get_clipping_rectangle(&pcx, &pcy, &pcw, &pch);
7119 al_set_clipping_rectangle((int)ax, (int)data_y, (int)(wgt->w - sb_w), (int)data_h);
7120 /* pass 1: row backgrounds (tint + selection). All primitives, drawn before
7121 the held text pass so no primitive is issued while bitmap drawing is held. */
7122 for (i = 0; i < visible; i++) {
7123 int row = gd->scroll_offset + i;
7124 float ry = data_y + (float)i * row_h;
7125 int sel, tinted;
7126 if (row >= nb_rows) break;
7127 sel = (row == gd->selected_row || (gd->multiselect && gd->row_sel && gd->row_sel[row])) ? 1 : 0;
7128 tinted = (gd->row_has_color && gd->row_color && gd->row_has_color[row]) ? 1 : 0;
7129 if (tinted) {
7130 ALLEGRO_COLOR t = gd->row_color[row];
7131 al_draw_filled_rectangle(ax, ry, ax + wgt->w - sb_w, ry + row_h, t);
7132 }
7133 if (sel) {
7134 if (tinted) {
7135 /* an opaque selection fill would hide the tint: darken the tinted
7136 row and outline it instead, so it reads as both */
7137 al_draw_filled_rectangle(ax, ry, ax + wgt->w - sb_w, ry + row_h, al_map_rgba_f(0.0f, 0.0f, 0.0f, 0.35f));
7138 al_draw_rectangle(ax + 1.0f, ry + 1.0f, ax + wgt->w - sb_w - 1.0f, ry + row_h - 1.0f, wgt->theme.border_active, 2.0f);
7139 } else {
7140 al_draw_filled_rectangle(ax, ry, ax + wgt->w - sb_w, ry + row_h, wgt->theme.bg_hover);
7141 }
7142 }
7143 }
7144 /* pass 2: cell text, batched into one held block (all cells share the font
7145 glyph atlas). The text colour is recomputed here from the same sel/tint
7146 state used in pass 1. */
7147 al_hold_bitmap_drawing(true);
7148 for (i = 0; i < visible; i++) {
7149 int row = gd->scroll_offset + i;
7150 float ry = data_y + (float)i * row_h;
7151 int sel, tinted;
7152 ALLEGRO_COLOR tint_text = wgt->theme.text_normal;
7153 if (row >= nb_rows) break;
7154 sel = (row == gd->selected_row || (gd->multiselect && gd->row_sel && gd->row_sel[row])) ? 1 : 0;
7155 tinted = (gd->row_has_color && gd->row_color && gd->row_has_color[row]) ? 1 : 0;
7156 if (tinted) {
7157 ALLEGRO_COLOR t = gd->row_color[row];
7158 /* pick the text color from the tint luminance so a light highlight
7159 (yellow, cyan) keeps dark text and a dark one keeps light text */
7160 float lum = 0.299f * t.r + 0.587f * t.g + 0.114f * t.b;
7161 tint_text = (lum > 0.55f) ? al_map_rgb(0, 0, 0) : al_map_rgb(255, 255, 255);
7162 if (sel) tint_text = al_map_rgb(255, 255, 255);
7163 }
7164 cx = ax + pad - gd->h_scroll;
7165 for (c = 0; c < gd->nb_cols; c++) {
7166 size_t pc = _datagrid_phys(gd, c);
7167 const char* cell = gd->cells[(size_t)row * gd->nb_cols + pc];
7168 ALLEGRO_COLOR col = tinted ? tint_text : (sel ? wgt->theme.text_active : wgt->theme.text_normal);
7169 if (!gd->cols[pc].visible) continue;
7170 _draw_text_truncated(font, col, cx, ry + (row_h - fh) * 0.5f, gd->cols[pc].width - pad, cell ? cell : "");
7171 cx += gd->cols[pc].width;
7172 }
7173 }
7174 al_hold_bitmap_drawing(false);
7175 al_set_clipping_rectangle(pcx, pcy, pcw, pch);
7176
7177 /* vertical separators between the visible columns (header top to grid bottom), so
7178 column boundaries read as a grid and the resizable edges are visible. A
7179 semi-transparent black overlay reads as a bit darker than whatever is behind it
7180 (the header cells or a row), so the line never blends into the header background
7181 the way a border-colored line does. */
7182 {
7183 ALLEGRO_COLOR sep = al_map_rgba_f(0.0f, 0.0f, 0.0f, 0.35f);
7184 float vx = ax - gd->h_scroll;
7185 size_t vc;
7186 for (vc = 0; vc < gd->nb_cols; vc++) {
7187 size_t pc = _datagrid_phys(gd, vc);
7188 if (!gd->cols[pc].visible) continue;
7189 vx += gd->cols[pc].width;
7190 if (vx >= ax + wgt->w - sb_w) break; /* past the right border / scrollbar */
7191 if (vx <= ax) continue; /* separator scrolled off the left */
7192 al_draw_line(vx, ay, vx, ay + wgt->h - sb_h, sep, 1.0f);
7193 }
7194 }
7195 al_draw_line(ax, ay + header_h, ax + wgt->w - sb_w, ay + header_h, wgt->theme.border_normal, 1.0f);
7196 al_draw_rectangle(ax, ay, ax + wgt->w, ay + wgt->h, wgt->theme.border_normal, _min_thickness(wgt->theme.border_thickness));
7197 if (need_sb)
7198 _draw_rows_scrollbar(ax, data_y, wgt->w, data_h, nb_rows, visible, gd->scroll_offset, style, &wgt->theme);
7199 if (need_hsb)
7200 _draw_cols_scrollbar(ax, ay + wgt->h - sb_h, pane_w, content_w, gd->h_scroll, style, &wgt->theme);
7201}
7202
7205static void _draw_splitpane(N_GUI_WIDGET* wgt, float ox, float oy) {
7206 const N_GUI_SPLITPANE_DATA* sd = (const N_GUI_SPLITPANE_DATA*)wgt->data;
7207 float ax = ox + wgt->x;
7208 float ay = oy + wgt->y;
7209 int hovered = (wgt->state & (N_GUI_STATE_HOVER | N_GUI_STATE_ACTIVE)) ? 1 : 0;
7210 ALLEGRO_COLOR bar = hovered ? wgt->theme.border_active : wgt->theme.border_normal;
7211 float th = sd->divider;
7212 if (sd->orientation == N_GUI_SPLIT_VERTICAL) {
7213 float dx = ax + sd->ratio * wgt->w - th * 0.5f;
7214 float gx = dx + th * 0.5f;
7215 al_draw_filled_rectangle(dx, ay, dx + th, ay + wgt->h, wgt->theme.bg_normal);
7216 al_draw_line(gx, ay + wgt->h * 0.5f - th * 1.5f, gx, ay + wgt->h * 0.5f + th * 1.5f, bar, 2.0f);
7217 } else {
7218 float dy = ay + sd->ratio * wgt->h - th * 0.5f;
7219 float gy = dy + th * 0.5f;
7220 al_draw_filled_rectangle(ax, dy, ax + wgt->w, dy + th, wgt->theme.bg_normal);
7221 al_draw_line(ax + wgt->w * 0.5f - th * 1.5f, gy, ax + wgt->w * 0.5f + th * 1.5f, gy, bar, 2.0f);
7222 }
7223}
7224
7226static void _draw_listbox(N_GUI_WIDGET* wgt, float ox, float oy, ALLEGRO_FONT* default_font, N_GUI_STYLE* style) {
7228 float ax = ox + wgt->x;
7229 float ay = oy + wgt->y;
7230 ALLEGRO_FONT* font = wgt->font ? wgt->font : default_font;
7231
7232 /* background */
7233 if (ld->bg_bitmap) {
7234 al_draw_scaled_bitmap(ld->bg_bitmap, 0, 0,
7235 (float)al_get_bitmap_width(ld->bg_bitmap), (float)al_get_bitmap_height(ld->bg_bitmap),
7236 ax, ay, wgt->w, wgt->h, 0);
7237 } else {
7238 al_draw_filled_rectangle(ax, ay, ax + wgt->w, ay + wgt->h, wgt->theme.bg_normal);
7239 }
7240 /* the frame is drawn LAST (see below), not here: when wgt->h is an exact multiple
7241 of the row height the last row's selection / hover fill reaches ay + wgt->h and
7242 would paint over the bottom border if the frame were drawn first. */
7243
7244 if (!font) {
7245 /* no font: nothing else draws, so frame now and return */
7246 al_draw_rectangle(ax, ay, ax + wgt->w, ay + wgt->h, wgt->theme.border_normal, wgt->theme.border_thickness);
7247 return;
7248 }
7249 float fh = (float)al_get_font_line_height(font);
7250 float ih = ld->item_height > fh ? ld->item_height : fh + style->item_height_pad;
7251 /* Canonicalise: a listbox created with a too-small item_height
7252 (or with the style default of 20 when the active font is
7253 taller) would otherwise draw rows at `ih` but have every hit-
7254 test / hover-row / DnD helper read the raw `ld->item_height`,
7255 producing off-by-N row mismatches. Write the effective value
7256 back so all consumers agree on one pixel height. */
7257 ld->item_height = ih;
7258 int visible_count = (int)(wgt->h / ih);
7259 float pad = style->item_text_padding;
7260
7261 /* clamp scroll_offset to valid range */
7262 int max_off = (int)ld->nb_items - visible_count;
7263 if (max_off < 0) max_off = 0;
7264 if (ld->scroll_offset > max_off) ld->scroll_offset = max_off;
7265 if (ld->scroll_offset < 0) ld->scroll_offset = 0;
7266
7267 int need_scrollbar = ((int)ld->nb_items > visible_count) ? 1 : 0;
7268 float sb_w = need_scrollbar ? style->scrollbar_size : 0;
7269 float item_area_w = wgt->w - sb_w;
7270
7271 /* pass 1: item backgrounds (selection / hover tint / skin bitmaps), all
7272 primitives and non-font bitmaps, drawn before the held text pass */
7273 for (int i = 0; i < visible_count && (size_t)(i + ld->scroll_offset) < ld->nb_items; i++) {
7274 int idx = i + ld->scroll_offset;
7275 float iy = ay + (float)i * ih;
7276 const N_GUI_LISTITEM* item = &ld->items[idx];
7277
7278 int row_hovered = (ld->hover_row == idx) ? 1 : 0;
7279 if (item->selected && ld->item_selected_bitmap) {
7280 al_draw_scaled_bitmap(ld->item_selected_bitmap, 0, 0,
7281 (float)al_get_bitmap_width(ld->item_selected_bitmap), (float)al_get_bitmap_height(ld->item_selected_bitmap),
7282 ax + style->item_selection_inset, iy, item_area_w - style->item_selection_inset * 2, ih, 0);
7283 } else if (item->selected) {
7284 al_draw_filled_rectangle(ax + style->item_selection_inset, iy, ax + item_area_w - style->item_selection_inset, iy + ih, wgt->theme.bg_active);
7285 } else if (row_hovered) {
7286 /* Hover tint on the row under the cursor (only when the
7287 row isn't selected, selection wins). */
7288 al_draw_filled_rectangle(ax + style->item_selection_inset, iy,
7289 ax + item_area_w - style->item_selection_inset, iy + ih,
7290 wgt->theme.bg_hover);
7291 } else if (ld->item_bg_bitmap) {
7292 al_draw_scaled_bitmap(ld->item_bg_bitmap, 0, 0,
7293 (float)al_get_bitmap_width(ld->item_bg_bitmap), (float)al_get_bitmap_height(ld->item_bg_bitmap),
7294 ax + style->item_selection_inset, iy, item_area_w - style->item_selection_inset * 2, ih, 0);
7295 }
7296 }
7297 /* pass 2: item labels, batched into one held block (shared font atlas) */
7298 {
7299 float item_max_w = item_area_w - pad * 2.0f;
7300 al_hold_bitmap_drawing(true);
7301 for (int i = 0; i < visible_count && (size_t)(i + ld->scroll_offset) < ld->nb_items; i++) {
7302 int idx = i + ld->scroll_offset;
7303 float iy = ay + (float)i * ih;
7304 const N_GUI_LISTITEM* item = &ld->items[idx];
7305 _draw_text_truncated(font, item->selected ? wgt->theme.text_active : wgt->theme.text_normal,
7306 ax + pad, iy + (ih - fh) / 2.0f, item_max_w, item->text);
7307 }
7308 al_hold_bitmap_drawing(false);
7309 }
7310
7311 /* draw scrollbar indicator when items overflow */
7312 if (need_scrollbar) {
7313 float sb_x = ax + wgt->w - sb_w;
7314 /* scrollbar track */
7315 al_draw_filled_rectangle(sb_x, ay, ax + wgt->w, ay + wgt->h, wgt->theme.bg_normal);
7316 al_draw_line(sb_x, ay, sb_x, ay + wgt->h, wgt->theme.border_normal, 1.0f);
7317 /* thumb */
7318 float ratio = (float)visible_count / (float)ld->nb_items;
7319 float thumb_h = ratio * wgt->h;
7320 if (thumb_h < style->scrollbar_thumb_min) thumb_h = style->scrollbar_thumb_min;
7321 float track_range = wgt->h - thumb_h;
7322 float pos_ratio = (max_off > 0) ? (float)ld->scroll_offset / (float)max_off : 0;
7323 float thumb_y = ay + pos_ratio * track_range;
7324 float thumb_pad = style->scrollbar_thumb_padding;
7325 al_draw_filled_rounded_rectangle(sb_x + thumb_pad, thumb_y, ax + wgt->w - thumb_pad, thumb_y + thumb_h,
7326 2.0f, 2.0f, wgt->theme.bg_hover);
7327 }
7328
7329 /* frame last, over the rows and scrollbar, so no row fill can eat an edge */
7330 al_draw_rectangle(ax, ay, ax + wgt->w, ay + wgt->h, wgt->theme.border_normal, wgt->theme.border_thickness);
7331}
7332
7334static void _draw_radiolist(N_GUI_WIDGET* wgt, float ox, float oy, ALLEGRO_FONT* default_font, const N_GUI_STYLE* style) {
7336 float ax = ox + wgt->x;
7337 float ay = oy + wgt->y;
7338 ALLEGRO_FONT* font = wgt->font ? wgt->font : default_font;
7339
7340 int hovered = (wgt->state & N_GUI_STATE_HOVER) ? 1 : 0;
7341
7342 /* background, honour hover so the radiolist behaves like the
7343 other interactive widgets even though per-row hover is not
7344 tracked separately (no consumer today). */
7345 if (rd->bg_bitmap) {
7346 al_draw_scaled_bitmap(rd->bg_bitmap, 0, 0,
7347 (float)al_get_bitmap_width(rd->bg_bitmap), (float)al_get_bitmap_height(rd->bg_bitmap),
7348 ax, ay, wgt->w, wgt->h, 0);
7349 } else {
7350 ALLEGRO_COLOR bg = hovered ? wgt->theme.bg_hover : wgt->theme.bg_normal;
7351 al_draw_filled_rectangle(ax, ay, ax + wgt->w, ay + wgt->h, bg);
7352 }
7353 ALLEGRO_COLOR bd = hovered ? wgt->theme.border_hover : wgt->theme.border_normal;
7354 al_draw_rectangle(ax, ay, ax + wgt->w, ay + wgt->h, bd, wgt->theme.border_thickness);
7355
7356 if (!font) return;
7357 float fh = (float)al_get_font_line_height(font);
7358 float ih = rd->item_height > fh ? rd->item_height : fh + style->item_height_pad;
7359 int visible_count = (int)(wgt->h / ih);
7360 float radio_r = (ih - style->item_height_pad * 2) / 2.0f;
7361 if (radio_r < style->radio_circle_min_r) radio_r = style->radio_circle_min_r;
7362 float pad = style->radio_label_gap;
7363
7364 /* clamp scroll_offset to valid range */
7365 int max_off = (int)rd->nb_items - visible_count;
7366 if (max_off < 0) max_off = 0;
7367 if (rd->scroll_offset > max_off) rd->scroll_offset = max_off;
7368 if (rd->scroll_offset < 0) rd->scroll_offset = 0;
7369
7370 int need_scrollbar = ((int)rd->nb_items > visible_count) ? 1 : 0;
7371
7372 /* pass 1: item backgrounds and radio circles (skin bitmaps + primitives),
7373 drawn before the held label pass so no primitive runs inside a hold */
7374 for (int i = 0; i < visible_count && (size_t)(i + rd->scroll_offset) < rd->nb_items; i++) {
7375 int idx = i + rd->scroll_offset;
7376 float iy = ay + (float)i * ih;
7377 float cy = iy + ih / 2.0f;
7378 float cx = ax + pad + radio_r;
7379
7380 /* item background bitmap */
7381 if (idx == rd->selected_index && rd->item_selected_bitmap) {
7382 al_draw_scaled_bitmap(rd->item_selected_bitmap, 0, 0,
7383 (float)al_get_bitmap_width(rd->item_selected_bitmap), (float)al_get_bitmap_height(rd->item_selected_bitmap),
7384 ax, iy, wgt->w, ih, 0);
7385 } else if (rd->item_bg_bitmap) {
7386 al_draw_scaled_bitmap(rd->item_bg_bitmap, 0, 0,
7387 (float)al_get_bitmap_width(rd->item_bg_bitmap), (float)al_get_bitmap_height(rd->item_bg_bitmap),
7388 ax, iy, wgt->w, ih, 0);
7389 }
7390
7391 /* outer circle */
7392 al_draw_circle(cx, cy, radio_r, wgt->theme.border_normal, _min_thickness(style->radio_circle_border_thickness));
7393
7394 /* inner filled circle if selected */
7395 if (idx == rd->selected_index) {
7396 al_draw_filled_circle(cx, cy, radio_r - style->radio_inner_offset, wgt->theme.text_active);
7397 }
7398 }
7399 /* pass 2: labels, batched into one held block (shared font atlas) */
7400 al_hold_bitmap_drawing(true);
7401 for (int i = 0; i < visible_count && (size_t)(i + rd->scroll_offset) < rd->nb_items; i++) {
7402 int idx = i + rd->scroll_offset;
7403 float iy = ay + (float)i * ih;
7404 float cx = ax + pad + radio_r;
7405 al_draw_text(font, wgt->theme.text_normal,
7406 cx + radio_r + pad, iy + (ih - fh) / 2.0f, 0, rd->items[idx].text);
7407 }
7408 al_hold_bitmap_drawing(false);
7409
7410 /* draw scrollbar indicator when items overflow */
7411 if (need_scrollbar) {
7412 float sb_w = style->scrollbar_size;
7413 float sb_x = ax + wgt->w - sb_w;
7414 al_draw_filled_rectangle(sb_x, ay, ax + wgt->w, ay + wgt->h, wgt->theme.bg_normal);
7415 al_draw_line(sb_x, ay, sb_x, ay + wgt->h, wgt->theme.border_normal, 1.0f);
7416 float ratio = (float)visible_count / (float)rd->nb_items;
7417 float thumb_h = ratio * wgt->h;
7418 if (thumb_h < style->scrollbar_thumb_min) thumb_h = style->scrollbar_thumb_min;
7419 float track_range = wgt->h - thumb_h;
7420 float pos_ratio = (max_off > 0) ? (float)rd->scroll_offset / (float)max_off : 0;
7421 float thumb_y = ay + pos_ratio * track_range;
7422 float thumb_pad = style->scrollbar_thumb_padding;
7423 al_draw_filled_rounded_rectangle(sb_x + thumb_pad, thumb_y, ax + wgt->w - thumb_pad, thumb_y + thumb_h,
7424 2.0f, 2.0f, wgt->theme.bg_hover);
7425 }
7426}
7427
7429static void _draw_combobox(N_GUI_WIDGET* wgt, float ox, float oy, ALLEGRO_FONT* default_font, const N_GUI_STYLE* style) {
7431 float ax = ox + wgt->x;
7432 float ay = oy + wgt->y;
7433 ALLEGRO_FONT* font = wgt->font ? wgt->font : default_font;
7434
7435 if (cd->bg_bitmap) {
7436 al_draw_scaled_bitmap(cd->bg_bitmap, 0, 0,
7437 (float)al_get_bitmap_width(cd->bg_bitmap), (float)al_get_bitmap_height(cd->bg_bitmap),
7438 ax, ay, wgt->w, wgt->h, 0);
7439 } else {
7440 int rounded = _shape_rounded(style, N_GUI_SHAPE_RECT);
7441 _draw_themed_rect(&wgt->theme, wgt->state, ax, ay, wgt->w, wgt->h, rounded);
7442 }
7443
7444 if (!font) return;
7445 float pad = style->item_text_padding;
7446
7447 /* display selected item text (truncated to fit) */
7448 const char* display_text = "";
7449 if (cd->selected_index >= 0 && (size_t)cd->selected_index < cd->nb_items) {
7450 display_text = cd->items[cd->selected_index].text;
7451 }
7452 int cbbbx = 0, cbbby = 0, cbbbw = 0, cbbbh = 0;
7453 _text_dims(font, display_text[0] ? display_text : "Ay", &cbbbx, &cbbby, &cbbbw, &cbbbh);
7454 float fh = (float)cbbbh;
7455 float max_text_w = wgt->w - pad - style->dropdown_arrow_reserve;
7457 ax + pad, ay + (wgt->h - fh) / 2.0f - (float)cbbby, max_text_w, display_text);
7458
7459 /* dropdown arrow */
7460 float arrow_x = ax + wgt->w - style->dropdown_arrow_reserve;
7461 float arrow_y = ay + wgt->h / 2.0f;
7462 ALLEGRO_COLOR ac = wgt->theme.text_normal;
7463 al_draw_line(arrow_x, arrow_y - style->dropdown_arrow_half_h, arrow_x + style->dropdown_arrow_half_w, arrow_y + style->dropdown_arrow_half_h, ac, _min_thickness(style->dropdown_arrow_thickness));
7464 al_draw_line(arrow_x + style->dropdown_arrow_half_w, arrow_y + style->dropdown_arrow_half_h, arrow_x + style->dropdown_arrow_half_w * 2, arrow_y - style->dropdown_arrow_half_h, ac, _min_thickness(style->dropdown_arrow_thickness));
7465}
7466
7468/* Compute the effective top-Y of an open dropdown / dropmenu panel.
7469 * widget_top = oy + wgt->y (absolute Y of the widget's top edge)
7470 * widget_bottom = oy + wgt->y + wgt->h (absolute Y of the widget's bottom edge)
7471 * panel_h = intended panel height
7472 * expand_up = non-zero if caller requests upward expansion (flag set)
7473 *
7474 * When expand_up is set, open upward if there is room above; fall back to
7475 * downward if not. When expand_up is clear, open downward; auto-flip upward
7476 * only if the panel doesn't fit below and does fit above. If neither fits,
7477 * whichever side has more free space wins. Keeps draw + click paths in sync. */
7478static float _dropdown_panel_y(const N_GUI_CTX* ctx, float widget_top, float widget_bottom, float panel_h, int expand_up) {
7479 float ay_down = widget_bottom;
7480 float ay_up = widget_top - panel_h;
7481 int fits_down = (ctx->display_h <= 0) || (ay_down + panel_h <= (float)ctx->display_h);
7482 int fits_up = (ay_up >= 0.0f);
7483 if (expand_up) {
7484 return fits_up ? ay_up : (fits_down ? ay_down : ay_up);
7485 }
7486 if (!fits_down && fits_up) return ay_up;
7487 if (!fits_down && !fits_up) {
7488 /* neither fits, pick the side with more room */
7489 float space_down = (ctx->display_h > 0) ? ((float)ctx->display_h - widget_bottom) : panel_h;
7490 float space_up = widget_top;
7491 return (space_up > space_down) ? ay_up : ay_down;
7492 }
7493 return ay_down;
7494}
7495
7497 if (ctx->open_combobox_id < 0) return;
7499 if (!wgt || !wgt->data) return;
7501 if (!cd->is_open || cd->nb_items == 0) return;
7502
7503 /* find the window containing this widget to get absolute position */
7504 float ox = 0, oy = 0;
7505 const N_GUI_WINDOW* owner = _find_widget_window(ctx, wgt->id, &ox, &oy);
7506 if (!owner) return;
7507 /* the overlay belongs to its window's display, not to every pass */
7508 if (!_win_on_pass(ctx, owner)) return;
7509
7510 ALLEGRO_FONT* font = wgt->font ? wgt->font : ctx->default_font;
7511 if (!font) return;
7512
7513 float ax = ox + wgt->x;
7514 float fh = (float)al_get_font_line_height(font);
7515 float ih = cd->item_height > fh ? cd->item_height : fh + ctx->style.item_height_pad;
7516 int vis = (int)cd->nb_items;
7517 if (vis > cd->max_visible) vis = cd->max_visible;
7518 float dropdown_h = (float)vis * ih;
7519 float ay = _dropdown_panel_y(ctx, oy + wgt->y, oy + wgt->y + wgt->h, dropdown_h,
7521 float pad = ctx->style.item_text_padding;
7522
7523 /* dropdown width, auto-expand if flag is set */
7524 float dropdown_w = wgt->w;
7525 if (cd->flags & N_GUI_COMBOBOX_AUTO_WIDTH) {
7526 float max_item_w = 0;
7527 for (size_t i = 0; i < cd->nb_items; i++) {
7528 float tw = _text_w(font, cd->items[i].text);
7529 if (tw > max_item_w) max_item_w = tw;
7530 }
7531 float needed = max_item_w + pad * 2;
7532 if (needed > dropdown_w) dropdown_w = needed;
7533 /* apply cap */
7534 float cap = ctx->style.combobox_max_dropdown_width;
7535 if (cap <= 0) cap = ctx->display_w > 0 ? (float)ctx->display_w : 4096.0f;
7536 if (dropdown_w > cap) dropdown_w = cap;
7537 /* clamp to display right edge */
7538 if (ctx->display_w > 0 && ax + dropdown_w > (float)ctx->display_w) {
7539 dropdown_w = (float)ctx->display_w - ax;
7540 if (dropdown_w < wgt->w) dropdown_w = wgt->w;
7541 }
7542 }
7543
7544 /* scrollbar geometry, only shown when items exceed max_visible */
7545 int need_scrollbar = ((int)cd->nb_items > cd->max_visible);
7546 float sb_size = ctx->style.scrollbar_size;
7547 if (sb_size < 10.0f) sb_size = 10.0f;
7548 float item_area_w = need_scrollbar ? dropdown_w - sb_size : dropdown_w;
7549
7550 /* dropdown background */
7551 if (cd->bg_bitmap) {
7552 al_draw_scaled_bitmap(cd->bg_bitmap, 0, 0,
7553 (float)al_get_bitmap_width(cd->bg_bitmap), (float)al_get_bitmap_height(cd->bg_bitmap),
7554 ax, ay, dropdown_w, dropdown_h, 0);
7555 } else {
7556 al_draw_filled_rectangle(ax, ay, ax + dropdown_w, ay + dropdown_h, wgt->theme.bg_normal);
7557 }
7558 /* the border is drawn LAST (after the items and the scrollbar), not here: a
7559 selection / hover fill on the last visible item reaches ay + dropdown_h, and
7560 drawing the frame first let that fill paint over the bottom edge so the list
7561 looked unbordered. Same class of fix as the datagrid, which already frames last. */
7562
7563 /* clip item drawing to the dropdown area */
7564 int prev_cx = 0, prev_cy = 0, prev_cw = 0, prev_ch = 0;
7565 al_get_clipping_rectangle(&prev_cx, &prev_cy, &prev_cw, &prev_ch);
7566 al_set_clipping_rectangle((int)ax, (int)ay, (int)dropdown_w, (int)dropdown_h);
7567
7568 /* items: pass 1 draws the selection / hover backgrounds (primitives + skin
7569 bitmaps), pass 2 draws the labels inside one held block */
7570 float max_text_w = item_area_w - pad * 2;
7571 for (int i = 0; i < vis && (size_t)(i + cd->scroll_offset) < cd->nb_items; i++) {
7572 int idx = i + cd->scroll_offset;
7573 float iy = ay + (float)i * ih;
7574 /* the navigation highlight (moved by hover, wheel, and Up/Down) */
7575 int hovered = (idx == cd->highlight_index);
7576
7577 if (idx == cd->selected_index) {
7578 if (cd->item_selected_bitmap) {
7579 al_draw_scaled_bitmap(cd->item_selected_bitmap, 0, 0,
7580 (float)al_get_bitmap_width(cd->item_selected_bitmap), (float)al_get_bitmap_height(cd->item_selected_bitmap),
7581 ax + ctx->style.item_selection_inset, iy, item_area_w - ctx->style.item_selection_inset * 2, ih, 0);
7582 } else {
7583 al_draw_filled_rectangle(ax + ctx->style.item_selection_inset, iy, ax + item_area_w - ctx->style.item_selection_inset, iy + ih, wgt->theme.bg_active);
7584 }
7585 } else if (hovered) {
7586 if (cd->item_bg_bitmap) {
7587 al_draw_scaled_bitmap(cd->item_bg_bitmap, 0, 0,
7588 (float)al_get_bitmap_width(cd->item_bg_bitmap), (float)al_get_bitmap_height(cd->item_bg_bitmap),
7589 ax + ctx->style.item_selection_inset, iy, item_area_w - ctx->style.item_selection_inset * 2, ih, 0);
7590 } else {
7591 al_draw_filled_rectangle(ax + ctx->style.item_selection_inset, iy, ax + item_area_w - ctx->style.item_selection_inset, iy + ih, wgt->theme.bg_hover);
7592 }
7593 }
7594 }
7595 al_hold_bitmap_drawing(true);
7596 for (int i = 0; i < vis && (size_t)(i + cd->scroll_offset) < cd->nb_items; i++) {
7597 int idx = i + cd->scroll_offset;
7598 float iy = ay + (float)i * ih;
7599 ALLEGRO_COLOR tc = (idx == cd->selected_index) ? wgt->theme.text_active : wgt->theme.text_normal;
7600 _draw_text_truncated(font, tc, ax + pad, iy + (ih - fh) / 2.0f, max_text_w, cd->items[idx].text);
7601 }
7602 al_hold_bitmap_drawing(false);
7603
7604 al_set_clipping_rectangle(prev_cx, prev_cy, prev_cw, prev_ch);
7605
7606 /* scrollbar */
7607 if (need_scrollbar) {
7608 float sb_x = ax + dropdown_w - sb_size;
7609 /* track */
7610 al_draw_filled_rectangle(sb_x, ay, sb_x + sb_size, ay + dropdown_h,
7612 /* thumb, proportional to visible / total, positioned by scroll_offset */
7613 float ratio = (float)cd->max_visible / (float)cd->nb_items;
7614 if (ratio > 1.0f) ratio = 1.0f;
7615 float thumb_h = ratio * dropdown_h;
7616 float thumb_min = ctx->style.scrollbar_thumb_min;
7617 if (thumb_min < 12.0f) thumb_min = 12.0f;
7618 if (thumb_h < thumb_min) thumb_h = thumb_min;
7619 int max_offset = (int)cd->nb_items - cd->max_visible;
7620 float pos_ratio = (max_offset > 0) ? (float)cd->scroll_offset / (float)max_offset : 0.0f;
7621 float thumb_y = ay + pos_ratio * (dropdown_h - thumb_h);
7622 float thumb_pad = ctx->style.scrollbar_thumb_padding;
7623 float corner_r = ctx->style.scrollbar_thumb_corner_r;
7624 al_draw_filled_rounded_rectangle(sb_x + thumb_pad, thumb_y,
7625 sb_x + sb_size - thumb_pad,
7626 thumb_y + thumb_h,
7627 corner_r, corner_r,
7629 }
7630
7631 /* frame last, over the items and scrollbar, so no row fill can eat an edge */
7632 al_draw_rectangle(ax, ay, ax + dropdown_w, ay + dropdown_h, wgt->theme.border_active, _min_thickness(ctx->style.dropdown_border_thickness));
7633}
7634
7636static void _draw_dropmenu(N_GUI_WIDGET* wgt, float ox, float oy, ALLEGRO_FONT* default_font, const N_GUI_STYLE* style) {
7638 float ax = ox + wgt->x;
7639 float ay = oy + wgt->y;
7640 ALLEGRO_FONT* font = wgt->font ? wgt->font : default_font;
7641
7642 int draw_state = wgt->state;
7643 if (dd->is_open) draw_state |= N_GUI_STATE_ACTIVE;
7644
7645 _draw_themed_rect(&wgt->theme, draw_state, ax, ay, wgt->w, wgt->h, style->shape_mode != N_GUI_SHAPE_RECT);
7646 /* dropmenu panel and button default to rounded (shape_mode != RECT) */
7647
7648 if (font && dd->label[0]) {
7649 ALLEGRO_COLOR tc = _text_for_state(&wgt->theme, draw_state);
7650 int dbbx = 0, dbby = 0, dbbw = 0, dbbh = 0;
7651 _text_dims(font, dd->label, &dbbx, &dbby, &dbbw, &dbbh);
7652 float fh = (float)dbbh;
7653 float pad = style->item_text_padding;
7654 float max_text_w = wgt->w - pad - style->dropdown_arrow_reserve;
7655 _draw_text_truncated(font, tc, ax + pad, ay + (wgt->h - fh) / 2.0f - (float)dbby, max_text_w, dd->label);
7656 }
7657
7658 /* dropdown arrow: a down-chevron when closed, flipped to an up-chevron while
7659 the panel is open so the button shows its open/closed state at a glance */
7660 float arrow_x = ax + wgt->w - style->dropdown_arrow_reserve;
7661 float arrow_y = ay + wgt->h / 2.0f;
7662 ALLEGRO_COLOR ac = wgt->theme.text_normal;
7663 float tip = dd->is_open ? -style->dropdown_arrow_half_h : style->dropdown_arrow_half_h;
7664 al_draw_line(arrow_x, arrow_y - tip, arrow_x + style->dropdown_arrow_half_w, arrow_y + tip, ac, _min_thickness(style->dropdown_arrow_thickness));
7665 al_draw_line(arrow_x + style->dropdown_arrow_half_w, arrow_y + tip, arrow_x + style->dropdown_arrow_half_w * 2, arrow_y - tip, ac, _min_thickness(style->dropdown_arrow_thickness));
7666}
7667
7670 if (ctx->open_dropmenu_id < 0) return;
7672 if (!wgt || !wgt->data) return;
7674 if (!dd->is_open || dd->nb_entries == 0) return;
7675
7676 /* find the window containing this widget to get absolute position */
7677 float ox = 0, oy = 0;
7678 const N_GUI_WINDOW* owner = _find_widget_window(ctx, wgt->id, &ox, &oy);
7679 if (!owner) return;
7680 /* the overlay belongs to its window's display, not to every pass */
7681 if (!_win_on_pass(ctx, owner)) return;
7682
7683 ALLEGRO_FONT* font = wgt->font ? wgt->font : ctx->default_font;
7684 if (!font) return;
7685
7686 float ax = ox + wgt->x;
7687 float fh = (float)al_get_font_line_height(font);
7688 float ih = dd->item_height > fh ? dd->item_height : fh + ctx->style.item_height_pad;
7689 int vis = (int)dd->nb_entries;
7690 if (vis > dd->max_visible) vis = dd->max_visible;
7691 float panel_h = (float)vis * ih;
7692 float ay = _dropdown_panel_y(ctx, oy + wgt->y, oy + wgt->y + wgt->h, panel_h,
7694 float pad = ctx->style.item_text_padding;
7695 /* the panel widens beyond the (often narrow) button so entries are not clipped */
7696 float panel_w = n_gui_dropmenu_panel_width(ctx, wgt->id);
7697
7698 /* scrollbar geometry, only shown when entries exceed max_visible */
7699 int need_scrollbar = ((int)dd->nb_entries > dd->max_visible);
7700 float sb_size = ctx->style.scrollbar_size;
7701 if (sb_size < 10.0f) sb_size = 10.0f;
7702 float item_area_w = need_scrollbar ? panel_w - sb_size : panel_w;
7703
7704 /* panel background (rounded to match the button unless the GUI is square) */
7705 int panel_rounded = (ctx->style.shape_mode != N_GUI_SHAPE_RECT);
7706 float prx = panel_rounded ? wgt->theme.corner_rx : 0.0f;
7707 float pry = panel_rounded ? wgt->theme.corner_ry : 0.0f;
7708 if (dd->panel_bitmap) {
7709 al_draw_scaled_bitmap(dd->panel_bitmap, 0, 0,
7710 (float)al_get_bitmap_width(dd->panel_bitmap), (float)al_get_bitmap_height(dd->panel_bitmap),
7711 ax, ay, panel_w, panel_h, 0);
7712 } else if (panel_rounded) {
7713 al_draw_filled_rounded_rectangle(ax, ay, ax + panel_w, ay + panel_h, prx, pry, wgt->theme.bg_normal);
7714 } else {
7715 al_draw_filled_rectangle(ax, ay, ax + panel_w, ay + panel_h, wgt->theme.bg_normal);
7716 }
7717 /* the border is drawn LAST (after the entries and the scrollbar), not here: a
7718 hover fill on the last visible entry reaches ay + panel_h, and drawing the frame
7719 first let that fill paint over the bottom edge so the panel looked unbordered. */
7720
7721 /* entries: pass 1 draws the hover backgrounds (primitives / skin bitmaps),
7722 pass 2 draws the labels inside one held block */
7723 float max_text_w = item_area_w - pad * 2.0f;
7724 for (int i = 0; i < vis && (size_t)(i + dd->scroll_offset) < dd->nb_entries; i++) {
7725 int idx = i + dd->scroll_offset;
7726 float iy = ay + (float)i * ih;
7727 /* the navigation highlight (moved by hover, wheel, and Up/Down) */
7728 int hovered = (idx == dd->highlight_index);
7729
7730 if (hovered) {
7731 if (dd->item_hover_bitmap) {
7732 al_draw_scaled_bitmap(dd->item_hover_bitmap, 0, 0,
7733 (float)al_get_bitmap_width(dd->item_hover_bitmap), (float)al_get_bitmap_height(dd->item_hover_bitmap),
7734 ax + ctx->style.item_selection_inset, iy, item_area_w - ctx->style.item_selection_inset * 2, ih, 0);
7735 } else {
7736 al_draw_filled_rectangle(ax + ctx->style.item_selection_inset, iy, ax + item_area_w - ctx->style.item_selection_inset, iy + ih, wgt->theme.bg_hover);
7737 }
7738 }
7739 }
7740 al_hold_bitmap_drawing(true);
7741 for (int i = 0; i < vis && (size_t)(i + dd->scroll_offset) < dd->nb_entries; i++) {
7742 int idx = i + dd->scroll_offset;
7743 float iy = ay + (float)i * ih;
7744 int hovered = (idx == dd->highlight_index);
7745 ALLEGRO_COLOR tc = hovered ? wgt->theme.text_hover : wgt->theme.text_normal;
7746 _draw_text_truncated(font, tc, ax + pad, iy + (ih - fh) / 2.0f, max_text_w, dd->entries[idx].text);
7747 }
7748 al_hold_bitmap_drawing(false);
7749
7750 /* scrollbar */
7751 if (need_scrollbar) {
7752 float sb_x = ax + panel_w - sb_size;
7753 /* track */
7754 al_draw_filled_rectangle(sb_x, ay, sb_x + sb_size, ay + panel_h,
7756 /* thumb, proportional to visible / total, positioned by scroll_offset */
7757 float ratio = (float)dd->max_visible / (float)dd->nb_entries;
7758 if (ratio > 1.0f) ratio = 1.0f;
7759 float thumb_h = ratio * panel_h;
7760 float thumb_min = ctx->style.scrollbar_thumb_min;
7761 if (thumb_min < 12.0f) thumb_min = 12.0f;
7762 if (thumb_h < thumb_min) thumb_h = thumb_min;
7763 int max_offset = (int)dd->nb_entries - dd->max_visible;
7764 float pos_ratio = (max_offset > 0) ? (float)dd->scroll_offset / (float)max_offset : 0.0f;
7765 float thumb_y = ay + pos_ratio * (panel_h - thumb_h);
7766 float thumb_pad = ctx->style.scrollbar_thumb_padding;
7767 float corner_r = ctx->style.scrollbar_thumb_corner_r;
7768 al_draw_filled_rounded_rectangle(sb_x + thumb_pad, thumb_y,
7769 sb_x + sb_size - thumb_pad,
7770 thumb_y + thumb_h,
7771 corner_r, corner_r,
7773 }
7774
7775 /* frame last, over the entries and scrollbar, so no hover fill can eat an edge */
7776 if (panel_rounded)
7777 al_draw_rounded_rectangle(ax, ay, ax + panel_w, ay + panel_h, prx, pry, wgt->theme.border_active, _min_thickness(ctx->style.dropdown_border_thickness));
7778 else
7779 al_draw_rectangle(ax, ay, ax + panel_w, ay + panel_h, wgt->theme.border_active, _min_thickness(ctx->style.dropdown_border_thickness));
7780}
7781
7783static void _draw_image(N_GUI_WIDGET* wgt, float ox, float oy) {
7785 float ax = ox + wgt->x;
7786 float ay = oy + wgt->y;
7787
7788 /* border */
7789 al_draw_rectangle(ax, ay, ax + wgt->w, ay + wgt->h, wgt->theme.border_normal, wgt->theme.border_thickness);
7790
7791 if (!id->bitmap) return;
7792 float bw = (float)al_get_bitmap_width(id->bitmap);
7793 float bh = (float)al_get_bitmap_height(id->bitmap);
7794
7795 if (id->scale_mode == N_GUI_IMAGE_STRETCH) {
7796 al_draw_scaled_bitmap(id->bitmap, 0, 0, bw, bh, ax, ay, wgt->w, wgt->h, 0);
7797 } else if (id->scale_mode == N_GUI_IMAGE_CENTER) {
7798 float dx = ax + (wgt->w - bw) / 2.0f;
7799 float dy = ay + (wgt->h - bh) / 2.0f;
7800 al_draw_bitmap(id->bitmap, dx, dy, 0);
7801 } else {
7802 /* N_GUI_IMAGE_FIT */
7803 float scale_x = wgt->w / bw;
7804 float scale_y = wgt->h / bh;
7805 float scale = (scale_x < scale_y) ? scale_x : scale_y;
7806 float dw = bw * scale;
7807 float dh = bh * scale;
7808 float dx = ax + (wgt->w - dw) / 2.0f;
7809 float dy = ay + (wgt->h - dh) / 2.0f;
7810 al_draw_scaled_bitmap(id->bitmap, 0, 0, bw, bh, dx, dy, dw, dh, 0);
7811 }
7812}
7813
7819static int _label_char_at_x(const N_GUI_LABEL_DATA* lb, ALLEGRO_FONT* font, float click_x) {
7820 if (!font || !lb->text[0]) return -1;
7821 if (click_x <= 0) return 0;
7822 size_t len = strlen(lb->text);
7823 char tmp[N_GUI_TEXT_MAX];
7824 int best = 0;
7825 float best_dist = click_x;
7826 for (size_t i = 0; i < len;) {
7827 int clen = _utf8_char_len((unsigned char)lb->text[i]);
7828 if (i + (size_t)clen > len) clen = (int)(len - i);
7829 size_t pos = i + (size_t)clen;
7830 memcpy(tmp, lb->text, pos);
7831 tmp[pos] = '\0';
7832 float tw = _text_w(font, tmp);
7833 float dist = click_x - tw;
7834 if (dist < 0) dist = -dist;
7835 if (dist < best_dist) {
7836 best_dist = dist;
7837 best = (int)pos;
7838 }
7839 if (tw > click_x) break;
7840 i += (size_t)clen;
7841 }
7842 return best;
7843}
7844
7848static float _label_text_origin_x(const N_GUI_LABEL_DATA* lb, ALLEGRO_FONT* font, float ax, float wgt_w, float win_w, float wgt_x, float label_padding) {
7849 float effective_w = wgt_w;
7850 if (win_w > 0) {
7851 float max_from_win = win_w - wgt_x;
7852 if (max_from_win > effective_w) effective_w = max_from_win;
7853 }
7854 int lbbx = 0, lbby = 0, lbbw = 0, lbbh = 0;
7855 _text_dims(font, lb->text, &lbbx, &lbby, &lbbw, &lbbh);
7856 float tw = (float)lbbw;
7857 float max_text_w = effective_w - label_padding * 2;
7858 if (lb->align == N_GUI_ALIGN_CENTER) {
7859 if (tw > max_text_w) return ax + label_padding;
7860 return ax + (effective_w - tw) / 2.0f - (float)lbbx;
7861 }
7862 if (lb->align == N_GUI_ALIGN_RIGHT) {
7863 if (tw > max_text_w) return ax + label_padding;
7864 return ax + effective_w - tw - label_padding - (float)lbbx;
7865 }
7866 /* LEFT or JUSTIFIED (justified single-line fallback) */
7867 return ax + label_padding;
7868}
7869
7870static void _draw_label(N_GUI_WIDGET* wgt, float ox, float oy, ALLEGRO_FONT* default_font, float win_w, N_GUI_STYLE* style) {
7872 float ax = ox + wgt->x;
7873 float ay = oy + wgt->y;
7874 ALLEGRO_FONT* font = wgt->font ? wgt->font : default_font;
7875
7876 /* optional background bitmap */
7877 if (lb->bg_bitmap) {
7878 al_draw_scaled_bitmap(lb->bg_bitmap, 0, 0,
7879 (float)al_get_bitmap_width(lb->bg_bitmap), (float)al_get_bitmap_height(lb->bg_bitmap),
7880 ax, ay, wgt->w, wgt->h, 0);
7881 }
7882
7883 if (!font || !lb->text[0]) return;
7884
7885 int lbbbx = 0, lbbby = 0, lbbbw = 0, lbbbh = 0;
7886 _text_dims(font, lb->text, &lbbbx, &lbbby, &lbbbw, &lbbbh);
7887 float fh = (float)lbbbh;
7888
7889 /* effective widget width: expand into available window space when resizable */
7890 float effective_w = wgt->w;
7891 if (win_w > 0) {
7892 float max_from_win = win_w - wgt->x;
7893 if (max_from_win > effective_w) effective_w = max_from_win;
7894 }
7895
7896 int is_link = (lb->link[0] != '\0') ? 1 : 0;
7897 ALLEGRO_COLOR tc;
7898 if (is_link) {
7899 /* links: show in a distinctive colour, underlined on hover */
7900 if (wgt->state & N_GUI_STATE_HOVER) {
7901 tc = style->link_color_hover;
7902 } else {
7903 tc = style->link_color_normal;
7904 }
7905 } else {
7906 tc = _text_for_state(&wgt->theme, wgt->state);
7907 }
7908
7909 float tw = (float)lbbbw;
7910 float max_text_w = effective_w - style->label_padding * 2; /* padding each side */
7911 float tx;
7912
7913 if (lb->align == N_GUI_ALIGN_JUSTIFIED) {
7914 float lpad = style->label_padding;
7915 float view_h = wgt->h - lpad;
7916 float content_h = _label_content_height(lb->text, font, max_text_w);
7917 int need_scrollbar = (content_h > view_h) ? 1 : 0;
7918 float sb_size = need_scrollbar ? style->scrollbar_size : 0;
7919 float text_w = max_text_w - sb_size;
7920
7921 /* clamp scroll_y */
7922 if (need_scrollbar) {
7923 float max_sy = content_h - view_h;
7924 if (max_sy < 0) max_sy = 0;
7925 if (lb->scroll_y > max_sy) lb->scroll_y = max_sy;
7926 if (lb->scroll_y < 0) lb->scroll_y = 0;
7927 } else {
7928 lb->scroll_y = 0;
7929 }
7930
7931 /* clip to widget bounds (minus scrollbar) */
7932 int pcx, pcy, pcw, pch;
7933 al_get_clipping_rectangle(&pcx, &pcy, &pcw, &pch);
7934 _set_clipping_rect_transformed((int)ax, (int)ay, (int)(wgt->w - sb_size), (int)wgt->h);
7935
7936 float text_y = ay + lpad / 2.0f - lb->scroll_y;
7937 /* draw selection highlight for justified text */
7938 if (lb->sel_start >= 0 && lb->sel_end >= 0 && lb->sel_start != lb->sel_end) {
7939 _draw_justified_selection(font, ax + lpad, text_y, text_w, content_h + fh,
7940 lb->text, lb->sel_start, lb->sel_end, wgt->theme.selection_color);
7941 }
7942 /* pass a large avail_h so _draw_text_justified doesn't truncate; clipping handles visibility */
7943 _draw_text_justified(font, tc, ax + lpad, text_y, text_w, content_h + fh, lb->text);
7944
7945 /* underline for links on hover */
7946 if (is_link && (wgt->state & N_GUI_STATE_HOVER)) {
7947 float draw_w = tw < text_w ? tw : text_w;
7948 al_draw_line(ax + lpad, text_y + fh, ax + lpad + draw_w, text_y + fh, tc, _min_thickness(style->link_underline_thickness));
7949 }
7950
7951 al_set_clipping_rectangle(pcx, pcy, pcw, pch);
7952
7953 /* draw scrollbar */
7954 if (need_scrollbar) {
7955 _draw_widget_vscrollbar(ax, ay + lpad / 2.0f, wgt->w, view_h, content_h, lb->scroll_y, style);
7956 }
7957 return;
7958 }
7959
7960 float ty = ay + (wgt->h - fh) / 2.0f - (float)lbbby;
7961
7962 if (lb->align == N_GUI_ALIGN_CENTER) {
7963 if (tw > max_text_w) {
7964 _draw_text_truncated(font, tc, ax + style->label_padding, ty, max_text_w, lb->text);
7965 tx = ax + style->label_padding;
7966 } else {
7967 tx = ax + (effective_w - tw) / 2.0f - (float)lbbbx;
7968 al_draw_text(font, tc, tx, ty, 0, lb->text);
7969 }
7970 } else if (lb->align == N_GUI_ALIGN_RIGHT) {
7971 if (tw > max_text_w) {
7972 _draw_text_truncated(font, tc, ax + style->label_padding, ty, max_text_w, lb->text);
7973 tx = ax + style->label_padding;
7974 } else {
7975 tx = ax + effective_w - tw - style->label_padding - (float)lbbbx;
7976 al_draw_text(font, tc, tx, ty, 0, lb->text);
7977 }
7978 } else {
7979 /* N_GUI_ALIGN_LEFT */
7980 tx = ax + style->label_padding;
7981 if (tw > max_text_w) {
7982 _draw_text_truncated(font, tc, tx, ty, max_text_w, lb->text);
7983 } else {
7984 al_draw_text(font, tc, tx, ty, 0, lb->text);
7985 }
7986 }
7987
7988 /* draw selection highlight for non-justified labels */
7989 if (lb->sel_start >= 0 && lb->sel_end >= 0 && lb->sel_start != lb->sel_end) {
7990 int slo = lb->sel_start < lb->sel_end ? lb->sel_start : lb->sel_end;
7991 int shi = lb->sel_start < lb->sel_end ? lb->sel_end : lb->sel_start;
7992 size_t tlen = strlen(lb->text);
7993 if ((size_t)slo > tlen) slo = (int)tlen;
7994 if ((size_t)shi > tlen) shi = (int)tlen;
7995 char stmp[N_GUI_TEXT_MAX];
7996 memcpy(stmp, lb->text, (size_t)slo);
7997 stmp[slo] = '\0';
7998 float sx1 = _text_w(font, stmp);
7999 memcpy(stmp, lb->text, (size_t)shi);
8000 stmp[shi] = '\0';
8001 float sx2 = _text_w(font, stmp);
8002 al_draw_filled_rectangle(tx + sx1, ty, tx + sx2, ty + fh, wgt->theme.selection_color);
8003 }
8004
8005 /* underline for links on hover */
8006 if (is_link && (wgt->state & N_GUI_STATE_HOVER)) {
8007 float draw_w = tw < max_text_w ? tw : max_text_w;
8008 al_draw_line(tx, ty + fh, tx + draw_w, ty + fh, tc, _min_thickness(style->link_underline_thickness));
8009 }
8010}
8011
8015/* owner-draw custom widget: delegate the interior to the user callback, handing it
8016 the widget's absolute top-left and the effective font. Input is not routed here. */
8017static void _draw_custom(N_GUI_WIDGET* wgt, float ox, float oy, ALLEGRO_FONT* default_font) {
8019 if (!cd || !cd->draw) return;
8020 cd->draw(wgt, ox + wgt->x, oy + wgt->y, wgt->font ? wgt->font : default_font, cd->user_data);
8021}
8022
8023static void _draw_widget(N_GUI_WIDGET* wgt, float ox, float oy, ALLEGRO_FONT* default_font, float win_w, N_GUI_STYLE* style) {
8024 if (!wgt || !wgt->visible) return;
8025
8026 /* disabled widgets: force idle state visually, draw with reduced opacity */
8027 int saved_state = wgt->state;
8028 if (!wgt->enabled) {
8029 wgt->state = N_GUI_STATE_IDLE;
8030 }
8031
8032 switch (wgt->type) {
8033 case N_GUI_TYPE_BUTTON:
8034 _draw_button(wgt, ox, oy, default_font, style);
8035 break;
8036 case N_GUI_TYPE_SLIDER:
8037 _draw_slider(wgt, ox, oy, default_font, style);
8038 break;
8040 _draw_textarea(wgt, ox, oy, default_font, style);
8041 break;
8043 _draw_checkbox(wgt, ox, oy, default_font, style);
8044 break;
8046 _draw_scrollbar(wgt, ox, oy, style);
8047 break;
8048 case N_GUI_TYPE_LISTBOX:
8049 _draw_listbox(wgt, ox, oy, default_font, style);
8050 break;
8052 _draw_radiolist(wgt, ox, oy, default_font, style);
8053 break;
8055 _draw_combobox(wgt, ox, oy, default_font, style);
8056 break;
8057 case N_GUI_TYPE_IMAGE:
8058 _draw_image(wgt, ox, oy);
8059 break;
8060 case N_GUI_TYPE_LABEL:
8061 _draw_label(wgt, ox, oy, default_font, win_w, style);
8062 break;
8064 _draw_dropmenu(wgt, ox, oy, default_font, style);
8065 break;
8067 _draw_splitpane(wgt, ox, oy);
8068 break;
8069 case N_GUI_TYPE_HEXVIEW:
8070 _draw_hexview(wgt, ox, oy, default_font, style);
8071 break;
8073 _draw_syntaxview(wgt, ox, oy, default_font, style);
8074 break;
8076 _draw_datagrid(wgt, ox, oy, default_font, style);
8077 break;
8079 _draw_progressbar(wgt, ox, oy, default_font, style);
8080 break;
8081 case N_GUI_TYPE_CUSTOM:
8082 _draw_custom(wgt, ox, oy, default_font);
8083 break;
8084 default:
8085 break;
8086 }
8087
8088 /* restore state and draw dimming overlay for disabled widgets */
8089 if (!wgt->enabled) {
8090 wgt->state = saved_state;
8091 float dx = ox + wgt->x;
8092 float dy = oy + wgt->y;
8093 al_draw_filled_rectangle(dx, dy, dx + wgt->w, dy + wgt->h,
8094 al_map_rgba(0, 0, 0, 120));
8095 }
8096}
8097
8098/* TITLEBAR BUTTONS (minimize, maximize, close) */
8099
8101static float _tb_btn_resolved_size(const N_GUI_WINDOW* win, const N_GUI_STYLE* style) {
8102 if (style->tb_btn_size > 0) return style->tb_btn_size;
8103 return win->titlebar_h - 4.0f;
8104}
8105
8107static float _tb_buttons_area_width(const N_GUI_WINDOW* win, const N_GUI_STYLE* style) {
8108 if (win->flags & N_GUI_WIN_FRAMELESS) return 0.0f;
8109 int count = 0;
8110 if (win->flags & N_GUI_WIN_BTN_MINIMIZE) count++;
8111 if (win->flags & N_GUI_WIN_BTN_MAXIMIZE) count++;
8112 if (win->flags & N_GUI_WIN_BTN_CLOSE) count++;
8113 if (count == 0) return 0.0f;
8114 float sz = _tb_btn_resolved_size(win, style);
8115 return (float)count * sz + (float)(count - 1) * style->tb_btn_spacing + style->tb_btn_right_margin;
8116}
8117
8121static int _tb_button_rect(const N_GUI_WINDOW* win, const N_GUI_STYLE* style, int btn_type, float* out_x, float* out_y, float* out_w, float* out_h) {
8122 if (win->flags & N_GUI_WIN_FRAMELESS) return 0;
8123 int flag = 0;
8124 if (btn_type == N_GUI_TB_BTN_MINIMIZE)
8126 else if (btn_type == N_GUI_TB_BTN_MAXIMIZE)
8128 else if (btn_type == N_GUI_TB_BTN_CLOSE)
8129 flag = N_GUI_WIN_BTN_CLOSE;
8130 if (!(win->flags & flag)) return 0;
8131
8132 float sz = _tb_btn_resolved_size(win, style);
8133 float right_edge = win->x + win->w - style->tb_btn_right_margin;
8134
8135 /* position index from the right: close=0, maximize=1, minimize=2 (but only for enabled buttons) */
8136 int pos = 0;
8137 if (btn_type == N_GUI_TB_BTN_CLOSE) {
8138 pos = 0;
8139 } else if (btn_type == N_GUI_TB_BTN_MAXIMIZE) {
8140 pos = (win->flags & N_GUI_WIN_BTN_CLOSE) ? 1 : 0;
8141 } else { /* MINIMIZE */
8142 pos = 0;
8143 if (win->flags & N_GUI_WIN_BTN_CLOSE) pos++;
8144 if (win->flags & N_GUI_WIN_BTN_MAXIMIZE) pos++;
8145 }
8146
8147 *out_x = right_edge - sz - (float)pos * (sz + style->tb_btn_spacing);
8148 *out_y = win->y + (win->titlebar_h - sz) / 2.0f;
8149 *out_w = sz;
8150 *out_h = sz;
8151 return 1;
8152}
8153
8155static void _draw_tb_buttons(N_GUI_WINDOW* win, const N_GUI_STYLE* style) {
8157 for (int i = 0; i < 3; i++) {
8158 int bt = btn_types[i];
8159 float bx, by, bw, bh;
8160 if (!_tb_button_rect(win, style, bt, &bx, &by, &bw, &bh)) continue;
8161
8162 /* determine visual state */
8163 int state = N_GUI_STATE_IDLE;
8164 if (win->tb_buttons.pressed == bt)
8165 state = N_GUI_STATE_ACTIVE;
8166 else if (win->tb_buttons.hovered == bt)
8167 state = N_GUI_STATE_HOVER;
8168
8169 /* select theme */
8171
8172 /* select bitmap */
8173 ALLEGRO_BITMAP* bmp = NULL;
8174 if (bt == N_GUI_TB_BTN_MINIMIZE) {
8177 else if (state == N_GUI_STATE_HOVER && win->tb_buttons.minimize_hover_bitmap)
8179 else
8180 bmp = win->tb_buttons.minimize_bitmap;
8181 } else if (bt == N_GUI_TB_BTN_MAXIMIZE) {
8184 else if (state == N_GUI_STATE_HOVER && win->tb_buttons.maximize_hover_bitmap)
8186 else
8187 bmp = win->tb_buttons.maximize_bitmap;
8188 } else { /* CLOSE */
8191 else if (state == N_GUI_STATE_HOVER && win->tb_buttons.close_hover_bitmap)
8192 bmp = win->tb_buttons.close_hover_bitmap;
8193 else
8194 bmp = win->tb_buttons.close_bitmap;
8195 }
8196
8197 if (bmp) {
8198 al_draw_scaled_bitmap(bmp, 0, 0,
8199 (float)al_get_bitmap_width(bmp), (float)al_get_bitmap_height(bmp),
8200 bx, by, bw, bh, 0);
8201 } else {
8202 /* draw themed background */
8203 _draw_themed_rect(theme, state, bx, by, bw, bh, 1);
8204
8205 /* draw glyph */
8206 ALLEGRO_COLOR gc = _text_for_state(theme, state);
8207 float cx = bx + bw / 2.0f;
8208 float cy = by + bh / 2.0f;
8209 float g = bw * 0.22f; /* glyph half-extent */
8210 float thick = style->tb_btn_glyph_thickness;
8211
8212 if (bt == N_GUI_TB_BTN_MINIMIZE) {
8213 /* horizontal line in lower third */
8214 al_draw_line(cx - g, cy + g * 0.5f, cx + g, cy + g * 0.5f, gc, thick);
8215 } else if (bt == N_GUI_TB_BTN_MAXIMIZE) {
8216 if (win->state & N_GUI_WIN_MAXIMISED) {
8217 /* restore icon: two overlapping rectangles */
8218 float off = g * 0.4f;
8219 al_draw_rectangle(cx - g + off, cy - g, cx + g, cy + g - off, gc, thick);
8220 al_draw_rectangle(cx - g, cy - g + off, cx + g - off, cy + g, gc, thick);
8221 } else {
8222 /* single rectangle outline */
8223 al_draw_rectangle(cx - g, cy - g, cx + g, cy + g, gc, thick);
8224 }
8225 } else { /* CLOSE: X shape */
8226 al_draw_line(cx - g, cy - g, cx + g, cy + g, gc, thick);
8227 al_draw_line(cx + g, cy - g, cx - g, cy + g, gc, thick);
8228 }
8229 }
8230 }
8231}
8232
8234static void _draw_window(N_GUI_WINDOW* win, ALLEGRO_FONT* default_font, N_GUI_STYLE* style) {
8235 if (!(win->state & N_GUI_WIN_OPEN)) return;
8236
8237 ALLEGRO_FONT* font = win->font ? win->font : default_font;
8238 int frameless = (win->flags & N_GUI_WIN_FRAMELESS) ? 1 : 0;
8239 float tbh = frameless ? 0.0f : win->titlebar_h;
8240
8241 /* title bar (skip for frameless windows) */
8242 if (!frameless) {
8243 if (win->titlebar_bitmap) {
8244 al_draw_scaled_bitmap(win->titlebar_bitmap, 0, 0,
8245 (float)al_get_bitmap_width(win->titlebar_bitmap), (float)al_get_bitmap_height(win->titlebar_bitmap),
8246 win->x, win->y, win->w, tbh, 0);
8247 } else {
8248 ALLEGRO_COLOR tb_bg = win->theme.bg_active;
8249 al_draw_filled_rounded_rectangle(win->x, win->y, win->x + win->w, win->y + tbh,
8250 win->theme.corner_rx, win->theme.corner_ry, tb_bg);
8251 }
8252 if (font && win->title[0]) {
8253 int tbbx = 0, tbby = 0, tbbw = 0, tbbh = 0;
8254 _text_dims(font, win->title, &tbbx, &tbby, &tbbw, &tbbh);
8255 float fh = (float)tbbh;
8256 float btn_area = _tb_buttons_area_width(win, style);
8257 float max_title_w = win->w - style->title_max_w_reserve - btn_area;
8259 win->x + style->title_padding, win->y + (tbh - fh) / 2.0f - (float)tbby, max_title_w, win->title);
8260 }
8261 _draw_tb_buttons(win, style);
8262 }
8263
8264 ALLEGRO_COLOR tb_bd = win->theme.border_normal;
8265
8266 /* body (if not minimised) */
8267 if (!(win->state & N_GUI_WIN_MINIMISED)) {
8268 float body_y = win->y + tbh;
8269 float body_h = win->h - tbh;
8270
8271 /* determine if scrollbars are needed */
8272 int need_vscroll = 0;
8273 int need_hscroll = 0;
8274 float scrollbar_size = style->scrollbar_size;
8275
8276 if (win->flags & N_GUI_WIN_AUTO_SCROLLBAR) {
8277 _window_update_content_size(win, default_font);
8278 if (win->content_h > body_h) need_vscroll = 1;
8279 if (win->content_w > win->w) need_hscroll = 1;
8280 /* adjust for scrollbar space */
8281 if (need_vscroll && win->content_w > (win->w - scrollbar_size)) need_hscroll = 1;
8282 if (need_hscroll && win->content_h > (body_h - scrollbar_size)) need_vscroll = 1;
8283 /* vertical-only panels: suppress the horizontal scrollbar (content wider than
8284 the window is clipped rather than horizontally scrolled) */
8285 if (win->flags & N_GUI_WIN_NO_HSCROLL) {
8286 need_hscroll = 0;
8287 win->scroll_x = 0;
8288 }
8289 }
8290
8291 float content_area_w = win->w - (need_vscroll ? scrollbar_size : 0);
8292 float content_area_h = body_h - (need_hscroll ? scrollbar_size : 0);
8293
8294 /* clamp scroll */
8295 if (need_vscroll) {
8296 float max_sy = win->content_h - content_area_h;
8297 if (max_sy < 0) max_sy = 0;
8298 if (win->scroll_y > max_sy) win->scroll_y = max_sy;
8299 if (win->scroll_y < 0) win->scroll_y = 0;
8300 } else {
8301 win->scroll_y = 0;
8302 }
8303 if (need_hscroll) {
8304 float max_sx = win->content_w - content_area_w;
8305 if (max_sx < 0) max_sx = 0;
8306 if (win->scroll_x > max_sx) win->scroll_x = max_sx;
8307 if (win->scroll_x < 0) win->scroll_x = 0;
8308 } else {
8309 win->scroll_x = 0;
8310 }
8311
8312 /* Body background. It starts one pixel ABOVE body_y so it tucks under the
8313 * title bar. A window at a fractional y (a centred pop-up lands on .5 often
8314 * enough) rasterises the title bar's bottom edge and the body's top edge to
8315 * different device rows, which left a one-pixel strip painted by neither and
8316 * showing whatever was behind the window. The overlap is hidden under the
8317 * title bar, which is drawn opaque just above, and only the fill is nudged:
8318 * body_y itself still drives the widget origin, the scroll maths and hit
8319 * testing, so nothing moves. */
8320 float bg_top = frameless ? body_y : body_y - 1.0f;
8321 if (win->bg_bitmap) {
8322 _draw_bitmap_scaled(win->bg_bitmap, win->x, bg_top, win->w, body_h + (body_y - bg_top), win->bg_scale_mode);
8323 } else {
8324 al_draw_filled_rectangle(win->x, bg_top, win->x + win->w, win->y + win->h,
8325 win->theme.bg_normal);
8326 }
8327 al_draw_rounded_rectangle(win->x, win->y, win->x + win->w, win->y + win->h,
8328 win->theme.corner_rx, win->theme.corner_ry, tb_bd,
8330
8331 /* draw widgets with clipping to content area */
8332 float ox = win->x - win->scroll_x;
8333 float oy = body_y - win->scroll_y;
8334
8335 /* set clipping rectangle to window content area */
8336 int prev_cx, prev_cy, prev_cw, prev_ch;
8337 al_get_clipping_rectangle(&prev_cx, &prev_cy, &prev_cw, &prev_ch);
8338 /* Frameless windows have no chrome, their content is edge-to-edge
8339 * by design (drag-handle holders for HUD icons), so skip the
8340 * thumb-padding inset that would clip the leftmost pixels. */
8341 int clip_pad = frameless ? 0 : (int)style->scrollbar_thumb_padding;
8342 _set_clipping_rect_transformed((int)win->x + clip_pad, (int)body_y,
8343 (int)content_area_w - clip_pad, (int)content_area_h);
8344
8345 /* pass width hint so labels can adapt:
8346 * - auto-scrollbar windows: use content width so labels are never truncated
8347 * (the clipping rect + scrollbars handle overflow)
8348 * - resizable windows (without auto-scrollbar): use window width so labels expand
8349 * - fixed windows: 0 (use widget's own width) */
8350 float label_win_w = 0;
8352 label_win_w = win->content_w + style->title_max_w_reserve + style->label_padding;
8353 else if (win->flags & N_GUI_WIN_RESIZABLE)
8354 label_win_w = win->w;
8355 list_foreach(node, win->widgets) {
8356 _draw_widget((N_GUI_WIDGET*)node->ptr, ox, oy, default_font, label_win_w, style);
8357 }
8358
8359 /* restore clipping */
8360 al_set_clipping_rectangle(prev_cx, prev_cy, prev_cw, prev_ch);
8361
8362 /* draw automatic scrollbars */
8363 if (need_vscroll) {
8364 float sb_x = win->x + win->w - scrollbar_size;
8365 float sb_y = body_y;
8366 float sb_h = content_area_h;
8367 /* leave room for resize corner when no horizontal scrollbar */
8368 if ((win->flags & N_GUI_WIN_RESIZABLE) && !need_hscroll) sb_h -= scrollbar_size;
8369
8370 /* track */
8371 al_draw_filled_rectangle(sb_x, sb_y, sb_x + scrollbar_size, sb_y + sb_h,
8372 style->scrollbar_track_color);
8373
8374 /* thumb */
8375 float ratio = content_area_h / win->content_h;
8376 if (ratio > 1.0f) ratio = 1.0f;
8377 float thumb_h = ratio * sb_h;
8378 if (thumb_h < style->scrollbar_thumb_min) thumb_h = style->scrollbar_thumb_min;
8379 float max_scroll = win->content_h - content_area_h;
8380 float pos_ratio = (max_scroll > 0) ? win->scroll_y / max_scroll : 0;
8381 float thumb_y = sb_y + pos_ratio * (sb_h - thumb_h);
8382 al_draw_filled_rounded_rectangle(sb_x + style->scrollbar_thumb_padding, thumb_y, sb_x + scrollbar_size - style->scrollbar_thumb_padding,
8383 thumb_y + thumb_h, style->scrollbar_thumb_corner_r, style->scrollbar_thumb_corner_r,
8384 style->scrollbar_thumb_color);
8385 }
8386 if (need_hscroll) {
8387 float sb_x = win->x;
8388 float sb_y = win->y + win->h - scrollbar_size;
8389 float sb_w = content_area_w;
8390 /* leave room for resize corner when no vertical scrollbar */
8391 if ((win->flags & N_GUI_WIN_RESIZABLE) && !need_vscroll) sb_w -= scrollbar_size;
8392
8393 /* track */
8394 al_draw_filled_rectangle(sb_x, sb_y, sb_x + sb_w, sb_y + scrollbar_size,
8395 style->scrollbar_track_color);
8396
8397 /* thumb */
8398 float ratio = content_area_w / win->content_w;
8399 if (ratio > 1.0f) ratio = 1.0f;
8400 float thumb_w = ratio * sb_w;
8401 if (thumb_w < style->scrollbar_thumb_min) thumb_w = style->scrollbar_thumb_min;
8402 float max_scroll = win->content_w - content_area_w;
8403 float pos_ratio = (max_scroll > 0) ? win->scroll_x / max_scroll : 0;
8404 float thumb_x = sb_x + pos_ratio * (sb_w - thumb_w);
8405 al_draw_filled_rounded_rectangle(thumb_x, sb_y + style->scrollbar_thumb_padding, thumb_x + thumb_w,
8406 sb_y + scrollbar_size - style->scrollbar_thumb_padding, style->scrollbar_thumb_corner_r, style->scrollbar_thumb_corner_r,
8407 style->scrollbar_thumb_color);
8408 }
8409
8410 /* draw resize handle if window is resizable */
8411 if (win->flags & N_GUI_WIN_RESIZABLE) {
8412 float rx = win->x + win->w;
8413 float ry = win->y + win->h;
8414 float grip_size = style->grip_size;
8415 ALLEGRO_COLOR grip_color = style->grip_color;
8416 /* three diagonal lines in bottom-right corner */
8417 float grip_thick = _min_thickness(style->grip_line_thickness);
8418 al_draw_line(rx - grip_size, ry - 2, rx - 2, ry - grip_size, grip_color, grip_thick);
8419 al_draw_line(rx - grip_size + 4, ry - 2, rx - 2, ry - grip_size + 4, grip_color, grip_thick);
8420 al_draw_line(rx - grip_size + 8, ry - 2, rx - 2, ry - grip_size + 8, grip_color, grip_thick);
8421 }
8422 } else {
8423 al_draw_rounded_rectangle(win->x, win->y, win->x + win->w, win->y + tbh,
8424 win->theme.corner_rx, win->theme.corner_ry, tb_bd,
8426 }
8427}
8428
8431 float max_r = 0, max_b = 0;
8432 list_foreach(wnode, ctx->windows) {
8433 const N_GUI_WINDOW* win = (N_GUI_WINDOW*)wnode->ptr;
8434 if (!(win->state & N_GUI_WIN_OPEN)) continue;
8435 /* detached windows sit in their own display and never contribute to the
8436 * main display's global scroll bounds */
8437 if (!_win_on_pass(ctx, win)) continue;
8438 float r = win->x + win->w;
8439 float b = win->y + win->h;
8440 if (r > max_r) max_r = r;
8441 if (b > max_b) max_b = b;
8442 }
8443 ctx->gui_bounds_w = max_r;
8444 ctx->gui_bounds_h = max_b;
8445}
8446
8448static bool _tooltip_line_cb(int line_num, const char* line, int size, void* extra) {
8449 (void)line_num;
8450 (void)line;
8451 (void)size;
8452 (*(int*)extra)++;
8453 return true;
8454}
8455
8470static void _draw_tooltip(N_GUI_CTX* ctx) {
8471 if (!(ctx->style.tooltip_delay > 0.0f && ctx->tooltip_widget_id >= 0 && (al_get_time() - ctx->tooltip_armed_at) >= (double)ctx->style.tooltip_delay))
8472 return;
8473 {
8474 float ox = 0.0f, oy = 0.0f;
8475 N_GUI_WINDOW* tip_win = _find_widget_window(ctx, ctx->tooltip_widget_id, &ox, &oy);
8476 ALLEGRO_DISPLAY* owner = tip_win ? tip_win->native : NULL;
8477 N_GUI_WIDGET* tip_wgt = n_gui_get_widget(ctx, ctx->tooltip_widget_id);
8478 /* only draw in the pass of the display this widget's window lives on */
8479 if (owner != ctx->pass_display)
8480 return;
8481 if (tip_wgt && tip_wgt->visible && tip_wgt->tooltip && tip_wgt->tooltip[0] && ctx->default_font) {
8482 float disp_w = (owner ? (float)al_get_display_width(owner) : (float)ctx->display_w);
8483 float disp_h = (owner ? (float)al_get_display_height(owner) : (float)ctx->display_h);
8484 /* clamp rectangle: the widget's window bounds intersected with the display,
8485 falling back to the whole display if that rect is unusable */
8486 float cl = 0.0f, ct = 0.0f, cr = disp_w, cb = disp_h;
8487 if (tip_win) {
8488 if (tip_win->x > cl) cl = tip_win->x;
8489 if (tip_win->y > ct) ct = tip_win->y;
8490 if (disp_w > 0.0f && tip_win->x + tip_win->w < cr) cr = tip_win->x + tip_win->w;
8491 if (disp_h > 0.0f && tip_win->y + tip_win->h < cb) cb = tip_win->y + tip_win->h;
8492 if (cr - cl < 8.0f || cb - ct < 8.0f) {
8493 cl = 0.0f;
8494 ct = 0.0f;
8495 cr = disp_w;
8496 cb = disp_h;
8497 }
8498 }
8499 {
8500 float line_h = (float)al_get_font_line_height(ctx->default_font) + 2.0f;
8501 float max_text_w = (cr - cl) - 4.0f - 16.0f; /* window inner width minus a 2px margin and the bubble's 8px h-pad each side */
8502 float natural_w, text_w, bw, bh, bx, by;
8503 int nlines = 1;
8504 if (max_text_w < 1.0f) max_text_w = 1.0f; /* window too tiny: best effort */
8505 natural_w = _text_w(ctx->default_font, tip_wgt->tooltip);
8506 if (natural_w <= max_text_w) {
8507 text_w = natural_w; /* fits on one line */
8508 } else {
8509 text_w = max_text_w; /* wrap to the available width */
8510 nlines = 0;
8511 al_do_multiline_text(ctx->default_font, max_text_w, tip_wgt->tooltip, _tooltip_line_cb, &nlines);
8512 if (nlines < 1) nlines = 1;
8513 }
8514 bw = text_w + 16.0f;
8515 bh = (float)nlines * line_h + 10.0f;
8516 bx = (float)ctx->mouse_x + 14.0f;
8517 by = (float)ctx->mouse_y + 20.0f;
8518 /* keep the whole bubble inside the clamp rect: shift left, and flip above
8519 the cursor when it would overflow the bottom */
8520 if (bx + bw > cr) bx = cr - bw;
8521 if (bx < cl) bx = cl;
8522 if (by + bh > cb) by = (float)ctx->mouse_y - bh - 6.0f;
8523 if (by + bh > cb) by = cb - bh;
8524 if (by < ct) by = ct;
8525 al_draw_filled_rounded_rectangle(bx, by, bx + bw, by + bh, 4.0f, 4.0f, ctx->style.tooltip_bg);
8526 al_draw_rounded_rectangle(bx, by, bx + bw, by + bh, 4.0f, 4.0f, ctx->style.tooltip_border, 1.0f);
8527 al_draw_multiline_text(ctx->default_font, ctx->style.tooltip_fg, bx + 8.0f, by + 5.0f, max_text_w, line_h, ALLEGRO_ALIGN_LEFT, tip_wgt->tooltip);
8528 }
8529 }
8530 }
8531}
8532
8543 __n_assert(ctx, return);
8544
8545 /* this is the main display's pass: detached windows are skipped here and
8546 * drawn by n_gui_draw_detached into their own displays */
8547 ctx->pass_display = NULL;
8548
8549 /* compute bounds and determine if global scrollbars are needed.
8550 * When virtual canvas is active, compare bounds against virtual dimensions
8551 * (since window positions are in virtual coordinates). */
8553 float scrollbar_size = ctx->style.global_scrollbar_size;
8554 int need_global_vscroll = 0;
8555 int need_global_hscroll = 0;
8556
8557 /* gate on both dimensions: virtual canvas is only active when both are set */
8558 int virtual_active = (ctx->virtual_w > 0 && ctx->virtual_h > 0);
8559 float eff_w = virtual_active ? ctx->virtual_w : ctx->display_w;
8560 float eff_h = virtual_active ? ctx->virtual_h : ctx->display_h;
8561
8562 if (eff_w > 0 && eff_h > 0) {
8563 if (ctx->gui_bounds_h > eff_h) need_global_vscroll = 1;
8564 if (ctx->gui_bounds_w > eff_w) need_global_hscroll = 1;
8565 /* account for scrollbar space */
8566 if (need_global_vscroll && ctx->gui_bounds_w > (eff_w - scrollbar_size)) need_global_hscroll = 1;
8567 if (need_global_hscroll && ctx->gui_bounds_h > (eff_h - scrollbar_size)) need_global_vscroll = 1;
8568
8569 /* clamp global scroll */
8570 float view_w = eff_w - (need_global_vscroll ? scrollbar_size : 0);
8571 float view_h = eff_h - (need_global_hscroll ? scrollbar_size : 0);
8572 if (need_global_vscroll) {
8573 float max_sy = ctx->gui_bounds_h - view_h;
8574 if (max_sy < 0) max_sy = 0;
8575 if (ctx->global_scroll_y > max_sy) ctx->global_scroll_y = max_sy;
8576 if (ctx->global_scroll_y < 0) ctx->global_scroll_y = 0;
8577 } else {
8578 ctx->global_scroll_y = 0;
8579 }
8580 if (need_global_hscroll) {
8581 float max_sx = ctx->gui_bounds_w - view_w;
8582 if (max_sx < 0) max_sx = 0;
8583 if (ctx->global_scroll_x > max_sx) ctx->global_scroll_x = max_sx;
8584 if (ctx->global_scroll_x < 0) ctx->global_scroll_x = 0;
8585 } else {
8586 ctx->global_scroll_x = 0;
8587 }
8588 }
8589
8590 /* apply global scroll in virtual units, then scale + offset to physical display */
8591 ALLEGRO_TRANSFORM global_tf, prev_tf;
8592 al_copy_transform(&prev_tf, al_get_current_transform());
8593 al_identity_transform(&global_tf);
8594 al_translate_transform(&global_tf, -ctx->global_scroll_x, -ctx->global_scroll_y);
8595 if (ctx->virtual_w > 0 && ctx->virtual_h > 0 && ctx->gui_scale > 0) {
8596 al_scale_transform(&global_tf, ctx->gui_scale, ctx->gui_scale);
8597 al_translate_transform(&global_tf, ctx->gui_offset_x, ctx->gui_offset_y);
8598 }
8599 al_compose_transform(&global_tf, &prev_tf);
8600 al_use_transform(&global_tf);
8601
8602 /* clip to the content area (excluding global scrollbar space) */
8603 int prev_cx, prev_cy, prev_cw, prev_ch;
8604 int set_clip = 0;
8605 if (need_global_vscroll || need_global_hscroll) {
8606 al_get_clipping_rectangle(&prev_cx, &prev_cy, &prev_cw, &prev_ch);
8607 /* clipping is in physical pixels; map virtual scrollbar area to screen */
8608 float clip_vw = eff_w - (need_global_vscroll ? scrollbar_size : 0);
8609 float clip_vh = eff_h - (need_global_hscroll ? scrollbar_size : 0);
8610 if (ctx->virtual_w > 0 && ctx->virtual_h > 0 && ctx->gui_scale > 0) {
8611 al_set_clipping_rectangle((int)ctx->gui_offset_x, (int)ctx->gui_offset_y,
8612 (int)(clip_vw * ctx->gui_scale),
8613 (int)(clip_vh * ctx->gui_scale));
8614 } else {
8615 al_set_clipping_rectangle(0, 0, (int)clip_vw, (int)clip_vh);
8616 }
8617 set_clip = 1;
8618 }
8619
8620 /* auto-apply autofit for windows that have it configured */
8621 list_foreach(anode, ctx->windows) {
8622 const N_GUI_WINDOW* awin = (N_GUI_WINDOW*)anode->ptr;
8623 if (awin && awin->autofit_flags && _win_on_pass(ctx, awin)) {
8624 n_gui_window_apply_autofit(ctx, awin->id);
8625 }
8626 }
8627
8628 list_foreach(node, ctx->windows) {
8629 N_GUI_WINDOW* dwin = (N_GUI_WINDOW*)node->ptr;
8630 if (!_win_on_pass(ctx, dwin)) continue;
8631 _draw_window(dwin, ctx->default_font, &ctx->style);
8632 /* Per-window custom rendering, drawn at this window's z-order so it
8633 * is occluded by any window drawn later in the loop. */
8634 if (dwin && dwin->on_content_draw && (dwin->state & N_GUI_WIN_OPEN) && !(dwin->state & N_GUI_WIN_MINIMISED)) {
8635 dwin->on_content_draw(dwin->id, dwin->on_content_draw_data);
8636 }
8637 }
8638 /* draw combobox dropdown overlay on top of everything */
8640 /* draw dropdown menu panel overlay on top of everything */
8642
8643 /* restore transform and clipping */
8644 if (set_clip) {
8645 al_set_clipping_rectangle(prev_cx, prev_cy, prev_cw, prev_ch);
8646 }
8647
8648 /* draw global scrollbars: apply virtual canvas transform (without scroll)
8649 * so scrollbars appear at the edges of the virtual canvas area */
8650 if (need_global_vscroll || need_global_hscroll) {
8651 ALLEGRO_TRANSFORM sb_tf;
8652 al_identity_transform(&sb_tf);
8653 if (ctx->virtual_w > 0 && ctx->virtual_h > 0 && ctx->gui_scale > 0) {
8654 al_scale_transform(&sb_tf, ctx->gui_scale, ctx->gui_scale);
8655 al_translate_transform(&sb_tf, ctx->gui_offset_x, ctx->gui_offset_y);
8656 }
8657 al_compose_transform(&sb_tf, &prev_tf);
8658 al_use_transform(&sb_tf);
8659 }
8660
8661 if (need_global_vscroll) {
8662 float sb_x = eff_w - scrollbar_size;
8663 float sb_y = 0;
8664 float sb_h = eff_h - (need_global_hscroll ? scrollbar_size : 0);
8665 float view_h = sb_h;
8666
8667 /* track */
8668 al_draw_filled_rectangle(sb_x, sb_y, sb_x + scrollbar_size, sb_y + sb_h,
8670
8671 /* thumb */
8672 float ratio = view_h / ctx->gui_bounds_h;
8673 if (ratio > 1.0f) ratio = 1.0f;
8674 float thumb_h = ratio * sb_h;
8675 if (thumb_h < ctx->style.global_scrollbar_thumb_min) thumb_h = ctx->style.global_scrollbar_thumb_min;
8676 float max_scroll = ctx->gui_bounds_h - view_h;
8677 float pos_ratio = (max_scroll > 0) ? ctx->global_scroll_y / max_scroll : 0;
8678 float thumb_y = sb_y + pos_ratio * (sb_h - thumb_h);
8679 al_draw_filled_rounded_rectangle(sb_x + ctx->style.global_scrollbar_thumb_padding, thumb_y, sb_x + scrollbar_size - ctx->style.global_scrollbar_thumb_padding,
8682 al_draw_rounded_rectangle(sb_x + ctx->style.global_scrollbar_thumb_padding, thumb_y, sb_x + scrollbar_size - ctx->style.global_scrollbar_thumb_padding,
8685 }
8686 if (need_global_hscroll) {
8687 float sb_x = 0;
8688 float sb_y = eff_h - scrollbar_size;
8689 float sb_w = eff_w - (need_global_vscroll ? scrollbar_size : 0);
8690 float view_w = sb_w;
8691
8692 /* track */
8693 al_draw_filled_rectangle(sb_x, sb_y, sb_x + sb_w, sb_y + scrollbar_size,
8695
8696 /* thumb */
8697 float ratio = view_w / ctx->gui_bounds_w;
8698 if (ratio > 1.0f) ratio = 1.0f;
8699 float thumb_w = ratio * sb_w;
8700 if (thumb_w < ctx->style.global_scrollbar_thumb_min) thumb_w = ctx->style.global_scrollbar_thumb_min;
8701 float max_scroll = ctx->gui_bounds_w - view_w;
8702 float pos_ratio = (max_scroll > 0) ? ctx->global_scroll_x / max_scroll : 0;
8703 float thumb_x = sb_x + pos_ratio * (sb_w - thumb_w);
8704 al_draw_filled_rounded_rectangle(thumb_x, sb_y + ctx->style.global_scrollbar_thumb_padding, thumb_x + thumb_w,
8707 al_draw_rounded_rectangle(thumb_x, sb_y + ctx->style.global_scrollbar_thumb_padding, thumb_x + thumb_w,
8710 }
8711
8712 /* tooltip bubble: after the pointer rested on a tooltip widget (main-display pass;
8713 * a widget on a detached native window is drawn in that window's pass instead) */
8714 _draw_tooltip(ctx);
8715
8716 al_use_transform(&prev_tf);
8717
8718 /* The current state is now on the back buffer, so clear the dirty bit. Done
8719 * last, after the draw-time adjustments above (scroll clamping, autofit,
8720 * bounds), so those are reflected in this frame. n_gui_needs_redraw stays
8721 * true while an animation is pending or new input/state arrives. */
8722 ctx->dirty = 0;
8723}
8724
8738 __n_assert(ctx, return);
8739 __n_assert(ctx->windows, return);
8740
8741 ALLEGRO_BITMAP* prev_target = al_get_target_bitmap();
8742
8743 /* Deferred release pass, before drawing so a window closed this frame does
8744 * not get one last frame painted into a display that is about to go away.
8745 * Collected first because _release_native_window can pump the platform
8746 * event loop, which must not happen while iterating the window list. */
8747 int pending = 0;
8748 list_foreach(cnode, ctx->windows) {
8749 const N_GUI_WINDOW* cwin = (const N_GUI_WINDOW*)cnode->ptr;
8750 if (cwin && cwin->native && cwin->native_close_pending) pending = 1;
8751 }
8752 while (pending) {
8753 N_GUI_WINDOW* victim = NULL;
8754 pending = 0;
8755 list_foreach(cnode, ctx->windows) {
8756 N_GUI_WINDOW* cwin = (N_GUI_WINDOW*)cnode->ptr;
8757 if (cwin && cwin->native && cwin->native_close_pending) {
8758 victim = cwin;
8759 break;
8760 }
8761 }
8762 if (!victim) break;
8763 /* keep the pop-up geometry the window will fall back to, and let
8764 * want_native survive so re-opening restores the native window */
8765 int keep_want = victim->want_native;
8766 _release_native_window(ctx, victim);
8767 victim->x = victim->saved_x;
8768 victim->y = victim->saved_y;
8769 victim->w = victim->saved_w;
8770 victim->h = victim->saved_h;
8771 victim->flags = victim->saved_flags;
8772 victim->want_native = keep_want;
8773 list_foreach(cnode, ctx->windows) {
8774 const N_GUI_WINDOW* cwin = (const N_GUI_WINDOW*)cnode->ptr;
8775 if (cwin && cwin->native && cwin->native_close_pending) pending = 1;
8776 }
8777 }
8778
8779 list_foreach(node, ctx->windows) {
8780 N_GUI_WINDOW* win = (N_GUI_WINDOW*)node->ptr;
8781 if (!win || !win->native) continue;
8782 if (!(win->state & N_GUI_WIN_OPEN)) continue;
8783 /* the OS took the surface away (Android), drawing into it is undefined */
8784 if (win->native_halted) continue;
8785 /* the window manager iconified the window: ALLEGRO_MINIMIZED is
8786 * read-only, so this is the only way to know, and there is nothing to
8787 * paint into a display the user cannot see */
8788 if (al_get_display_flags(win->native) & ALLEGRO_MINIMIZED) continue;
8789
8790 al_set_target_backbuffer(win->native);
8791
8792 ALLEGRO_TRANSFORM id_tf, prev_tf;
8793 al_copy_transform(&prev_tf, al_get_current_transform());
8794 al_identity_transform(&id_tf);
8795 al_use_transform(&id_tf);
8796
8797 al_clear_to_color(win->theme.bg_normal);
8798
8799 ctx->pass_display = win->native;
8800 if (win->autofit_flags) {
8801 n_gui_window_apply_autofit(ctx, win->id);
8802 }
8803 _draw_window(win, ctx->default_font, &ctx->style);
8804 if (win->on_content_draw && !(win->state & N_GUI_WIN_MINIMISED)) {
8805 win->on_content_draw(win->id, win->on_content_draw_data);
8806 }
8807 /* an open combobox/dropmenu overlay, and a tooltip for a widget on this
8808 * detached window, draw on the display they live on */
8811 _draw_tooltip(ctx);
8812 ctx->pass_display = NULL;
8813
8814 al_use_transform(&prev_tf);
8815 al_flip_display();
8816 }
8817
8818 if (prev_target) al_set_target_bitmap(prev_target);
8819}
8820
8821/* VIRTUAL CANVAS / DISPLAY / DPI */
8822
8831void n_gui_set_virtual_size(N_GUI_CTX* ctx, float w, float h) {
8832 __n_assert(ctx, return);
8833 if ((w > 0) != (h > 0)) {
8834 n_log(LOG_ERR, "n_gui_set_virtual_size: both dimensions must be > 0 (got w=%.1f, h=%.1f); virtual canvas not changed", w, h);
8835 return;
8836 }
8837 ctx->virtual_w = w;
8838 ctx->virtual_h = h;
8840}
8841
8850 __n_assert(ctx, return);
8851
8852 if (ctx->virtual_w <= 0 || ctx->virtual_h <= 0 ||
8853 ctx->display_w <= 0 || ctx->display_h <= 0) {
8854 ctx->gui_scale = 1.0f;
8855 ctx->gui_offset_x = 0.0f;
8856 ctx->gui_offset_y = 0.0f;
8857 return;
8858 }
8859
8860 float sx = ctx->display_w / ctx->virtual_w;
8861 float sy = ctx->display_h / ctx->virtual_h;
8862 ctx->gui_scale = (sx < sy) ? sx : sy;
8863 if (ctx->gui_scale < 0.01f) ctx->gui_scale = 0.01f;
8864
8865 float scaled_w = ctx->virtual_w * ctx->gui_scale;
8866 float scaled_h = ctx->virtual_h * ctx->gui_scale;
8867 ctx->gui_offset_x = (ctx->display_w - scaled_w) * 0.5f;
8868 ctx->gui_offset_y = (ctx->display_h - scaled_h) * 0.5f;
8869}
8870
8876void n_gui_screen_to_virtual(const N_GUI_CTX* ctx, float sx, float sy, float* vx, float* vy) {
8877 __n_assert(ctx, return);
8878
8879 if (ctx->virtual_w <= 0 || ctx->virtual_h <= 0 || ctx->gui_scale <= 0) {
8880 if (vx) *vx = sx;
8881 if (vy) *vy = sy;
8882 return;
8883 }
8884
8885 float scale = ctx->gui_scale;
8886 if (vx) *vx = (sx - ctx->gui_offset_x) / scale;
8887 if (vy) *vy = (sy - ctx->gui_offset_y) / scale;
8888}
8889
8890/* ADAPTIVE RESIZE PUBLIC API */
8891
8898 __n_assert(ctx, return);
8899 ctx->resize_mode = mode;
8900
8901 if (mode == N_GUI_RESIZE_ADAPTIVE) {
8902 /* disable virtual canvas scaling, virtual tracks display */
8903 ctx->virtual_w = 0;
8904 ctx->virtual_h = 0;
8905 ctx->gui_scale = 1.0f;
8906 ctx->gui_offset_x = 0.0f;
8907 ctx->gui_offset_y = 0.0f;
8908
8909 /* set reference display size */
8910 ctx->ref_display_w = ctx->display_w;
8911 ctx->ref_display_h = ctx->display_h;
8912
8913 /* capture normalized coords for all windows */
8914 list_foreach(wnode, ctx->windows) {
8915 N_GUI_WINDOW* win = (N_GUI_WINDOW*)wnode->ptr;
8917 }
8918 }
8919}
8920
8925// cppcheck-suppress constParameterPointer ; public API uses non-const for consistency
8927 __n_assert(ctx, return 0);
8928 return ctx->resize_mode;
8929}
8930
8936void n_gui_window_set_resize_policy(N_GUI_CTX* ctx, int window_id, int policy) {
8937 __n_assert(ctx, return);
8938 N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
8939 if (!win) return;
8940 win->resize_policy = policy;
8942}
8943
8948// cppcheck-suppress constParameterPointer ; public API uses non-const for consistency
8950 __n_assert(ctx, return 0);
8951 // cppcheck-suppress constVariablePointer ; returned from non-const API
8952 N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
8953 if (!win) return 0;
8954 return win->resize_policy;
8955}
8956
8963 __n_assert(ctx, return);
8964 N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
8965 if (!win) return;
8966
8967 /* update reference to current display size before recapture */
8968 ctx->ref_display_w = ctx->display_w;
8969 ctx->ref_display_h = ctx->display_h;
8971}
8972
8978void n_gui_apply_adaptive_resize(N_GUI_CTX* ctx, float new_w, float new_h) {
8979 __n_assert(ctx, return);
8980 if (new_w <= 0 || new_h <= 0) return;
8981
8982 list_foreach(wnode, ctx->windows) {
8983 N_GUI_WINDOW* win = (N_GUI_WINDOW*)wnode->ptr;
8984 /* a detached window is sized by its own OS window, not by the main
8985 * display: resizing it here would fight the window manager */
8986 if (win->native) continue;
8987
8988 switch (win->resize_policy) {
8990 win->x = win->norm_x * new_w;
8991 win->y = win->norm_y * new_h;
8992 /* size unchanged */
8993 break;
8994
8996 win->x = win->norm_x * new_w;
8997 win->y = win->norm_y * new_h;
8998 win->w = win->norm_w * new_w;
8999 win->h = win->norm_h * new_h;
9000
9001 /* enforce minimums only on user-resizable (non-frameless) windows */
9002 if (!(win->flags & N_GUI_WIN_FRAMELESS)) {
9003 if (win->w < win->min_w) win->w = win->min_w;
9004 if (win->h < win->min_h) win->h = win->min_h;
9005 }
9006
9007 /* scale child widgets proportionally to new window size */
9008 list_foreach(wgn, win->widgets) {
9009 N_GUI_WIDGET* wgt = (N_GUI_WIDGET*)wgn->ptr;
9010 wgt->x = wgt->norm_x * win->w;
9011 wgt->y = wgt->norm_y * win->h;
9012 wgt->w = wgt->norm_w * win->w;
9013 wgt->h = wgt->norm_h * win->h;
9014 }
9015 break;
9016 }
9017
9019 default:
9020 /* do nothing */
9021 break;
9022 }
9023 }
9024
9025 /* keep ref_display_w/h at the ORIGINAL creation size so norms remain
9026 stable across multiple resizes (always relative to initial layout) */
9027}
9028
9034void n_gui_set_display_size(N_GUI_CTX* ctx, float w, float h) {
9035 __n_assert(ctx, return);
9036 ctx->display_w = w;
9037 ctx->display_h = h;
9038 if (ctx->resize_mode == N_GUI_RESIZE_ADAPTIVE) {
9039 n_gui_apply_adaptive_resize(ctx, w, h);
9040 } else {
9041 if (ctx->virtual_w > 0 && ctx->virtual_h > 0) {
9043 }
9044 }
9045}
9046
9054void n_gui_set_shape_mode(N_GUI_CTX* ctx, int shape_mode) {
9055 __n_assert(ctx, return);
9057}
9058
9065 return ctx ? ctx->style.shape_mode : N_GUI_SHAPE_ROUNDED;
9066}
9067
9071void n_gui_set_display(N_GUI_CTX* ctx, ALLEGRO_DISPLAY* display) {
9072 __n_assert(ctx, return);
9073 ctx->display = display;
9074}
9075
9079void n_gui_set_dpi_scale(N_GUI_CTX* ctx, float scale) {
9080 __n_assert(ctx, return);
9081 if (scale < 0.25f) scale = 0.25f;
9082 if (scale > 8.0f) scale = 8.0f;
9083 ctx->dpi_scale = scale;
9084}
9085
9090 __n_assert(ctx, return 1.0f);
9091 return ctx->dpi_scale;
9092}
9093
9113float n_gui_detect_dpi_scale(N_GUI_CTX* ctx, ALLEGRO_DISPLAY* display) {
9114 __n_assert(ctx, return 1.0f);
9115 __n_assert(display, return 1.0f);
9116
9117 float scale = 1.0f;
9118
9119 /* Method: compare physical pixel size to logical window size.
9120 * On HiDPI displays, al_get_display_width returns the logical size while
9121 * the backbuffer bitmap has the physical pixel size. */
9122 int logical_w = al_get_display_width(display);
9123 int logical_h = al_get_display_height(display);
9124 ALLEGRO_BITMAP* backbuffer = al_get_backbuffer(display);
9125 if (backbuffer && logical_w > 0 && logical_h > 0) {
9126 int physical_w = al_get_bitmap_width(backbuffer);
9127 int physical_h = al_get_bitmap_height(backbuffer);
9128 float scale_x = (float)physical_w / (float)logical_w;
9129 float scale_y = (float)physical_h / (float)logical_h;
9130 scale = (scale_x > scale_y) ? scale_x : scale_y;
9131 if (scale < 0.5f) scale = 1.0f; /* sanity */
9132 }
9133
9134#ifdef ALLEGRO_ANDROID
9135 /* On Android, use the display DPI option if available.
9136 * Android standard baseline is 160 DPI; scale = actual_dpi / 160. */
9137 int dpi = al_get_display_option(display, ALLEGRO_DEFAULT_DISPLAY_ADAPTER);
9138 if (dpi <= 0) {
9139 /* fallback: try the pixel ratio we already computed */
9140 } else {
9141 float android_scale = (float)dpi / 160.0f;
9142 if (android_scale >= 0.5f) scale = android_scale;
9143 }
9144#endif
9145
9146 if (scale < 0.25f) scale = 0.25f;
9147 if (scale > 8.0f) scale = 8.0f;
9148 ctx->dpi_scale = scale;
9149 return scale;
9150}
9151
9152/* EVENT PROCESSING */
9153
9155static void _slider_update_from_mouse(N_GUI_WIDGET* wgt, float mx, float my, float win_x, float win_content_y) {
9157 if (wgt->w <= 0) return;
9158 if (wgt->h <= 0) return;
9159 double ratio;
9160
9161 if (sd->orientation == N_GUI_SLIDER_V) {
9162 float ay = win_content_y + wgt->y;
9163 float local_y = my - ay;
9164 /* vertical slider: bottom = max, top = min, so invert */
9165 ratio = 1.0 - (double)local_y / (double)wgt->h;
9166 } else {
9167 float ax = win_x + wgt->x;
9168 float local_x = mx - ax;
9169 ratio = (double)local_x / (double)wgt->w;
9170 }
9171 if (ratio < 0.0) ratio = 0.0;
9172 if (ratio > 1.0) ratio = 1.0;
9173 double new_val = sd->min_val + ratio * (sd->max_val - sd->min_val);
9174 if (sd->step > 0.0)
9175 new_val = _slider_snap_value(new_val, sd->min_val, sd->max_val, sd->step);
9176 else
9177 new_val = _clamp(new_val, sd->min_val, sd->max_val);
9178 if (new_val != sd->value) {
9179 sd->value = new_val;
9180 if (sd->on_change) {
9181 sd->on_change(wgt->id, sd->value, sd->user_data);
9182 }
9183 }
9184}
9185
9187static void _scrollbar_update_from_mouse(N_GUI_WIDGET* wgt, float mx, float my, float win_x, float win_content_y, const N_GUI_STYLE* style) {
9189 float ax = win_x + wgt->x;
9190 float ay = win_content_y + wgt->y;
9191
9192 double ratio_viewport = sb->viewport_size / sb->content_size;
9193 if (ratio_viewport > 1.0) ratio_viewport = 1.0;
9194 double max_scroll = sb->content_size - sb->viewport_size;
9195 if (max_scroll < 0) max_scroll = 0;
9196
9197 double pos_ratio;
9198 if (sb->orientation == N_GUI_SCROLLBAR_V) {
9199 float thumb_h = (float)(ratio_viewport * (double)wgt->h);
9200 if (thumb_h < style->scrollbar_thumb_min) thumb_h = style->scrollbar_thumb_min;
9201 float track_range = wgt->h - thumb_h;
9202 if (track_range <= 0) return;
9203 pos_ratio = (double)(my - ay - thumb_h / 2.0f) / (double)track_range;
9204 } else {
9205 float thumb_w = (float)(ratio_viewport * (double)wgt->w);
9206 if (thumb_w < style->scrollbar_thumb_min) thumb_w = style->scrollbar_thumb_min;
9207 float track_range = wgt->w - thumb_w;
9208 if (track_range <= 0) return;
9209 pos_ratio = (double)(mx - ax - thumb_w / 2.0f) / (double)track_range;
9210 }
9211 if (pos_ratio < 0.0) pos_ratio = 0.0;
9212 if (pos_ratio > 1.0) pos_ratio = 1.0;
9213 sb->scroll_pos = pos_ratio * max_scroll;
9214 if (sb->on_scroll) {
9215 sb->on_scroll(wgt->id, sb->scroll_pos, sb->user_data);
9216 }
9217}
9218
9220static int _utf8_encode(int cp, char* out) {
9221 if (cp < 0x80) {
9222 out[0] = (char)cp;
9223 return 1;
9224 }
9225 if (cp < 0x800) {
9226 out[0] = (char)(0xC0 | (cp >> 6));
9227 out[1] = (char)(0x80 | (cp & 0x3F));
9228 return 2;
9229 }
9230 if (cp < 0x10000) {
9231 out[0] = (char)(0xE0 | (cp >> 12));
9232 out[1] = (char)(0x80 | ((cp >> 6) & 0x3F));
9233 out[2] = (char)(0x80 | (cp & 0x3F));
9234 return 3;
9235 }
9236 if (cp < 0x110000) {
9237 out[0] = (char)(0xF0 | (cp >> 18));
9238 out[1] = (char)(0x80 | ((cp >> 12) & 0x3F));
9239 out[2] = (char)(0x80 | ((cp >> 6) & 0x3F));
9240 out[3] = (char)(0x80 | (cp & 0x3F));
9241 return 4;
9242 }
9243 return 0;
9244}
9245
9248 td->sel_start = td->cursor_pos;
9249 td->sel_end = td->cursor_pos;
9250}
9251
9255 if (!_textarea_has_selection(td)) return;
9256 size_t lo, hi;
9257 _textarea_sel_range(td, &lo, &hi);
9258 size_t del_len = hi - lo;
9259 memmove(&td->text[lo], &td->text[hi], td->text_len - hi + 1);
9260 td->text_len -= del_len;
9261 td->cursor_pos = lo;
9263 if (td->on_change) td->on_change(wgt->id, td->text, td->user_data);
9264}
9265
9267static int _textarea_copy_to_clipboard(const N_GUI_TEXTAREA_DATA* td, ALLEGRO_DISPLAY* display) {
9268 if (!display || !_textarea_has_selection(td)) return 0;
9269 size_t lo, hi;
9270 _textarea_sel_range(td, &lo, &hi);
9271 size_t len = hi - lo;
9272 char* tmp = NULL;
9273 Malloc(tmp, char, len + 1);
9274 if (!tmp) return 0;
9275 memcpy(tmp, &td->text[lo], len);
9276 tmp[len] = '\0';
9278 FreeNoLog(tmp);
9279 return 1;
9280}
9281
9285static void _textarea_paste_clip(N_GUI_WIDGET* wgt, N_GUI_TEXTAREA_DATA* td, const char* clip) {
9286 if (!clip) return;
9287 /* delete selection first if any */
9288 if (_textarea_has_selection(td)) {
9290 }
9291 size_t clip_len = strlen(clip);
9292 /* filter out non-printable except newline in multiline mode;
9293 * skip \r to normalize CRLF for Allegro5 compatibility */
9294 char filtered[N_GUI_TEXT_MAX];
9295 size_t flen = 0;
9296 for (size_t ci = 0; ci < clip_len && flen < N_GUI_TEXT_MAX - 1; ci++) {
9297 if (clip[ci] == '\r') {
9298 continue; /* strip CR, CRLF becomes just LF */
9299 } else if (clip[ci] == '\n') {
9300 if (td->multiline) filtered[flen++] = '\n';
9301 } else if ((unsigned char)clip[ci] >= 32 || ((unsigned char)clip[ci] & 0xC0) == 0x80) {
9302 filtered[flen++] = clip[ci];
9303 } else if (((unsigned char)clip[ci] & 0xC0) == 0xC0) {
9304 filtered[flen++] = clip[ci]; /* UTF-8 lead byte */
9305 }
9306 }
9307 filtered[flen] = '\0';
9308 if (flen > 0 && td->text_len + flen <= td->char_limit) {
9309 memmove(&td->text[td->cursor_pos + flen], &td->text[td->cursor_pos], td->text_len - td->cursor_pos + 1);
9310 memcpy(&td->text[td->cursor_pos], filtered, flen);
9311 td->cursor_pos += flen;
9312 td->text_len += flen;
9314 if (td->on_change) td->on_change(wgt->id, td->text, td->user_data);
9315 }
9316}
9317
9324 if (!ctx || !_ctx_io_display(ctx) || !n_clipboard_available()) return;
9325 char* sel = NULL;
9326 /* a syntax view holds its own selection id */
9327 if (ctx->selected_syntaxview_id >= 0) {
9329 }
9330 /* a label selection */
9331 if (!sel && ctx->selected_label_id >= 0) {
9333 if (lw && lw->type == N_GUI_TYPE_LABEL && lw->data) {
9335 if (lb->sel_start >= 0 && lb->sel_end >= 0 && lb->sel_start != lb->sel_end) {
9336 int slo = lb->sel_start < lb->sel_end ? lb->sel_start : lb->sel_end;
9337 int shi = lb->sel_start < lb->sel_end ? lb->sel_end : lb->sel_start;
9338 size_t tlen = strlen(lb->text);
9339 if ((size_t)slo > tlen) slo = (int)tlen;
9340 if ((size_t)shi > tlen) shi = (int)tlen;
9341 int len = shi - slo;
9342 if (len > 0) {
9343 Malloc(sel, char, (size_t)len + 1);
9344 if (sel) {
9345 memcpy(sel, &lb->text[slo], (size_t)len);
9346 sel[len] = '\0';
9347 }
9348 }
9349 }
9350 }
9351 }
9352 /* the focused textarea's selection */
9353 if (!sel && ctx->focused_widget_id >= 0) {
9354 const N_GUI_WIDGET* fw = n_gui_get_widget(ctx, ctx->focused_widget_id);
9355 if (fw && fw->type == N_GUI_TYPE_TEXTAREA && fw->data) {
9356 const N_GUI_TEXTAREA_DATA* td = (const N_GUI_TEXTAREA_DATA*)fw->data;
9357 if (_textarea_has_selection(td)) {
9358 size_t lo, hi;
9359 _textarea_sel_range(td, &lo, &hi);
9360 size_t len = hi - lo;
9361 Malloc(sel, char, len + 1);
9362 if (sel) {
9363 memcpy(sel, &td->text[lo], len);
9364 sel[len] = '\0';
9365 }
9366 }
9367 }
9368 }
9369 if (sel) {
9370 if (sel[0])
9372 Free(sel);
9373 }
9374}
9375
9379static int _textarea_handle_key(N_GUI_WIDGET* wgt, ALLEGRO_EVENT* ev, ALLEGRO_FONT* font, float pad, float sb_size, N_GUI_CTX* ctx) {
9381
9382 /* reset blink timer so cursor stays visible during typing */
9383 td->cursor_time = al_get_time();
9384 /* clear mouse-wheel scroll flag so auto-scroll resumes following the cursor */
9385 td->scroll_from_wheel = 0;
9386
9387 int shift = (ev->keyboard.modifiers & ALLEGRO_KEYMOD_SHIFT) ? 1 : 0;
9388 int ctrl = (ev->keyboard.modifiers & ALLEGRO_KEYMOD_CTRL) ? 1 : 0;
9389
9390 /* Ctrl+A: select all */
9391 if (ctrl && ev->keyboard.keycode == ALLEGRO_KEY_A) {
9392 td->sel_start = 0;
9393 td->sel_end = td->text_len;
9394 td->cursor_pos = td->text_len;
9395 _ngui_publish_primary_selection(ctx); /* select-to-copy: mirror the selection to PRIMARY */
9396 return 1;
9397 }
9398
9399 /* Ctrl+C: copy */
9400 if (ctrl && ev->keyboard.keycode == ALLEGRO_KEY_C) {
9401 if (ctx && _ctx_io_display(ctx)) {
9403 }
9404 return 1;
9405 }
9406
9407 /* Ctrl+X: cut */
9408 if (ctrl && ev->keyboard.keycode == ALLEGRO_KEY_X) {
9409 if (ctx && _ctx_io_display(ctx) && _textarea_has_selection(td)) {
9412 }
9413 return 1;
9414 }
9415
9416 /* Ctrl+V: paste */
9417 if (ctrl && ev->keyboard.keycode == ALLEGRO_KEY_V) {
9418 if (ctx && _ctx_io_display(ctx)) {
9420 if (clip) {
9421 _textarea_paste_clip(wgt, td, clip);
9422 al_free(clip);
9423 }
9424 }
9425 return 1;
9426 }
9427
9428 if (ev->keyboard.keycode == ALLEGRO_KEY_BACKSPACE) {
9429 if (_textarea_has_selection(td)) {
9431 } else if (td->cursor_pos > 0 && td->text_len > 0) {
9432 size_t erase_start = td->cursor_pos;
9433 int cont_count = 0;
9434 do {
9435 erase_start--;
9436 cont_count++;
9437 } while (erase_start > 0 && cont_count <= 3 && ((unsigned char)td->text[erase_start] & 0xC0) == 0x80);
9438 size_t erase_len = td->cursor_pos - erase_start;
9439 memmove(&td->text[erase_start], &td->text[td->cursor_pos], td->text_len - td->cursor_pos + 1);
9440 td->cursor_pos = erase_start;
9441 td->text_len -= erase_len;
9443 if (td->on_change) td->on_change(wgt->id, td->text, td->user_data);
9444 }
9445 return 1;
9446 }
9447 if (ev->keyboard.keycode == ALLEGRO_KEY_DELETE) {
9448 if (_textarea_has_selection(td)) {
9450 } else if (td->cursor_pos < td->text_len) {
9451 int clen = _utf8_char_len((unsigned char)td->text[td->cursor_pos]);
9452 if (td->cursor_pos + (size_t)clen > td->text_len) clen = (int)(td->text_len - td->cursor_pos);
9453 memmove(&td->text[td->cursor_pos], &td->text[td->cursor_pos + (size_t)clen], td->text_len - td->cursor_pos - (size_t)clen + 1);
9454 td->text_len -= (size_t)clen;
9456 if (td->on_change) td->on_change(wgt->id, td->text, td->user_data);
9457 }
9458 return 1;
9459 }
9460 if (ev->keyboard.keycode == ALLEGRO_KEY_LEFT) {
9461 if (!shift && _textarea_has_selection(td)) {
9462 /* collapse selection to left edge */
9463 size_t lo, hi;
9464 _textarea_sel_range(td, &lo, &hi);
9465 td->cursor_pos = lo;
9467 } else if (td->cursor_pos > 0) {
9468 if (shift && !_textarea_has_selection(td)) {
9469 td->sel_start = td->cursor_pos;
9470 }
9471 int skip = 0;
9472 do {
9473 td->cursor_pos--;
9474 } while (skip++ < 3 && td->cursor_pos > 0 && ((unsigned char)td->text[td->cursor_pos] & 0xC0) == 0x80);
9475 if (shift)
9476 td->sel_end = td->cursor_pos;
9477 else
9479 }
9480 return 1;
9481 }
9482 if (ev->keyboard.keycode == ALLEGRO_KEY_RIGHT) {
9483 if (!shift && _textarea_has_selection(td)) {
9484 /* collapse selection to right edge */
9485 size_t lo, hi;
9486 _textarea_sel_range(td, &lo, &hi);
9487 td->cursor_pos = hi;
9489 } else if (td->cursor_pos < td->text_len) {
9490 if (shift && !_textarea_has_selection(td)) {
9491 td->sel_start = td->cursor_pos;
9492 }
9493 int clen = _utf8_char_len((unsigned char)td->text[td->cursor_pos]);
9494 td->cursor_pos += (size_t)clen;
9495 if (td->cursor_pos > td->text_len) td->cursor_pos = td->text_len;
9496 if (shift)
9497 td->sel_end = td->cursor_pos;
9498 else
9500 }
9501 return 1;
9502 }
9503 if (ev->keyboard.keycode == ALLEGRO_KEY_HOME) {
9504 if (shift && !_textarea_has_selection(td)) {
9505 td->sel_start = td->cursor_pos;
9506 }
9507 td->cursor_pos = 0;
9508 if (shift)
9509 td->sel_end = td->cursor_pos;
9510 else
9512 return 1;
9513 }
9514 if (ev->keyboard.keycode == ALLEGRO_KEY_END) {
9515 if (shift && !_textarea_has_selection(td)) {
9516 td->sel_start = td->cursor_pos;
9517 }
9518 td->cursor_pos = td->text_len;
9519 if (shift)
9520 td->sel_end = td->cursor_pos;
9521 else
9523 return 1;
9524 }
9525
9526 /* UP/DOWN arrow keys: move cursor to previous/next visual line in multiline mode */
9527 if (ev->keyboard.keycode == ALLEGRO_KEY_UP || ev->keyboard.keycode == ALLEGRO_KEY_DOWN) {
9528 if (!td->multiline || !font) return 1;
9529 if (shift && !_textarea_has_selection(td)) {
9530 td->sel_start = td->cursor_pos;
9531 }
9532
9533 /* compute wrap width consistently with rendering: subtract scrollbar
9534 * width when content overflows, then subtract padding. */
9535 float base_wrap_w = wgt->w - pad * 2;
9536 float text_area_h = wgt->h - pad * 2;
9537 float content_h = _textarea_content_height(td, font, wgt->w, pad);
9538 float wrap_w = (content_h > text_area_h)
9539 ? (wgt->w - sb_size - pad * 2)
9540 : base_wrap_w;
9541
9542 /* pass 1: find the cursor's visual line number and x offset */
9543 float cx = 0;
9544 int line = 0;
9545 int cursor_line = 0;
9546 float cursor_x = 0;
9547
9548 for (size_t i = 0; i < td->text_len;) {
9549 if (i == td->cursor_pos) {
9550 cursor_line = line;
9551 cursor_x = cx;
9552 }
9553 if (td->text[i] == '\n') {
9554 cx = 0;
9555 line++;
9556 i++;
9557 continue;
9558 }
9559 int clen = _utf8_char_len((unsigned char)td->text[i]);
9560 if (i + (size_t)clen > td->text_len) clen = (int)(td->text_len - i);
9561 char ch[5];
9562 memcpy(ch, &td->text[i], (size_t)clen);
9563 ch[clen] = '\0';
9564 float cw = _text_w(font, ch);
9565 if (cx + cw > wrap_w) {
9566 cx = 0;
9567 line++;
9568 if (i == td->cursor_pos) {
9569 cursor_line = line;
9570 cursor_x = cx;
9571 }
9572 }
9573 cx += cw;
9574 i += (size_t)clen;
9575 }
9576 if (td->cursor_pos >= td->text_len) {
9577 cursor_line = line;
9578 cursor_x = cx;
9579 }
9580 int total_lines = line;
9581
9582 /* determine target line */
9583 int target_line;
9584 if (ev->keyboard.keycode == ALLEGRO_KEY_UP) {
9585 if (cursor_line <= 0) {
9586 td->cursor_pos = 0;
9587 return 1;
9588 }
9589 target_line = cursor_line - 1;
9590 } else {
9591 if (cursor_line >= total_lines) {
9592 td->cursor_pos = td->text_len;
9593 return 1;
9594 }
9595 target_line = cursor_line + 1;
9596 }
9597
9598 /* pass 2: find the byte position on target_line closest to cursor_x */
9599 cx = 0;
9600 line = 0;
9601 size_t best_pos = 0;
9602 float best_dist = 1e9f;
9603
9604 /* check position 0 */
9605 if (line == target_line) {
9606 best_dist = (cursor_x >= 0) ? cursor_x : -cursor_x;
9607 best_pos = 0;
9608 }
9609
9610 for (size_t i = 0; i < td->text_len;) {
9611 if (td->text[i] == '\n') {
9612 if (line >= target_line) break;
9613 cx = 0;
9614 line++;
9615 if (line == target_line) {
9616 float dist = (cursor_x >= cx) ? (cursor_x - cx) : (cx - cursor_x);
9617 if (dist < best_dist) {
9618 best_dist = dist;
9619 best_pos = i + 1;
9620 }
9621 }
9622 i++;
9623 continue;
9624 }
9625 int clen = _utf8_char_len((unsigned char)td->text[i]);
9626 if (i + (size_t)clen > td->text_len) clen = (int)(td->text_len - i);
9627 char ch[5];
9628 memcpy(ch, &td->text[i], (size_t)clen);
9629 ch[clen] = '\0';
9630 float cw = _text_w(font, ch);
9631 if (cx + cw > wrap_w) {
9632 if (line >= target_line) break;
9633 cx = 0;
9634 line++;
9635 if (line == target_line) {
9636 float dist = (cursor_x >= cx) ? (cursor_x - cx) : (cx - cursor_x);
9637 if (dist < best_dist) {
9638 best_dist = dist;
9639 best_pos = i;
9640 }
9641 }
9642 }
9643 cx += cw;
9644 /* check position after this character */
9645 if (line == target_line) {
9646 float dist = (cursor_x >= cx) ? (cursor_x - cx) : (cx - cursor_x);
9647 if (dist < best_dist) {
9648 best_dist = dist;
9649 best_pos = i + (size_t)clen;
9650 if (best_pos > td->text_len) best_pos = td->text_len;
9651 }
9652 }
9653 i += (size_t)clen;
9654 }
9655
9656 td->cursor_pos = best_pos;
9657 if (shift)
9658 td->sel_end = td->cursor_pos;
9659 else
9661 return 1;
9662 }
9663
9664 if (ev->keyboard.keycode == ALLEGRO_KEY_ENTER) {
9665 if (td->multiline) {
9666 if (_textarea_has_selection(td)) {
9668 }
9669 if (td->text_len < td->char_limit) {
9670 memmove(&td->text[td->cursor_pos + 1], &td->text[td->cursor_pos], td->text_len - td->cursor_pos + 1);
9671 td->text[td->cursor_pos] = '\n';
9672 td->cursor_pos++;
9673 td->text_len++;
9675 if (td->on_change) td->on_change(wgt->id, td->text, td->user_data);
9676 }
9677 }
9678 /* in single-line mode, let ENTER pass through to button keybinds */
9679 return td->multiline ? 1 : 0;
9680 }
9681
9682 /* printable character: encode Unicode code point as UTF-8 */
9683 if (ev->keyboard.unichar >= 32) {
9684 /* delete selection first */
9685 if (_textarea_has_selection(td)) {
9687 }
9688 char utf8[4];
9689 int utf8_len = _utf8_encode(ev->keyboard.unichar, utf8);
9690 if (utf8_len > 0 && td->text_len + (size_t)utf8_len <= td->char_limit) {
9691 memmove(&td->text[td->cursor_pos + (size_t)utf8_len], &td->text[td->cursor_pos], td->text_len - td->cursor_pos + 1);
9692 memcpy(&td->text[td->cursor_pos], utf8, (size_t)utf8_len);
9693 td->cursor_pos += (size_t)utf8_len;
9694 td->text_len += (size_t)utf8_len;
9696 if (td->on_change) td->on_change(wgt->id, td->text, td->user_data);
9697 }
9698 }
9699 return 1;
9700}
9701
9709static void _tb_button_action(N_GUI_CTX* ctx, N_GUI_WINDOW* win, int btn_type) {
9710 /* An own-chrome window draws these buttons instead of the window manager, so
9711 * closing has to take the OS window with it: otherwise the content is hidden and
9712 * an undismissable OS window is left on screen. Minimise and maximise already
9713 * mirror themselves onto the native window (see _native_apply_minimised). */
9714 int own_native = (win->native != NULL) && _native_own_chrome(win);
9715
9716 if (btn_type == N_GUI_TB_BTN_MINIMIZE) {
9717 if (win->tb_buttons.on_minimize)
9719 else
9720 n_gui_minimize_window(ctx, win->id); /* mirrors onto the OS window itself */
9721 } else if (btn_type == N_GUI_TB_BTN_MAXIMIZE) {
9722 if (win->tb_buttons.on_maximize)
9724 else
9725 n_gui_maximize_window(ctx, win->id);
9726 } else if (btn_type == N_GUI_TB_BTN_CLOSE) {
9727 if (win->tb_buttons.on_close)
9729 else
9730 n_gui_close_window(ctx, win->id);
9731 /* Release the OS window too, on the same deferred path a window-manager
9732 * close request takes. Only when the window actually ended up closed, so a
9733 * close callback that keeps it open (a confirmation, say) still wins. */
9734 if (own_native && !(win->state & N_GUI_WIN_OPEN))
9735 win->native_close_pending = 1;
9736 }
9737}
9738
9749static void _list_panel_highlight_step(int* highlight, int* scroll, int nb, int max_visible, int delta, int fallback) {
9750 int hi;
9751 if (!highlight || !scroll || nb <= 0)
9752 return;
9753 hi = *highlight;
9754 if (hi < 0) {
9755 /* nothing highlighted yet: seed so the first step lands on the natural item
9756 (a downward step -> the first item, an upward step -> the last, or the
9757 given fallback such as the combobox's selected value) */
9758 if (fallback >= 0)
9759 hi = fallback;
9760 else
9761 hi = (delta >= 0) ? -1 : nb;
9762 }
9763 hi += delta;
9764 if (hi < 0)
9765 hi = 0;
9766 if (hi >= nb)
9767 hi = nb - 1;
9768 *highlight = hi;
9769 /* keep the highlighted item within the visible window */
9770 if (hi < *scroll)
9771 *scroll = hi;
9772 else if (max_visible > 0 && hi >= *scroll + max_visible)
9773 *scroll = hi - max_visible + 1;
9774 if (*scroll < 0)
9775 *scroll = 0;
9776}
9777
9783static int _datagrid_resize_hover(N_GUI_CTX* ctx, float px, float py) {
9784 int over = 0;
9785 list_foreach(wnode, ctx->windows) {
9786 N_GUI_WINDOW* win = (N_GUI_WINDOW*)wnode->ptr;
9787 float ox, oy;
9788 if (!win || !(win->state & N_GUI_WIN_OPEN)) continue;
9789 if (!_win_on_pass(ctx, win)) continue;
9790 ox = win->x - win->scroll_x;
9791 oy = win->y + _win_tbh(win) - win->scroll_y;
9792 list_foreach(wgn, win->widgets) {
9793 const N_GUI_WIDGET* wgt = (const N_GUI_WIDGET*)wgn->ptr;
9794 const N_GUI_DATAGRID_DATA* gd;
9795 ALLEGRO_FONT* font;
9796 float ax, ay, fh, row_h;
9797 if (!wgt || !wgt->visible || wgt->type != N_GUI_TYPE_DATAGRID || !wgt->data) continue;
9798 ax = ox + wgt->x;
9799 ay = oy + wgt->y;
9800 if (!_point_in_rect(px, py, ax, ay, wgt->w, wgt->h)) continue;
9801 gd = (const N_GUI_DATAGRID_DATA*)wgt->data;
9802 font = wgt->font ? wgt->font : ctx->default_font;
9803 fh = font ? (float)al_get_font_line_height(font) : 16.0f;
9804 row_h = fh + ctx->style.item_height_pad;
9805 /* this (topmost so far) grid claims the point: its verdict overrides a
9806 lower grid's, whether or not the point lands on a border */
9807 over = 0;
9808 if (py < ay + row_h) { /* header row only */
9809 float bx = ax - gd->h_scroll;
9810 for (size_t c = 0; c < gd->nb_cols; c++) {
9811 size_t pc = _datagrid_phys(gd, c);
9812 float right;
9813 if (!gd->cols[pc].visible) continue;
9814 right = bx + gd->cols[pc].width;
9815 if (px >= right - 4.0f && px <= right + 4.0f) {
9816 over = 1;
9817 break;
9818 }
9819 bx = right;
9820 }
9821 }
9822 }
9823 }
9824 return over;
9825}
9826
9836int n_gui_process_event(N_GUI_CTX* ctx, ALLEGRO_EVENT event) {
9837 __n_assert(ctx, return 0);
9838
9839 int event_consumed = 0;
9840
9841 /* Display routing. Resolve which pass this event belongs to: a detached
9842 * window's display, or the main one (NULL, which also covers events that
9843 * name no display and the headless tests that synthesize events by hand).
9844 * Windows not on the pass are skipped by every hit test below, so a click
9845 * on a native window cannot reach a pop-up that happens to sit at the same
9846 * coordinates on the main display. Allegro already delivers mouse
9847 * coordinates relative to the display the event came from, and a detached
9848 * window sits at (0,0) in its own display, so the coordinate maths in the
9849 * hit tests is the same for both kinds of pass. */
9850 {
9851 ALLEGRO_DISPLAY* src = _event_display(&event);
9852 ctx->pass_display = NULL;
9853 if (src && src != ctx->display) {
9854 list_foreach(pnode, ctx->windows) {
9855 const N_GUI_WINDOW* pwin = (const N_GUI_WINDOW*)pnode->ptr;
9856 if (pwin && pwin->native == src) {
9857 ctx->pass_display = src;
9858 break;
9859 }
9860 }
9861 }
9862 /* the display that owns the input drives clipboard and cursor calls */
9863 if (src) ctx->active_display = src;
9864 }
9865
9866 /* Redraw bookkeeping for n_gui_needs_redraw: these event types can change
9867 * what is drawn (a click/keystroke changes hover, focus, selection, scroll,
9868 * or a widget value through a callback; a display resize/expose changes
9869 * geometry). Motion is handled in its own block below, where the
9870 * over-window test is already computed, so idle movement over the game
9871 * background does not force a redraw. */
9872 switch (event.type) {
9873 case ALLEGRO_EVENT_MOUSE_BUTTON_DOWN:
9874 case ALLEGRO_EVENT_MOUSE_BUTTON_UP:
9875 case ALLEGRO_EVENT_KEY_DOWN:
9876 case ALLEGRO_EVENT_KEY_CHAR:
9877 case ALLEGRO_EVENT_KEY_UP:
9878 case ALLEGRO_EVENT_MOUSE_WARPED:
9879 case ALLEGRO_EVENT_DISPLAY_RESIZE:
9880 case ALLEGRO_EVENT_DISPLAY_EXPOSE:
9881 case ALLEGRO_EVENT_DISPLAY_CLOSE:
9882 case ALLEGRO_EVENT_DISPLAY_SWITCH_IN:
9883 case ALLEGRO_EVENT_DISPLAY_SWITCH_OUT:
9884 case ALLEGRO_EVENT_DISPLAY_HALT_DRAWING:
9885 case ALLEGRO_EVENT_DISPLAY_RESUME_DRAWING:
9886 ctx->dirty = 1;
9887 break;
9888 default:
9889 break;
9890 }
9891
9892 /* Native window events. The window manager owns a detached window's frame,
9893 * so a close request, a resize and an acknowledge all arrive here rather
9894 * than through our own title bar buttons. */
9895 if (ctx->pass_display) {
9896 N_GUI_WINDOW* nwin = NULL;
9897 list_foreach(nnode, ctx->windows) {
9898 N_GUI_WINDOW* w = (N_GUI_WINDOW*)nnode->ptr;
9899 if (w && w->native == ctx->pass_display) {
9900 nwin = w;
9901 break;
9902 }
9903 }
9904 if (nwin && event.type == ALLEGRO_EVENT_DISPLAY_CLOSE) {
9905 /* Same contract as the pop-up close button: a caller-supplied
9906 * callback decides what happens, the default is to close the
9907 * window. Either way the display itself is released at the next
9908 * n_gui_draw_detached, never from inside event processing. */
9909 if (nwin->tb_buttons.on_close) {
9911 /* the callback may have closed, re-attached or destroyed
9912 * nothing at all: only force the release when it left the
9913 * window both open and detached */
9914 if (nwin->native && (nwin->state & N_GUI_WIN_OPEN)) {
9915 nwin->state &= ~N_GUI_WIN_OPEN;
9916 nwin->native_close_pending = 1;
9917 }
9918 } else {
9919 nwin->state &= ~N_GUI_WIN_OPEN;
9920 nwin->native_close_pending = 1;
9921 }
9922 ctx->pass_display = NULL;
9923 return 1;
9924 }
9925 /* Drawing halt / resume. Mandatory on Android (and harmless elsewhere):
9926 * the OS takes the surface away and Allegro requires the acknowledge
9927 * before it will release it. Drawing into a halted display is undefined,
9928 * so n_gui_draw_detached skips the window until it resumes. */
9929 if (nwin && event.type == ALLEGRO_EVENT_DISPLAY_HALT_DRAWING) {
9930 nwin->native_halted = 1;
9931 al_acknowledge_drawing_halt(nwin->native);
9932 ctx->pass_display = NULL;
9933 return 1;
9934 }
9935 if (nwin && event.type == ALLEGRO_EVENT_DISPLAY_RESUME_DRAWING) {
9936 al_acknowledge_drawing_resume(nwin->native);
9937 nwin->native_halted = 0;
9938 ctx->pass_display = NULL;
9939 return 1;
9940 }
9941 if (nwin && event.type == ALLEGRO_EVENT_DISPLAY_RESIZE) {
9942 al_acknowledge_resize(nwin->native);
9943 float new_w = (float)al_get_display_width(nwin->native);
9944 float new_h = (float)al_get_display_height(nwin->native);
9945 /* A minimised own-chrome window is deliberately shrunk to its title
9946 * bar, so this resize does not describe the window's real size.
9947 * Take the width but leave the height alone, it is what the window
9948 * grows back to (see _native_apply_minimised). */
9949 if ((nwin->state & N_GUI_WIN_MINIMISED) && _native_own_chrome(nwin)) {
9950 if (new_w > 0.0f) {
9951 nwin->w = new_w;
9952 nwin->native_w = new_w;
9953 }
9954 ctx->pass_display = NULL;
9955 return 1;
9956 }
9957 if (new_w > 0.0f && new_h > 0.0f) {
9958 if ((nwin->detach_flags & N_GUI_DETACH_SCALE_CONTENT) && nwin->w > 0.0f && nwin->h > 0.0f) {
9959 list_foreach(wgn, nwin->widgets) {
9960 N_GUI_WIDGET* wgt = (N_GUI_WIDGET*)wgn->ptr;
9961 if (!wgt) continue;
9962 wgt->x = wgt->norm_x * new_w;
9963 wgt->y = wgt->norm_y * new_h;
9964 wgt->w = wgt->norm_w * new_w;
9965 wgt->h = wgt->norm_h * new_h;
9966 }
9967 }
9968 nwin->w = new_w;
9969 nwin->h = new_h;
9970 nwin->native_w = new_w;
9971 nwin->native_h = new_h;
9972 }
9973 ctx->pass_display = NULL;
9974 return 1;
9975 }
9976 }
9977
9978 /* When the display loses focus (e.g. OS window is moved to another monitor via the
9979 * title bar, or another window steals focus), examples typically call
9980 * al_flush_event_queue() which discards any pending MOUSE_BUTTON_UP events.
9981 * Without resetting our state here, the GUI would keep its drag/resize flags set
9982 * and continue moving/resizing windows on the next MOUSE_AXES event, which is the
9983 * root cause of GUI windows being unintentionally resized when the OS window is
9984 * moved between monitors. Scoped to the pass so switching away from a native
9985 * window does not cancel a drag in progress on the main display. */
9986 if (event.type == ALLEGRO_EVENT_DISPLAY_SWITCH_OUT) {
9987 ctx->mouse_b1 = 0;
9988 ctx->mouse_b1_prev = 0;
9989 list_foreach(wnode, ctx->windows) {
9990 N_GUI_WINDOW* win = (N_GUI_WINDOW*)wnode->ptr;
9991 if (!_win_on_pass(ctx, win)) continue;
9993 win->native_drag_anchored = 0;
9996 }
9997 ctx->global_vscroll_drag = 0;
9998 ctx->global_hscroll_drag = 0;
9999 ctx->pass_display = NULL;
10000 return 0;
10001 }
10002
10003 /* track mouse */
10004 if (event.type == ALLEGRO_EVENT_MOUSE_AXES) {
10005 ctx->mouse_x = event.mouse.x;
10006 ctx->mouse_y = event.mouse.y;
10007 /* Over-window test, computed once and shared by the tooltip and
10008 * datagrid-cursor scans below. Both scans only ever match widgets that
10009 * live inside a window, so when the pointer is over empty background
10010 * (the common case for a GUI composited over a game, at the mouse
10011 * sample rate) they would each walk every widget just to return
10012 * "nothing". Skipping them off-window is the same result for O(windows)
10013 * instead of O(widgets). Only needed when no button is held (both scans
10014 * are already gated on that). */
10015 int over_win = (!ctx->mouse_b1) ? _pointer_over_window(ctx, (float)ctx->mouse_x, (float)ctx->mouse_y) : 0;
10016 /* redraw bookkeeping: a motion changes what is drawn only when it is
10017 * over a window (hover / tooltip / cursor), during a drag, on a wheel
10018 * tick, or when it just left the GUI (the hover state must be cleared
10019 * one last time). Motion over the game background otherwise leaves the
10020 * GUI unchanged, so an idle event-driven host is not forced to redraw. */
10021 if (over_win || ctx->mouse_b1 || ctx->prev_over_win || event.mouse.dz != 0 || event.mouse.dw != 0)
10022 ctx->dirty = 1;
10023 if (!ctx->mouse_b1) ctx->prev_over_win = over_win;
10024 /* tooltip arming: track the hovered tooltip widget; any movement
10025 * re-arms the delay so the bubble shows once the pointer rests.
10026 * Disabled hosts (style.tooltip_delay <= 0, e.g. touch screens where
10027 * the emulated cursor parks on the last tap forever) and held-button
10028 * drags skip the whole widget scan - it would run at the touch
10029 * sample rate in the middle of a stroke for nothing */
10030 if (ctx->style.tooltip_delay > 0.0f && !ctx->mouse_b1) {
10031 int tip_id = over_win ? _tooltip_widget_at(ctx, (float)ctx->mouse_x, (float)ctx->mouse_y) : -1;
10032 int moved = (abs(ctx->mouse_x - ctx->tooltip_anchor_x) > 2) || (abs(ctx->mouse_y - ctx->tooltip_anchor_y) > 2);
10033 if (tip_id != ctx->tooltip_widget_id || moved) {
10034 ctx->tooltip_widget_id = tip_id;
10035 ctx->tooltip_armed_at = al_get_time();
10036 ctx->tooltip_anchor_x = ctx->mouse_x;
10037 ctx->tooltip_anchor_y = ctx->mouse_y;
10038 /* keep needs_redraw true until the bubble is due to appear, so an
10039 * idle host still draws the reveal frame once the pointer rests */
10040 if (tip_id >= 0) {
10041 double due = ctx->tooltip_armed_at + (double)ctx->style.tooltip_delay + N_GUI_ANIM_TAIL_SEC;
10042 if (due > ctx->anim_until) ctx->anim_until = due;
10043 }
10044 }
10045 } else if (ctx->tooltip_widget_id >= 0) {
10046 ctx->tooltip_widget_id = -1;
10047 }
10048 /* discoverability: show a horizontal-resize cursor while the pointer rests on a
10049 datagrid column border (where a drag resizes the column), and the default
10050 cursor elsewhere. Skipped while a button is held so a resize/scroll drag keeps
10051 the cursor it started with. Only the applied shape is re-set, to avoid churn. */
10052 if (_ctx_io_display(ctx) && !ctx->mouse_b1) {
10053 int want = (over_win && _datagrid_resize_hover(ctx, (float)ctx->mouse_x, (float)ctx->mouse_y))
10054 ? ALLEGRO_SYSTEM_MOUSE_CURSOR_RESIZE_E
10055 : ALLEGRO_SYSTEM_MOUSE_CURSOR_DEFAULT;
10056 if (want != ctx->cursor_shape) {
10057 al_set_system_mouse_cursor(_ctx_io_display(ctx), (ALLEGRO_SYSTEM_MOUSE_CURSOR)want);
10058 ctx->cursor_shape = want;
10059 }
10060 }
10061 }
10062 if (event.type == ALLEGRO_EVENT_MOUSE_BUTTON_DOWN && event.mouse.button == 1) {
10063 ctx->tooltip_widget_id = -1; /* a click dismisses the tooltip */
10064 ctx->mouse_b1_prev = ctx->mouse_b1;
10065 ctx->mouse_b1 = 1;
10066 ctx->mouse_x = event.mouse.x;
10067 ctx->mouse_y = event.mouse.y;
10068 }
10069 if (event.type == ALLEGRO_EVENT_MOUSE_BUTTON_UP && event.mouse.button == 1) {
10070 ctx->mouse_b1_prev = ctx->mouse_b1;
10071 ctx->mouse_b1 = 0;
10072 /* end a column-resize drag: notify the host so it can persist the layout */
10073 if (ctx->scrollbar_drag_widget_id >= 0) {
10075 if (up_wgt && up_wgt->type == N_GUI_TYPE_DATAGRID && up_wgt->data) {
10077 gd->h_scroll_dragging = 0; /* end any horizontal-scrollbar drag */
10078 if (gd->col_resize_col >= 0) {
10079 gd->col_resize_col = -1;
10080 if (gd->on_columns_changed)
10081 gd->on_columns_changed(up_wgt->id, gd->user_data);
10082 }
10083 }
10084 }
10085 ctx->scrollbar_drag_widget_id = -1;
10086 }
10087 if (event.type == ALLEGRO_EVENT_MOUSE_BUTTON_DOWN && event.mouse.button == 2) {
10088 /* right-click: keep the cursor position current so the context-menu hit
10089 test below works even with no preceding MOUSE_AXES event */
10090 ctx->tooltip_widget_id = -1;
10091 ctx->mouse_x = event.mouse.x;
10092 ctx->mouse_y = event.mouse.y;
10093 }
10094
10095 /* Unified widget scrollbar drag: update scroll position while mouse held */
10096 if (ctx->scrollbar_drag_widget_id >= 0 && ctx->mouse_b1 &&
10097 (event.type == ALLEGRO_EVENT_MOUSE_AXES || event.type == ALLEGRO_EVENT_MOUSE_BUTTON_DOWN)) {
10099 if (drag_wgt && drag_wgt->data) {
10100 float d_ox = 0, d_oy = 0;
10101 const N_GUI_WINDOW* d_win = _find_widget_window(ctx, drag_wgt->id, &d_ox, &d_oy);
10102 if (d_win) {
10103 ALLEGRO_FONT* d_font = drag_wgt->font ? drag_wgt->font : ctx->default_font;
10104 float d_fh = d_font ? (float)al_get_font_line_height(d_font) : 16.0f;
10105 float d_my = (float)ctx->mouse_y;
10106 float d_mx = (float)ctx->mouse_x;
10107 switch (drag_wgt->type) {
10108 case N_GUI_TYPE_COMBOBOX: {
10109 N_GUI_COMBOBOX_DATA* cbd = (N_GUI_COMBOBOX_DATA*)drag_wgt->data;
10110 if (cbd->is_open && (int)cbd->nb_items > cbd->max_visible) {
10111 float ih = cbd->item_height > d_fh ? cbd->item_height : d_fh + ctx->style.item_height_pad;
10112 float d_ay = d_oy + drag_wgt->y + drag_wgt->h;
10113 float dd_h = (float)cbd->max_visible * ih;
10114 cbd->scroll_offset = _scrollbar_calc_scroll_int(d_my, d_ay, dd_h, cbd->max_visible, (int)cbd->nb_items, ctx->style.scrollbar_thumb_min);
10115 }
10116 break;
10117 }
10118 case N_GUI_TYPE_DROPMENU: {
10119 N_GUI_DROPMENU_DATA* dmd = (N_GUI_DROPMENU_DATA*)drag_wgt->data;
10120 if (dmd->is_open && (int)dmd->nb_entries > dmd->max_visible) {
10121 float ih = dmd->item_height > d_fh ? dmd->item_height : d_fh + ctx->style.item_height_pad;
10122 float d_ay = d_oy + drag_wgt->y + drag_wgt->h;
10123 float dd_h = (float)dmd->max_visible * ih;
10124 dmd->scroll_offset = _scrollbar_calc_scroll_int(d_my, d_ay, dd_h, dmd->max_visible, (int)dmd->nb_entries, ctx->style.scrollbar_thumb_min);
10125 }
10126 break;
10127 }
10128 case N_GUI_TYPE_LISTBOX: {
10129 N_GUI_LISTBOX_DATA* lbd = (N_GUI_LISTBOX_DATA*)drag_wgt->data;
10130 float ih = lbd->item_height > d_fh ? lbd->item_height : d_fh + ctx->style.item_height_pad;
10131 float d_ay = d_oy + drag_wgt->y;
10132 int visible = (int)(drag_wgt->h / ih);
10133 lbd->scroll_offset = _scrollbar_calc_scroll_int(d_my, d_ay, drag_wgt->h, visible, (int)lbd->nb_items, ctx->style.scrollbar_thumb_min);
10134 break;
10135 }
10136 case N_GUI_TYPE_RADIOLIST: {
10138 float ih = rld->item_height > d_fh ? rld->item_height : d_fh + ctx->style.item_height_pad;
10139 float d_ay = d_oy + drag_wgt->y;
10140 int visible = (int)(drag_wgt->h / ih);
10141 rld->scroll_offset = _scrollbar_calc_scroll_int(d_my, d_ay, drag_wgt->h, visible, (int)rld->nb_items, ctx->style.scrollbar_thumb_min);
10142 break;
10143 }
10144 case N_GUI_TYPE_TEXTAREA: {
10145 N_GUI_TEXTAREA_DATA* tatd = (N_GUI_TEXTAREA_DATA*)drag_wgt->data;
10146 if (tatd->multiline) {
10147 float ta_pad = ctx->style.textarea_padding;
10148 float ta_view_h = drag_wgt->h - ta_pad * 2;
10149 float ta_sb_w = ctx->style.scrollbar_size;
10150 float ta_text_w = drag_wgt->w - ta_sb_w;
10151 float ta_content_h = _textarea_content_height(tatd, d_font, ta_text_w, ta_pad);
10152 float d_ay = d_oy + drag_wgt->y + ta_pad;
10153 tatd->scroll_y = (int)_scrollbar_calc_scroll(
10154 d_my, d_ay, ta_view_h,
10155 ta_view_h, ta_content_h,
10157 }
10158 break;
10159 }
10160 case N_GUI_TYPE_SPLITPANE: {
10162 float d_ax = d_ox + drag_wgt->x;
10163 float d_ay = d_oy + drag_wgt->y;
10164 float r;
10166 r = (drag_wgt->w > 0) ? ((float)ctx->mouse_x - d_ax) / drag_wgt->w : spd->ratio;
10167 else
10168 r = (drag_wgt->h > 0) ? (d_my - d_ay) / drag_wgt->h : spd->ratio;
10169 if (r < spd->min_ratio) r = spd->min_ratio;
10170 if (r > spd->max_ratio) r = spd->max_ratio;
10171 spd->ratio = r;
10172 if (spd->on_change) spd->on_change(drag_wgt->id, r, spd->user_data);
10173 break;
10174 }
10175 case N_GUI_TYPE_HEXVIEW: {
10176 N_GUI_HEXVIEW_DATA* hd = (N_GUI_HEXVIEW_DATA*)drag_wgt->data;
10177 float row_h = d_fh + 2.0f;
10178 int bpr = hd->bytes_per_row > 0 ? hd->bytes_per_row : 16;
10179 int nb_rows = (int)((hd->len + (size_t)bpr - 1) / (size_t)bpr);
10180 int visible = (int)((drag_wgt->h - ctx->style.textarea_padding * 2.0f) / row_h);
10181 float d_ay = d_oy + drag_wgt->y;
10182 if (visible < 1) visible = 1;
10183 hd->scroll_offset = _scrollbar_calc_scroll_int(d_my, d_ay, drag_wgt->h, visible, nb_rows, ctx->style.scrollbar_thumb_min);
10184 break;
10185 }
10186 case N_GUI_TYPE_SYNTAXVIEW: {
10188 float row_h = d_fh + 2.0f;
10189 int nb_lines = (_syntaxview_ensure_lines(yd), yd->cached_nb_lines);
10190 int visible = (int)((drag_wgt->h - ctx->style.textarea_padding * 2.0f) / row_h);
10191 float d_ay = d_oy + drag_wgt->y;
10192 if (visible < 1) visible = 1;
10193 yd->scroll_offset = _scrollbar_calc_scroll_int(d_my, d_ay, drag_wgt->h, visible, nb_lines, ctx->style.scrollbar_thumb_min);
10194 break;
10195 }
10196 case N_GUI_TYPE_DATAGRID: {
10198 float row_h = d_fh + ctx->style.item_height_pad;
10199 float header_h = row_h;
10200 float data_h = drag_wgt->h - header_h;
10201 int visible = (int)(data_h / row_h);
10202 float d_ay = d_oy + drag_wgt->y + header_h;
10203 if (visible < 1) visible = 1;
10204 if (gd->col_resize_col >= 0 && (size_t)gd->col_resize_col < gd->nb_cols) {
10205 /* a header-border drag resizes the column live */
10206 float nw = gd->col_resize_w0 + ((float)d_mx - gd->col_resize_x0);
10207 if (nw < 16.0f) nw = 16.0f;
10208 gd->cols[gd->col_resize_col].width = nw;
10209 } else if (gd->h_scroll_dragging) {
10210 /* dragging the horizontal scrollbar thumb: map the cursor x to a
10211 pixel offset over the content width */
10212 float d_ax = d_ox + drag_wgt->x;
10213 float dg_content_w = 0.0f, dg_pane_w = 0.0f;
10214 _datagrid_hmetrics(drag_wgt, gd, d_font, &ctx->style, &dg_content_w, &dg_pane_w, NULL);
10215 gd->h_scroll = _scrollbar_calc_scroll((float)d_mx, d_ax, dg_pane_w, dg_pane_w, dg_content_w, ctx->style.scrollbar_thumb_min);
10216 } else {
10217 gd->scroll_offset = _scrollbar_calc_scroll_int(d_my, d_ay, data_h, visible, (int)gd->nb_rows, ctx->style.scrollbar_thumb_min);
10218 }
10219 break;
10220 }
10221 default:
10222 break;
10223 }
10224 return 1;
10225 }
10226 }
10227 }
10228
10229 /* screen-space mouse (for global scrollbar interaction) */
10230 float screen_mx = (float)ctx->mouse_x;
10231 float screen_my = (float)ctx->mouse_y;
10232 /* Virtual-space mouse: reverse the virtual canvas transform first.
10233 * The virtual canvas and the global scroll belong to the main display only,
10234 * n_gui_draw_detached paints a native window under an identity transform.
10235 * Applying either on a detached pass would offset every hit test in that
10236 * window by the main display's letterbox and scroll. */
10237 float virtual_mx = screen_mx;
10238 float virtual_my = screen_my;
10239 if (!ctx->pass_display && ctx->virtual_w > 0 && ctx->virtual_h > 0 && ctx->gui_scale > 0) {
10240 virtual_mx = (screen_mx - ctx->gui_offset_x) / ctx->gui_scale;
10241 virtual_my = (screen_my - ctx->gui_offset_y) / ctx->gui_scale;
10242 }
10243 /* GUI-space mouse (offset by global scroll) */
10244 float mx = virtual_mx;
10245 float my = virtual_my;
10246 if (!ctx->pass_display) {
10247 mx += ctx->global_scroll_x;
10248 my += ctx->global_scroll_y;
10249 }
10250 int just_pressed = (ctx->mouse_b1 == 1 && ctx->mouse_b1_prev == 0 &&
10251 event.type == ALLEGRO_EVENT_MOUSE_BUTTON_DOWN)
10252 ? 1
10253 : 0;
10254 int just_released = (ctx->mouse_b1 == 0 && ctx->mouse_b1_prev == 1 &&
10255 event.type == ALLEGRO_EVENT_MOUSE_BUTTON_UP)
10256 ? 1
10257 : 0;
10258 /* a right mouse-button press, used to raise a widget's context menu */
10259 int just_right_pressed = (event.type == ALLEGRO_EVENT_MOUSE_BUTTON_DOWN &&
10260 event.mouse.button == 2)
10261 ? 1
10262 : 0;
10263 /* a middle mouse-button press pastes the PRIMARY selection into the focused text
10264 * field, mirroring the middle-click paste of native X11 apps (Allegro button 3) */
10265 int just_middle_pressed = (event.type == ALLEGRO_EVENT_MOUSE_BUTTON_DOWN &&
10266 event.mouse.button == 3)
10267 ? 1
10268 : 0;
10269 if (just_middle_pressed && ctx->focused_widget_id >= 0 && _ctx_io_display(ctx)) {
10271 if (fw && fw->type == N_GUI_TYPE_TEXTAREA && (fw->state & N_GUI_STATE_FOCUSED) && fw->data) {
10273 if (clip) {
10275 al_free(clip);
10276 }
10277 }
10278 }
10279
10280 /* effective dimensions: gate on both dimensions for virtual canvas */
10281 int virtual_active = (ctx->virtual_w > 0 && ctx->virtual_h > 0);
10282 float eff_w = virtual_active ? ctx->virtual_w : ctx->display_w;
10283 float eff_h = virtual_active ? ctx->virtual_h : ctx->display_h;
10284
10285 /* handle global scrollbar drag */
10286 if (ctx->global_vscroll_drag || ctx->global_hscroll_drag) {
10287 if (ctx->mouse_b1 && (event.type == ALLEGRO_EVENT_MOUSE_AXES || event.type == ALLEGRO_EVENT_MOUSE_BUTTON_DOWN)) {
10288 float scrollbar_size = ctx->style.global_scrollbar_size;
10290 if (ctx->global_vscroll_drag && eff_h > 0) {
10291 int need_hscroll = (ctx->gui_bounds_w > eff_w) ? 1 : 0;
10292 float sb_h = eff_h - (need_hscroll ? scrollbar_size : 0);
10293 float view_h = sb_h;
10294 float ratio = view_h / ctx->gui_bounds_h;
10295 if (ratio > 1.0f) ratio = 1.0f;
10296 float thumb_h = ratio * sb_h;
10297 if (thumb_h < ctx->style.global_scrollbar_thumb_min) thumb_h = ctx->style.global_scrollbar_thumb_min;
10298 float max_scroll = ctx->gui_bounds_h - view_h;
10299 float track_range = sb_h - thumb_h;
10300 if (track_range > 0 && max_scroll > 0) {
10301 float pos_ratio = (virtual_my - thumb_h / 2.0f) / track_range;
10302 if (pos_ratio < 0) pos_ratio = 0;
10303 if (pos_ratio > 1) pos_ratio = 1;
10304 ctx->global_scroll_y = pos_ratio * max_scroll;
10305 }
10306 }
10307 if (ctx->global_hscroll_drag && eff_w > 0) {
10308 int need_vscroll = (ctx->gui_bounds_h > eff_h) ? 1 : 0;
10309 float sb_w = eff_w - (need_vscroll ? scrollbar_size : 0);
10310 float view_w = sb_w;
10311 float ratio = view_w / ctx->gui_bounds_w;
10312 if (ratio > 1.0f) ratio = 1.0f;
10313 float thumb_w = ratio * sb_w;
10314 if (thumb_w < ctx->style.global_scrollbar_thumb_min) thumb_w = ctx->style.global_scrollbar_thumb_min;
10315 float max_scroll = ctx->gui_bounds_w - view_w;
10316 float track_range = sb_w - thumb_w;
10317 if (track_range > 0 && max_scroll > 0) {
10318 float pos_ratio = (virtual_mx - thumb_w / 2.0f) / track_range;
10319 if (pos_ratio < 0) pos_ratio = 0;
10320 if (pos_ratio > 1) pos_ratio = 1;
10321 ctx->global_scroll_x = pos_ratio * max_scroll;
10322 }
10323 }
10324 return 1;
10325 }
10326 if (just_released) {
10327 ctx->global_vscroll_drag = 0;
10328 ctx->global_hscroll_drag = 0;
10329 }
10330 return 1;
10331 }
10332
10333 /* check global scrollbar click */
10334 if (just_pressed && eff_w > 0 && eff_h > 0) {
10335 float scrollbar_size = ctx->style.global_scrollbar_size;
10337 int need_vscroll = (ctx->gui_bounds_h > eff_h) ? 1 : 0;
10338 int need_hscroll = (ctx->gui_bounds_w > eff_w) ? 1 : 0;
10339 if (need_vscroll && ctx->gui_bounds_w > (eff_w - scrollbar_size)) need_hscroll = 1;
10340 if (need_hscroll && ctx->gui_bounds_h > (eff_h - scrollbar_size)) need_vscroll = 1;
10341
10342 if (need_vscroll) {
10343 float sb_x = eff_w - scrollbar_size;
10344 float sb_h = eff_h - (need_hscroll ? scrollbar_size : 0);
10345 if (_point_in_rect(virtual_mx, virtual_my, sb_x, 0, scrollbar_size, sb_h)) {
10346 ctx->global_vscroll_drag = 1;
10347 return 1;
10348 }
10349 }
10350 if (need_hscroll) {
10351 float sb_y = eff_h - scrollbar_size;
10352 float sb_w = eff_w - (need_vscroll ? scrollbar_size : 0);
10353 if (_point_in_rect(virtual_mx, virtual_my, 0, sb_y, sb_w, scrollbar_size)) {
10354 ctx->global_hscroll_drag = 1;
10355 return 1;
10356 }
10357 }
10358 }
10359
10360 /* handle open combobox dropdown first (it overlays everything). Like the
10361 dropmenu above, a hover over the open panel is consumed so the widgets
10362 beneath the floating list never receive hover/focus. */
10363 if (ctx->open_combobox_id >= 0 &&
10364 (just_pressed || event.type == ALLEGRO_EVENT_MOUSE_AXES)) {
10365 N_GUI_WIDGET* cb_wgt = n_gui_get_widget(ctx, ctx->open_combobox_id);
10366 if (cb_wgt && cb_wgt->data) {
10368
10369 /* find absolute position of the combobox (account for window scroll) */
10370 float cb_ox = 0, cb_oy = 0;
10371 _find_widget_window(ctx, cb_wgt->id, &cb_ox, &cb_oy);
10372
10373 ALLEGRO_FONT* cb_font = cb_wgt->font ? cb_wgt->font : ctx->default_font;
10374 float cb_fh = cb_font ? (float)al_get_font_line_height(cb_font) : 16.0f;
10375 float cb_ih = cbd->item_height > cb_fh ? cbd->item_height : cb_fh + ctx->style.item_height_pad;
10376 float cb_ax = cb_ox + cb_wgt->x;
10377 float cb_pad = ctx->style.item_text_padding;
10378 int cb_vis = (int)cbd->nb_items;
10379 if (cb_vis > cbd->max_visible) cb_vis = cbd->max_visible;
10380 float cb_dd_h = (float)cb_vis * cb_ih;
10381 float cb_ay = _dropdown_panel_y(ctx, cb_oy + cb_wgt->y, cb_oy + cb_wgt->y + cb_wgt->h,
10382 cb_dd_h, cbd->flags & N_GUI_COMBOBOX_EXPAND_UP);
10383
10384 /* compute effective dropdown width (mirrors _draw_combobox_dropdown) */
10385 float cb_dd_w = cb_wgt->w;
10386 if ((cbd->flags & N_GUI_COMBOBOX_AUTO_WIDTH) && cb_font) {
10387 float cb_max_tw = 0;
10388 for (size_t ci = 0; ci < cbd->nb_items; ci++) {
10389 float tw = _text_w(cb_font, cbd->items[ci].text);
10390 if (tw > cb_max_tw) cb_max_tw = tw;
10391 }
10392 float cb_needed = cb_max_tw + cb_pad * 2;
10393 if (cb_needed > cb_dd_w) cb_dd_w = cb_needed;
10394 float cb_cap = ctx->style.combobox_max_dropdown_width;
10395 if (cb_cap <= 0) cb_cap = ctx->display_w > 0 ? (float)ctx->display_w : 4096.0f;
10396 if (cb_dd_w > cb_cap) cb_dd_w = cb_cap;
10397 if (ctx->display_w > 0 && cb_ax + cb_dd_w > (float)ctx->display_w) {
10398 cb_dd_w = (float)ctx->display_w - cb_ax;
10399 if (cb_dd_w < cb_wgt->w) cb_dd_w = cb_wgt->w;
10400 }
10401 }
10402
10403 if (!just_pressed) {
10404 /* hover or wheel over the open panel. A move sets the highlight to the
10405 item under the pointer; the wheel steps the highlight and scrolls to
10406 keep it visible. Consume either way so nothing beneath is affected. */
10407 if (_point_in_rect(mx, my, cb_ax, cb_ay, cb_dd_w, cb_dd_h)) {
10408 if (event.type == ALLEGRO_EVENT_MOUSE_AXES && event.mouse.dz != 0) {
10410 (int)cbd->nb_items, cbd->max_visible,
10411 -event.mouse.dz, cbd->selected_index);
10412 } else {
10413 int cb_hi = cbd->scroll_offset + (int)((my - cb_ay) / cb_ih);
10414 if (cb_hi >= 0 && (size_t)cb_hi < cbd->nb_items) cbd->highlight_index = cb_hi;
10415 }
10416 return 1;
10417 }
10418 } else if (_point_in_rect(mx, my, cb_ax, cb_ay, cb_dd_w, cb_dd_h)) {
10419 /* Check if click is on the scrollbar area (right edge) */
10420 int cb_need_sb = ((int)cbd->nb_items > cbd->max_visible);
10421 float cb_sb_size = ctx->style.scrollbar_size;
10422 if (cb_sb_size < 10.0f) cb_sb_size = 10.0f;
10423 float cb_item_w = cb_need_sb ? cb_dd_w - cb_sb_size : cb_dd_w;
10424
10425 if (cb_need_sb && mx >= cb_ax + cb_item_w) {
10426 /* Clicked on scrollbar, scroll to position, start drag */
10427 cbd->scroll_offset = _scrollbar_calc_scroll_int(my, cb_ay, cb_dd_h, cbd->max_visible, (int)cbd->nb_items, ctx->style.scrollbar_thumb_min);
10428 ctx->scrollbar_drag_widget_id = cb_wgt->id;
10429 return 1; /* consumed, keep dropdown open */
10430 }
10431
10432 /* Clicked on item area, select item */
10433 int clicked_idx = cbd->scroll_offset + (int)((my - cb_ay) / cb_ih);
10434 if (clicked_idx >= 0 && (size_t)clicked_idx < cbd->nb_items) {
10435 cbd->selected_index = clicked_idx;
10436 if (cbd->on_select) cbd->on_select(cb_wgt->id, clicked_idx, cbd->user_data);
10437 }
10438 cbd->is_open = 0;
10439 ctx->open_combobox_id = -1;
10440 return 1; /* event consumed by dropdown */
10441 } else {
10442 /* clicked outside dropdown - close it */
10443 cbd->is_open = 0;
10444 ctx->open_combobox_id = -1;
10445 /* fall through to normal processing */
10446 }
10447 } else {
10448 ctx->open_combobox_id = -1;
10449 }
10450 }
10451
10452 /* handle an open dropdown menu panel first. While a menu is open its floating
10453 panel visually overlaps the widgets beneath it, so ANY mouse event over the
10454 panel (a hover as well as a click) is consumed here: otherwise a hover would
10455 fall through to the general widget logic below and highlight/focus whatever
10456 sits under the panel. */
10457 if (ctx->open_dropmenu_id >= 0 &&
10458 (just_pressed || event.type == ALLEGRO_EVENT_MOUSE_AXES)) {
10459 N_GUI_WIDGET* dm_wgt = n_gui_get_widget(ctx, ctx->open_dropmenu_id);
10460 if (dm_wgt && dm_wgt->data) {
10462
10463 /* find absolute position of the dropmenu widget (account for window scroll) */
10464 float dm_ox = 0, dm_oy = 0;
10465 _find_widget_window(ctx, dm_wgt->id, &dm_ox, &dm_oy);
10466
10467 ALLEGRO_FONT* dm_font = dm_wgt->font ? dm_wgt->font : ctx->default_font;
10468 float dm_fh = dm_font ? (float)al_get_font_line_height(dm_font) : 16.0f;
10469 float dm_ih = dmd->item_height > dm_fh ? dmd->item_height : dm_fh + ctx->style.item_height_pad;
10470 float dm_ax = dm_ox + dm_wgt->x;
10471 int dm_vis = (int)dmd->nb_entries;
10472 if (dm_vis > dmd->max_visible) dm_vis = dmd->max_visible;
10473 float dm_panel_h = (float)dm_vis * dm_ih;
10474 float dm_ay = _dropdown_panel_y(ctx, dm_oy + dm_wgt->y, dm_oy + dm_wgt->y + dm_wgt->h,
10475 dm_panel_h, dmd->flags & N_GUI_DROPMENU_EXPAND_UP);
10476 /* the open panel is widened to fit its entries; hit-test the same width */
10477 float dm_panel_w = n_gui_dropmenu_panel_width(ctx, dm_wgt->id);
10478 int over_panel = _point_in_rect(mx, my, dm_ax, dm_ay, dm_panel_w, dm_panel_h);
10479
10480 if (!just_pressed) {
10481 /* hover or wheel while the menu is open. Over the panel: a move sets the
10482 highlight to the entry under the pointer, the wheel steps the highlight
10483 and scrolls to keep it visible, and the event is consumed so nothing
10484 beneath is affected. Off the panel it falls through so other widgets
10485 update hover normally. */
10486 if (over_panel) {
10487 if (event.type == ALLEGRO_EVENT_MOUSE_AXES && event.mouse.dz != 0) {
10489 (int)dmd->nb_entries, dmd->max_visible,
10490 -event.mouse.dz, -1);
10491 } else {
10492 int dm_hi = dmd->scroll_offset + (int)((my - dm_ay) / dm_ih);
10493 if (dm_hi >= 0 && (size_t)dm_hi < dmd->nb_entries) dmd->highlight_index = dm_hi;
10494 }
10495 return 1;
10496 }
10497 } else if (over_panel) {
10498 /* check if click is on the scrollbar area (right edge) */
10499 int dm_need_sb = ((int)dmd->nb_entries > dmd->max_visible);
10500 float dm_sb_size = ctx->style.scrollbar_size;
10501 if (dm_sb_size < 10.0f) dm_sb_size = 10.0f;
10502 float dm_item_w = dm_need_sb ? dm_panel_w - dm_sb_size : dm_panel_w;
10503
10504 if (dm_need_sb && mx >= dm_ax + dm_item_w) {
10505 /* Clicked on scrollbar, scroll to position, start drag */
10506 dmd->scroll_offset = _scrollbar_calc_scroll_int(my, dm_ay, dm_panel_h, dmd->max_visible, (int)dmd->nb_entries, ctx->style.scrollbar_thumb_min);
10507 ctx->scrollbar_drag_widget_id = dm_wgt->id;
10508 return 1; /* consumed, keep panel open */
10509 }
10510
10511 /* clicked on item area */
10512 int clicked_idx = dmd->scroll_offset + (int)((my - dm_ay) / dm_ih);
10513 if (clicked_idx >= 0 && (size_t)clicked_idx < dmd->nb_entries) {
10514 N_GUI_DROPMENU_ENTRY* entry = &dmd->entries[clicked_idx];
10515 if (entry->on_click) {
10516 entry->on_click(dm_wgt->id, clicked_idx, entry->tag, entry->user_data);
10517 }
10518 }
10519 dmd->is_open = 0;
10520 ctx->open_dropmenu_id = -1;
10521 return 1; /* event consumed by dropdown menu */
10522 } else {
10523 /* clicked outside panel - close the menu */
10524 dmd->is_open = 0;
10525 ctx->open_dropmenu_id = -1;
10526 /* if click landed on the button itself, consume event so widget handler doesn't re-open */
10527 float dm_btn_ax = dm_ox + dm_wgt->x;
10528 float dm_btn_ay = dm_oy + dm_wgt->y;
10529 if (_point_in_rect(mx, my, dm_btn_ax, dm_btn_ay, dm_wgt->w, dm_wgt->h)) {
10530 return 1;
10531 }
10532 /* fall through to normal processing */
10533 }
10534 } else {
10535 ctx->open_dropmenu_id = -1;
10536 }
10537 }
10538
10539 /* iterate windows back-to-front, but for hit testing we want front-to-back
10540 (last in list = front), so we walk backward */
10541 if (event.type == ALLEGRO_EVENT_MOUSE_AXES ||
10542 event.type == ALLEGRO_EVENT_MOUSE_BUTTON_DOWN ||
10543 event.type == ALLEGRO_EVENT_MOUSE_BUTTON_UP) {
10544 /* reset hover for all widgets and titlebar buttons */
10545 list_foreach(wnode, ctx->windows) {
10546 N_GUI_WINDOW* win = (N_GUI_WINDOW*)wnode->ptr;
10547 /* display routing: only windows on the pass this event belongs to */
10548 if (!_win_on_pass(ctx, win)) continue;
10549 if (!(win->state & N_GUI_WIN_OPEN)) continue;
10551 list_foreach(wgn, win->widgets) {
10552 N_GUI_WIDGET* wgt = (N_GUI_WIDGET*)wgn->ptr;
10553 if (!wgt) continue;
10554 wgt->state &= ~N_GUI_STATE_HOVER;
10555 /* also clear listbox per-row hover so a widget that
10556 now has no pointer over it doesn't keep painting
10557 a hover highlight from the previous frame */
10558 if (wgt->type == N_GUI_TYPE_LISTBOX && wgt->data) {
10559 ((N_GUI_LISTBOX_DATA*)wgt->data)->hover_row = -1;
10560 }
10561 }
10562 }
10563
10564 /* check if any window has an active drag/resize/scroll (mouse capture) */
10565 LIST_NODE* captured_wnode = NULL;
10566 for (LIST_NODE* wnode = ctx->windows->end; wnode; wnode = wnode->prev) {
10567 const N_GUI_WINDOW* win = (N_GUI_WINDOW*)wnode->ptr;
10568 /* display routing: only windows on the pass this event belongs to */
10569 if (!_win_on_pass(ctx, win)) continue;
10570 if (!(win->state & N_GUI_WIN_OPEN)) continue;
10572 captured_wnode = wnode;
10573 break;
10574 }
10575 }
10576
10577 /* find topmost window hit (iterate from end), skip if captured */
10578 LIST_NODE* hit_wnode = captured_wnode;
10579 if (!hit_wnode) {
10580 for (LIST_NODE* wnode = ctx->windows->end; wnode; wnode = wnode->prev) {
10581 N_GUI_WINDOW* win = (N_GUI_WINDOW*)wnode->ptr;
10582 /* display routing: only windows on the pass this event belongs to */
10583 if (!_win_on_pass(ctx, win)) continue;
10584 if (!(win->state & N_GUI_WIN_OPEN)) continue;
10585
10586 float win_h_check = (win->state & N_GUI_WIN_MINIMISED) ? _win_tbh(win) : win->h;
10587 if (_point_in_rect(mx, my, win->x, win->y, win->w, win_h_check)) {
10588 hit_wnode = wnode;
10589 break;
10590 }
10591 }
10592 }
10593
10594 if (hit_wnode) {
10595 event_consumed = 1;
10596 N_GUI_WINDOW* win = (N_GUI_WINDOW*)hit_wnode->ptr;
10597
10598 /* only process new clicks/interactions when not in a captured operation */
10599 if (!captured_wnode) {
10600 /* bring to front on click */
10601 if (just_pressed) {
10602 n_gui_raise_window(ctx, win->id);
10603 /* re-fetch since node may have moved */
10604 win = n_gui_get_window(ctx, win->id);
10605 if (!win) return 1;
10606
10607 /* clear focus from previously focused widget on a new primary click
10608 * inside a window; textarea clicks will re-set focus below */
10609 if (ctx->focused_widget_id >= 0) {
10610 N_GUI_WIDGET* prev_fw = n_gui_get_widget(ctx, ctx->focused_widget_id);
10611 if (prev_fw) prev_fw->state &= ~N_GUI_STATE_FOCUSED;
10612 ctx->focused_widget_id = -1;
10613 }
10614 }
10615
10616 /* check auto-scrollbar clicks before resize handle */
10617 int auto_scrollbar_hit = 0;
10618 if ((win->flags & N_GUI_WIN_AUTO_SCROLLBAR) && !(win->state & N_GUI_WIN_MINIMISED) && just_pressed) {
10619 float scrollbar_size = ctx->style.scrollbar_size;
10620 float body_h = win->h - _win_tbh(win);
10621 float body_y = win->y + _win_tbh(win);
10622 int need_vscroll = 0, need_hscroll = 0;
10624 if (win->content_h > body_h) need_vscroll = 1;
10625 if (win->content_w > win->w) need_hscroll = 1;
10626 if (need_vscroll && win->content_w > (win->w - scrollbar_size)) need_hscroll = 1;
10627 if (need_hscroll && win->content_h > (body_h - scrollbar_size)) need_vscroll = 1;
10628 float content_area_w = win->w - (need_vscroll ? scrollbar_size : 0);
10629 float content_area_h = body_h - (need_hscroll ? scrollbar_size : 0);
10630
10631 /* vertical auto-scrollbar track */
10632 if (need_vscroll) {
10633 float sb_x = win->x + win->w - scrollbar_size;
10634 float sb_y = body_y;
10635 float sb_h = content_area_h;
10636 /* leave room for resize corner when no horizontal scrollbar */
10637 if ((win->flags & N_GUI_WIN_RESIZABLE) && !need_hscroll) sb_h -= scrollbar_size;
10638 if (_point_in_rect(mx, my, sb_x, sb_y, scrollbar_size, sb_h)) {
10640 auto_scrollbar_hit = 1;
10641 win->scroll_y = _scrollbar_calc_scroll(my, sb_y, sb_h, content_area_h, win->content_h, ctx->style.scrollbar_thumb_min);
10642 }
10643 }
10644 /* horizontal auto-scrollbar track */
10645 if (need_hscroll && !auto_scrollbar_hit) {
10646 float sb_x = win->x;
10647 float sb_y = win->y + win->h - scrollbar_size;
10648 float sb_w = content_area_w;
10649 /* leave room for resize corner when no vertical scrollbar */
10650 if ((win->flags & N_GUI_WIN_RESIZABLE) && !need_vscroll) sb_w -= scrollbar_size;
10651 if (_point_in_rect(mx, my, sb_x, sb_y, sb_w, scrollbar_size)) {
10653 auto_scrollbar_hit = 1;
10654 win->scroll_x = _scrollbar_calc_scroll(mx, sb_x, sb_w, content_area_w, win->content_w, ctx->style.scrollbar_thumb_min);
10655 }
10656 }
10657 }
10658
10659 /* check resize handle (bottom-right corner, 14x14 px) - skip if auto-scrollbar was hit or maximised */
10660 if (!auto_scrollbar_hit && (win->flags & N_GUI_WIN_RESIZABLE) && !(win->state & (N_GUI_WIN_MINIMISED | N_GUI_WIN_MAXIMISED))) {
10661 float grip = ctx->style.grip_size + 2.0f;
10662 float rx = win->x + win->w - grip;
10663 float ry = win->y + win->h - grip;
10664 /* when auto-scrollbars are visible, restrict resize to the corner square only */
10665 int in_resize_area = 0;
10666 if (win->flags & N_GUI_WIN_AUTO_SCROLLBAR) {
10667 float scrollbar_size = ctx->style.scrollbar_size;
10668 float body_h = win->h - _win_tbh(win);
10669 int need_vscroll = 0, need_hscroll = 0;
10671 if (win->content_h > body_h) need_vscroll = 1;
10672 if (win->content_w > win->w) need_hscroll = 1;
10673 if (need_vscroll && win->content_w > (win->w - scrollbar_size)) need_hscroll = 1;
10674 if (need_hscroll && win->content_h > (body_h - scrollbar_size)) need_vscroll = 1;
10675 if (need_vscroll || need_hscroll) {
10676 /* resize corner is only the small square at bottom-right where scrollbars don't reach */
10677 float corner_x = win->x + win->w - scrollbar_size;
10678 float corner_y = win->y + win->h - scrollbar_size;
10679 in_resize_area = just_pressed && _point_in_rect(mx, my, corner_x, corner_y, scrollbar_size, scrollbar_size);
10680 } else {
10681 in_resize_area = just_pressed && _point_in_rect(mx, my, rx, ry, grip, grip);
10682 }
10683 } else {
10684 in_resize_area = just_pressed && _point_in_rect(mx, my, rx, ry, grip, grip);
10685 }
10686 if (in_resize_area) {
10687 win->state |= N_GUI_WIN_RESIZING;
10688 win->drag_ox = win->w - (mx - win->x);
10689 win->drag_oy = win->h - (my - win->y);
10690 }
10691 }
10692
10693 /* title bar: buttons / drag */
10695 _point_in_rect(mx, my, win->x, win->y, win->w, _win_tbh(win))) {
10696 /* check titlebar buttons first */
10697 int tb_btn_hit = N_GUI_TB_BTN_NONE;
10698 float tbx, tby, tbw, tbhh;
10699 if (_tb_button_rect(win, &ctx->style, N_GUI_TB_BTN_CLOSE, &tbx, &tby, &tbw, &tbhh) &&
10700 _point_in_rect(mx, my, tbx, tby, tbw, tbhh)) {
10701 tb_btn_hit = N_GUI_TB_BTN_CLOSE;
10702 } else if (_tb_button_rect(win, &ctx->style, N_GUI_TB_BTN_MAXIMIZE, &tbx, &tby, &tbw, &tbhh) &&
10703 _point_in_rect(mx, my, tbx, tby, tbw, tbhh)) {
10704 tb_btn_hit = N_GUI_TB_BTN_MAXIMIZE;
10705 } else if (_tb_button_rect(win, &ctx->style, N_GUI_TB_BTN_MINIMIZE, &tbx, &tby, &tbw, &tbhh) &&
10706 _point_in_rect(mx, my, tbx, tby, tbw, tbhh)) {
10707 tb_btn_hit = N_GUI_TB_BTN_MINIMIZE;
10708 }
10709
10710 /* hover tracking */
10711 win->tb_buttons.hovered = tb_btn_hit;
10712
10713 if (just_pressed && tb_btn_hit != N_GUI_TB_BTN_NONE) {
10714 /* button press, do NOT start drag */
10715 win->tb_buttons.pressed = tb_btn_hit;
10716 } else if (just_pressed && !(win->flags & N_GUI_WIN_FIXED_POSITION) && !(win->state & N_GUI_WIN_MAXIMISED)) {
10717 /* start drag */
10718 win->state |= N_GUI_WIN_DRAGGING;
10719 win->drag_ox = mx - win->x;
10720 win->drag_oy = my - win->y;
10721 _native_drag_begin(win);
10722 }
10723
10724 /* release handling for titlebar buttons */
10725 if (just_released && win->tb_buttons.pressed != N_GUI_TB_BTN_NONE) {
10726 int btn = win->tb_buttons.pressed;
10728 if (_tb_button_rect(win, &ctx->style, btn, &tbx, &tby, &tbw, &tbhh) &&
10729 _point_in_rect(mx, my, tbx, tby, tbw, tbhh)) {
10730 _tb_button_action(ctx, win, btn);
10731 }
10732 }
10734 /* widget area (account for scroll offsets) */
10735 float content_x = win->x - win->scroll_x;
10736 float content_y = win->y + _win_tbh(win) - win->scroll_y;
10737
10738 /* iterate topmost-first (last added draws on top) so the
10739 widget under the cursor that the user actually sees gets
10740 the event, then break -- only the top widget is hit. A
10741 forward walk would hand a click to an earlier-added widget
10742 that merely overlaps (e.g. a split pane drawn under a grid). */
10743 for (LIST_NODE* wgn = win->widgets ? win->widgets->end : NULL; wgn; wgn = wgn->prev) {
10744 N_GUI_WIDGET* wgt = (N_GUI_WIDGET*)wgn->ptr;
10745 if (!wgt || !wgt->visible || !wgt->enabled) continue;
10746 float ax = content_x + wgt->x;
10747 float ay = content_y + wgt->y;
10748
10749 if (_point_in_rect(mx, my, ax, ay, wgt->w, wgt->h)) {
10750 wgt->state |= N_GUI_STATE_HOVER;
10751
10752 /* listbox: translate mouse-Y into the row
10753 index under the cursor so the draw path
10754 can paint a hover highlight. */
10755 if (wgt->type == N_GUI_TYPE_LISTBOX && wgt->data) {
10756 N_GUI_LISTBOX_DATA* ld =
10757 (N_GUI_LISTBOX_DATA*)wgt->data;
10758 float ih = ld->item_height > 0.0f
10759 ? ld->item_height
10760 : 20.0f;
10761 int row_in_view = (int)((my - ay) / ih);
10762 int row = row_in_view + ld->scroll_offset;
10763 if (row >= 0 && (size_t)row < ld->nb_items) {
10764 ld->hover_row = row;
10765 } else {
10766 ld->hover_row = -1;
10767 }
10768 }
10769
10770 if (just_pressed) {
10771 wgt->state |= N_GUI_STATE_ACTIVE;
10772
10773 /* set focus on any interactive widget type */
10774 if (wgt->type == N_GUI_TYPE_SLIDER ||
10775 wgt->type == N_GUI_TYPE_CHECKBOX ||
10776 wgt->type == N_GUI_TYPE_LISTBOX ||
10777 wgt->type == N_GUI_TYPE_RADIOLIST ||
10778 wgt->type == N_GUI_TYPE_COMBOBOX ||
10779 wgt->type == N_GUI_TYPE_SCROLLBAR ||
10780 wgt->type == N_GUI_TYPE_DROPMENU ||
10781 wgt->type == N_GUI_TYPE_TEXTAREA) {
10782 wgt->state |= N_GUI_STATE_FOCUSED;
10783 ctx->focused_widget_id = wgt->id;
10784 }
10785
10786 if (wgt->type == N_GUI_TYPE_BUTTON) {
10787 /* click callback on release */
10788 }
10789 if (wgt->type == N_GUI_TYPE_CHECKBOX) {
10791 ckd->checked = !ckd->checked;
10792 if (ckd->on_toggle) ckd->on_toggle(wgt->id, ckd->checked, ckd->user_data);
10793 }
10794 if (wgt->type == N_GUI_TYPE_SLIDER) {
10795 _slider_update_from_mouse(wgt, mx, my, content_x, content_y);
10796 }
10797 if (wgt->type == N_GUI_TYPE_SCROLLBAR) {
10798 _scrollbar_update_from_mouse(wgt, mx, my, content_x, content_y, &ctx->style);
10799 }
10800 if (wgt->type == N_GUI_TYPE_TEXTAREA) {
10801 /* clear any label selection when clicking a textarea */
10802 if (ctx->selected_label_id >= 0) {
10804 if (prev && prev->type == N_GUI_TYPE_LABEL && prev->data) {
10805 N_GUI_LABEL_DATA* plb = (N_GUI_LABEL_DATA*)prev->data;
10806 plb->sel_start = -1;
10807 plb->sel_end = -1;
10808 plb->sel_dragging = 0;
10809 }
10810 ctx->selected_label_id = -1;
10811 }
10813 N_GUI_TEXTAREA_DATA* click_td = (N_GUI_TEXTAREA_DATA*)wgt->data;
10814 click_td->cursor_time = al_get_time();
10815 ALLEGRO_FONT* tf = wgt->font ? wgt->font : ctx->default_font;
10816 if (tf) {
10817 /* check if click is on the scrollbar area */
10818 float text_pad = ctx->style.textarea_padding;
10819 if (click_td->multiline) {
10820 float ta_view_h = wgt->h - text_pad * 2;
10821 float ta_content_h = _textarea_content_height(click_td, tf, wgt->w, text_pad);
10822 int ta_need_sb = (ta_content_h > ta_view_h) ? 1 : 0;
10823 float ta_sb_w = ta_need_sb ? ctx->style.scrollbar_size : 0;
10824 if (ta_need_sb && mx > ax + wgt->w - ta_sb_w) {
10825 /* scrollbar track click: jump + start drag */
10826 float ta_text_w = wgt->w - ta_sb_w;
10827 ta_content_h = _textarea_content_height(click_td, tf, ta_text_w, text_pad);
10828 click_td->scroll_y = (int)_scrollbar_calc_scroll(
10829 my, ay + text_pad, ta_view_h,
10830 ta_view_h, ta_content_h,
10832 ctx->scrollbar_drag_widget_id = wgt->id;
10833 goto textarea_click_done;
10834 }
10835 }
10836 size_t pos = _textarea_pos_from_mouse(click_td, tf, mx, my,
10837 ax, ay, wgt->w, wgt->h,
10838 text_pad, ctx->style.scrollbar_size);
10839 click_td->cursor_pos = pos;
10840 click_td->sel_start = pos;
10841 click_td->sel_end = pos;
10842 }
10843 textarea_click_done:;
10844 }
10845 if (wgt->type == N_GUI_TYPE_LABEL) {
10846 N_GUI_LABEL_DATA* click_lb = (N_GUI_LABEL_DATA*)wgt->data;
10847 if (click_lb) {
10849 /* clear previous label selection */
10850 if (ctx->selected_label_id >= 0 && ctx->selected_label_id != wgt->id) {
10852 if (prev && prev->type == N_GUI_TYPE_LABEL && prev->data) {
10853 N_GUI_LABEL_DATA* plb = (N_GUI_LABEL_DATA*)prev->data;
10854 plb->sel_start = -1;
10855 plb->sel_end = -1;
10856 plb->sel_dragging = 0;
10857 }
10858 }
10859 ALLEGRO_FONT* lf = wgt->font ? wgt->font : ctx->default_font;
10860 if (lf) {
10861 int char_pos;
10862 if (click_lb->align == N_GUI_ALIGN_JUSTIFIED) {
10863 float lpad = ctx->style.label_padding;
10864 float effective_w = wgt->w;
10865 float lww = 0;
10867 lww = win->content_w + ctx->style.title_max_w_reserve + lpad;
10868 else if (win->flags & N_GUI_WIN_RESIZABLE)
10869 lww = win->w;
10870 if (lww > 0) {
10871 float mfw = lww - wgt->x;
10872 if (mfw > effective_w) effective_w = mfw;
10873 }
10874 float max_text_w = effective_w - lpad * 2;
10875 float content_h = _label_content_height(click_lb->text, lf, max_text_w);
10876 float view_h = wgt->h - lpad;
10877 float sb_sz = (content_h > view_h) ? ctx->style.scrollbar_size : 0;
10878 float tw2 = max_text_w - sb_sz;
10879 char_pos = _justified_char_at_pos(click_lb->text, lf, tw2,
10880 ax + lpad, ay + lpad / 2.0f, click_lb->scroll_y, mx, my);
10881 } else {
10882 float lww = 0;
10884 lww = win->content_w + ctx->style.title_max_w_reserve + ctx->style.label_padding;
10885 else if (win->flags & N_GUI_WIN_RESIZABLE)
10886 lww = win->w;
10887 float text_ox = _label_text_origin_x(click_lb, lf, ax, wgt->w, lww, wgt->x, ctx->style.label_padding);
10888 float click_x = mx - text_ox;
10889 char_pos = _label_char_at_x(click_lb, lf, click_x);
10890 }
10891 if (char_pos >= 0) {
10892 click_lb->sel_start = char_pos;
10893 click_lb->sel_end = char_pos;
10894 click_lb->sel_dragging = 1;
10895 ctx->selected_label_id = wgt->id;
10896 }
10897 }
10898 }
10899 }
10900 if (wgt->type == N_GUI_TYPE_SYNTAXVIEW) {
10902 ALLEGRO_FONT* yf = wgt->font ? wgt->font : ctx->default_font;
10903 /* clear a selection held by a label */
10904 if (ctx->selected_label_id >= 0) {
10906 if (prev && prev->type == N_GUI_TYPE_LABEL && prev->data) {
10907 N_GUI_LABEL_DATA* plb = (N_GUI_LABEL_DATA*)prev->data;
10908 plb->sel_start = -1;
10909 plb->sel_end = -1;
10910 plb->sel_dragging = 0;
10911 }
10912 ctx->selected_label_id = -1;
10913 }
10914 /* clear a selection held by another syntax view */
10915 if (ctx->selected_syntaxview_id >= 0 && ctx->selected_syntaxview_id != wgt->id) {
10917 if (prev && prev->type == N_GUI_TYPE_SYNTAXVIEW && prev->data) {
10919 pyd->sel_start = -1;
10920 pyd->sel_end = -1;
10921 pyd->sel_dragging = 0;
10922 }
10923 }
10924 if (click_yd && yf && click_yd->text) {
10925 float ypad = ctx->style.textarea_padding;
10926 int off = _syntaxview_offset_from_mouse(click_yd, yf, mx, my, ax, ay, ypad);
10927 click_yd->sel_start = off;
10928 click_yd->sel_end = off;
10929 click_yd->sel_dragging = 1;
10930 ctx->selected_syntaxview_id = wgt->id;
10931 }
10932 }
10933 if (wgt->type == N_GUI_TYPE_COMBOBOX) {
10935 cbd->is_open = !cbd->is_open;
10936 if (cbd->is_open) {
10937 ctx->open_combobox_id = wgt->id;
10938 /* start the keyboard/wheel highlight on the current value */
10939 cbd->highlight_index = cbd->selected_index;
10940 /* auto-scroll to center the selected item in the visible area */
10941 if (cbd->selected_index >= 0 && cbd->nb_items > (size_t)cbd->max_visible) {
10942 int target = cbd->selected_index - cbd->max_visible / 2;
10943 if (target < 0) target = 0;
10944 int max_off = (int)cbd->nb_items - cbd->max_visible;
10945 if (target > max_off) target = max_off;
10946 cbd->scroll_offset = target;
10947 } else {
10948 cbd->scroll_offset = 0;
10949 }
10950 } else {
10951 ctx->open_combobox_id = -1;
10952 }
10953 }
10954 if (wgt->type == N_GUI_TYPE_LISTBOX) {
10956 ALLEGRO_FONT* lf = wgt->font ? wgt->font : ctx->default_font;
10957 float lfh = lf ? (float)al_get_font_line_height(lf) : 16.0f;
10958 float lih = lbd->item_height > lfh ? lbd->item_height : lfh + ctx->style.item_height_pad;
10959 int lb_visible = (int)(wgt->h / lih);
10960 int lb_need_sb = ((int)lbd->nb_items > lb_visible) ? 1 : 0;
10961 float lb_sb_w = lb_need_sb ? ctx->style.scrollbar_size : 0;
10962 /* clamp scroll_offset */
10963 int lb_max_off = (int)lbd->nb_items - lb_visible;
10964 if (lb_max_off < 0) lb_max_off = 0;
10965 if (lbd->scroll_offset > lb_max_off) lbd->scroll_offset = lb_max_off;
10966 if (lbd->scroll_offset < 0) lbd->scroll_offset = 0;
10967 /* skip click if on the scrollbar area */
10968 if (lb_need_sb && mx > ax + wgt->w - lb_sb_w) {
10969 /* scrollbar track click: jump scroll position + start drag */
10970 lbd->scroll_offset = _scrollbar_calc_scroll_int(my, ay, wgt->h, lb_visible, (int)lbd->nb_items, ctx->style.scrollbar_thumb_min);
10971 ctx->scrollbar_drag_widget_id = wgt->id;
10972 } else {
10973 int clicked_idx = lbd->scroll_offset + (int)((my - ay) / lih);
10974 if (clicked_idx >= 0 && (size_t)clicked_idx < lbd->nb_items) {
10975 if (lbd->selection_mode == N_GUI_SELECT_SINGLE) {
10976 for (size_t si = 0; si < lbd->nb_items; si++) lbd->items[si].selected = 0;
10977 lbd->items[clicked_idx].selected = 1;
10978 if (lbd->on_select) lbd->on_select(wgt->id, clicked_idx, 1, lbd->user_data);
10979 } else if (lbd->selection_mode == N_GUI_SELECT_MULTIPLE) {
10980 lbd->items[clicked_idx].selected = !lbd->items[clicked_idx].selected;
10981 if (lbd->on_select) lbd->on_select(wgt->id, clicked_idx, lbd->items[clicked_idx].selected, lbd->user_data);
10982 }
10983 /* N_GUI_SELECT_NONE: no selection change */
10984 }
10985 }
10986 }
10987 if (wgt->type == N_GUI_TYPE_SPLITPANE) {
10988 const N_GUI_SPLITPANE_DATA* spd = (const N_GUI_SPLITPANE_DATA*)wgt->data;
10989 float band = spd->divider * 2.0f;
10990 int on_div;
10991 if (spd->orientation == N_GUI_SPLIT_VERTICAL) {
10992 float dcx = ax + spd->ratio * wgt->w;
10993 on_div = (mx >= dcx - band && mx <= dcx + band);
10994 } else {
10995 float dcy = ay + spd->ratio * wgt->h;
10996 on_div = (my >= dcy - band && my <= dcy + band);
10997 }
10998 if (on_div) ctx->scrollbar_drag_widget_id = wgt->id;
10999 }
11000 if (wgt->type == N_GUI_TYPE_HEXVIEW) {
11002 ALLEGRO_FONT* hf = wgt->font ? wgt->font : ctx->default_font;
11003 float hfh = hf ? (float)al_get_font_line_height(hf) : 16.0f;
11004 float row_h = hfh + 2.0f;
11005 int bpr = hd->bytes_per_row > 0 ? hd->bytes_per_row : 16;
11006 int nb_rows = (int)((hd->len + (size_t)bpr - 1) / (size_t)bpr);
11007 int hv_visible = (int)((wgt->h - ctx->style.textarea_padding * 2.0f) / row_h);
11008 int hv_need_sb;
11009 float hv_sb_w;
11010 if (hv_visible < 1) hv_visible = 1;
11011 hv_need_sb = (nb_rows > hv_visible) ? 1 : 0;
11012 hv_sb_w = hv_need_sb ? ctx->style.scrollbar_size : 0.0f;
11013 if (hv_need_sb && mx > ax + wgt->w - hv_sb_w) {
11014 hd->scroll_offset = _scrollbar_calc_scroll_int(my, ay, wgt->h, hv_visible, nb_rows, ctx->style.scrollbar_thumb_min);
11015 ctx->scrollbar_drag_widget_id = wgt->id;
11016 }
11017 }
11018 if (wgt->type == N_GUI_TYPE_SYNTAXVIEW) {
11020 ALLEGRO_FONT* yf = wgt->font ? wgt->font : ctx->default_font;
11021 float yfh = yf ? (float)al_get_font_line_height(yf) : 16.0f;
11022 float row_h = yfh + 2.0f;
11023 int nb_lines = (_syntaxview_ensure_lines(yd), yd->cached_nb_lines);
11024 int yv_visible = (int)((wgt->h - ctx->style.textarea_padding * 2.0f) / row_h);
11025 int yv_need_sb;
11026 float yv_sb_w;
11027 if (yv_visible < 1) yv_visible = 1;
11028 yv_need_sb = (nb_lines > yv_visible) ? 1 : 0;
11029 yv_sb_w = yv_need_sb ? ctx->style.scrollbar_size : 0.0f;
11030 if (yv_need_sb && mx > ax + wgt->w - yv_sb_w) {
11031 yd->scroll_offset = _scrollbar_calc_scroll_int(my, ay, wgt->h, yv_visible, nb_lines, ctx->style.scrollbar_thumb_min);
11032 ctx->scrollbar_drag_widget_id = wgt->id;
11033 }
11034 }
11035 if (wgt->type == N_GUI_TYPE_DATAGRID) {
11037 ALLEGRO_FONT* gf = wgt->font ? wgt->font : ctx->default_font;
11038 float gfh = gf ? (float)al_get_font_line_height(gf) : 16.0f;
11039 float row_h = gfh + ctx->style.item_height_pad;
11040 float header_h = row_h;
11041 float data_h = wgt->h - header_h;
11042 int dg_visible = (int)(data_h / row_h);
11043 int dg_need_sb;
11044 float dg_sb_w;
11045 if (dg_visible < 1) dg_visible = 1;
11046 float dg_content_w = 0.0f, dg_pane_w = 0.0f, dg_hsb_h;
11047 int dg_need_hsb = 0;
11048 dg_need_sb = ((int)gd->nb_rows > dg_visible) ? 1 : 0;
11049 dg_sb_w = dg_need_sb ? ctx->style.scrollbar_size : 0.0f;
11050 _datagrid_hmetrics(wgt, gd, gf, &ctx->style, &dg_content_w, &dg_pane_w, &dg_need_hsb);
11051 dg_hsb_h = dg_need_hsb ? ctx->style.scrollbar_size : 0.0f;
11052 if (dg_need_hsb && my > ay + wgt->h - dg_hsb_h && mx < ax + dg_pane_w) {
11053 /* click/drag on the horizontal scrollbar along the bottom */
11054 gd->h_scroll = _scrollbar_calc_scroll(mx, ax, dg_pane_w, dg_pane_w, dg_content_w, ctx->style.scrollbar_thumb_min);
11055 gd->h_scroll_dragging = 1;
11056 ctx->scrollbar_drag_widget_id = wgt->id;
11057 } else if (dg_need_sb && mx > ax + wgt->w - dg_sb_w && my >= ay + header_h) {
11058 gd->scroll_offset = _scrollbar_calc_scroll_int(my, ay + header_h, data_h, dg_visible, (int)gd->nb_rows, ctx->style.scrollbar_thumb_min);
11059 ctx->scrollbar_drag_widget_id = wgt->id;
11060 } else if (my < ay + header_h) {
11061 /* header click: a drag within a small zone of a
11062 visible column's right border resizes it, else
11063 clicking a header cell sorts by that column */
11064 float bx = ax - gd->h_scroll;
11065 size_t cc;
11066 int hit = -1, resize_col = -1;
11067 for (cc = 0; cc < gd->nb_cols; cc++) {
11068 size_t pc = _datagrid_phys(gd, cc);
11069 float right;
11070 if (!gd->cols[pc].visible) continue;
11071 right = bx + gd->cols[pc].width;
11072 if (mx >= right - 4.0f && mx <= right + 4.0f) {
11073 resize_col = (int)pc;
11074 break;
11075 }
11076 if (mx >= bx && mx < right) {
11077 hit = (int)pc;
11078 break;
11079 }
11080 bx = right;
11081 }
11082 if (resize_col >= 0) {
11083 gd->col_resize_col = resize_col;
11084 gd->col_resize_x0 = (float)mx;
11085 gd->col_resize_w0 = gd->cols[resize_col].width;
11086 ctx->scrollbar_drag_widget_id = wgt->id;
11087 } else if (hit >= 0) {
11088 int dir = (gd->sort_col == hit && gd->sort_dir > 0) ? -1 : 1;
11089 n_gui_datagrid_sort(ctx, wgt->id, hit, dir);
11090 }
11091 } else {
11092 int clicked = gd->scroll_offset + (int)((my - (ay + header_h)) / row_h);
11093 if (clicked >= 0 && (size_t)clicked < gd->nb_rows) {
11094 if (gd->multiselect) {
11095 /* Ctrl toggles a row, Shift selects the range from the
11096 anchor, a plain click selects only the clicked row */
11097 int ctrl = 0, shift = 0;
11098 if (al_is_keyboard_installed()) {
11099 ALLEGRO_KEYBOARD_STATE kb;
11100 al_get_keyboard_state(&kb);
11101 ctrl = al_key_down(&kb, ALLEGRO_KEY_LCTRL) || al_key_down(&kb, ALLEGRO_KEY_RCTRL);
11102 shift = al_key_down(&kb, ALLEGRO_KEY_LSHIFT) || al_key_down(&kb, ALLEGRO_KEY_RSHIFT);
11103 }
11105 if (gd->row_sel) {
11106 if (shift && gd->anchor_row >= 0 && (size_t)gd->anchor_row < gd->nb_rows) {
11107 int lo = gd->anchor_row < clicked ? gd->anchor_row : clicked;
11108 int hi = gd->anchor_row < clicked ? clicked : gd->anchor_row;
11109 int rr;
11110 memset(gd->row_sel, 0, gd->rows_cap);
11111 for (rr = lo; rr <= hi; rr++) gd->row_sel[rr] = 1;
11112 } else if (ctrl) {
11113 gd->row_sel[clicked] = gd->row_sel[clicked] ? 0 : 1;
11114 gd->anchor_row = clicked;
11115 } else {
11116 memset(gd->row_sel, 0, gd->rows_cap);
11117 gd->row_sel[clicked] = 1;
11118 gd->anchor_row = clicked;
11119 }
11120 }
11121 }
11122 gd->selected_row = clicked;
11123 if (gd->on_select) gd->on_select(wgt->id, clicked, gd->user_data);
11124 }
11125 }
11126 }
11127 if (wgt->type == N_GUI_TYPE_RADIOLIST) {
11129 ALLEGRO_FONT* rf = wgt->font ? wgt->font : ctx->default_font;
11130 float rfh = rf ? (float)al_get_font_line_height(rf) : 16.0f;
11131 float rih = rld->item_height > rfh ? rld->item_height : rfh + ctx->style.item_height_pad;
11132 int rl_visible = (int)(wgt->h / rih);
11133 int rl_need_sb = ((int)rld->nb_items > rl_visible) ? 1 : 0;
11134 float rl_sb_w = rl_need_sb ? ctx->style.scrollbar_size : 0;
11135 /* clamp scroll_offset */
11136 int rl_max_off = (int)rld->nb_items - rl_visible;
11137 if (rl_max_off < 0) rl_max_off = 0;
11138 if (rld->scroll_offset > rl_max_off) rld->scroll_offset = rl_max_off;
11139 if (rld->scroll_offset < 0) rld->scroll_offset = 0;
11140 /* skip click if on the scrollbar area */
11141 if (rl_need_sb && mx > ax + wgt->w - rl_sb_w) {
11142 /* scrollbar track click: jump scroll position + start drag */
11143 rld->scroll_offset = _scrollbar_calc_scroll_int(my, ay, wgt->h, rl_visible, (int)rld->nb_items, ctx->style.scrollbar_thumb_min);
11144 ctx->scrollbar_drag_widget_id = wgt->id;
11145 } else {
11146 int clicked_idx = rld->scroll_offset + (int)((my - ay) / rih);
11147 if (clicked_idx >= 0 && (size_t)clicked_idx < rld->nb_items) {
11148 rld->selected_index = clicked_idx;
11149 if (rld->on_select) rld->on_select(wgt->id, clicked_idx, rld->user_data);
11150 }
11151 }
11152 }
11153 if (wgt->type == N_GUI_TYPE_LABEL) {
11155 if (lbl->link[0] && lbl->on_link_click) {
11156 lbl->on_link_click(wgt->id, lbl->link, lbl->user_data);
11157 }
11158 }
11159 if (wgt->type == N_GUI_TYPE_DROPMENU) {
11161 dmd->is_open = !dmd->is_open;
11162 if (dmd->is_open) {
11163 /* close any other open dropdown/combobox first */
11164 if (ctx->open_combobox_id >= 0) {
11165 N_GUI_WIDGET* old_cb = n_gui_get_widget(ctx, ctx->open_combobox_id);
11166 if (old_cb && old_cb->data) ((N_GUI_COMBOBOX_DATA*)old_cb->data)->is_open = 0;
11167 ctx->open_combobox_id = -1;
11168 }
11169 if (ctx->open_dropmenu_id >= 0 && ctx->open_dropmenu_id != wgt->id) {
11170 N_GUI_WIDGET* old_dm = n_gui_get_widget(ctx, ctx->open_dropmenu_id);
11171 if (old_dm && old_dm->data) ((N_GUI_DROPMENU_DATA*)old_dm->data)->is_open = 0;
11172 }
11173 ctx->open_dropmenu_id = wgt->id;
11174 /* nothing pre-highlighted until the user hovers, wheels, or arrows */
11175 dmd->highlight_index = -1;
11176 /* call on_open callback to rebuild dynamic entries */
11177 if (dmd->on_open) {
11178 dmd->on_open(wgt->id, dmd->on_open_user_data);
11179 }
11180 } else {
11181 ctx->open_dropmenu_id = -1;
11182 }
11183 }
11184 }
11185 if (just_right_pressed && wgt->type == N_GUI_TYPE_DATAGRID && wgt->data) {
11186 /* right-click a data row: select it and raise the
11187 context callback with the cursor position */
11189 ALLEGRO_FONT* gf = wgt->font ? wgt->font : ctx->default_font;
11190 float gfh = gf ? (float)al_get_font_line_height(gf) : 16.0f;
11191 float row_h = gfh + ctx->style.item_height_pad;
11192 float header_h = row_h;
11193 if (my >= ay + header_h) {
11194 int clicked = gdc->scroll_offset + (int)((my - (ay + header_h)) / row_h);
11195 if (clicked >= 0 && (size_t)clicked < gdc->nb_rows) {
11196 /* right-clicking a row outside the current multi-selection
11197 selects just that row; right-clicking inside it keeps the
11198 whole selection so the menu can act on all of it */
11199 if (gdc->multiselect) {
11201 if (gdc->row_sel && !gdc->row_sel[clicked]) {
11202 memset(gdc->row_sel, 0, gdc->rows_cap);
11203 gdc->row_sel[clicked] = 1;
11204 gdc->anchor_row = clicked;
11205 }
11206 }
11207 gdc->selected_row = clicked;
11208 if (gdc->on_context) gdc->on_context(wgt->id, clicked, (int)mx, (int)my, gdc->user_data);
11209 }
11210 }
11211 }
11212 break; /* only top widget gets the event */
11213 }
11214 }
11215
11216 /* frameless windows: drag from empty body area */
11217 if ((win->flags & N_GUI_WIN_FRAMELESS) && just_pressed &&
11218 !(win->flags & N_GUI_WIN_FIXED_POSITION) &&
11220 win->state |= N_GUI_WIN_DRAGGING;
11221 win->drag_ox = mx - win->x;
11222 win->drag_oy = my - win->y;
11223 _native_drag_begin(win);
11224 }
11225 }
11226
11227 } /* end if (!captured_wnode) */
11228
11229 /* clear titlebar button press if released outside the button */
11230 if (just_released && win->tb_buttons.pressed != N_GUI_TB_BTN_NONE) {
11232 }
11233
11234 /* window dragging */
11235 if ((win->state & N_GUI_WIN_DRAGGING) && ctx->mouse_b1 && !(win->flags & N_GUI_WIN_FIXED_POSITION)) {
11236 if (_native_own_chrome(win)) {
11237 /* Move the OS window and keep the pseudo-window pinned at
11238 * the display origin. The target is absolute: the position
11239 * the window sat at when the title bar was grabbed, plus how
11240 * far the pointer has travelled on the desktop since. See
11241 * _native_drag_begin for why a display-local delta cannot be
11242 * used here. */
11243 int nx = 0, ny = 0;
11244 int have_target = 0;
11245 if (win->native_drag_anchored) {
11246 int cx = 0, cy = 0;
11247 if (al_get_mouse_cursor_position(&cx, &cy)) {
11248 nx = win->native_drag_wx + (cx - win->native_drag_cx);
11249 ny = win->native_drag_wy + (cy - win->native_drag_cy);
11250 have_target = 1;
11251 }
11252 }
11253 if (!have_target) {
11254 /* no desktop cursor coordinates on this platform: fall
11255 * back to the display-local delta, which drags correctly
11256 * as long as the events are not stale */
11257 int dx = (int)(mx - win->drag_ox);
11258 int dy = (int)(my - win->drag_oy);
11259 if (dx != 0 || dy != 0) {
11260 int wx = 0, wy = 0;
11261 al_get_window_position(win->native, &wx, &wy);
11262 nx = wx + dx;
11263 ny = wy + dy;
11264 have_target = 1;
11265 }
11266 }
11267 /* only talk to the window manager on a real change, every
11268 * al_set_window_position is a round trip */
11269 if (have_target && (nx != win->native_pos_x || ny != win->native_pos_y)) {
11270 al_set_window_position(win->native, nx, ny);
11271 win->native_pos_x = nx;
11272 win->native_pos_y = ny;
11273 }
11274 } else {
11275 win->x = mx - win->drag_ox;
11276 win->y = my - win->drag_oy;
11277 }
11278 }
11279 if (just_released && (win->state & N_GUI_WIN_DRAGGING) && !(win->flags & N_GUI_WIN_FIXED_POSITION)) {
11280 win->state &= ~N_GUI_WIN_DRAGGING;
11281 win->native_drag_anchored = 0;
11282 if (_native_own_chrome(win)) {
11283 /* the window manager is free to have constrained the move
11284 * (screen edges, panels), so record where the window really
11285 * ended up rather than the last position asked for */
11286 int wx = 0, wy = 0;
11287 al_get_window_position(win->native, &wx, &wy);
11288 win->native_pos_x = wx;
11289 win->native_pos_y = wy;
11290 }
11291 if (ctx->resize_mode == N_GUI_RESIZE_ADAPTIVE) {
11293 }
11294 }
11295
11296 /* window resizing */
11297 if ((win->state & N_GUI_WIN_RESIZING) && ctx->mouse_b1) {
11298 float new_w = (mx - win->x) + win->drag_ox;
11299 float new_h = (my - win->y) + win->drag_oy;
11300 if (new_w < win->min_w) new_w = win->min_w;
11301 if (new_h < win->min_h) new_h = win->min_h;
11302 if (_native_own_chrome(win)) {
11303 /* the grip resizes the OS window; only act on a real change
11304 * so a motion-rate stream of al_resize_display calls (each
11305 * one a round trip to the window manager) is avoided */
11306 int nw = (int)(new_w + 0.5f);
11307 int nh = (int)(new_h + 0.5f);
11308 if (nw != (int)(win->w + 0.5f) || nh != (int)(win->h + 0.5f)) {
11309 al_resize_display(win->native, nw, nh);
11310 win->native_w = (float)nw;
11311 win->native_h = (float)nh;
11312 }
11313 /* the DISPLAY_RESIZE that follows confirms these, setting
11314 them now keeps the frame being drawn consistent */
11315 new_w = (float)nw;
11316 new_h = (float)nh;
11317 }
11318 win->w = new_w;
11319 win->h = new_h;
11320 }
11321 if (just_released && (win->state & N_GUI_WIN_RESIZING)) {
11322 win->state &= ~N_GUI_WIN_RESIZING;
11323 if (ctx->resize_mode == N_GUI_RESIZE_ADAPTIVE) {
11325 }
11326 }
11327
11328 /* auto-scrollbar dragging */
11330 float scrollbar_size = ctx->style.scrollbar_size;
11331 float body_h = win->h - _win_tbh(win);
11332 float body_y = win->y + _win_tbh(win);
11333 int need_vscroll = 0, need_hscroll = 0;
11335 if (win->content_h > body_h) need_vscroll = 1;
11336 if (win->content_w > win->w) need_hscroll = 1;
11337 if (need_vscroll && win->content_w > (win->w - scrollbar_size)) need_hscroll = 1;
11338 if (need_hscroll && win->content_h > (body_h - scrollbar_size)) need_vscroll = 1;
11339 float content_area_w = win->w - (need_vscroll ? scrollbar_size : 0);
11340 float content_area_h = body_h - (need_hscroll ? scrollbar_size : 0);
11341
11342 if (win->state & N_GUI_WIN_VSCROLL_DRAG) {
11343 float sb_y = body_y;
11344 float sb_h = content_area_h;
11345 if ((win->flags & N_GUI_WIN_RESIZABLE) && !need_hscroll) sb_h -= scrollbar_size;
11346 win->scroll_y = _scrollbar_calc_scroll(my, sb_y, sb_h, content_area_h, win->content_h, ctx->style.scrollbar_thumb_min);
11347 }
11348 if (win->state & N_GUI_WIN_HSCROLL_DRAG) {
11349 float sb_x = win->x;
11350 float sb_w = content_area_w;
11351 if ((win->flags & N_GUI_WIN_RESIZABLE) && !need_vscroll) sb_w -= scrollbar_size;
11352 win->scroll_x = _scrollbar_calc_scroll(mx, sb_x, sb_w, content_area_w, win->content_w, ctx->style.scrollbar_thumb_min);
11353 }
11354 }
11355 if (just_released && (win->state & (N_GUI_WIN_VSCROLL_DRAG | N_GUI_WIN_HSCROLL_DRAG))) {
11357 }
11358 } else {
11359 /* clicked outside all windows */
11360 if (just_pressed) {
11361 if (ctx->focused_widget_id != -1) {
11363 if (fw) fw->state &= ~N_GUI_STATE_FOCUSED;
11364 ctx->focused_widget_id = -1;
11365 }
11366 }
11367 }
11368
11369 /* handle active slider/scrollbar drag even outside widget bounds */
11370 if (ctx->mouse_b1 && event.type == ALLEGRO_EVENT_MOUSE_AXES) {
11371 list_foreach(wnode, ctx->windows) {
11372 N_GUI_WINDOW* win = (N_GUI_WINDOW*)wnode->ptr;
11373 /* display routing: only windows on the pass this event belongs to */
11374 if (!_win_on_pass(ctx, win)) continue;
11375 if (!(win->state & N_GUI_WIN_OPEN)) continue;
11376 float content_x = win->x - win->scroll_x;
11377 float content_y = win->y + _win_tbh(win) - win->scroll_y;
11378 list_foreach(wgn, win->widgets) {
11379 N_GUI_WIDGET* wgt = (N_GUI_WIDGET*)wgn->ptr;
11380 if (!wgt) continue;
11381 if (wgt->state & N_GUI_STATE_ACTIVE) {
11382 if (wgt->type == N_GUI_TYPE_SLIDER) {
11383 _slider_update_from_mouse(wgt, mx, my, content_x, content_y);
11384 }
11385 if (wgt->type == N_GUI_TYPE_SCROLLBAR) {
11386 _scrollbar_update_from_mouse(wgt, mx, my, content_x, content_y, &ctx->style);
11387 }
11388 if (wgt->type == N_GUI_TYPE_SYNTAXVIEW) {
11390 if (drag_yd && drag_yd->sel_dragging) {
11391 ALLEGRO_FONT* yf = wgt->font ? wgt->font : ctx->default_font;
11392 if (yf && drag_yd->text) {
11393 float yax = content_x + wgt->x;
11394 float yay = content_y + wgt->y;
11395 drag_yd->sel_end = _syntaxview_offset_from_mouse(drag_yd, yf, mx, my, yax, yay, ctx->style.textarea_padding);
11396 }
11397 }
11398 }
11399 if (wgt->type == N_GUI_TYPE_LABEL) {
11400 N_GUI_LABEL_DATA* drag_lb = (N_GUI_LABEL_DATA*)wgt->data;
11401 if (drag_lb && drag_lb->sel_dragging) {
11402 ALLEGRO_FONT* lf = wgt->font ? wgt->font : ctx->default_font;
11403 if (lf) {
11404 float lax = content_x + wgt->x;
11405 float lay = content_y + wgt->y;
11406 int char_pos;
11407 if (drag_lb->align == N_GUI_ALIGN_JUSTIFIED) {
11408 float lpad = ctx->style.label_padding;
11409 float effective_w = wgt->w;
11410 float lww = 0;
11412 lww = win->content_w + ctx->style.title_max_w_reserve + lpad;
11413 else if (win->flags & N_GUI_WIN_RESIZABLE)
11414 lww = win->w;
11415 if (lww > 0) {
11416 float mfw = lww - wgt->x;
11417 if (mfw > effective_w) effective_w = mfw;
11418 }
11419 float max_text_w = effective_w - lpad * 2;
11420 float content_h2 = _label_content_height(drag_lb->text, lf, max_text_w);
11421 float view_h = wgt->h - lpad;
11422 float sb_sz = (content_h2 > view_h) ? ctx->style.scrollbar_size : 0;
11423 float tw2 = max_text_w - sb_sz;
11424 char_pos = _justified_char_at_pos(drag_lb->text, lf, tw2,
11425 lax + lpad, lay + lpad / 2.0f, drag_lb->scroll_y, mx, my);
11426 } else {
11427 float lww = 0;
11429 lww = win->content_w + ctx->style.title_max_w_reserve + ctx->style.label_padding;
11430 else if (win->flags & N_GUI_WIN_RESIZABLE)
11431 lww = win->w;
11432 float text_ox = _label_text_origin_x(drag_lb, lf, lax, wgt->w, lww, wgt->x, ctx->style.label_padding);
11433 float click_x = mx - text_ox;
11434 char_pos = _label_char_at_x(drag_lb, lf, click_x);
11435 }
11436 if (char_pos >= 0) {
11437 drag_lb->sel_end = char_pos;
11438 }
11439 }
11440 }
11441 }
11442 if (wgt->type == N_GUI_TYPE_TEXTAREA) {
11444 ALLEGRO_FONT* tf = wgt->font ? wgt->font : ctx->default_font;
11445 if (tf && drag_td) {
11446 float ta_ax = content_x + wgt->x;
11447 float ta_ay = content_y + wgt->y;
11448 float text_pad = ctx->style.textarea_padding;
11449 size_t pos = _textarea_pos_from_mouse(drag_td, tf, mx, my,
11450 ta_ax, ta_ay, wgt->w, wgt->h,
11451 text_pad, ctx->style.scrollbar_size);
11452 drag_td->cursor_pos = pos;
11453 drag_td->sel_end = pos;
11454 drag_td->cursor_time = al_get_time();
11455 }
11456 }
11457 }
11458 }
11459 }
11460 }
11461
11462 /* release active state on mouse up */
11463 if (just_released) {
11464 /* A button's on_click may open/raise a window, which reorders
11465 * ctx->windows (n_gui_raise_window removes + re-pushes + re-sorts).
11466 * Firing it inside the loops below would free or relink the node the
11467 * list_foreach iterator has already cached as "next", giving a
11468 * use-after-free on the following step. Defer the single released
11469 * button's callback until after both loops finish. */
11470 N_GUI_BUTTON_DATA* pending_click = NULL;
11471 int pending_click_id = -1;
11472 list_foreach(wnode, ctx->windows) {
11473 N_GUI_WINDOW* win = (N_GUI_WINDOW*)wnode->ptr;
11474 /* display routing: only windows on the pass this event belongs to */
11475 if (!_win_on_pass(ctx, win)) continue;
11476 list_foreach(wgn, win->widgets) {
11477 N_GUI_WIDGET* wgt = (N_GUI_WIDGET*)wgn->ptr;
11478 if (!wgt) continue;
11479 if (wgt->state & N_GUI_STATE_ACTIVE) {
11480 /* fire button callback on release if still hovering */
11481 if (wgt->type == N_GUI_TYPE_BUTTON) {
11482 float content_x = win->x - win->scroll_x;
11483 float content_y = win->y + _win_tbh(win) - win->scroll_y;
11484 float ax = content_x + wgt->x;
11485 float ay = content_y + wgt->y;
11486 if (_point_in_rect(mx, my, ax, ay, wgt->w, wgt->h)) {
11488 /* Toggle mode: flip toggled state on each click */
11489 if (bd->toggle_mode) {
11490 bd->toggled = !bd->toggled;
11491 }
11492 if (bd->on_click && !pending_click) {
11493 pending_click = bd;
11494 pending_click_id = wgt->id;
11495 }
11496 }
11497 }
11498 /* stop label drag selection on release */
11499 if (wgt->type == N_GUI_TYPE_LABEL) {
11500 N_GUI_LABEL_DATA* rel_lb = (N_GUI_LABEL_DATA*)wgt->data;
11501 if (rel_lb) rel_lb->sel_dragging = 0;
11502 }
11503 /* stop syntax-view drag selection on release */
11504 if (wgt->type == N_GUI_TYPE_SYNTAXVIEW) {
11506 if (rel_yd) rel_yd->sel_dragging = 0;
11507 }
11508 wgt->state &= ~N_GUI_STATE_ACTIVE;
11509 }
11510 }
11511 }
11512 if (pending_click && pending_click->on_click)
11513 pending_click->on_click(pending_click_id, pending_click->user_data);
11514 /* select-to-copy: a finished mouse drag-selection goes to PRIMARY */
11516 }
11517 }
11518
11519 /* Tab / Shift+Tab: focus navigation across widgets in the active window.
11520 * Ctrl+Tab in a textarea inserts a literal tab, handled below by the textarea key handler. */
11521 /* keyboard navigation for an open combobox / dropmenu panel: Up/Down (and
11522 Home/End) move the highlight, Enter commits it, Escape closes without a change.
11523 Keyed off the open-panel state (not focus) so it works the moment the panel
11524 opens. KEY_CHAR so a held key auto-repeats. Takes priority over Tab below. */
11525 if (!event_consumed && event.type == ALLEGRO_EVENT_KEY_CHAR &&
11526 (ctx->open_combobox_id >= 0 || ctx->open_dropmenu_id >= 0)) {
11527 int kc = event.keyboard.keycode;
11528 if (ctx->open_combobox_id >= 0) {
11530 N_GUI_COMBOBOX_DATA* cd = (w && w->data) ? (N_GUI_COMBOBOX_DATA*)w->data : NULL;
11531 if (cd && cd->is_open) {
11532 int nb = (int)cd->nb_items;
11533 if (kc == ALLEGRO_KEY_UP) {
11535 event_consumed = 1;
11536 } else if (kc == ALLEGRO_KEY_DOWN) {
11538 event_consumed = 1;
11539 } else if (kc == ALLEGRO_KEY_HOME) {
11541 event_consumed = 1;
11542 } else if (kc == ALLEGRO_KEY_END) {
11544 event_consumed = 1;
11545 } else if (kc == ALLEGRO_KEY_ENTER || kc == ALLEGRO_KEY_PAD_ENTER) {
11546 if (cd->highlight_index >= 0 && cd->highlight_index < nb) {
11548 if (cd->on_select) cd->on_select(w->id, cd->selected_index, cd->user_data);
11549 }
11550 cd->is_open = 0;
11551 ctx->open_combobox_id = -1;
11552 event_consumed = 1;
11553 } else if (kc == ALLEGRO_KEY_ESCAPE) {
11554 cd->is_open = 0;
11555 ctx->open_combobox_id = -1;
11556 event_consumed = 1;
11557 }
11558 }
11559 }
11560 if (!event_consumed && ctx->open_dropmenu_id >= 0) {
11562 N_GUI_DROPMENU_DATA* dd = (w && w->data) ? (N_GUI_DROPMENU_DATA*)w->data : NULL;
11563 if (dd && dd->is_open) {
11564 int nb = (int)dd->nb_entries;
11565 if (kc == ALLEGRO_KEY_UP) {
11567 event_consumed = 1;
11568 } else if (kc == ALLEGRO_KEY_DOWN) {
11570 event_consumed = 1;
11571 } else if (kc == ALLEGRO_KEY_HOME) {
11573 event_consumed = 1;
11574 } else if (kc == ALLEGRO_KEY_END) {
11576 event_consumed = 1;
11577 } else if (kc == ALLEGRO_KEY_ENTER || kc == ALLEGRO_KEY_PAD_ENTER) {
11578 if (dd->highlight_index >= 0 && dd->highlight_index < nb) {
11579 N_GUI_DROPMENU_ENTRY* entry = &dd->entries[dd->highlight_index];
11580 if (entry->on_click) entry->on_click(w->id, dd->highlight_index, entry->tag, entry->user_data);
11581 }
11582 dd->is_open = 0;
11583 ctx->open_dropmenu_id = -1;
11584 event_consumed = 1;
11585 } else if (kc == ALLEGRO_KEY_ESCAPE) {
11586 dd->is_open = 0;
11587 ctx->open_dropmenu_id = -1;
11588 event_consumed = 1;
11589 }
11590 }
11591 }
11592 }
11593
11594 if (!event_consumed && event.type == ALLEGRO_EVENT_KEY_CHAR && event.keyboard.keycode == ALLEGRO_KEY_TAB) {
11595 int ctrl = (event.keyboard.modifiers & ALLEGRO_KEYMOD_CTRL) ? 1 : 0;
11596 int shift = (event.keyboard.modifiers & ALLEGRO_KEYMOD_SHIFT) ? 1 : 0;
11597
11598 /* Ctrl+Tab in a focused textarea inserts a tab character, skip navigation */
11599 int is_ctrl_tab_in_textarea = 0;
11600 if (ctrl && ctx->focused_widget_id >= 0) {
11601 const N_GUI_WIDGET* fw = n_gui_get_widget(ctx, ctx->focused_widget_id);
11602 if (fw && fw->type == N_GUI_TYPE_TEXTAREA && (fw->state & N_GUI_STATE_FOCUSED))
11603 is_ctrl_tab_in_textarea = 1;
11604 }
11605
11606 if (!is_ctrl_tab_in_textarea) {
11607 /* find the window that contains the focused widget (or the topmost window) */
11608 N_GUI_WINDOW* tab_win = NULL;
11609 if (ctx->focused_widget_id >= 0) {
11610 tab_win = _find_focused_window(ctx);
11611 }
11612 if (!tab_win && ctx->windows && ctx->windows->end) {
11613 /* no focus yet, use topmost open window */
11614 for (LIST_NODE* wn = ctx->windows->end; wn; wn = wn->prev) {
11615 N_GUI_WINDOW* w = (N_GUI_WINDOW*)wn->ptr;
11616 /* display routing: only windows on the pass this event belongs to */
11617 if (!_win_on_pass(ctx, w)) continue;
11618 if (w && (w->state & N_GUI_WIN_OPEN) && !(w->state & N_GUI_WIN_MINIMISED)) {
11619 tab_win = w;
11620 break;
11621 }
11622 }
11623 }
11624
11625 if (tab_win && tab_win->widgets && tab_win->widgets->nb_items > 0) {
11626 /* build an ordered list of focusable widget ids */
11627 int focusable_ids[512];
11628 int nfocusable = 0;
11629 list_foreach(wgn, tab_win->widgets) {
11630 const N_GUI_WIDGET* w = (N_GUI_WIDGET*)wgn->ptr;
11631 if (w && w->visible && w->enabled && _is_focusable_type(w->type) && nfocusable < 512) {
11632 focusable_ids[nfocusable++] = w->id;
11633 }
11634 }
11635 if (nfocusable > 0) {
11636 /* find current position */
11637 int cur_idx = -1;
11638 for (int i = 0; i < nfocusable; i++) {
11639 if (focusable_ids[i] == ctx->focused_widget_id) {
11640 cur_idx = i;
11641 break;
11642 }
11643 }
11644 int next_idx;
11645 if (shift) {
11646 next_idx = (cur_idx <= 0) ? nfocusable - 1 : cur_idx - 1;
11647 } else {
11648 next_idx = (cur_idx < 0 || cur_idx >= nfocusable - 1) ? 0 : cur_idx + 1;
11649 }
11650 n_gui_set_focus(ctx, focusable_ids[next_idx]);
11651 }
11652 }
11653 event_consumed = 1;
11654 }
11655 }
11656
11657 /* keyboard events -> focused textarea */
11658 if (!event_consumed && event.type == ALLEGRO_EVENT_KEY_CHAR && ctx->focused_widget_id != -1) {
11660 if (fw && fw->type == N_GUI_TYPE_TEXTAREA && (fw->state & N_GUI_STATE_FOCUSED)) {
11661 ALLEGRO_FONT* ta_font = fw->font ? fw->font : ctx->default_font;
11662 float ta_pad = ctx->style.textarea_padding;
11663 float ta_sb = ctx->style.scrollbar_size;
11664 if (_textarea_handle_key(fw, &event, ta_font, ta_pad, ta_sb, ctx)) {
11665 event_consumed = 1;
11666 /* select-to-copy: a shift-navigation selection goes to PRIMARY */
11667 if (event.keyboard.modifiers & ALLEGRO_KEYMOD_SHIFT)
11669 }
11670 }
11671 }
11672
11673 /* keyboard events -> focused non-textarea widgets (slider, listbox, radiolist, combobox, scrollbar, checkbox) */
11674 if (!event_consumed && event.type == ALLEGRO_EVENT_KEY_CHAR && ctx->focused_widget_id >= 0) {
11676 if (fw && fw->visible && fw->enabled && (fw->state & N_GUI_STATE_FOCUSED) && fw->data) {
11677 int kc = event.keyboard.keycode;
11678
11679 /* slider keyboard */
11680 if (fw->type == N_GUI_TYPE_SLIDER) {
11682 double step = sd->step > 0.0 ? sd->step : (sd->max_val - sd->min_val) / 20.0;
11683 if (step <= 0) step = 1.0;
11684 double new_val = sd->value;
11685 int handled = 0;
11686
11687 if (sd->orientation == N_GUI_SLIDER_H) {
11688 if (kc == ALLEGRO_KEY_RIGHT) {
11689 new_val += step;
11690 handled = 1;
11691 } else if (kc == ALLEGRO_KEY_LEFT) {
11692 new_val -= step;
11693 handled = 1;
11694 }
11695 } else {
11696 if (kc == ALLEGRO_KEY_UP) {
11697 new_val += step;
11698 handled = 1;
11699 } else if (kc == ALLEGRO_KEY_DOWN) {
11700 new_val -= step;
11701 handled = 1;
11702 }
11703 }
11704 if (kc == ALLEGRO_KEY_HOME) {
11705 new_val = sd->min_val;
11706 handled = 1;
11707 } else if (kc == ALLEGRO_KEY_END) {
11708 new_val = sd->max_val;
11709 handled = 1;
11710 }
11711
11712 if (handled) {
11713 if (sd->step > 0.0)
11714 new_val = _slider_snap_value(new_val, sd->min_val, sd->max_val, sd->step);
11715 else
11716 new_val = _clamp(new_val, sd->min_val, sd->max_val);
11717 if (new_val != sd->value) {
11718 sd->value = new_val;
11719 if (sd->on_change) sd->on_change(fw->id, sd->value, sd->user_data);
11720 }
11721 event_consumed = 1;
11722 }
11723 }
11724
11725 /* listbox keyboard */
11726 if (fw->type == N_GUI_TYPE_LISTBOX) {
11728 if (ld->selection_mode != N_GUI_SELECT_NONE && ld->nb_items > 0) {
11729 int handled = 0;
11730 /* find first selected item for single-select navigation */
11731 int cur_sel = -1;
11733 for (size_t i = 0; i < ld->nb_items; i++) {
11734 if (ld->items[i].selected) {
11735 cur_sel = (int)i;
11736 break;
11737 }
11738 }
11739 }
11740 int new_sel = cur_sel;
11741 if (kc == ALLEGRO_KEY_UP && cur_sel > 0) {
11742 new_sel = cur_sel - 1;
11743 handled = 1;
11744 } else if (kc == ALLEGRO_KEY_DOWN && cur_sel < (int)ld->nb_items - 1) {
11745 new_sel = cur_sel + 1;
11746 handled = 1;
11747 } else if (kc == ALLEGRO_KEY_DOWN && cur_sel < 0) {
11748 new_sel = 0;
11749 handled = 1;
11750 } else if (kc == ALLEGRO_KEY_HOME) {
11751 new_sel = 0;
11752 handled = 1;
11753 } else if (kc == ALLEGRO_KEY_END) {
11754 new_sel = (int)ld->nb_items - 1;
11755 handled = 1;
11756 }
11757
11758 if (handled && ld->selection_mode == N_GUI_SELECT_SINGLE && new_sel != cur_sel) {
11759 for (size_t i = 0; i < ld->nb_items; i++) ld->items[i].selected = 0;
11760 ld->items[new_sel].selected = 1;
11761 if (ld->on_select) ld->on_select(fw->id, new_sel, 1, ld->user_data);
11762 /* auto-scroll to keep selection visible */
11763 ALLEGRO_FONT* lf = fw->font ? fw->font : ctx->default_font;
11764 float lfh = lf ? (float)al_get_font_line_height(lf) : 16.0f;
11765 float lih = ld->item_height > lfh ? ld->item_height : lfh + ctx->style.item_height_pad;
11766 int visible = (int)(fw->h / lih);
11767 if (new_sel < ld->scroll_offset) ld->scroll_offset = new_sel;
11768 if (new_sel >= ld->scroll_offset + visible) ld->scroll_offset = new_sel - visible + 1;
11769 event_consumed = 1;
11770 }
11771 }
11772 }
11773
11774 /* radiolist keyboard */
11775 if (fw->type == N_GUI_TYPE_RADIOLIST) {
11777 if (rd->nb_items > 0) {
11778 int cur_sel = rd->selected_index;
11779 int new_sel = cur_sel;
11780 int handled = 0;
11781 if (kc == ALLEGRO_KEY_UP && cur_sel > 0) {
11782 new_sel = cur_sel - 1;
11783 handled = 1;
11784 } else if (kc == ALLEGRO_KEY_DOWN && cur_sel < (int)rd->nb_items - 1) {
11785 new_sel = cur_sel + 1;
11786 handled = 1;
11787 } else if (kc == ALLEGRO_KEY_DOWN && cur_sel < 0) {
11788 new_sel = 0;
11789 handled = 1;
11790 } else if (kc == ALLEGRO_KEY_HOME) {
11791 new_sel = 0;
11792 handled = 1;
11793 } else if (kc == ALLEGRO_KEY_END) {
11794 new_sel = (int)rd->nb_items - 1;
11795 handled = 1;
11796 }
11797
11798 if (handled && new_sel != cur_sel) {
11799 rd->selected_index = new_sel;
11800 if (rd->on_select) rd->on_select(fw->id, new_sel, rd->user_data);
11801 /* auto-scroll to keep selection visible */
11802 ALLEGRO_FONT* rf = fw->font ? fw->font : ctx->default_font;
11803 float rfh = rf ? (float)al_get_font_line_height(rf) : 16.0f;
11804 float rih = rd->item_height > rfh ? rd->item_height : rfh + ctx->style.item_height_pad;
11805 int visible = (int)(fw->h / rih);
11806 if (new_sel < rd->scroll_offset) rd->scroll_offset = new_sel;
11807 if (new_sel >= rd->scroll_offset + visible) rd->scroll_offset = new_sel - visible + 1;
11808 event_consumed = 1;
11809 }
11810 }
11811 }
11812
11813 /* combobox keyboard (when closed) */
11814 if (fw->type == N_GUI_TYPE_COMBOBOX) {
11816 if (!cd->is_open && cd->nb_items > 0) {
11817 int cur_sel = cd->selected_index;
11818 int new_sel = cur_sel;
11819 int handled = 0;
11820 if (kc == ALLEGRO_KEY_UP && cur_sel > 0) {
11821 new_sel = cur_sel - 1;
11822 handled = 1;
11823 } else if (kc == ALLEGRO_KEY_DOWN && cur_sel < (int)cd->nb_items - 1) {
11824 new_sel = cur_sel + 1;
11825 handled = 1;
11826 } else if (kc == ALLEGRO_KEY_DOWN && cur_sel < 0) {
11827 new_sel = 0;
11828 handled = 1;
11829 } else if (kc == ALLEGRO_KEY_HOME) {
11830 new_sel = 0;
11831 handled = 1;
11832 } else if (kc == ALLEGRO_KEY_END) {
11833 new_sel = (int)cd->nb_items - 1;
11834 handled = 1;
11835 }
11836
11837 if (handled && new_sel != cur_sel) {
11838 cd->selected_index = new_sel;
11839 if (cd->on_select) cd->on_select(fw->id, new_sel, cd->user_data);
11840 event_consumed = 1;
11841 }
11842 }
11843 }
11844
11845 /* scrollbar keyboard */
11846 if (fw->type == N_GUI_TYPE_SCROLLBAR) {
11848 double max_scroll = sb->content_size - sb->viewport_size;
11849 if (max_scroll > 0) {
11850 double step = max_scroll / 20.0;
11851 if (step <= 0) step = 1.0;
11852 double new_pos = sb->scroll_pos;
11853 int handled = 0;
11854 if (sb->orientation == N_GUI_SCROLLBAR_V) {
11855 if (kc == ALLEGRO_KEY_UP) {
11856 new_pos -= step;
11857 handled = 1;
11858 } else if (kc == ALLEGRO_KEY_DOWN) {
11859 new_pos += step;
11860 handled = 1;
11861 }
11862 } else {
11863 if (kc == ALLEGRO_KEY_LEFT) {
11864 new_pos -= step;
11865 handled = 1;
11866 } else if (kc == ALLEGRO_KEY_RIGHT) {
11867 new_pos += step;
11868 handled = 1;
11869 }
11870 }
11871 if (kc == ALLEGRO_KEY_HOME) {
11872 new_pos = 0;
11873 handled = 1;
11874 } else if (kc == ALLEGRO_KEY_END) {
11875 new_pos = max_scroll;
11876 handled = 1;
11877 }
11878
11879 if (handled) {
11880 if (new_pos < 0) new_pos = 0;
11881 if (new_pos > max_scroll) new_pos = max_scroll;
11882 if (new_pos != sb->scroll_pos) {
11883 sb->scroll_pos = new_pos;
11884 if (sb->on_scroll) sb->on_scroll(fw->id, sb->scroll_pos, sb->user_data);
11885 }
11886 event_consumed = 1;
11887 }
11888 }
11889 }
11890
11891 /* checkbox keyboard (Space/Enter to toggle) */
11892 if (fw->type == N_GUI_TYPE_CHECKBOX) {
11893 if (kc == ALLEGRO_KEY_SPACE || kc == ALLEGRO_KEY_ENTER) {
11895 cd->checked = !cd->checked;
11896 if (cd->on_toggle) cd->on_toggle(fw->id, cd->checked, cd->user_data);
11897 event_consumed = 1;
11898 }
11899 }
11900 }
11901 }
11902
11903 /* Ctrl+C on label selection: copy selected text from the most recently selected label */
11904 if (!event_consumed && event.type == ALLEGRO_EVENT_KEY_DOWN &&
11905 (event.keyboard.modifiers & ALLEGRO_KEYMOD_CTRL) &&
11906 event.keyboard.keycode == ALLEGRO_KEY_C && _ctx_io_display(ctx) &&
11907 ctx->selected_label_id >= 0) {
11908 N_GUI_WIDGET* sel_wgt = n_gui_get_widget(ctx, ctx->selected_label_id);
11909 if (sel_wgt && sel_wgt->type == N_GUI_TYPE_LABEL && sel_wgt->data) {
11910 N_GUI_LABEL_DATA* lb = (N_GUI_LABEL_DATA*)sel_wgt->data;
11911 if (lb->sel_start >= 0 && lb->sel_end >= 0 && lb->sel_start != lb->sel_end) {
11912 int slo = lb->sel_start < lb->sel_end ? lb->sel_start : lb->sel_end;
11913 int shi = lb->sel_start < lb->sel_end ? lb->sel_end : lb->sel_start;
11914 size_t tlen = strlen(lb->text);
11915 if ((size_t)slo > tlen) slo = (int)tlen;
11916 if ((size_t)shi > tlen) shi = (int)tlen;
11917 int len = shi - slo;
11918 if (len > 0) {
11919 char clip[N_GUI_TEXT_MAX];
11920 memcpy(clip, &lb->text[slo], (size_t)len);
11921 clip[len] = '\0';
11923 event_consumed = 1;
11924 }
11925 }
11926 }
11927 }
11928
11929 /* Ctrl+A selects all and Ctrl+C copies the selection of the active syntax view */
11930 if (!event_consumed && event.type == ALLEGRO_EVENT_KEY_DOWN &&
11931 (event.keyboard.modifiers & ALLEGRO_KEYMOD_CTRL) &&
11932 ctx->selected_syntaxview_id >= 0 &&
11933 (event.keyboard.keycode == ALLEGRO_KEY_C || event.keyboard.keycode == ALLEGRO_KEY_A)) {
11934 if (event.keyboard.keycode == ALLEGRO_KEY_A) {
11936 _ngui_publish_primary_selection(ctx); /* select-to-copy: mirror to PRIMARY */
11937 event_consumed = 1;
11938 } else if (_ctx_io_display(ctx)) {
11940 if (sel) {
11942 Free(sel);
11943 event_consumed = 1;
11944 }
11945 }
11946 }
11947
11948 /* keyboard events -> button key bindings.
11949 * Two passes:
11950 * 1. Focused bindings (key_focus_only=1): fire only when the focused widget
11951 * is the button itself or one of the button's key_sources. These are
11952 * checked first and bypass the widget_focused guard so they work even
11953 * when a textarea has focus.
11954 * 2. Global bindings (key_focus_only=0): fire when no interactive widget
11955 * has focus. Exception: ENTER passes through single-line textareas. */
11956 if (event.type == ALLEGRO_EVENT_KEY_DOWN && !event_consumed) {
11957 int kc = event.keyboard.keycode;
11958 int ev_mods = event.keyboard.modifiers & N_GUI_KEY_MOD_MASK;
11959
11960 /* Pass 1: focused key bindings, check before the widget_focused guard */
11961 for (LIST_NODE* wnode = ctx->windows->end; wnode && !event_consumed; wnode = wnode->prev) {
11962 N_GUI_WINDOW* win = (N_GUI_WINDOW*)wnode->ptr;
11963 /* display routing: only windows on the pass this event belongs to */
11964 if (!_win_on_pass(ctx, win)) continue;
11965 if (!(win->state & N_GUI_WIN_OPEN)) {
11966 continue;
11967 }
11968 list_foreach(wgn, win->widgets) {
11969 N_GUI_WIDGET* wgt = (N_GUI_WIDGET*)wgn->ptr;
11970 if (!wgt || !wgt->visible || !wgt->enabled || wgt->type != N_GUI_TYPE_BUTTON) {
11971 continue;
11972 }
11974 if (!bd || !bd->key_focus_only || bd->keycode != kc) {
11975 continue;
11976 }
11977 if (bd->key_modifiers != 0 && ev_mods != bd->key_modifiers) {
11978 continue;
11979 }
11980 /* check if focused widget is this button or a listed source */
11981 int focus_match = (ctx->focused_widget_id == wgt->id);
11982 if (!focus_match) {
11983 for (int i = 0; i < N_GUI_KEY_SOURCES_MAX && bd->key_sources[i] >= 0; i++) {
11984 if (ctx->focused_widget_id == bd->key_sources[i]) {
11985 focus_match = 1;
11986 break;
11987 }
11988 }
11989 }
11990 if (!focus_match) {
11991 continue;
11992 }
11993 if (bd->toggle_mode) {
11994 bd->toggled = !bd->toggled;
11995 }
11996 /* keybind-triggered press: flash the active visual briefly so the
11997 * user sees the button react just like on a mouse click. */
11998 bd->key_press_until = al_get_time() + N_GUI_KEY_PRESS_FLASH_SEC;
11999 if (bd->key_press_until + N_GUI_ANIM_TAIL_SEC > ctx->anim_until) ctx->anim_until = bd->key_press_until + N_GUI_ANIM_TAIL_SEC; /* keep redrawing through the flash */
12000 if (bd->on_click) {
12001 bd->on_click(wgt->id, bd->user_data);
12002 }
12003 event_consumed = 1;
12004 break;
12005 }
12006 if (event_consumed) {
12007 /* the on_click callback may have mutated ctx->windows,
12008 * n_gui_add_window's z-order re-sort rebuilds the list and
12009 * frees every node, so wnode must not be stepped by the
12010 * for-increment. Exit the window loop without touching it. */
12011 break;
12012 }
12013 }
12014
12015 /* Pass 2: global key bindings, skip when an interactive widget has focus.
12016 * Exception: for single-line textareas, ENTER passes through. */
12017 if (!event_consumed) {
12018 int widget_focused = 0;
12019 if (ctx->focused_widget_id >= 0) {
12021 if (fw && (fw->state & N_GUI_STATE_FOCUSED)) {
12022 if (fw->type == N_GUI_TYPE_TEXTAREA) {
12023 const N_GUI_TEXTAREA_DATA* ftd = (N_GUI_TEXTAREA_DATA*)fw->data;
12024 /* for single-line textareas, allow ENTER to pass through */
12025 if (ftd && (ftd->multiline || kc != ALLEGRO_KEY_ENTER)) {
12026 widget_focused = 1;
12027 }
12028 } else if (_is_focusable_type(fw->type)) {
12029 widget_focused = 1;
12030 }
12031 }
12032 }
12033 if (!widget_focused) {
12034 /* Iterate windows from frontmost (end of list) to backmost (start)
12035 * and stop as soon as a button consumes the event. */
12036 for (LIST_NODE* wnode = ctx->windows->end; wnode && !event_consumed; wnode = wnode->prev) {
12037 N_GUI_WINDOW* win = (N_GUI_WINDOW*)wnode->ptr;
12038 /* display routing: only windows on the pass this event belongs to */
12039 if (!_win_on_pass(ctx, win)) continue;
12040 if (!(win->state & N_GUI_WIN_OPEN)) {
12041 continue;
12042 }
12043 list_foreach(wgn, win->widgets) {
12044 N_GUI_WIDGET* wgt = (N_GUI_WIDGET*)wgn->ptr;
12045 if (!wgt || !wgt->visible || !wgt->enabled || wgt->type != N_GUI_TYPE_BUTTON) {
12046 continue;
12047 }
12049 if (bd && !bd->key_focus_only && bd->keycode == kc) {
12050 if (bd->key_modifiers != 0 && ev_mods != bd->key_modifiers) {
12051 continue;
12052 }
12053 if (bd->toggle_mode) {
12054 bd->toggled = !bd->toggled;
12055 }
12056 /* keybind-triggered press: flash the active visual
12057 * briefly so the user sees the button react just
12058 * like on a mouse click. */
12059 bd->key_press_until = al_get_time() + N_GUI_KEY_PRESS_FLASH_SEC;
12060 if (bd->key_press_until + N_GUI_ANIM_TAIL_SEC > ctx->anim_until) ctx->anim_until = bd->key_press_until + N_GUI_ANIM_TAIL_SEC; /* keep redrawing through the flash */
12061 if (bd->on_click) {
12062 bd->on_click(wgt->id, bd->user_data);
12063 }
12064 event_consumed = 1;
12065 break;
12066 }
12067 }
12068 if (event_consumed) {
12069 /* same node-invalidation guard as pass 1 above:
12070 * the callback may have rebuilt ctx->windows. */
12071 break;
12072 }
12073 }
12074 }
12075 }
12076 }
12077
12078 /* mouse wheel scrolling for listbox/radiolist/combobox/dropmenu and auto-scrollbar windows.
12079 * Iterate from topmost window (end of list) to bottommost (start) so that the
12080 * frontmost window under the cursor receives the scroll event first. */
12081 if (event.type == ALLEGRO_EVENT_MOUSE_AXES && event.mouse.dz != 0) {
12082 int scroll_consumed = 0;
12083
12084 /* An open combobox consumes the wheel to step its highlight (scrolling to
12085 follow), even when the pointer is not directly over the floating panel. */
12086 if (ctx->open_combobox_id >= 0) {
12087 N_GUI_WIDGET* cb_wgt = n_gui_get_widget(ctx, ctx->open_combobox_id);
12088 if (cb_wgt && cb_wgt->data) {
12090 if (cbd->is_open && cbd->nb_items > 0) {
12092 (int)cbd->nb_items, cbd->max_visible,
12093 -event.mouse.dz, cbd->selected_index);
12094 scroll_consumed = 1;
12095 }
12096 }
12097 }
12098 /* Likewise an open dropmenu panel. */
12099 if (!scroll_consumed && ctx->open_dropmenu_id >= 0) {
12100 N_GUI_WIDGET* dm_wgt = n_gui_get_widget(ctx, ctx->open_dropmenu_id);
12101 if (dm_wgt && dm_wgt->data) {
12103 if (dmd->is_open && dmd->nb_entries > 0) {
12105 (int)dmd->nb_entries, dmd->max_visible,
12106 -event.mouse.dz, -1);
12107 scroll_consumed = 1;
12108 }
12109 }
12110 }
12111 if (scroll_consumed) return 1;
12112
12113 for (LIST_NODE* wnode = ctx->windows->end; wnode; wnode = wnode->prev) {
12114 N_GUI_WINDOW* win = (N_GUI_WINDOW*)wnode->ptr;
12115 /* display routing: only windows on the pass this event belongs to */
12116 if (!_win_on_pass(ctx, win)) continue;
12117 if (!(win->state & N_GUI_WIN_OPEN) || (win->state & N_GUI_WIN_MINIMISED)) continue;
12118
12119 float win_h = win->h;
12120 if (!_point_in_rect(mx, my, win->x, win->y, win->w, win_h)) continue;
12121
12122 float content_x = win->x;
12123 float content_y = win->y + _win_tbh(win);
12124
12125 /* first check if a widget wants the scroll */
12126 list_foreach(wgn, win->widgets) {
12127 N_GUI_WIDGET* wgt = (N_GUI_WIDGET*)wgn->ptr;
12128 if (!wgt || !wgt->visible || !wgt->enabled) continue;
12129 float ax = content_x + wgt->x - win->scroll_x;
12130 float ay = content_y + wgt->y - win->scroll_y;
12131 if (!_point_in_rect(mx, my, ax, ay, wgt->w, wgt->h)) continue;
12132
12133 if (wgt->type == N_GUI_TYPE_LISTBOX) {
12135 ld->scroll_offset -= event.mouse.dz;
12136 if (ld->scroll_offset < 0) ld->scroll_offset = 0;
12137 ALLEGRO_FONT* lf = wgt->font ? wgt->font : ctx->default_font;
12138 float lfh = lf ? (float)al_get_font_line_height(lf) : 16.0f;
12139 float lih = ld->item_height > lfh ? ld->item_height : lfh + ctx->style.item_height_pad;
12140 int visible_count = (int)(wgt->h / lih);
12141 int max_off = (int)ld->nb_items - visible_count;
12142 if (max_off < 0) max_off = 0;
12143 if (ld->scroll_offset > max_off) ld->scroll_offset = max_off;
12144 scroll_consumed = 1;
12145 }
12146 if (wgt->type == N_GUI_TYPE_HEXVIEW) {
12148 ALLEGRO_FONT* hf = wgt->font ? wgt->font : ctx->default_font;
12149 float hfh = hf ? (float)al_get_font_line_height(hf) : 16.0f;
12150 float row_h = hfh + 2.0f;
12151 int bpr = hd->bytes_per_row > 0 ? hd->bytes_per_row : 16;
12152 int nb_rows = (int)((hd->len + (size_t)bpr - 1) / (size_t)bpr);
12153 int visible_count = (int)((wgt->h - ctx->style.textarea_padding * 2.0f) / row_h);
12154 int max_off;
12155 if (visible_count < 1) visible_count = 1;
12156 max_off = nb_rows - visible_count;
12157 if (max_off < 0) max_off = 0;
12158 hd->scroll_offset -= event.mouse.dz;
12159 if (hd->scroll_offset < 0) hd->scroll_offset = 0;
12160 if (hd->scroll_offset > max_off) hd->scroll_offset = max_off;
12161 scroll_consumed = 1;
12162 }
12163 if (wgt->type == N_GUI_TYPE_SYNTAXVIEW) {
12165 ALLEGRO_FONT* yf = wgt->font ? wgt->font : ctx->default_font;
12166 float yfh = yf ? (float)al_get_font_line_height(yf) : 16.0f;
12167 float row_h = yfh + 2.0f;
12168 int nb_lines = (_syntaxview_ensure_lines(yd), yd->cached_nb_lines);
12169 int visible_count = (int)((wgt->h - ctx->style.textarea_padding * 2.0f) / row_h);
12170 int max_off;
12171 if (visible_count < 1) visible_count = 1;
12172 max_off = nb_lines - visible_count;
12173 if (max_off < 0) max_off = 0;
12174 yd->scroll_offset -= event.mouse.dz;
12175 if (yd->scroll_offset < 0) yd->scroll_offset = 0;
12176 if (yd->scroll_offset > max_off) yd->scroll_offset = max_off;
12177 scroll_consumed = 1;
12178 }
12179 if (wgt->type == N_GUI_TYPE_DATAGRID) {
12181 ALLEGRO_FONT* gf = wgt->font ? wgt->font : ctx->default_font;
12182 float gfh = gf ? (float)al_get_font_line_height(gf) : 16.0f;
12183 float row_h = gfh + ctx->style.item_height_pad;
12184 float data_h = wgt->h - row_h;
12185 int visible_count = (int)(data_h / row_h);
12186 int max_off;
12187 if (visible_count < 1) visible_count = 1;
12188 max_off = (int)gd->nb_rows - visible_count;
12189 if (max_off < 0) max_off = 0;
12190 /* Shift+wheel scrolls horizontally (the columns), plain wheel vertically */
12191 int dg_shift = 0;
12192 if (al_is_keyboard_installed()) {
12193 ALLEGRO_KEYBOARD_STATE kb;
12194 al_get_keyboard_state(&kb);
12195 dg_shift = al_key_down(&kb, ALLEGRO_KEY_LSHIFT) || al_key_down(&kb, ALLEGRO_KEY_RSHIFT);
12196 }
12197 if (dg_shift) {
12198 gd->h_scroll -= (float)event.mouse.dz * (row_h * 2.0f);
12199 if (gd->h_scroll < 0) gd->h_scroll = 0;
12200 /* the draw path clamps the upper bound to the live content width */
12201 } else {
12202 gd->scroll_offset -= event.mouse.dz;
12203 if (gd->scroll_offset < 0) gd->scroll_offset = 0;
12204 if (gd->scroll_offset > max_off) gd->scroll_offset = max_off;
12205 }
12206 scroll_consumed = 1;
12207 }
12208 if (wgt->type == N_GUI_TYPE_RADIOLIST) {
12210 rd->scroll_offset -= event.mouse.dz;
12211 if (rd->scroll_offset < 0) rd->scroll_offset = 0;
12212 ALLEGRO_FONT* rf = wgt->font ? wgt->font : ctx->default_font;
12213 float rfh = rf ? (float)al_get_font_line_height(rf) : 16.0f;
12214 float rih = rd->item_height > rfh ? rd->item_height : rfh + ctx->style.item_height_pad;
12215 int visible_count = (int)(wgt->h / rih);
12216 int max_off = (int)rd->nb_items - visible_count;
12217 if (max_off < 0) max_off = 0;
12218 if (rd->scroll_offset > max_off) rd->scroll_offset = max_off;
12219 scroll_consumed = 1;
12220 }
12221 if (wgt->type == N_GUI_TYPE_TEXTAREA) {
12223 if (std->multiline) {
12224 float scroll_step = ctx->style.scroll_step;
12225 std->scroll_y -= (int)((float)event.mouse.dz * scroll_step);
12226 std->scroll_from_wheel = 1;
12227 /* clamp is done at draw time */
12228 scroll_consumed = 1;
12229 }
12230 }
12231 if (wgt->type == N_GUI_TYPE_SLIDER) {
12233 double step = sd->step > 0.0 ? sd->step : (sd->max_val - sd->min_val) / 20.0;
12234 if (step <= 0) step = 1.0;
12235 double new_val = sd->value + (double)event.mouse.dz * step;
12236 if (sd->step > 0.0)
12237 new_val = _slider_snap_value(new_val, sd->min_val, sd->max_val, sd->step);
12238 else
12239 new_val = _clamp(new_val, sd->min_val, sd->max_val);
12240 if (new_val != sd->value) {
12241 sd->value = new_val;
12242 if (sd->on_change) {
12243 sd->on_change(wgt->id, sd->value, sd->user_data);
12244 }
12245 }
12246 scroll_consumed = 1;
12247 }
12248 if (wgt->type == N_GUI_TYPE_SCROLLBAR) {
12250 double max_scroll = sbd->content_size - sbd->viewport_size;
12251 if (max_scroll > 0) {
12252 double step = max_scroll / 20.0;
12253 if (step <= 0) step = 1.0;
12254 sbd->scroll_pos -= (double)event.mouse.dz * step;
12255 if (sbd->scroll_pos < 0) sbd->scroll_pos = 0;
12256 if (sbd->scroll_pos > max_scroll) sbd->scroll_pos = max_scroll;
12257 if (sbd->on_scroll) {
12258 sbd->on_scroll(wgt->id, sbd->scroll_pos, sbd->user_data);
12259 }
12260 scroll_consumed = 1;
12261 }
12262 }
12263 if (wgt->type == N_GUI_TYPE_COMBOBOX) {
12265 if (cbd->is_open && cbd->nb_items > 0) {
12266 cbd->scroll_offset -= event.mouse.dz;
12267 if (cbd->scroll_offset < 0) cbd->scroll_offset = 0;
12268 int max_vis = cbd->max_visible > 0 ? cbd->max_visible : 8;
12269 int max_off = (int)cbd->nb_items - max_vis;
12270 if (max_off < 0) max_off = 0;
12271 if (cbd->scroll_offset > max_off) cbd->scroll_offset = max_off;
12272 scroll_consumed = 1;
12273 }
12274 }
12275 if (wgt->type == N_GUI_TYPE_DROPMENU) {
12277 if (dmd->is_open && dmd->nb_entries > 0) {
12278 dmd->scroll_offset -= event.mouse.dz;
12279 if (dmd->scroll_offset < 0) dmd->scroll_offset = 0;
12280 int max_vis = dmd->max_visible > 0 ? dmd->max_visible : 8;
12281 int max_off = (int)dmd->nb_entries - max_vis;
12282 if (max_off < 0) max_off = 0;
12283 if (dmd->scroll_offset > max_off) dmd->scroll_offset = max_off;
12284 scroll_consumed = 1;
12285 }
12286 }
12287 if (wgt->type == N_GUI_TYPE_LABEL) {
12289 if (sld->align == N_GUI_ALIGN_JUSTIFIED) {
12290 float scroll_step = ctx->style.scroll_step;
12291 sld->scroll_y -= (float)event.mouse.dz * scroll_step;
12292 /* clamp is done at draw time */
12293 scroll_consumed = 1;
12294 }
12295 }
12296 /* Stop at the first widget under the cursor that actually consumes
12297 * the wheel. A non-scrolling widget that merely overlaps the cursor
12298 * (e.g. a splitpane spanning a datagrid it sits beneath) must not
12299 * swallow the event: keep scanning so a scrollable widget drawn on
12300 * top of it still receives the scroll. */
12301 if (scroll_consumed) break;
12302 }
12303
12304 /* if no widget consumed the scroll, let the window auto-scroll */
12305 if (!scroll_consumed && (win->flags & N_GUI_WIN_AUTO_SCROLLBAR)) {
12306 float scroll_step = ctx->style.scroll_step;
12307 win->scroll_y -= (float)event.mouse.dz * scroll_step;
12308 /* clamp is done at draw time */
12309 scroll_consumed = 1;
12310 }
12311 if (scroll_consumed) event_consumed = 1;
12312 break; /* only one window gets scroll */
12313 }
12314
12315 /* if no window consumed the scroll and global scrollbars are available,
12316 * scroll the global viewport */
12317 if (!scroll_consumed && eff_w > 0 && eff_h > 0) {
12319 if (ctx->gui_bounds_h > eff_h || ctx->gui_bounds_w > eff_w) {
12320 float scroll_step = ctx->style.global_scroll_step;
12321 ctx->global_scroll_y -= (float)event.mouse.dz * scroll_step;
12322 /* clamp is done at draw time */
12323 event_consumed = 1;
12324 }
12325 }
12326 }
12327
12328 return event_consumed;
12329}
12330
12338 __n_assert(ctx, return 0);
12339 float mx = (float)ctx->mouse_x;
12340 float my = (float)ctx->mouse_y;
12341 /* transform to virtual space when virtual canvas is active */
12342 if (ctx->virtual_w > 0 && ctx->virtual_h > 0 && ctx->gui_scale > 0) {
12343 mx = (mx - ctx->gui_offset_x) / ctx->gui_scale;
12344 my = (my - ctx->gui_offset_y) / ctx->gui_scale;
12345 }
12346 list_foreach(wnode, ctx->windows) {
12347 N_GUI_WINDOW* win = (N_GUI_WINDOW*)wnode->ptr;
12348 if (!(win->state & N_GUI_WIN_OPEN)) continue;
12349 /* the question this answers is "should the host let the main display's
12350 * mouse through to game logic", so detached windows never count: they
12351 * own their display entirely and the host draws nothing else there */
12352 if (win->native) continue;
12353 float win_h = (win->state & N_GUI_WIN_MINIMISED) ? _win_tbh(win) : win->h;
12354 if (_point_in_rect(mx, my, win->x, win->y, win->w, win_h)) {
12355 return 1;
12356 }
12357 }
12358 return 0;
12359}
12360
12361/* JSON THEME I/O (requires cJSON) */
12362
12363#ifdef HAVE_CJSON
12364
12365#ifndef __windows__
12366#include <fcntl.h>
12367#include <unistd.h>
12368
12369/* Open a file for writing with explicit owner-only write permission (0644)
12370 * instead of the world-writable 0666 that fopen() requests before the umask
12371 * is applied. On Windows the POSIX mode bits do not apply, so fopen() is used
12372 * directly. */
12373static FILE* _gui_fopen_write(const char* path, const char* mode) {
12374 int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
12375 if (fd < 0)
12376 return NULL;
12377 FILE* f = fdopen(fd, mode);
12378 if (!f)
12379 close(fd);
12380 return f;
12381}
12382#else
12383#define _gui_fopen_write(path, mode) fopen((path), (mode))
12384#endif
12385
12387static void _json_add_color(cJSON* parent, const char* name, ALLEGRO_COLOR c) {
12388 unsigned char r, g, b, a;
12389 al_unmap_rgba(c, &r, &g, &b, &a);
12390 cJSON* arr = cJSON_CreateArray();
12391 cJSON_AddItemToArray(arr, cJSON_CreateNumber(r));
12392 cJSON_AddItemToArray(arr, cJSON_CreateNumber(g));
12393 cJSON_AddItemToArray(arr, cJSON_CreateNumber(b));
12394 cJSON_AddItemToArray(arr, cJSON_CreateNumber(a));
12395 cJSON_AddItemToObject(parent, name, arr);
12396}
12397
12399static ALLEGRO_COLOR _json_get_color(cJSON* parent, const char* name, ALLEGRO_COLOR fallback) {
12400 cJSON* arr = cJSON_GetObjectItemCaseSensitive(parent, name);
12401 if (!arr || !cJSON_IsArray(arr) || cJSON_GetArraySize(arr) < 4) return fallback;
12402 int r = (int)cJSON_GetArrayItem(arr, 0)->valuedouble;
12403 int g = (int)cJSON_GetArrayItem(arr, 1)->valuedouble;
12404 int b = (int)cJSON_GetArrayItem(arr, 2)->valuedouble;
12405 int a = (int)cJSON_GetArrayItem(arr, 3)->valuedouble;
12406 return al_map_rgba((unsigned char)r, (unsigned char)g, (unsigned char)b, (unsigned char)a);
12407}
12408
12410static float _json_get_float(cJSON* parent, const char* name, float fallback) {
12411 cJSON* item = cJSON_GetObjectItemCaseSensitive(parent, name);
12412 if (!item || !cJSON_IsNumber(item)) return fallback;
12413 return (float)item->valuedouble;
12414}
12415
12417static int _json_get_int(cJSON* parent, const char* name, int fallback) {
12418 cJSON* item = cJSON_GetObjectItemCaseSensitive(parent, name);
12419 if (!item || !cJSON_IsNumber(item)) return fallback;
12420 return (int)item->valuedouble;
12421}
12422
12429int n_gui_save_theme_json(N_GUI_CTX* ctx, const char* filepath) {
12430 __n_assert(ctx, return -1);
12431 __n_assert(filepath, return -1);
12432
12433 cJSON* root = cJSON_CreateObject();
12434 if (!root) return -1;
12435
12436 /* theme colours */
12437 cJSON* theme = cJSON_CreateObject();
12438 N_GUI_THEME* t = &ctx->default_theme;
12439 _json_add_color(theme, "bg_normal", t->bg_normal);
12440 _json_add_color(theme, "bg_hover", t->bg_hover);
12441 _json_add_color(theme, "bg_active", t->bg_active);
12442 _json_add_color(theme, "border_normal", t->border_normal);
12443 _json_add_color(theme, "border_hover", t->border_hover);
12444 _json_add_color(theme, "border_active", t->border_active);
12445 _json_add_color(theme, "text_normal", t->text_normal);
12446 _json_add_color(theme, "text_hover", t->text_hover);
12447 _json_add_color(theme, "text_active", t->text_active);
12448 _json_add_color(theme, "selection_color", t->selection_color);
12449 cJSON_AddNumberToObject(theme, "border_thickness", t->border_thickness);
12450 cJSON_AddNumberToObject(theme, "corner_rx", t->corner_rx);
12451 cJSON_AddNumberToObject(theme, "corner_ry", t->corner_ry);
12452 cJSON_AddItemToObject(root, "theme", theme);
12453
12454 /* style values */
12455 cJSON* sty = cJSON_CreateObject();
12456 N_GUI_STYLE* s = &ctx->style;
12457 cJSON_AddNumberToObject(sty, "titlebar_h", s->titlebar_h);
12458 cJSON_AddNumberToObject(sty, "min_win_w", s->min_win_w);
12459 cJSON_AddNumberToObject(sty, "min_win_h", s->min_win_h);
12460 cJSON_AddNumberToObject(sty, "title_padding", s->title_padding);
12461 cJSON_AddNumberToObject(sty, "title_max_w_reserve", s->title_max_w_reserve);
12462
12463 cJSON_AddNumberToObject(sty, "tb_btn_size", s->tb_btn_size);
12464 cJSON_AddNumberToObject(sty, "tb_btn_spacing", s->tb_btn_spacing);
12465 cJSON_AddNumberToObject(sty, "tb_btn_right_margin", s->tb_btn_right_margin);
12466 cJSON_AddNumberToObject(sty, "tb_btn_glyph_thickness", s->tb_btn_glyph_thickness);
12467
12468 cJSON_AddNumberToObject(sty, "scrollbar_size", s->scrollbar_size);
12469 cJSON_AddNumberToObject(sty, "scrollbar_thumb_min", s->scrollbar_thumb_min);
12470 cJSON_AddNumberToObject(sty, "scrollbar_thumb_padding", s->scrollbar_thumb_padding);
12471 cJSON_AddNumberToObject(sty, "scrollbar_thumb_corner_r", s->scrollbar_thumb_corner_r);
12472 _json_add_color(sty, "scrollbar_track_color", s->scrollbar_track_color);
12473 _json_add_color(sty, "scrollbar_thumb_color", s->scrollbar_thumb_color);
12474
12475 cJSON_AddNumberToObject(sty, "global_scrollbar_size", s->global_scrollbar_size);
12476 cJSON_AddNumberToObject(sty, "global_scrollbar_thumb_min", s->global_scrollbar_thumb_min);
12477 cJSON_AddNumberToObject(sty, "global_scrollbar_thumb_padding", s->global_scrollbar_thumb_padding);
12478 cJSON_AddNumberToObject(sty, "global_scrollbar_thumb_corner_r", s->global_scrollbar_thumb_corner_r);
12479 cJSON_AddNumberToObject(sty, "global_scrollbar_border_thickness", s->global_scrollbar_border_thickness);
12480 _json_add_color(sty, "global_scrollbar_track_color", s->global_scrollbar_track_color);
12481 _json_add_color(sty, "global_scrollbar_thumb_color", s->global_scrollbar_thumb_color);
12482 _json_add_color(sty, "global_scrollbar_thumb_border_color", s->global_scrollbar_thumb_border_color);
12483
12484 cJSON_AddNumberToObject(sty, "grip_size", s->grip_size);
12485 cJSON_AddNumberToObject(sty, "grip_line_thickness", s->grip_line_thickness);
12486 _json_add_color(sty, "grip_color", s->grip_color);
12487
12488 cJSON_AddNumberToObject(sty, "slider_track_size", s->slider_track_size);
12489 cJSON_AddNumberToObject(sty, "slider_track_corner_r", s->slider_track_corner_r);
12490 cJSON_AddNumberToObject(sty, "slider_track_border_thickness", s->slider_track_border_thickness);
12491 cJSON_AddNumberToObject(sty, "slider_handle_min_r", s->slider_handle_min_r);
12492 cJSON_AddNumberToObject(sty, "slider_handle_edge_offset", s->slider_handle_edge_offset);
12493 cJSON_AddNumberToObject(sty, "slider_handle_border_thickness", s->slider_handle_border_thickness);
12494 cJSON_AddNumberToObject(sty, "slider_value_label_offset", s->slider_value_label_offset);
12495
12496 cJSON_AddNumberToObject(sty, "textarea_padding", s->textarea_padding);
12497 cJSON_AddNumberToObject(sty, "textarea_cursor_width", s->textarea_cursor_width);
12498 cJSON_AddNumberToObject(sty, "textarea_cursor_blink_period", s->textarea_cursor_blink_period);
12499
12500 cJSON_AddNumberToObject(sty, "checkbox_max_size", s->checkbox_max_size);
12501 cJSON_AddNumberToObject(sty, "checkbox_mark_margin", s->checkbox_mark_margin);
12502 cJSON_AddNumberToObject(sty, "checkbox_mark_thickness", s->checkbox_mark_thickness);
12503 cJSON_AddNumberToObject(sty, "checkbox_label_gap", s->checkbox_label_gap);
12504 cJSON_AddNumberToObject(sty, "checkbox_label_offset", s->checkbox_label_offset);
12505
12506 cJSON_AddNumberToObject(sty, "radio_circle_min_r", s->radio_circle_min_r);
12507 cJSON_AddNumberToObject(sty, "radio_circle_border_thickness", s->radio_circle_border_thickness);
12508 cJSON_AddNumberToObject(sty, "radio_inner_offset", s->radio_inner_offset);
12509 cJSON_AddNumberToObject(sty, "radio_label_gap", s->radio_label_gap);
12510
12511 cJSON_AddNumberToObject(sty, "listbox_default_item_height", s->listbox_default_item_height);
12512 cJSON_AddNumberToObject(sty, "radiolist_default_item_height", s->radiolist_default_item_height);
12513 cJSON_AddNumberToObject(sty, "combobox_max_visible", s->combobox_max_visible);
12514 cJSON_AddNumberToObject(sty, "dropmenu_max_visible", s->dropmenu_max_visible);
12515 cJSON_AddNumberToObject(sty, "shape_mode", s->shape_mode);
12516 cJSON_AddNumberToObject(sty, "item_text_padding", s->item_text_padding);
12517 cJSON_AddNumberToObject(sty, "item_selection_inset", s->item_selection_inset);
12518 cJSON_AddNumberToObject(sty, "item_height_pad", s->item_height_pad);
12519
12520 cJSON_AddNumberToObject(sty, "dropdown_arrow_reserve", s->dropdown_arrow_reserve);
12521 cJSON_AddNumberToObject(sty, "dropdown_arrow_thickness", s->dropdown_arrow_thickness);
12522 cJSON_AddNumberToObject(sty, "dropdown_arrow_half_h", s->dropdown_arrow_half_h);
12523 cJSON_AddNumberToObject(sty, "dropdown_arrow_half_w", s->dropdown_arrow_half_w);
12524 cJSON_AddNumberToObject(sty, "dropdown_border_thickness", s->dropdown_border_thickness);
12525
12526 cJSON_AddNumberToObject(sty, "label_padding", s->label_padding);
12527 cJSON_AddNumberToObject(sty, "link_underline_thickness", s->link_underline_thickness);
12528 _json_add_color(sty, "link_color_normal", s->link_color_normal);
12529 _json_add_color(sty, "link_color_hover", s->link_color_hover);
12530
12531 cJSON_AddNumberToObject(sty, "scroll_step", s->scroll_step);
12532 cJSON_AddNumberToObject(sty, "global_scroll_step", s->global_scroll_step);
12533 cJSON_AddNumberToObject(sty, "combobox_max_dropdown_width", s->combobox_max_dropdown_width);
12534 cJSON_AddItemToObject(root, "style", sty);
12535
12536 /* write to file */
12537 char* json_str = cJSON_Print(root);
12538 cJSON_Delete(root);
12539 if (!json_str) return -1;
12540
12541 FILE* fp = _gui_fopen_write(filepath, "w");
12542 if (!fp) {
12543 cJSON_free(json_str);
12544 return -1;
12545 }
12546 fprintf(fp, "%s\n", json_str);
12547 fclose(fp);
12548 cJSON_free(json_str);
12549 return 0;
12550}
12551
12558int n_gui_load_theme_json(N_GUI_CTX* ctx, const char* filepath) {
12559 __n_assert(ctx, return -1);
12560 __n_assert(filepath, return -1);
12561
12562 FILE* fp = fopen(filepath, "r");
12563 if (!fp) return -1;
12564
12565 fseek(fp, 0, SEEK_END);
12566 long fsize = ftell(fp);
12567 fseek(fp, 0, SEEK_SET);
12568 if (fsize <= 0 || fsize > 1024 * 1024) { /* sanity: max 1 MB */
12569 fclose(fp);
12570 return -1;
12571 }
12572
12573 char* buf = NULL;
12574 Malloc(buf, char, (size_t)fsize + 1);
12575 if (!buf) {
12576 fclose(fp);
12577 return -1;
12578 }
12579 size_t nread = fread(buf, 1, (size_t)fsize, fp);
12580 fclose(fp);
12581 buf[nread] = '\0';
12582
12583 cJSON* root = cJSON_Parse(buf);
12584 FreeNoLog(buf);
12585 if (!root) return -1;
12586
12587 /* parse theme colours */
12588 cJSON* theme = cJSON_GetObjectItemCaseSensitive(root, "theme");
12589 if (theme) {
12590 N_GUI_THEME* t = &ctx->default_theme;
12591 t->bg_normal = _json_get_color(theme, "bg_normal", t->bg_normal);
12592 t->bg_hover = _json_get_color(theme, "bg_hover", t->bg_hover);
12593 t->bg_active = _json_get_color(theme, "bg_active", t->bg_active);
12594 t->border_normal = _json_get_color(theme, "border_normal", t->border_normal);
12595 t->border_hover = _json_get_color(theme, "border_hover", t->border_hover);
12596 t->border_active = _json_get_color(theme, "border_active", t->border_active);
12597 t->text_normal = _json_get_color(theme, "text_normal", t->text_normal);
12598 t->text_hover = _json_get_color(theme, "text_hover", t->text_hover);
12599 t->text_active = _json_get_color(theme, "text_active", t->text_active);
12600 t->selection_color = _json_get_color(theme, "selection_color", t->selection_color);
12601 t->border_thickness = _json_get_float(theme, "border_thickness", t->border_thickness);
12602 t->corner_rx = _json_get_float(theme, "corner_rx", t->corner_rx);
12603 t->corner_ry = _json_get_float(theme, "corner_ry", t->corner_ry);
12604
12605 /* apply to all existing windows and widgets */
12606 list_foreach(wnode, ctx->windows) {
12607 N_GUI_WINDOW* win = (N_GUI_WINDOW*)wnode->ptr;
12608 win->theme = *t;
12609 list_foreach(wgn, win->widgets) {
12610 N_GUI_WIDGET* wgt = (N_GUI_WIDGET*)wgn->ptr;
12611 if (wgt) wgt->theme = *t;
12612 }
12613 }
12614 }
12615
12616 /* parse style values */
12617 cJSON* sty = cJSON_GetObjectItemCaseSensitive(root, "style");
12618 if (sty) {
12619 N_GUI_STYLE* s = &ctx->style;
12620 s->titlebar_h = _json_get_float(sty, "titlebar_h", s->titlebar_h);
12621 s->min_win_w = _json_get_float(sty, "min_win_w", s->min_win_w);
12622 s->min_win_h = _json_get_float(sty, "min_win_h", s->min_win_h);
12623 s->title_padding = _json_get_float(sty, "title_padding", s->title_padding);
12624 s->title_max_w_reserve = _json_get_float(sty, "title_max_w_reserve", s->title_max_w_reserve);
12625
12626 s->tb_btn_size = _json_get_float(sty, "tb_btn_size", s->tb_btn_size);
12627 s->tb_btn_spacing = _json_get_float(sty, "tb_btn_spacing", s->tb_btn_spacing);
12628 s->tb_btn_right_margin = _json_get_float(sty, "tb_btn_right_margin", s->tb_btn_right_margin);
12629 s->tb_btn_glyph_thickness = _json_get_float(sty, "tb_btn_glyph_thickness", s->tb_btn_glyph_thickness);
12630
12631 s->scrollbar_size = _json_get_float(sty, "scrollbar_size", s->scrollbar_size);
12632 s->scrollbar_thumb_min = _json_get_float(sty, "scrollbar_thumb_min", s->scrollbar_thumb_min);
12633 s->scrollbar_thumb_padding = _json_get_float(sty, "scrollbar_thumb_padding", s->scrollbar_thumb_padding);
12634 s->scrollbar_thumb_corner_r = _json_get_float(sty, "scrollbar_thumb_corner_r", s->scrollbar_thumb_corner_r);
12635 s->scrollbar_track_color = _json_get_color(sty, "scrollbar_track_color", s->scrollbar_track_color);
12636 s->scrollbar_thumb_color = _json_get_color(sty, "scrollbar_thumb_color", s->scrollbar_thumb_color);
12637
12638 s->global_scrollbar_size = _json_get_float(sty, "global_scrollbar_size", s->global_scrollbar_size);
12639 s->global_scrollbar_thumb_min = _json_get_float(sty, "global_scrollbar_thumb_min", s->global_scrollbar_thumb_min);
12640 s->global_scrollbar_thumb_padding = _json_get_float(sty, "global_scrollbar_thumb_padding", s->global_scrollbar_thumb_padding);
12641 s->global_scrollbar_thumb_corner_r = _json_get_float(sty, "global_scrollbar_thumb_corner_r", s->global_scrollbar_thumb_corner_r);
12642 s->global_scrollbar_border_thickness = _json_get_float(sty, "global_scrollbar_border_thickness", s->global_scrollbar_border_thickness);
12643 s->global_scrollbar_track_color = _json_get_color(sty, "global_scrollbar_track_color", s->global_scrollbar_track_color);
12644 s->global_scrollbar_thumb_color = _json_get_color(sty, "global_scrollbar_thumb_color", s->global_scrollbar_thumb_color);
12645 s->global_scrollbar_thumb_border_color = _json_get_color(sty, "global_scrollbar_thumb_border_color", s->global_scrollbar_thumb_border_color);
12646
12647 s->grip_size = _json_get_float(sty, "grip_size", s->grip_size);
12648 s->grip_line_thickness = _json_get_float(sty, "grip_line_thickness", s->grip_line_thickness);
12649 s->grip_color = _json_get_color(sty, "grip_color", s->grip_color);
12650
12651 s->slider_track_size = _json_get_float(sty, "slider_track_size", s->slider_track_size);
12652 s->slider_track_corner_r = _json_get_float(sty, "slider_track_corner_r", s->slider_track_corner_r);
12653 s->slider_track_border_thickness = _json_get_float(sty, "slider_track_border_thickness", s->slider_track_border_thickness);
12654 s->slider_handle_min_r = _json_get_float(sty, "slider_handle_min_r", s->slider_handle_min_r);
12655 s->slider_handle_edge_offset = _json_get_float(sty, "slider_handle_edge_offset", s->slider_handle_edge_offset);
12656 s->slider_handle_border_thickness = _json_get_float(sty, "slider_handle_border_thickness", s->slider_handle_border_thickness);
12657 s->slider_value_label_offset = _json_get_float(sty, "slider_value_label_offset", s->slider_value_label_offset);
12658
12659 s->textarea_padding = _json_get_float(sty, "textarea_padding", s->textarea_padding);
12660 s->textarea_cursor_width = _json_get_float(sty, "textarea_cursor_width", s->textarea_cursor_width);
12661 s->textarea_cursor_blink_period = _json_get_float(sty, "textarea_cursor_blink_period", s->textarea_cursor_blink_period);
12662
12663 s->checkbox_max_size = _json_get_float(sty, "checkbox_max_size", s->checkbox_max_size);
12664 s->checkbox_mark_margin = _json_get_float(sty, "checkbox_mark_margin", s->checkbox_mark_margin);
12665 s->checkbox_mark_thickness = _json_get_float(sty, "checkbox_mark_thickness", s->checkbox_mark_thickness);
12666 s->checkbox_label_gap = _json_get_float(sty, "checkbox_label_gap", s->checkbox_label_gap);
12667 s->checkbox_label_offset = _json_get_float(sty, "checkbox_label_offset", s->checkbox_label_offset);
12668
12669 s->radio_circle_min_r = _json_get_float(sty, "radio_circle_min_r", s->radio_circle_min_r);
12670 s->radio_circle_border_thickness = _json_get_float(sty, "radio_circle_border_thickness", s->radio_circle_border_thickness);
12671 s->radio_inner_offset = _json_get_float(sty, "radio_inner_offset", s->radio_inner_offset);
12672 s->radio_label_gap = _json_get_float(sty, "radio_label_gap", s->radio_label_gap);
12673
12674 s->listbox_default_item_height = _json_get_float(sty, "listbox_default_item_height", s->listbox_default_item_height);
12675 s->radiolist_default_item_height = _json_get_float(sty, "radiolist_default_item_height", s->radiolist_default_item_height);
12676 s->combobox_max_visible = _json_get_int(sty, "combobox_max_visible", s->combobox_max_visible);
12677 s->dropmenu_max_visible = _json_get_int(sty, "dropmenu_max_visible", s->dropmenu_max_visible);
12678 s->shape_mode = _json_get_int(sty, "shape_mode", s->shape_mode);
12679 s->item_text_padding = _json_get_float(sty, "item_text_padding", s->item_text_padding);
12680 s->item_selection_inset = _json_get_float(sty, "item_selection_inset", s->item_selection_inset);
12681 s->item_height_pad = _json_get_float(sty, "item_height_pad", s->item_height_pad);
12682
12683 s->dropdown_arrow_reserve = _json_get_float(sty, "dropdown_arrow_reserve", s->dropdown_arrow_reserve);
12684 s->dropdown_arrow_thickness = _json_get_float(sty, "dropdown_arrow_thickness", s->dropdown_arrow_thickness);
12685 s->dropdown_arrow_half_h = _json_get_float(sty, "dropdown_arrow_half_h", s->dropdown_arrow_half_h);
12686 s->dropdown_arrow_half_w = _json_get_float(sty, "dropdown_arrow_half_w", s->dropdown_arrow_half_w);
12687 s->dropdown_border_thickness = _json_get_float(sty, "dropdown_border_thickness", s->dropdown_border_thickness);
12688
12689 s->label_padding = _json_get_float(sty, "label_padding", s->label_padding);
12690 s->link_underline_thickness = _json_get_float(sty, "link_underline_thickness", s->link_underline_thickness);
12691 s->link_color_normal = _json_get_color(sty, "link_color_normal", s->link_color_normal);
12692 s->link_color_hover = _json_get_color(sty, "link_color_hover", s->link_color_hover);
12693
12694 s->scroll_step = _json_get_float(sty, "scroll_step", s->scroll_step);
12695 s->global_scroll_step = _json_get_float(sty, "global_scroll_step", s->global_scroll_step);
12696 s->combobox_max_dropdown_width = _json_get_float(sty, "combobox_max_dropdown_width", s->combobox_max_dropdown_width);
12697 }
12698
12699 cJSON_Delete(root);
12700 return 0;
12701}
12702
12703/* persistent state bits, transient DRAGGING/RESIZING/SCROLL-DRAG are never serialised */
12704#define _N_GUI_WIN_STATE_PERSIST_MASK (N_GUI_WIN_OPEN | N_GUI_WIN_MINIMISED | N_GUI_WIN_MAXIMISED)
12705
12712int n_gui_save_layout_json(N_GUI_CTX* ctx, const char* filepath) {
12713 __n_assert(ctx, return -1);
12714 __n_assert(filepath, return -1);
12715
12716 cJSON* root = cJSON_CreateObject();
12717 if (!root) return -1;
12718
12719 cJSON_AddNumberToObject(root, "version", 1);
12720 /* Display size at save time, the loader scales saved (x,y,w,h) by
12721 * (current_display / saved_display) so a dragged HUD stays in its
12722 * relative spot when the user relaunches on a bigger / smaller
12723 * screen. Missing or zero saved dims disable the scaling (legacy
12724 * files and in-process tests behave as before). */
12725 cJSON_AddNumberToObject(root, "display_w", ctx->display_w);
12726 cJSON_AddNumberToObject(root, "display_h", ctx->display_h);
12727 cJSON* wins = cJSON_CreateArray();
12728 if (!wins) {
12729 cJSON_Delete(root);
12730 return -1;
12731 }
12732
12733 list_foreach(wnode, ctx->windows) {
12734 N_GUI_WINDOW* win = (N_GUI_WINDOW*)wnode->ptr;
12735 if (!win) continue;
12736 /* Closed windows are persisted too: many UI modules treat
12737 * close/open as a visibility toggle (editor panels, mod tools,
12738 * NPC dialog popups). Without persisting their closed geometry,
12739 * the user's dragged position would be lost the moment the
12740 * window is hidden, and most modules close their windows
12741 * before logout. The destroy+recreate pattern (combat HUD on
12742 * target change, hotbar on assignment) is now safe because the
12743 * loader has a dedupe pre-pass that drops stale closed entries
12744 * when a same-title open entry follows in the list (see
12745 * test_load_stale_closed_dedupe).
12746 *
12747 * Multiple OPEN windows sharing a title still resolve via the
12748 * loader's per-context match counter (first-seen wins), so the
12749 * save order here just needs to be deterministic. */
12750 cJSON* wj = cJSON_CreateObject();
12751 if (!wj) continue;
12752 /* A detached window's live x/y/w/h describe its native display, not a
12753 * spot on the main display. Persist the pop-up geometry it falls back
12754 * to (and the pop-up flags, since detaching forces frameless), with the
12755 * native size and desktop position carried separately. */
12756 int detached = (win->native != NULL);
12757 float px = detached ? win->saved_x : win->x;
12758 float py = detached ? win->saved_y : win->y;
12759 float pw = detached ? win->saved_w : win->w;
12760 float ph = detached ? win->saved_h : win->h;
12761 int pflags = detached ? win->saved_flags : win->flags;
12762 int npx = win->native_pos_x;
12763 int npy = win->native_pos_y;
12764 float nw = win->native_w;
12765 float nh = win->native_h;
12766 if (detached) {
12767 al_get_window_position(win->native, &npx, &npy);
12768 nw = (float)al_get_display_width(win->native);
12769 nh = (float)al_get_display_height(win->native);
12770 }
12771 cJSON_AddStringToObject(wj, "title", win->title);
12772 cJSON_AddNumberToObject(wj, "x", px);
12773 cJSON_AddNumberToObject(wj, "y", py);
12774 cJSON_AddNumberToObject(wj, "w", pw);
12775 cJSON_AddNumberToObject(wj, "h", ph);
12776 cJSON_AddNumberToObject(wj, "state", win->state & _N_GUI_WIN_STATE_PERSIST_MASK);
12777 cJSON_AddNumberToObject(wj, "flags", pflags);
12778 if (win->want_native) {
12779 cJSON_AddNumberToObject(wj, "native", 1);
12780 cJSON_AddNumberToObject(wj, "detach_flags", win->detach_flags);
12781 if (nw > 0.0f && nh > 0.0f) {
12782 cJSON_AddNumberToObject(wj, "native_w", nw);
12783 cJSON_AddNumberToObject(wj, "native_h", nh);
12784 }
12785 if (npx != INT_MIN && npy != INT_MIN) {
12786 cJSON_AddNumberToObject(wj, "native_pos_x", npx);
12787 cJSON_AddNumberToObject(wj, "native_pos_y", npy);
12788 }
12789 }
12790 cJSON_AddNumberToObject(wj, "z_order", win->z_order);
12791 cJSON_AddNumberToObject(wj, "z_value", win->z_value);
12792 cJSON_AddNumberToObject(wj, "norm_x", win->norm_x);
12793 cJSON_AddNumberToObject(wj, "norm_y", win->norm_y);
12794 cJSON_AddNumberToObject(wj, "norm_w", win->norm_w);
12795 cJSON_AddNumberToObject(wj, "norm_h", win->norm_h);
12796 cJSON_AddNumberToObject(wj, "autofit_flags", win->autofit_flags);
12797 cJSON_AddNumberToObject(wj, "autofit_border", win->autofit_border);
12798 cJSON_AddNumberToObject(wj, "resize_policy", win->resize_policy);
12799
12800 /* User-adjustable state of this window's KEYED widgets (splitpane ratios,
12801 * datagrid column layouts). Only widgets given a persist key are saved. */
12802 {
12803 cJSON* warr = NULL;
12804 list_foreach(wgn, win->widgets) {
12805 N_GUI_WIDGET* wgt = (N_GUI_WIDGET*)wgn->ptr;
12806 cJSON* w2;
12807 if (!wgt || !wgt->persist_key[0] || !wgt->data)
12808 continue;
12809 if (wgt->type != N_GUI_TYPE_SPLITPANE && wgt->type != N_GUI_TYPE_DATAGRID)
12810 continue;
12811 if (!warr && !(warr = cJSON_CreateArray()))
12812 break;
12813 w2 = cJSON_CreateObject();
12814 if (!w2)
12815 continue;
12816 cJSON_AddStringToObject(w2, "key", wgt->persist_key);
12817 if (wgt->type == N_GUI_TYPE_SPLITPANE) {
12818 cJSON_AddStringToObject(w2, "kind", "splitpane");
12819 cJSON_AddNumberToObject(w2, "ratio", ((N_GUI_SPLITPANE_DATA*)wgt->data)->ratio);
12820 } else { /* N_GUI_TYPE_DATAGRID */
12821 int nc = (int)n_gui_datagrid_get_column_count(ctx, wgt->id);
12822 cJSON* jo;
12823 cJSON* jv;
12824 cJSON* jw;
12825 int c;
12826 if (nc > N_GUI_PERSIST_MAX_COLS)
12828 cJSON_AddStringToObject(w2, "kind", "datagrid");
12829 cJSON_AddNumberToObject(w2, "ncols", nc);
12830 jo = cJSON_AddArrayToObject(w2, "order");
12831 jv = cJSON_AddArrayToObject(w2, "visible");
12832 jw = cJSON_AddArrayToObject(w2, "width");
12833 for (c = 0; c < nc; c++) {
12834 int phys = n_gui_datagrid_display_to_physical(ctx, wgt->id, c);
12835 if (jo) cJSON_AddItemToArray(jo, cJSON_CreateNumber(phys >= 0 ? phys : c));
12836 if (jv) cJSON_AddItemToArray(jv, cJSON_CreateNumber(n_gui_datagrid_get_column_visible(ctx, wgt->id, c)));
12837 if (jw) cJSON_AddItemToArray(jw, cJSON_CreateNumber((double)n_gui_datagrid_get_column_width(ctx, wgt->id, c)));
12838 }
12839 }
12840 cJSON_AddItemToArray(warr, w2);
12841 }
12842 if (warr)
12843 cJSON_AddItemToObject(wj, "widgets", warr);
12844 }
12845 cJSON_AddItemToArray(wins, wj);
12846 }
12847 cJSON_AddItemToObject(root, "windows", wins);
12848
12849 char* json_str = cJSON_Print(root);
12850 cJSON_Delete(root);
12851 if (!json_str) return -1;
12852
12853 FILE* fp = _gui_fopen_write(filepath, "w");
12854 if (!fp) {
12855 cJSON_free(json_str);
12856 return -1;
12857 }
12858 fprintf(fp, "%s\n", json_str);
12859 fclose(fp);
12860 cJSON_free(json_str);
12861 return 0;
12862}
12863
12870int n_gui_load_layout_json(N_GUI_CTX* ctx, const char* filepath) {
12871 __n_assert(ctx, return -1);
12872 __n_assert(filepath, return -1);
12873
12874 FILE* fp = fopen(filepath, "r");
12875 if (!fp) {
12876 n_log(LOG_DEBUG, "n_gui_load_layout_json: %s not found (first run?)", filepath);
12877 return -1;
12878 }
12879
12880 fseek(fp, 0, SEEK_END);
12881 long fsize = ftell(fp);
12882 fseek(fp, 0, SEEK_SET);
12883 if (fsize <= 0 || fsize > 4 * 1024 * 1024) { /* sanity: max 4 MB */
12884 fclose(fp);
12885 return -1;
12886 }
12887
12888 char* buf = NULL;
12889 Malloc(buf, char, (size_t)fsize + 1);
12890 if (!buf) {
12891 fclose(fp);
12892 return -1;
12893 }
12894 size_t nread = fread(buf, 1, (size_t)fsize, fp);
12895 fclose(fp);
12896 buf[nread] = '\0';
12897
12898 cJSON* root = cJSON_Parse(buf);
12899 FreeNoLog(buf);
12900 if (!root) return -1;
12901
12902 cJSON* wins = cJSON_GetObjectItemCaseSensitive(root, "windows");
12903 if (!wins || !cJSON_IsArray(wins)) {
12904 cJSON_Delete(root);
12905 return -1;
12906 }
12907
12908 /* Cross-session proportional scaling: if the layout was saved with
12909 * a different display size than the current one, scale every loaded
12910 * (x,y,w,h) by the size ratio. Falls back to 1.0 (no scaling) when
12911 * the file was saved pre-scaling support (display_w/h absent or 0)
12912 * or when the current context display size is 0 (in-process tests). */
12913 float saved_w = _json_get_float(root, "display_w", 0.0f);
12914 float saved_h = _json_get_float(root, "display_h", 0.0f);
12915 float sx = 1.0f, sy = 1.0f;
12916 if (saved_w > 0.0f && ctx->display_w > 0.0f) sx = ctx->display_w / saved_w;
12917 if (saved_h > 0.0f && ctx->display_h > 0.0f) sy = ctx->display_h / saved_h;
12918
12919 /* Build a per-title match counter on the context side so duplicate titles
12920 * consume their JSON entries in creation order (first-seen-first-match). */
12921 int n_entries = cJSON_GetArraySize(wins);
12922 int* consumed = NULL;
12923 if (n_entries > 0) {
12924 Malloc(consumed, int, (size_t)n_entries);
12925 if (!consumed) {
12926 cJSON_Delete(root);
12927 return -1;
12928 }
12929 memset(consumed, 0, sizeof(int) * (size_t)n_entries);
12930 }
12931
12932 /* Dedupe pre-pass for stale closed windows. Old saves (from before
12933 * the save-side skip-closed-windows fix) accumulated one entry per
12934 * rebuild cycle: N-1 closed stragglers with state=0 plus the live
12935 * one with state=1, all sharing a title. The matcher assigns JSON
12936 * entries to context windows in list order, so the stale entries
12937 * would land on the live window and apply state=0, hiding the HUD
12938 * until the next rebuild re-opens it (the "text appears only after
12939 * resize" symptom).
12940 *
12941 * We ONLY mark a closed entry as consumed when a later same-title
12942 * entry is marked OPEN. That preserves legitimate cases where an
12943 * app creates multiple windows sharing a title (uncommon but valid). */
12944 for (int i = 0; i < n_entries; i++) {
12945 if (consumed[i]) continue;
12946 cJSON* wi = cJSON_GetArrayItem(wins, i);
12947 if (!wi) continue;
12948 cJSON* state_i = cJSON_GetObjectItemCaseSensitive(wi, "state");
12949 int is_open_i = state_i && cJSON_IsNumber(state_i) &&
12950 (((int)state_i->valuedouble) & N_GUI_WIN_OPEN);
12951 if (is_open_i) continue;
12952 cJSON* ti = cJSON_GetObjectItemCaseSensitive(wi, "title");
12953 if (!ti || !cJSON_IsString(ti) || !ti->valuestring) continue;
12954 for (int j = i + 1; j < n_entries; j++) {
12955 cJSON* wj = cJSON_GetArrayItem(wins, j);
12956 if (!wj) continue;
12957 cJSON* state_j = cJSON_GetObjectItemCaseSensitive(wj, "state");
12958 int is_open_j = state_j && cJSON_IsNumber(state_j) &&
12959 (((int)state_j->valuedouble) & N_GUI_WIN_OPEN);
12960 if (!is_open_j) continue;
12961 cJSON* tj = cJSON_GetObjectItemCaseSensitive(wj, "title");
12962 if (tj && cJSON_IsString(tj) && tj->valuestring &&
12963 strcmp(ti->valuestring, tj->valuestring) == 0) {
12964 consumed[i] = 1;
12965 break;
12966 }
12967 }
12968 }
12969
12970 list_foreach(wnode, ctx->windows) {
12971 N_GUI_WINDOW* win = (N_GUI_WINDOW*)wnode->ptr;
12972 if (!win) continue;
12973 cJSON* match = NULL;
12974 int match_idx = -1;
12975 int idx = 0;
12976 cJSON* wj = NULL;
12977 cJSON_ArrayForEach(wj, wins) {
12978 if (!consumed || consumed[idx] == 0) {
12979 cJSON* t = cJSON_GetObjectItemCaseSensitive(wj, "title");
12980 if (t && cJSON_IsString(t) && t->valuestring &&
12981 strncmp(t->valuestring, win->title, N_GUI_ID_MAX) == 0) {
12982 match = wj;
12983 match_idx = idx;
12984 break;
12985 }
12986 }
12987 idx++;
12988 }
12989 if (!match) continue;
12990 if (consumed) consumed[match_idx] = 1;
12991
12992 /* Only (x,y) participate in cross-session scaling, they express
12993 * *placement* in screen coordinates and should track the display
12994 * proportionally. (w,h) are intrinsic to the widget layout (HUD
12995 * bar widths, editor panel sizes, etc.), so they are loaded
12996 * verbatim and clamped below. Scaling w/h across sessions caused
12997 * cascading corruption: repeated save/load at different display
12998 * sizes produced insane dimensions (HUD Player w=65 instead of
12999 * 220, Chat Input h=11 instead of 22). */
13000 win->x = _json_get_float(match, "x", win->x) * sx;
13001 win->y = _json_get_float(match, "y", win->y) * sy;
13002 win->w = _json_get_float(match, "w", win->w);
13003 win->h = _json_get_float(match, "h", win->h);
13004
13005 /* Sanity clamp: if the saved file was produced by an earlier
13006 * buggy build (or if the display shrank dramatically), the
13007 * values may be way out of range. Loaded geometry entirely
13008 * outside the current display is reset to (0, 0) so the window
13009 * stays reachable, the user can drag it from there or hit the
13010 * settings Reset button. Width/height clamped to the display so
13011 * giant bogus dimensions can't blow the layout. Minimums keep
13012 * the window clickable. */
13013 if (ctx->display_w > 0.0f) {
13014 if (win->x < 0.0f || win->x > ctx->display_w) win->x = 0.0f;
13015 if (win->w < 20.0f) win->w = 20.0f;
13016 if (win->w > ctx->display_w) win->w = ctx->display_w;
13017 }
13018 if (ctx->display_h > 0.0f) {
13019 if (win->y < 0.0f || win->y > ctx->display_h) win->y = 0.0f;
13020 if (win->h < 20.0f) win->h = 20.0f;
13021 if (win->h > ctx->display_h) win->h = ctx->display_h;
13022 }
13023 /* keep only persistent state bits from the file; preserve any
13024 * transient bits already on the window (there shouldn't be any at load time). */
13025 int saved_state = _json_get_int(match, "state", win->state & _N_GUI_WIN_STATE_PERSIST_MASK);
13026 win->state = (win->state & ~_N_GUI_WIN_STATE_PERSIST_MASK) |
13027 (saved_state & _N_GUI_WIN_STATE_PERSIST_MASK);
13028 win->flags = _json_get_int(match, "flags", win->flags);
13029 win->z_order = _json_get_int(match, "z_order", win->z_order);
13030 win->z_value = _json_get_int(match, "z_value", win->z_value);
13031 win->norm_x = _json_get_float(match, "norm_x", win->norm_x);
13032 win->norm_y = _json_get_float(match, "norm_y", win->norm_y);
13033 win->norm_w = _json_get_float(match, "norm_w", win->norm_w);
13034 win->norm_h = _json_get_float(match, "norm_h", win->norm_h);
13035 win->autofit_flags = _json_get_int(match, "autofit_flags", win->autofit_flags);
13036 win->autofit_border = _json_get_float(match, "autofit_border", win->autofit_border);
13037 win->resize_policy = _json_get_int(match, "resize_policy", win->resize_policy);
13038
13039 /* Native (detached) window restore. The geometry applied above is the
13040 * pop-up fallback, keep it as such. The native window itself is only
13041 * created when the context has an event queue and the window is open,
13042 * otherwise want_native carries the intent and the next
13043 * n_gui_open_window (which the host calls once its queue exists) does
13044 * it. Native size and desktop position are NOT scaled by the display
13045 * ratio: they are desktop coordinates, not main-display ones. */
13046 if (_json_get_int(match, "native", 0)) {
13047 win->saved_x = win->x;
13048 win->saved_y = win->y;
13049 win->saved_w = win->w;
13050 win->saved_h = win->h;
13051 win->saved_flags = win->flags;
13052 win->detach_flags = _json_get_int(match, "detach_flags", N_GUI_DETACH_NONE);
13053 win->native_w = _json_get_float(match, "native_w", 0.0f);
13054 win->native_h = _json_get_float(match, "native_h", 0.0f);
13055 win->native_pos_x = _json_get_int(match, "native_pos_x", INT_MIN);
13056 win->native_pos_y = _json_get_int(match, "native_pos_y", INT_MIN);
13057 win->want_native = 1;
13058 if (ctx->event_queue && !win->native && (win->state & N_GUI_WIN_OPEN)) {
13059 n_gui_window_detach(ctx, win->id, win->detach_flags);
13060 }
13061 }
13062 }
13063
13064 /* Retain entries that matched NO live window (lazily-created panels not
13065 * yet built) so n_gui_add_window can apply their position when the
13066 * window is created later. Skip titles that DO have a live window (those
13067 * are eager windows already placed above; re-applying on a later rebuild
13068 * would fight the caller's preserve-position logic). */
13070 ctx->pending_layout = NULL;
13071 ctx->pending_layout_count = 0;
13072 if (n_entries > 0) {
13073 int npend = 0;
13074 for (int i = 0; i < n_entries; i++)
13075 if (consumed && !consumed[i]) npend++;
13076 if (npend > 0) {
13077 Malloc(ctx->pending_layout, N_GUI_PENDING_GEOM, (size_t)npend);
13078 if (ctx->pending_layout) {
13079 int k = 0;
13080 for (int i = 0; i < n_entries && k < npend; i++) {
13081 if (consumed && consumed[i]) continue;
13082 cJSON* wi = cJSON_GetArrayItem(wins, i);
13083 cJSON* t = wi ? cJSON_GetObjectItemCaseSensitive(wi, "title") : NULL;
13084 if (!t || !cJSON_IsString(t) || !t->valuestring || !t->valuestring[0])
13085 continue;
13086 /* Skip if a live window already owns this title. */
13087 int live = 0;
13088 list_foreach(lwn, ctx->windows) {
13089 const N_GUI_WINDOW* lw = (N_GUI_WINDOW*)lwn->ptr;
13090 if (lw && strncmp(lw->title, t->valuestring, N_GUI_ID_MAX) == 0) {
13091 live = 1;
13092 break;
13093 }
13094 }
13095 if (live) continue;
13096 strncpy(ctx->pending_layout[k].title, t->valuestring, N_GUI_ID_MAX - 1);
13097 ctx->pending_layout[k].title[N_GUI_ID_MAX - 1] = '\0';
13098 ctx->pending_layout[k].x = _json_get_float(wi, "x", 0.0f) * sx;
13099 ctx->pending_layout[k].y = _json_get_float(wi, "y", 0.0f) * sy;
13100 ctx->pending_layout[k].w = _json_get_float(wi, "w", 0.0f) * sx;
13101 ctx->pending_layout[k].h = _json_get_float(wi, "h", 0.0f) * sy;
13103 ctx->pending_layout[k].consumed = 0;
13104 k++;
13105 }
13106 ctx->pending_layout_count = k;
13107 }
13108 }
13109 }
13110
13111 /* Stash every saved per-window widget state (keyed splitpanes/datagrids) as
13112 * pending, applied to each widget when n_gui_widget_set_persist_key is next
13113 * called with a matching window title + key. This covers lazily-created widgets
13114 * (Fissure builds its views/popups after the layout is loaded). */
13116 ctx->pending_widgets = NULL;
13117 ctx->pending_widgets_count = 0;
13118 {
13119 int nw = 0;
13120 cJSON* wi;
13121 cJSON_ArrayForEach(wi, wins) {
13122 cJSON* warr = cJSON_GetObjectItemCaseSensitive(wi, "widgets");
13123 if (warr && cJSON_IsArray(warr))
13124 nw += cJSON_GetArraySize(warr);
13125 }
13126 if (nw > 0) {
13127 Malloc(ctx->pending_widgets, N_GUI_PENDING_WIDGET, (size_t)nw);
13128 if (ctx->pending_widgets) {
13129 int k = 0;
13130 cJSON* wi2;
13131 cJSON_ArrayForEach(wi2, wins) {
13132 cJSON* t = cJSON_GetObjectItemCaseSensitive(wi2, "title");
13133 cJSON* warr = cJSON_GetObjectItemCaseSensitive(wi2, "widgets");
13134 cJSON* we;
13135 const char* title = (t && cJSON_IsString(t) && t->valuestring) ? t->valuestring : "";
13136 if (!warr || !cJSON_IsArray(warr) || !title[0])
13137 continue;
13138 cJSON_ArrayForEach(we, warr) {
13139 cJSON* jk = cJSON_GetObjectItemCaseSensitive(we, "key");
13140 cJSON* jkind = cJSON_GetObjectItemCaseSensitive(we, "kind");
13142 const char* kind;
13143 if (k >= nw)
13144 break;
13145 if (!jk || !cJSON_IsString(jk) || !jk->valuestring || !jk->valuestring[0])
13146 continue;
13147 if (!jkind || !cJSON_IsString(jkind) || !jkind->valuestring)
13148 continue;
13149 kind = jkind->valuestring;
13150 p = &ctx->pending_widgets[k];
13151 memset(p, 0, sizeof(*p));
13152 strncpy(p->title, title, N_GUI_ID_MAX - 1);
13153 strncpy(p->key, jk->valuestring, N_GUI_ID_MAX - 1);
13154 if (strcmp(kind, "splitpane") == 0) {
13156 p->ratio = _json_get_float(we, "ratio", 0.5f);
13157 } else if (strcmp(kind, "datagrid") == 0) {
13158 cJSON* jo = cJSON_GetObjectItemCaseSensitive(we, "order");
13159 cJSON* jv = cJSON_GetObjectItemCaseSensitive(we, "visible");
13160 cJSON* jw = cJSON_GetObjectItemCaseSensitive(we, "width");
13161 int nc = _json_get_int(we, "ncols", 0);
13162 int c;
13164 if (nc < 0) nc = 0;
13166 p->ncols = nc;
13167 for (c = 0; c < nc; c++) {
13168 const cJSON* eo = (jo && cJSON_IsArray(jo)) ? cJSON_GetArrayItem(jo, c) : NULL;
13169 const cJSON* ev = (jv && cJSON_IsArray(jv)) ? cJSON_GetArrayItem(jv, c) : NULL;
13170 const cJSON* ew = (jw && cJSON_IsArray(jw)) ? cJSON_GetArrayItem(jw, c) : NULL;
13171 p->order[c] = eo ? (int)eo->valuedouble : c;
13172 p->visible[c] = (ev && ((int)ev->valuedouble)) ? 1 : (ev ? 0 : 1);
13173 p->width[c] = ew ? (float)ew->valuedouble : 0.0f;
13174 }
13175 } else {
13176 continue; /* unknown kind: leave the slot for the next entry */
13177 }
13178 k++;
13179 }
13180 }
13181 ctx->pending_widgets_count = k;
13182 }
13183 }
13184 }
13185
13186 FreeNoLog(consumed);
13187 cJSON_Delete(root);
13188 return 0;
13189}
13190
13191#else /* !HAVE_CJSON */
13192
13199int n_gui_save_theme_json(N_GUI_CTX* ctx, const char* filepath) {
13200 (void)ctx;
13201 (void)filepath;
13202 n_log(LOG_ERR, "n_gui_save_theme_json: cJSON not available (compile with -DHAVE_CJSON)");
13203 return -1;
13204}
13205
13212int n_gui_load_theme_json(N_GUI_CTX* ctx, const char* filepath) {
13213 (void)ctx;
13214 (void)filepath;
13215 n_log(LOG_ERR, "n_gui_load_theme_json: cJSON not available (compile with -DHAVE_CJSON)");
13216 return -1;
13217}
13218
13225int n_gui_save_layout_json(N_GUI_CTX* ctx, const char* filepath) {
13226 (void)ctx;
13227 (void)filepath;
13228 n_log(LOG_ERR, "n_gui_save_layout_json: cJSON not available (compile with -DHAVE_CJSON)");
13229 return -1;
13230}
13231
13238int n_gui_load_layout_json(N_GUI_CTX* ctx, const char* filepath) {
13239 (void)ctx;
13240 (void)filepath;
13241 n_log(LOG_ERR, "n_gui_load_layout_json: cJSON not available (compile with -DHAVE_CJSON)");
13242 return -1;
13243}
13244
13245#endif /* HAVE_CJSON */
13246
13247static void _n_gui_tab_button_clicked(int widget_id, void* user_data) {
13248 N_GUI_TAB_PANEL* panel = (N_GUI_TAB_PANEL*)user_data;
13249 if (!panel) return;
13250 int clicked = -1;
13251 for (int i = 0; i < panel->nb_tabs; i++) {
13252 if (panel->button_ids[i] == widget_id) {
13253 clicked = i;
13254 break;
13255 }
13256 }
13257 if (clicked < 0) return;
13258 n_gui_tab_set_active(panel, clicked);
13259 if (panel->on_tab_change) panel->on_tab_change(clicked, panel->user_data);
13260}
13261
13262N_GUI_TAB_PANEL* n_gui_tab_create(N_GUI_CTX* ctx, int window_id, float x, float y, float button_w, float button_h, void (*on_tab_change)(int, void*), void* user_data) {
13263 if (!ctx) return NULL;
13264 N_GUI_TAB_PANEL* panel = NULL;
13265 Malloc(panel, N_GUI_TAB_PANEL, 1);
13266 if (!panel) return NULL;
13267 panel->ctx = ctx;
13268 panel->parent_window_id = window_id;
13269 panel->nb_tabs = 0;
13270 panel->active_tab = -1;
13271 panel->x = x;
13272 panel->y = y;
13273 panel->button_w = button_w;
13274 panel->button_h = button_h;
13275 panel->on_tab_change = on_tab_change;
13276 panel->user_data = user_data;
13277 for (int i = 0; i < N_GUI_TAB_MAX; i++) {
13278 panel->button_ids[i] = -1;
13279 panel->content_window_ids[i] = -1;
13280 }
13281 return panel;
13282}
13283
13284int n_gui_tab_add(N_GUI_TAB_PANEL* panel, const char* label) {
13285 if (!panel || !label || panel->nb_tabs >= N_GUI_TAB_MAX) return -1;
13286 int idx = panel->nb_tabs;
13287 float bx = panel->x + (float)idx * panel->button_w;
13288 int btn_id = n_gui_add_toggle_button(panel->ctx, panel->parent_window_id,
13289 label, bx, panel->y, panel->button_w, panel->button_h,
13290 N_GUI_SHAPE_RECT, (idx == 0) ? 1 : 0,
13292 if (btn_id < 0) return -1;
13293 panel->button_ids[idx] = btn_id;
13294 panel->nb_tabs++;
13295 if (idx == 0) panel->active_tab = 0;
13296 return idx;
13297}
13298
13299void n_gui_tab_set_content_window(N_GUI_TAB_PANEL* panel, int tab_index, int window_id) {
13300 if (!panel || tab_index < 0 || tab_index >= panel->nb_tabs) return;
13301 panel->content_window_ids[tab_index] = window_id;
13302}
13303
13304void n_gui_tab_set_active(N_GUI_TAB_PANEL* panel, int index) {
13305 if (!panel || index < 0 || index >= panel->nb_tabs) return;
13306 for (int i = 0; i < panel->nb_tabs; i++) {
13307 n_gui_button_set_toggled(panel->ctx, panel->button_ids[i], (i == index) ? 1 : 0);
13308 if (panel->content_window_ids[i] >= 0) {
13309 if (i == index)
13310 n_gui_open_window(panel->ctx, panel->content_window_ids[i]);
13311 else
13312 n_gui_close_window(panel->ctx, panel->content_window_ids[i]);
13313 }
13314 }
13315 panel->active_tab = index;
13316}
13317
13319 return panel ? panel->active_tab : -1;
13320}
13321
13323 if (!panel || !*panel) return;
13324 FreeNoLog(*panel);
13325 *panel = NULL;
13326}
13327
13328/* TREE VIEW */
13329
13330static void _n_gui_tree_listbox_selected(int widget_id, int index, int selected, void* user_data) {
13331 (void)widget_id;
13332 N_GUI_TREE* tree = (N_GUI_TREE*)user_data;
13333 if (!tree || !selected) return;
13334 if (index < 0 || index >= tree->nb_visible) return;
13335 int node_idx = tree->visible_map[index];
13336 if (node_idx < 0 || node_idx >= tree->nb_nodes) return;
13337 if (tree->nodes[node_idx].has_children) n_gui_tree_toggle_expand(tree, node_idx);
13338 if (tree->on_select) tree->on_select(node_idx, tree->user_data);
13339}
13340
13341N_GUI_TREE* n_gui_tree_create(N_GUI_CTX* ctx, int window_id, float x, float y, float w, float h, void (*on_select)(int, void*), void* user_data) {
13342 if (!ctx) return NULL;
13343 N_GUI_TREE* tree = NULL;
13344 Malloc(tree, N_GUI_TREE, 1);
13345 if (!tree) return NULL;
13346 tree->ctx = ctx;
13347 tree->window_id = window_id;
13348 tree->nb_nodes = 0;
13349 tree->nb_visible = 0;
13350 tree->on_select = on_select;
13351 tree->user_data = user_data;
13352 tree->on_drop = NULL;
13353 tree->drop_user_data = NULL;
13354 tree->drag_source_node = -1;
13355 tree->drag_armed_node = -1;
13356 tree->drag_hover_node = -1;
13358 tree->drag_active = 0;
13359 tree->drag_press_x = 0.0f;
13360 tree->drag_press_y = 0.0f;
13361 tree->mouse_b1_prev = 0;
13362 tree->listbox_id = n_gui_add_listbox(ctx, window_id, x, y, w, h,
13364 if (tree->listbox_id < 0) {
13365 FreeNoLog(tree);
13366 return NULL;
13367 }
13368 return tree;
13369}
13370
13371static int _n_gui_tree_node_visible(N_GUI_TREE* tree, int node_index) {
13372 int pi = tree->nodes[node_index].parent_index;
13373 while (pi >= 0) {
13374 if (!tree->nodes[pi].expanded) return 0;
13375 pi = tree->nodes[pi].parent_index;
13376 }
13377 return 1;
13378}
13379
13380int n_gui_tree_add_node(N_GUI_TREE* tree, const char* label, int parent_index, void* user_data) {
13381 if (!tree || !label || tree->nb_nodes >= N_GUI_TREE_MAX) return -1;
13382 if (parent_index >= tree->nb_nodes) return -1;
13383 int idx = tree->nb_nodes;
13384 N_GUI_TREE_NODE* node = &tree->nodes[idx];
13385 snprintf(node->label, sizeof(node->label), "%s", label);
13386 node->parent_index = parent_index;
13387 node->expanded = 0;
13388 node->has_children = 0;
13389 node->user_data = user_data;
13390 if (parent_index < 0) {
13391 node->depth = 0;
13392 } else {
13393 node->depth = tree->nodes[parent_index].depth + 1;
13394 tree->nodes[parent_index].has_children = 1;
13395 }
13396 tree->nb_nodes++;
13397 n_gui_tree_rebuild(tree);
13398 return idx;
13399}
13400
13402 if (!tree) return;
13403 tree->nb_nodes = 0;
13404 tree->nb_visible = 0;
13405 n_gui_listbox_clear(tree->ctx, tree->listbox_id);
13406}
13407
13408void n_gui_tree_remove_node(N_GUI_TREE* tree, int node_index) {
13409 if (!tree) return;
13410 if (node_index < 0 || node_index >= tree->nb_nodes) return;
13411
13412 /* Mark the target and every descendant for removal. Iterate until
13413 no new marks appear so children of already-marked parents are
13414 captured regardless of insertion order. */
13415 char removed[N_GUI_TREE_MAX];
13416 memset(removed, 0, (size_t)tree->nb_nodes);
13417 removed[node_index] = 1;
13418 int changed = 1;
13419 while (changed) {
13420 changed = 0;
13421 for (int i = 0; i < tree->nb_nodes; i++) {
13422 if (removed[i]) continue;
13423 int p = tree->nodes[i].parent_index;
13424 if (p >= 0 && removed[p]) {
13425 removed[i] = 1;
13426 changed = 1;
13427 }
13428 }
13429 }
13430
13431 /* Build old-index -> new-index map for surviving nodes. */
13432 int remap[N_GUI_TREE_MAX];
13433 int new_count = 0;
13434 for (int i = 0; i < tree->nb_nodes; i++) {
13435 remap[i] = removed[i] ? -1 : new_count++;
13436 }
13437
13438 /* Compact nodes in place, translating parent_index through remap. */
13439 for (int i = 0; i < tree->nb_nodes; i++) {
13440 if (removed[i]) continue;
13441 int dst = remap[i];
13442 if (dst != i) tree->nodes[dst] = tree->nodes[i];
13443 if (tree->nodes[dst].parent_index >= 0) {
13444 tree->nodes[dst].parent_index = remap[tree->nodes[dst].parent_index];
13445 }
13446 }
13447 tree->nb_nodes = new_count;
13448
13449 /* Recompute has_children flags from scratch, any node whose
13450 only children were removed must no longer show the marker. */
13451 for (int i = 0; i < tree->nb_nodes; i++) {
13452 tree->nodes[i].has_children = 0;
13453 }
13454 for (int i = 0; i < tree->nb_nodes; i++) {
13455 int p = tree->nodes[i].parent_index;
13456 if (p >= 0) tree->nodes[p].has_children = 1;
13457 }
13458
13459 n_gui_tree_rebuild(tree);
13460}
13461
13462void n_gui_tree_set_label(N_GUI_TREE* tree, int node_index, const char* new_label) {
13463 if (!tree || !new_label) return;
13464 if (node_index < 0 || node_index >= tree->nb_nodes) return;
13465 snprintf(tree->nodes[node_index].label,
13466 sizeof(tree->nodes[node_index].label), "%s", new_label);
13467 n_gui_tree_rebuild(tree);
13468}
13469
13471 if (!tree) return -1;
13472 int row = n_gui_listbox_get_selected(tree->ctx, tree->listbox_id);
13473 if (row < 0 || row >= tree->nb_visible) return -1;
13474 return tree->visible_map[row];
13475}
13476
13477void n_gui_tree_set_selection(N_GUI_TREE* tree, int node_index) {
13478 if (!tree) return;
13479 if (node_index < 0 || node_index >= tree->nb_nodes) return;
13480
13481 /* Expand every ancestor so node_index will appear in visible_map. */
13482 int p = tree->nodes[node_index].parent_index;
13483 while (p >= 0) {
13484 tree->nodes[p].expanded = 1;
13485 p = tree->nodes[p].parent_index;
13486 }
13487 n_gui_tree_rebuild(tree);
13488
13489 /* Find the visible row for the node and mark it selected. */
13490 for (int row = 0; row < tree->nb_visible; row++) {
13491 if (tree->visible_map[row] == node_index) {
13492 n_gui_listbox_set_selected(tree->ctx, tree->listbox_id, row, 1);
13494 return;
13495 }
13496 }
13497}
13498
13499int n_gui_tree_find_by_user_data(const N_GUI_TREE* tree, const void* ptr) {
13500 if (!tree || !ptr) return -1;
13501 for (int i = 0; i < tree->nb_nodes; i++) {
13502 if (tree->nodes[i].user_data == ptr) return i;
13503 }
13504 return -1;
13505}
13506
13507void n_gui_tree_toggle_expand(N_GUI_TREE* tree, int node_index) {
13508 if (!tree || node_index < 0 || node_index >= tree->nb_nodes) return;
13509 if (!tree->nodes[node_index].has_children) return;
13510 tree->nodes[node_index].expanded = !tree->nodes[node_index].expanded;
13511 n_gui_tree_rebuild(tree);
13512}
13513
13515 if (!tree) return;
13516 /* Remember the logical node that was selected before we rewrite
13517 the listbox. Without this, any rebuild triggered by user
13518 interaction (e.g. clicking a folder to expand it) drops the
13519 selection because n_gui_listbox_clear wipes per-row state, so
13520 folder nodes never appear visually selected. Requests weren't
13521 affected because clicking a leaf doesn't trigger a rebuild. */
13522 int saved_node = n_gui_tree_get_selection(tree);
13523 int saved_scroll = n_gui_listbox_get_scroll_offset(tree->ctx, tree->listbox_id);
13524 n_gui_listbox_clear(tree->ctx, tree->listbox_id);
13525 tree->nb_visible = 0;
13526 for (int i = 0; i < tree->nb_nodes; i++) {
13527 if (!_n_gui_tree_node_visible(tree, i)) continue;
13528 N_GUI_TREE_NODE* node = &tree->nodes[i];
13529 char display[256];
13530 char indent[64];
13531 int pad = node->depth * 2;
13532 if (pad >= (int)sizeof(indent) - 1) pad = (int)sizeof(indent) - 2;
13533 memset(indent, ' ', (size_t)pad);
13534 indent[pad] = '\0';
13535 const char* marker = node->has_children ? (node->expanded ? "v " : "> ") : " ";
13536 snprintf(display, sizeof(display), "%s%s%s", indent, marker, node->label);
13538 if (tree->nb_visible < N_GUI_TREE_MAX) {
13539 tree->visible_map[tree->nb_visible] = i;
13540 tree->nb_visible++;
13541 }
13542 }
13543 /* Restore selection if the saved node is still visible. If the
13544 node was removed, collapsed-behind-an-ancestor or otherwise
13545 gone, leave the selection cleared, callers that need a
13546 specific new selection use n_gui_tree_set_selection. */
13547 if (saved_node >= 0) {
13548 for (int row = 0; row < tree->nb_visible; row++) {
13549 if (tree->visible_map[row] == saved_node) {
13551 row, 1);
13552 break;
13553 }
13554 }
13555 }
13556 /* Restore scroll position, clamped to valid range */
13557 if (saved_scroll > 0) {
13558 n_gui_listbox_set_scroll_offset(tree->ctx, tree->listbox_id, saved_scroll);
13559 }
13560}
13561
13563 if (!tree || !*tree) return;
13564 FreeNoLog(*tree);
13565 *tree = NULL;
13566}
13567
13568/* TREE DRAG-REORDER */
13569
13571#define _N_GUI_TREE_DRAG_THRESHOLD_PX 4.0f
13572
13574 n_gui_tree_on_drop_t on_drop,
13575 void* user_data) {
13576 if (!tree) return;
13577 tree->on_drop = on_drop;
13578 tree->drop_user_data = user_data;
13579 tree->drag_source_node = -1;
13580 tree->drag_armed_node = -1;
13581 tree->drag_hover_node = -1;
13582 tree->drag_active = 0;
13583}
13584
13593 float* out_x,
13594 float* out_y,
13595 float* out_w,
13596 float* out_h,
13597 float* out_item_area_w,
13598 float* out_inset,
13599 float* out_item_h,
13600 int* out_scroll_off) {
13601 if (!tree) return 0;
13602 const N_GUI_WIDGET* w = n_gui_get_widget(tree->ctx, tree->listbox_id);
13603 if (!w || w->type != N_GUI_TYPE_LISTBOX || !w->data) return 0;
13604 const N_GUI_WINDOW* win = n_gui_get_window(tree->ctx, tree->window_id);
13605 if (!win) return 0;
13606 const N_GUI_LISTBOX_DATA* ld = (const N_GUI_LISTBOX_DATA*)w->data;
13607 const N_GUI_STYLE* style = &tree->ctx->style;
13608
13609 *out_x = win->x + w->x;
13610 *out_y = win->y + win->titlebar_h + w->y;
13611 *out_w = w->w;
13612 *out_h = w->h;
13613 *out_item_h = (ld->item_height > 0.0f) ? ld->item_height : 20.0f;
13614 *out_scroll_off = ld->scroll_offset;
13615
13616 /* Match _draw_listbox: the scrollbar column is reserved when the
13617 number of items overflows the visible area. Subtract it from
13618 the content-area width so hit-testing and overlay indicators
13619 stop exactly where row highlights stop. */
13620 int visible_count = (int)(w->h / *out_item_h);
13621 int need_scrollbar = ((int)ld->nb_items > visible_count) ? 1 : 0;
13622 float sb_w = need_scrollbar ? style->scrollbar_size : 0.0f;
13623 *out_item_area_w = w->w - sb_w;
13624 *out_inset = style->item_selection_inset;
13625 return 1;
13626}
13627
13632static int _n_gui_tree_hit_row(N_GUI_TREE* tree, float mx, float my, int* out_node_idx, int* out_drop_pos) {
13633 float lx, ly, lw, lh, iw, inset, ih;
13634 int scroll;
13635 if (!_n_gui_tree_listbox_bounds(tree, &lx, &ly, &lw, &lh,
13636 &iw, &inset, &ih, &scroll))
13637 return 0;
13638 (void)lw; /* scrollbar column is excluded via iw below */
13639 (void)inset;
13640 if (mx < lx || mx > lx + iw || my < ly || my > ly + lh) return 0;
13641
13642 int row_in_view = (int)((my - ly) / ih);
13643 int actual_row = row_in_view + scroll;
13644 if (actual_row < 0 || actual_row >= tree->nb_visible) return 0;
13645
13646 int node_idx = tree->visible_map[actual_row];
13647 if (node_idx < 0 || node_idx >= tree->nb_nodes) return 0;
13648
13649 /* Tighter INTO zone: top 40% -> BEFORE, middle 20% -> INTO,
13650 bottom 40% -> AFTER. Previously the middle 40% was INTO,
13651 which made it too easy to accidentally re-nest a folder
13652 when the user was trying to move it back out between
13653 siblings. */
13654 float y_in_row = (my - ly) - (float)row_in_view * ih;
13655 int pos;
13656 if (y_in_row < ih * 0.4f)
13657 pos = N_GUI_DROP_BEFORE;
13658 else if (y_in_row > ih * 0.6f)
13659 pos = N_GUI_DROP_AFTER;
13660 else
13661 pos = N_GUI_DROP_INTO;
13662
13663 *out_node_idx = node_idx;
13664 *out_drop_pos = pos;
13665 return 1;
13666}
13667
13669 if (!tree || !tree->ctx) return;
13670 if (!tree->on_drop) return;
13671
13672 const N_GUI_CTX* ctx = tree->ctx;
13673 int b1 = ctx->mouse_b1;
13674 int b1_prev = tree->mouse_b1_prev;
13675 tree->mouse_b1_prev = b1;
13676
13677 float mx = (float)ctx->mouse_x;
13678 float my = (float)ctx->mouse_y;
13679
13680 int hit_node = -1;
13681 int pos = N_GUI_DROP_INTO;
13682 int hit = _n_gui_tree_hit_row(tree, mx, my, &hit_node, &pos);
13683
13684 /* Mouse press: arm the drag if we're over a row. */
13685 if (b1 && !b1_prev) {
13686 if (hit) {
13687 tree->drag_armed_node = hit_node;
13688 tree->drag_press_x = mx;
13689 tree->drag_press_y = my;
13690 } else {
13691 tree->drag_armed_node = -1;
13692 }
13693 tree->drag_active = 0;
13694 tree->drag_source_node = -1;
13695 tree->drag_hover_node = -1;
13696 }
13697
13698 /* While mouse is held: promote to active drag once past threshold. */
13699 if (b1 && tree->drag_armed_node >= 0) {
13700 if (!tree->drag_active) {
13701 float dx = mx - tree->drag_press_x;
13702 float dy = my - tree->drag_press_y;
13703 if (dx * dx + dy * dy >= _N_GUI_TREE_DRAG_THRESHOLD_PX *
13705 tree->drag_active = 1;
13706 tree->drag_source_node = tree->drag_armed_node;
13707 }
13708 }
13709 if (tree->drag_active) {
13710 tree->drag_hover_node = hit ? hit_node : -1;
13711 tree->drop_position = hit ? pos : N_GUI_DROP_INTO;
13712 }
13713 }
13714
13715 /* Mouse release: fire callback if we actually dragged. */
13716 if (!b1 && b1_prev) {
13717 if (tree->drag_active && tree->drag_source_node >= 0 && tree->drag_hover_node >= 0 && tree->drag_source_node != tree->drag_hover_node) {
13718 tree->on_drop(tree->drag_source_node,
13719 tree->drag_hover_node,
13720 tree->drop_position,
13721 tree->drop_user_data);
13722 }
13723 tree->drag_source_node = -1;
13724 tree->drag_armed_node = -1;
13725 tree->drag_hover_node = -1;
13726 tree->drag_active = 0;
13727 }
13728}
13729
13731 if (!tree || !tree->ctx) return;
13732 if (!tree->drag_active || tree->drag_hover_node < 0) return;
13733
13734 float lx, ly, lw, lh, iw, inset, ih;
13735 int scroll;
13736 if (!_n_gui_tree_listbox_bounds(tree, &lx, &ly, &lw, &lh,
13737 &iw, &inset, &ih, &scroll))
13738 return;
13739 (void)lw; /* use iw (item area) so we stop at the scrollbar column */
13740
13741 /* Find the visible-row index of drag_hover_node, translate to
13742 absolute Y using the listbox's scroll offset. */
13743 int row_in_view = -1;
13744 for (int row = 0; row < tree->nb_visible; row++) {
13745 if (tree->visible_map[row] == tree->drag_hover_node) {
13746 row_in_view = row - scroll;
13747 break;
13748 }
13749 }
13750 if (row_in_view < 0) return;
13751
13752 float row_y = ly + (float)row_in_view * ih;
13753 if (row_y < ly || row_y + ih > ly + lh) {
13754 /* Off-screen, don't draw. */
13755 return;
13756 }
13757
13758 /* Align horizontal span with the selection/hover highlights so
13759 the indicator looks exactly like part of the row, not a
13760 stripe floating over the scrollbar or past the text area. */
13761 float left = lx + inset;
13762 float right = lx + iw - inset;
13763
13764 N_GUI_WIDGET* w = n_gui_get_widget(tree->ctx, tree->listbox_id);
13765 ALLEGRO_COLOR colour = w ? w->theme.border_active
13767 const float thickness = 2.0f;
13768 const float half = thickness * 0.5f;
13769
13770 if (tree->drop_position == N_GUI_DROP_INTO) {
13771 /* Inset by half the stroke so the OUTER edge of the outline
13772 coincides with (left, row_y) .. (right, row_y+ih), the
13773 same bounds the selection fill uses. Without this the
13774 outline straddles the boundary and looks wider than the
13775 selection rectangle. */
13776 al_draw_rectangle(left + half, row_y + half,
13777 right - half, row_y + ih - half,
13778 colour, thickness);
13779 } else {
13780 /* Line sits exactly on the row boundary (between rows N-1/N
13781 for BEFORE, N/N+1 for AFTER). The stroke straddles that
13782 boundary so the bar reads as "between two rows" rather
13783 than "inside one row". */
13784 float line_y = (tree->drop_position == N_GUI_DROP_BEFORE)
13785 ? row_y
13786 : row_y + ih;
13787 al_draw_line(left, line_y,
13788 right, line_y,
13789 colour, thickness);
13790 }
13791}
13792
13793/* KEY-VALUE TABLE */
13794
13795static void _n_gui_kv_add_clicked(int widget_id, void* user_data) {
13796 (void)widget_id;
13797 N_GUI_KVTABLE* table = (N_GUI_KVTABLE*)user_data;
13798 if (!table) return;
13799 n_gui_kvtable_add_row(table, "", "", "", 1);
13800}
13801
13802static void _n_gui_kv_remove_clicked(int widget_id, void* user_data) {
13803 N_GUI_KVTABLE* table = (N_GUI_KVTABLE*)user_data;
13804 if (!table) return;
13805 for (int i = 0; i < table->nb_rows; i++) {
13806 if (table->rows[i].remove_id == widget_id && table->rows[i].active) {
13807 n_gui_kvtable_remove_row(table, i);
13808 if (table->on_remove) table->on_remove(i, table->user_data);
13809 return;
13810 }
13811 }
13812}
13813
13814/* Apply full layout to all KV table widgets: positions, sizes, and norms.
13815 Scales horizontal metrics by the parent window's current width relative to
13816 the width captured at create time, so the table tracks the window the same
13817 way other SCALE-policy widgets do. Vertical metrics scale only under
13818 N_GUI_WIN_RESIZE_SCALE (see below). Updating norm_x/y/w/h after each call
13819 keeps n_gui_apply_adaptive_resize in sync on the next display resize. */
13821 if (!table) return;
13822 const N_GUI_WINDOW* win = n_gui_get_window(table->ctx, table->window_id);
13823 if (!win) return;
13824
13825 /* Widths always follow the window: a wider dialog gives the text columns more
13826 * room. Heights follow it only when the window scales its widgets
13827 * (N_GUI_WIN_RESIZE_SCALE), where the rows must stay consistent with the
13828 * already-scaled widgets around them. Under _MOVE / _NONE the rows keep their
13829 * design height instead, so a user-resizable dialog that grows taller reveals
13830 * MORE rows rather than fatter ones (the window's scrollbar takes over past
13831 * that). Fixed-size dialogs are unaffected either way: there win->h ==
13832 * init_win_h, so the factor is 1. */
13833 float sx = (table->init_win_w > 0.0f) ? (win->w / table->init_win_w) : 1.0f;
13834 /* the two ways a window scales the widgets around the table: the embedded
13835 * SCALE resize policy, and a detached native window carrying
13836 * N_GUI_DETACH_SCALE_CONTENT (which rescales widgets from their norms) */
13837 int scales_content = (win->resize_policy == N_GUI_WIN_RESIZE_SCALE) ||
13839 float sy = (scales_content && table->init_win_h > 0.0f) ? (win->h / table->init_win_h) : 1.0f;
13840
13841 float pad_x = table->padding * sx;
13842 float pad_y = table->padding * sy;
13843 float hdr_h_outer = table->header_height * sy;
13844 float row_h = table->row_height * sy;
13845 float gap = 6.0f * sx;
13846
13847 float total_w = win->w - pad_x * 2.0f;
13848 if (total_w < 0.0f) total_w = 0.0f;
13849
13850 float rh_inner = row_h - 4.0f * sy;
13851 if (rh_inner < 1.0f) rh_inner = 1.0f;
13852 float rw_inner = table->row_height * sx - 4.0f * sx;
13853 if (rw_inner < 1.0f) rw_inner = 1.0f;
13854 float lbl_h = 18.0f * sy;
13855 if (lbl_h < 1.0f) lbl_h = 1.0f;
13856 float chk_w = rw_inner;
13857 float rem_w = rw_inner;
13858
13859 /* Column layout: the key column and the remove ("x") button are always
13860 shown; Value, Description and the enabled checkbox are optional. A full
13861 three-text-column table keeps the historical 0.27*width proportions; a
13862 reduced table divides the remaining width evenly so a single-value list
13863 fills the row. */
13864 int text_cols = 1 + (table->show_value ? 1 : 0) + (table->show_desc ? 1 : 0);
13865 float trailing = (table->show_enabled ? (chk_w + gap) : 0.0f) + gap + rem_w;
13866 float col_w;
13867 if (text_cols == 3) {
13868 col_w = total_w * 0.27f;
13869 } else {
13870 float avail = total_w - trailing;
13871 if (avail < 0.0f) avail = 0.0f;
13872 col_w = avail / (float)text_cols;
13873 }
13874 if (col_w < 0.0f) col_w = 0.0f;
13875
13876 /* Reserve top_offset above the header so a caller can share the window with
13877 a toolbar/heading without overlapping the table. */
13878 float top = pad_y + table->top_offset * sy;
13879
13880 N_GUI_WIDGET* w;
13881
13882 /* Header labels (hidden columns are hidden and skipped). */
13883 float hx = pad_x;
13884 w = n_gui_get_widget(table->ctx, table->lbl_key);
13885 if (w) {
13886 w->x = hx;
13887 w->y = top;
13888 w->w = col_w;
13889 w->h = lbl_h;
13891 }
13892 hx += col_w;
13893 n_gui_set_widget_visible(table->ctx, table->lbl_value, table->show_value);
13894 if (table->show_value) {
13895 w = n_gui_get_widget(table->ctx, table->lbl_value);
13896 if (w) {
13897 w->x = hx;
13898 w->y = top;
13899 w->w = col_w;
13900 w->h = lbl_h;
13902 }
13903 hx += col_w;
13904 }
13905 n_gui_set_widget_visible(table->ctx, table->lbl_desc, table->show_desc);
13906 if (table->show_desc) {
13907 w = n_gui_get_widget(table->ctx, table->lbl_desc);
13908 if (w) {
13909 w->x = hx;
13910 w->y = top;
13911 w->w = col_w;
13912 w->h = lbl_h;
13914 }
13915 hx += col_w;
13916 }
13917 n_gui_set_widget_visible(table->ctx, table->lbl_enabled, table->show_enabled);
13918 if (table->show_enabled) {
13919 w = n_gui_get_widget(table->ctx, table->lbl_enabled);
13920 if (w) {
13921 w->x = hx + gap;
13922 w->y = top;
13923 w->w = chk_w;
13924 w->h = lbl_h;
13926 }
13927 }
13928
13929 /* Active rows below the header; hidden columns are hidden and skipped. */
13930 float cy = top + hdr_h_outer;
13931 for (int i = 0; i < table->nb_rows; i++) {
13932 if (!table->rows[i].active) continue;
13933 const N_GUI_KV_ROW* row = &table->rows[i];
13934 float fx = pad_x;
13935 w = n_gui_get_widget(table->ctx, row->key_id);
13936 if (w) {
13937 w->x = fx;
13938 w->y = cy;
13939 w->w = col_w;
13940 w->h = rh_inner;
13942 }
13943 fx += col_w;
13944 n_gui_set_widget_visible(table->ctx, row->value_id, table->show_value);
13945 if (table->show_value) {
13946 w = n_gui_get_widget(table->ctx, row->value_id);
13947 if (w) {
13948 w->x = fx;
13949 w->y = cy;
13950 w->w = col_w;
13951 w->h = rh_inner;
13953 }
13954 fx += col_w;
13955 }
13956 n_gui_set_widget_visible(table->ctx, row->desc_id, table->show_desc);
13957 if (table->show_desc) {
13958 w = n_gui_get_widget(table->ctx, row->desc_id);
13959 if (w) {
13960 w->x = fx;
13961 w->y = cy;
13962 w->w = col_w;
13963 w->h = rh_inner;
13965 }
13966 fx += col_w;
13967 }
13969 if (table->show_enabled) {
13970 w = n_gui_get_widget(table->ctx, row->enabled_id);
13971 if (w) {
13972 w->x = fx + gap;
13973 w->y = cy;
13974 w->w = chk_w;
13975 w->h = rh_inner;
13977 }
13978 fx += gap + chk_w;
13979 }
13980 fx += gap;
13981 w = n_gui_get_widget(table->ctx, row->remove_id);
13982 if (w) {
13983 w->x = fx;
13984 w->y = cy;
13985 w->w = rem_w;
13986 w->h = rh_inner;
13988 }
13989 cy += row_h;
13990 }
13991
13992 /* "+" add-row button anchored below the last active row. */
13993 w = n_gui_get_widget(table->ctx, table->btn_add);
13994 if (w) {
13995 float btn_w = 30.0f * sx;
13996 w->x = pad_x;
13997 w->y = cy;
13998 w->w = btn_w;
13999 w->h = rh_inner;
14001 }
14002}
14003
14004N_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) {
14005 if (!ctx) return NULL;
14006 N_GUI_KVTABLE* table = NULL;
14007 Malloc(table, N_GUI_KVTABLE, 1);
14008 if (!table) return NULL;
14009 memset(table, 0, sizeof(*table));
14010 table->ctx = ctx;
14011 table->window_id = window_id;
14012 table->nb_rows = 0;
14013 table->nb_active = 0;
14014 table->row_height = row_height;
14015 table->padding = padding;
14016 table->header_height = 18.0f + 2.0f;
14017 table->on_remove = on_remove;
14018 table->user_data = user_data;
14019 table->show_value = 1; /* full three-column table by default (backward compatible) */
14020 table->show_desc = 1;
14021 table->show_enabled = 1;
14022 table->key_placeholder = NULL;
14023 table->value_placeholder = NULL;
14024 table->top_offset = 0.0f;
14025
14026 /* Capture parent window dimensions for later scale ratios. */
14027 const N_GUI_WINDOW* wptr = n_gui_get_window(ctx, window_id);
14028 table->init_win_w = wptr ? wptr->w : 0.0f;
14029 table->init_win_h = wptr ? wptr->h : 0.0f;
14030
14031 /* Widgets are added at placeholder coords; _n_gui_kv_apply_layout
14032 sets final positions, sizes, and normalized coords below. */
14033 float rh_inner = row_height - 4.0f;
14034 if (rh_inner < 1.0f) rh_inner = 1.0f;
14035 float hdr_h_label = 18.0f;
14036 table->lbl_key = n_gui_add_label(ctx, window_id, "Key",
14037 padding, padding, 100.0f, hdr_h_label, N_GUI_ALIGN_LEFT);
14038 table->lbl_value = n_gui_add_label(ctx, window_id, "Value",
14039 padding, padding, 100.0f, hdr_h_label, N_GUI_ALIGN_LEFT);
14040 table->lbl_desc = n_gui_add_label(ctx, window_id, "Description",
14041 padding, padding, 100.0f, hdr_h_label, N_GUI_ALIGN_LEFT);
14042 table->lbl_enabled = n_gui_add_label(ctx, window_id, "On",
14043 padding, padding, rh_inner, hdr_h_label, N_GUI_ALIGN_LEFT);
14044
14045 table->btn_add = n_gui_add_button(ctx, window_id,
14046 "+", padding, padding, 30.0f, rh_inner,
14048
14050 return table;
14051}
14052
14054 const char* key,
14055 const char* value,
14056 const char* description,
14057 int enabled) {
14058 if (!table) return -1;
14059 N_GUI_CTX* ctx = table->ctx;
14060 int win = table->window_id;
14061 float pad = table->padding;
14062 float rh = table->row_height - 4.0f;
14063 if (rh < 1.0f) rh = 1.0f;
14064
14065 /* Reuse a slot freed by remove/clear before growing the array, so repeated
14066 clear+repopulate cycles do not accumulate widgets toward N_GUI_KV_MAX. */
14067 int idx = -1;
14068 for (int i = 0; i < table->nb_rows; i++) {
14069 if (!table->rows[i].active) {
14070 idx = i;
14071 break;
14072 }
14073 }
14074 N_GUI_KV_ROW* row;
14075 if (idx >= 0) {
14076 row = &table->rows[idx];
14077 n_gui_set_widget_visible(ctx, row->key_id, 1);
14078 n_gui_set_widget_visible(ctx, row->value_id, 1);
14079 n_gui_set_widget_visible(ctx, row->desc_id, 1);
14081 n_gui_set_widget_visible(ctx, row->remove_id, 1);
14082 n_gui_textarea_set_text(ctx, row->key_id, key ? key : "");
14083 n_gui_textarea_set_text(ctx, row->value_id, value ? value : "");
14084 n_gui_textarea_set_text(ctx, row->desc_id, description ? description : "");
14085 n_gui_checkbox_set_checked(ctx, row->enabled_id, enabled);
14086 } else {
14087 if (table->nb_rows >= N_GUI_KV_MAX) return -1;
14088 idx = table->nb_rows;
14089 row = &table->rows[idx];
14090 /* Placeholder coords; _n_gui_kv_apply_layout assigns the real positions,
14091 sizes, and norms below so the new widgets match the current scale of
14092 the parent window. */
14093 row->key_id = n_gui_add_textarea(ctx, win, pad, pad, 100.0f, rh, 0, 256, NULL, NULL);
14094 row->value_id = n_gui_add_textarea(ctx, win, pad, pad, 100.0f, rh, 0, 1024, NULL, NULL);
14095 row->desc_id = n_gui_add_textarea(ctx, win, pad, pad, 100.0f, rh, 0, 256, NULL, NULL);
14096 row->enabled_id = n_gui_add_checkbox(ctx, win, "", pad, pad, rh, rh, enabled, NULL, NULL);
14097 row->remove_id = n_gui_add_button(ctx, win, "x", pad, pad, rh, rh,
14099 if (key) n_gui_textarea_set_text(ctx, row->key_id, key);
14100 if (value) n_gui_textarea_set_text(ctx, row->value_id, value);
14101 if (description) n_gui_textarea_set_text(ctx, row->desc_id, description);
14104 table->nb_rows++;
14105 }
14106 row->active = 1;
14107 table->nb_active++;
14109 return idx;
14110}
14111
14112void n_gui_kvtable_remove_row(N_GUI_KVTABLE* table, int row_index) {
14113 if (!table || row_index < 0 || row_index >= table->nb_rows) return;
14114 if (!table->rows[row_index].active) return;
14115 N_GUI_KV_ROW* row = &table->rows[row_index];
14116 n_gui_set_widget_visible(table->ctx, row->key_id, 0);
14117 n_gui_set_widget_visible(table->ctx, row->value_id, 0);
14118 n_gui_set_widget_visible(table->ctx, row->desc_id, 0);
14119 n_gui_set_widget_visible(table->ctx, row->enabled_id, 0);
14120 n_gui_set_widget_visible(table->ctx, row->remove_id, 0);
14121 row->active = 0;
14122 table->nb_active--;
14124}
14125
14127 if (!table) return;
14128 for (int i = 0; i < table->nb_rows; i++) {
14129 if (!table->rows[i].active) continue;
14130 N_GUI_KV_ROW* row = &table->rows[i];
14131 n_gui_set_widget_visible(table->ctx, row->key_id, 0);
14132 n_gui_set_widget_visible(table->ctx, row->value_id, 0);
14133 n_gui_set_widget_visible(table->ctx, row->desc_id, 0);
14134 n_gui_set_widget_visible(table->ctx, row->enabled_id, 0);
14135 n_gui_set_widget_visible(table->ctx, row->remove_id, 0);
14136 row->active = 0;
14137 }
14138 table->nb_active = 0;
14139 /* nb_rows is kept: the freed slots (and their widgets) are reused by
14140 n_gui_kvtable_add_row so a reload does not grow the widget count. */
14142}
14143
14144void n_gui_kvtable_set_columns(N_GUI_KVTABLE* table, int show_value, int show_desc, int show_enabled) {
14145 if (!table) return;
14146 table->show_value = show_value ? 1 : 0;
14147 table->show_desc = show_desc ? 1 : 0;
14148 table->show_enabled = show_enabled ? 1 : 0;
14150}
14151
14152void n_gui_kvtable_set_placeholders(N_GUI_KVTABLE* table, const char* key_hint, const char* value_hint) {
14153 if (!table) return;
14154 FreeNoLog(table->key_placeholder);
14156 table->key_placeholder = key_hint ? strdup(key_hint) : NULL;
14157 table->value_placeholder = value_hint ? strdup(value_hint) : NULL;
14158 for (int i = 0; i < table->nb_rows; i++) {
14160 table->key_placeholder ? table->key_placeholder : "");
14162 table->value_placeholder ? table->value_placeholder : "");
14163 }
14164}
14165
14166void n_gui_kvtable_set_top_offset(N_GUI_KVTABLE* table, float top_offset) {
14167 if (!table) return;
14168 table->top_offset = top_offset;
14170}
14171
14173 return table ? table->nb_active : 0;
14174}
14175
14179
14181 if (!table || !*table) return;
14182 FreeNoLog((*table)->key_placeholder);
14183 FreeNoLog((*table)->value_placeholder);
14184 FreeNoLog(*table);
14185 *table = NULL;
14186}
14187
14188/* SECTION LIST (foldable accordion).
14189 An app-level helper that owns widget ids and re-stacks them with a running
14190 cursor plus per-widget visibility, exactly like the kvtable above; the core
14191 window/widget engine is untouched, so N_GUI_WIN_AUTO_SCROLLBAR keeps working
14192 from the visible-widget bounding box (a folded section's hidden members do
14193 not count toward content height). Relayout only mutates existing widgets in
14194 place (visibility + position), so it is safe to call from a widget callback. */
14195
14196/* Rebuild a header's label with the ASCII fold glyph for its current state. */
14198 N_GUI_SECTION* s = &sl->sections[idx];
14199 char label[80];
14200 snprintf(label, sizeof(label), "%s %s", s->folded ? "+" : "-", s->title);
14201 n_gui_button_set_label(sl->ctx, s->header_id, label);
14202}
14203
14204/* Internal header on_click: flip the matching section's fold state, relayout,
14205 and notify the app via on_toggle (so it can persist + resize its window). */
14206static void _n_gui_sectionlist_header_clicked(int widget_id, void* user_data) {
14207 N_GUI_SECTIONLIST* sl = (N_GUI_SECTIONLIST*)user_data;
14208 if (!sl) return;
14209 for (int i = 0; i < sl->nb_sections; i++) {
14210 if (sl->sections[i].header_id == widget_id) {
14211 sl->sections[i].folded = !sl->sections[i].folded;
14213 if (sl->on_toggle) sl->on_toggle(i, sl->sections[i].folded, sl->user_data);
14214 return;
14215 }
14216 }
14217}
14218
14219N_GUI_SECTIONLIST* n_gui_sectionlist_create(N_GUI_CTX* ctx, int window_id, float x, float y0, float width, float header_h, float gap, void (*on_toggle)(int, int, void*), void* user_data) {
14220 if (!ctx) return NULL;
14221 N_GUI_SECTIONLIST* sl = NULL;
14222 Malloc(sl, N_GUI_SECTIONLIST, 1);
14223 if (!sl) return NULL;
14224 memset(sl, 0, sizeof(*sl));
14225 sl->ctx = ctx;
14226 sl->window_id = window_id;
14227 sl->x = x;
14228 sl->y0 = y0;
14229 sl->width = width;
14230 sl->header_h = header_h;
14231 sl->gap = gap;
14232 sl->on_toggle = on_toggle;
14233 sl->user_data = user_data;
14234 sl->nb_sections = 0;
14235 return sl;
14236}
14237
14238int n_gui_sectionlist_add_section(N_GUI_SECTIONLIST* sl, const char* title, float header_y, int folded) {
14239 if (!sl || !title || sl->nb_sections >= N_GUI_SECTIONLIST_MAX) return -1;
14240 int idx = sl->nb_sections;
14241 N_GUI_SECTION* s = &sl->sections[idx];
14242 memset(s, 0, sizeof(*s));
14243 strncpy(s->title, title, sizeof(s->title) - 1);
14244 s->title[sizeof(s->title) - 1] = '\0';
14245 s->folded = folded ? 1 : 0;
14246 s->nb_widgets = 0;
14247 s->natural_h = 0.0f;
14248 char label[80];
14249 snprintf(label, sizeof(label), "%s %s", s->folded ? "+" : "-", s->title);
14251 sl->nb_sections++;
14252 return idx;
14253}
14254
14255void n_gui_sectionlist_add_widget(N_GUI_SECTIONLIST* sl, int section_index, int widget_id) {
14256 if (!sl || section_index < 0 || section_index >= sl->nb_sections) return;
14257 N_GUI_SECTION* s = &sl->sections[section_index];
14258 if (s->nb_widgets >= N_GUI_SECTION_WIDGETS_MAX) return;
14259 const N_GUI_WIDGET* w = n_gui_get_widget(sl->ctx, widget_id);
14260 const N_GUI_WIDGET* hdr = n_gui_get_widget(sl->ctx, s->header_id);
14261 if (!w || !hdr) return;
14262 /* member offsets are captured relative to the section's build-time content
14263 top; relayout then translates the whole block to the live cursor */
14264 float content_top0 = hdr->y + sl->header_h + sl->gap;
14265 float dy = w->y - content_top0;
14266 s->widget_ids[s->nb_widgets] = widget_id;
14267 s->widget_dy[s->nb_widgets] = dy;
14268 s->nb_widgets++;
14269 float bottom = dy + w->h;
14270 if (bottom > s->natural_h) s->natural_h = bottom;
14271}
14272
14274 if (!sl) return;
14275 sl->first_inset = (inset > 0.0f) ? inset : 0.0f;
14276}
14277
14279 if (!sl) return;
14280 const N_GUI_WINDOW* win = n_gui_get_window(sl->ctx, sl->window_id);
14281 float cur = sl->y0;
14282 for (int i = 0; i < sl->nb_sections; i++) {
14283 const N_GUI_SECTION* s = &sl->sections[i];
14285 if (hdr) {
14286 /* the first header may be inset from the left so a caller button can
14287 * sit before it on the same row */
14288 float inset = (i == 0) ? sl->first_inset : 0.0f;
14289 hdr->x = sl->x + inset;
14290 hdr->y = cur;
14291 hdr->w = sl->width - inset;
14292 if (win) _n_gui_widget_capture_normalized(win, hdr);
14293 }
14294 cur += sl->header_h + sl->gap;
14295 for (int k = 0; k < s->nb_widgets; k++) {
14297 if (!w) continue;
14298 if (s->folded) {
14300 } else {
14302 w->y = cur + s->widget_dy[k];
14303 if (win) _n_gui_widget_capture_normalized(win, w);
14304 }
14305 }
14306 if (!s->folded) cur += s->natural_h + sl->gap;
14308 }
14309}
14310
14311void n_gui_sectionlist_set_folded(N_GUI_SECTIONLIST* sl, int section_index, int folded) {
14312 if (!sl || section_index < 0 || section_index >= sl->nb_sections) return;
14313 sl->sections[section_index].folded = folded ? 1 : 0;
14315}
14316
14317int n_gui_sectionlist_is_folded(const N_GUI_SECTIONLIST* sl, int section_index) {
14318 if (!sl || section_index < 0 || section_index >= sl->nb_sections) return 0;
14319 return sl->sections[section_index].folded;
14320}
14321
14323 if (!sl) return 0.0f;
14324 float cur = sl->y0;
14325 for (int i = 0; i < sl->nb_sections; i++) {
14326 cur += sl->header_h + sl->gap;
14327 if (!sl->sections[i].folded) cur += sl->sections[i].natural_h + sl->gap;
14328 }
14329 return cur;
14330}
14331
14333 return sl ? sl->nb_sections : 0;
14334}
14335
14337 if (!sl || !*sl) return;
14338 FreeNoLog(*sl);
14339 *sl = NULL;
14340}
ALLEGRO_DISPLAY * display
Definition ex_fluid.c:54
void on_link_click(int widget_id, const char *link, void *user_data)
Definition ex_gui.c:177
void on_scroll(int widget_id, double pos, void *user_data)
Definition ex_gui.c:118
static int mode
int run
Definition ex_kafka.c:56
char * key
#define FreeNoLog(__ptr)
Free Handler without log.
Definition n_common.h:272
#define Malloc(__ptr, __struct, __size)
Malloc Handler to get errors and set to 0.
Definition n_common.h:204
#define __n_assert(__ptr, __ret)
macro to assert things
Definition n_common.h:279
#define Free(__ptr)
Free Handler to get errors.
Definition n_common.h:263
char title[128]
column header title
Definition n_gui.h:641
char title[128]
window title
Definition n_gui.h:1003
ALLEGRO_BITMAP * thumb_bitmap
optional bitmap for the thumb/handle, normal state (NULL = color theme)
Definition n_gui.h:533
float h
height
Definition n_gui.h:894
float scroll_y
vertical scroll offset for overflowing text (pixels)
Definition n_gui.h:799
float scroll_y
vertical scroll offset for auto-scrollbar (pixels)
Definition n_gui.h:1033
int visible
visibility flag
Definition n_gui.h:898
ALLEGRO_BITMAP * panel_bitmap
optional bitmap for the dropdown panel background (NULL = color theme)
Definition n_gui.h:866
int saved_flags
embedded feature flags saved by n_gui_window_detach (a detached window is forced frameless because th...
Definition n_gui.h:1105
char * text
owned copy of the text, or NULL
Definition n_gui.h:615
size_t * col_order
display order: display_pos -> physical column index (sized cols_cap), or NULL when the display order ...
Definition n_gui.h:658
ALLEGRO_BITMAP * handle_hover_bitmap
optional bitmap for the handle on hover (NULL = fallback to handle_bitmap)
Definition n_gui.h:441
int window_id
owning window for coord math
Definition n_gui.h:2480
float button_h
height of each tab button
Definition n_gui.h:2376
int is_open
is dropdown currently open
Definition n_gui.h:756
int content_window_ids[16]
content window IDs (-1 = none)
Definition n_gui.h:2370
int w
bounding box width in pixels
Definition n_gui.h:334
int native_drag_wx
native window x at the drag grab, the move is native_drag_wx plus the desktop cursor travel since the...
Definition n_gui.h:1126
float gap
vertical gap between rows
Definition n_gui.h:2421
int open_combobox_id
id of the combobox whose dropdown is currently open, or -1
Definition n_gui.h:1390
ALLEGRO_COLOR scrollbar_track_color
scrollbar track colour
Definition n_gui.h:1180
int folded
1 = collapsed (members hidden), 0 = expanded
Definition n_gui.h:2407
int max_visible
max visible items in dropdown
Definition n_gui.h:767
int selected_index
currently selected index (-1 = none)
Definition n_gui.h:754
void(* on_remove)(int, void *)
callback on row removal
Definition n_gui.h:2578
ALLEGRO_BITMAP * minimize_active_bitmap
optional bitmap for minimize button active state
Definition n_gui.h:969
int sort_col
column index used for sorting, or -1
Definition n_gui.h:676
int scroll_offset
first visible row
Definition n_gui.h:609
size_t items_capacity
allocated capacity
Definition n_gui.h:559
ALLEGRO_COLOR grip_color
grip line colour
Definition n_gui.h:1208
float dropdown_border_thickness
dropdown panel border thickness
Definition n_gui.h:1290
char * text
text content (dynamically allocated, text_alloc bytes)
Definition n_gui.h:463
int nb_rows
total rows (including removed)
Definition n_gui.h:2566
float slider_handle_edge_offset
handle circle offset from edge
Definition n_gui.h:1220
int bytes_per_row
bytes shown per row
Definition n_gui.h:607
int selected_row
selected data row (the primary / last-clicked row), or -1
Definition n_gui.h:680
float widget_dy[48]
each member's y offset from the section content top, captured at add time
Definition n_gui.h:2405
int parent_index
parent node index, or -1 for root
Definition n_gui.h:2456
int shape
shape type: N_GUI_SHAPE_RECT or N_GUI_SHAPE_ROUNDED
Definition n_gui.h:529
float x
saved x (already display-scaled)
Definition n_gui.h:1326
float content_h
total content height computed from widgets (internal)
Definition n_gui.h:1037
float global_scrollbar_thumb_padding
global thumb inset from track edge
Definition n_gui.h:1190
char label[128]
menu label (shown on the button)
Definition n_gui.h:845
size_t items_capacity
allocated capacity
Definition n_gui.h:752
N_GUI_TREE_NODE nodes[2048]
node storage
Definition n_gui.h:2481
float x
position x on screen
Definition n_gui.h:1005
float scrollbar_size
scrollbar track width/height
Definition n_gui.h:1172
float norm_y
normalized position y (0.0-1.0 fraction of reference display height)
Definition n_gui.h:1063
void(* on_select)(int widget_id, int index, void *user_data)
callback on selection change (widget_id, selected_index, user_data)
Definition n_gui.h:731
float scrollbar_thumb_corner_r
thumb corner radius
Definition n_gui.h:1178
void(* on_change)(int widget_id, float ratio, void *user_data)
callback fired while the divider is dragged (widget_id, ratio, user_data)
Definition n_gui.h:595
int nb_widgets
number of member widgets
Definition n_gui.h:2406
int is_dynamic
is this entry dynamic (rebuilt via callback each time menu opens)
Definition n_gui.h:827
float title_padding
padding left of title text
Definition n_gui.h:1156
float min_win_h
minimum window height (for resize)
Definition n_gui.h:1154
float dropdown_arrow_half_w
horizontal half-extent of the arrow chevron
Definition n_gui.h:1288
int toggled
toggle state: 0 = off/unclicked, 1 = on/clicked (only used when toggle_mode=1)
Definition n_gui.h:394
char ** cells
row-major cell strings (nb_rows * nb_cols owned char*)
Definition n_gui.h:670
int lbl_enabled
label widget ID for "On" header
Definition n_gui.h:2576
ALLEGRO_BITMAP * item_bg_bitmap
optional bitmap for per-item background, normal state (NULL = color theme)
Definition n_gui.h:569
int scroll_offset
scroll offset in items
Definition n_gui.h:721
float ref_display_h
reference display height at the time normalized values were last captured
Definition n_gui.h:1436
float slider_track_corner_r
track corner radius
Definition n_gui.h:1214
int y
bounding box y offset from draw origin
Definition n_gui.h:333
float titlebar_h
title bar height
Definition n_gui.h:1013
ALLEGRO_BITMAP * handle_active_bitmap
optional bitmap for the handle while dragging (NULL = fallback to handle_bitmap)
Definition n_gui.h:443
void(* on_tab_change)(int, void *)
callback on tab switch (may be NULL)
Definition n_gui.h:2377
int selected_syntaxview_id
id of the syntax-view widget with the most recent text selection, or -1
Definition n_gui.h:1430
float top_offset
unscaled y offset of the header row from the window top (default 0), to reserve space above the table...
Definition n_gui.h:2585
float norm_y
normalized position y (fraction of parent window h, used for SCALE resize)
Definition n_gui.h:918
int dropmenu_max_visible
default max visible items for dropmenu panel
Definition n_gui.h:1272
float norm_x
normalized position x (0.0-1.0 fraction of reference display width)
Definition n_gui.h:1061
float textarea_padding
Definition n_gui.h:1236
int sel_start
selection start byte offset into text, or -1 when nothing is selected
Definition n_gui.h:623
ALLEGRO_BITMAP * bg_bitmap
optional bitmap for the label background (NULL = no background)
Definition n_gui.h:807
float item_height
item height
Definition n_gui.h:864
N_GUI_THEME theme
color theme for this widget
Definition n_gui.h:902
float title_max_w_reserve
pixels reserved right of title for truncation
Definition n_gui.h:1158
int multiselect
1 = multi-row selection enabled (Ctrl/Shift-click), 0 = single-select
Definition n_gui.h:682
double anim_until
deadline (al_get_time seconds) until which a transient animation is in progress (button key-press fla...
Definition n_gui.h:1478
size_t rows_cap
allocated row capacity (in rows)
Definition n_gui.h:674
int want_native
1 when the window should live in a native OS window: set by n_gui_window_detach and by a loaded layou...
Definition n_gui.h:1088
char text[128]
item display text
Definition n_gui.h:547
int nb_tabs
number of tabs
Definition n_gui.h:2371
int key_sources[8]
widget IDs that can trigger this focused key binding.
Definition n_gui.h:409
float checkbox_label_gap
gap between box and label text
Definition n_gui.h:1250
int open_dropmenu_id
id of the dropdown menu whose panel is currently open, or -1
Definition n_gui.h:1394
float global_scrollbar_thumb_corner_r
global thumb corner radius
Definition n_gui.h:1192
N_GUI_THEME default_theme
default theme (applied to new widgets/windows)
Definition n_gui.h:1366
int flags
bitmask of N_GUI_DROPMENU_* flags
Definition n_gui.h:876
float saved_y
embedded y saved by n_gui_window_detach, restored by n_gui_window_attach
Definition n_gui.h:1098
float dpi_scale
DPI scale factor (1.0 = normal, 1.25 = 125%, 2.0 = HiDPI, etc.)
Definition n_gui.h:1408
void * user_data
user data for callback
Definition n_gui.h:811
int scroll_from_wheel
set to 1 when scroll was changed by mouse wheel, cleared on key input
Definition n_gui.h:484
ALLEGRO_COLOR tooltip_fg
tooltip text color
Definition n_gui.h:1235
int btn_add
button widget ID for "+" add row
Definition n_gui.h:2577
ALLEGRO_BITMAP * bg_bitmap
optional bitmap for the window body background (NULL = color fill)
Definition n_gui.h:1045
float scrollbar_thumb_padding
thumb inset from track edge
Definition n_gui.h:1176
float w
width
Definition n_gui.h:892
ALLEGRO_COLOR text_normal
text normal
Definition n_gui.h:355
int remove_id
button widget ID for the remove action
Definition n_gui.h:2557
int pressed
which button is currently pressed (N_GUI_TB_BTN_*)
Definition n_gui.h:951
void * on_maximize_user_data
user data for on_maximize callback
Definition n_gui.h:989
int drop_position
N_GUI_DROP_BEFORE / INTO / AFTER.
Definition n_gui.h:2494
float w
window width
Definition n_gui.h:1009
int combobox_max_visible
default max visible items for combobox dropdown
Definition n_gui.h:1270
ALLEGRO_BITMAP * item_bg_bitmap
optional bitmap for per-item background, normal state (NULL = color theme)
Definition n_gui.h:771
float norm_h
normalized height (fraction of parent window h)
Definition n_gui.h:922
void * user_data
user data for callback
Definition n_gui.h:2486
int sel_dragging
1 while a mouse drag is extending the selection, 0 otherwise
Definition n_gui.h:627
void(* on_open)(int widget_id, void *user_data)
callback to rebuild dynamic entries each time the menu is opened.
Definition n_gui.h:872
size_t cols_cap
allocated column capacity
Definition n_gui.h:655
int mouse_b1_prev
previous mouse button 1 state
Definition n_gui.h:1376
int listbox_id
listbox widget ID
Definition n_gui.h:2479
float corner_rx
corner radius for rounded shapes
Definition n_gui.h:363
ALLEGRO_COLOR selection_color
text selection highlight colour (semi-transparent recommended)
Definition n_gui.h:367
int drag_hover_node
current hover node, or -1
Definition n_gui.h:2493
ALLEGRO_BITMAP * bitmap
optional bitmap for the button (NULL = color theme)
Definition n_gui.h:384
int flags
bitmask of N_GUI_COMBOBOX_* flags
Definition n_gui.h:779
float gui_offset_x
horizontal letterbox offset for virtual canvas
Definition n_gui.h:1416
int h_scroll_dragging
1 while the user is dragging the horizontal scrollbar thumb (so the shared scrollbar-drag handler rou...
Definition n_gui.h:699
int pending_layout_count
number of valid entries in pending_layout
Definition n_gui.h:1448
int state
saved minimised/maximised bits, applied on create
Definition n_gui.h:1330
float tooltip_delay
inner padding
Definition n_gui.h:1229
int key_focus_only
if 1, keycode fires only when the button itself or a source widget has focus.
Definition n_gui.h:406
int value_visible
draw the built-in value label next to/below the handle.
Definition n_gui.h:457
int ncols
column count (kind == DATAGRID)
Definition n_gui.h:1346
N_GUI_KV_ROW rows[128]
row storage
Definition n_gui.h:2565
char * tooltip
owned tooltip text shown after the pointer rests on the widget (NULL = none); heap-allocated with no ...
Definition n_gui.h:909
int type
widget type (N_GUI_TYPE_*)
Definition n_gui.h:886
int state
current state flags (N_GUI_STATE_*)
Definition n_gui.h:896
float scroll_step
pixels per mouse wheel notch for window auto-scroll
Definition n_gui.h:1311
int multiline
0 = single line, 1 = multiline
Definition n_gui.h:471
int lines_valid
0 when cached_nb_lines / cached_headers_end are stale (rebuild on next use).
Definition n_gui.h:631
int key_id
textarea widget ID for the key
Definition n_gui.h:2553
ALLEGRO_BITMAP * bitmap_hover
optional bitmap for hover state
Definition n_gui.h:386
int kind
N_GUI_TYPE_SPLITPANE or N_GUI_TYPE_DATAGRID.
Definition n_gui.h:1344
int button_ids[16]
toggle button widget IDs
Definition n_gui.h:2369
int max_visible
max visible items
Definition n_gui.h:862
float gui_bounds_w
total bounding box width of all windows (computed internally)
Definition n_gui.h:1404
double key_press_until
absolute al_get_time() deadline until which the button is rendered in its pressed (N_GUI_STATE_ACTIVE...
Definition n_gui.h:413
float x
position x relative to parent window
Definition n_gui.h:888
float drag_press_x
mouse x at press time
Definition n_gui.h:2496
float global_scrollbar_size
global scrollbar track width/height
Definition n_gui.h:1186
int highlight_index
open-panel navigation cursor for the wheel and Up/Down keys (-1 = none).
Definition n_gui.h:860
void(* on_toggle)(int, int, void *)
callback(section_index, folded, user_data) on a header click (may be NULL)
Definition n_gui.h:2425
float textarea_cursor_width
cursor width in pixels
Definition n_gui.h:1238
char text[4096]
text to display
Definition n_gui.h:793
void * user_data
user data for callback
Definition n_gui.h:2378
char title[128]
owning window title to match on
Definition n_gui.h:1342
float tb_btn_size
titlebar button size (0 = auto: titlebar_h - 4)
Definition n_gui.h:1162
float norm_h
normalized height (fraction of reference display height, used in SCALE mode)
Definition n_gui.h:1067
char value_format[32]
printf-style format string applied to the numeric value when the built-in readout draws.
Definition n_gui.h:452
float min_win_w
minimum window width (for resize)
Definition n_gui.h:1152
ALLEGRO_BITMAP * bg_bitmap
optional bitmap for the combobox background (NULL = color theme)
Definition n_gui.h:769
ALLEGRO_BITMAP * maximize_bitmap
optional bitmap for maximize button normal state (NULL = color theme)
Definition n_gui.h:971
void(* on_change)(int widget_id, double value, void *user_data)
callback on value change
Definition n_gui.h:445
float item_height
item height in pixels
Definition n_gui.h:723
int native_drag_wy
native window y at the drag grab (see native_drag_wx)
Definition n_gui.h:1128
float drag_ox
drag offset x (internal)
Definition n_gui.h:1025
void(* on_click)(int widget_id, void *user_data)
callback on click (widget_id passed)
Definition n_gui.h:415
float row_height
unscaled row height in pixels
Definition n_gui.h:2568
int expanded
1 if expanded
Definition n_gui.h:2457
N_GUI_LISTITEM * items
dynamic array of items
Definition n_gui.h:748
char label[128]
display text
Definition n_gui.h:2455
int mode
highlighting mode: N_GUI_SYNTAX_*
Definition n_gui.h:619
int drag_armed_node
pressed-but-not-yet-dragging
Definition n_gui.h:2492
ALLEGRO_BITMAP * close_hover_bitmap
optional bitmap for close button hover state
Definition n_gui.h:979
int show_enabled
1 to show the enabled-checkbox column (default 1)
Definition n_gui.h:2582
N_GUI_THEME theme
color theme for this window chrome
Definition n_gui.h:1021
int sel_end
selection end byte offset (tracks the cursor while a drag is in progress)
Definition n_gui.h:625
int id
unique widget id
Definition n_gui.h:884
int native_close_pending
1 when the native window must be released at the next safe point (an ALLEGRO_EVENT_DISPLAY_CLOSE arri...
Definition n_gui.h:1094
int sort_dir
sort direction: 1 ascending, -1 descending
Definition n_gui.h:678
float saved_w
embedded width saved by n_gui_window_detach, restored by n_gui_window_attach
Definition n_gui.h:1100
char title[64]
section title (header label minus the fold glyph)
Definition n_gui.h:2402
int desc_id
textarea widget ID for the description
Definition n_gui.h:2555
int widget_ids[48]
member widget ids
Definition n_gui.h:2404
float width
column width in pixels
Definition n_gui.h:643
int next_window_id
next window id to assign
Definition n_gui.h:1362
float item_selection_inset
item highlight inset from left/right edges
Definition n_gui.h:1276
int header_id
clickable header button widget id
Definition n_gui.h:2403
float saved_h
embedded height saved by n_gui_window_detach, restored by n_gui_window_attach
Definition n_gui.h:1102
ALLEGRO_COLOR border_normal
border normal
Definition n_gui.h:349
void(* on_click)(int widget_id, int entry_index, int tag, void *user_data)
callback when this entry is clicked (widget_id, entry_index, tag, user_data)
Definition n_gui.h:831
N_GUI_SECTION sections[16]
section storage
Definition n_gui.h:2422
void(* on_content_draw)(int window_id, void *user_data)
optional callback invoked during n_gui_draw immediately after this window's own content is drawn,...
Definition n_gui.h:1074
float init_win_w
parent window width at create time
Definition n_gui.h:2571
int has_children
1 if node has children
Definition n_gui.h:2459
N_GUI_THEME close_theme
theme for close button (defaults to red hover/active)
Definition n_gui.h:963
void * user_data
user data for callback
Definition n_gui.h:833
float value
progress fraction, clamped to 0.0 .
Definition n_gui.h:817
int scroll_offset
scroll offset
Definition n_gui.h:855
ALLEGRO_COLOR tooltip_bg
tooltip bubble background color
Definition n_gui.h:1231
ALLEGRO_COLOR text_hover
text hover
Definition n_gui.h:357
float restore_x
saved x position before maximise (for restore)
Definition n_gui.h:953
float dropdown_arrow_half_h
vertical half-extent of the arrow chevron
Definition n_gui.h:1286
int z_value
z-value for N_GUI_ZORDER_FIXED mode (lower = behind, higher = on top)
Definition n_gui.h:1043
ALLEGRO_BITMAP * item_bg_bitmap
optional bitmap for per-item background, normal state (NULL = color theme)
Definition n_gui.h:727
int highlight_index
open-panel navigation cursor for the wheel and Up/Down keys (-1 = none).
Definition n_gui.h:763
double cursor_time
timestamp of last cursor activity (for blink reset)
Definition n_gui.h:486
size_t entries_capacity
allocated capacity
Definition n_gui.h:851
int mouse_y
mouse state tracking
Definition n_gui.h:1372
float grip_line_thickness
grip line thickness
Definition n_gui.h:1206
int window_id
window holding the headers and members
Definition n_gui.h:2416
int lbl_key
label widget ID for "Key" header
Definition n_gui.h:2573
char key[128]
widget persist key to match on
Definition n_gui.h:1343
size_t sel_end
selection end position (tracks cursor during selection)
Definition n_gui.h:478
int resize_policy
per-window resize policy: N_GUI_WIN_RESIZE_NONE, _MOVE, or _SCALE
Definition n_gui.h:1059
N_GUI_THEME default_tb_close_theme
default theme for titlebar close button
Definition n_gui.h:1440
size_t nb_items
number of items
Definition n_gui.h:715
char persist_key[128]
optional stable key for persisting this widget's user-adjustable state (splitpane divider ratio,...
Definition n_gui.h:914
int native_drag_cx
desktop x of the mouse cursor when a native (N_GUI_DETACH_OWN_CHROME) title bar drag started.
Definition n_gui.h:1121
void(* on_close)(int window_id, void *user_data)
callback on close button click (NULL = default: clear N_GUI_WIN_OPEN)
Definition n_gui.h:991
char title[128]
window title to match on
Definition n_gui.h:1325
void * user_data
user data for callback
Definition n_gui.h:777
float first_inset
content-local left inset applied to the FIRST header only, so a button can sit before it on the same ...
Definition n_gui.h:2424
int cached_nb_lines
cached line count (matches _text_line_count), valid when lines_valid
Definition n_gui.h:633
float padding
unscaled padding in pixels
Definition n_gui.h:2569
unsigned char * data
owned copy of the bytes, or NULL
Definition n_gui.h:603
int detach_flags
options the window was detached with (N_GUI_DETACH_*), remembered so a close/open cycle and a restore...
Definition n_gui.h:1084
n_gui_tree_on_drop_t on_drop
drag-reorder state (managed by n_gui_tree_tick)
Definition n_gui.h:2489
void(* on_select)(int, void *)
selection callback
Definition n_gui.h:2485
int shape
shape type: N_GUI_SHAPE_RECT, N_GUI_SHAPE_ROUNDED, N_GUI_SHAPE_BITMAP
Definition n_gui.h:390
int z_order
z-order mode (N_GUI_ZORDER_NORMAL, _ALWAYS_ON_TOP, _ALWAYS_BEHIND, _FIXED)
Definition n_gui.h:1041
float max_ratio
upper clamp applied to ratio
Definition n_gui.h:591
int tag
user-defined tag for the entry (e.g.
Definition n_gui.h:829
int mouse_b1
mouse button 1 state
Definition n_gui.h:1374
float button_w
width of each tab button
Definition n_gui.h:2375
float display_w
display/viewport width (set via n_gui_set_display_size)
Definition n_gui.h:1396
int nb_sections
number of sections
Definition n_gui.h:2423
N_GUI_LISTITEM * items
dynamic array of items
Definition n_gui.h:713
float width
header width
Definition n_gui.h:2419
ALLEGRO_BITMAP * box_hover_bitmap
optional bitmap for the checkbox square on hover (NULL = fallback to box_bitmap/box_checked_bitmap)
Definition n_gui.h:511
int h
bounding box height in pixels
Definition n_gui.h:335
float grip_size
grip area size
Definition n_gui.h:1204
void * user_data
opaque user pointer handed back to the callback (not owned by n_gui)
Definition n_gui.h:941
float radio_label_gap
gap between circle and label text
Definition n_gui.h:1262
N_GUI_CTX * ctx
GUI context.
Definition n_gui.h:2478
ALLEGRO_DISPLAY * display
display pointer for clipboard operations (set via n_gui_set_display)
Definition n_gui.h:1426
void * drop_user_data
user data for on_drop
Definition n_gui.h:2490
float saved_x
embedded x saved by n_gui_window_detach, restored by n_gui_window_attach
Definition n_gui.h:1096
float norm_w
normalized width (fraction of reference display width, used in SCALE mode)
Definition n_gui.h:1065
float content_w
total content width computed from widgets (internal)
Definition n_gui.h:1039
N_GUI_PENDING_WIDGET * pending_widgets
saved widget state for keyed widgets that did NOT exist when the layout was loaded; n_gui_widget_set_...
Definition n_gui.h:1453
void * user_data
user data for this node
Definition n_gui.h:2460
float norm_x
normalized position x (fraction of parent window w, used for SCALE resize)
Definition n_gui.h:916
ALLEGRO_COLOR global_scrollbar_thumb_color
global scrollbar thumb colour
Definition n_gui.h:1198
int selected_label_id
id of the label widget with the most recent text selection, or -1
Definition n_gui.h:1428
float radio_inner_offset
inner filled circle shrink from outer
Definition n_gui.h:1260
float min_ratio
lower clamp applied to ratio
Definition n_gui.h:589
float min_w
minimum width
Definition n_gui.h:1029
unsigned char * row_sel
per-row selection flags (1 = selected), sized rows_cap; NULL until needed
Definition n_gui.h:684
int selected_index
currently selected index (-1 = none)
Definition n_gui.h:719
ALLEGRO_BITMAP * track_bitmap
optional bitmap for the track/rail background (NULL = color theme)
Definition n_gui.h:435
int mouse_x
mouse state tracking
Definition n_gui.h:1370
N_GUI_TB_BUTTONS tb_buttons
titlebar button state (minimize, maximize, close)
Definition n_gui.h:1069
int order[64]
display position -> physical column
Definition n_gui.h:1347
ALLEGRO_COLOR scrollbar_thumb_color
scrollbar thumb colour
Definition n_gui.h:1182
int active_tab
currently active tab index
Definition n_gui.h:2372
float y
saved y
Definition n_gui.h:1327
float native_h
last known native window height (see native_w)
Definition n_gui.h:1111
float checkbox_max_size
maximum box size
Definition n_gui.h:1244
size_t nb_entries
number of entries
Definition n_gui.h:849
int visible_map[2048]
visible row to node index
Definition n_gui.h:2483
int tooltip_anchor_x
pointer x when the tooltip was last armed (movement re-arms)
Definition n_gui.h:1386
float checkbox_mark_thickness
checkmark line thickness
Definition n_gui.h:1248
unsigned char * row_has_color
per-row tint flags (1 = row_color[row] is painted), sized rows_cap; NULL until a row color is set
Definition n_gui.h:691
ALLEGRO_BITMAP * close_bitmap
optional bitmap for close button normal state (NULL = color theme)
Definition n_gui.h:977
int sel_start
selection start byte offset (-1 = no selection)
Definition n_gui.h:801
ALLEGRO_BITMAP * bitmap
bitmap to display (not owned, not freed)
Definition n_gui.h:785
ALLEGRO_BITMAP * fill_bitmap
optional bitmap for the filled portion of the track (NULL = color theme)
Definition n_gui.h:437
ALLEGRO_COLOR tooltip_border
tooltip bubble border color
Definition n_gui.h:1233
void * on_close_user_data
user data for on_close callback
Definition n_gui.h:993
N_GUI_THEME default_tb_btn_theme
default theme for titlebar minimize/maximize buttons
Definition n_gui.h:1438
int tooltip_widget_id
widget the pointer is resting on for tooltip purposes (-1 = none)
Definition n_gui.h:1382
float header_height
unscaled column-header row height
Definition n_gui.h:2570
float global_scroll_step
pixels per mouse wheel notch for global scroll
Definition n_gui.h:1313
float h
window height
Definition n_gui.h:1011
float item_height
item height in dropdown
Definition n_gui.h:765
int hover_row
index of the row the mouse is currently over, or -1.
Definition n_gui.h:579
void * user_data
user data for callback
Definition n_gui.h:515
double scroll_pos
current scroll position
Definition n_gui.h:527
void(* on_change)(int widget_id, const char *text, void *user_data)
callback on text change
Definition n_gui.h:495
ALLEGRO_BITMAP * item_selected_bitmap
optional bitmap for per-item background, selected/highlighted (NULL = color theme)
Definition n_gui.h:773
void * user_data
user data for callback
Definition n_gui.h:447
float autofit_origin_y
original insertion point y for N_GUI_AUTOFIT_CENTER (set by n_gui_window_set_autofit)
Definition n_gui.h:1057
N_GUI_DATAGRID_COL * cols
column definitions (physical/storage order; cells are indexed by these)
Definition n_gui.h:651
int scroll_offset
scroll offset in items
Definition n_gui.h:563
int native_drag_cy
desktop y of the mouse cursor at the drag grab (see native_drag_cx)
Definition n_gui.h:1123
size_t len
number of bytes
Definition n_gui.h:605
int selected
selection state (for listbox)
Definition n_gui.h:549
ALLEGRO_BITMAP * minimize_hover_bitmap
optional bitmap for minimize button hover state
Definition n_gui.h:967
ALLEGRO_BITMAP * item_hover_bitmap
optional bitmap for the hovered entry background (NULL = color theme)
Definition n_gui.h:868
float virtual_h
virtual canvas height (0 = disabled / identity transform)
Definition n_gui.h:1412
int drag_active
1 once past threshold
Definition n_gui.h:2495
float y0
content-local y of the first header
Definition n_gui.h:2418
N_GUI_CTX * ctx
GUI context.
Definition n_gui.h:2563
int shape_mode
global widget shape override: N_GUI_SHAPE_ROUNDED forces a rounded GUI, N_GUI_SHAPE_RECT forces a squ...
Definition n_gui.h:1297
int visible
1 = drawn, 0 = hidden (kept in storage, skipped in the header and rows)
Definition n_gui.h:645
float global_scroll_x
global horizontal scroll offset (when GUI exceeds display)
Definition n_gui.h:1400
int cached_headers_end
cached first blank-line index (HTTP headers end), or INT_MAX; valid when lines_valid
Definition n_gui.h:635
int native_pos_x
last known native window position on the desktop, applied when the native window is re-created.
Definition n_gui.h:1114
float init_win_h
parent window height at create time
Definition n_gui.h:2572
float checkbox_mark_margin
checkmark inset from box edge
Definition n_gui.h:1246
ALLEGRO_BITMAP * track_bitmap
optional bitmap for the scrollbar track background (NULL = color theme)
Definition n_gui.h:531
ALLEGRO_DISPLAY * native
native OS window backing this pseudo-window, or NULL (the default) when the window is drawn as a pop-...
Definition n_gui.h:1081
float checkbox_label_offset
horizontal offset from box edge to label text
Definition n_gui.h:1252
N_GUI_CUSTOM_DRAW draw
paint callback invoked during n_gui_draw (NULL = draw nothing)
Definition n_gui.h:939
void(* on_columns_changed)(int widget_id, void *user_data)
callback fired after the user resizes a column by dragging its header border (widget_id,...
Definition n_gui.h:668
ALLEGRO_BITMAP * maximize_active_bitmap
optional bitmap for maximize button active state
Definition n_gui.h:975
ALLEGRO_BITMAP * titlebar_bitmap
optional bitmap for the titlebar background (NULL = color fill)
Definition n_gui.h:1047
int scroll_offset
scroll offset in dropdown
Definition n_gui.h:758
N_GUI_CTX * ctx
GUI context.
Definition n_gui.h:2367
int mouse_b1_prev
previous frame's b1 state
Definition n_gui.h:2498
void * data
widget-specific data (union via void pointer)
Definition n_gui.h:906
double value
current value (always snapped to step)
Definition n_gui.h:427
N_GUI_DROPMENU_ENTRY * entries
dynamic array of entries
Definition n_gui.h:847
float drag_oy
drag offset y (internal)
Definition n_gui.h:1027
float slider_track_border_thickness
track outline thickness
Definition n_gui.h:1216
int pending_widgets_count
number of valid entries in pending_widgets
Definition n_gui.h:1455
int nb_active
active row count
Definition n_gui.h:2567
ALLEGRO_COLOR link_color_normal
link colour (normal)
Definition n_gui.h:1305
float slider_handle_min_r
minimum handle circle radius
Definition n_gui.h:1218
int scale_mode
scale mode: N_GUI_IMAGE_FIT, N_GUI_IMAGE_STRETCH, N_GUI_IMAGE_CENTER
Definition n_gui.h:787
float restore_h
saved height before maximise (for restore)
Definition n_gui.h:959
ALLEGRO_BITMAP * item_selected_bitmap
optional bitmap for per-item background, selected/highlighted (NULL = color theme)
Definition n_gui.h:729
ALLEGRO_BITMAP * bg_bitmap
optional bitmap for the radiolist background (NULL = color theme)
Definition n_gui.h:725
float x
content-local x of every header and the layout origin
Definition n_gui.h:2417
int show_value
1 to show the Value column (default 1)
Definition n_gui.h:2580
ALLEGRO_BITMAP * box_bitmap
optional bitmap for the checkbox square, unchecked state (NULL = color theme)
Definition n_gui.h:507
void * on_open_user_data
user data for on_open callback
Definition n_gui.h:874
float drag_press_y
mouse y at press time
Definition n_gui.h:2497
int show_desc
1 to show the Description column (default 1)
Definition n_gui.h:2581
float y
y origin of tab button row
Definition n_gui.h:2374
float divider
divider thickness in pixels
Definition n_gui.h:593
float dropdown_arrow_reserve
horizontal space reserved for the arrow on the right
Definition n_gui.h:1282
ALLEGRO_BITMAP * close_active_bitmap
optional bitmap for close button active state
Definition n_gui.h:981
float y
position y relative to parent window
Definition n_gui.h:890
int lbl_desc
label widget ID for "Description" header
Definition n_gui.h:2575
int orientation
orientation: N_GUI_SLIDER_H or N_GUI_SLIDER_V
Definition n_gui.h:433
float global_scrollbar_border_thickness
global thumb border thickness
Definition n_gui.h:1194
int state
state flags (N_GUI_WIN_*)
Definition n_gui.h:1015
int visible[64]
per-physical-column visibility
Definition n_gui.h:1348
int flags
feature flags (N_GUI_WIN_AUTO_SCROLLBAR, N_GUI_WIN_RESIZABLE, etc.)
Definition n_gui.h:1017
int orientation
N_GUI_SPLIT_VERTICAL or N_GUI_SPLIT_HORIZONTAL.
Definition n_gui.h:585
void * user_data
user data for callback
Definition n_gui.h:541
int active
1 if active, 0 if removed
Definition n_gui.h:2558
double max_val
maximum value
Definition n_gui.h:425
HASH_TABLE * widgets_by_id
hash table for fast widget lookup by id
Definition n_gui.h:1358
float border_thickness
border thickness
Definition n_gui.h:361
float link_underline_thickness
link underline thickness
Definition n_gui.h:1303
ALLEGRO_BITMAP * box_checked_bitmap
optional bitmap for the checkbox square, checked state (NULL = color theme)
Definition n_gui.h:509
int autofit_flags
bitmask of N_GUI_AUTOFIT_* flags (0 = no auto-fitting)
Definition n_gui.h:1051
unsigned char consumed
1 once applied to a created window
Definition n_gui.h:1331
float ratio
divider position as a fraction of the widget extent (0..1)
Definition n_gui.h:587
int drag_source_node
node being dragged, or -1
Definition n_gui.h:2491
N_GUI_STYLE style
configurable style (sizes, colours, paddings)
Definition n_gui.h:1424
ALLEGRO_FONT * font
font for the title bar
Definition n_gui.h:1023
float col_resize_x0
cursor x at the start of a column-resize drag
Definition n_gui.h:662
char * key_placeholder
owned hint shown in each key textarea, or NULL
Definition n_gui.h:2583
void * user_data
user data for callback
Definition n_gui.h:497
N_GUI_THEME btn_theme
theme for minimize and maximize buttons
Definition n_gui.h:961
ALLEGRO_COLOR text_active
text active
Definition n_gui.h:359
float ref_display_w
reference display width at the time normalized values were last captured
Definition n_gui.h:1434
float restore_w
saved width before maximise (for restore)
Definition n_gui.h:957
int nb_nodes
number of nodes
Definition n_gui.h:2482
size_t items_capacity
allocated capacity
Definition n_gui.h:717
int parent_window_id
parent window for tab buttons
Definition n_gui.h:2368
double content_size
total content size
Definition n_gui.h:523
float autofit_origin_x
original insertion point x for N_GUI_AUTOFIT_CENTER (set by n_gui_window_set_autofit)
Definition n_gui.h:1055
void * user_data
user data for callback
Definition n_gui.h:575
double step
step increment (0 is treated as 1).
Definition n_gui.h:429
float global_scroll_y
global vertical scroll offset (when GUI exceeds display)
Definition n_gui.h:1402
int scrollbar_drag_widget_id
id of the widget whose scrollbar is being dragged, or -1
Definition n_gui.h:1392
ALLEGRO_BITMAP * maximize_hover_bitmap
optional bitmap for maximize button hover state
Definition n_gui.h:973
float min_h
minimum height
Definition n_gui.h:1031
float listbox_default_item_height
default item height for listbox
Definition n_gui.h:1266
void * user_data
user data for callback
Definition n_gui.h:707
size_t nb_cols
number of columns
Definition n_gui.h:653
ALLEGRO_COLOR global_scrollbar_thumb_border_color
global scrollbar thumb border colour
Definition n_gui.h:1200
float tb_btn_spacing
gap between titlebar buttons
Definition n_gui.h:1164
ALLEGRO_COLOR border_active
border active
Definition n_gui.h:353
float scroll_x
horizontal scroll offset for single-line text (pixels)
Definition n_gui.h:482
float label_padding
horizontal padding each side
Definition n_gui.h:1301
float titlebar_h
title bar height
Definition n_gui.h:1150
ALLEGRO_EVENT_QUEUE * event_queue
event queue the context registers native window event sources into (set via n_gui_set_event_queue).
Definition n_gui.h:1458
float ratio
divider ratio (kind == SPLITPANE)
Definition n_gui.h:1345
float gui_offset_y
vertical letterbox offset for virtual canvas
Definition n_gui.h:1418
int hovered
which button is currently hovered (N_GUI_TB_BTN_*)
Definition n_gui.h:949
ALLEGRO_BITMAP * item_selected_bitmap
optional bitmap for per-item background, selected/highlighted (NULL = color theme)
Definition n_gui.h:571
N_GUI_PENDING_GEOM * pending_layout
layout entries for windows that did NOT exist when the saved layout was loaded (lazily-created panels...
Definition n_gui.h:1446
void(* on_select)(int widget_id, int index, int selected, void *user_data)
callback on selection change (widget_id, item_index, selected, user_data)
Definition n_gui.h:573
float natural_h
stacked height of the members when expanded
Definition n_gui.h:2408
int scroll_offset
first visible line
Definition n_gui.h:621
float item_height
item height in pixels
Definition n_gui.h:565
void(* on_select)(int widget_id, int index, void *user_data)
callback on selection change (widget_id, selected_index, user_data)
Definition n_gui.h:775
int focused_widget_id
id of the widget that currently has focus, or -1
Definition n_gui.h:1368
float h_scroll
horizontal scroll offset in pixels: how far the columns are scrolled to the left when their total wid...
Definition n_gui.h:696
int id
unique window id
Definition n_gui.h:1001
int global_vscroll_drag
1 if global scrollbar vertical thumb is being dragged
Definition n_gui.h:1420
int sel_dragging
1 if mouse is actively dragging to select text
Definition n_gui.h:805
int nb_visible
number of visible rows
Definition n_gui.h:2484
ALLEGRO_COLOR global_scrollbar_track_color
global scrollbar track colour
Definition n_gui.h:1196
float item_height_pad
min padding added to font height for item height
Definition n_gui.h:1278
float display_h
display/viewport height
Definition n_gui.h:1398
size_t len
text length in bytes
Definition n_gui.h:617
N_GUI_LISTITEM * items
dynamic array of items
Definition n_gui.h:555
void * user_data
user data for callback
Definition n_gui.h:733
float corner_ry
corner radius Y for rounded shapes
Definition n_gui.h:365
int checked
checked state
Definition n_gui.h:505
int sel_end
selection end byte offset (-1 = no selection)
Definition n_gui.h:803
float width[64]
per-physical-column width
Definition n_gui.h:1349
int bg_scale_mode
scale mode for bg_bitmap: N_GUI_IMAGE_FIT, N_GUI_IMAGE_STRETCH, or N_GUI_IMAGE_CENTER
Definition n_gui.h:1049
size_t text_alloc
allocated buffer size (char_limit + 1)
Definition n_gui.h:467
ALLEGRO_DISPLAY * pass_display
display the pass currently being processed or drawn belongs to: NULL for the host's main display (and...
Definition n_gui.h:1465
ALLEGRO_COLOR bg_normal
background normal
Definition n_gui.h:343
ALLEGRO_BITMAP * handle_bitmap
optional bitmap for the draggable handle, normal state (NULL = color theme)
Definition n_gui.h:439
ALLEGRO_FONT * font
font used by this widget (NULL = context default)
Definition n_gui.h:904
float norm_w
normalized width (fraction of parent window w)
Definition n_gui.h:920
int resize_mode
context resize mode: N_GUI_RESIZE_VIRTUAL or N_GUI_RESIZE_ADAPTIVE
Definition n_gui.h:1432
float virtual_w
virtual canvas width (0 = disabled / identity transform)
Definition n_gui.h:1410
float radiolist_default_item_height
default item height for radiolist
Definition n_gui.h:1268
void * user_data
user data for the callback
Definition n_gui.h:2426
void(* on_minimize)(int window_id, void *user_data)
callback on minimize button click (NULL = default: toggle N_GUI_WIN_MINIMISED)
Definition n_gui.h:983
char label[128]
label displayed next to the checkbox
Definition n_gui.h:503
float slider_value_label_offset
gap between slider end and value label
Definition n_gui.h:1224
void(* on_context)(int widget_id, int row, int x, int y, void *user_data)
callback on right-click over a data row, for a context menu (widget_id, row, x, y,...
Definition n_gui.h:705
size_t nb_items
number of items
Definition n_gui.h:557
int depth
depth in tree (0 = root)
Definition n_gui.h:2458
float tb_btn_glyph_thickness
line thickness for titlebar button glyphs
Definition n_gui.h:1168
ALLEGRO_COLOR bg_active
background active/pressed
Definition n_gui.h:347
char mask_char
mask character for password fields (0 = no masking, e.g.
Definition n_gui.h:490
ALLEGRO_BITMAP * minimize_bitmap
optional bitmap for minimize button normal state (NULL = color theme)
Definition n_gui.h:965
int global_hscroll_drag
1 if global scrollbar horizontal thumb is being dragged
Definition n_gui.h:1422
float w
saved width (display-scaled), 0 when absent
Definition n_gui.h:1328
LIST * windows
ordered list of N_GUI_WINDOW* (back to front)
Definition n_gui.h:1356
char text[128]
display text
Definition n_gui.h:825
int scroll_y
scroll offset for long text
Definition n_gui.h:480
char text[4096]
optional centered overlay text (empty string = none)
Definition n_gui.h:819
void(* on_scroll)(int widget_id, double scroll_pos, void *user_data)
callback on scroll
Definition n_gui.h:539
int x
bounding box x offset from draw origin
Definition n_gui.h:332
float gui_bounds_h
total bounding box height of all windows (computed internally)
Definition n_gui.h:1406
double min_val
minimum value
Definition n_gui.h:423
float textarea_cursor_blink_period
cursor blink period in seconds (full cycle on+off)
Definition n_gui.h:1240
int lbl_value
label widget ID for "Value" header
Definition n_gui.h:2574
void(* on_toggle)(int widget_id, int checked, void *user_data)
callback on toggle
Definition n_gui.h:513
ALLEGRO_COLOR bg_hover
background hover
Definition n_gui.h:345
size_t sel_start
selection anchor position (where shift-click/shift-arrow started).
Definition n_gui.h:476
int window_id
parent window
Definition n_gui.h:2564
int cursor_shape
system mouse cursor currently applied to the display (an ALLEGRO_SYSTEM_MOUSE_CURSOR_* value),...
Definition n_gui.h:1380
void * on_content_draw_data
user data passed to on_content_draw
Definition n_gui.h:1076
float dropdown_arrow_thickness
arrow stroke thickness
Definition n_gui.h:1284
float tb_btn_right_margin
right margin from window edge to rightmost button
Definition n_gui.h:1166
void * user_data
user data for callback
Definition n_gui.h:2579
ALLEGRO_COLOR link_color_hover
link colour (hover)
Definition n_gui.h:1307
float radio_circle_min_r
minimum outer circle radius
Definition n_gui.h:1256
ALLEGRO_BITMAP * bg_bitmap
optional bitmap for the listbox background (NULL = color theme)
Definition n_gui.h:567
float item_text_padding
text padding inside list/combo/dropmenu items
Definition n_gui.h:1274
int next_widget_id
next widget id to assign
Definition n_gui.h:1360
int enabled_id
checkbox widget ID for the enabled toggle
Definition n_gui.h:2556
char * value_placeholder
owned hint shown in each value textarea, or NULL
Definition n_gui.h:2584
float autofit_border
padding/border around content for auto-fit (pixels, applied on each side)
Definition n_gui.h:1053
float gui_scale
computed uniform scale factor for virtual canvas
Definition n_gui.h:1414
int anchor_row
anchor row for Shift-click range selection, or -1
Definition n_gui.h:686
int native_drag_anchored
1 when the four native_drag_* fields hold a usable grab, that is when al_get_mouse_cursor_position wo...
Definition n_gui.h:1133
size_t cursor_pos
cursor position in text
Definition n_gui.h:473
float restore_y
saved y position before maximise (for restore)
Definition n_gui.h:955
int scroll_offset
first visible data row
Definition n_gui.h:693
void * user_data
user data for callback
Definition n_gui.h:417
int mode
mode: N_GUI_SLIDER_VALUE or N_GUI_SLIDER_PERCENT
Definition n_gui.h:431
int dirty
non-zero when the GUI's appearance may have changed since the last n_gui_draw.
Definition n_gui.h:1474
size_t char_limit
maximum character limit (0 = N_GUI_TEXT_MAX)
Definition n_gui.h:469
void(* on_select)(int widget_id, int row, void *user_data)
callback on row selection (widget_id, row, user_data)
Definition n_gui.h:701
int toggle_mode
toggle mode: 0 = momentary (default), 1 = toggle (stays clicked/unclicked)
Definition n_gui.h:392
int is_open
is the menu currently open
Definition n_gui.h:853
ALLEGRO_COLOR border_hover
border hover
Definition n_gui.h:351
N_GUI_CTX * ctx
GUI context.
Definition n_gui.h:2415
void * on_minimize_user_data
user data for on_minimize callback
Definition n_gui.h:985
float scrollbar_thumb_min
minimum thumb dimension
Definition n_gui.h:1174
int native_halted
1 while the native window's drawing is halted.
Definition n_gui.h:1139
int native_pos_y
last known native window y position (see native_pos_x)
Definition n_gui.h:1116
float x
x origin of first tab button
Definition n_gui.h:2373
ALLEGRO_BITMAP * thumb_hover_bitmap
optional bitmap for the thumb on hover (NULL = fallback to thumb_bitmap)
Definition n_gui.h:535
int value_id
textarea widget ID for the value
Definition n_gui.h:2554
char label[128]
label displayed on the button
Definition n_gui.h:382
ALLEGRO_BITMAP * bg_bitmap
optional bitmap for the text area background (NULL = color theme)
Definition n_gui.h:488
int align
text alignment: N_GUI_ALIGN_LEFT, N_GUI_ALIGN_CENTER, N_GUI_ALIGN_RIGHT
Definition n_gui.h:797
int prev_over_win
whether the pointer was over a window on the previous motion event, so a motion that leaves the GUI s...
Definition n_gui.h:1481
float native_w
last known native window width, 0 until the window is first detached.
Definition n_gui.h:1109
size_t nb_rows
number of rows
Definition n_gui.h:672
void(* on_maximize)(int window_id, void *user_data)
callback on maximize button click (NULL = default: toggle N_GUI_WIN_MAXIMISED)
Definition n_gui.h:987
size_t text_len
current text length
Definition n_gui.h:465
char link[4096]
optional hyperlink URL (empty string = no link)
Definition n_gui.h:795
int orientation
orientation: N_GUI_SCROLLBAR_H or N_GUI_SCROLLBAR_V
Definition n_gui.h:521
float global_scrollbar_thumb_min
global minimum thumb dimension
Definition n_gui.h:1188
double viewport_size
visible viewport size
Definition n_gui.h:525
int col_resize_col
physical column being resized by a header-border drag, or -1
Definition n_gui.h:660
unsigned char consumed
1 once applied
Definition n_gui.h:1350
void(* on_link_click)(int widget_id, const char *link, void *user_data)
callback when link is clicked (widget_id, link_url, user_data)
Definition n_gui.h:809
ALLEGRO_FONT * default_font
default font (must be set before adding widgets)
Definition n_gui.h:1364
float combobox_max_dropdown_width
maximum dropdown width for N_GUI_COMBOBOX_AUTO_WIDTH (0 = display width)
Definition n_gui.h:1317
int selection_mode
selection mode: N_GUI_SELECT_NONE, N_GUI_SELECT_SINGLE, N_GUI_SELECT_MULTIPLE
Definition n_gui.h:561
float h
saved height (display-scaled), 0 when absent
Definition n_gui.h:1329
size_t nb_items
number of items
Definition n_gui.h:750
int key_modifiers
required modifier key flags for the keybind (0 = no modifier requirement, matches any modifier state ...
Definition n_gui.h:403
ALLEGRO_COLOR * row_color
per-row background tint, sized rows_cap; NULL until a row color is set
Definition n_gui.h:688
float header_h
header button height
Definition n_gui.h:2420
LIST * widgets
list of N_GUI_WIDGET* contained in this window
Definition n_gui.h:1019
void * user_data
user data for callback
Definition n_gui.h:597
float y
position y on screen
Definition n_gui.h:1007
ALLEGRO_BITMAP * bitmap_active
optional bitmap for active/pressed state
Definition n_gui.h:388
int tooltip_anchor_y
pointer y when the tooltip was last armed
Definition n_gui.h:1388
double tooltip_armed_at
time the tooltip hover was (re)armed, from al_get_time()
Definition n_gui.h:1384
char * placeholder
optional placeholder/hint text drawn dimmed while the field is empty (NULL = none); cleared from view...
Definition n_gui.h:493
int enabled
enabled flag (1 = enabled, 0 = disabled: drawn dimmed and ignores input)
Definition n_gui.h:900
float radio_circle_border_thickness
outer circle border thickness
Definition n_gui.h:1258
float col_resize_w0
original width of the column at the start of a resize drag
Definition n_gui.h:664
int keycode
bound keyboard keycode (0 = none).
Definition n_gui.h:397
ALLEGRO_BITMAP * thumb_active_bitmap
optional bitmap for the thumb while dragging (NULL = fallback to thumb_bitmap)
Definition n_gui.h:537
float slider_track_size
track width (vertical) or height (horizontal)
Definition n_gui.h:1212
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
float scroll_x
horizontal scroll offset for auto-scrollbar (pixels)
Definition n_gui.h:1035
float slider_handle_border_thickness
handle border thickness
Definition n_gui.h:1222
const char * n_gui_syntaxview_get_text(N_GUI_CTX *ctx, int widget_id)
get the text currently shown by a syntax view
Definition n_gui.c:3679
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_combobox_set_bitmaps(N_GUI_CTX *ctx, int widget_id, ALLEGRO_BITMAP *bg, ALLEGRO_BITMAP *item_bg, ALLEGRO_BITMAP *item_selected)
Set optional bitmap overlays on a combobox widget.
Definition n_gui.c:4938
int n_gui_load_theme_json(N_GUI_CTX *ctx, const char *filepath)
load a theme and style from a JSON file
Definition n_gui.c:12558
int n_gui_listbox_get_scroll_offset(N_GUI_CTX *ctx, int widget_id)
get the current scroll offset (in items)
Definition n_gui.c:3512
void n_gui_dropmenu_clear(N_GUI_CTX *ctx, int widget_id)
Remove all entries from a dropdown menu.
Definition n_gui.c:4723
#define N_GUI_STATE_HOVER
mouse is hovering the widget
Definition n_gui.h:201
void n_gui_sectionlist_set_folded(N_GUI_SECTIONLIST *sl, int section_index, int folded)
set a section's folded state and relayout; does NOT fire on_toggle (for programmatic restore)
Definition n_gui.c:14311
void n_gui_update_transform(N_GUI_CTX *ctx)
Recalculate scale and offset from virtual canvas to physical display.
Definition n_gui.c:8849
#define N_GUI_DROPMENU_EXPAND_UP
dropdown panel opens upward (above the button) instead of downward.
Definition n_gui.h:840
int n_gui_add_hexview(N_GUI_CTX *ctx, int window_id, float x, float y, float w, float h)
Add a read-only hexadecimal byte viewer.
Definition n_gui.c:2707
int n_gui_button_is_toggled(N_GUI_CTX *ctx, int widget_id)
Check if a toggle button is currently in the "on" state.
Definition n_gui.c:2209
void n_gui_datagrid_sort(N_GUI_CTX *ctx, int widget_id, int col, int dir)
sort the rows by a column (dir 1 ascending, -1 descending)
Definition n_gui.c:4176
float n_gui_detect_dpi_scale(N_GUI_CTX *ctx, ALLEGRO_DISPLAY *display)
Detect and apply DPI scale from an Allegro display.
Definition n_gui.c:9113
float n_gui_splitpane_get_ratio(N_GUI_CTX *ctx, int widget_id)
get the current divider ratio of a split pane
Definition n_gui.c:3538
void n_gui_toggle_window(N_GUI_CTX *ctx, int window_id)
Toggle window visibility (show if hidden, hide if shown)
Definition n_gui.c:1950
void n_gui_datagrid_set_on_columns_changed(N_GUI_CTX *ctx, int widget_id, void(*cb)(int, void *))
set the callback fired after the user drags a column border to resize it, so the host can persist the...
Definition n_gui.c:4171
void n_gui_datagrid_set_selected(N_GUI_CTX *ctx, int widget_id, int row)
set the selected row index (fires the on_select callback)
Definition n_gui.c:3958
void n_gui_hexview_set_data(N_GUI_CTX *ctx, int widget_id, const unsigned char *data, size_t len)
set (copy) the bytes shown by a hex viewer
Definition n_gui.c:3588
#define N_GUI_TREE_MAX
maximum number of nodes in a tree view
Definition n_gui.h:2451
#define N_GUI_ZORDER_NORMAL
default z-order: window participates in normal raise/lower ordering
Definition n_gui.h:308
#define N_GUI_ALIGN_LEFT
left aligned text
Definition n_gui.h:189
void n_gui_raise_window(N_GUI_CTX *ctx, int window_id)
Bring a window to the front (top of draw order).
Definition n_gui.c:1824
void n_gui_datagrid_clear_row_color(N_GUI_CTX *ctx, int widget_id, int row)
remove the background tint of data row row (it draws untinted again)
Definition n_gui.c:4051
int n_gui_add_slider(N_GUI_CTX *ctx, int window_id, float x, float y, float w, float h, double min_val, double max_val, double initial, int mode, void(*on_change)(int, double, void *), void *user_data)
Add a slider widget.
Definition n_gui.c:2442
void n_gui_combobox_set_flags(N_GUI_CTX *ctx, int widget_id, int flags)
Set combobox feature flags.
Definition n_gui.c:2915
void n_gui_window_apply_autofit(N_GUI_CTX *ctx, int window_id)
Trigger auto-fit recalculation for a window.
Definition n_gui.c:2035
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
int n_gui_needs_redraw(N_GUI_CTX *ctx)
Return non-zero if the GUI must be redrawn this frame, i.e.
Definition n_gui.c:978
int n_gui_datagrid_get_row_count(N_GUI_CTX *ctx, int widget_id)
get the number of rows in a data grid
Definition n_gui.c:3938
#define N_GUI_SYNTAX_HTTP
syntax view: HTTP message highlighting (request/status line and headers)
Definition n_gui.h:135
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
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_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
#define N_GUI_DETACH_SCALE_CONTENT
scale the window's widgets when the native window is resized by the user.
Definition n_gui.h:256
#define N_GUI_TB_BTN_NONE
no titlebar button
Definition n_gui.h:270
#define N_GUI_SLIDER_PERCENT
slider uses 0-100 percentage
Definition n_gui.h:157
#define N_GUI_DROP_BEFORE
drop position relative to a tree row (drag-and-drop)
Definition n_gui.h:2464
int n_gui_kvtable_get_count(const N_GUI_KVTABLE *table)
get the number of active rows
Definition n_gui.c:14172
double n_gui_scrollbar_get_pos(N_GUI_CTX *ctx, int widget_id)
get the current scroll position of a scrollbar widget
Definition n_gui.c:3319
void n_gui_tree_set_label(N_GUI_TREE *tree, int node_index, const char *new_label)
change the display label of a node
Definition n_gui.c:13462
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
int n_gui_add_vslider(N_GUI_CTX *ctx, int window_id, float x, float y, float w, float h, double min_val, double max_val, double initial, int mode, void(*on_change)(int, double, void *), void *user_data)
Add a vertical slider widget.
Definition n_gui.c:2484
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
void n_gui_window_set_bitmaps(N_GUI_CTX *ctx, int window_id, ALLEGRO_BITMAP *bg, ALLEGRO_BITMAP *titlebar, int bg_scale_mode)
Set optional bitmap overlays on a window's body and titlebar.
Definition n_gui.c:4797
void n_gui_set_widget_theme(N_GUI_CTX *ctx, int widget_id, N_GUI_THEME theme)
Override the theme of a specific widget.
Definition n_gui.c:3042
#define N_GUI_SECTIONLIST_MAX
maximum number of foldable sections in a section list
Definition n_gui.h:2395
const char * n_gui_datagrid_get_cell(N_GUI_CTX *ctx, int widget_id, int row, int col)
get a cell string by row and column, or NULL if out of range
Definition n_gui.c:3944
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_dropmenu_get_count(N_GUI_CTX *ctx, int widget_id)
Get number of entries in a dropdown menu.
Definition n_gui.c:4748
int n_gui_radiolist_add_item(N_GUI_CTX *ctx, int widget_id, const char *text)
add an item to a radiolist widget
Definition n_gui.c:4349
#define N_GUI_WIN_VSCROLL_DRAG
window vertical auto-scrollbar is being dragged
Definition n_gui.h:219
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:2240
#define N_GUI_TYPE_DATAGRID
widget type: sortable column data grid
Definition n_gui.h:119
void n_gui_lower_window(N_GUI_CTX *ctx, int window_id)
Lower a window to the bottom of the draw order.
Definition n_gui.c:1842
void n_gui_label_set_text(N_GUI_CTX *ctx, int widget_id, const char *text)
set the text of a label widget
Definition n_gui.c:4511
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
size_t n_gui_datagrid_get_column_count(N_GUI_CTX *ctx, int widget_id)
number of columns in a data grid (physical), or 0
Definition n_gui.c:4115
int n_gui_add_listbox(N_GUI_CTX *ctx, int window_id, float x, float y, float w, float h, int selection_mode, void(*on_select)(int, int, int, void *), void *user_data)
Add a listbox widget.
Definition n_gui.c:2620
#define N_GUI_COMBOBOX_EXPAND_UP
dropdown panel opens upward (above the widget) instead of downward.
Definition n_gui.h:743
#define N_GUI_PERSIST_MAX_COLS
Maximum datagrid columns whose layout is persisted per widget.
Definition n_gui.h:1335
float n_gui_datagrid_get_h_scroll(N_GUI_CTX *ctx, int widget_id)
horizontal scroll offset in pixels (how far the columns are scrolled left when their total width exce...
Definition n_gui.c:4093
#define N_GUI_COMBOBOX_AUTO_WIDTH
dropdown panel expands to fit the longest item text
Definition n_gui.h:738
#define N_GUI_TYPE_LABEL
widget type: static text label (with optional hyperlink)
Definition n_gui.h:109
#define N_GUI_WIN_RESIZE_SCALE
reposition AND resize proportionally, child widgets scale too
Definition n_gui.h:290
void n_gui_datagrid_set_row_color(N_GUI_CTX *ctx, int widget_id, int row, ALLEGRO_COLOR color)
paint data row row with a background tint (a per-row highlight, e.g.
Definition n_gui.c:4041
void n_gui_datagrid_set_scroll_offset(N_GUI_CTX *ctx, int widget_id, int offset)
set the first visible data row (clamped to the row count); the draw path re-clamps to the live viewpo...
Definition n_gui.c:4082
size_t n_gui_datagrid_get_selected_rows(N_GUI_CTX *ctx, int widget_id, int *out, size_t max)
fill out with the indices of the selected rows (ascending), up to max, and return the total number of...
Definition n_gui.c:3995
int n_gui_datagrid_get_scroll_offset(N_GUI_CTX *ctx, int widget_id)
first visible data row (the vertical scroll position), or 0.
Definition n_gui.c:4076
#define N_GUI_TYPE_CHECKBOX
widget type: checkbox
Definition n_gui.h:97
void n_gui_window_set_tb_button_bitmaps(N_GUI_CTX *ctx, int window_id, int btn_type, ALLEGRO_BITMAP *normal, ALLEGRO_BITMAP *hover, ALLEGRO_BITMAP *active)
Set optional bitmap overlays on a titlebar button.
Definition n_gui.c:1723
void n_gui_focus_window(N_GUI_CTX *ctx, int window_id)
Give a window keyboard focus and bring it to the front.
Definition n_gui.c:1881
#define N_GUI_KV_MAX
maximum number of rows in a KV table
Definition n_gui.h:2549
#define N_GUI_TYPE_DROPMENU
widget type: dropdown menu with static and dynamic entries
Definition n_gui.h:111
int n_gui_process_event(N_GUI_CTX *ctx, ALLEGRO_EVENT event)
Process an allegro event through the GUI system.
Definition n_gui.c:9836
int n_gui_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
#define N_GUI_DETACH_NONE
no option
Definition n_gui.h:248
int n_gui_add_label(N_GUI_CTX *ctx, int window_id, const char *text, float x, float y, float w, float h, int align)
Add a static text label.
Definition n_gui.c:2966
void n_gui_set_widget_enabled(N_GUI_CTX *ctx, int widget_id, int enabled)
Enable or disable a widget.
Definition n_gui.c:3084
int n_gui_add_radiolist(N_GUI_CTX *ctx, int window_id, float x, float y, float w, float h, void(*on_select)(int, int, void *), void *user_data)
Add a radio list widget (single selection with radio bullets)
Definition n_gui.c:2839
#define N_GUI_SPLIT_HORIZONTAL
split pane: horizontal divider, regions are top and bottom
Definition n_gui.h:130
#define N_GUI_WIN_FIXED_POSITION
disable window dragging (default:enable)
Definition n_gui.h:231
void n_gui_tab_free(N_GUI_TAB_PANEL **panel)
free a tab panel (does not destroy the N_GUI widgets)
Definition n_gui.c:13322
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
void n_gui_listbox_set_bitmaps(N_GUI_CTX *ctx, int widget_id, ALLEGRO_BITMAP *bg, ALLEGRO_BITMAP *item_bg, ALLEGRO_BITMAP *item_selected)
Set optional bitmap overlays on a listbox widget.
Definition n_gui.c:4906
void n_gui_label_set_link(N_GUI_CTX *ctx, int widget_id, const char *link)
set the link URL of a label widget
Definition n_gui.c:4531
void n_gui_datagrid_set_on_context(N_GUI_CTX *ctx, int widget_id, void(*on_context)(int, int, int, int, void *))
set the right-click context callback; fired (with the cursor x/y in GUI coordinates) when a data row ...
Definition n_gui.c:3967
#define N_GUI_RESIZE_VIRTUAL
fixed virtual canvas with uniform scaling (default/existing behavior)
Definition n_gui.h:280
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_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_button_set_keycode_focused(N_GUI_CTX *ctx, int widget_id, int keycode, int modifiers, const int *sources, int source_count)
Set a focused key binding on a button.
Definition n_gui.c:2421
void n_gui_radiolist_clear(N_GUI_CTX *ctx, int widget_id)
remove all items from a radiolist widget
Definition n_gui.c:4370
#define N_GUI_TYPE_SPLITPANE
widget type: split pane with a draggable divider between two regions
Definition n_gui.h:113
void n_gui_kvtable_clear(N_GUI_KVTABLE *table)
remove every row at once.
Definition n_gui.c:14126
void n_gui_slider_set_value_format(N_GUI_CTX *ctx, int widget_id, const char *fmt)
Override the built-in value readout's printf format string.
Definition n_gui.c:3222
void n_gui_listbox_set_selected(N_GUI_CTX *ctx, int widget_id, int index, int selected)
set the selection state of a listbox item
Definition n_gui.c:3501
#define N_GUI_SELECT_NONE
no selection allowed (display only)
Definition n_gui.h:173
int(* n_gui_tree_on_drop_t)(int src_node, int dst_node, int position, void *user_data)
drag-reorder drop callback.
Definition n_gui.h:2474
#define N_GUI_TYPE_HEXVIEW
widget type: read-only hexadecimal byte viewer
Definition n_gui.h:115
int n_gui_add_window_auto(N_GUI_CTX *ctx, const char *title, float x, float y)
Add a window with automatic sizing (use n_gui_window_autosize after adding widgets)
Definition n_gui.c:1942
void n_gui_syntaxview_set_selection(N_GUI_CTX *ctx, int widget_id, int start, int end)
set the selection range of a syntax view (byte offsets into the text)
Definition n_gui.c:3719
double n_gui_slider_get_value(N_GUI_CTX *ctx, int widget_id)
get the current value of a slider widget
Definition n_gui.c:3159
int n_gui_listbox_add_item(N_GUI_CTX *ctx, int widget_id, const char *text)
add an item to a listbox widget
Definition n_gui.c:3371
N_GUI_THEME n_gui_default_tb_btn_theme(void)
Build a default theme for titlebar minimize/maximize buttons.
Definition n_gui.c:728
void n_gui_set_virtual_size(N_GUI_CTX *ctx, float w, float h)
Set the virtual canvas size for resolution-independent scaling.
Definition n_gui.c:8831
int n_gui_tooltip_pending(N_GUI_CTX *ctx)
Report the tooltip phase.
Definition n_gui.c:2323
int n_gui_datagrid_is_row_selected(N_GUI_CTX *ctx, int widget_id, int row)
report whether a given data row is selected (1) or not (0)
Definition n_gui.c:3986
float n_gui_get_dpi_scale(const N_GUI_CTX *ctx)
Get current DPI scale factor.
Definition n_gui.c:9089
void n_gui_tree_clear(N_GUI_TREE *tree)
remove all nodes and clear the backing listbox; tree object remains valid
Definition n_gui.c:13401
N_GUI_THEME n_gui_default_theme(void)
Build a sensible default colour theme.
Definition n_gui.c:651
#define N_GUI_WIN_RESIZE_NONE
no adaptation: absolute position and size unchanged
Definition n_gui.h:286
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:1903
int n_gui_radiolist_get_selected(N_GUI_CTX *ctx, int widget_id)
get the selected item index in a radiolist widget
Definition n_gui.c:4386
void n_gui_tab_set_content_window(N_GUI_TAB_PANEL *panel, int tab_index, int window_id)
associate a content window with a tab
Definition n_gui.c:13299
#define N_GUI_TYPE_SLIDER
widget type: slider
Definition n_gui.h:93
#define N_GUI_WIN_OPEN
window is visible
Definition n_gui.h:211
void n_gui_button_set_label(N_GUI_CTX *ctx, int widget_id, const char *label)
Replace the label text drawn on a button.
Definition n_gui.c:2221
void n_gui_button_set_state_bitmaps(N_GUI_CTX *ctx, int widget_id, ALLEGRO_BITMAP *normal, ALLEGRO_BITMAP *hover, ALLEGRO_BITMAP *active)
Skin a button (or toggle button) with per-state bitmaps.
Definition n_gui.c:2365
int n_gui_save_theme_json(N_GUI_CTX *ctx, const char *filepath)
save the current theme and style to a JSON file
Definition n_gui.c:12429
#define N_GUI_SELECT_SINGLE
single item selection
Definition n_gui.h:175
#define N_GUI_TB_BTN_CLOSE
close titlebar button
Definition n_gui.h:276
int n_gui_combobox_add_item(N_GUI_CTX *ctx, int widget_id, const char *text)
add an item to a combo box
Definition n_gui.c:4431
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
float n_gui_sectionlist_content_height(const N_GUI_SECTIONLIST *sl)
total content height in pixels under the current fold state, for sizing/scrolling the host window
Definition n_gui.c:14322
void n_gui_window_autosize(N_GUI_CTX *ctx, int window_id)
Recompute and apply minimum-fit size for a window based on its current widgets.
Definition n_gui.c:1987
int n_gui_syntaxview_get_line_count(N_GUI_CTX *ctx, int widget_id)
get the number of text lines in a syntax view
Definition n_gui.c:3667
#define N_GUI_SECTION_WIDGETS_MAX
maximum number of member widgets per section
Definition n_gui.h:2398
void n_gui_window_set_minimize_callback(N_GUI_CTX *ctx, int window_id, void(*on_minimize)(int, void *), void *user_data)
Set the minimize button callback for a window.
Definition n_gui.c:1681
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:2666
const char * n_gui_widget_get_persist_key(N_GUI_CTX *ctx, int widget_id)
Get a widget's persist key ("" when unset), or NULL when the widget is invalid.
Definition n_gui.c:2306
int n_gui_datagrid_add_column(N_GUI_CTX *ctx, int widget_id, const char *title, float width)
append a column with a header title and pixel width; returns the column index or -1
Definition n_gui.c:3805
void n_gui_set_focus(N_GUI_CTX *ctx, int widget_id)
Set keyboard focus to a specific widget.
Definition n_gui.c:3105
void n_gui_tree_rebuild(N_GUI_TREE *tree)
rebuild the listbox to reflect current tree state
Definition n_gui.c:13514
void n_gui_datagrid_set_h_scroll(N_GUI_CTX *ctx, int widget_id, float px)
set the horizontal scroll offset in pixels (clamped to >= 0; the draw path re-clamps to the live cont...
Definition n_gui.c:4099
void n_gui_datagrid_set_multiselect(N_GUI_CTX *ctx, int widget_id, int enabled)
enable (1) or disable (0) multi-row selection.
Definition n_gui.c:3973
void n_gui_textarea_set_selection(N_GUI_CTX *ctx, int widget_id, size_t start, size_t end)
set the text selection range (byte offsets into text content)
Definition n_gui.c:5258
#define N_GUI_SELECT_MULTIPLE
multiple item selection
Definition n_gui.h:177
void n_gui_scrollbar_set_bitmaps(N_GUI_CTX *ctx, int widget_id, ALLEGRO_BITMAP *track, ALLEGRO_BITMAP *thumb, ALLEGRO_BITMAP *thumb_hover, ALLEGRO_BITMAP *thumb_active)
Set optional bitmap overlays on a scrollbar widget.
Definition n_gui.c:4830
#define N_GUI_WIN_FRAMELESS
frameless window: no title bar drawn, drag via window body unless N_GUI_WIN_FIXED_POSITION is also se...
Definition n_gui.h:233
void n_gui_set_shape_mode(N_GUI_CTX *ctx, int shape_mode)
Set the global widget shape (round or square GUI).
Definition n_gui.c:9054
int n_gui_add_combobox(N_GUI_CTX *ctx, int window_id, float x, float y, float w, float h, void(*on_select)(int, int, void *), void *user_data)
Add a combo box widget (dropdown selector)
Definition n_gui.c:2874
void n_gui_syntaxview_set_mode(N_GUI_CTX *ctx, int widget_id, int mode)
set the highlighting mode of a syntax view
Definition n_gui.c:3655
void n_gui_button_set_toggled(N_GUI_CTX *ctx, int widget_id, int toggled)
Set the toggle state of a button.
Definition n_gui.c:2379
void n_gui_progressbar_set_text(N_GUI_CTX *ctx, int widget_id, const char *text)
set the centered overlay text (NULL or "" clears it)
Definition n_gui.c:4297
void n_gui_tab_set_active(N_GUI_TAB_PANEL *panel, int index)
set the active tab (toggles buttons, opens/closes content windows)
Definition n_gui.c:13304
#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_slider_set_step(N_GUI_CTX *ctx, int widget_id, double step)
set slider step increment.
Definition n_gui.c:3211
int n_gui_add_toggle_button(N_GUI_CTX *ctx, int window_id, const char *label, float x, float y, float w, float h, int shape, int initial_state, void(*on_click)(int, void *), void *user_data)
Add a toggle button widget (stays clicked/unclicked on single click)
Definition n_gui.c:2193
int n_gui_get_topmost_open_window(const N_GUI_CTX *ctx)
Get the id of the frontmost open, non-minimized window.
Definition n_gui.c:1861
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
int n_gui_add_label_link(N_GUI_CTX *ctx, int window_id, const char *text, const char *link, float x, float y, float w, float h, int align, void(*on_link_click)(int, const char *, void *), void *user_data)
Add a static text label with hyperlink.
Definition n_gui.c:3007
void n_gui_textarea_set_mask_char(N_GUI_CTX *ctx, int widget_id, char mask)
Set a mask character for password-style input.
Definition n_gui.c:4877
void n_gui_window_set_tb_close_theme(N_GUI_CTX *ctx, int window_id, N_GUI_THEME theme)
Set the theme for the titlebar close button on a specific window.
Definition n_gui.c:1713
void n_gui_window_set_tb_btn_theme(N_GUI_CTX *ctx, int window_id, N_GUI_THEME theme)
Set the theme for titlebar minimize/maximize buttons on a specific window.
Definition n_gui.c:1703
int n_gui_add_progressbar(N_GUI_CTX *ctx, int window_id, float x, float y, float w, float h)
add a horizontal progress bar (display only); returns the widget id or -1
Definition n_gui.c:4253
int n_gui_tab_get_active(const N_GUI_TAB_PANEL *panel)
get the active tab index
Definition n_gui.c:13318
size_t n_gui_hexview_get_length(N_GUI_CTX *ctx, int widget_id)
get the number of bytes currently held by a hex viewer
Definition n_gui.c:3610
int n_gui_listbox_get_selected(N_GUI_CTX *ctx, int widget_id)
get the index of the first selected item in a listbox
Definition n_gui.c:3465
#define N_GUI_AUTOFIT_EXPAND_LEFT
expand leftward instead of rightward when adjusting width
Definition n_gui.h:300
int n_gui_add_image(N_GUI_CTX *ctx, int window_id, float x, float y, float w, float h, ALLEGRO_BITMAP *bitmap, int scale_mode)
Add an image display widget.
Definition n_gui.c:2937
void n_gui_widget_set_persist_key(N_GUI_CTX *ctx, int widget_id, const char *key)
Give a widget a stable key so its user-adjustable state (a splitpane's divider ratio,...
Definition n_gui.c:2293
void n_gui_label_set_bitmap(N_GUI_CTX *ctx, int widget_id, ALLEGRO_BITMAP *bg)
Set optional background bitmap on a label widget.
Definition n_gui.c:4969
void n_gui_draw(N_GUI_CTX *ctx)
Draw all visible windows and their widgets.
Definition n_gui.c:8542
void n_gui_progressbar_set_value(N_GUI_CTX *ctx, int widget_id, float value)
set the progress fraction (clamped to 0.0 .
Definition n_gui.c:4279
#define N_GUI_TYPE_SYNTAXVIEW
widget type: read-only syntax-highlighted text view
Definition n_gui.h:117
#define N_GUI_WIN_MINIMISED
window is minimised (title bar only)
Definition n_gui.h:213
#define N_GUI_SLIDER_H
horizontal slider (default)
Definition n_gui.h:161
int n_gui_tree_find_by_user_data(const N_GUI_TREE *tree, const void *ptr)
find the first node whose user_data pointer equals ptr, or -1.
Definition n_gui.c:13499
void n_gui_tree_set_selection(N_GUI_TREE *tree, int node_index)
programmatically select a node; ancestors are expanded so the node is visible, and the listbox scroll...
Definition n_gui.c:13477
#define N_GUI_AUTOFIT_CENTER
center the window on its insertion point after auto-fit (overrides EXPAND_LEFT/EXPAND_UP for centerin...
Definition n_gui.h:304
int n_gui_tab_add(N_GUI_TAB_PANEL *panel, const char *label)
add a tab to the panel, returns tab index
Definition n_gui.c:13284
void n_gui_tree_set_on_drop(N_GUI_TREE *tree, n_gui_tree_on_drop_t on_drop, void *user_data)
install (or clear, with NULL) a drag-reorder drop callback.
Definition n_gui.c:13573
void n_gui_slider_set_bitmaps(N_GUI_CTX *ctx, int widget_id, ALLEGRO_BITMAP *track, ALLEGRO_BITMAP *fill, ALLEGRO_BITMAP *handle, ALLEGRO_BITMAP *handle_hover, ALLEGRO_BITMAP *handle_active)
Set optional bitmap overlays on a slider widget.
Definition n_gui.c:4812
#define N_GUI_SYNTAX_XML
syntax view: XML/HTML highlighting (tags, attributes, quoted values, comments)
Definition n_gui.h:139
int n_gui_window_get_zorder(N_GUI_CTX *ctx, int window_id)
Get window z-order mode.
Definition n_gui.c:1921
void n_gui_tree_free(N_GUI_TREE **tree)
free a tree view (does not destroy the N_GUI listbox)
Definition n_gui.c:13562
N_GUI_SECTIONLIST * n_gui_sectionlist_create(N_GUI_CTX *ctx, int window_id, float x, float y0, float width, float header_h, float gap, void(*on_toggle)(int, int, void *), void *user_data)
create an empty section list anchored at (x,y0) in window_id; sections stack downward from y0
Definition n_gui.c:14219
void n_gui_datagrid_move_column(N_GUI_CTX *ctx, int widget_id, int from, int to)
move the column at display position from to display position to, shifting the columns in between (reo...
Definition n_gui.c:4156
#define N_GUI_TYPE_IMAGE
widget type: image display
Definition n_gui.h:107
int n_gui_window_get_resize_policy(N_GUI_CTX *ctx, int window_id)
Get per-window resize policy.
Definition n_gui.c:8949
void n_gui_checkbox_set_checked(N_GUI_CTX *ctx, int widget_id, int checked)
set the checked state of a checkbox widget
Definition n_gui.c:3304
int n_gui_dropmenu_add_dynamic_entry(N_GUI_CTX *ctx, int widget_id, const char *text, int tag, void(*on_click)(int, int, int, void *), void *user_data)
Add a dynamic entry (rebuilt each time menu opens)
Definition n_gui.c:4669
void n_gui_apply_adaptive_resize(N_GUI_CTX *ctx, float new_w, float new_h)
Apply adaptive resize: reposition/resize all windows according to their policies for the new display ...
Definition n_gui.c:8978
int n_gui_tree_get_selection(N_GUI_TREE *tree)
get the index of the currently selected node, or -1 when nothing is selected or the selection points ...
Definition n_gui.c:13470
int n_gui_is_widget_enabled(N_GUI_CTX *ctx, int widget_id)
Check if a widget is enabled.
Definition n_gui.c:3093
void n_gui_button_set_keycode(N_GUI_CTX *ctx, int widget_id, int keycode, int modifiers)
Bind a keyboard key with optional modifier requirements to a button.
Definition n_gui.c:2411
#define N_GUI_SYNTAX_JSON
syntax view: JSON highlighting (keys, strings, numbers, punctuation)
Definition n_gui.h:137
N_GUI_THEME n_gui_default_tb_close_theme(void)
Build a default theme for the titlebar close button.
Definition n_gui.c:751
char * n_gui_syntaxview_get_selected_text(N_GUI_CTX *ctx, int widget_id)
get a copy of the currently selected text in a syntax view
Definition n_gui.c:3692
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
void n_gui_set_dpi_scale(N_GUI_CTX *ctx, float scale)
Set DPI scale factor manually (default 1.0)
Definition n_gui.c:9079
#define N_GUI_TYPE_RADIOLIST
widget type: radio list (single select radio buttons)
Definition n_gui.h:103
#define N_GUI_SLIDER_V
vertical slider
Definition n_gui.h:163
int n_gui_get_resize_mode(N_GUI_CTX *ctx)
Get current resize mode.
Definition n_gui.c:8926
N_GUI_WIDGET * n_gui_get_widget(N_GUI_CTX *ctx, int widget_id)
Get a widget pointer by id.
Definition n_gui.c:3028
int n_gui_dropmenu_add_entry(N_GUI_CTX *ctx, int widget_id, const char *text, int tag, void(*on_click)(int, int, int, void *), void *user_data)
Add a static entry to a dropdown menu.
Definition n_gui.c:4646
int n_gui_datagrid_get_row_color(N_GUI_CTX *ctx, int widget_id, int row, ALLEGRO_COLOR *out)
report whether data row row carries a background tint (1) or not (0), and copy the tint into out when...
Definition n_gui.c:4059
#define N_GUI_ZORDER_ALWAYS_BEHIND
always drawn behind normal windows, cannot be raised above them
Definition n_gui.h:312
#define N_GUI_IMAGE_STRETCH
stretch to fill bounds
Definition n_gui.h:183
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
#define N_GUI_ZORDER_ALWAYS_ON_TOP
always drawn on top of normal windows, cannot be lowered behind them
Definition n_gui.h:310
int n_gui_add_syntaxview(N_GUI_CTX *ctx, int window_id, float x, float y, float w, float h, int mode)
Add a read-only syntax-highlighted text view.
Definition n_gui.c:2745
void n_gui_datagrid_clear_selection(N_GUI_CTX *ctx, int widget_id)
clear the entire selection (no row selected)
Definition n_gui.c:4016
void n_gui_dropmenu_set_bitmaps(N_GUI_CTX *ctx, int widget_id, ALLEGRO_BITMAP *panel, ALLEGRO_BITMAP *item_hover)
Set optional bitmap overlays on a dropmenu widget.
Definition n_gui.c:4954
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
void n_gui_slider_set_value_visible(N_GUI_CTX *ctx, int widget_id, int visible)
Toggle the built-in value label.
Definition n_gui.c:3234
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_datagrid_select_row(N_GUI_CTX *ctx, int widget_id, int row, int selected)
select (1) or deselect (0) a single row in a multi-select grid programmatically (e....
Definition n_gui.c:4025
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_DROP_AFTER
Definition n_gui.h:2466
void n_gui_datagrid_clear_rows(N_GUI_CTX *ctx, int widget_id)
remove all rows from a data grid (columns are kept)
Definition n_gui.c:3920
void n_gui_slider_set_range(N_GUI_CTX *ctx, int widget_id, double min_val, double max_val)
set slider min/max range, clamping the current value if needed
Definition n_gui.c:3191
const char * n_gui_listbox_get_item_text(N_GUI_CTX *ctx, int widget_id, int index)
get the text of a listbox item
Definition n_gui.c:3448
void n_gui_radiolist_set_bitmaps(N_GUI_CTX *ctx, int widget_id, ALLEGRO_BITMAP *bg, ALLEGRO_BITMAP *item_bg, ALLEGRO_BITMAP *item_selected)
Set optional bitmap overlays on a radiolist widget.
Definition n_gui.c:4922
N_GUI_TREE * n_gui_tree_create(N_GUI_CTX *ctx, int window_id, float x, float y, float w, float h, void(*on_select)(int, void *), void *user_data)
create a tree view in an existing window
Definition n_gui.c:13341
#define N_GUI_WIN_MAXIMISED
window is maximised (full display size)
Definition n_gui.h:223
int n_gui_add_button_bitmap(N_GUI_CTX *ctx, int window_id, const char *label, float x, float y, float w, float h, ALLEGRO_BITMAP *normal, ALLEGRO_BITMAP *hover, ALLEGRO_BITMAP *active, void(*on_click)(int, void *), void *user_data)
Add a bitmap-based button.
Definition n_gui.c:2165
#define N_GUI_IMAGE_CENTER
draw at original size, centered
Definition n_gui.h:185
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
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_button_set_toggle_mode(N_GUI_CTX *ctx, int widget_id, int toggle_mode)
Enable or disable toggle mode on a button.
Definition n_gui.c:2392
int n_gui_datagrid_display_to_physical(N_GUI_CTX *ctx, int widget_id, int display_pos)
physical column shown at display position display_pos, or -1 if out of range
Definition n_gui.c:4150
#define N_GUI_ALIGN_CENTER
center aligned text
Definition n_gui.h:191
#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
float n_gui_datagrid_get_column_width(N_GUI_CTX *ctx, int widget_id, int col)
current pixel width of physical column col, or 0 if out of range
Definition n_gui.c:4144
void n_gui_tree_toggle_expand(N_GUI_TREE *tree, int node_index)
toggle expand/collapse of a node
Definition n_gui.c:13507
#define N_GUI_TYPE_SCROLLBAR
widget type: scrollbar
Definition n_gui.h:99
void n_gui_textarea_scroll_to_offset(N_GUI_CTX *ctx, int widget_id, size_t byte_offset)
scroll a multiline textarea so that the given byte offset is vertically centered
Definition n_gui.c:5275
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
#define N_GUI_TYPE_PROGRESSBAR
widget type: horizontal progress bar (display only)
Definition n_gui.h:122
const char * n_gui_datagrid_get_column_title(N_GUI_CTX *ctx, int widget_id, int col)
header title of physical column col, or "" if out of range
Definition n_gui.c:4120
#define N_GUI_TB_BTN_MAXIMIZE
maximize/restore titlebar button
Definition n_gui.h:274
void(* N_GUI_CUSTOM_DRAW)(N_GUI_WIDGET *widget, float x, float y, ALLEGRO_FONT *font, void *user_data)
Owner-draw callback for a custom widget (N_GUI_TYPE_CUSTOM).
Definition n_gui.h:934
#define N_GUI_TB_BTN_MINIMIZE
minimize titlebar button
Definition n_gui.h:272
#define N_GUI_SPLIT_VERTICAL
split pane: vertical divider, regions are left and right
Definition n_gui.h:128
void n_gui_textarea_set_bitmap(N_GUI_CTX *ctx, int widget_id, ALLEGRO_BITMAP *bg)
Set optional background bitmap on a textarea widget.
Definition n_gui.c:4863
void n_gui_listbox_clear(N_GUI_CTX *ctx, int widget_id)
remove all items from a listbox widget
Definition n_gui.c:3418
#define N_GUI_TYPE_BUTTON
widget type: button
Definition n_gui.h:91
#define N_GUI_TYPE_COMBOBOX
widget type: combo box (dropdown)
Definition n_gui.h:105
float n_gui_dropmenu_panel_width(N_GUI_CTX *ctx, int widget_id)
Width the open drop-down panel needs to show its entries.
Definition n_gui.c:4771
#define N_GUI_TYPE_CUSTOM
widget type: owner-draw custom widget (rendering delegated to a callback)
Definition n_gui.h:125
int n_gui_add_scrollbar(N_GUI_CTX *ctx, int window_id, float x, float y, float w, float h, int orientation, int shape, double content_size, double viewport_size, void(*on_scroll)(int, double, void *), void *user_data)
Add a scrollbar widget.
Definition n_gui.c:2577
N_GUI_TEXT_DIMS n_gui_get_text_dims(ALLEGRO_FONT *font, const char *text)
get the bounding box dimensions of text rendered with the given font.
Definition n_gui.c:103
#define N_GUI_WIN_BTN_MINIMIZE
enable minimize button on the title bar
Definition n_gui.h:235
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_window_is_minimised(N_GUI_CTX *ctx, int window_id)
Check if a window is currently minimised.
Definition n_gui.c:1255
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
#define N_GUI_ZORDER_FIXED
fixed z-value: window is sorted within a dedicated group between ALWAYS_BEHIND and NORMAL windows.
Definition n_gui.h:319
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:3249
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
int n_gui_get_shape_mode(N_GUI_CTX *ctx)
Get the global widget shape.
Definition n_gui.c:9064
#define N_GUI_WIN_HSCROLL_DRAG
window horizontal auto-scrollbar is being dragged
Definition n_gui.h:221
void n_gui_checkbox_set_bitmaps(N_GUI_CTX *ctx, int widget_id, ALLEGRO_BITMAP *box, ALLEGRO_BITMAP *box_checked, ALLEGRO_BITMAP *box_hover)
Set optional bitmap overlays on a checkbox widget.
Definition n_gui.c:4847
void n_gui_listbox_set_scroll_offset(N_GUI_CTX *ctx, int widget_id, int offset)
set the scroll offset (in items), clamps to valid range
Definition n_gui.c:3519
float n_gui_progressbar_get_value(N_GUI_CTX *ctx, int widget_id)
get the current progress fraction (0.0 on an invalid widget)
Definition n_gui.c:4290
int n_gui_sectionlist_get_count(const N_GUI_SECTIONLIST *sl)
number of sections in the list
Definition n_gui.c:14332
#define N_GUI_TEXT_MAX
maximum length for textarea content
Definition n_gui.h:87
#define N_GUI_STATE_IDLE
widget is idle / normal state
Definition n_gui.h:199
void n_gui_textarea_scroll_to_bottom(N_GUI_CTX *ctx, int widget_id)
scroll a multiline textarea to the bottom
Definition n_gui.c:5236
size_t n_gui_textarea_get_text_length(N_GUI_CTX *ctx, int widget_id)
return the current text length in bytes
Definition n_gui.c:5336
void n_gui_open_window(N_GUI_CTX *ctx, int window_id)
Open (show) a window.
Definition n_gui.c:1206
int n_gui_window_get_zvalue(N_GUI_CTX *ctx, int window_id)
Get window z-value (for N_GUI_ZORDER_FIXED mode).
Definition n_gui.c:1931
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
#define N_GUI_WIN_BTN_MAXIMIZE
enable maximize/restore button on the title bar
Definition n_gui.h:237
#define N_GUI_DROP_INTO
Definition n_gui.h:2465
int n_gui_sectionlist_add_section(N_GUI_SECTIONLIST *sl, const char *title, float header_y, int folded)
add a section with a header button at header_y (members added next are captured relative to it); retu...
Definition n_gui.c:14238
void n_gui_dropmenu_set_entry_text(N_GUI_CTX *ctx, int widget_id, int index, const char *text)
Update text of an existing entry.
Definition n_gui.c:4734
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_tree_add_node(N_GUI_TREE *tree, const char *label, int parent_index, void *user_data)
add a node to the tree
Definition n_gui.c:13380
#define N_GUI_WIN_BTN_CLOSE
enable close button on the title bar
Definition n_gui.h:239
int n_gui_listbox_get_count(N_GUI_CTX *ctx, int widget_id)
get the number of items in a listbox widget
Definition n_gui.c:3433
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
void n_gui_dropmenu_set_label(N_GUI_CTX *ctx, int widget_id, const char *label)
Replace the button label shown on a dropmenu.
Definition n_gui.c:4631
void n_gui_tree_draw_overlay(N_GUI_TREE *tree)
per-frame overlay drawing: draws the active drop-indicator on top of the listbox.
Definition n_gui.c:13730
const char * n_gui_combobox_get_item_text(N_GUI_CTX *ctx, int widget_id, int index)
get the text of the combobox item at index ("" if out of range / not a combobox)
Definition n_gui.c:4461
int n_gui_count_detached(N_GUI_CTX *ctx)
Count the windows currently detached into native OS windows.
Definition n_gui.c:1609
#define N_GUI_WIN_NO_HSCROLL
with N_GUI_WIN_AUTO_SCROLLBAR, never show the horizontal scrollbar (vertical-only scrolling): content...
Definition n_gui.h:244
#define N_GUI_ZORDER_POPUP
popup/modal dialogs: drawn above EVERYTHING including ALWAYS_ON_TOP windows.
Definition n_gui.h:326
void n_gui_dropmenu_set_flags(N_GUI_CTX *ctx, int widget_id, int flags)
Set dropmenu feature flags (bitmask of N_GUI_DROPMENU_* values)
Definition n_gui.c:4612
void n_gui_splitpane_set_ratio(N_GUI_CTX *ctx, int widget_id, float ratio)
set the divider ratio of a split pane (clamped to its min/max)
Definition n_gui.c:3550
int n_gui_checkbox_is_checked(N_GUI_CTX *ctx, int widget_id)
check if a checkbox widget is checked
Definition n_gui.c:3290
#define N_GUI_WIN_RESIZE_MOVE
reposition proportionally, keep pixel size
Definition n_gui.h:288
void n_gui_splitpane_set_limits(N_GUI_CTX *ctx, int widget_id, float min_ratio, float max_ratio)
set the minimum and maximum allowed divider ratio of a split pane
Definition n_gui.c:3566
#define N_GUI_STATE_ACTIVE
widget is being pressed / dragged
Definition n_gui.h:203
void n_gui_set_default_theme(N_GUI_CTX *ctx, N_GUI_THEME theme)
Set the default theme on a context and sync scrollbar style colors.
Definition n_gui.c:758
int n_gui_combobox_get_selected(N_GUI_CTX *ctx, int widget_id)
get the selected item index in a combobox widget
Definition n_gui.c:4453
void n_gui_tree_remove_node(N_GUI_TREE *tree, int node_index)
remove a node and its entire subtree; remaining node indices are compacted (callers holding indices a...
Definition n_gui.c:13408
void n_gui_image_set_bitmap(N_GUI_CTX *ctx, int widget_id, ALLEGRO_BITMAP *bitmap)
set the bitmap of an image widget
Definition n_gui.c:4496
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
int n_gui_window_is_maximised(N_GUI_CTX *ctx, int window_id)
Check if a window is currently maximised.
Definition n_gui.c:1344
int n_gui_datagrid_add_row(N_GUI_CTX *ctx, int widget_id, const char **values)
append a row of cell strings (values must have one entry per column); returns the row index or -1
Definition n_gui.c:3862
void n_gui_syntaxview_set_text(N_GUI_CTX *ctx, int widget_id, const char *text)
set (copy) the text shown by a syntax view
Definition n_gui.c:3624
void n_gui_slider_set_value(N_GUI_CTX *ctx, int widget_id, double value)
set the value of a slider widget
Definition n_gui.c:3173
#define N_GUI_AUTOFIT_EXPAND_UP
expand upward instead of downward when adjusting height
Definition n_gui.h:302
#define N_GUI_SHAPE_BITMAP
bitmap-based rendering
Definition n_gui.h:151
#define N_GUI_SHAPE_ROUNDED
rounded rectangle shape
Definition n_gui.h:149
#define N_GUI_ALIGN_RIGHT
right aligned text
Definition n_gui.h:193
int n_gui_sectionlist_is_folded(const N_GUI_SECTIONLIST *sl, int section_index)
query a section's folded state (1 = folded, 0 = expanded)
Definition n_gui.c:14317
void n_gui_datagrid_set_column_width(N_GUI_CTX *ctx, int widget_id, int col, float width)
set the pixel width of physical column col (clamped to a small minimum)
Definition n_gui.c:4138
void n_gui_syntaxview_scroll_to_offset(N_GUI_CTX *ctx, int widget_id, int byte_offset)
scroll a syntax view so the line holding a byte offset is vertically centered
Definition n_gui.c:3751
#define N_GUI_AUTOFIT_H
auto-adjust window height to content
Definition n_gui.h:296
void n_gui_sectionlist_add_widget(N_GUI_SECTIONLIST *sl, int section_index, int widget_id)
register an existing widget as a member of a section (captures its current y as the fold offset)
Definition n_gui.c:14255
void n_gui_datagrid_set_column_visible(N_GUI_CTX *ctx, int widget_id, int col, int visible)
show (1) or hide (0) physical column col; hidden columns keep their cells but are skipped in the head...
Definition n_gui.c:4126
void n_gui_sectionlist_set_first_inset(N_GUI_SECTIONLIST *sl, float inset)
set a left inset applied to the FIRST section's header only, so a caller can place a button before it...
Definition n_gui.c:14273
void n_gui_radiolist_set_selected(N_GUI_CTX *ctx, int widget_id, int index)
set the selected item in a radiolist widget
Definition n_gui.c:4400
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_KEY_MOD_MASK
mask of supported modifier flags for button keybind matching
Definition n_gui.h:372
#define N_GUI_ALIGN_JUSTIFIED
justified text (spread words to fill width)
Definition n_gui.h:195
void n_gui_set_widget_visible(N_GUI_CTX *ctx, int widget_id, int visible)
Show or hide a widget.
Definition n_gui.c:3075
void n_gui_syntaxview_select_all(N_GUI_CTX *ctx, int widget_id)
select the entire text of a syntax view
Definition n_gui.c:3739
N_GUI_TAB_PANEL * n_gui_tab_create(N_GUI_CTX *ctx, int window_id, float x, float y, float button_w, float button_h, void(*on_tab_change)(int, void *), void *user_data)
create a tab panel in an existing window
Definition n_gui.c:13262
void n_gui_mark_dirty(N_GUI_CTX *ctx)
Mark the GUI as needing a redraw.
Definition n_gui.c:974
#define N_GUI_ID_MAX
maximum length for widget id/name strings
Definition n_gui.h:85
N_GUI_STYLE n_gui_default_style(void)
Build sensible default style (all configurable sizes, colours, paddings)
Definition n_gui.c:788
#define N_GUI_TAB_MAX
maximum number of tabs in a single tab panel
Definition n_gui.h:2363
void n_gui_datagrid_clear_row_colors(N_GUI_CTX *ctx, int widget_id)
remove every row background tint (the rows and cells are kept)
Definition n_gui.c:4069
#define N_GUI_AUTOFIT_W
auto-adjust window width to content
Definition n_gui.h:294
void n_gui_window_set_maximize_callback(N_GUI_CTX *ctx, int window_id, void(*on_maximize)(int, void *), void *user_data)
Set the maximize button callback for a window.
Definition n_gui.c:1692
void n_gui_scrollbar_set_sizes(N_GUI_CTX *ctx, int widget_id, double content_size, double viewport_size)
set the content and viewport sizes of a scrollbar widget
Definition n_gui.c:3350
#define N_GUI_WIN_AUTO_SCROLLBAR
enable automatic scrollbars when content exceeds window size
Definition n_gui.h:227
#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
#define N_GUI_RESIZE_ADAPTIVE
virtual size tracks display; windows adapt per their resize_policy
Definition n_gui.h:282
void n_gui_textarea_set_placeholder(N_GUI_CTX *ctx, int widget_id, const char *text)
Set placeholder/hint text shown dimmed while a textarea is empty.
Definition n_gui.c:4889
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
void n_gui_sectionlist_free(N_GUI_SECTIONLIST **sl)
free the section list (does not destroy the N_GUI widgets or window)
Definition n_gui.c:14336
#define N_GUI_TYPE_LISTBOX
widget type: listbox (selectable list)
Definition n_gui.h:101
int n_gui_datagrid_get_selected(N_GUI_CTX *ctx, int widget_id)
get the selected row index, or -1 if none
Definition n_gui.c:3952
int n_gui_listbox_is_selected(N_GUI_CTX *ctx, int widget_id, int index)
check if a listbox item is selected
Definition n_gui.c:3483
void n_gui_window_update_normalized(N_GUI_CTX *ctx, int window_id)
Recapture normalized coordinates for a window from its current absolute position/size.
Definition n_gui.c:8962
#define N_GUI_SYNTAX_YAML
syntax view: YAML highlighting (keys, quoted values, list markers, comments)
Definition n_gui.h:141
int n_gui_add_checkbox(N_GUI_CTX *ctx, int window_id, const char *label, float x, float y, float w, float h, int initial_checked, void(*on_toggle)(int, int, void *), void *user_data)
Add a checkbox widget.
Definition n_gui.c:2543
void n_gui_combobox_clear(N_GUI_CTX *ctx, int widget_id)
remove all items from a combobox widget
Definition n_gui.c:4417
void n_gui_sectionlist_relayout(N_GUI_SECTIONLIST *sl)
re-stack all sections from y0 by current fold state: hide folded members, show and reposition expande...
Definition n_gui.c:14278
#define N_GUI_SYNTAX_JS
syntax view: JavaScript highlighting (keywords, strings, numbers, comments)
Definition n_gui.h:143
int n_gui_datagrid_get_column_visible(N_GUI_CTX *ctx, int widget_id, int col)
whether physical column col is visible (1) or hidden (0); 0 if out of range
Definition n_gui.c:4132
#define N_GUI_SHAPE_RECT
rectangle shape (default)
Definition n_gui.h:147
void n_gui_combobox_set_selected(N_GUI_CTX *ctx, int widget_id, int index)
set the selected item in a combobox widget
Definition n_gui.c:4478
int n_gui_listbox_remove_item(N_GUI_CTX *ctx, int widget_id, int index)
remove an item from a listbox widget
Definition n_gui.c:3394
#define N_GUI_SCROLLBAR_V
vertical scrollbar
Definition n_gui.h:169
N_GUI_THEME n_gui_make_theme(ALLEGRO_COLOR bg, ALLEGRO_COLOR bg_hover, ALLEGRO_COLOR bg_active, ALLEGRO_COLOR border, ALLEGRO_COLOR border_hover, ALLEGRO_COLOR border_active, ALLEGRO_COLOR text, ALLEGRO_COLOR text_hover, ALLEGRO_COLOR text_active, float border_thickness, float corner_rx, float corner_ry)
Create a custom theme with explicit colours.
Definition n_gui.c:685
int n_gui_add_dropmenu(N_GUI_CTX *ctx, int window_id, const char *label, float x, float y, float w, float h, void(*on_open)(int, void *), void *on_open_user_data)
Add a dropdown menu widget.
Definition n_gui.c:4568
int n_gui_add_datagrid(N_GUI_CTX *ctx, int window_id, float x, float y, float w, float h, void(*on_select)(int, int, void *), void *user_data)
Add a sortable data grid widget.
Definition n_gui.c:2787
void n_gui_screen_to_virtual(const N_GUI_CTX *ctx, float sx, float sy, float *vx, float *vy)
Convert physical screen coordinates to virtual canvas coordinates.
Definition n_gui.c:8876
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
int n_gui_add_custom(N_GUI_CTX *ctx, int window_id, float x, float y, float w, float h, N_GUI_CUSTOM_DRAW draw, void *user_data)
add an owner-draw custom widget; returns the widget id or -1
Definition n_gui.c:4315
#define N_GUI_KEY_SOURCES_MAX
maximum number of source widgets for a focused key binding
Definition n_gui.h:375
#define N_GUI_WIN_RESIZABLE
enable user-resizable window with a drag handle at bottom-right
Definition n_gui.h:229
#define N_GUI_STATE_FOCUSED
widget has keyboard focus
Definition n_gui.h:205
void n_gui_tree_tick(N_GUI_TREE *tree)
per-frame poll: reads ctx->mouse_* to track drag state and fires the on_drop callback on release.
Definition n_gui.c:13668
#define N_GUI_WIN_DRAGGING
window is being dragged
Definition n_gui.h:215
void n_gui_reset_all_widget_themes(N_GUI_CTX *ctx)
Reset all widget and window themes to ctx->default_theme.
Definition n_gui.c:3056
#define N_GUI_TYPE_TEXTAREA
widget type: text area
Definition n_gui.h:95
void n_gui_window_set_autofit(N_GUI_CTX *ctx, int window_id, int autofit_flags, float border)
Configure auto-fit behavior for a dialog window.
Definition n_gui.c:2016
void n_gui_scrollbar_set_pos(N_GUI_CTX *ctx, int widget_id, double pos)
set the scroll position of a scrollbar widget
Definition n_gui.c:3333
void n_gui_dropmenu_clear_dynamic(N_GUI_CTX *ctx, int widget_id)
Remove all dynamic entries (keep static ones)
Definition n_gui.c:4694
button specific data
Definition n_gui.h:380
checkbox specific data
Definition n_gui.h:501
combo box specific data
Definition n_gui.h:746
The top-level GUI context that holds all windows.
Definition n_gui.h:1354
Owner-draw custom widget data: a paint callback plus its user pointer.
Definition n_gui.h:937
one data grid column definition
Definition n_gui.h:639
data grid specific data: a sortable column table
Definition n_gui.h:649
dropdown menu specific data
Definition n_gui.h:843
A single entry in a dropdown menu (static or dynamic)
Definition n_gui.h:823
hex viewer specific data: a read-only byte dump
Definition n_gui.h:601
image widget specific data
Definition n_gui.h:783
a single row in the KV table
Definition n_gui.h:2552
key-value table built from textareas, checkboxes, and buttons
Definition n_gui.h:2562
static text label specific data
Definition n_gui.h:791
listbox specific data
Definition n_gui.h:553
a single item in a list/radio/combo widget
Definition n_gui.h:545
A saved window position retained for a window not yet created at load time (see N_GUI_CTX::pending_la...
Definition n_gui.h:1324
Saved user-adjustable state for a keyed widget that did not exist when the layout was loaded (a lazil...
Definition n_gui.h:1341
Progress-bar widget data (display only).
Definition n_gui.h:815
radio list specific data
Definition n_gui.h:711
scrollbar specific data
Definition n_gui.h:519
a single foldable section: a clickable header plus the member widgets it shows/hides
Definition n_gui.h:2401
a vertical accordion of foldable sections inside one window; folding hides a section's widgets and re...
Definition n_gui.h:2414
slider specific data
Definition n_gui.h:421
split pane specific data: a draggable divider between two regions
Definition n_gui.h:583
Global style holding every configurable layout constant.
Definition n_gui.h:1147
syntax view specific data: read-only highlighted text
Definition n_gui.h:613
tab panel built from toggle buttons with content window management
Definition n_gui.h:2366
Titlebar button state for a window (minimize, maximize, close)
Definition n_gui.h:947
bounding box dimensions returned by n_gui_get_text_dims()
Definition n_gui.h:331
text area specific data
Definition n_gui.h:461
Color theme for a widget.
Definition n_gui.h:341
tree view built from an N_GUI listbox
Definition n_gui.h:2477
a single node in the tree view
Definition n_gui.h:2454
A single GUI widget.
Definition n_gui.h:882
A pseudo window that contains widgets.
Definition n_gui.h:999
int ht_get_ptr(HASH_TABLE *table, const char *key, void **val)
get pointer at 'key' from 'table'
Definition n_hash.c:2110
int destroy_ht(HASH_TABLE **table)
empty a table and destroy it
Definition n_hash.c:2244
HASH_TABLE * new_ht(size_t size)
Create a hash table with the given size.
Definition n_hash.c:2011
int ht_put_ptr(HASH_TABLE *table, const char *key, void *ptr, void(*destructor)(void *ptr), void *(*duplicator)(void *ptr))
put an arbitrary pointer value with given key in the targeted hash table
Definition n_hash.c:2164
LIST_NODE * end
pointer to the end of the list
Definition n_list.h:68
void * ptr
void pointer to store
Definition n_list.h:46
LIST_NODE * start
pointer to the start of the list
Definition n_list.h:66
size_t nb_items
number of item currently in the list
Definition n_list.h:61
void(* destroy_func)(void *ptr)
pointer to destructor function if any, else NULL
Definition n_list.h:49
#define UNLIMITED_LIST_ITEMS
flag to pass to new_generic_list for an unlimited number of item in the list.
Definition n_list.h:73
int list_push(LIST *list, void *ptr, void(*destructor)(void *ptr))
Add a pointer to the end of the list.
Definition n_list.c:228
#define list_foreach(__ITEM_, __LIST_)
ForEach macro helper, safe for node removal during iteration.
Definition n_list.h:89
int list_unshift(LIST *list, void *ptr, void(*destructor)(void *ptr))
Add a pointer at the start of the list.
Definition n_list.c:317
int list_destroy(LIST **list)
Empty and Free a list container.
Definition n_list.c:548
void * remove_list_node_f(LIST *list, LIST_NODE *node)
Internal function called each time we need to get a node out of a list.
Definition n_list.c:76
LIST * new_generic_list(size_t max_items)
Initialiaze a generic list container to max_items pointers.
Definition n_list.c:37
Structure of a generic list node.
Definition n_list.h:44
#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
#define LOG_WARNING
warning conditions
Definition n_log.h:78
int n_clipboard_set(int which, const char *text)
Own which selection and serve text to any app that requests it.
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.
void n_clipboard_destroy(void)
Stop the backend thread and release the display connection.
System clipboard: full X11 selection support on Linux, Win32 clipboard on Windows/MinGW.
#define N_CLIPBOARD_PRIMARY
the X11 PRIMARY selection (mouse select-to-copy / middle-click paste); a no-op on Windows,...
Definition n_clipboard.h:59
#define N_CLIPBOARD_CLIPBOARD
the Ctrl+C / Ctrl+V clipboard selection
Definition n_clipboard.h:56
static void _draw_justified_selection(ALLEGRO_FONT *font, float x, float y, float max_w, float max_h, const char *text, int sel_start, int sel_end, ALLEGRO_COLOR sel_color)
helper: draw selection highlight rectangles over justified/wrapped text.
Definition n_gui.c:5465
static int _n_gui_tree_hit_row(N_GUI_TREE *tree, float mx, float my, int *out_node_idx, int *out_drop_pos)
Hit-test the mouse against the listbox's visible rows and fill the out params.
Definition n_gui.c:13632
static size_t _textarea_pos_from_mouse(const N_GUI_TEXTAREA_DATA *td, ALLEGRO_FONT *font, float mx, float my, float ax, float ay, float widget_w, float widget_h, float pad, float scrollbar_size)
helper: find the byte position in a textarea closest to the given mouse coordinates (mx,...
Definition n_gui.c:5877
static float _text_w(ALLEGRO_FONT *font, const char *text)
helper: get the advance width of text.
Definition n_gui.c:214
static void _draw_rows_scrollbar(float ax, float ay, float w, float h, int nb_rows, int visible, int scroll_offset, const N_GUI_STYLE *style, N_GUI_THEME *theme)
draw a row-indexed vertical scrollbar (shared by hexview, syntaxview and datagrid) at the right edge ...
Definition n_gui.c:6404
static void _n_gui_sectionlist_header_clicked(int widget_id, void *user_data)
Definition n_gui.c:14206
static void _register_widget(N_GUI_CTX *ctx, N_GUI_WIDGET *w)
helper: register a widget in the hash table
Definition n_gui.c:596
static float _json_get_float(cJSON *parent, const char *name, float fallback)
helper: read a float from JSON, return fallback if missing
Definition n_gui.c:12410
static N_GUI_WINDOW * _find_focused_window(N_GUI_CTX *ctx)
helper: find the window containing a focused widget
Definition n_gui.c:122
static ALLEGRO_COLOR _bg_for_state(const N_GUI_THEME *t, int state)
helper: pick colour based on widget state
Definition n_gui.c:5647
static void _n_gui_tree_listbox_selected(int widget_id, int index, int selected, void *user_data)
Definition n_gui.c:13330
#define _N_GUI_TREE_DRAG_THRESHOLD_PX
pixels of mouse movement before a press becomes a drag
Definition n_gui.c:13571
static void _draw_text_truncated(ALLEGRO_FONT *font, ALLEGRO_COLOR color, float x, float y, float max_w, const char *text)
helper: draw text truncated to a maximum pixel width.
Definition n_gui.c:5070
static int _native_monitor_info(ALLEGRO_DISPLAY *display, ALLEGRO_MONITOR_INFO *info)
helper: the monitor rectangle a native window sits on.
Definition n_gui.c:361
static void _syntax_run(ALLEGRO_FONT *font, ALLEGRO_COLOR col, float *cx, float y, const char *s, int len)
draw a run of text and advance the pen x by its width
Definition n_gui.c:6537
#define N_GUI_MEAS_KEY_MAX
Definition n_gui.c:167
static float _tb_buttons_area_width(const N_GUI_WINDOW *win, const N_GUI_STYLE *style)
compute the total width consumed by enabled titlebar buttons (0 if none/frameless)
Definition n_gui.c:8107
static void _draw_textarea(N_GUI_WIDGET *wgt, float ox, float oy, ALLEGRO_FONT *default_font, N_GUI_STYLE *style)
draw a textarea widget
Definition n_gui.c:6011
unsigned char have_dims
Definition n_gui.c:178
static ALLEGRO_DISPLAY * _event_display(const ALLEGRO_EVENT *event)
helper: the display an event originates from, or NULL when the event carries no display (timer,...
Definition n_gui.c:380
static void _normalize_crlf(char *s)
helper: strip CR from a string in-place, normalizing CRLF to LF.
Definition n_gui.c:137
static void _draw_progressbar(N_GUI_WIDGET *wgt, float ox, float oy, ALLEGRO_FONT *default_font, N_GUI_STYLE *style)
draw a sortable data grid: fixed header row plus scrollable data rows
Definition n_gui.c:7031
static int _is_focusable_type(int type)
helper: check if a widget type is focusable via Tab navigation
Definition n_gui.c:110
static void _draw_slider(N_GUI_WIDGET *wgt, float ox, float oy, ALLEGRO_FONT *default_font, N_GUI_STYLE *style)
draw a slider widget (horizontal or vertical)
Definition n_gui.c:5742
static void _draw_image(N_GUI_WIDGET *wgt, float ox, float oy)
draw an image widget
Definition n_gui.c:7783
static int _zorder_group(const N_GUI_WINDOW *w)
Definition n_gui.c:1745
static void _draw_themed_rect(N_GUI_THEME *t, int state, float x, float y, float w, float h, int rounded)
draw a themed rectangle or rounded rectangle.
Definition n_gui.c:5669
static void _draw_dropmenu(N_GUI_WIDGET *wgt, float ox, float oy, ALLEGRO_FONT *default_font, const N_GUI_STYLE *style)
draw a dropdown menu button (closed state)
Definition n_gui.c:7636
static float _label_content_height(const char *text, ALLEGRO_FONT *font, float max_w)
helper: compute the total content height of justified/wrapped label text, in pixels.
Definition n_gui.c:5566
static int _syntax_js_regex_possible(char c)
whether a '/' may start a regex literal after last significant char c (start of line,...
Definition n_gui.c:6561
static N_GUI_WINDOW * _find_widget_window(N_GUI_CTX *ctx, int wgt_id, float *ox, float *oy)
helper: find the parent window of a widget by id, returning the window's content origin (accounting f...
Definition n_gui.c:503
static void _tb_button_action(N_GUI_CTX *ctx, N_GUI_WINDOW *win, int btn_type)
Dispatch a titlebar button action, invoke the user callback if set, otherwise fall back to the built-...
Definition n_gui.c:9709
static ALLEGRO_BITMAP * _select_state_bitmap(int state, ALLEGRO_BITMAP *normal_bmp, ALLEGRO_BITMAP *hover_bmp, ALLEGRO_BITMAP *active_bmp)
helper: select per-state bitmap with fallback chain.
Definition n_gui.c:5003
static int _datagrid_resize_hover(N_GUI_CTX *ctx, float px, float py)
Whether the pointer at (px,py) rests on a datagrid column-resize border: the header row of a visible ...
Definition n_gui.c:9783
static void _native_drag_begin(N_GUI_WINDOW *win)
helper: anchor an own-chrome window's title bar drag on the desktop cursor.
Definition n_gui.c:292
static float _label_text_origin_x(const N_GUI_LABEL_DATA *lb, ALLEGRO_FONT *font, float ax, float wgt_w, float win_w, float wgt_x, float label_padding)
helper: compute the screen-space x origin where label text starts, accounting for alignment.
Definition n_gui.c:7848
static int _syntaxview_offset_from_mouse(const N_GUI_SYNTAXVIEW_DATA *yd, ALLEGRO_FONT *font, float mx, float my, float ax, float ay, float pad)
Map a mouse position over a syntax view to a byte offset into its text.
Definition n_gui.c:6848
static void _set_clipping_rect_transformed(int wx, int wy, int ww, int wh)
helper: set clipping rectangle in world space, transforming to screen space.
Definition n_gui.c:5015
static int _n_gui_tree_node_visible(N_GUI_TREE *tree, int node_index)
Definition n_gui.c:13371
static void _n_gui_kv_apply_layout(N_GUI_KVTABLE *table)
Definition n_gui.c:13820
static void _datagrid_ensure_sel(N_GUI_DATAGRID_DATA *gd)
helper: ensure the per-row selection array exists, sized to rows_cap
Definition n_gui.c:3844
static void _draw_dropmenu_panel(N_GUI_CTX *ctx)
draw the dropdown menu panel overlay (called after all windows)
Definition n_gui.c:7669
static int _win_on_pass(const N_GUI_CTX *ctx, const N_GUI_WINDOW *win)
helper: whether a window takes part in the pass currently being processed or drawn.
Definition n_gui.c:263
static void _draw_widget(N_GUI_WIDGET *wgt, float ox, float oy, ALLEGRO_FONT *default_font, float win_w, N_GUI_STYLE *style)
Definition n_gui.c:8023
static int _native_own_chrome(const N_GUI_WINDOW *win)
helper: whether a window is detached AND keeps drawing its own chrome (N_GUI_DETACH_OWN_CHROME).
Definition n_gui.c:271
static ALLEGRO_DISPLAY * _ctx_io_display(const N_GUI_CTX *ctx)
helper: the display to use for clipboard and mouse-cursor calls.
Definition n_gui.c:424
static void _window_update_content_size(N_GUI_WINDOW *win, ALLEGRO_FONT *default_font)
helper: recompute content extents for a window (for scrollbar support)
Definition n_gui.c:2089
char key[32]
Definition n_gui.c:174
static void _n_gui_window_capture_normalized(const N_GUI_CTX *ctx, N_GUI_WINDOW *win)
capture normalized coords for a window and its widgets from current absolutes
Definition n_gui.c:1021
static void _draw_textarea_placeholder(const N_GUI_TEXTAREA_DATA *td, const N_GUI_WIDGET *wgt, ALLEGRO_FONT *font, float tx, float ty)
draw a textarea's placeholder/hint at (tx,ty), dimmed, while the field is empty; a no-op once any tex...
Definition n_gui.c:5999
static int _shape_rounded(const N_GUI_STYLE *style, int widget_shape)
resolve whether a shape-aware widget draws rounded: the global shape_mode override (style->shape_mode...
Definition n_gui.c:5686
static float _min_thickness(float requested)
helper: compute minimum line thickness so it stays >= 1 physical pixel.
Definition n_gui.c:5633
static float _scrollbar_calc_scroll(float mouse, float track_start, float track_length, float viewport, float content, float thumb_min)
helper: compute scroll position from mouse coordinate using thumb-aware math.
Definition n_gui.c:461
static float _tb_btn_resolved_size(const N_GUI_WINDOW *win, const N_GUI_STYLE *style)
return the effective titlebar button size (0 = auto from titlebar_h)
Definition n_gui.c:8101
static int _syntax_js_regex_kw(const char *s, int n)
whether the n bytes at s are a keyword after which an expression (and therefore a regex literal) can ...
Definition n_gui.c:6549
static void _textarea_clear_selection(N_GUI_TEXTAREA_DATA *td)
clear the selection (set both anchors to cursor_pos)
Definition n_gui.c:9247
static void _release_native_window(N_GUI_CTX *ctx, N_GUI_WINDOW *win)
forward declaration: unregister and destroy a window's native display, leaving the window's geometry ...
Definition n_gui.c:1374
static int _text_line_count(const char *s)
helper: destroy a single widget (called by list destructor)
Definition n_gui.c:6525
static void _draw_scrollbar(N_GUI_WIDGET *wgt, float ox, float oy, const N_GUI_STYLE *style)
draw a scrollbar widget
Definition n_gui.c:6352
static void _textarea_sel_range(const N_GUI_TEXTAREA_DATA *td, size_t *lo, size_t *hi)
get the ordered selection range (lo, hi)
Definition n_gui.c:5987
unsigned char have_adv
Definition n_gui.c:177
static FILE * _gui_fopen_write(const char *path, const char *mode)
Definition n_gui.c:12373
static void _draw_widget_vscrollbar(float area_x, float area_y, float area_w, float view_h, float content_h, float scroll_y, N_GUI_STYLE *style)
helper: draw a mini vertical scrollbar inside a widget.
Definition n_gui.c:5606
static char * _ngui_clip_get(ALLEGRO_DISPLAY *display, int which)
Definition n_gui.c:53
static void _sort_windows_by_zorder(N_GUI_CTX *ctx)
Sort windows list by z-order: ALWAYS_BEHIND first, then FIXED (by z_value), then NORMAL,...
Definition n_gui.c:1766
static void _draw_custom(N_GUI_WIDGET *wgt, float ox, float oy, ALLEGRO_FONT *default_font)
draw a single widget.
Definition n_gui.c:8017
static ALLEGRO_COLOR _color_with_alpha(ALLEGRO_COLOR c, float a)
Helper: return a copy of c with the given alpha (0..1).
Definition n_gui.c:718
static int _justified_char_at_pos(const char *text, ALLEGRO_FONT *font, float max_w, float text_x, float text_y, float scroll_y, float click_mx, float click_my)
helper: find the byte offset in justified text closest to a click position.
Definition n_gui.c:5346
static float _textarea_content_height(const N_GUI_TEXTAREA_DATA *td, ALLEGRO_FONT *font, float widget_w, float pad)
helper: compute the total content height of multiline textarea text (with line-wrap),...
Definition n_gui.c:5203
static N_GUI_MEAS_ENTRY * _ngui_meas_slot(ALLEGRO_FONT *font, const char *text, size_t len)
find (or claim) the cache slot for (font, text[len]).
Definition n_gui.c:186
static int _tooltip_widget_at(N_GUI_CTX *ctx, float px, float py)
helper: top-most visible tooltip-bearing widget under a gui-space point (-1 = none)
Definition n_gui.c:2329
static int _textarea_handle_key(N_GUI_WIDGET *wgt, ALLEGRO_EVENT *ev, ALLEGRO_FONT *font, float pad, float sb_size, N_GUI_CTX *ctx)
handle textarea key input.
Definition n_gui.c:9379
static int _cell_is_num(const char *s)
true when every character of a non-empty string is a decimal digit
Definition n_gui.c:3784
static void _ngui_publish_primary_selection(N_GUI_CTX *ctx)
publish the active text selection (syntax view, label, or focused textarea) to the X11 PRIMARY select...
Definition n_gui.c:9323
static void _draw_combobox(N_GUI_WIDGET *wgt, float ox, float oy, ALLEGRO_FONT *default_font, const N_GUI_STYLE *style)
draw a combobox widget (closed state)
Definition n_gui.c:7429
static int _items_grow(N_GUI_LISTITEM **items, const size_t *nb, size_t *cap)
helper: ensure items array has room for one more, returns 1 on success
Definition n_gui.c:606
static void _draw_syntaxview(N_GUI_WIDGET *wgt, float ox, float oy, ALLEGRO_FONT *default_font, N_GUI_STYLE *style)
Definition n_gui.c:6933
static int _scrollbar_calc_scroll_int(float mouse, float track_start, float track_length, int visible_items, int total_items, float thumb_min)
helper: integer variant of _scrollbar_calc_scroll for item-based widgets.
Definition n_gui.c:484
static void _draw_hexview(N_GUI_WIDGET *wgt, float ox, float oy, ALLEGRO_FONT *default_font, N_GUI_STYLE *style)
draw a read-only hex viewer: offset, hex bytes, and an ASCII gutter
Definition n_gui.c:6471
static void _draw_tooltip(N_GUI_CTX *ctx)
helper: draw the hover-tooltip bubble for the currently armed tooltip widget, but ONLY in the render ...
Definition n_gui.c:8470
static void _datagrid_apply_columns(N_GUI_CTX *ctx, int widget_id, int ncols, const int *order, const int *visible, const float *width)
Definition n_gui.c:2251
static void _destroy_widget(void *ptr)
Definition n_gui.c:527
static size_t _datagrid_phys(const N_GUI_DATAGRID_DATA *gd, size_t dp)
physical column index shown at display position dp (identity when no order).
Definition n_gui.c:3839
static void _draw_datagrid(N_GUI_WIDGET *wgt, float ox, float oy, ALLEGRO_FONT *default_font, N_GUI_STYLE *style)
Definition n_gui.c:7059
static int _point_in_rect(float px, float py, float rx, float ry, float rw, float rh)
helper: test if point is inside a rectangle
Definition n_gui.c:429
static ALLEGRO_COLOR _text_for_state(const N_GUI_THEME *t, int state)
Definition n_gui.c:5659
static int _cell_cmp(const char *a, const char *b)
compare two cell strings: numerically when both are integers, else case-insensitive
Definition n_gui.c:3794
#define N_GUI_ANIM_TAIL_SEC
Definition n_gui.c:78
static double _clamp(double v, double lo, double hi)
helper: clamp a double between lo and hi
Definition n_gui.c:83
static int _n_gui_tree_listbox_bounds(N_GUI_TREE *tree, float *out_x, float *out_y, float *out_w, float *out_h, float *out_item_area_w, float *out_inset, float *out_item_h, int *out_scroll_off)
Return the listbox's absolute (screen-space) bounds and geometry via out params.
Definition n_gui.c:13592
static void _textarea_delete_selection(N_GUI_WIDGET *wgt)
delete the currently selected text, move cursor to selection start
Definition n_gui.c:9253
static int _json_get_int(cJSON *parent, const char *name, int fallback)
helper: read an int from JSON, return fallback if missing
Definition n_gui.c:12417
unsigned short len
Definition n_gui.c:173
static N_GUI_WIDGET * _new_widget(N_GUI_CTX *ctx, int type, float x, float y, float w, float h)
helper: allocate and initialise a base widget
Definition n_gui.c:624
static int _tb_button_rect(const N_GUI_WINDOW *win, const N_GUI_STYLE *style, int btn_type, float *out_x, float *out_y, float *out_w, float *out_h)
compute the absolute rect for a titlebar button.
Definition n_gui.c:8121
static void _draw_tb_buttons(N_GUI_WINDOW *win, const N_GUI_STYLE *style)
draw the titlebar buttons for a window
Definition n_gui.c:8155
static double _slider_snap_value(double val, double min_val, double max_val, double step)
helper: snap a slider value to the nearest valid step from min_val.
Definition n_gui.c:92
static void _draw_radiolist(N_GUI_WIDGET *wgt, float ox, float oy, ALLEGRO_FONT *default_font, const N_GUI_STYLE *style)
draw a radiolist widget
Definition n_gui.c:7334
static void _list_panel_highlight_step(int *highlight, int *scroll, int nb, int max_visible, int delta, int fallback)
helper: move an open list panel's highlight cursor (combobox / dropmenu) by delta items and scroll th...
Definition n_gui.c:9749
static void _draw_bitmap_scaled(ALLEGRO_BITMAP *bmp, float dx, float dy, float dw, float dh, int mode)
helper: draw a bitmap into a rectangle, respecting the given scale mode.
Definition n_gui.c:4982
static void _draw_checkbox(N_GUI_WIDGET *wgt, float ox, float oy, ALLEGRO_FONT *default_font, N_GUI_STYLE *style)
draw a checkbox widget
Definition n_gui.c:6304
static void _draw_combobox_dropdown(N_GUI_CTX *ctx)
Definition n_gui.c:7496
static void _n_gui_kv_add_clicked(int widget_id, void *user_data)
Definition n_gui.c:13795
static void _n_gui_tab_button_clicked(int widget_id, void *user_data)
Definition n_gui.c:13247
static N_GUI_DATAGRID_DATA * _datagrid_data(N_GUI_CTX *ctx, int widget_id)
fetch a data grid's column table for an API call, or NULL on a bad id.
Definition n_gui.c:4109
static int _textarea_has_selection(const N_GUI_TEXTAREA_DATA *td)
return 1 if the textarea has an active selection
Definition n_gui.c:5982
static bool _tooltip_line_cb(int line_num, const char *line, int size, void *extra)
helper: al_do_multiline_text callback that just counts the wrapped lines.
Definition n_gui.c:8448
static ALLEGRO_COLOR _border_for_state(const N_GUI_THEME *t, int state)
Definition n_gui.c:5653
static N_GUI_MEAS_ENTRY _ngui_meas_cache[1024u]
Definition n_gui.c:181
static float _syntax_prefix_w(ALLEGRO_FONT *font, const char *s, int n)
pixel width of the first n bytes of s in font (bounded copy).
Definition n_gui.c:6834
#define _N_GUI_WIN_STATE_PERSIST_MASK
Definition n_gui.c:12704
static void _ngui_clip_set(ALLEGRO_DISPLAY *display, int which, const char *text)
Definition n_gui.c:41
static LIST_NODE * _find_window_node(N_GUI_CTX *ctx, int window_id)
helper: find a window node in the context list by id
Definition n_gui.c:585
static void _draw_text_justified(ALLEGRO_FONT *font, ALLEGRO_COLOR color, float x, float y, float max_w, float max_h, const char *text)
helper: draw justified text within a given width, with multi-line word wrapping.
Definition n_gui.c:5114
static int _dropmenu_entries_grow(N_GUI_DROPMENU_ENTRY **entries, const size_t *nb, size_t *cap)
helper: ensure dropmenu entries array has room for one more
Definition n_gui.c:4547
static void _draw_listbox(N_GUI_WIDGET *wgt, float ox, float oy, ALLEGRO_FONT *default_font, N_GUI_STYLE *style)
draw a listbox widget
Definition n_gui.c:7226
static float _dropdown_panel_y(const N_GUI_CTX *ctx, float widget_top, float widget_bottom, float panel_h, int expand_up)
draw the combobox dropdown overlay (called after all windows)
Definition n_gui.c:7478
static void _draw_splitpane(N_GUI_WIDGET *wgt, float ox, float oy)
draw a split pane: only the divider bar is painted (the app fills the two regions with its own widget...
Definition n_gui.c:7205
static void _textarea_paste_clip(N_GUI_WIDGET *wgt, N_GUI_TEXTAREA_DATA *td, const char *clip)
insert clipboard text at the cursor, replacing any selection.
Definition n_gui.c:9285
static int _textarea_copy_to_clipboard(const N_GUI_TEXTAREA_DATA *td, ALLEGRO_DISPLAY *display)
copy selected text to clipboard.
Definition n_gui.c:9267
static void _slider_update_from_mouse(N_GUI_WIDGET *wgt, float mx, float my, float win_x, float win_content_y)
handle slider drag (horizontal or vertical)
Definition n_gui.c:9155
static void _draw_button(N_GUI_WIDGET *wgt, float ox, float oy, ALLEGRO_FONT *default_font, const N_GUI_STYLE *style)
draw a button widget
Definition n_gui.c:5693
static int _pointer_over_window(const N_GUI_CTX *ctx, float px, float py)
helper: is the pointer over any open window that takes part in the current pass? O(windows) and used ...
Definition n_gui.c:439
unsigned int hash
Definition n_gui.c:172
static int _datagrid_ensure_colors(N_GUI_DATAGRID_DATA *gd)
helper: ensure the per-row tint arrays exist, sized to rows_cap (rows start untinted); returns 1 when...
Definition n_gui.c:3852
static void _scrollbar_update_from_mouse(N_GUI_WIDGET *wgt, float mx, float my, float win_x, float win_content_y, const N_GUI_STYLE *style)
handle scrollbar drag
Definition n_gui.c:9187
static void _draw_window(N_GUI_WINDOW *win, ALLEGRO_FONT *default_font, N_GUI_STYLE *style)
draw a window chrome + its widgets
Definition n_gui.c:8234
static float _win_tbh(const N_GUI_WINDOW *win)
helper: effective title bar height (0 for frameless windows)
Definition n_gui.c:254
#define N_GUI_KEY_PRESS_FLASH_SEC
Definition n_gui.c:71
static void _compute_gui_bounds(N_GUI_CTX *ctx)
helper: compute the bounding box of all open windows
Definition n_gui.c:8430
static void _draw_label(N_GUI_WIDGET *wgt, float ox, float oy, ALLEGRO_FONT *default_font, float win_w, N_GUI_STYLE *style)
Definition n_gui.c:7870
static void _n_gui_widget_capture_normalized(const N_GUI_WINDOW *win, N_GUI_WIDGET *wgt)
capture normalized coords for a single widget relative to its parent window
Definition n_gui.c:1046
static void _destroy_window(void *ptr)
helper: destroy a single window (called by list destructor)
Definition n_gui.c:575
static ALLEGRO_COLOR _json_get_color(cJSON *parent, const char *name, ALLEGRO_COLOR fallback)
helper: read an RGBA colour from a JSON array [r,g,b,a]
Definition n_gui.c:12399
const ALLEGRO_FONT * font
Definition n_gui.c:170
static int _utf8_char_len(unsigned char c)
return the byte length of a UTF-8 character from its lead byte
Definition n_gui.c:5193
static void _syntax_draw_line(ALLEGRO_FONT *font, N_GUI_THEME *th, float x, float y, const char *s, int n, int mode, int line_idx, int in_headers)
draw one text line with mode-specific highlighting starting at (x,y)
Definition n_gui.c:6582
static void _json_add_color(cJSON *parent, const char *name, ALLEGRO_COLOR c)
helper: add an RGBA colour as a JSON array [r,g,b,a] (0-255)
Definition n_gui.c:12387
static void _native_apply_minimised(N_GUI_WINDOW *win)
helper: mirror an own-chrome window's minimised state onto its OS window by shrinking the display to ...
Definition n_gui.c:317
static void _syntaxview_ensure_lines(N_GUI_SYNTAXVIEW_DATA *yd)
forward declaration: (re)build a syntaxview's cached line count and headers-end index when stale.
Definition n_gui.c:6903
static int _label_char_at_x(const N_GUI_LABEL_DATA *lb, ALLEGRO_FONT *font, float click_x)
draw a label widget.
Definition n_gui.c:7819
static void _apply_pending_widget(N_GUI_CTX *ctx, const char *title, const N_GUI_WIDGET *wgt)
Definition n_gui.c:2271
static void _draw_cols_scrollbar(float ax, float bar_y, float track_w, float content_w, float h_scroll, const N_GUI_STYLE *style, N_GUI_THEME *theme)
draw a horizontal scrollbar along the bottom edge of a widget: track_w is the width of the scrollable...
Definition n_gui.c:6427
static void _text_dims(ALLEGRO_FONT *font, const char *text, int *x, int *y, int *w, int *h)
helper: memoized al_get_text_dimensions (bounding box x/y/w/h).
Definition n_gui.c:229
static void _n_gui_kv_remove_clicked(int widget_id, void *user_data)
Definition n_gui.c:13802
static int _syntax_js_keyword(const char *s, int n)
whether the n bytes at s form a JavaScript keyword or literal name
Definition n_gui.c:6567
#define N_GUI_MEAS_CACHE_SIZE
Definition n_gui.c:166
static void _n_gui_sectionlist_set_header_label(N_GUI_SECTIONLIST *sl, int idx)
Definition n_gui.c:14197
static void _datagrid_hmetrics(const N_GUI_WIDGET *wgt, const N_GUI_DATAGRID_DATA *gd, ALLEGRO_FONT *font, const N_GUI_STYLE *style, float *content_w, float *pane_w, int *need_hsb)
Compute the datagrid horizontal-scroll metrics used identically by the draw path and the mouse handle...
Definition n_gui.c:6449
static void _clear_syntaxview_selection(N_GUI_CTX *ctx)
drop any active syntax-view text selection (used when focus moves to another selectable widget,...
Definition n_gui.c:6885
static int _utf8_encode(int cp, char *out)
encode a Unicode code point into UTF-8, return the number of bytes written (0 on error)
Definition n_gui.c:9220
GUI system: buttons, sliders, text areas, checkboxes, scrollbars, dropdown menus, windows.